错误里看不出是哪一行
主线位置:贯穿算子主线。
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! 一行按变体判断错误):
| 环境变量 | 每次失败的 mlp | matches!(e, Error::ShapeMismatchBinaryOp { .. }) |
|---|---|---|
| 不设 | 0.46 µs | true |
RUST_BACKTRACE=1 | 12.06 µs | false |
RUST_LIB_BACKTRACE=1 | 12.14 µs | false |
RUST_BACKTRACE=1 RUST_LIB_BACKTRACE=0 | 0.47 µs | true |
出处
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 三个字段。
.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!/matchcandle 的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;按变体处理前先剥包装。