Rust in Everything
基于 candle 0.11.0

cannot find tensor model.embed_tokens.weight

主线位置:推理主线第一步「读 config 和权重」,权重是自己用 safetensors 存的,或者是分片的目录。
版本:PyTorch 2.15.0 / transformers 5.22.0 / safetensors 0.8.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
  m["tied 的模型"] --> sm["save_model"]
  sm --> lm["只留 lm_head"]
  lm --> e1["找不到 embed_tokens"]
  e1 -->|"rename_f"| ok["加载成功"]
  sh["分片目录"] --> idx["index.json"]
  idx -->|"当文件传"| e2["header too large"]
  idx -->|"读 weight_map"| ok

实验代码:PyTorch 侧 make_tied.py,candle 侧 crate pitb-exp 的 bin p17_tied(release)。权重是本地的 SmolLM2-135M-Instruct。平台 M2 Max、macOS 27、rustc 1.99.0。绑定权重在两边各是什么,见键名对不上;分片和 mmap,见 safetensors。

PyTorch 的写法

SmolLM2 的输出层和词嵌入是同一个参数(tie_word_embeddings: true)。safetensors.torch.save_file 拒绝存共享内存的张量,报错里推荐 save_model,于是很多人这样存:

model = AutoModelForCausalLM.from_pretrained(model_dir)
print("tied:", model.lm_head.weight.data_ptr() == model.model.embed_tokens.weight.data_ptr())
save_model(model, f"{out}/save_model.safetensors")

另一个常见的存法是 save_pretrained,模型大时它按 max_shard_size 分片:

model.save_pretrained(f"{out}/sharded", max_shard_size="100MB")
re = AutoModelForCausalLM.from_pretrained(f"{out}/sharded")
tied: True
save_model keys: 272 embed_tokens: False lm_head: True
sharded: ['config.json', 'generation_config.json', 'model-00001-of-00003.safetensors', 'model-00002-of-00003.safetensors', 'model-00003-of-00003.safetensors', 'model.safetensors.index.json']
weight_map: 272 files: ['model-00001-of-00003.safetensors', 'model-00002-of-00003.safetensors', 'model-00003-of-00003.safetensors']
from_pretrained(sharded dir) ok: LlamaForCausalLM

PyTorch 一侧两种都读得回来:把模型的词嵌入清零后用 safetensors.torch.load_model 读 save_model 的文件,返回 (set(), [])(没有缺失、没有多余),词嵌入又有了值,因为两个名字指的是同一个参数,写进 lm_head.weight 就是写进词嵌入;from_pretrained 收的是目录,自己去读 index.json。

搬到 candle 的现象

let path = format!("{OUT}/st/save_model.safetensors");
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[&path], DType::F32, &dev)? };
println!("as is: {:?}", Llama::load(vb.clone(), &cfg).err().map(|e| e.to_string()));

let index = format!("{OUT}/st/sharded/model.safetensors.index.json");
let e = unsafe { VarBuilder::from_mmaped_safetensors(&[&index], DType::F32, &dev) }.err();
println!("index.json as file: {:?}", e.map(|e| e.to_string()));
as is: Some("cannot find tensor model.embed_tokens.weight")
index.json as file: Some("path: \".../st/sharded/model.safetensors.index.json\" header too large")

(路径前缀换成了 ...。)第一个:文件里有 272 个张量,名字只差一个,模型要的第一个就是它。第二个:报错说的是 safetensors 的头部,和 JSON 毫无关系。

原因

save_model 留下了哪个名字。 共享存储的两个名字只能存一个,safetensors 0.8.0 的 _remove_duplicate_names 在能完整覆盖存储的名字里排序后取第一个,lm_head.weight 排在 model.embed_tokens.weight 前面。transformers 的 save_pretrained 留的则是 model.embed_tokens.weight(键名对不上)。candle 的 Llama::load 第一行就要词嵌入,绑定时用词嵌入的张量当 lm_head,根本不去找 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"))?
        };
        // ...

文件里明明有这份数据,只是挂在另一个名字下。

index.json。 from_mmaped_safetensors 把每个路径都当成 safetensors 文件。safetensors 文件的前 8 个字节是小端的头部长度,这个 JSON 文件的前 8 个字节是 {\n "met,读成 u64 是 8387229874382703227,超过 safetensors 0.8.0 的上限 MAX_HEADER_SIZE(100,000,000 字节),于是 HeaderTooLarge。candle 没有 from_pretrained 那样「给目录、自己找文件」的入口,分片要自己读 weight_map(safetensors 的分片一节和它的动脑筋答案)。

改法

save_model 的文件:用 rename_f 把词嵌入的名字指到 lm_head.weight:

let vb = vb.rename_f(|name: &str| match name {
  "model.embed_tokens.weight" => "lm_head.weight".to_string(),
  _ => name.to_string(),
});
let model = Llama::load(vb, &cfg)?;

分片目录:读 weight_map,取出不重复的文件名,一起交给 from_mmaped_safetensors:

let j: serde_json::Value = serde_json::from_slice(&std::fs::read(&index)?)?;
let map = j["weight_map"].as_object().ok_or_else(|| anyhow!("no weight_map"))?;
let mut files: Vec<String> = map.values().filter_map(|v| v.as_str()).map(str::to_string).collect();
files.sort();
files.dedup();
let files: Vec<String> = files.iter().map(|f| format!("{OUT}/st/sharded/{f}")).collect();
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&files, DType::F32, &dev)? };
rename_f: logits max|diff|=2.98e-5 (max|ref| 20.6), greedy same prefix 20/20
shards: 3
shards: logits max|diff|=2.98e-5 (max|ref| 20.6), greedy same prefix 20/20

两种都和 transformers f32 差 2.98e-5(最大 |ref| 20.6,容差 1e-4),20 个贪心 token 全同,和读原始的 model.safetensors 一样:数据是同一组 bf16 数,只是换了名字或换了文件。

能控制存法时,更省事的是存的时候就用 save_pretrained,它留下的名字就是 candle 要的那个。

怎么检查

  • 先看文件里有什么:Python safe_open(f, "pt").keys(),或 candle 的 MmapedSafetensors::new(f)?.tensors()。tied 的模型里应该只有 model.embed_tokens.weight、没有 lm_head.weight;反过来就是 save_model 存的。
  • header too large 几乎总是「传进去的不是 safetensors 文件」,先看路径的扩展名。
  • 模型目录里有 model.safetensors.index.json 就是分片的,没有就是单文件,加载代码按这两种分支写。

小结

  • safetensors.torch.save_model 对绑定的权重只留 lm_head.weight,candle 的 Llama::load 要的是 model.embed_tokens.weight,报 cannot find tensor model.embed_tokens.weight。用 rename_f 把这个名字指过去,logits 差 2.98e-5。
  • model.safetensors.index.json 不是 safetensors 文件,当成文件传给 from_mmaped_safetensors 报 header too large。读它的 weight_map,把几个分片文件一起传进去。
  • PyTorch 侧的 load_model / from_pretrained 替你处理了这两件事,candle 侧没有。

💬 关联讨论

发起讨论
加载中...