Rust in Everything
基于 candle 0.11.0

Llama::load 和 forward 会 panic

主线位置:推理主线第二步「建模块」和第三步「前向」,把 candle 的 llama 放进一个长时间运行的服务里,指望所有错误都是 Err。

版本:PyTorch 2.15.0 / transformers 5.22.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
  cfg["config"] --> ld["Llama load"]
  ld -->|"层数比文件多"| p1["unwrap panic"]
  ld -->|"flash 为 true"| ok["加载成功"]
  ok --> fw["forward"]
  fw -->|"没开 feature"| p2["unimplemented"]
  fw -->|"位置 8192"| e3["narrow Err"]

实验代码:PyTorch 侧 llama_py.py、kv.py,candle 侧 crate pitb-exp 的 bin p23_panics(release)。模型是本地的 SmolLM2-135M-Instruct 权重,f32。平台 M2 Max、macOS 27、rustc 1.99.0。panic 行里 cargo 缓存的路径前缀换成了 ...。

PyTorch 的写法

三种情况在 transformers 里的样子:

cfg = AutoConfig.from_pretrained(model_dir, num_hidden_layers=31)
m = AutoModelForCausalLM.from_pretrained(model_dir, config=cfg, dtype=torch.float32)
print("31 layers loaded:", len(m.model.layers))
try:
  AutoModelForCausalLM.from_pretrained(model_dir, attn_implementation="flash_attention_2")
except Exception as e:
  print(type(e).__name__, str(e)[:200])

(PyTorch 的输出里有一段加粗用的终端控制符,这里去掉了;MISSING 一共 9 行,中间略去。)

[transformers] LlamaForCausalLM LOAD REPORT from: ...
Key                                             | Status  | 
------------------------------------------------+---------+-
model.layers.30.self_attn.o_proj.weight         | MISSING | 
model.layers.30.self_attn.q_proj.weight         | MISSING | 
...
model.layers.30.mlp.gate_proj.weight            | MISSING | 

Notes:
- MISSING:	those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.
31 layers loaded: 31
ImportError FlashAttention2 has been toggled on, but it cannot be used due to the following error: the package for FlashAttention2 doesn't seem to be installed.

位置超过 max_position_embeddings(kv.py):

far = model(ids[:, :1], position_ids=torch.tensor([[8192]])).logits
position 8192: (1, 1, 49152) finite True
  • 缺层:随机初始化,打一份报告,模型照样能用(输出是错的)。
  • 没装 flash-attn:from_pretrained 当场抛异常,try 接得住。
  • 位置 8192:照算,因为 cos / sin 每次按 position_ids 现算。

没有一种会让 Python 进程退出。

搬到 candle 的现象

p23_panics 按模式改 config 或参数,再加载、前向:

let mut raw: serde_json::Value = serde_json::from_slice(&std::fs::read(format!("{MODEL}/config.json"))?)?;
if mode.starts_with("layer31") {
  raw["num_hidden_layers"] = 31.into();
}
let cfg: LlamaConfig = serde_json::from_value(raw)?;
let cfg = cfg.into_config(mode == "flash");
let model = Llama::load(vb, &cfg)?;
println!("loaded");
let mut cache = Cache::new(true, DType::F32, &cfg, &dev)?;
let (ids, pos): (&[u32], usize) = if mode.starts_with("far") { (&PROMPT[..1], 8192) } else { (&PROMPT, 0) };
let l = model.forward(&Tensor::new(ids, &dev)?.unsqueeze(0)?, pos, &mut cache)?;
== layer31

thread 'main' (9129703) 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.30.self_attn.q_proj.weight
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit 101
== flash
loaded

thread 'main' (9129725) panicked at .../candle-transformers-0.11.0/src/models/llama.rs:258:5:
not implemented: compile with '--features flash-attn'
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit 101
== far
loaded
Error: narrow invalid args start + len > dim_len: [8192, 32], dim: 0, start: 8192, len:1
exit 1

前两种是 panic,退出码 101,main 里的 ? 接不住;在服务里,panic 时持有的 std::sync::Mutex 会被毒化。第三种是正常的 Err,但 transformers 那边根本不报错。

原因

缺层。 Llama::load 建 30 个 Block 用的是 .unwrap():

        let blocks: Vec<_> = (0..cfg.num_hidden_layers)
            .map(|i| Block::load(vb.pp(format!("model.layers.{i}")), cfg).unwrap())
            .collect();

词嵌入、model.norm、lm_head 缺了是 Err,层里缺了是 panic。为什么这样写、在服务里怎么兜住,见源码卷的坑 cannot find tensor。

