这个报错通常落在两类里:一类是键名对不上(检查点里有的层模型里没有,或模型要的层检查点里没有),另一类是键名对得上但张量形状不同。区分办法是先看报错文本本身的措辞,再用一小段脚本把两侧的键名和形状都打印出来,多数情况下不用猜就能定位到具体哪一层。
报错里出现 Missing key(s) / Unexpected key(s),多半是键名或前缀问题;出现 size mismatch,则是形状问题,两者可能同时出现。建议先用 load_state_dict(strict=False) 把 missing_keys、unexpected_keys 打出来,再逐键对比形状,最后决定是写键名映射还是回训练脚本重新导出检查点。改键名前务必确认两侧层语义一致,不要为了消除报错强行对齐。
完整阅读报错栈,定位是 load_state_dict 还是其他环节
PyTorch 的加载报错一般由 load_state_dict 抛出,但栈顶未必是它。从上往下读,找到最后一帧出现在你自己代码里的那一行,通常就是调用加载的地方。常见的几类文本可以先对照:
RuntimeError: Error(s) in loading state_dict for XXX: Missing key(s) in state_dict: "..."—— 模型需要的键在检查点里没找到,属于键名问题。RuntimeError: ... Unexpected key(s) in state_dict: "..."—— 检查点里有模型不认识的键,同样属于键名问题,常见于多了module.或_orig_mod.前缀。RuntimeError: ... size mismatch for xxx.weight: copying a param with shape torch.Size([A, B]) from checkpoint, the shape in current model is torch.Size([C, D])—— 键名对上了,形状不对。ModuleNotFoundError或_pickle.UnpicklingError—— 这不是键名也不是形状问题,多半是保存时的环境依赖缺失或检查点损坏,需要先解决反序列化。
如果栈里出现的是你自己的 load_checkpoint()、from_pretrained() 之类的封装函数,就要再往里找一步,确认它内部到底调了哪个 load_state_dict。有些封装会在加载前做键名重写,报错里的名字可能已经被改过,这时以封装函数内部的原始调用为准。
打印检查点中的键名和模型 state_dict 的键名
先不要改任何模型代码,只做对比。把检查点解出成 state_dict,和 model.state_dict() 都取到,然后用集合运算找差异。下面的片段放在模型实例化之后、加载之前执行:
import torch
ckpt = torch.load(ckpt_path, map_location="cpu")
# 不同项目保存结构不同,按实际取
sd = ckpt.get("state_dict", ckpt.get("model", ckpt))
model_sd = model.state_dict()
ckpt_keys = set(sd.keys())
model_keys = set(model_sd.keys())
print("模型需要但检查点缺失:", sorted(model_keys - ckpt_keys))
print("检查点多余但模型不用:", sorted(ckpt_keys - model_keys))
print("两边都有:", len(ckpt_keys & model_keys))
# 看前缀分布,判断是不是整体多了一层包装
from collections import Counter
print(Counter(k.split(".")[0] for k in ckpt_keys))
如果「模型需要但缺失」和「检查点多余」数量都很大,而且多出来的键普遍带同一个前缀(比如 module.、model.),基本可以判定是保存时外面包了一层容器,属于键名映射问题,不需要动模型结构。如果两侧差集都为空,那键名是一致的,问题在形状,可以直接进下一节。
检查形状不匹配的键,对比检查点与模型中的张量形状
对交集里的每个键逐个比形状,只打印不一致的,输出量小、便于定位:
mismatch = []
for k in sorted(ckpt_keys & model_keys):
a = tuple(sd[k].shape)
b = tuple(model_sd[k].shape)
if a != b:
mismatch.append((k, a, b))
for k, a, b in mismatch:
print(f"{k}\n ckpt {a}\n model {b}")
print("形状不一致键数:", len(mismatch))
看维度时要分几种情况:
- 维度数相同但数值互换,例如检查点是
[vocab, hidden]、模型是[hidden, vocab],通常是保存时做过转置,或两侧对权重矩阵的朝向约定不同。 - 维度数不同,例如检查点
[N]、模型[1, N],多半是保存时多做了unsqueeze或少做了squeeze。 - 隐层维度整体不同(如 4096 对 5120),说明检查点和当前模型配置不是同一规格,改形状没有意义,要换匹配的配置或换检查点。
- 只剩一个维度不同且等于并行切分倍数,可能检查点是被张量并行或流水并行切过的分片,需要先合并再加载。
顺手也确认一下:某个键只在检查点里出现(键名问题)和某个键两边都有但形状不同(形状问题),要分开处理,不要混在一起改。
尝试用 strict=False 加载并记录缺失和多余键
strict=False 不会因为键名差异直接抛错,适合在不改动权重文件的前提下先看清缺了哪些层。调用方式:
res = model.load_state_dict(sd, strict=False)
print("missing_keys:", res.missing_keys)
print("unexpected_keys:", res.unexpected_keys)
解读方式:missing_keys 是模型有、检查点没给的键,这些层会保持当前初始化值,如果是主干层没加载上,推理结果基本不可用;unexpected_keys 是检查点有、模型不用的键,单独多出一些一般不影响运行,但可能意味着真的少加载了对应模块。
还有一种情况要留意:键名完全一致、strict=False 也没报错,但权重其实没被替换。判断办法是加载前后对同一层取一次张量做对比,例如:
before = model_sd["some_layer.weight"].clone()
model.load_state_dict(sd, strict=False)
after = model.state_dict()["some_layer.weight"]
print("该层是否变化:", not torch.equal(before, after))
如果没有变化,说明这一层没被真正写入,需要回头检查传入的 sd 是否就是期望的那份。
根据差异决定是改键名映射还是重新导出检查点
差异类型决定修复方向,常见处理分三类:
- 只多前缀:先写一个去掉前缀的重建逻辑,映射后再加载。
new_sd = {k[len("module."):] if k.startswith("module.") else k: v for k, v in sd.items()}
如果是反向缺失(模型需要带前缀的键),就把前缀补上。做完这一步重新跑 strict 加载确认键集合一致。 - 个别层名不同:维护一张显式映射表,例如
{"encoder.block.0": "transformer.layers.0"},把检查点的键按表转换后重建新的 state_dict。映射前要确认两侧层的语义确实对应,层级顺序、参数角色都要对得上,只凭名字相似去对齐容易埋坑。 - 形状不同:优先考虑从训练脚本重新导出检查点,尤其是隐层维度、词表大小不一致的情况,硬改形状会破坏权重含义。若是并行分片保存的检查点,先在保存侧合并成完整权重再加载,比在加载侧拼接更可控。
无论走哪条路,改完后都要做一次端到端验证:用一条固定输入跑一次前向,检查输出是否合理,例如 logits 不出现大面积 NaN、动作或分类结果落在正常范围;如果手上还留着旧环境的输出,可以把同一输入的两个结果放在一起比对。只有加载不再报错、且推理结果符合预期,才说明修复到位,单单消除报错并不等于加载正确。