LingBot-VLA 2.0 加载检查点报错——是键名不匹配还是权重形状不同

文章导读
这个报错通常落在两类里:一类是键名对不上(检查点里有的层模型里没有,或模型要的层检查点里没有),另一类是键名对得上但张量形状不同。区分办法是先看报错文本本身的措辞,再用一小段脚本把两侧的键名和形状都打印出来,多数情况下不用猜就能定位到具体哪一层。
📋 目录
  1. 壹 完整阅读报错栈,定位是 load_state_dict 还是其他环节
  2. 贰 打印检查点中的键名和模型 state_dict 的键名
  3. 叁 检查形状不匹配的键,对比检查点与模型中的张量形状
  4. 肆 尝试用 strict=False 加载并记录缺失和多余键
  5. 伍 根据差异决定是改键名映射还是重新导出检查点
A A

这个报错通常落在两类里:一类是键名对不上(检查点里有的层模型里没有,或模型要的层检查点里没有),另一类是键名对得上但张量形状不同。区分办法是先看报错文本本身的措辞,再用一小段脚本把两侧的键名和形状都打印出来,多数情况下不用猜就能定位到具体哪一层。

报错里出现 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.),基本可以判定是保存时外面包了一层容器,属于键名映射问题,不需要动模型结构。如果两侧差集都为空,那键名是一致的,问题在形状,可以直接进下一节。

LingBot-VLA 2.0 加载检查点报错——是键名不匹配还是权重形状不同

检查形状不匹配的键,对比检查点与模型中的张量形状

对交集里的每个键逐个比形状,只打印不一致的,输出量小、便于定位:

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 是检查点有、模型不用的键,单独多出一些一般不影响运行,但可能意味着真的少加载了对应模块。

LingBot-VLA 2.0 加载检查点报错——是键名不匹配还是权重形状不同

还有一种情况要留意:键名完全一致、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、动作或分类结果落在正常范围;如果手上还留着旧环境的输出,可以把同一输入的两个结果放在一起比对。只有加载不再报错、且推理结果符合预期,才说明修复到位,单单消除报错并不等于加载正确。