From ee67e3e98f1f50e4a1230872ebf11b40d434c605 Mon Sep 17 00:00:00 2001 From: Benjamin Bossan Date: Fri, 5 Jun 2026 14:39:48 +0200 Subject: [PATCH] FIX Bug when targeting an nn.Parameter on the root module Using LoRA, we allow to target nn.Parameters directly by using target_parametes=[...]. This works by replacing the module that this parameter belongs to with lora.ParamWrapper. However, this cannot work if the parameter lives on the root model itself, since that would require replacing the whole model. What actually happened was that the module would self-reference, leading to an infinite recursion. With this fix, we now detect this degenerate case and raise a proper error. Note that I discovered this only by accident while working on a test with a dummy model. It should be extremely unlikely that any real world use cases would require targeting a parameter on the root model. If we get such reports, we can think about a better solution. --- src/peft/tuners/tuners_utils.py | 9 +++++++++ tests/test_target_parameters.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/peft/tuners/tuners_utils.py b/src/peft/tuners/tuners_utils.py index 85345b6781..6af166e52c 100644 --- a/src/peft/tuners/tuners_utils.py +++ b/src/peft/tuners/tuners_utils.py @@ -1083,6 +1083,15 @@ def strip_base_layer_from_name(module_name): def create_and_replace_param(module_name, key, param_name): # helper function to avoid duplication + if module_name == "": + # nn.Parameters that are registered directly on the top-level module (i.e. the module passed to + # get_peft_model) cannot be targeted. Wrapping the parameter would require replacing the module that + # holds it with lora.ParamWrapper, but that module is its own parent, so the wrapper ends up registered + # as a submodule of the very module it wraps. This creates a cyclic module graph, resulting in an error. + raise ValueError( + f"Targeting an nn.Parameter on the top-level module is not supported (parameter '{param_name}'). " + ) + parent, target, target_name = _get_submodules(model, module_name) unwrapped_module_name = strip_base_layer_from_name(module_name) unwrapped_module = model.get_submodule(unwrapped_module_name) diff --git a/tests/test_target_parameters.py b/tests/test_target_parameters.py index 6a31e8aa78..af97074f98 100644 --- a/tests/test_target_parameters.py +++ b/tests/test_target_parameters.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + import pytest import torch from torch import nn @@ -598,3 +600,20 @@ def test_target_parameter_init_does_not_warn_about_unknown_layer_type(self, recw warn_messages = (w.message.args[0] for w in recwarn.list) msg_start = "Unsupported layer type" assert not any(msg.startswith(msg_start) for msg in warn_messages) + + def test_target_parameter_on_top_level_module_raises(self): + # nn.Parameters that are registered directly on the top-level module (i.e. the module passed to get_peft_model) + # cannot be targeted. Wrapping the parameter would require replacing the module that holds it with + # lora.ParamWrapper, but that module is its own parent, so the wrapper ends up registered as a submodule of the + # very module it wraps. This creates a cyclic module graph, resulting in an error. + + class MyModule(nn.Module): + # module with a 2d and a 3d nn.Parameter registered directly on the top-level module + def __init__(self): + super().__init__() + self.param = nn.Parameter(torch.zeros(10, 10)) + + config = LoraConfig(target_parameters=["param"]) + msg = re.escape("Targeting an nn.Parameter on the top-level module is not supported (parameter 'param')") + with pytest.raises(ValueError, match=msg): + get_peft_model(MyModule(), config)