diff --git a/src/peft/tuners/tuners_utils.py b/src/peft/tuners/tuners_utils.py index a6be1a8e84..01a4888d9d 100644 --- a/src/peft/tuners/tuners_utils.py +++ b/src/peft/tuners/tuners_utils.py @@ -1167,7 +1167,8 @@ def _replace_module(self, parent, child_name, new_module, child) -> None: child = child.base_layer if not hasattr(new_module, "base_layer"): - new_module.weight = child.weight + if hasattr(child, "weight"): + new_module.weight = child.weight if hasattr(child, "bias"): new_module.bias = child.bias diff --git a/tests/test_other.py b/tests/test_other.py index 693399a257..755b6b29bf 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -274,6 +274,37 @@ def test_transient_attribute_access_non_existing_adapter(self, mlp): model.lin1.weight +class TestModulesToSaveMergeAndUnload: + def test_merge_and_unload_without_weight(self): + # Regression test for #3213: merge_and_unload should not crash when modules_to_save + # targets a module without a weight attribute (e.g., a transformer encoder block) + class ModuleWithoutWeight(nn.Module): + def __init__(self): + super().__init__() + self.foo = nn.Linear(1, 1) + + def forward(self, x): + return self.foo(x) + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.lin0 = nn.Linear(1, 2) + self.mod1 = ModuleWithoutWeight() + + def forward(self, x): + return self.mod1(self.lin0(x)) + + from peft import LoraConfig, get_peft_model + config = LoraConfig(target_modules=["lin0"], modules_to_save=["mod1"]) + model = get_peft_model(Model(), config) + + # This should not raise an AttributeError + merged = model.merge_and_unload() + assert not hasattr(merged.mod1, "weight") + + + class TestModulesToSaveKwargsOnlyForward: """Regression test for #3191: modules listed in `modules_to_save` whose parent calls them with keyword arguments only (e.g. Gemma's `vision_tower(pixel_values=...)`) used to crash with `TypeError: forward() missing 1 required