Files
wehub-resource-sync caf324b09d
tests / check_code_quality (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Blocked by required conditions
tests / tests (ubuntu-latest, 3.11) (push) Blocked by required conditions
Deploy "method_comparison" Gradio to Spaces / deploy (push) Waiting to run
Deploy "PEFT shop" Gradio app to Spaces / deploy (push) Waiting to run
tests on transformers main / tests (push) Waiting to run
tests / tests (ubuntu-latest, 3.12) (push) Blocked by required conditions
tests / tests (ubuntu-latest, 3.13) (push) Blocked by required conditions
tests / tests (windows-latest, 3.10) (push) Blocked by required conditions
tests / tests (windows-latest, 3.11) (push) Blocked by required conditions
tests / tests (windows-latest, 3.12) (push) Blocked by required conditions
tests / tests (windows-latest, 3.13) (push) Blocked by required conditions
Secret Leaks / trufflehog (push) Waiting to run
CI security linting / zizmor latest via Cargo (push) Waiting to run
Build documentation / build (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 13:24:42 +08:00

346 lines
12 KiB
Python

# Copyright 2025-present the HuggingFace Inc. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Test PEFT method x quantization method matrix, focusing on basic tests.
"""
from dataclasses import dataclass
import pytest
import torch
from accelerate.utils.memory import clear_device_cache
from transformers import AutoModelForCausalLM, BitsAndBytesConfig, TorchAoConfig
from peft import BOFTConfig, MissConfig, VeraConfig, get_peft_model
from peft.import_utils import (
is_bnb_4bit_available,
is_bnb_available,
is_gptqmodel_available,
is_torchao_available,
)
from peft.tuners.tuners_utils import BaseTunerLayer
from peft.utils import infer_device
from peft.utils.quantization_utils import (
Bnb4bitBackend,
Bnb8bitBackend,
ForwardOnlyQuantizationBackend,
TorchaoBackend,
)
from .testing_utils import hub_online_once, set_init_weights_false
SEED = 0
DEVICE = infer_device()
MIN_CORR = 0.9
MAX_MSE = 1.0
@dataclass
class Bnb8bitLoader:
name = "bnb_8bit"
backend_cls = Bnb8bitBackend
supports_merge = True
supports_non_quantized_comparison = True
model_id = "peft-internal-testing/opt-125m"
expected_layer_count = 24 # (q_proj, v_proj) x 12 layers
def load_model(self):
quant_config = BitsAndBytesConfig(load_in_8bit=True)
with hub_online_once(self.model_id):
return AutoModelForCausalLM.from_pretrained(
self.model_id, quantization_config=quant_config, device_map={"": DEVICE}
)
@dataclass
class Bnb4bitLoader:
name = "bnb_4bit"
backend_cls = Bnb4bitBackend
supports_merge = True
supports_non_quantized_comparison = True
model_id = "peft-internal-testing/opt-125m"
expected_layer_count = 24 # (q_proj, v_proj) x 12 layers
def load_model(self):
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=False,
bnb_4bit_compute_dtype=torch.float32,
)
with hub_online_once(self.model_id):
return AutoModelForCausalLM.from_pretrained(
self.model_id, quantization_config=quant_config, device_map={"": DEVICE}
)
@dataclass
class TorchAoInt8WeightOnlyLoader:
name = "torchao_int8_weight_only"
backend_cls = TorchaoBackend
supports_merge = True
supports_non_quantized_comparison = True
model_id = "peft-internal-testing/opt-125m"
expected_layer_count = 24 # (q_proj, v_proj) x 12 layers
def load_model(self):
from torchao.quantization import Int8WeightOnlyConfig
quant_config = TorchAoConfig(quant_type=Int8WeightOnlyConfig())
with hub_online_once(self.model_id):
return AutoModelForCausalLM.from_pretrained(
self.model_id, quantization_config=quant_config, device_map={"": DEVICE}
)
@dataclass
class TorchAoInt8DynamicActivationInt8WeightLoader:
name = "torchao_int8_dynamic_activation_int8"
backend_cls = TorchaoBackend
supports_merge = False
supports_non_quantized_comparison = True
model_id = "peft-internal-testing/opt-125m"
expected_layer_count = 24 # (q_proj, v_proj) x 12 layers
def load_model(self):
from torchao.quantization import Int8DynamicActivationInt8WeightConfig
quant_config = TorchAoConfig(quant_type=Int8DynamicActivationInt8WeightConfig())
with hub_online_once(self.model_id):
return AutoModelForCausalLM.from_pretrained(
self.model_id, quantization_config=quant_config, device_map={"": DEVICE}
)
@dataclass
class Gptq4bitLoader:
name = "gptq_4bit"
backend_cls = ForwardOnlyQuantizationBackend
supports_merge = False
# No on-the-fly quantization path; the comparison would need a separate fp model.
supports_non_quantized_comparison = False
model_id = "marcsun13/opt-350m-gptq-4bit"
expected_layer_count = 24 # (q_proj, v_proj) x 12 layers
def load_model(self):
from transformers import GPTQConfig
quant_config = GPTQConfig(bits=4)
with hub_online_once(self.model_id):
return AutoModelForCausalLM.from_pretrained(
self.model_id,
quantization_config=quant_config,
dtype=torch.float16,
device_map={"": DEVICE},
)
QUANTIZATION_BACKENDS = []
if is_bnb_available():
QUANTIZATION_BACKENDS.append(Bnb8bitLoader())
if is_bnb_4bit_available():
QUANTIZATION_BACKENDS.append(Bnb4bitLoader())
if is_torchao_available():
QUANTIZATION_BACKENDS.append(TorchAoInt8WeightOnlyLoader())
QUANTIZATION_BACKENDS.append(TorchAoInt8DynamicActivationInt8WeightLoader())
if is_gptqmodel_available():
QUANTIZATION_BACKENDS.append(Gptq4bitLoader())
def _quant_id(backend):
return backend.name
TEST_CASES = [
(
BOFTConfig,
{"boft_block_size": 4, "target_modules": ["q_proj", "v_proj"]},
),
(
MissConfig,
{"r": 2},
),
(
MissConfig,
{"r": 2, "init_weights": "bat"},
),
(
VeraConfig,
{"r": 8, "target_modules": ["q_proj", "v_proj"]},
),
]
def _peft_id(val):
"""Generate test id config_cls / config_kwargs."""
if isinstance(val, dict):
id_ = str(val).replace(" ", "")
else: # the PEFT config class
id_ = val.__name__.removesuffix("Config").lower()
return id_
def check_outputs_similar(x, y, min_corr=MIN_CORR, max_mse=MAX_MSE):
# As quantization introduces a lot of error, use generous tolerances
assert x.shape == y.shape
corr = torch.corrcoef(torch.stack((x.flatten(), y.flatten())))
mse = ((x - y) ** 2).mean()
corr_checks = corr[0, 1] >= min_corr
mse_checks = mse <= max_mse
if not corr_checks and not mse_checks:
assert False, f"both correlation ({corr[0, 1]:.4f}>={min_corr}) and MSE ({mse:.4f}<={max_mse}) check failed"
if not corr_checks:
assert False, f"correlation ({corr[0, 1]:.4f}>={min_corr}) check failed"
if not mse_checks:
assert False, f"MSE ({mse:.4f}<={max_mse}) check failed"
class TestQuantization:
"""Test for PEFT method x quantization method
Note: It is recommended to keep the number of tests low, as the number of combinations is already large as is. This
means testing multiple things per test, even if this is generally not desired. The reason is that we want to keep
the number of model initializations to a minimum, as those take time.
"""
@pytest.fixture(autouse=True)
def set_seed(self):
torch.manual_seed(SEED)
@pytest.fixture(autouse=True)
def cleanup(self):
yield
clear_device_cache(garbage_collection=True)
@pytest.fixture
def dummy_input(self):
return torch.arange(10).view(1, -1).to(DEVICE)
@pytest.mark.parametrize("quant", QUANTIZATION_BACKENDS, ids=_quant_id)
@pytest.mark.parametrize("config_cls,config_kwargs", TEST_CASES, ids=_peft_id)
def test_quantization_backend_is_set_and_repr(self, config_cls, config_kwargs, quant):
"""PEFT layers should have quantization_backend set"""
model = quant.load_model()
config = config_cls(**config_kwargs)
model = get_peft_model(model, config)
quantized_layers = [
m for m in model.modules() if isinstance(m, BaseTunerLayer) and m.quantization_backend is not None
]
assert len(quantized_layers) == quant.expected_layer_count
for layer in quantized_layers:
rep = repr(layer)
assert "quantization_backend=" in rep
@pytest.mark.parametrize("quant", QUANTIZATION_BACKENDS, ids=_quant_id)
@pytest.mark.parametrize("config_cls,config_kwargs", TEST_CASES, ids=_peft_id)
def test_forward_changes_output(self, config_cls, config_kwargs, quant, dummy_input):
"""Check that the forward pass works, also check if the results are affected"""
config_kwargs = set_init_weights_false(config_cls, config_kwargs)
model = quant.load_model()
with torch.inference_mode():
out_base = model(dummy_input).logits
config = config_cls(**config_kwargs)
model = get_peft_model(model, config)
with torch.inference_mode():
out_peft = model(dummy_input).logits
atol, rtol = 1e-3, 1e-3
assert not torch.allclose(out_base, out_peft, atol=atol, rtol=rtol)
@pytest.mark.parametrize("quant", QUANTIZATION_BACKENDS, ids=_quant_id)
@pytest.mark.parametrize("config_cls,config_kwargs", TEST_CASES, ids=_peft_id)
def test_quantized_output_similar_to_non_quantized(self, config_cls, config_kwargs, quant, dummy_input):
"""Quantized PEFT output should be similar to non-quantized PEFT output.
Both models use the same adapter config with non-identity init. The outputs won't match exactly due to
quantization noise, but should be in the same ballpark.
"""
if not quant.supports_non_quantized_comparison:
pytest.skip(f"{quant.name} is pre-quantized; no on-the-fly non-quantized counterpart for comparison")
config_kwargs = set_init_weights_false(config_cls, config_kwargs)
# Quantized model
model = quant.load_model()
config = config_cls(**config_kwargs)
torch.manual_seed(SEED)
model = get_peft_model(model, config).eval()
with torch.inference_mode():
out_quant = model(dummy_input).logits
del model
# Non-quantized model
with hub_online_once(quant.model_id):
model = AutoModelForCausalLM.from_pretrained(quant.model_id, device_map={"": DEVICE})
config = config_cls(**config_kwargs.copy())
torch.manual_seed(SEED)
model = get_peft_model(model, config).eval()
with torch.inference_mode():
out_non_quant = model(dummy_input).logits
check_outputs_similar(out_non_quant, out_quant)
@pytest.mark.parametrize("quant", QUANTIZATION_BACKENDS, ids=_quant_id)
@pytest.mark.parametrize("config_cls,config_kwargs", TEST_CASES, ids=_peft_id)
def test_merge_unmerge_unload(self, config_cls, config_kwargs, quant, dummy_input):
"""Check merge and unmerge roundtrip"""
if not quant.supports_merge:
pytest.skip(f"{quant.name} does not support merging")
if (DEVICE == "cpu") and isinstance(quant, Bnb4bitLoader):
pytest.skip("Bnb 4 bit quant with CPU results in high variance, skipping")
config_kwargs = set_init_weights_false(config_cls, config_kwargs)
model = quant.load_model()
config = config_cls(**config_kwargs)
torch.manual_seed(SEED)
model = get_peft_model(model, config).eval()
with torch.inference_mode():
out_before = model(dummy_input).logits
model.merge_adapter()
with torch.inference_mode():
out_merged = model(dummy_input).logits
check_outputs_similar(out_before, out_merged)
model.unmerge_adapter()
with torch.inference_mode():
out_unmerged = model(dummy_input).logits
check_outputs_similar(out_before, out_unmerged)
model.merge_adapter(safe_merge=True)
with torch.inference_mode():
out_merged_safe = model(dummy_input).logits
check_outputs_similar(out_before, out_merged_safe)
model.unmerge_adapter()
model = model.merge_and_unload()
with torch.inference_mode():
out_unloaded = model(dummy_input).logits
check_outputs_similar(out_before, out_unloaded)