Skip to content

modules_to_save module whose name contains the LoRA prefix ("lora_") is silently not restored on load #3433

Description

@zelinewang

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.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions