System Info
peft main @ cea8213 (0.19.2.dev0), torch 2.13, transformers 5.13, CPU-only.
Who can help?
This is a state-dict save/load correctness issue in modules_to_save handling.
Reproduction
A modules_to_save module whose name contains the adapter's parameter prefix (for LoRA, lora_) is silently not restored on load. The clearest example is a module literally named lora_head:
import copy, torch
from torch import nn
from peft import LoraConfig, get_peft_model, get_peft_model_state_dict, set_peft_model_state_dict
class ModelWithLoraNamedHead(nn.Module):
def __init__(self):
super().__init__()
self.body = nn.Linear(2, 2, bias=False)
self.lora_head = nn.Linear(2, 2, bias=False) # ordinary module, name contains "lora_"
def forward(self, x):
return self.lora_head(self.body(x))
config = LoraConfig(target_modules=["body"], modules_to_save=["lora_head"])
source = get_peft_model(ModelWithLoraNamedHead(), config)
with torch.no_grad():
source.base_model.model.lora_head.modules_to_save["default"].weight.fill_(7.0)
state_dict = get_peft_model_state_dict(source)
target = get_peft_model(ModelWithLoraNamedHead(), copy.deepcopy(config))
result = set_peft_model_state_dict(target, state_dict)
print("unexpected keys:", [k for k in result.unexpected_keys if "lora_head" in k])
loaded = target.base_model.model.lora_head.modules_to_save["default"].weight
print("weight restored:", bool(torch.allclose(loaded, torch.full_like(loaded, 7.0))))
Output:
unexpected keys: [
'base_model.model.lora_head.default.original_module.weight',
'base_model.model.lora_head.default.modules_to_save.weight',
'base_model.model.lora_head.default.modules_to_save.default.weight',
]
weight restored: False
The trained lora_head weight stays at its initialization, and because loading is non-strict the mismatch is silent — a caller that doesn't inspect result.unexpected_keys keeps running with the wrong head weights.
Expected behavior
The modules_to_save weight should round-trip regardless of the module's name, as it does for any name that doesn't contain lora_.
Root cause
In src/peft/utils/save_and_load.py, _insert_adapter_name_into_state_dict decides whether a key belongs to the adapter with a substring test:
if parameter_prefix in key: # parameter_prefix == "lora_"
For a modules_to_save module named lora_head, "lora_" in "...lora_head..." is True, so the key is treated as a LoRA parameter and the adapter name is spliced in at the wrong position. A prefix/boundary-aware check (rather than an unanchored in) would classify these keys correctly.
Notes
I came across this while auditing save_and_load.py for edge cases; it needs a module name that contains the lora_ prefix, so it's narrow in practice, but the result is a silent wrong-weight load. I have a small fix along the lines above plus a regression test ready. Per .ai/AGENTS.md, I wanted to check in first rather than open a PR directly — would you welcome one? (Disclosure: the audit and this write-up were done with AI assistance; the reproduction above was run against the real editable checkout.)
System Info
peft
main@cea8213(0.19.2.dev0), torch 2.13, transformers 5.13, CPU-only.Who can help?
This is a state-dict save/load correctness issue in
modules_to_savehandling.Reproduction
A
modules_to_savemodule whose name contains the adapter's parameter prefix (for LoRA,lora_) is silently not restored on load. The clearest example is a module literally namedlora_head:Output:
The trained
lora_headweight stays at its initialization, and because loading is non-strict the mismatch is silent — a caller that doesn't inspectresult.unexpected_keyskeeps running with the wrong head weights.Expected behavior
The
modules_to_saveweight should round-trip regardless of the module's name, as it does for any name that doesn't containlora_.Root cause
In
src/peft/utils/save_and_load.py,_insert_adapter_name_into_state_dictdecides whether a key belongs to the adapter with a substring test:For a
modules_to_savemodule namedlora_head,"lora_" in "...lora_head..."isTrue, so the key is treated as a LoRA parameter and the adapter name is spliced in at the wrong position. A prefix/boundary-aware check (rather than an unanchoredin) would classify these keys correctly.Notes
I came across this while auditing
save_and_load.pyfor edge cases; it needs a module name that contains thelora_prefix, so it's narrow in practice, but the result is a silent wrong-weight load. I have a small fix along the lines above plus a regression test ready. Per.ai/AGENTS.md, I wanted to check in first rather than open a PR directly — would you welcome one? (Disclosure: the audit and this write-up were done with AI assistance; the reproduction above was run against the real editable checkout.)