Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/peft/peft_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1862,7 +1862,12 @@ def add_adapter(
else:
peft_config.modules_to_save.extend(classifier_module_names)

return super().add_adapter(adapter_name, peft_config, low_cpu_mem_usage=low_cpu_mem_usage)
return super().add_adapter(
adapter_name,
peft_config,
low_cpu_mem_usage=low_cpu_mem_usage,
autocast_adapter_dtype=autocast_adapter_dtype,
)

def forward(
self,
Expand Down Expand Up @@ -2713,7 +2718,12 @@ def add_adapter(
else:
peft_config.modules_to_save.extend(classifier_module_names)

return super().add_adapter(adapter_name, peft_config, low_cpu_mem_usage=low_cpu_mem_usage)
return super().add_adapter(
adapter_name,
peft_config,
low_cpu_mem_usage=low_cpu_mem_usage,
autocast_adapter_dtype=autocast_adapter_dtype,
)

def forward(
self,
Expand Down Expand Up @@ -2943,7 +2953,12 @@ def add_adapter(
else:
peft_config.modules_to_save.extend(qa_module_names)

return super().add_adapter(adapter_name, peft_config, low_cpu_mem_usage=low_cpu_mem_usage)
return super().add_adapter(
adapter_name,
peft_config,
low_cpu_mem_usage=low_cpu_mem_usage,
autocast_adapter_dtype=autocast_adapter_dtype,
)

def forward(
self,
Expand Down
7 changes: 7 additions & 0 deletions tests/test_decoder_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,13 @@ def test_adapter_name(self, model_id, config_cls, config_kwargs):
_skip_if_not_conv1d_supported(model_id, config_cls)
self._test_adapter_name(model_id, config_cls, config_kwargs.copy())

@pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_add_adapter_no_autocast_adapter_dtype(self, model_id, config_cls, config_kwargs, dtype):
_skip_if_not_conv1d_supported(model_id, config_cls)
self._test_add_adapter_no_autocast_adapter_dtype(model_id, config_cls, config_kwargs.copy(), dtype=dtype)

@pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS)
def test_prepare_for_training_parametrized(self, model_id, config_cls, config_kwargs):
Expand Down
6 changes: 6 additions & 0 deletions tests/test_encoder_decoder_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,12 @@ def test_attributes_parametrized(self, model_id, config_cls, config_kwargs):
def test_adapter_name(self, model_id, config_cls, config_kwargs):
self._test_adapter_name(model_id, config_cls, config_kwargs)

@pytest.mark.parametrize("model_id", PEFT_ENCODER_DECODER_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_add_adapter_no_autocast_adapter_dtype(self, model_id, config_cls, config_kwargs, dtype):
self._test_add_adapter_no_autocast_adapter_dtype(model_id, config_cls, config_kwargs, dtype=dtype)

@pytest.mark.parametrize("model_id", PEFT_ENCODER_DECODER_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS)
def test_prepare_for_training_parametrized(self, model_id, config_cls, config_kwargs):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_seq_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,13 @@ def test_adapter_name(self, model_id, config_cls, config_kwargs):
_skip_encoder_models(model_id, config_cls)
self._test_adapter_name(model_id, config_cls, config_kwargs.copy())

@pytest.mark.parametrize("model_id", PEFT_SEQ_CLS_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_add_adapter_no_autocast_adapter_dtype(self, model_id, config_cls, config_kwargs, dtype):
_skip_encoder_models(model_id, config_cls)
self._test_add_adapter_no_autocast_adapter_dtype(model_id, config_cls, config_kwargs.copy(), dtype=dtype)

@pytest.mark.parametrize("model_id", PEFT_SEQ_CLS_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS)
def test_prepare_for_training_parametrized(self, model_id, config_cls, config_kwargs):
Expand Down
83 changes: 83 additions & 0 deletions tests/test_token_classification_qa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2026-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 governing permissions and limitations under the License.

import pytest
import torch
from transformers import AutoModelForQuestionAnswering, AutoModelForTokenClassification

from peft import BOFTConfig, IA3Config, LoraConfig, VeraConfig

from .testing_common import PeftCommonTester


# Note: models from peft-internal-testing are just the safetensors versions of hf-internal-testing. The auto classes
# add the token classification / question answering head to the same backbone.
PEFT_TOKEN_CLS_MODELS_TO_TEST = [
"peft-internal-testing/tiny-random-BertForSequenceClassification",
"peft-internal-testing/tiny-random-RobertaForSequenceClassification",
]

PEFT_QA_MODELS_TO_TEST = PEFT_TOKEN_CLS_MODELS_TO_TEST


def _all_configs(task_type):
return [
(LoraConfig, {"task_type": task_type, "target_modules": None}),
(IA3Config, {"task_type": task_type, "target_modules": None, "feedforward_modules": None}),
(BOFTConfig, {"task_type": task_type, "target_modules": None}),
(VeraConfig, {"task_type": task_type, "target_modules": None, "r": 8}),
]


TOKEN_CLS_CONFIGS = _all_configs("TOKEN_CLS")
QA_CONFIGS = _all_configs("QUESTION_ANS")


class TestTokenClassificationModels(PeftCommonTester):
r"""
Tests for `PeftModelForTokenClassification`, which overrides `add_adapter` to add its head to `modules_to_save`.
Most of the functionality is already covered by the other model tests.
"""

transformers_class = AutoModelForTokenClassification

def prepare_inputs_for_testing(self):
input_ids = torch.tensor([[1, 1, 1], [1, 2, 1]]).to(self.torch_device)
attention_mask = torch.tensor([[1, 1, 1], [1, 0, 1]]).to(self.torch_device)
return {"input_ids": input_ids, "attention_mask": attention_mask}

@pytest.mark.parametrize("model_id", PEFT_TOKEN_CLS_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", TOKEN_CLS_CONFIGS)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_add_adapter_no_autocast_adapter_dtype(self, model_id, config_cls, config_kwargs, dtype):
self._test_add_adapter_no_autocast_adapter_dtype(model_id, config_cls, config_kwargs.copy(), dtype=dtype)


class TestQuestionAnsweringModels(PeftCommonTester):
r"""
Tests for `PeftModelForQuestionAnswering`, which overrides `add_adapter` to add its head to `modules_to_save`. Most
of the functionality is already covered by the other model tests.
"""

transformers_class = AutoModelForQuestionAnswering

def prepare_inputs_for_testing(self):
input_ids = torch.tensor([[1, 1, 1], [1, 2, 1]]).to(self.torch_device)
attention_mask = torch.tensor([[1, 1, 1], [1, 0, 1]]).to(self.torch_device)
return {"input_ids": input_ids, "attention_mask": attention_mask}

@pytest.mark.parametrize("model_id", PEFT_QA_MODELS_TO_TEST)
@pytest.mark.parametrize("config_cls,config_kwargs", QA_CONFIGS)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_add_adapter_no_autocast_adapter_dtype(self, model_id, config_cls, config_kwargs, dtype):
self._test_add_adapter_no_autocast_adapter_dtype(model_id, config_cls, config_kwargs.copy(), dtype=dtype)
59 changes: 59 additions & 0 deletions tests/testing_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,65 @@ def _test_load_multiple_adapters(self, model_id, config_cls, config_kwargs):
assert load_result1.missing_keys == []
assert load_result2.missing_keys == []

def _test_add_adapter_no_autocast_adapter_dtype(self, model_id, config_cls, config_kwargs, dtype):
# With autocast_adapter_dtype=False, adapters that are added after the PeftModel was created must keep the
# dtype of the base model instead of being upcast to float32. This covers add_adapter, which some task types
# override, as well as load_adapter, which routes through add_adapter.
if issubclass(config_cls, PromptLearningConfig):
pytest.skip("Prompt learning does not create tuner layers whose dtype could be autocast.")
if config_cls == AdaLoraConfig:
pytest.skip("AdaLoRA does not support multiple adapters")
if issubclass(config_cls, ShadowConfig):
# ShadowPEFT does not support multiple adapters for sequence classification. On top of that, its layer
# weights never follow the base model dtype: ShadowPEFT wraps whole decoder layers, so
# BaseTunerLayer.get_base_layer() returns a module without a `weight` attribute,
# _move_adapter_to_device_of_base_layer() cannot determine a dtype and returns early, and
# shadow_down/shadow_up/shadow_update_* keep the float32 that nn.Linear defaults to regardless of
# autocast_adapter_dtype. That is unrelated to the task-type add_adapter fix this test covers.
pytest.skip("ShadowPEFT layer weights do not follow the base model dtype")

def get_adapter_dtype(model, adapter_name):
dtypes = set()
for name, param in model.named_parameters():
if (model.prefix in name) and (adapter_name in name) and param.is_floating_point():
dtypes.add(param.dtype)
if not dtypes:
raise ValueError("Could not determine the dtype of this adapter")
return dtypes

with hub_online_once(model_id):
model = self.transformers_class.from_pretrained(model_id, dtype=dtype)

expected_dtype = {dtype}
if any(param.dtype == torch.float32 for param in model.parameters()):
# Some architectures pin individual modules to float32 through transformers'
# `_keep_in_fp32_modules`, even though the rest of the model is loaded in a lower precision. T5 does
# this for `wo` to avoid overflow:
# https://github.com/huggingface/transformers/blob/3283d5f78ed6836d39430c8190a6e0500be78698/src/transformers/models/t5/modeling_t5.py#L537
# An adapter on such a module correctly inherits the float32 dtype of its own base layer, so float32
# has to be allowed on top of `dtype` here. Of all models used by the tests, only T5 loaded in
# float16 hits this branch; every other model and dtype keeps the strict single-dtype expectation.
expected_dtype.add(torch.float32)

config = config_cls(
base_model_name_or_path=model_id,
**config_kwargs,
)
model = get_peft_model(model, config, autocast_adapter_dtype=False)
# Subset, not equality: get_adapter_dtype never returns an empty set, so when expected_dtype holds a
# single dtype this is the same check as equality.
assert get_adapter_dtype(model, "default") <= expected_dtype

model.add_adapter("added", config, autocast_adapter_dtype=False)
assert get_adapter_dtype(model, "added") <= expected_dtype

with tempfile.TemporaryDirectory() as tmp_dirname:
model.save_pretrained(tmp_dirname)

# load_adapter goes through the same add_adapter code path
model.load_adapter(tmp_dirname, adapter_name="loaded", autocast_adapter_dtype=False)
assert get_adapter_dtype(model, "loaded") <= expected_dtype

def _test_merge_layers_fp16(self, model_id, config_cls, config_kwargs):
_skip_if_merging_not_supported(model_id, config_cls, config_kwargs)
_skip_if_conv1d_not_supported(model_id, config_cls, config_kwargs)
Expand Down