# Copyright 2023-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. import json import platform import tempfile from unittest.mock import Mock, call, patch import pytest import torch from accelerate.test_utils.testing import get_backend from safetensors.torch import load_file as safe_load_file from transformers import ( AutoModelForCausalLM, AutoTokenizer, DataCollatorForLanguageModeling, Trainer, TrainingArguments, ) from transformers.modeling_outputs import CausalLMOutputWithPast from peft import ( AdaLoraConfig, BeftConfig, BOFTConfig, C3AConfig, CartridgeConfig, CPTConfig, DeftConfig, DeloraConfig, FourierFTConfig, FrodConfig, GloraConfig, GraloraConfig, HiraConfig, HRAConfig, IA3Config, LoraConfig, MissConfig, OFTConfig, OSFConfig, PrefixTuningConfig, PromptEmbedding, PromptEncoderConfig, PromptTuningConfig, PromptTuningInit, PsoftConfig, PveraConfig, RoadConfig, ShiraConfig, TaskType, TinyLoraConfig, UniLoraConfig, VBLoRAConfig, VeraConfig, WaveFTConfig, get_peft_model, ) from .testing_common import PeftCommonTester from .testing_utils import device_count, hub_online_once, load_dataset_english_quotes, set_init_weights_false # Note: some models from peft-internal-testing are just the safetensors versions of hf-internal-testing PEFT_DECODER_MODELS_TO_TEST = [ "peft-internal-testing/tiny-random-OPTForCausalLM", "peft-internal-testing/tiny-random-GPT2LMHeadModel", "peft-internal-testing/tiny-random-GPTJForCausalLM", "trl-internal-testing/tiny-random-LlamaForCausalLM", "peft-internal-testing/tiny-dummy-qwen2", "hf-internal-testing/tiny-random-Gemma3ForCausalLM", ] SMALL_GRID_MODELS = [ "hf-internal-testing/tiny-random-gpt2", "peft-internal-testing/tiny-random-OPTForCausalLM", "hf-internal-testing/tiny-random-MistralForCausalLM", "peft-internal-testing/tiny-dummy-qwen2", "trl-internal-testing/tiny-random-LlamaForCausalLM", ] # TODO Missing from this list are LoKr, LoHa, LN Tuning, add them # Note: If the PEFT method offers an initialization option to make it an identity transform (typically via the # init_weights argument), then this option should be set here, if it's not already the default. ALL_CONFIGS = [ ( AdaLoraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "total_step": 1, }, ), ( BeftConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( BOFTConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( MissConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "r": 2, }, ), ( CPTConfig, { "task_type": "CAUSAL_LM", "cpt_token_ids": [0, 1, 2, 3, 4, 5, 6, 7], # Example token IDs for testing "cpt_mask": [1, 1, 1, 1, 1, 1, 1, 1], "cpt_tokens_type_mask": [1, 2, 2, 2, 3, 3, 4, 4], }, ), ( DeftConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( DeloraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "r": 2, }, ), ( FourierFTConfig, { "task_type": "CAUSAL_LM", "n_frequency": 10, "target_modules": None, }, ), ( FrodConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "sparse_rate": 0.01, }, ), ( GraloraConfig, { "task_type": "CAUSAL_LM", "r": 8, "alpha": 16, "target_modules": None, "gralora_dropout": 0.05, "gralora_k": 2, "hybrid_r": 0, }, ), ( GraloraConfig, { "task_type": "CAUSAL_LM", "r": 16, "alpha": 32, "target_modules": None, "gralora_dropout": 0.05, "gralora_k": 4, "hybrid_r": 4, }, ), ( GloraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "init_weights": True, }, ), ( GloraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "init_weights": False, }, ), ( HiraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( HRAConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( IA3Config, { "task_type": "CAUSAL_LM", "target_modules": None, "feedforward_modules": None, }, ), ( LoraConfig, { "task_type": "CAUSAL_LM", "r": 8, "lora_alpha": 32, "target_modules": None, "lora_dropout": 0.05, "bias": "none", }, ), # Activated LoRA (aLoRA) ( LoraConfig, { "task_type": "CAUSAL_LM", "r": 8, "lora_alpha": 32, "target_modules": None, "lora_dropout": 0.05, "bias": "none", "alora_invocation_tokens": [1], }, ), ( LoraConfig, { "task_type": "CAUSAL_LM", "r": 8, "lora_alpha": 32, "target_modules": None, "lora_dropout": 0.05, "bias": "none", # not one test input sequence will ever have this token, this should do nothing at all "alora_invocation_tokens": [1000], }, ), # LoRA + trainable tokens ( LoraConfig, { "task_type": "CAUSAL_LM", "r": 8, "lora_alpha": 32, "target_modules": None, "lora_dropout": 0.05, "bias": "none", "trainable_token_indices": [0, 1, 3], }, ), ( OFTConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( PrefixTuningConfig, { "task_type": "CAUSAL_LM", "num_virtual_tokens": 10, }, ), ( PrefixTuningConfig, { "task_type": "CAUSAL_LM", "num_virtual_tokens": 10, "init_weights": "zero", }, ), ( PromptEncoderConfig, { "task_type": "CAUSAL_LM", "num_virtual_tokens": 10, "encoder_hidden_size": 32, }, ), ( PromptTuningConfig, { "task_type": "CAUSAL_LM", "num_virtual_tokens": 10, }, ), ( RoadConfig, { "task_type": "CAUSAL_LM", "variant": "road_1", "group_size": 2, }, ), ( ShiraConfig, { "r": 1, "task_type": "CAUSAL_LM", "target_modules": None, "init_weights": False, }, ), ( VBLoRAConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "vblora_dropout": 0.05, "vector_length": 1, "num_vectors": 2, }, ), ( VeraConfig, { "task_type": "CAUSAL_LM", "r": 8, "target_modules": None, "vera_dropout": 0.05, "projection_prng_key": 0xFF, "d_initial": 0.1, "save_projection": True, "bias": "none", }, ), ( UniLoraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, "theta_d_length": 257, }, ), ( TinyLoraConfig, { "task_type": "CAUSAL_LM", "target_modules": None, }, ), ( PveraConfig, { "r": 8, "pvera_dropout": 0.05, "task_type": "CAUSAL_LM", }, ), ( C3AConfig, { "task_type": "CAUSAL_LM", "block_size": 1, # Some test cases contain shapes of prime numbers where `block_size` must be 1 "target_modules": None, }, ), ( WaveFTConfig, { "task_type": "CAUSAL_LM", "n_frequency": 8, "target_modules": None, }, ), ( OSFConfig, { "task_type": "CAUSAL_LM", }, ), ( PsoftConfig, { "task_type": "CAUSAL_LM", "r": 4, "psoft_alpha": 4, }, ), ] def _skip_if_not_conv1d_supported(model_id, config_cls): if "GPT2LMHeadModel" in model_id and config_cls in [ BeftConfig, BOFTConfig, GloraConfig, HRAConfig, OFTConfig, OSFConfig, RoadConfig, ShiraConfig, C3AConfig, MissConfig, DeloraConfig, PsoftConfig, ]: pytest.skip("Skipping Beft/BOFT/GLoRA/HRA/OFT/Road/SHiRA/C3A/MiSS/OSF/DeLoRA/PSOFT for GPT2LMHeadModel") def _skip_alora_no_activation(config_cls, config_kwargs): if config_cls is LoraConfig and config_kwargs.get("alora_invocation_tokens") == [1000]: pytest.skip("Skipping aLoRA no-activation-case because the test expects changed output which there won't be.") def _skip_osf_disable_adapter_test(config_cls): if config_cls is OSFConfig: pytest.skip( "Skipping OSF for disable_adapter test because OSF uses exact SVD decomposition, so outputs are identical until training." ) def check_beft_config(config_cls, model_id, config_kwargs): if isinstance(config_cls, BeftConfig): return elif "gptj" in model_id.lower(): config_kwargs["target_modules"] = ["fc_out"] elif "llama" in model_id.lower(): pytest.skip("Skip tests for Llama models because layers have no bias term") elif "gemma3" in model_id.lower(): pytest.skip("Skip tests for Gemma3 models because layers have no bias term") else: return class TestDecoderModels(PeftCommonTester): transformers_class = AutoModelForCausalLM 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_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_attributes_parametrized(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_model_attr(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) 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) def test_prepare_for_training_parametrized(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_prepare_for_training(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) def test_prompt_tuning_text_prepare_for_training(self, model_id, config_cls, config_kwargs): if config_cls != PromptTuningConfig: pytest.skip(f"This test does not apply to {config_cls}") config_kwargs = config_kwargs.copy() config_kwargs["prompt_tuning_init"] = PromptTuningInit.TEXT config_kwargs["prompt_tuning_init_text"] = "This is a test prompt." config_kwargs["tokenizer_name_or_path"] = model_id self._test_prepare_for_training(model_id, config_cls, config_kwargs.copy()) def test_prompt_tuning_text_tokenizer_kwargs(self): # Allow users to pass additional arguments to Tokenizer.from_pretrained # Fix for #1032 mock = Mock() orig_from_pretrained = AutoTokenizer.from_pretrained def mock_autotokenizer_from_pretrained(*args, **kwargs): mock(*args, **kwargs) return orig_from_pretrained(config.tokenizer_name_or_path) model_id = "peft-internal-testing/tiny-random-OPTForCausalLM" config = PromptTuningConfig( base_model_name_or_path=model_id, tokenizer_name_or_path=model_id, num_virtual_tokens=10, prompt_tuning_init=PromptTuningInit.TEXT, task_type="CAUSAL_LM", prompt_tuning_init_text="This is a test prompt.", tokenizer_kwargs={"cache_dir": "/tmp/somewhere", "foo": "bar"}, ) model = self.transformers_class.from_pretrained(model_id).to(self.torch_device) with patch("transformers.AutoTokenizer.from_pretrained", mock_autotokenizer_from_pretrained): _ = get_peft_model(model, config) expected_call = call(model_id, cache_dir="/tmp/somewhere", foo="bar") assert mock.call_args == expected_call def test_prompt_tuning_trust_remote_code(self, tmp_path, monkeypatch): # See #2888 for details # This is a test for a hypothetical exploit that would enable trust_remote_code (and thus RCE) when a user loads # a malicious prompt tuning model. This is because PEFT would just pass the on the tokenizer_kwargs defined in # the prompt tuning config unsanitzed, which means that if the tokenizer is also malicious, the malicious code # would be executed. For this exploit to work, a user cannot load a model using PeftModel.from_pretrained as # normal, because the tokenizer is only loaded in training mode. Although the attacker could set # inference_mode=True in the adapter_config.json, that would still not work because prompt tuning methods cannot # be loaded in inference mode. Therefore, the only way for the exploit to work would be if the user manually # loads the model, as is shown below. model_id = "peft-internal-testing/tiny-random-OPTForCausalLM" with hub_online_once(model_id): # crafting the malicious checkpoint: model = AutoModelForCausalLM.from_pretrained(model_id) config = PromptTuningConfig( num_virtual_tokens=10, task_type=TaskType.CAUSAL_LM, tokenizer_name_or_path=model_id, prompt_tuning_init=PromptTuningInit.TEXT, prompt_tuning_init_text="hello", tokenizer_kwargs={"trust_remote_code": "foobar"}, ) model = get_peft_model(model, config) model.save_pretrained(tmp_path) with open(tmp_path / "adapter_config.json") as f: config_dict = json.load(f) # disable inference mode config_dict["inference_mode"] = False with open(tmp_path / "adapter_config.json", "w") as f: json.dump(config_dict, f) del model # applying a mock to check the used parameters used_args = [] used_kwargs = {} orig_from_pretrained = AutoTokenizer.from_pretrained def fake_from_pretrained(*args, **kwargs): used_args.extend(args) used_kwargs.update(kwargs) return orig_from_pretrained(*args, **kwargs) monkeypatch.setattr(AutoTokenizer, "from_pretrained", fake_from_pretrained) # user code: loading the malicious checkpoint model = AutoModelForCausalLM.from_pretrained(model_id) config = PromptTuningConfig.from_pretrained(tmp_path) PromptEmbedding(config, model.model.decoder.embed_tokens) # check that neither args nor kwargs used trust_remote_code='foobar' assert "foobar" not in used_args assert used_kwargs.get("trust_remote_code") != "foobar" @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_prompt_tuning_sample_vocab_prepare_for_training(self, model_id, config_cls, config_kwargs): if config_cls != PromptTuningConfig: pytest.skip(f"This test does not apply to {config_cls}") config_kwargs = config_kwargs.copy() config_kwargs["prompt_tuning_init"] = PromptTuningInit.SAMPLE_VOCAB config_kwargs["tokenizer_name_or_path"] = model_id self._test_prepare_for_training(model_id, config_cls, config_kwargs.copy()) def test_prompt_tuning_config_invalid_args(self): # Raise an error when tokenizer_kwargs is used with prompt_tuning_init!='TEXT', because this argument has no # function in that case model_id = "peft-internal-testing/tiny-random-OPTForCausalLM" with pytest.raises(ValueError, match="tokenizer_kwargs only valid when using prompt_tuning_init='TEXT'."): PromptTuningConfig( base_model_name_or_path=model_id, tokenizer_name_or_path=model_id, num_virtual_tokens=10, task_type="CAUSAL_LM", prompt_tuning_init_text="This is a test prompt.", prompt_tuning_init=PromptTuningInit.RANDOM, # <= should not be used together with tokenizer_kwargs tokenizer_kwargs={"trust_remote_code": True, "foo": "bar"}, ) @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_save_pretrained(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_save_pretrained(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) def test_save_pretrained_pickle(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_save_pretrained(model_id, config_cls, config_kwargs.copy(), safe_serialization=False) @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_save_pretrained_selected_adapters(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_save_pretrained_selected_adapters(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) def test_save_pretrained_selected_adapters_pickle(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_save_pretrained_selected_adapters( model_id, config_cls, config_kwargs.copy(), safe_serialization=False ) @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_from_pretrained_config_construction(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_from_pretrained_config_construction(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) def test_merge_layers(self, model_id, config_cls, config_kwargs): config_kwargs = set_init_weights_false(config_cls, config_kwargs) check_beft_config(config_cls, model_id, config_kwargs) self._test_merge_layers(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) def test_merge_layers_multi(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) config_kwargs = set_init_weights_false(config_cls, config_kwargs) check_beft_config(config_cls, model_id, config_kwargs) self._test_merge_layers_multi(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) def test_merge_layers_nan(self, model_id, config_cls, config_kwargs): config_kwargs = set_init_weights_false(config_cls, config_kwargs) check_beft_config(config_cls, model_id, config_kwargs) self._test_merge_layers_nan(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) def test_mixed_adapter_batches(self, model_id, config_cls, config_kwargs): if config_cls != LoraConfig: pytest.skip("Mixed adapter batches not supported for this config.") _skip_alora_no_activation(config_cls, config_kwargs) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_mixed_adapter_batches(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) def test_generate_with_mixed_adapter_batches(self, model_id, config_cls, config_kwargs): if config_cls != LoraConfig: pytest.skip("Mixed adapter batches not supported for this config.") config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_generate_with_mixed_adapter_batches_and_beam_search(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) def test_generate(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_generate(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) def test_generate_pos_args(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_generate_pos_args(model_id, config_cls, config_kwargs.copy(), raises_err=False) @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_merge_layers_fp16(self, model_id, config_cls, config_kwargs): config_kwargs = config_kwargs.copy() check_beft_config(config_cls, model_id, config_kwargs) self._test_merge_layers_fp16(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) def test_generate_half_prec(self, model_id, config_cls, config_kwargs): self._test_generate_half_prec(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) def test_training_decoders(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_training(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) def test_training_decoders_layer_indexing(self, model_id, config_cls, config_kwargs): self._test_training_layer_indexing(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("use_reentrant", [True, False]) def test_training_decoders_gradient_checkpointing(self, model_id, config_cls, config_kwargs, use_reentrant): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_training_gradient_checkpointing( model_id, config_cls, config_kwargs.copy(), use_reentrant=use_reentrant ) @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_inference_safetensors(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_inference_safetensors(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) def test_peft_model_device_map(self, model_id, config_cls, config_kwargs): self._test_peft_model_device_map(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) def test_delete_adapter(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_delete_adapter(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) def test_delete_inactive_adapter(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_delete_inactive_adapter(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) def test_adding_multiple_adapters_with_bias_raises(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) self._test_adding_multiple_adapters_with_bias_raises(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) def test_unload_adapter(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) _skip_alora_no_activation(config_cls, config_kwargs) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_unload_adapter(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) def test_weighted_combination_of_adapters(self, model_id, config_cls, config_kwargs): config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_weighted_combination_of_adapters(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) def test_training_prompt_learning_tasks(self, model_id, config_cls, config_kwargs): self._test_training_prompt_learning_tasks(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) def test_disable_adapter(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) _skip_alora_no_activation(config_cls, config_kwargs) _skip_osf_disable_adapter_test(config_cls) config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_disable_adapter(model_id, config_cls, config_kwargs.copy()) def test_generate_adalora_no_dropout(self): # test for issue #730 model_id = "peft-internal-testing/tiny-random-OPTForCausalLM" config_kwargs = { "target_modules": None, "task_type": "CAUSAL_LM", "lora_dropout": 0.0, "total_step": 1, } self._test_generate(model_id, AdaLoraConfig, config_kwargs.copy()) @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_passing_input_embeds_works(self, model_id, config_cls, config_kwargs): _skip_if_not_conv1d_supported(model_id, config_cls) if (platform.system() == "Darwin") and (config_cls == PrefixTuningConfig): # the error is: # > RuntimeError: unsupported operation: more than one element of the written-to tensor refers to a single # > memory location. Please clone() the tensor before performing the operation. # in transformers sdpa_mask_older_torch. As we (currently) cannot upgrade PyTorch on MacOS GH runners, we're # stuck with this error. # TODO: remove if torch can be upgraded on MacOS or if MacOS CI is removed pytest.skip("Prefix tuning fails on MacOS in this case, not worth fixing") self._test_passing_input_embeds_works("", model_id, config_cls, config_kwargs.copy()) def test_lora_layer_replication(self): model_id = "trl-internal-testing/tiny-random-LlamaForCausalLM" config_kwargs = { "target_modules": ["down_proj", "up_proj"], "task_type": "CAUSAL_LM", "lora_dropout": 0.0, "layer_replication": [[0, 1], [0, 2], [1, 2]], } model = self.transformers_class.from_pretrained(model_id).to(self.torch_device) config = LoraConfig(base_model_name_or_path=model_id, **config_kwargs) assert len(model.model.layers) == 2, "Expected 2 layers in original model." model = get_peft_model(model, config) layers = model.base_model.model.model.layers assert len(layers) == 4, "Expected 4 layers in adapted model." assert ( layers[0].mlp.up_proj.base_layer.weight.data.storage().data_ptr() == layers[1].mlp.up_proj.base_layer.weight.data.storage().data_ptr() and layers[2].mlp.up_proj.base_layer.weight.data.storage().data_ptr() == layers[3].mlp.up_proj.base_layer.weight.data.storage().data_ptr() ), "Expected layers 0-1 and 2-3 to share weights" assert ( layers[0].mlp.up_proj.base_layer.weight.data.storage().data_ptr() != layers[2].mlp.up_proj.base_layer.weight.data.storage().data_ptr() ), "Expected layers 0 and 2 to have different weights" assert ( layers[0].mlp.up_proj.lora_A.default.weight.data.storage().data_ptr() != layers[1].mlp.up_proj.lora_A.default.weight.data.storage().data_ptr() and layers[2].mlp.up_proj.lora_A.default.weight.data.storage().data_ptr() != layers[3].mlp.up_proj.lora_A.default.weight.data.storage().data_ptr() ), "Expected all LoRA adapters to have distinct weights" assert len([n for n, _ in model.named_parameters() if ".lora_A." in n]) == 8, ( "Expected 8 LoRA adapters since we are adding one each for up and down." ) self._test_prepare_for_training(model_id, LoraConfig, config_kwargs.copy()) self._test_generate(model_id, LoraConfig, config_kwargs.copy()) def test_prefix_tuning_qwen2_with_grouped_query_attention(self): # See 1901, fixes a bug with handling GQA model_id = "peft-internal-testing/tiny-dummy-qwen2" with hub_online_once(model_id): base_model = AutoModelForCausalLM.from_pretrained(model_id) peft_config = PrefixTuningConfig(num_virtual_tokens=10, task_type="CAUSAL_LM") model = get_peft_model(base_model, peft_config) x = torch.tensor([[1, 2, 3]]) # does not raise model(x) def test_prefix_tuning_qwen3_with_grouped_query_attention(self): # See 2881, fixes a bug with handling GQA model_id = "trl-internal-testing/tiny-Qwen3ForCausalLM" with hub_online_once(model_id): base_model = AutoModelForCausalLM.from_pretrained(model_id) peft_config = PrefixTuningConfig(num_virtual_tokens=10, task_type="CAUSAL_LM") model = get_peft_model(base_model, peft_config) x = torch.tensor([[1, 2, 3]]) # does not raise model(x) def test_prefix_tuning_offsets_position_ids_in_forward(self, monkeypatch): # Regression: RoPE models need position_ids offset for prefix tuning. model_id = "trl-internal-testing/tiny-random-LlamaForCausalLM" with hub_online_once(model_id): base = AutoModelForCausalLM.from_pretrained(model_id) peft_config = PrefixTuningConfig(num_virtual_tokens=4, task_type="CAUSAL_LM", prefix_projection=False) model = get_peft_model(base, peft_config) captured = {} def fake_forward(*args, **kwargs): captured["position_ids"] = kwargs.get("position_ids") input_ids = kwargs.get("input_ids") if input_ids is None and args: input_ids = args[0] batch, seq_len = input_ids.shape logits = torch.zeros((batch, seq_len, base.config.vocab_size), device=input_ids.device) return CausalLMOutputWithPast(logits=logits) monkeypatch.setattr(model.base_model, "forward", fake_forward) input_ids = torch.randint(0, base.config.vocab_size, (1, 3)) position_ids = torch.arange(input_ids.shape[1]).unsqueeze(0) _ = model(input_ids=input_ids, position_ids=position_ids) assert captured["position_ids"] is not None assert torch.equal(captured["position_ids"], position_ids + peft_config.num_virtual_tokens) def test_prefix_tuning_mistral(self): # See issue 869, 1962 _, device_count, _ = get_backend() if device_count > 1: pytest.skip("PEFT Mistral training with DP does not work, skipping") model_id = "hf-internal-testing/tiny-random-MistralForCausalLM" base_model = AutoModelForCausalLM.from_pretrained(model_id) peft_config = PrefixTuningConfig(num_virtual_tokens=10, task_type="CAUSAL_LM") model = get_peft_model(base_model, peft_config) tokenizer = AutoTokenizer.from_pretrained(model_id) tokenizer.pad_token = tokenizer.eos_token def process(samples): tokenized = tokenizer(samples["quote"], truncation=True, max_length=128) return tokenized data = load_dataset_english_quotes() data = data.map(process, batched=True) with tempfile.TemporaryDirectory() as tmp_dirname: trainer = Trainer( model=model, train_dataset=data["train"], args=TrainingArguments( num_train_epochs=1, max_steps=5, per_device_train_batch_size=4, output_dir=tmp_dirname, ), data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False), ) trainer.train() @pytest.mark.parametrize("model_id", SMALL_GRID_MODELS) @pytest.mark.parametrize( "config_cls,config_kwargs", [ ( PromptTuningConfig, { "num_virtual_tokens": 10, "task_type": "CAUSAL_LM", }, ), ( PrefixTuningConfig, { "num_virtual_tokens": 10, "task_type": "CAUSAL_LM", }, ), ( CartridgeConfig, { "num_virtual_tokens": 10, "num_frozen_tokens": 1, "task_type": "CAUSAL_LM", }, ), ( PromptEncoderConfig, { "num_virtual_tokens": 10, "encoder_hidden_size": 32, "task_type": "CAUSAL_LM", }, ), ( CPTConfig, { "task_type": "CAUSAL_LM", "cpt_token_ids": [0, 1, 2, 3, 4, 5, 6, 7], # Example token IDs for testing "cpt_mask": [1, 1, 1, 1, 1, 1, 1, 1], "cpt_tokens_type_mask": [1, 2, 2, 2, 3, 3, 4, 4], }, ), ], ) def test_prompt_learning_with_gradient_checkpointing(self, model_id, config_cls, config_kwargs): # See issue 869 # Test prompt learning methods with gradient checkpointing in a semi realistic setting. # Prefix tuning does not work if the model uses the new caching implementation. In that case, a helpful error # should be raised. # skip if multi GPU, since this results in DataParallel usage by Trainer, which fails with "CUDA device # assertion", breaking subsequent tests if device_count > 1: pytest.skip("Skip on multi-GPU setups") peft_config = config_cls(base_model_name_or_path=model_id, **config_kwargs) base_model = self.transformers_class.from_pretrained(model_id) base_model.gradient_checkpointing_enable() try: model = get_peft_model(base_model, peft_config) except ValueError as exc: # Some methods will raise a helpful error. After this, exit the test, as training would fail. assert config_cls in (PrefixTuningConfig, CartridgeConfig) assert "does not work with gradient checkpointing" in str(exc) return tokenizer = AutoTokenizer.from_pretrained(model_id) tokenizer.pad_token = tokenizer.eos_token def process(samples): tokenized = tokenizer(samples["quote"], truncation=True, max_length=128) return tokenized data = load_dataset_english_quotes() data = data.map(process, batched=True) with tempfile.TemporaryDirectory() as tmp_dirname: trainer = Trainer( model=model, train_dataset=data["train"], args=TrainingArguments( num_train_epochs=1, max_steps=3, per_device_train_batch_size=4, output_dir=tmp_dirname, ), data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False), ) trainer.train() @pytest.mark.parametrize("save_embedding_layers", ["auto", True, False]) @pytest.mark.parametrize( "peft_config", [ (LoraConfig(target_modules=["lin0", "embed_tokens"], init_lora_weights=False)), (LoraConfig(target_modules=r".*\.embed_tokens", init_lora_weights=False)), ], ) def test_save_pretrained_targeting_lora_to_embedding_layer(self, save_embedding_layers, tmp_path, peft_config): model_id = "trl-internal-testing/tiny-random-LlamaForCausalLM" with hub_online_once(model_id): model = AutoModelForCausalLM.from_pretrained(model_id) model = get_peft_model(model, peft_config) if save_embedding_layers == "auto": # assert warning msg_start = "Setting `save_embedding_layers` to `True` as embedding layers found in `target_modules`." with pytest.warns(UserWarning, match=msg_start): model.save_pretrained(tmp_path, save_embedding_layers=save_embedding_layers) else: model.save_pretrained(tmp_path, save_embedding_layers=save_embedding_layers) state_dict = safe_load_file(tmp_path / "adapter_model.safetensors") contains_embedding = "base_model.model.model.embed_tokens.base_layer.weight" in state_dict if save_embedding_layers in ["auto", True]: assert contains_embedding assert torch.allclose( model.base_model.model.model.embed_tokens.base_layer.weight, state_dict["base_model.model.model.embed_tokens.base_layer.weight"], ) else: assert not contains_embedding @pytest.mark.parametrize("use_dora", [False, True]) def test_lora_embed_scale_is_applied(self, use_dora): """Test that LoRA correctly handles embeddings with scaling (e.g., Gemma3).""" model_id = "hf-internal-testing/tiny-random-Gemma3ForCausalLM" with hub_online_once(model_id): base_model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device) orig_embedding = base_model.get_input_embeddings() peft_config = LoraConfig(target_modules=["embed_tokens"], init_lora_weights=False, use_dora=use_dora) peft_model = get_peft_model(base_model, peft_config) x = torch.arange(10).to(self.torch_device) peft_embedding = peft_model.base_model.model.get_input_embeddings() embedding_output = peft_embedding(x) max_embedding_output = embedding_output.abs().max(0)[0] assert (max_embedding_output < 100.0).all() peft_model.merge_adapter() embedding_merged = peft_embedding(x) assert torch.allclose(embedding_output, embedding_merged, atol=1e-5, rtol=1e-5) peft_model.unmerge_adapter() # set embed_scale to an absurdly high value, then check that the embedding output is also scaled to a high # value orig_embedding.embed_scale.fill_(10000.0) max_embedding_output = peft_embedding(x).abs().max(0)[0] assert (max_embedding_output > 100.0).all() # set embed_scale to zero, then check that the embedding output is also zero orig_embedding.embed_scale.fill_(0) embedding_output = peft_embedding(x) assert (embedding_output == 0.0).all() def test_lora_embed_scale_is_applied_mixed_batch(self): """Test that LoRA correctly handles embeddings with scaling in mixed batch mode.""" model_id = "hf-internal-testing/tiny-random-Gemma3ForCausalLM" with hub_online_once(model_id): base_model = AutoModelForCausalLM.from_pretrained(model_id) orig_embedding = base_model.get_input_embeddings() peft_config = LoraConfig(target_modules=["embed_tokens"], init_lora_weights=False) peft_model = get_peft_model(base_model, peft_config) peft_model.add_adapter("adapter2", peft_config) # sanity check: with the default embed_scale, the embedding output should be reasonably sized peft_embedding = peft_model.base_model.model.get_input_embeddings() input_ids = torch.arange(10).unsqueeze(0).repeat(2, 1) adapter_names = ["default", "adapter2"] max_embedding_output = peft_embedding(input_ids, adapter_names=adapter_names).abs().max() assert max_embedding_output < 100.0 # set embed_scale to an absurdly high value, then check that the embedding output is also scaled to a high # value orig_embedding.embed_scale.fill_(10000.0) max_embedding_output = peft_embedding(input_ids, adapter_names=adapter_names).abs().max() assert max_embedding_output > 100.0 # set embed_scale to zero, then check that the embedding output is also zero orig_embedding.embed_scale.fill_(0) embedding_output = peft_embedding(input_ids, adapter_names=adapter_names) assert (embedding_output == 0.0).all() @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_set_requires_grad_prompt_learning_raises(self, config_cls, config_kwargs): # Test that for prompt learning, calling set_requires_grad raises an error with an appropriate error message. # Note that for non-prompt learning methods, set_requires_grad is being tested for custom models, so there is no # specific test here. model_id = PEFT_DECODER_MODELS_TO_TEST[0] # it's enough to test this with one model config = config_cls( base_model_name_or_path=model_id, **config_kwargs, ) if not config.is_prompt_learning: pytest.skip("This test is only for prompt learning methods.") with hub_online_once(model_id + config_kwargs.get("tokenizer_name_or_path", "")): model = self.transformers_class.from_pretrained(model_id).to(self.torch_device) model = get_peft_model(model, config) msg = "Setting `requires_grad` is not supported for prompt learning methods like" with pytest.raises(TypeError, match=msg): model.set_requires_grad(adapter_names="adpater0") @pytest.mark.parametrize("model_id", PEFT_DECODER_MODELS_TO_TEST) @pytest.mark.parametrize("config_cls,config_kwargs", ALL_CONFIGS) def test_lora_conversion(self, model_id, config_cls, config_kwargs): # Test for the ability to convert a PEFT adapter into a LoRA adapter (if the adapter supports it). It's not # necessary to run this with all model types, only checking decoder models. _skip_if_not_conv1d_supported(model_id, config_cls) if config_kwargs.get("alora_invocation_tokens"): # very large conversion error, not sure why pytest.skip("Skipping LoRA conversion for aLoRA.") config_kwargs = set_init_weights_false(config_cls, config_kwargs) self._test_lora_conversion(model_id, config_cls, config_kwargs) def test_merge_and_unload_fixes_tie_word_embeddings_config(self): # See https://github.com/huggingface/transformers/issues/45127 model_id = "trl-internal-testing/tiny-random-LlamaForCausalLM" with hub_online_once(model_id): model = AutoModelForCausalLM.from_pretrained(model_id, tie_word_embeddings=True) assert model.config.tie_word_embeddings peft_model = get_peft_model(model, LoraConfig(target_modules=["embed_tokens"], init_lora_weights=False)) with pytest.warns(UserWarning, match="Setting.*tie_word_embeddings"): merged = peft_model.merge_and_unload() assert not merged.config.tie_word_embeddings assert merged.lm_head.weight is not merged.model.embed_tokens.weight assert merged.lm_head.weight.data_ptr() != merged.model.embed_tokens.weight.data_ptr() def test_prefix_tuning_gemma4_works(self): # see #3201 # The issue was that head dim differs depending on whether sliding window attention is being used or not: # https://github.com/huggingface/transformers/blob/223fe5231b783fbfb25296bb0a243dad5d158cde/src/transformers/models/gemma4/modeling_gemma4.py#L1147 # Prefix tuning could deal with different sizes, resulting in a size error model_id = "peft-internal-testing/tiny-random-gemma4-E2B" with hub_online_once(model_id): model = AutoModelForCausalLM.from_pretrained( model_id, dtype=torch.bfloat16, ).to(self.torch_device) config = PrefixTuningConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=20, prefix_projection=False, ) model = get_peft_model(model, config) inputs = torch.arange(10).view(1, -1).to(self.torch_device) model(inputs) # does not raise # do mini training run torch.manual_seed(0) labels = torch.ones_like(inputs) optim = torch.optim.SGD(model.parameters(), lr=100.0) losses = [] for _ in range(5): optim.zero_grad() outputs = model(inputs, labels=labels) loss = outputs.loss loss.backward() optim.step() losses.append(loss) assert torch.isfinite(loss) assert not torch.isclose(losses[0], losses[-1], atol=1e-6, rtol=1e-3) def test_prefix_tuning_gemma4_warns_if_some_layers_skipped(self): # See previous test_prefix_tuning_gemma4_works. When the embedding matrix is too small to fit any layer targeted # by prefix tuning, raise an error model_id = "peft-internal-testing/tiny-random-gemma4-E2B" with hub_online_once(model_id): model = AutoModelForCausalLM.from_pretrained( model_id, dtype=torch.bfloat16, ).to(self.torch_device) config = PrefixTuningConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=20, prefix_projection=False, ) text_config = model.config.get_text_config() text_config.num_kv_shared_layers = 1 # set to lower value (was 2) model = get_peft_model(model, config) inputs = torch.arange(10).view(1, -1).to(self.torch_device) with pytest.warns(UserWarning, match=r"skipped \[.*\] due to KV shape"): model(inputs) def test_prefix_tuning_gemma4_raises_if_all_layers_skipped(self): # See previous test_prefix_tuning_gemma4_works. When the embedding matrix is too small to fit any layer targeted # by prefix tuning, raise an error model_id = "peft-internal-testing/tiny-random-gemma4-E2B" with hub_online_once(model_id): model = AutoModelForCausalLM.from_pretrained( model_id, dtype=torch.bfloat16, ).to(self.torch_device) config = PrefixTuningConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=20, prefix_projection=False, ) model = get_peft_model(model, config) text_config = model.config.get_text_config() text_config.num_key_value_heads = 999 inputs = torch.arange(10).view(1, -1).to(self.torch_device) with pytest.raises(ValueError, match="skipped every layer"): model(inputs)