【Bug已解决】Discrepancy between layers names from `get_peft_model` and `.load_adapter` 解决方案 【Bug已解决】Discrepancy between layers names fromget_peft_modeland.load_adapter解决方案一、现象长什么样在 PEFT 里我们通常会做两件事用get_peft_model(base, config)当场把 LoRA 注入到一个基座模型上训练完用peft_model.save_pretrained(lora_a)存盘之后在另一个进程里用PeftModel.from_pretrained(base2, lora_a)或者peft_model.load_adapter(lora_a, adapter_namelora_a)把权重加载回来。问题出现在加载回来的 adapter 权重键名和当初get_peft_model注入时产出的键名对不上。具体表现有几种打印state_dict()的键会发现保存前是base_model.model.fc1.lora_A.default.weight加载后却变成了base_model.model.base_model.model.fc1.lora_A.lora_a.weight加载时抛出KeyError: base_model.model.fc1.lora_A.default.weight提示缺失键或者更隐蔽权重都能塞进去但load_state_dict静默把lora_A落到了错误的模块上推理结果和训练时完全不一致在已经是一个PeftModel的模型上再调用load_adapter叠加第二个 adapter 时前缀会嵌套成两层base_model.model.base_model.model...导致第二次加载直接size mismatch。这个不一致的本质是注入inject阶段和加载load阶段对“目标模块路径”的命名规则不完全一致尤其在嵌套 PeftModel、自定义模型、带modules_to_save、device_map多卡、量化模型这几种场景下会被放大。二、背景要理解这个 bug得先搞清楚 PEFT 在内存里到底把 LoRA 放在哪、用什么样的键名。get_peft_model做的事是遍历基座模型凡是模块名命中target_modules的nn.Linear/Conv1d等就用一个lora.Linear把原层包起来原层作为base_layer属性挂在新层下面。此时参数名长这样以fc1为例base_model.model.fc1.base_layer.weight # 冻结的原始权重 base_model.model.fc1.base_layer.bias base_model.model.fc1.lora_A.default.weight # LoRA 的 A 矩阵 base_model.model.fc1.lora_B.default.weight # LoRA 的 B 矩阵注意前缀是base_model.model.其中第一个base_model是PeftModel外层包装第二个model是基座模型本身的属性名多数 HF 模型把子模块挂在.model上。save_pretrained会把lora_A.default/lora_B.default这些键连同adapter_config.json写进磁盘这里default是默认 adapter 名。而load_adapter(path, adapter_nameX)的逻辑是先读取磁盘上的adapter_config.json拿到target_modules然后重新在当前的必须是 PeftModel 的基础上按规则注入一个名叫X的 adapter。注入时它生成的键名是lora_A.X.weight而不是保存时的lora_A.default.weight。于是矛盾就来了如果保存时默认 adapter 叫default加载时你指定的adapter_name叫lora_a那键名从default变成lora_a直接对不上如果你加载到的目标已经是一个PeftModel比如你在另一个 LoRA 上叠加前缀又会多套一层base_model.model.导致键完全错位from_pretrained则会用保存时的adapter_name读 config 里的active_adapter去对齐所以通常比load_adapter更稳。下面用可运行的代码把三种情况逐一复现。三、根因根因可以归纳为一句话get_peft_model注入阶段的键名前缀和 adapter 名与load_adapter加载阶段的键名前缀和 adapter 名是两套各自独立计算出来的规则它们只在“都是默认 adapter、且基座未被二次包装”的最简单情况下才恰好相等。拆开看有三条具体的错配路径adapter 名不一致保存时的active_adapter是default加载时load_adapter用你传入的adapter_name。两者不同 → 键里default与X对不上。前缀嵌套在已经是PeftModel的模型上再load_adapter目标模块遍历的起点变成了外层的base_model.model于是新键比原键又多了一层base_model.model.变成base_model.model.base_model.model.fc1...。目标模块解析差异get_peft_model是在原始基座上解析target_modules而load_adapter是在可能已被包装过的当前模型上再次解析。若中间模型被prepare_model_for_kbit_training、被device_map拆层、或被自定义forward改动过子模块结构两次解析命中的模块集合可能不同于是键集合也不同。这三点叠加就让“保存前能跑、加载后跑不对”成了一个高频坑。四、最小可运行复现下面这段脚本不依赖任何大模型权重用一个 10 行的小模型就能稳定复现键名错配。请确保torch与peft已安装pip install torch peft。import torch import torch.nn as nn from peft import get_peft_model, LoraConfig, PeftModel class TinyModel(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(16, 16) self.fc2 nn.Linear(16, 4) def forward(self, x): return self.fc2(torch.relu(self.fc1(x))) def show_lora_keys(model, tag): keys [k for k in model.state_dict().keys() if lora in k] print(f\n--- {tag} ---) for k in keys: print( , k) # 1) 用 get_peft_model 注入键名里是 default 前缀 base TinyModel() cfg LoraConfig( r4, lora_alpha8, target_modules[fc1, fc2], lora_dropout0.0, biasnone, ) peft get_peft_model(base, cfg) show_lora_keys(peft, get_peft_model 注入后) # 2) 保存 peft.save_pretrained(lora_a_demo) # 3) 在另一个进程/另一个 base 上用 from_pretrained 加载推荐做法 base2 TinyModel() loaded PeftModel.from_pretrained(base2, lora_a_demo) show_lora_keys(loaded, from_pretrained 加载后) # 4) 在已经加载的 PeftModel 上再用 load_adapter 叠加 前缀嵌套 名字错配 base3 TinyModel() first PeftModel.from_pretrained(base3, lora_a_demo) first.load_adapter(lora_a_demo, adapter_namelora_a) # 故意换名字 show_lora_keys(first, load_adapter 叠加后错配)运行后你会看到第一组键是...fc1.lora_A.default.weight第二组from_pretrained键名保持一致仍然是...fc1.lora_A.default.weight第三组load_adapter 叠加出现了...fc1.lora_A.lora_a.weight且如果你打开的是已经被包装两次的结构前缀会变成base_model.model.base_model.model.fc1...。只要加载阶段的adapter_name和保存阶段不一致或者前缀被嵌套load_state_dict就会报缺失键或把权重塞错模块。五、解决方案第一层最小直接修复最直接、最稳的做法是加载时优先用PeftModel.from_pretrained并且不要随意给load_adapter换一个和保存时不同的 adapter 名。修复 1用 from_pretrained 代替 load_adapterbase TinyModel() model PeftModel.from_pretrained(base, lora_a_demo) model model.merge_and_unload() # 推理前合并键名自动归一from_pretrained会读取磁盘adapter_config.json里的active_adapter用它去对齐键名因此默认就能对上default。修复 2如果必须用 load_adapter保持 adapter 名一致保存时如果默认名是default加载时也用defaultbase TinyModel() peft_base get_peft_model(base, cfg) peft_base.load_adapter(lora_a_demo, adapter_namedefault) # 与保存时同名 peft_base.set_adapter(default)修复 3加载前先打印并核对键集合import os def keys_of_saved(path): from safetensors.torch import load_file f os.path.join(path, adapter_model.safetensors) if os.path.exists(f): return set(load_file(f).keys()) # 退回 bin f os.path.join(path, adapter_model.bin) return set(torch.load(f, map_locationcpu).keys()) saved_keys keys_of_saved(lora_a_demo) print(磁盘键数量:, len(saved_keys)) for k in sorted(saved_keys): print( , k)先把磁盘键打出来再比对加载后模型的state_dict()键肉眼确认default/lora_a与base_model.model前缀是否一致是最快定位错配的方法。六、解决方案第二层结构性改进当你的流程需要在同一个基座上叠加多个 adapter比如 A 训练完、再加载 B 做对比实验应当从结构上避免前缀嵌套和名字冲突。改进 1用 add_adapter 显式命名而不是反复 load_adapterfrom peft import PeftModel base TinyModel() m get_peft_model(base, cfg) # 这是 adapter default # 想叠加第二个用 add_adapter 显式给新名字并保存时指定同名 second_cfg LoraConfig( r4, lora_alpha8, target_modules[fc1, fc2], lora_dropout0.0, biasnone, ) m.add_adapter(exp_b, second_cfg) # 新 adapter 名 exp_b m.set_adapter(exp_b) m.save_pretrained(lora_b_demo) # 存的是 exp_b # 之后加载名字要对上 m2 PeftModel.from_pretrained(TinyModel(), lora_b_demo) m2.set_adapter(exp_b) # 与保存名一致改进 2写一个键名归一化函数统一两套命名当你必须把“别人的、名字不统一的 adapter”塞进自己的模型时不要手动改文件而是写个映射器在内存里重命名def normalize_adapter_keys(state_dict, src_namedefault, dst_namedefault): 把 src_name 的 LoRA 键改成 dst_name并消除多余前缀。 new {} for k, v in state_dict.items(): nk k # 去掉可能多出来的嵌套前缀 nk nk.replace(base_model.model.base_model.model., base_model.model.) # 替换 adapter 名 if flora_A.{src_name}. in nk: nk nk.replace(flora_A.{src_name}., flora_A.{dst_name}.) if flora_B.{src_name}. in nk: nk nk.replace(flora_B.{src_name}., flora_B.{dst_name}.) new[nk] v return new # 用法加载别人给的权重前先归一 raw torch.load(foreign_adapter/adapter_model.bin, map_locationcpu) fixed normalize_adapter_keys(raw, src_namedefault, dst_namemy_adapter) missing, unexpected model.load_state_dict(fixed, strictFalse) print(missing:, missing) print(unexpected:, unexpected)注意load_state_dict(..., strictFalse)一定要打印missing和unexpected否则错配会被静默吞掉——这正是“能加载但推理不对”的根源。改进 3自定义模型统一 target_modules 命名如果你用的是非标准模型子模块不挂在.model下或层名带序号前缀在LoraConfig里用完整模块名列表而不是模糊子串能减少两次解析结果不一致cfg LoraConfig( r4, lora_alpha8, # 明确写出完整路径避免两次解析命中不同集合 target_modules[ encoder.block.0.layer.0.SelfAttention.q, encoder.block.0.layer.0.SelfAttention.v, ], lora_dropout0.0, )七、解决方案第三层断言 / CI 守护把“加载前后键名必须能对齐”变成一道不可跳过的断言能在 CI 里拦住任何一次破坏命名规则的改动。import torch import pytest from safetensors.torch import load_file from peft import get_peft_model, LoraConfig, PeftModel def _saved_keys(path): import os f os.path.join(path, adapter_model.safetensors) return set(load_file(f).keys()) def _adapter_keys(model, adapter_name): return { k for k in model.state_dict().keys() if lora in k and f.{adapter_name}. in k } def test_load_keys_align_with_saved(): class M(torch.nn.Module): def __init__(self): super().__init__() self.fc1 torch.nn.Linear(16, 16) self.fc2 torch.nn.Linear(16, 4) def forward(self, x): return self.fc2(torch.nn.functional.relu(self.fc1(x))) cfg LoraConfig(r4, lora_alpha8, target_modules[fc1, fc2], lora_dropout0.0) peft get_peft_model(M(), cfg) peft.save_pretrained(ci_lora) loaded PeftModel.from_pretrained(M(), ci_lora) saved {k for k in _saved_keys(ci_lora) if lora in k} got _adapter_keys(loaded, default) # 去掉前缀差异后集合应当相等 def strip(k): return k.replace(base_model.model., ) assert {strip(k) for k in saved} {strip(k) for k in got} def test_no_nested_prefix(): class M(torch.nn.Module): def __init__(self): super().__init__() self.fc1 torch.nn.Linear(16, 16) def forward(self, x): return self.fc1(x) cfg LoraConfig(r4, lora_alpha8, target_modules[fc1], lora_dropout0.0) m PeftModel.from_pretrained(M(), ci_lora) m.load_adapter(ci_lora, adapter_namedefault) for k in m.state_dict().keys(): assert base_model.model.base_model.model. not in k, f嵌套前缀: {k}这两个测试分别守护“键集合对齐”和“无前缀嵌套”。只要有人在保存/加载路径上改了命名规则CI 立刻变红。八、排查清单遇到 LoRA 加载键名对不上按下面顺序查先打印磁盘键用safetensors.torch.load_file或torch.load读出adapter_model.*的键确认里面是default还是别的名字。对比加载后键打印model.state_dict()里含lora的键看default/X与base_model.model前缀是否一致。确认加载方式是从零from_pretrained推荐还是在已有PeftModel上load_adapter易嵌套确认 adapter 名load_adapter(..., adapter_name...)传入的名字必须和保存时active_adapter一致不确定就打印model.peft_config看。load_state_dict(strictFalse)必须看返回值missing和unexpected任意非空都说明错配不要忽略。多卡 / 量化场景device_map、load_in_4bit会改变模块解析顺序保存与加载要完全一致。modules_to_save一并检查带modules_to_save时键里还有original_module和modules_to_save.default加载阶段若不重建该 wrapper 也会缺键。用归一化函数兜底跨来源 adapter 用第六节normalize_adapter_keys先重命名再加载。九、小结get_peft_model注入阶段和load_adapter加载阶段对 LoRA 权重的键名前缀base_model.model.与 adapter 名default/自定义名是各自独立计算的只在“默认 adapter 且基座未被二次包装”的最简情形下才恰好相等。一旦你换了adapter_name、在已有PeftModel上叠加、或用了device_map/量化/自定义模型键名就会错配表现为KeyError、前缀嵌套、或“能加载但推理不对”。最小修复是优先用PeftModel.from_pretrained、保持adapter_name与保存时一致、加载后用strictFalse并核对 missing/unexpected结构性改进是用add_adapter显式管理多 adapter、写一个键名归一化映射器、对自定义模型用完整模块路径最后用 CI 断言守护“键集合对齐”和“无前缀嵌套”把这类命名回归挡在合并之前。