键名对不上
主线位置:推理主线第一步「读 config 和权重」的最后一关:文件里的键名、形状和模型代码要的对不上。
版本:PyTorch 2.15.0 / transformers 5.22.0 / safetensors 0.8.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
m["模型要 名字 形状"] --> rn["rename_f 闭包"]
rn --> f{"文件里有吗"}
f -->|没有| e1["cannot find tensor"]
f -->|有| s{"形状对吗"}
s -->|不对| e2["shape mismatch"]
s -->|对| ok["Tensor"]
e1 --> blk{"在层里吗"}
e2 --> blk
blk -->|是| pn["Llama load panic"]
blk -->|否| er["Err"]
实验代码:PyTorch 侧 rename_py.py、strict_py.py,candle 侧 crate weights-exp 的 bin rename(release)。权重是本地的 SmolLM2-135M-Instruct。平台 M2 Max、macOS 27、rustc 1.99.0。VarBuilder 怎么拼出完整名字,见源码卷的 VarBuilder;前缀拼错、配置漏字段的几种报错,见 cannot find tensor。
改名:rename_f
PyTorch 里键名多一层前缀很常见。torch.compile 包过的模型就是一例:
compiled = torch.compile(model)
keys = list(compiled.state_dict())
print("compiled keys:", keys[:2], "...", len(keys))
sd = {k: v for k, v in compiled.state_dict().items() if k != "_orig_mod.lm_head.weight"}
save_file(sd, f"{out}/compiled.safetensors", metadata={"format": "pt"})
compiled keys: ['_orig_mod.model.embed_tokens.weight', '_orig_mod.model.layers.0.self_attn.q_proj.weight'] ... 273
torch.compile 返回的 OptimizedModule 把原模型放在 _orig_mod 属性下,state_dict 的每个键就多了 _orig_mod.(这里还没真正编译,state_dict 不触发编译)。DistributedDataParallel 的 module. 是同一回事。PyTorch 里的处理是加载前把键改掉。candle 里改的是另一个方向:rename_f 接一个闭包,模型每要一个名字,先过一遍闭包,得到的才是到文件里查的名字:
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[path], DType::F32, &Device::Cpu)? };
let e = Llama::load(vb.clone(), &cfg).err();
eprintln!("as is: {:?}", e.map(|e| e.to_string()));
let vb = vb.rename_f(|name: &str| format!("_orig_mod.{name}"));
let model = Llama::load(vb, &cfg)?;
as is: Some("cannot find tensor model.embed_tokens.weight")
with rename_f: ok
logits n=49152 max|diff|=2.98e-05
worst=2.98e-05 tol=1e-04 OK
改名后的 logits 和 transformers 差 2.98e-5(最大 |ref| 20.6,容差 1e-4),和不改名读原文件一样。闭包收到的是拼好前缀的完整名字(model.layers.0.self_attn.q_proj.weight),返回值是文件里的完整名字;不需要改的原样返回。老 BERT 的 gamma / beta 也是这样改,见 cannot find tensor embeddings.word_embeddings.weight。
先 rename_f,再 pp
rename_f 要包在根 VarBuilder 上。在 pp 过的 VarBuilder 上调用,前缀会出现两次:
let renamed = root.pp("model").rename_f(move |name: &str| {
log.lock().map(|mut s| s.push(name.to_string())).ok();
name.to_string()
});
eprintln!("prefix of renamed vb: {:?}", renamed.prefix());
let r = renamed.get(576, "norm.weight");
// ...
let r = root.rename_f(|name: &str| name.to_string()).pp("model").get(576, "norm.weight");
prefix of renamed vb: "model"
renamed.get(576, "norm.weight"): Err("cannot find tensor model.model.norm.weight")
closure saw: ["model.norm.weight"]
root.rename_f(..).pp("model").get: Ok([576])
闭包本身什么都没改,收到的也是对的 model.norm.weight,查的却是 model.model.norm.weight。rename 把当前的 VarBuilder(连同它的前缀 path)整个包进 Rename 后端,新的 VarBuilder 又复制了同一个前缀:外层拼一次,交给闭包,里层 get_with_hints_dtype 再拼一次。这个行为从 rename_f 加进来(#2052)就是这样,origin/main 上没变;我认为是上游的 bug,文档注释的例子只在根上调用,碰不到它。
绑定权重:tie_word_embeddings
SmolLM2 的 config.json 写着 "tie_word_embeddings": true:输出层 lm_head 和词嵌入用同一个矩阵。PyTorch 里是同一个 Parameter,state_dict 里却有两个键:
tied: True
state_dict keys: 273 has lm_head.weight: True
(tied 是 m.lm_head.weight.data_ptr() == m.model.embed_tokens.weight.data_ptr()。)存成 safetensors 时,两个键指着同一块内存,各个工具的处理不同:
try:
save_file(model.state_dict(), f"{out}/tied_save_file.safetensors")
except Exception as e:
print("save_file:", type(e).__name__, str(e)[:400])
save_model(model, f"{out}/tied_save_model.safetensors")
save_file: RuntimeError
Some tensors share memory, this will lead to duplicate memory on disk and potential differences when loading them again: [{'lm_head.weight', 'model.embed_tokens.weight'}].
A potential way to correctly save your model is to use `save_model`.
More information at https://huggingface.co/docs/safetensors/torch_shared_tensors
save_model keys: 272 embed: False lm_head: True
safetensors.torch.save_file拒绝共享内存的张量。save_model去重,留下的是lm_head.weight:safetensors 0.8.0 的_remove_duplicate_names在完整覆盖存储的名字里取sorted(...)[0],l排在m前面。- transformers 的
save_pretrained留的是model.embed_tokens.weight(272 个键,没有lm_head.weight),和原始文件一样。
candle 的 Llama::load 按配置决定要不要 lm_head.weight:
pub fn load(vb: VarBuilder, cfg: &Config) -> Result<Self> {
let wte = embedding(cfg.vocab_size, cfg.hidden_size, vb.pp("model.embed_tokens"))?;
let lm_head = if cfg.tie_word_embeddings {
Linear::from_weights(wte.embeddings().clone(), None)
} else {
linear(cfg.hidden_size, cfg.vocab_size, vb.pp("lm_head"))?
};
// ...
绑定时 lm_head 用的是词嵌入张量的 clone(),Tensor 的 clone 只加一个引用计数(引用与不可变),两者是同一份数据,和 PyTorch 的同一个 Parameter 对应。文件里有没有 lm_head.weight 都不看。
PR #2508(「Add some llama-3.2 examples.」,首次进入 0.7.2)之前,Llama::load 只有一行 let lm_head = linear(cfg.hidden_size, cfg.vocab_size, vb.pp("lm_head"))?;,总是去文件里要 lm_head.weight,LlamaConfig 里也没有 tie_word_embeddings 字段。输出层和词嵌入绑定、文件里没有 lm_head.weight 的模型就报 cannot find tensor lm_head.weight。这个 PR 的提交说明里有一条「Support tie-word-embeddings for llama.」:加了字段,into_config 里用 unwrap_or(false) 补默认值,load 改成上面的 if。这个默认值留到了今天,所以手写配置漏了这个字段,又回到「去要 lm_head.weight」(cannot find tensor)。
反过来,save_model 存的那个文件没有 model.embed_tokens.weight,词嵌入那一步就找不到,用 rename_f 指过去:
let vb = vb.rename_f(|name: &str| match name {
"model.embed_tokens.weight" => "lm_head.weight".to_string(),
_ => name.to_string(),
});
as is: Some("cannot find tensor model.embed_tokens.weight")
with rename_f: ok
logits n=49152 max|diff|=2.98e-05
worst=2.98e-05 tol=1e-04 OK
tie_word_embeddings 设成 false,再用 rename_f 把 lm_head.weight 指到 model.embed_tokens.weight。logits 一样吗?CPU 上 F32 加载的峰值 RSS 比用配置绑定(841 MiB)多多少?缺键、多余的键、形状不对
PyTorch 的 load_state_dict 默认 strict=True,三类问题一次列全。下面的字典删掉了 model.layers.3.mlp.up_proj.weight、改名成 model.layers.3.mlp.up.weight,再把 k_proj 换成一个 [576, 576] 的张量:
sd = load_file(f"{model_dir}/model.safetensors") # 272 个键,没有 lm_head.weight
bad = dict(sd)
del bad["model.layers.3.mlp.up_proj.weight"]
bad["model.layers.3.mlp.up.weight"] = sd["model.layers.3.mlp.up_proj.weight"]
bad["model.layers.0.self_attn.k_proj.weight"] = torch.zeros(576, 576, dtype=torch.bfloat16)
try:
model.load_state_dict(bad)
except RuntimeError as e:
print("strict:", e)
r = model.load_state_dict({k: v for k, v in bad.items() if k != "model.layers.0.self_attn.k_proj.weight"}, strict=False)
print("strict=False:", r)
strict: Error(s) in loading state_dict for LlamaForCausalLM:
Missing key(s) in state_dict: "model.layers.3.mlp.up_proj.weight", "lm_head.weight".
Unexpected key(s) in state_dict: "model.layers.3.mlp.up.weight".
size mismatch for model.layers.0.self_attn.k_proj.weight: copying a param with shape torch.Size([576, 576]) from checkpoint, the shape in current model is torch.Size([192, 576]).
strict=False: _IncompatibleKeys(missing_keys=['model.layers.0.self_attn.k_proj.weight', 'model.layers.3.mlp.up_proj.weight', 'lm_head.weight'], unexpected_keys=['model.layers.3.mlp.up.weight'])
lm_head.weight 也被列为缺失:load_state_dict 只比键,不知道绑定,绑定是 from_pretrained 做的。from_pretrained 本身对缺键更宽容,读一个少了 up_proj 的文件(PyTorch 的输出里有加粗用的终端控制符,这里去掉了):
[transformers] LlamaForCausalLM LOAD REPORT from: ./missing
Key | Status |
----------------------------------+---------+-
model.layers.3.mlp.up_proj.weight | MISSING |
Notes:
- MISSING: those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.
up_proj after load: mean -2.085178675770294e-05 std 0.04164252057671547
模型照样建出来,这个权重按 initializer_range(0.0417)随机初始化(没设种子,每次运行的值不同),标准差 0.0416,接着就能生成,只是输出是错的。
candle 没有 strict 这个开关,缺一个就停,停在第一个:
--- missing
thread 'main' (9059046) panicked at .../candle-transformers-0.11.0/src/models/llama.rs:524:75:
called `Result::unwrap()` on an `Err` value: cannot find tensor model.layers.3.mlp.up_proj.weight
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
--- errors
k_proj.weight: Err: shape mismatch for model.layers.0.self_attn.k_proj.weight, expected: [576, 576], got: [192, 576]
q_proj.weght: Err: cannot find tensor model.layers.0.self_attn.q_proj.weght
- 缺的键在某一层里,
Llama::load的逐层加载是unwrap(),进程 panic(退出码 101);词嵌入、norm、lm_head缺了才是Err。为什么是 panic、服务里怎么兜住,见 cannot find tensor。 - 形状不对是
shape mismatch for <名字>, expected: <模型要的>, got: <文件里的>,和 PyTorch 的size mismatch一样给出两个形状,顺序相反:PyTorch 先说文件、后说模型。 - 每个报错只有一个名字,不提示近似的名字;burn 会列出全部缺失和未用的键,还给「Did you mean」(导入 PyTorch 权重)。
自己找多余的键
candle 也不报 Unexpected key(s):文件里多出来的张量没人要,就一直不读。rename_f 的闭包每次请求都会经过,正好用来记账,再和文件里的名字做差集:
let asked = Arc::new(Mutex::new(BTreeSet::new()));
let log = asked.clone();
let vb = base.rename_f(move |name: &str| {
if let Ok(mut s) = log.lock() {
s.insert(name.to_string());
}
name.to_string()
});
let _model = Llama::load(vb, &cfg)?;
let asked = asked.lock().map_err(|_| anyhow::anyhow!("poisoned"))?;
let unused: Vec<_> = in_file.difference(&asked).collect();
let missing: Vec<_> = asked.difference(&in_file).collect();
in_file 对 safetensors 来自 MmapedSafetensors::tensors(),对 .pt 来自 pickle::read_pth_tensor_info:
model.safetensors: in file 272, requested 272, unused [], missing []
sd_zip.pt: in file 273, requested 272, unused ["lm_head.weight"], missing []
torch.save 存的 state_dict 多一个 lm_head.weight,绑定时没人要它,这是预期的;如果 unused 里出现 up.weight 这种名字,就是改名没改到。闭包要求 Fn + Send + Sync + 'static,所以用 Arc<Mutex<..>> 而不是一个 &mut Vec。
动脑筋答案
tie_config: loaded
logits n=49152 max|diff|=2.98e-05
worst=2.98e-05 tol=1e-04 OK
tie_rename: loaded
logits n=49152 max|diff|=2.98e-05
worst=2.98e-05 tol=1e-04 OK
tie_config 841.3 MiB
tie_config 841.5 MiB
tie_config 843.6 MiB
tie_rename 949.4 MiB
tie_rename 949.4 MiB
tie_rename 949.3 MiB
- logits 一样:两种写法和 transformers 都差 2.98e-5,
lm_head的值是同一组数。 - 峰值多 108 MiB,三次都一样。
tie_word_embeddings为false时,Llama::load走linear(.., vb.pp("lm_head")),rename_f把名字指到词嵌入,VarBuilder又从文件里读一遍、转一遍 F32,得到一份独立的张量:49152 × 576 × 4 字节 = 113246208 字节,正好 108 MiB。 rename_f只改名字,不知道两个名字该共享一份数据;共享要在模型代码里写(Linear::from_weights(wte.embeddings().clone(), None))。
小结
rename_f的闭包把模型要的完整名字翻译成文件里的名字:torch.compile的_orig_mod.、DDP 的module.、save_model只留下的lm_head.weight都这样处理。要在根VarBuilder上调用,pp之后再rename_f前缀会拼两次。- 绑定权重:PyTorch 是同一个
Parameter、state_dict里两个键;candle 的Llama::load按tie_word_embeddings用词嵌入张量的clone(),共享一份。用rename_f指过去也能算对,但多读一份(SmolLM2 F32 多 108 MiB)。 - PyTorch
strict=True一次列出缺失、多余和形状不对的全部键;from_pretrained缺键时随机初始化并打一份报告。candle 停在第一个:cannot find tensor <名字>或shape mismatch for <名字>, expected: .., got: ..,在Llama::load的层里是 panic。 - candle 不报多余的键;用记账的
rename_f和文件里的名字做差集。