Rust in Everything
基于 candle-core 0.11.0

错误里看不出是哪一行

主线位置:贯穿算子主线。a.matmul(&b)? 的每一步失败时都构造一个 Error 并调 .bt(),是否附带调用栈由环境变量决定。错误枚举和 bt 的实现见错误。

flowchart TB
  e["Error::Xxx.bt()"] --> env{"RUST_LIB_BACKTRACE 或 RUST_BACKTRACE"}
  env -->|未设| one["一行文字 无位置"]
  env -->|设了| bt["包进 WithBacktrace"]
  bt --> dbg{"有调试信息"}
  dbg -->|debug| line["函数名加行号"]
  dbg -->|release| fn["只有函数名"]
  ctx["调用处 context"] --> named["一行文字 带位置说明"]

实验 crate exp-candle/pa-pit,bin p06_bt;M2 Max、macOS 27、rustc 1.99.0。debug 和 release 分别注明。

现象

一个三层的小 MLP,第二个权重的形状写错了:

fn mlp(x: &Tensor, w1: &Tensor, w2: &Tensor, w3: &Tensor) -> candle_core::Result<Tensor> {
  let h = x.matmul(w1)?.relu()?;
  let h = h.matmul(w2)?.relu()?;
  h.matmul(w3)
}

不设环境变量(release):

--- Display (1 lines)
shape mismatch in matmul, lhs: [4, 16], rhs: [8, 16]

函数里有三个 matmul,错误只说「matmul 的形状不对」和两个形状。模型大了,[4, 16] 这样的形状会在很多地方出现。

打开 RUST_BACKTRACE=1。debug 构建(共 44 行,这里截取):

shape mismatch in matmul, lhs: [4, 16], rhs: [8, 16]
   0: std::backtrace_rs::backtrace::libunwind::trace
...
   4: <candle_core::error::Error>::bt
             at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/candle-core-0.11.0/src/error.rs:264:25
   5: <candle_core::tensor::Tensor>::matmul
             at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/candle-core-0.11.0/src/tensor.rs:1527:14
   6: p06_bt::mlp
             at ./src/bin/p06_bt.rs:7:13
   7: p06_bt::main
...

第 6 帧指向 p06_bt.rs:7,就是 w2 那一行。release 构建(共 11 行):

shape mismatch in matmul, lhs: [4, 16], rhs: [8, 16]
   0: <std::backtrace::Backtrace>::create
   1: <std::backtrace::Backtrace>::capture
   2: <candle_core::error::Error>::bt
   3: <candle_core::tensor::Tensor>::matmul
   4: p06_bt::mlp
   5: p06_bt::main
...

只有函数名,没有行号:知道是 mlp 里的某个 matmul,还是不知道是哪一个。

打开调用栈还有两个副作用(release,同一个程序,失败的 mlp 调 2000 次取平均;matches! 一行按变体判断错误):

环境变量每次失败的 mlpmatches!(e, Error::ShapeMismatchBinaryOp { .. })
不设0.46 µstrue
RUST_BACKTRACE=112.06 µsfalse
RUST_LIB_BACKTRACE=112.14 µsfalse
RUST_BACKTRACE=1 RUST_LIB_BACKTRACE=00.47 µstrue

出处

Error::bt:

    pub fn bt(self) -> Self {
        let backtrace = std::backtrace::Backtrace::capture();
        match backtrace.status() {
            std::backtrace::BacktraceStatus::Disabled
            | std::backtrace::BacktraceStatus::Unsupported => self,
            _ => Self::WithBacktrace {
                inner: Box::new(self),
                backtrace: Box::new(backtrace),
            },
        }
    }
  • 捕获与否完全由标准库的 Backtrace::capture 决定:先看 RUST_LIB_BACKTRACE,没设再看 RUST_BACKTRACE。默认关闭,所以错误里什么位置信息都没有。
  • 捕获到了,原来的错误被包进 WithBacktrace,matches! 直接匹配原变体就失败了。
  • 调用栈靠调试信息还原文件和行号。cargo build --release 默认不带调试信息,只剩符号表里的函数名。

