rename_f 之后前缀出现两次
主线位置:推理主线第二步「建模块」,手写一个子模块,权重文件的键名带着
torch.compile的_orig_mod.前缀。
版本:PyTorch 2.15.0 / transformers 5.22.0(CPU)、candle 0.11.0(CPU,accelerate)
sequenceDiagram participant M as Mlp load participant O as 外层 VarBuilder participant F as 闭包 participant I as 里层 VarBuilder M->>O: get gate_proj O->>F: 拼上前缀后的全名 F->>I: 加上 _orig_mod. I->>I: 再拼一次前缀 I-->>M: cannot find tensor
实验代码:PyTorch 侧 make_compiled.py,candle 侧 crate pitb-exp 的 bin p18_prefix(release)。权重是本地的 SmolLM2-135M-Instruct。平台 M2 Max、macOS 27、rustc 1.99.0。rename_f 的一般用法见键名对不上。
PyTorch 的写法
torch.compile 包过的模型,state_dict 的每个键都多了 _orig_mod.。存下来,再在 PyTorch 里读回:
compiled = torch.compile(model)
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"})
# PyTorch 的习惯:在字典上改键名,然后整个模型 load_state_dict
fresh = AutoModelForCausalLM.from_pretrained(model_dir, dtype=torch.float32).eval()
r = fresh.load_state_dict({k.removeprefix("_orig_mod."): v for k, v in sd.items()}, strict=False)
x = torch.linspace(-1, 1, 576).reshape(1, 576)
y = fresh.model.layers[0].mlp(x)
keys: ['_orig_mod.model.embed_tokens.weight'] ... 272
load_state_dict: _IncompatibleKeys(missing_keys=['lm_head.weight'], unexpected_keys=[])
layer0 mlp y[:4]: [-80.592041, -6.956567, 60.557873, 40.253838]
(lm_head.weight 列为缺失是因为它和词嵌入绑定、存的时候去掉了。)PyTorch 里改名发生在字典上,和模块的层级无关:键名是一个完整的字符串,removeprefix 一次就完事。
搬到 candle 的现象
candle 里改名是在 VarBuilder 上挂一个闭包。只搬第 0 层的 MLP,先 pp 到这一层,再挂上「加 _orig_mod.」的闭包,看起来很自然:
let add_prefix = move |name: &str| {
log.lock().map(|mut s| s.push(name.to_string())).ok();
format!("_orig_mod.{name}")
};
let vb = root.pp("model.layers.0.mlp").rename_f(add_prefix.clone());
println!("pp then rename_f: {:?}", Mlp::load(vb).err().map(|e| e.to_string()));
println!("closure saw: {:?}", seen.lock().map_err(|_| anyhow!("poisoned"))?.last());
Mlp::load 里是三个 linear_no_bias(.., vb.pp("gate_proj")) 之类。输出:
pp then rename_f: Some("cannot find tensor model.layers.0.mlp._orig_mod.model.layers.0.mlp.gate_proj.weight")
closure saw: Some("model.layers.0.mlp.gate_proj.weight")
闭包收到的是完整、正确的名字,返回的是 _orig_mod.model.layers.0.mlp.gate_proj.weight,正好是文件里的键;真正去查的却在前面又多了一个 model.layers.0.mlp.。
原因
VarBuilder::rename(rename_f 只是把闭包装箱后调它)把当前的 VarBuilder 整个包进 Rename 后端,新的 VarBuilder 又复制了同一个前缀 path:
pub fn rename<R: Renamer + Send + Sync + 'a>(self, renamer: R) -> Self {
let dtype = self.dtype();
let device = self.device().clone();
let path = self.path.clone();
let backend = Rename::new(self, renamer);
// ...
Self {
data: Arc::new(data),
dtype,
path,
_phantom: std::marker::PhantomData,
}
Rename 的 get 拿到外层拼好的全名,过一遍闭包,再交给里面那个 VarBuilder 的 get_with_hints_dtype,而里面那个还带着 model.layers.0.mlp 这个前缀,又拼一次:
let name = self.renamer.rename(name);
self.inner
.get_with_hints_dtype(s, &name, h, dtype)?
.to_device(dev)
在根 VarBuilder 上调用时前缀是空的,拼两次也看不出来;rename_f 的文档注释里的例子正是在根上调用的。这个行为从 rename_f 加进来(#2052)就是这样,origin/main 上 var_builder.rs 和 0.11.0 相同。我认为是上游的 bug:rename 包进去的应该是去掉前缀的 VarBuilder。
改法
顺序反过来:先在根上 rename_f,再 pp:
let vb = root.rename_f(add_prefix).pp("model.layers.0.mlp");
let mlp = Mlp::load(vb)?;
let y = mlp.forward(x)?;
rename_f then pp: y[:4]=[-80.59204, -6.956567, 60.557873, 40.253838] max|diff|=1.53e-5 (max|ref| 230.2)
和 PyTorch 的第 0 层 MLP 输出差 1.53e-5,输出最大 230.2,相对差 6.6e-8,是 f32 矩阵乘求和顺序的量级(容差按相对 1e-6)。pp 在 rename_f 之后,前缀只在外层;Rename 里包着的是根,前缀为空。
一个项目里只在一处(加载入口)对根 VarBuilder 调 rename_f,把全部改名规则写在这一个闭包里,子模块只拿 pp 过的 VarBuilder,就不会碰到这个问题。
怎么检查
vb.prefix()不为空时不要调rename_f。可以写成一个只接受根的辅助函数,在里面assert!(vb.prefix().is_empty())。- 报错里的名字有一段重复(
a.b.X.a.b.c)就是这个坑。闭包里记一下收到的名字(上面的log)能确认闭包本身没错。 - 加载完用记账的
rename_f和文件里的键做差集,见键名对不上的「自己找多余的键」。
小结
- PyTorch 在
state_dict字典上改键名,和模块层级无关;candle 在VarBuilder上挂闭包。 vb.pp("x").rename_f(f)让前缀x在外层和Rename里层各拼一次,报cannot find tensor x.<f 的结果>,闭包收到的名字是对的。- 改法:
root.rename_f(f).pp("x"),SmolLM2 第 0 层 MLP 和 PyTorch 差 1.53e-5(最大 230.2)。 origin/main上没有改;在根上调用就碰不到。