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
493 lines
23 KiB
Python
493 lines
23 KiB
Python
# Copyright 2024-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 functools
|
|
import inspect
|
|
import re
|
|
|
|
import packaging.version
|
|
import pytest
|
|
import torch
|
|
import transformers
|
|
from torch import nn
|
|
from transformers import AutoModelForCausalLM, AutoModelForImageTextToText
|
|
|
|
from peft import LoraConfig, PeftModel, get_peft_model
|
|
from peft.tuners import lora
|
|
from peft.utils import infer_device
|
|
from peft.utils.integrations import init_empty_weights, skip_init_on_device
|
|
|
|
from .testing_utils import hub_online_once
|
|
|
|
|
|
class MLP(nn.Module):
|
|
def __init__(self, bias=True):
|
|
super().__init__()
|
|
self.lin0 = nn.Linear(10, 20, bias=bias)
|
|
self.relu = nn.ReLU()
|
|
self.drop = nn.Dropout(0.5)
|
|
self.lin1 = nn.Linear(20, 2, bias=bias)
|
|
|
|
|
|
def get_mlp():
|
|
return MLP()
|
|
|
|
|
|
class TestInitEmptyWeights:
|
|
def test_init_empty_weights_works(self):
|
|
# this is a very rudimentary test, as init_empty_weights is copied almost 1:1 from accelerate and is tested
|
|
# there
|
|
with init_empty_weights():
|
|
mlp = get_mlp()
|
|
|
|
expected = torch.device("meta")
|
|
assert all(p.device == expected for p in mlp.parameters())
|
|
|
|
def test_skip_init_on_device_works(self):
|
|
# when a function is decorated with skip_init_on_device, the parameters are not moved to meta device, even when
|
|
# inside the context
|
|
decorated_fn = skip_init_on_device(get_mlp)
|
|
with init_empty_weights():
|
|
mlp = decorated_fn()
|
|
|
|
expected = torch.device("cpu")
|
|
assert all(p.device == expected for p in mlp.parameters())
|
|
|
|
def test_skip_init_on_device_works_outside_context(self):
|
|
# same as before, but ensure that skip_init_on_device does not break when no init_empty_weights context is used
|
|
decorated_fn = skip_init_on_device(get_mlp)
|
|
mlp = decorated_fn()
|
|
expected = torch.device("cpu")
|
|
assert all(p.device == expected for p in mlp.parameters())
|
|
|
|
def test_skip_init_on_device_not_permanent(self):
|
|
# ensure that after skip_init_on_device has been used, init_empty_weights reverts to its original functionality
|
|
|
|
# with decorator => cpu
|
|
decorated_fn = skip_init_on_device(get_mlp)
|
|
with init_empty_weights():
|
|
mlp = decorated_fn()
|
|
|
|
expected = torch.device("cpu")
|
|
assert all(p.device == expected for p in mlp.parameters())
|
|
|
|
# without decorator => meta
|
|
with init_empty_weights():
|
|
mlp = get_mlp()
|
|
|
|
expected = torch.device("meta")
|
|
assert all(p.device == expected for p in mlp.parameters())
|
|
|
|
def test_skip_init_on_device_nested(self):
|
|
# ensure that skip_init_on_device works even if the decorated function is nested inside another decorated
|
|
# function
|
|
@skip_init_on_device
|
|
def outer_fn():
|
|
@skip_init_on_device
|
|
def inner_fn():
|
|
return get_mlp()
|
|
|
|
mlp0 = inner_fn()
|
|
mlp1 = get_mlp()
|
|
return mlp0, mlp1
|
|
|
|
with init_empty_weights():
|
|
mlp0, mlp1 = outer_fn()
|
|
|
|
expected = torch.device("cpu")
|
|
assert all(p.device == expected for p in mlp0.parameters())
|
|
assert all(p.device == expected for p in mlp1.parameters())
|
|
|
|
|
|
# TODO Remove this once patch_moe_parameter_targeting is removed from Transformers
|
|
@pytest.fixture(params=[False, True], ids=["without_transformers_moe_patch", "with_transformers_moe_patch"])
|
|
def _transformers_moe_patch(request):
|
|
"""Parametrize tests over the MoE parameter-targeting patch being active/inactive.
|
|
|
|
The transformers patch `patch_moe_parameter_targeting` could hide a bug in PEFT when it comes to detecting the
|
|
correct in_features/out_features of a 3-dim MoE parameter. The patch is applied by transformers when their
|
|
`load_adapter` method is being called. Therefore, the order in which the tests were executed could hide or surface
|
|
the bug.
|
|
|
|
With this fixture, we ensure that each test is run twice, once without and once with the patch. This should mirror
|
|
real world usage, where the patch may or may not be active. At fixture exit, the patch is always removed.
|
|
|
|
For details, see discussion in #3165
|
|
|
|
"""
|
|
try:
|
|
from transformers.integrations.peft import patch_moe_parameter_targeting
|
|
except (ImportError, AttributeError):
|
|
patch_moe_parameter_targeting = None
|
|
|
|
should_patch = request.param
|
|
|
|
if should_patch and patch_moe_parameter_targeting is None:
|
|
pytest.skip(
|
|
"Transformers patch_moe_parameter_targeting no longer exists; skipping the 'patched' test variant."
|
|
)
|
|
|
|
is_patched = hasattr(lora.layer.ParamWrapper.update_layer, "__wrapped__")
|
|
orig_update_layer = inspect.unwrap(lora.layer.ParamWrapper.update_layer)
|
|
|
|
def new_update_layer(layer, *args, **kwargs):
|
|
# this is copied 1:1 from transformers:
|
|
# https://github.com/huggingface/transformers/blob/bc7ee236fca35e771b6b393178a192add1469243/src/transformers/integrations/peft.py#L394-L401
|
|
did_swap = getattr(layer, "_did_swap_in_out_features", False)
|
|
if not did_swap and layer.parameter_name in ("down_proj", "gate_up_proj"):
|
|
tmp_in_features = layer.in_features
|
|
layer.in_features = layer.out_features
|
|
layer.out_features = tmp_in_features
|
|
layer._did_swap_in_out_features = True
|
|
return orig_update_layer(layer, *args, **kwargs)
|
|
|
|
# 4 cases:
|
|
# should_patch | is_patched
|
|
# true | true
|
|
# true | false
|
|
# false | true
|
|
# false | false
|
|
|
|
if not should_patch and not is_patched:
|
|
yield
|
|
return
|
|
|
|
if not should_patch and is_patched:
|
|
lora.layer.ParamWrapper.update_layer = orig_update_layer
|
|
yield
|
|
return
|
|
|
|
if should_patch and is_patched:
|
|
try:
|
|
yield
|
|
finally:
|
|
lora.layer.ParamWrapper.update_layer = orig_update_layer
|
|
return
|
|
|
|
if should_patch and not is_patched:
|
|
try:
|
|
lora.layer.ParamWrapper.update_layer = functools.wraps(lora.layer.ParamWrapper.update_layer)(
|
|
new_update_layer
|
|
)
|
|
yield
|
|
finally:
|
|
lora.layer.ParamWrapper.update_layer = orig_update_layer
|
|
return
|
|
# this is unreachable
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
packaging.version.parse(transformers.__version__) < packaging.version.parse("5.4.0"),
|
|
reason="PEFT weight conversion is fixed in transformers 5.4.0",
|
|
)
|
|
@pytest.mark.usefixtures("_transformers_moe_patch") # TODO remove once patch_moe_parameter_targeting is removed
|
|
class TestTransformersV5:
|
|
"""Unit tests intended to test proper working of PEFT with Transformers v5"""
|
|
|
|
torch_device = infer_device()
|
|
|
|
@pytest.fixture
|
|
def expected_logits(self):
|
|
# original logits were:
|
|
# tensor([[[ 0.2676, 0.3870, 0.2956, ..., 0.4624, 0.1966, 0.2539],
|
|
# [-0.6706, -0.0969, -0.6240, ..., -0.0201, 0.7099, -0.3099],
|
|
# [ 0.0663, 0.1653, 0.7189, ..., 0.5905, 0.0649, 0.5839],
|
|
# ...,
|
|
# [-0.2712, -0.6451, -0.0219, ..., -0.4344, 0.5471, -0.9355],
|
|
# [-0.3607, 0.4526, 0.2750, ..., 0.1082, 0.7179, 0.8487],
|
|
# [ 0.5826, -0.1407, -0.3131, ..., 0.1026, 0.6878, -0.3382]]],
|
|
# device='cuda:0')
|
|
expected_logits_0_to_3 = torch.Tensor(
|
|
[
|
|
[0.2676, 0.3870, 0.2956],
|
|
[-0.6706, -0.0969, -0.6240],
|
|
[0.0663, 0.1653, 0.7189],
|
|
]
|
|
).to(device=self.torch_device, dtype=torch.float16)
|
|
return expected_logits_0_to_3
|
|
|
|
def test_mixtral_v4_lora_weight_conversion_transformers_load_adapter(self, expected_logits):
|
|
# Load a PEFT adapter trained with transformers v4 on Mixtral, which now has converted weights (MoE), using the
|
|
# Transformers integration (model.load_adapter).
|
|
inputs = torch.arange(10).view(1, -1).to(device=self.torch_device)
|
|
model_id = "hf-internal-testing/Mixtral-tiny"
|
|
lora_id = "peft-internal-testing/mixtral-pre-v5-lora"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id)
|
|
|
|
# test AutoModel.load_adapter
|
|
model.load_adapter(lora_id)
|
|
model.to(self.torch_device)
|
|
with torch.inference_mode():
|
|
output = model(inputs).logits
|
|
|
|
# a little bit of deviation but that's fine
|
|
atol, rtol = 1e-3, 1e-4
|
|
assert torch.allclose(output[0, :3, :3], expected_logits, atol=atol, rtol=rtol)
|
|
|
|
def test_mixtral_v4_lora_weight_conversion_peft_model_from_pretrained(self, expected_logits):
|
|
# Load a PEFT adapter trained with transformers v4 on Mixtral, which now has converted weights (MoE), using
|
|
# PeftModel.from_pretrained
|
|
inputs = torch.arange(10).view(1, -1).to(device=self.torch_device)
|
|
model_id = "hf-internal-testing/Mixtral-tiny"
|
|
lora_id = "peft-internal-testing/mixtral-pre-v5-lora"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
|
|
# test PeftModel.from_pretrained
|
|
model = PeftModel.from_pretrained(model, lora_id)
|
|
with torch.inference_mode():
|
|
output = model(inputs).logits
|
|
|
|
# a little bit of deviation but that's fine
|
|
atol, rtol = 1e-3, 1e-4
|
|
assert torch.allclose(output[0, :3, :3], expected_logits, atol=atol, rtol=rtol)
|
|
|
|
def test_mixtral_v4_lora_weight_conversion_peft_model_load_adapter(self, expected_logits):
|
|
# Same as the previous test, but using PeftModel.load_adapter
|
|
inputs = torch.arange(10).view(1, -1).to(device=self.torch_device)
|
|
model_id = "hf-internal-testing/Mixtral-tiny"
|
|
lora_id = "peft-internal-testing/mixtral-pre-v5-lora"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
|
|
# create a PeftModel instance
|
|
model = get_peft_model(model, LoraConfig(target_modules=["q_proj"]))
|
|
|
|
# test PeftModel.load_adapter
|
|
model.load_adapter(lora_id, adapter_name="other")
|
|
model.set_adapter("other")
|
|
with torch.inference_mode():
|
|
output = model(inputs).logits
|
|
atol, rtol = 1e-3, 1e-4
|
|
assert torch.allclose(output[0, :3, :3], expected_logits, atol=atol, rtol=rtol)
|
|
|
|
def test_mixtral_save_load_roundtrip(self, expected_logits, tmp_path):
|
|
# Load the v4 checkpoint with PEFT, save it (now v5 format) and load it again
|
|
inputs = torch.arange(10).view(1, -1).to(device=self.torch_device)
|
|
model_id = "hf-internal-testing/Mixtral-tiny"
|
|
lora_id = "peft-internal-testing/mixtral-pre-v5-lora"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
|
|
model = PeftModel.from_pretrained(model, lora_id)
|
|
model.save_pretrained(tmp_path)
|
|
del model
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
model = PeftModel.from_pretrained(model, tmp_path)
|
|
|
|
with torch.inference_mode():
|
|
output = model(inputs).logits
|
|
|
|
# a little bit of deviation but that's fine
|
|
atol, rtol = 1e-3, 1e-4
|
|
assert torch.allclose(output[0, :3, :3], expected_logits, atol=atol, rtol=rtol)
|
|
|
|
def test_add_lora_to_mixtral_v5_works(self):
|
|
# Ensure that using LoRA directly with a v5 model still works
|
|
inputs = torch.arange(10).view(1, -1).to(device=self.torch_device)
|
|
model_id = "hf-internal-testing/Mixtral-tiny"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
with torch.inference_mode():
|
|
output_base = model(inputs).logits
|
|
|
|
lora_config = LoraConfig(
|
|
target_modules=["q_proj", "k_proj", "v_proj"],
|
|
target_parameters=["gate.weight", "experts.gate_up_proj", "experts.down_proj"],
|
|
)
|
|
model = get_peft_model(model, lora_config).eval() # no error
|
|
|
|
with torch.inference_mode():
|
|
output_lora = model(inputs).logits
|
|
# sanity check
|
|
assert torch.allclose(output_base, output_lora)
|
|
|
|
num_lora_layers = len([m for m in model.modules() if isinstance(m, lora.LoraLayer)])
|
|
# sanity check
|
|
expected_num_lora_layers = 12 # 2 layers with 6 lora layers each
|
|
assert num_lora_layers == expected_num_lora_layers
|
|
|
|
def test_qwen_v4_lora_weight_conversion_peft_model_from_pretrained(self):
|
|
# Load a PEFT adapter trained with transformers v4 on Qwen3 MoE, which now has converted weights (MoE), using
|
|
# PeftModel.from_pretrained
|
|
inputs = torch.arange(10).view(1, -1).to(device=self.torch_device)
|
|
expected_logits = torch.Tensor(
|
|
[
|
|
[0.3644, -0.7487, -0.3190],
|
|
[0.2413, -0.8686, -0.5683],
|
|
[0.0333, -0.8790, -0.6361],
|
|
],
|
|
).to(device=self.torch_device)
|
|
model_id = "hf-internal-testing/tiny-qwen3-moe"
|
|
lora_id = "peft-internal-testing/qwen3-moe-pre-v5-lora"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
|
|
# test PeftModel.from_pretrained
|
|
model = PeftModel.from_pretrained(model, lora_id)
|
|
with torch.inference_mode():
|
|
output = model(inputs).logits
|
|
|
|
# a little bit of deviation but that's fine
|
|
atol, rtol = 1e-3, 1e-4
|
|
assert torch.allclose(output[0, :3, :3], expected_logits, atol=atol, rtol=rtol)
|
|
|
|
def test_qwen2_5_vl_works(self):
|
|
# https://github.com/huggingface/trl/issues/5428
|
|
|
|
# It can happen that a model returns an entry for get_checkpoint_conversion_mapping but there is nothing further
|
|
# to do because no weights are being fused (e.g. only renamed). In that case, we have no entry in
|
|
# _MOE_TARGET_MODULE_MAPPING. The bug was that we would call dict.__getitem__ instead of dict.get.
|
|
model_id = "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration"
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForImageTextToText.from_pretrained(model_id)
|
|
config = LoraConfig(target_modules=["q_proj", "v_proj"])
|
|
get_peft_model(model, config) # does not raise
|
|
|
|
@pytest.mark.parametrize(
|
|
"target_modules", [["up_proj", "down_proj", "score"], r".*\.(up_proj|down_proj)"], ids=["list", "regex"]
|
|
)
|
|
def test_qwen3_moe_partial_fusion_raises(self, target_modules):
|
|
# https://github.com/huggingface/trl/issues/5428
|
|
# Targeting up_proj but not gate_proj must raise -- they are fused into gate_up_proj. Covers the list and regex
|
|
# paths and guards the error-message handling. See `test_qwen3_moe_works` for a valid config.
|
|
model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM"
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id)
|
|
msg = re.escape(
|
|
"Cannot convert PEFT target(s) up_proj without also targeting gate_proj because they are fused into "
|
|
"gate_up_proj."
|
|
)
|
|
with pytest.raises(ValueError, match=msg):
|
|
get_peft_model(model, LoraConfig(target_modules=target_modules))
|
|
|
|
def test_qwen3_moe_works(self):
|
|
# https://github.com/huggingface/trl/issues/5428
|
|
# When correctly targeting both up and gate projection, there should be no error (see previous test)
|
|
model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM"
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id)
|
|
# target up_proj and gate_proj
|
|
config = LoraConfig(target_modules=["gate_proj", "up_proj", "down_proj", "score"])
|
|
get_peft_model(model, config) # does not raise
|
|
|
|
@pytest.mark.parametrize(
|
|
"regex",
|
|
[
|
|
# the shape ms-swift's get_multimodal_target_regex emits: anchored, prefix lookahead
|
|
r"^(model(?=\.).*\.(q_proj|k_proj|v_proj|gate_proj|up_proj|down_proj))$",
|
|
# a plain "ends with one of these projections" pattern
|
|
r".*\.(q_proj|k_proj|v_proj|gate_proj|up_proj|down_proj)",
|
|
],
|
|
)
|
|
def test_qwen3_moe_regex_target_modules_works(self, regex):
|
|
# Regression test for https://github.com/huggingface/peft/issues/3229: the conversion used to `set()` the regex
|
|
# string, splitting it into characters. It is now resolved to concrete names against the model.
|
|
model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM"
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id)
|
|
peft_model = get_peft_model(model, LoraConfig(target_modules=regex)) # does not raise
|
|
|
|
config = peft_model.peft_config["default"]
|
|
# non-fused targets stay in target_modules; fused experts move to target_parameters (rank/alpha doubled for the
|
|
# 2-way gate_up_proj). The regex does not name the router, so `gate.weight` is *not* pulled in (contrast
|
|
# `test_qwen3_moe_all_linear_target_modules_works`, where "all-linear" does pull it in).
|
|
assert config.target_modules == {"q_proj", "k_proj", "v_proj"}
|
|
assert set(config.target_parameters) == {"gate_up_proj", "down_proj"}
|
|
assert config.rank_pattern == {r".*\.gate_up_proj": config.r * 2}
|
|
assert config.alpha_pattern == {r".*\.gate_up_proj": config.lora_alpha * 2}
|
|
# the experts are actually adapted, not just recorded in the config
|
|
assert any("experts" in name for name, _ in peft_model.named_modules() if name.endswith("lora_A.default"))
|
|
|
|
def test_qwen3_moe_single_attention_regex_target_modules_works(self):
|
|
# A regex resolving to a single attention projection (no experts) converts cleanly: nothing moves to parameters.
|
|
model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM"
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id)
|
|
peft_model = get_peft_model(model, LoraConfig(target_modules=r".*\.q_proj")) # does not raise
|
|
|
|
config = peft_model.peft_config["default"]
|
|
assert config.target_modules == {"q_proj"}
|
|
assert not config.target_parameters
|
|
assert not config.rank_pattern
|
|
adapted = {
|
|
name.rsplit(".lora_A", 1)[0].rsplit(".", 1)[-1]
|
|
for name, _ in peft_model.named_modules()
|
|
if name.endswith("lora_A.default")
|
|
}
|
|
assert adapted == {"q_proj"}
|
|
|
|
def test_qwen3_moe_all_linear_target_modules_works(self):
|
|
# "all-linear" is matched by module type, not as a regex. On v4 the experts *and* the router `gate` were
|
|
# nn.Linear, so "all-linear" targeted them; after fusion the experts are stacked parameters and the router is a
|
|
# custom module, so "all-linear" must still carry all of them into target_parameters (gate -> gate.weight).
|
|
model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM"
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id)
|
|
peft_model = get_peft_model(model, LoraConfig(target_modules="all-linear")) # does not raise
|
|
|
|
config = peft_model.peft_config["default"]
|
|
adapted = {
|
|
name.rsplit(".lora_A", 1)[0].rsplit(".", 1)[-1]
|
|
for name, _ in peft_model.named_modules()
|
|
if name.endswith("lora_A.default")
|
|
}
|
|
# attention projections resolved as modules; experts and router (linear in v4) carried into target_parameters
|
|
assert {"q_proj", "k_proj", "v_proj", "o_proj"} <= adapted
|
|
# the router `gate` is adapted too -- it is a parameter (`gate.weight`) on v5, so unlike the regex case it has
|
|
# to be added by name; this is exactly what distinguishes "all-linear" from a regex that omits the router
|
|
assert set(config.target_parameters) == {"gate.weight", "gate_up_proj", "down_proj"}
|
|
assert config.rank_pattern == {r".*\.gate_up_proj": config.r * 2}
|
|
assert config.alpha_pattern == {r".*\.gate_up_proj": config.lora_alpha * 2}
|
|
# the experts and the router gate are actually adapted, not just recorded in the config
|
|
assert any("experts" in name for name, _ in peft_model.named_modules() if name.endswith("lora_A.default"))
|
|
assert any(name.endswith(".mlp.gate.lora_A.default") for name, _ in peft_model.named_modules())
|
|
|
|
def test_qwen3_moe_regex_target_modules_save_load_roundtrip(self, tmp_path):
|
|
# Save/reload a regex-targeted adapter. Reload re-runs the conversion on the already-converted config, so this
|
|
# also guards idempotency.
|
|
inputs = torch.arange(10).view(1, -1).to(self.torch_device)
|
|
model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM"
|
|
regex = r"^(model(?=\.).*\.(q_proj|k_proj|v_proj|gate_proj|up_proj|down_proj))$"
|
|
|
|
with hub_online_once(model_id):
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
# init_lora_weights=False gives the adapter non-zero (random) weights so it actually changes the output
|
|
peft_model = get_peft_model(model, LoraConfig(target_modules=regex, init_lora_weights=False))
|
|
with torch.inference_mode():
|
|
logits_before = peft_model(inputs).logits
|
|
peft_model.save_pretrained(tmp_path)
|
|
del model, peft_model
|
|
|
|
model = AutoModelForCausalLM.from_pretrained(model_id).to(self.torch_device)
|
|
reloaded = PeftModel.from_pretrained(model, tmp_path)
|
|
with torch.inference_mode():
|
|
logits_after = reloaded(inputs).logits
|
|
|
|
assert torch.allclose(logits_before, logits_after, atol=1e-5, rtol=1e-5)
|
|
# reload re-runs the conversion on the already-resolved config -- idempotent
|
|
reloaded_config = reloaded.peft_config["default"]
|
|
assert reloaded_config.target_modules == {"q_proj", "k_proj", "v_proj"}
|
|
assert {"gate_up_proj", "down_proj"} <= set(reloaded_config.target_parameters)
|
|
assert reloaded_config.rank_pattern == {r".*\.gate_up_proj": reloaded_config.r * 2}
|