错误本身不记录位置:Tensor 的方法没有 #[track_caller],ShapeMismatchBinaryOp 里只有 lhs、rhs、op 三个字段。

📝NOTE
冷知识:.bt() 和那段 FAQ 是同一个 PR 加的

.bt() 和 WithBacktrace 变体来自 PR #166(「Add backtrace information to errors where relevant.」)。之前 Error 只是一组普通变体,构造处直接 Err(Error::ShapeMismatchBinaryOp { .. });这个 PR 在 tensor.rs、shape.rs、layout.rs、storage.rs 等文件的构造处补上 .bt(),提交说明的三条是「Add backtrace information to errors where relevant.」「More backtrace information.」「Add to the FAQ.」。加进 README 的那段 FAQ 在 0.11.0 的 README 里还在,标题从「How to know where an error comes from.」改成了「Tracking down errors」,正文一字未改:「You can set RUST_BACKTRACE=1 to be provided with backtraces when a candle error is generated.」。release 构建下只有函数名、开销和 matches! 的问题,FAQ 里没有提。这个提交早于本地仓库最早的 tag,git tag --contains 只能查到 0.5.1。

修法

在调用处加 context。 返回 anyhow::Result 的函数里,每个可能出错的调用后面加一句说明:

use anyhow::Context as _;

fn mlp_ctx(x: &Tensor, w1: &Tensor, w2: &Tensor, w3: &Tensor) -> anyhow::Result<Tensor> {
  let h = x.matmul(w1).context("mlp: x @ w1")?.relu()?;
  let h = h.matmul(w2).context("mlp: h @ w2")?.relu()?;
  Ok(h.matmul(w3).context("mlp: h @ w3")?)
}

用 {:#} 打印(不设环境变量,release):

mlp: h @ w2: shape mismatch in matmul, lhs: [4, 16], rhs: [8, 16]

一行就够,release 下也有效;context 只在 Err 时才包装,成功路径上只多一次分支。candle 自己的 candle_core::Context 也能这样用,结果是 Error::WrappedContext。

需要调用栈时,按场景选环境变量和构建方式:

  • 开发时:debug 构建 + RUST_BACKTRACE=1,帧上有行号。
  • release 也要行号:给 release 加行表,CARGO_PROFILE_RELEASE_DEBUG=line-tables-only(或在 Cargo.toml 的 [profile.release] 写 debug = "line-tables-only")。同一个程序这样构建后,release 的第 6 帧也带上了 at ./src/bin/p06_bt.rs:7:13。
  • 只想让 panic 带调用栈、不想让 candle 的每个错误都抓栈:RUST_BACKTRACE=1 RUST_LIB_BACKTRACE=0,上表最后一行。

按变体处理错误时先剥包装:

fn root(e: &Error) -> &Error {
  match e {
    Error::WithBacktrace { inner, .. } => root(inner),
    Error::Context { inner, .. } => root(inner),
    Error::WithPath { inner, .. } => root(inner),
    e => e,
  }
}

matches!(root(&e), Error::ShapeMismatchBinaryOp { .. }) 在四种环境下都是 true。

怎么确认没中招

  • 拿到一个 candle 错误先问:它被 context 包过吗?日志里只有 shape mismatch in matmul, .. 一行,就说明这条路径上没有任何一层加过说明。
  • 生产环境不要靠 RUST_BACKTRACE 找错误位置:release 默认没有行号,失败的调用还从 0.46 µs 变成 12 µs。
  • 代码里 matches! / match candle 的 Error 变体时,用 RUST_BACKTRACE=1 跑一遍测试,确认结果不变。
  • burn 那边是 panic 停在库里的同类问题,见 panic 的位置在 burn-tensor 里。

小结

  • candle 的错误默认没有位置,只有一行文字和相关的形状、dtype。
  • RUST_BACKTRACE=1 让错误带调用栈:debug 有行号,release 只有函数名;每个错误多约 11 µs;变体被包进 WithBacktrace,matches! 失效。
  • 最实用的是在调用处 .context("…");要行号就给 release 加 line-tables-only;按变体处理前先剥包装。

💬 关联讨论

发起讨论
加载中...