Files
huggingface--peft/tests/test_lora_conversion.py
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
Build documentation / build (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:24:42 +08:00

694 lines
31 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.
import copy
import platform
import re
import pytest
import torch
from torch import nn
from transformers import AutoModelForCausalLM
from peft import (
C3AConfig,
IA3Config,
LoKrConfig,
LoraConfig,
MissConfig,
PeftModel,
PrefixTuningConfig,
convert_to_lora,
get_peft_model,
get_peft_model_state_dict,
save_as_lora,
set_peft_model_state_dict,
)
from peft.utils import infer_device
from .testing_utils import hub_online_once
class TestLoraConversion:
"""Test functionality to convert non-LoRA adapters to LoRA adapters
This is mainly testing with LoKr, as it would be wasteful to test with all compatible PEFT methods in detail. For a
broad suite of tests across PEFT methods, check test_decoder_models.py::test_lora_conversion.
We mainly use convert_to_lora and not save_as_lora here, as is just a thin wrapper around convert_to_lora and
involves disk IO, which we want to avoid as much as possible. For most users, save_as_lora will most likely be the
main entry point,
For comparing outputs, it's not ideal to check the logits, as most of them are close to zero and we cannot use
torch.allclose, as a certain deviation is expected from conversion. A robust way would be to check the hidden
states after subtracting the base model's hidden states (since the contribution of the adapter is what we want to
compare).
"""
model_id = "peft-internal-testing/tiny-random-OPTForCausalLM"
torch_device = infer_device()
base_model = None
def get_base_model(self):
if self.base_model is None:
with hub_online_once(self.model_id):
self.base_model = AutoModelForCausalLM.from_pretrained(self.model_id).to(self.torch_device)
return copy.deepcopy(self.base_model)
@pytest.fixture
def lokr_model(self):
torch.manual_seed(0)
return get_peft_model(self.get_base_model(), LoKrConfig(init_weights=False))
@staticmethod
def get_mse(output1, output2):
return nn.functional.mse_loss(output1.hidden_states[-1], output2.hidden_states[-1]).item()
def test_no_peft_layer_raises(self):
# Model without any PEFT layer should raise
base_model = self.get_base_model()
msg = "Could not detect any layer that supports LoRA conversion"
with pytest.raises(TypeError, match=msg):
convert_to_lora(base_model, rank=8)
def test_prompt_learning_model_raises(self):
# Prefix Tuning does not support LoRA conversion
base_model = self.get_base_model()
config = PrefixTuningConfig(num_virtual_tokens=10, task_type="CAUSAL_LM")
prefix_model = get_peft_model(base_model, config).eval()
assert not prefix_model.supports_lora_conversion()
msg = "Could not detect any layer that supports LoRA conversion"
with pytest.raises(TypeError, match=msg):
convert_to_lora(prefix_model, rank=8)
def test_peft_model_but_no_support_raises(self):
# IA3 has BaseTunerLayers but does not support LoRA conversion
base_model = self.get_base_model()
ia3_model = get_peft_model(base_model, IA3Config()).eval()
assert not ia3_model.supports_lora_conversion()
msg = "Some module types on this model do not support LoRA conversion"
with pytest.raises(TypeError, match=msg):
convert_to_lora(ia3_model, rank=8)
def test_model_with_unsupported_layers_raises(self):
# conv layers do not support LoRA conversion (yet)
# note: change this test if we add support for conv layer conversion
class MyModule(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(16, 16, 3)
self.lin = nn.Linear(16, 16)
lokr_model = get_peft_model(MyModule(), LoKrConfig(target_modules=["conv", "lin"])).eval()
assert not lokr_model.supports_lora_conversion()
msg = "Some module types on this model do not support LoRA conversion"
with pytest.raises(TypeError, match=msg):
convert_to_lora(lokr_model, rank=8)
def test_targeted_modules_identical(self, lokr_model):
lora_config, lora_state_dict = convert_to_lora(lokr_model, rank=8)
lokr_state_dict = lokr_model.state_dict()
# LoRA should have an entry for each layer targeted by LoKr
# cut off parameter name and PEFT method specific part of the name to obtain module name
modules_lokr = {k.rsplit(".", 2)[0] for k in lokr_state_dict.keys() if ".lokr" in k}
modules_lora = {k.rsplit(".", 2)[0] for k in lora_state_dict.keys() if ".lora" in k}
assert modules_lokr == modules_lora
# creating a new LoRA model based on the returned config should give the same state dict keys
base_model = self.get_base_model()
new_lora_model = get_peft_model(base_model, lora_config).eval()
new_lora_state_dict = get_peft_model_state_dict(new_lora_model)
assert lora_state_dict.keys() == new_lora_state_dict.keys()
def test_targeted_modules_identical_target_modules_str(self):
base_model = self.get_base_model()
lokr_config = LoKrConfig(target_modules=r".*\.q_proj", r=16, init_weights=False)
lokr_model = get_peft_model(base_model, lokr_config).eval()
lora_config, lora_state_dict = convert_to_lora(lokr_model, rank=8)
lokr_state_dict = lokr_model.state_dict()
# LoRA should have an entry for each layer targeted by LoKr
# cut off parameter name and PEFT method specific part of the name to obtain module name
modules_lokr = {k.rsplit(".", 2)[0] for k in lokr_state_dict.keys() if ".lokr" in k}
modules_lora = {k.rsplit(".", 2)[0] for k in lora_state_dict.keys() if ".lora" in k}
assert modules_lokr == modules_lora
# creating a new LoRA model based on the returned config should give the same state dict keys
base_model = self.get_base_model()
new_lora_model = get_peft_model(base_model, lora_config).eval()
new_lora_state_dict = get_peft_model_state_dict(new_lora_model)
assert lora_state_dict.keys() == new_lora_state_dict.keys()
def test_fixed_rank_lora_config(self, lokr_model):
# with a fixed rank, we expect target_modules to be set on the LoRA config but not rank_pattern, alpha_pattern
lora_config, _ = convert_to_lora(lokr_model, rank=8)
assert isinstance(lora_config, LoraConfig)
assert lora_config.r == 8
assert lora_config.lora_alpha == 8
assert lora_config.target_modules
assert not lora_config.rank_pattern
assert not lora_config.alpha_pattern
def test_dynamic_rank_lora_config(self, lokr_model):
# with a dynamic rank, we expect rank_pattern and alpha_pattern to be set
lora_config, state_dict = convert_to_lora(lokr_model, rank=0.5)
assert lora_config.r == 1 # dummy value
assert lora_config.lora_alpha == 1 # dummy value
assert lora_config.rank_pattern
assert lora_config.alpha_pattern
# rank and alpha are always the same, i.e. scaling is 1
assert lora_config.rank_pattern == lora_config.alpha_pattern
# for each module, two LoRA weights
assert 2 * len(lora_config.rank_pattern) == len(state_dict)
def test_dynamic_rank_1_lora_config(self, lokr_model):
# with a dynamic rank, we expect rank_pattern and alpha_pattern to be set
lora_config, state_dict = convert_to_lora(lokr_model, rank=1.0)
assert lora_config.r == 1 # dummy value
assert lora_config.lora_alpha == 1 # dummy value
assert lora_config.rank_pattern
assert lora_config.alpha_pattern
# rank and alpha are always the same, i.e. scaling is 1
assert lora_config.rank_pattern == lora_config.alpha_pattern
# for each module, two LoRA weights
assert 2 * len(lora_config.rank_pattern) == len(state_dict)
def test_threshold_wrong_value_raises(self, lokr_model):
# if a threshold is used, it must be between 0 and 1
msg = "If rank is a float, it is interpreted as a threshold. It must be between 0 and 1 but got 123.0"
with pytest.raises(ValueError, match=msg):
convert_to_lora(lokr_model, rank=123.0)
msg = "If rank is a float, it is interpreted as a threshold. It must be between 0 and 1 but got -0.5"
with pytest.raises(ValueError, match=msg):
convert_to_lora(lokr_model, rank=-0.5)
def test_rank_higher_than_weight_dim_raises(self, lokr_model):
# if the requested rank is higher than the weight dimension, we should raise
msg = re.escape("The chosen rank 123 is larger than the weight shape (16), please choose a lower rank")
with pytest.raises(ValueError, match=msg):
convert_to_lora(lokr_model, rank=123)
def test_fixed_rank_0_raises(self, lokr_model):
msg = "Passing a rank of 0 doesn't make sense"
with pytest.raises(ValueError, match=msg):
convert_to_lora(lokr_model, rank=0)
def test_converting_transformers_model_works(self, lokr_model, tmp_path):
# test that we can convert a transformers model that has loaded LoKr directly
assert lokr_model.supports_lora_conversion()
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
output_lokr = lokr_model(inputs, output_hidden_states=True)
lokr_model.save_pretrained(tmp_path)
# load directly with transformers
loaded_model = AutoModelForCausalLM.from_pretrained(tmp_path).to(self.torch_device)
with torch.inference_mode():
output_loaded = loaded_model(inputs, output_hidden_states=True)
# sanity check
atol, rtol = 1e-4, 1e-4
assert torch.allclose(output_lokr.logits, output_loaded.logits, atol=atol, rtol=rtol)
save_as_lora(tmp_path / "converted", lokr_model, rank=8)
lora_model = AutoModelForCausalLM.from_pretrained(tmp_path / "converted").to(self.torch_device)
# With from_pretrained, we don't get a load_result and thus cannot check for missing keys. As a proxy,
# instead check that no LoRA weight is all zeros (which would indicate a missing weight)
for name, param in lora_model.named_parameters():
if (".lora_A" in name) or (".lora_B" in name):
assert not torch.all(param == 0)
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
assert 0.0 < self.get_mse(output_converted, output_lokr) < 0.1
def test_converted_lora_approximates_original_adapter(self, lokr_model):
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
with lokr_model.disable_adapter():
output_base = lokr_model(inputs, output_hidden_states=True)
output_lokr = lokr_model(inputs, output_hidden_states=True)
# sanity check
atol, rtol = 1e-4, 1e-4
assert not torch.allclose(output_base.logits, output_lokr.logits, atol=atol, rtol=rtol)
##############
# fixed rank #
##############
lora_config, state_dict = convert_to_lora(lokr_model, rank=8)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
# by default, the LoRA model should be an identity transform
with torch.inference_mode():
output_lora = lora_model(inputs, output_hidden_states=True)
assert torch.allclose(output_base.logits, output_lora.logits, atol=atol, rtol=rtol)
# load the converted LoRA weights
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
# sanity check the number of trainable parameters
num_train_params, total_params = lora_model.get_nb_trainable_parameters()
assert 100 < num_train_params < 0.1 * total_params
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
mse_lora = self.get_mse(output_lora, output_lokr)
mse_converted = self.get_mse(output_converted, output_lokr)
assert mse_lora > 0.5
assert 0.0 < mse_converted < 0.1
###############################
# this time with dynamic rank #
###############################
lora_config, state_dict = convert_to_lora(lokr_model, rank=0.9)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
# sanity check the number of trainable parameters
num_train_params, total_params = lora_model.get_nb_trainable_parameters()
assert 100 < num_train_params < 0.1 * total_params
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
mse_converted = self.get_mse(output_converted, output_lokr)
assert 0.0 < mse_converted < 0.1
def test_with_tqdm_works(self, lokr_model, capsys):
# pass progressbar=True to use tqdm
convert_to_lora(lokr_model, rank=8, progressbar=True)
captured = capsys.readouterr()
assert "Converting to LoRA" in captured.err
def test_save_as_lora(self, lokr_model, tmp_path):
# whether using save_as_lora gives the same result as convert_to_lora
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
atol, rtol = 1e-4, 1e-4
lora_config, state_dict = convert_to_lora(lokr_model, rank=8)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_before = lora_model(inputs).logits
# test that save_as_lora works as expected
save_as_lora(tmp_path, lokr_model, rank=8)
base_model = self.get_base_model()
loaded_model = PeftModel.from_pretrained(base_model, tmp_path).to(self.torch_device)
with torch.inference_mode():
output_after = loaded_model(inputs).logits
assert torch.allclose(output_before, output_after, atol=atol, rtol=rtol)
def test_model_without_peft_config(self, lokr_model):
# Conversion also works with models that don't have a PeftConfig on them. This is a bit of a convoluted case,
# but conversion doesn't strictly rely on an existing peft_config, so it should still work.
def unwrap(peft_model):
unwrapped = peft_model.get_base_model()
del unwrapped.peft_config
return unwrapped
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
output_lokr = lokr_model(inputs, output_hidden_states=True)
# remove the PeftModel wrapper and the peft_config attribute -- this should still work
unwrapped_lokr_model = unwrap(lokr_model)
lora_config, state_dict = convert_to_lora(unwrapped_lokr_model, rank=8)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
unwrapped_lora_model = unwrap(lora_model)
# Note: On the unwrapped model, we cannot use set_peft_model_state_dict, as that requires a peft_config. Thus,
# we need to manually inject the adapter name into state_dict keys, which is done automatically when using
# set_peft_model_state_dict.
new_state_dict = {}
for k, v in state_dict.items():
new_k = k.replace(".lora_A.weight", ".lora_A.default.weight")
new_k = new_k.replace(".lora_B.weight", ".lora_B.default.weight")
new_state_dict[new_k] = v
load_result = unwrapped_lora_model.load_state_dict(new_state_dict, strict=False)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
mse = self.get_mse(output_converted, output_lokr)
assert 0.0 < mse < 0.1
def test_converted_lora_to_lora_works_and_warns(self):
# In general, there is no need to convert LoRA to LoRA, but it should still work. One possible use case would be
# to shrink the rank of an existing LoRA adapter. The resulting correlation in this test is surprisingly high,
# probably because the initial LoRA was not trained but initialized with random weights.
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
base_model = self.get_base_model()
with torch.inference_mode():
output_base = base_model(inputs, output_hidden_states=True)
orig_lora_config = LoraConfig(r=16, init_lora_weights=False)
orig_lora_model = get_peft_model(base_model, orig_lora_config).eval()
with torch.inference_mode():
output_orig_lora = orig_lora_model(inputs, output_hidden_states=True)
# sanity check
atol, rtol = 1e-4, 1e-4
assert not torch.allclose(output_base.logits, output_orig_lora.logits)
# convert from rank 16 to rank 8
msg = "Converting a PEFT adapter to LoRA that is already a LoRA adapter"
with pytest.warns(UserWarning, match=msg):
# check that a warning was issued
lora_config, state_dict = convert_to_lora(orig_lora_model, rank=8)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
# load the converted LoRA weights
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
mse_converted = self.get_mse(output_converted, output_orig_lora)
assert 0.0 < mse_converted < 0.1
def test_converted_lora_with_multiple_adapters(self, lokr_model):
# ensure that we can convert specific adapters when multiple are present
lokr_config = LoKrConfig(r=16, init_weights=False)
lokr_model.add_adapter("other", lokr_config)
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
output_lokr_default = lokr_model(inputs, output_hidden_states=True)
lokr_model.set_adapter("other")
output_lokr_other = lokr_model(inputs, output_hidden_states=True)
# sanity check
atol, rtol = 1e-4, 1e-4
assert not torch.allclose(output_lokr_default.logits, output_lokr_other.logits, atol=atol, rtol=rtol)
# convert the default adapter
lora_config_default, state_dict_default = convert_to_lora(lokr_model, rank=8)
base_model = self.get_base_model()
lora_model_default = get_peft_model(base_model, lora_config_default).eval()
# load the converted LoRA weights for the default adapter
load_result = set_peft_model_state_dict(lora_model_default, state_dict_default)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_converted_default = lora_model_default(inputs, output_hidden_states=True)
# convert the other adapter
lora_config_other, state_dict_other = convert_to_lora(lokr_model, rank=8, adapter_name="other")
base_model = self.get_base_model()
lora_model_other = get_peft_model(base_model, lora_config_other).eval()
# load the converted LoRA weights for the other adapter
load_result = set_peft_model_state_dict(lora_model_other, state_dict_other)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_converted_other = lora_model_other(inputs, output_hidden_states=True)
mse_default_default = self.get_mse(output_converted_default, output_lokr_default)
mse_other_other = self.get_mse(output_converted_other, output_lokr_other)
mse_default_other = self.get_mse(output_converted_default, output_lokr_other)
mse_other_default = self.get_mse(output_converted_other, output_lokr_default)
assert 0.0 < mse_default_default < 0.1
assert 0.0 < mse_other_other < 0.1
assert mse_default_other > 0.5
assert mse_other_default > 0.5
def test_convert_model_with_modules_to_save(self):
# If the original adapter defines modules_to_save, these need to be included in the LoRA adapter
model = self.get_base_model()
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
output_base = model(inputs, output_hidden_states=True)
# lokr is initialized as identity transform to ensure that modules_to_save is the thing that impacts the output
lokr_config = LoKrConfig(modules_to_save=["0.fc1"])
lokr_model = get_peft_model(model, lokr_config)
# ensure that the modules_to_save affects the output
lokr_model.base_model.model.model.decoder.layers[0].fc1.modules_to_save.default.weight.data.mul_(-10.0)
lokr_model.base_model.model.model.decoder.layers[0].fc1.modules_to_save.default.bias.data.mul_(-10.0)
with torch.inference_mode():
output_lokr = lokr_model(inputs, output_hidden_states=True)
# sanity check
atol, rtol = 1e-4, 1e-4
assert not torch.allclose(output_base.logits, output_lokr.logits, atol=atol, rtol=rtol)
lora_config, state_dict = convert_to_lora(lokr_model, rank=8)
assert lora_config.modules_to_save == lokr_config.modules_to_save
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
# load the converted LoRA weights
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
mse_converted = self.get_mse(output_converted, output_lokr)
# here we expect an actual loss of 0, since only the modules_to_save affect the result, and those are identical
assert mse_converted == 0.0
@pytest.mark.parametrize("bias", ["c3a_only", "all"])
def test_convert_model_with_trainable_bias_raises(self, bias):
# If the original adapter includes trainable bias terms, we raise. LoKr doesn't support this, so taking C3A
model = self.get_base_model()
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
c3a_config = C3AConfig(block_size=4, bias=bias)
c3a_model = get_peft_model(model, c3a_config)
msg = "The adapter's config sets bias"
with pytest.raises(ValueError, match=msg):
convert_to_lora(c3a_model, rank=8)
@pytest.mark.skipif(platform.system() != "Linux", reason="Running test involving torch.compile only on Linux.")
def test_with_torch_compile(self, lokr_model):
# ensure that we can call lora conversion with compilation
lora_config_no_comp, state_dict_no_comp = convert_to_lora(lokr_model, rank=8)
lora_config_comp, state_dict_comp = convert_to_lora(
lokr_model, rank=8, compile_kwargs={"mode": "max-autotune-no-cudagraphs"}
)
assert lora_config_no_comp.to_dict() == lora_config_comp.to_dict()
assert state_dict_no_comp.keys() == state_dict_comp.keys()
for key, weight_no_comp in state_dict_no_comp.items():
weight_comp = state_dict_comp[key]
assert torch.allclose(weight_comp, weight_no_comp)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_convert_float16_dtype(self, dtype):
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
torch.manual_seed(0)
base_model = self.get_base_model().to(dtype)
with torch.inference_mode():
output_base = base_model(inputs, output_hidden_states=True)
# load a LoKr model with 16 bit precision
lokr_model = get_peft_model(base_model, LoKrConfig(init_weights=False), autocast_adapter_dtype=False)
with torch.inference_mode():
output_lokr = lokr_model(inputs, output_hidden_states=True)
# sanity check
atol, rtol = 1e-4, 1e-4
assert not torch.allclose(output_base.logits, output_lokr.logits, atol=atol, rtol=rtol)
lora_config, state_dict = convert_to_lora(lokr_model, rank=8)
for weight in state_dict.values():
assert weight.dtype == dtype
base_model = self.get_base_model().to(dtype)
lora_model = get_peft_model(base_model, lora_config, autocast_adapter_dtype=False).eval()
# load the converted LoRA weights
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_converted = lora_model(inputs, output_hidden_states=True)
mse_converted = self.get_mse(output_converted, output_lokr)
assert 0.0 < mse_converted < 0.1
class TestMissLoraConversion:
"""Test MiSS to LoRA conversion for standard, mini, and bat modes."""
model_id = "peft-internal-testing/tiny-random-OPTForCausalLM"
torch_device = infer_device()
base_model = None
def get_base_model(self):
if self.base_model is None:
with hub_online_once(self.model_id):
self.base_model = AutoModelForCausalLM.from_pretrained(self.model_id).to(self.torch_device)
return copy.deepcopy(self.base_model)
@staticmethod
def get_mse(output1, output2):
return nn.functional.mse_loss(output1.hidden_states[-1], output2.hidden_states[-1]).item()
def _randomize_miss_blocks(self, model):
with torch.no_grad():
for m in model.modules():
if hasattr(m, "miss_block"):
for p in m.miss_block.values():
p.data.normal_(0, 0.01)
@pytest.fixture
def miss_model_standard(self):
torch.manual_seed(0)
config = MissConfig(r=4, init_weights=False, target_modules=["q_proj", "v_proj"])
return get_peft_model(self.get_base_model(), config)
@pytest.fixture
def miss_model_mini(self):
torch.manual_seed(0)
config = MissConfig(r=4, mini_r=2, init_weights="mini", target_modules=["q_proj", "v_proj"])
model = get_peft_model(self.get_base_model(), config)
self._randomize_miss_blocks(model)
return model
@pytest.fixture
def miss_model_bat(self):
torch.manual_seed(0)
config = MissConfig(r=4, init_weights="bat", target_modules=["q_proj", "v_proj"])
model = get_peft_model(self.get_base_model(), config)
self._randomize_miss_blocks(model)
return model
def test_miss_supports_lora_conversion(self, miss_model_standard, miss_model_mini, miss_model_bat):
assert miss_model_standard.supports_lora_conversion()
assert miss_model_mini.supports_lora_conversion()
assert miss_model_bat.supports_lora_conversion()
def test_miss_standard_exact_conversion(self, miss_model_standard):
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
output_miss = miss_model_standard(inputs, output_hidden_states=True)
lora_config, state_dict = convert_to_lora(miss_model_standard, rank=4)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_lora = lora_model(inputs, output_hidden_states=True)
mse = self.get_mse(output_lora, output_miss)
assert mse < 1e-5, f"Standard MiSS conversion should be exact, got mse={mse}"
def test_miss_mini_exact_conversion(self, miss_model_mini):
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
output_miss = miss_model_mini(inputs, output_hidden_states=True)
lora_config, state_dict = convert_to_lora(miss_model_mini, rank=4)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_lora = lora_model(inputs, output_hidden_states=True)
mse = self.get_mse(output_lora, output_miss)
assert mse < 1e-5, f"Mini MiSS conversion should be exact, got mse={mse}"
def test_miss_bat_approximate_conversion(self, miss_model_bat):
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
with torch.inference_mode():
with miss_model_bat.disable_adapter():
output_base = miss_model_bat(inputs, output_hidden_states=True)
output_miss = miss_model_bat(inputs, output_hidden_states=True)
atol, rtol = 1e-4, 1e-4
assert not torch.allclose(output_base.logits, output_miss.logits, atol=atol, rtol=rtol)
lora_config, state_dict = convert_to_lora(miss_model_bat, rank=4)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
load_result = set_peft_model_state_dict(lora_model, state_dict)
assert not load_result.unexpected_keys
with torch.inference_mode():
output_lora = lora_model(inputs, output_hidden_states=True)
mse = self.get_mse(output_lora, output_miss)
assert 0.0 < mse < 0.1
def test_miss_targeted_modules_identical(self, miss_model_standard):
_, lora_state_dict = convert_to_lora(miss_model_standard, rank=4)
miss_state_dict = miss_model_standard.state_dict()
modules_miss = {k.rsplit(".", 2)[0] for k in miss_state_dict.keys() if ".miss_block" in k}
modules_lora = {k.rsplit(".", 2)[0] for k in lora_state_dict.keys() if ".lora" in k}
assert modules_miss == modules_lora
def test_miss_save_as_lora(self, miss_model_standard, tmp_path):
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
atol, rtol = 1e-4, 1e-4
lora_config, state_dict = convert_to_lora(miss_model_standard, rank=4)
base_model = self.get_base_model()
lora_model = get_peft_model(base_model, lora_config).eval()
set_peft_model_state_dict(lora_model, state_dict)
with torch.inference_mode():
output_before = lora_model(inputs).logits
save_as_lora(tmp_path, miss_model_standard, rank=4)
base_model = self.get_base_model()
loaded_model = PeftModel.from_pretrained(base_model, tmp_path).to(self.torch_device)
with torch.inference_mode():
output_after = loaded_model(inputs).logits
assert torch.allclose(output_before, output_after, atol=atol, rtol=rtol)