flash-attn。 into_config(use_flash_attn) 只是把一个 bool 放进 Config,加载时不看。没开 flash-attn feature 时编进来的 flash_attn 是:

#[cfg(not(feature = "flash-attn"))]
fn flash_attn(_: &Tensor, _: &Tensor, _: &Tensor, _: f32, _: bool) -> Result<Tensor> {
    unimplemented!("compile with '--features flash-attn'")
}

函数签名返回 Result,函数体却是 unimplemented!。这个 feature 依赖 CUDA 的 candle-flash-attn,Mac 上本来就开不了。

位置。 Cache::new 按 max_position_embeddings(SmolLM2 是 8192)一次建好 cos / sin 表,apply_rotary_emb 用 narrow(0, index_pos, seq_len) 取行,越界就是 narrow invalid args。transformers 每次现算,没有上限。

origin/main 上 llama.rs 和 0.11.0 相同,三处都没有改。前两处我认为是上游的问题:同一个文件里别的错误都走 Result。

改法

缺层:加载前自己核对每一层的权重都在,把 panic 变成 Err:

fn check_layers(vb: &VarBuilder, cfg: &Config) -> Result<()> {
  for i in 0..cfg.num_hidden_layers {
    for w in ["self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj", "self_attn.o_proj",
              "mlp.gate_proj", "mlp.up_proj", "mlp.down_proj", "input_layernorm", "post_attention_layernorm"] {
      let name = format!("model.layers.{i}.{w}.weight");
      if !vb.contains_tensor(&name) {
        bail!("config has {} layers, file has no {name}", cfg.num_hidden_layers);
      }
    }
  }
  Ok(())
}
== layer31-check
Error: config has 31 layers, file has no model.layers.30.self_attn.q_proj.weight
exit 1

VarBuilder::contains_tensor 只查名字,不查形状;形状不对仍然会在 Llama::load 里 panic。要连形状一起兜住,用 catch_unwind 包住 Llama::load:

let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Llama::load(vb, &cfg)));
r.map_err(|p| anyhow::anyhow!("Llama::load panicked: {:?}", p.downcast_ref::<String>()))??
thread 'main' (9187338) 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.30.self_attn.q_proj.weight
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error: Llama::load panicked: Some("called `Result::unwrap()` on an `Err` value: cannot find tensor model.layers.30.self_attn.q_proj.weight")
exit 1

panic 消息仍然打到 stderr(默认的 panic hook),但进程以 Err 退出(退出码 1),调用方能处理。不写 AssertUnwindSafe 编译不过(E0277,dyn SimpleBackend 和 Metal 设备句柄不是 RefUnwindSafe);panic 后这个 VarBuilder 不再使用,所以这样写是安全的。release 配置里写了 panic = "abort" 时 catch_unwind 接不住,只能用 contains_tensor 那种预检。

flash-attn:Mac 上一律 into_config(false);要在 CUDA 机器上打开,写成 into_config(cfg!(feature = "flash-attn")),让 feature 和参数绑在一起。

位置:在调用 forward 之前检查:

if pos + ids.len() > cfg.max_position_embeddings {
  bail!("position {} past max_position_embeddings {}", pos + ids.len() - 1, cfg.max_position_embeddings);
}
== far-check
loaded
Error: position 8192 past max_position_embeddings 8192
exit 1

到了上限之后怎么办(截断上下文、滑动窗口)是应用的决定;candle 的报错其实替你画出了模型训练长度的边界,transformers 不画。

怎么检查

  • RUST_BACKTRACE=1 跑一遍加载和一次前向,看 panic 是否出自 candle-transformers:模型文件里的 unwrap、unimplemented!、todo! 都会这样出现。grep -n "unwrap()\|unimplemented!" llama.rs 能先看一眼。
  • 服务里把模型加载放在启动阶段,失败就让进程退出;请求处理里的 forward 用 catch_unwind 或单独的任务隔离。
  • 生成循环的长度上限取 min(max_new_tokens, max_position_embeddings - prompt_len)。

小结

  • 层数比文件多:transformers 随机初始化并打报告;candle 在 Llama::load 里 unwrap,panic(llama.rs:524)。加载前用 contains_tensor 逐层核对。
  • attn_implementation="flash_attention_2" 没装时 transformers 抛 ImportError;candle into_config(true) 加载成功、第一次前向 unimplemented! panic。Mac 上传 false。
  • 位置到 max_position_embeddings:transformers 照算;candle 报 narrow invalid args ... start: 8192。调用前自己检查。
  • origin/main 的 llama.rs 三处都没变。

💬 关联讨论

发起讨论
加载中...