Result 到处都是
主线位置:张量主线的「计算」一步:每个运算都可能失败,失败怎么表示、怎么传上去、怎么找到是哪一行。
版本:PyTorch 2.15.0(CPU、MPS)、candle 0.11.0(CPU,accelerate;Metal)
flowchart TB
op["x.matmul(w1)"] --> r{"Result"}
r -->|Ok| next["relu 继续"]
r -->|Err| q["问号返回调用方"]
q --> ctx["调用处 context"]
ctx --> top["main 打印一行"]
实验代码:candle 程序 errs(release)和几个故意编不过的小程序(cargo build --color never,debug),PyTorch 侧是 errs.py;平台 M2 Max、macOS 27、rustc 1.99.0。编译器输出里的本地路径前缀换成了 ...。
每一步都是 Result
h = (x @ w1).relu()
let h = x.matmul(&w1)?.relu()?;
PyTorch 的方法返回张量,失败时抛 RuntimeError。candle 的方法返回 candle_core::Result<Tensor>:tensor.rs 里直接写出来的 122 个 &self 公开方法,105 个返回 Result;不返回的 17 个是读元数据的(dtype、shape、dims、rank、device、is_contiguous 等)以及 detach、storage_and_layout 这类不会失败的。宏生成的一元、二元运算(relu、exp、add、broadcast_mul……)也都返回 Result,连只改布局的 t()、narrow 都是。
漏写一个 ?,不是运行时错误,是编译错误:
let h = x.matmul(&w).relu()?;
error[E0599]: no method named `relu` found for enum `Result<T, E>` in the current scope
--> src/bin/noq.rs:6:24
|
6 | let h = x.matmul(&w).relu()?;
| - ^^^^ method not found in `Result<candle_core::Tensor, candle_core::Error>`
| |
| method `relu` is available on `&candle_core::Tensor`
|
note: the method `relu` exists on the type `candle_core::Tensor`
--> .../candle-core-0.11.0/src/tensor.rs:80:9
...
help: use the `?` operator to extract the `candle_core::Tensor` value, propagating a `Result::Err` value to the caller
|
6 | let h = x.matmul(&w)?.relu()?;
| +
rustc 的建议就是改法。? 要求所在的函数也返回 Result:fn main() -> candle_core::Result<()> 或 anyhow::Result<()>,模块的 forward 返回 candle_core::Result<Tensor>(candle_core::Module trait 就是这么定义的)。
运算符也返回 Result
+ - * / 的 Output 是 Result<Tensor>,所以要写 (&a + &b)?。连着写几个:
let d = (&a + &b + &c)?;
let e = ((&a * 2.0)? + 1.0)?;
let f = (2.0 * &a + &b)?;
[3.0, 3.0, 3.0] [3.0, 3.0, 3.0] [3.0, 3.0, 3.0]
&a + &b + &c 能编过,因为 bin_trait! 也给 Result<B> 实现了运算符,左边的 Result 在里面用 self? 解开:
impl<B: std::borrow::Borrow<Tensor>> std::ops::$trait<&Tensor> for Result<B> {
type Output = Result<Tensor>;
fn $fn1(self, rhs: &Tensor) -> Self::Output {
Tensor::$fn1(self?.borrow(), rhs)
}
}
同一个宏给 Tensor 和 &Tensor 实现了 op f64,宏外面另有 f64 op Tensor 和 f64 op &Tensor,但没有 Result op f64。所以 &x * 2.0 + 1.0 编不过:
let y = (&x * 2.0 + 1.0)?;
error[E0277]: cannot add `{float}` to `Result<candle_core::Tensor, candle_core::Error>`
--> src/bin/scalarchain.rs:5:21
|
5 | let y = (&x * 2.0 + 1.0)?;
| ^ no implementation for `Result<candle_core::Tensor, candle_core::Error> + {float}`
|
= help: the trait `std::ops::Add<{float}>` is not implemented for `Result<candle_core::Tensor, candle_core::Error>`
...
在中间加一个 ?(上面的 e),或者写成一次 x.affine(2.0, 1.0)?。整数张量和浮点标量的运算还有别的问题,见整数运算。
Result + Tensor 是后来补的bin_trait! 里 Result<B> op Tensor 和 Result<B> op &Tensor 这两组实现来自 PR #697「More ops again.」。在这之前宏里已经有 Tensor op Result<B> 和 &Tensor op Result<B>,即右边可以是 Result,所以 &a + (&b + &c) 能写,左结合的 &a + &b + &c 却不能:&a + &b 的结果是 Result,它没有 +。这次提交在宏里加了 16 行,补上左边是 Result 的情况。提交说明只有标题。它早于本地仓库最早的 tag,git tag --contains 只能查到 0.5.1。
三类报错的原文
同一组失败的操作,两边的报错:
a = torch.ones(2, 3)
b = torch.ones(3, 2)
a + b
a @ a
a @ b.double()
a + b.t().double()
a @ b.to("mps")
("a + b", &a + &b),
("a.matmul(a)", a.matmul(&a)),
("a.matmul(b f64)", b.to_dtype(DType::F64).and_then(|b| a.matmul(&b))),
("a + b.t() f64", b.t().and_then(|b| b.to_dtype(DType::F64)).and_then(|b| &a + &b)),
("a cpu .matmul(b metal)", b.to_device(&metal).and_then(|b| a.matmul(&b))),
# PyTorch
a + b RuntimeError: The size of tensor a (3) must match the size of tensor b (2) at non-singleton dimension 1
a @ a RuntimeError: mm: mat1 and mat2 shapes cannot be multiplied (2x3 and 2x3)
a @ b.double() RuntimeError: expected m1 and m2 to have the same dtype, but got: float != double
a + b.t().double() ok torch.float64 [2, 3]
a.cpu() @ b.mps RuntimeError: Tensor for argument #1 'mat1' is on CPU, but expected it to be on GPU (while checking arguments for mm)
# candle
a + b Err: shape mismatch in add, lhs: [2, 3], rhs: [3, 2]
a.matmul(a) Err: shape mismatch in matmul, lhs: [2, 3], rhs: [2, 3]
a.matmul(b f64) Err: dtype mismatch in matmul, lhs: F32, rhs: F64
a + b.t() f64 Err: dtype mismatch in add, lhs: F32, rhs: F64
a cpu .matmul(b metal) Err: device mismatch in matmul, lhs: Cpu, rhs: Metal { gpu_id: 4294968354 }
- candle 的三类报错格式固定:
{shape|dtype|device} mismatch in {op}, lhs: .., rhs: ..。PyTorch 每个算子各说各的,但会说到哪一维(at non-singleton dimension 1)。 - PyTorch 的
float32 + float64自动提升,不报错;candle 报dtype mismatch。只有 matmul 这类算子 PyTorch 才要求同 dtype。 - candle 的
a + b是形状不同就报错,不看能不能广播,见形状与广播。
每个变体的定义和 Display 格式见源码卷的错误。
错误里没有位置
一个三层的 MLP,第二个权重的形状写错了。PyTorch:
def mlp(x, w1, w2, w3):
h = (x @ w1).relu()
h = (h @ w2).relu()
return h @ w3
Traceback (most recent call last):
File ".../errs.py", line 17, in <module>
mlp(x, w1, w2, w3)
~~~^^^^^^^^^^^^^^^
File ".../errs.py", line 10, in mlp
h = (h @ w2).relu()
~~^~~~
RuntimeError: mm: mat1 and mat2 shapes cannot be multiplied (4x16 and 8x16)
candle,main 返回 anyhow::Result<()>,错误直接 ? 上去:
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)
}
Error: shape mismatch in matmul, lhs: [4, 16], rhs: [8, 16]
退出码 1。异常栈指到了第 10 行和出错的那个 @;candle 只有一行,三个 matmul 里是哪一个,要自己从形状推。错误值里没有位置:ShapeMismatchBinaryOp 只有 lhs、rhs、op 三个字段,调用栈要设 RUST_BACKTRACE=1 才抓,release 构建下只有函数名,还会改变错误的变体,见源码卷的错误里看不出是哪一行。
实用的做法是在调用处加一句说明(anyhow::Context):
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")?)
}
{}: mlp: h @ w2
{:#}: mlp: h @ w2: shape mismatch in matmul, lhs: [4, 16], rhs: [8, 16]
注意 {} 只打印最外层的说明,要用 {:#} 才能在一行里看到整条链。
和别的库的错误放在一起
搬模型时,一个函数里会同时出现 candle、tokenizers、serde_json、文件读写的错误。anyhow::Result 能接住大多数,但接不住 tokenizers:
fn main() -> anyhow::Result<()> {
let tok = Tokenizer::from_file("tokenizer.json")?;
// ...
}
error[E0277]: `?` couldn't convert the error: `dyn std::error::Error + Send + Sync: Sized` is not satisfied
--> src/bin/tokq.rs:4:51
|
3 | fn main() -> anyhow::Result<()> {
| ------------------ required `dyn std::error::Error + Send + Sync: Sized` because of this
4 | let tok = Tokenizer::from_file("tokenizer.json")?;
| --------------------------------------^ doesn't have a size known at compile-time
| |
| this can't be annotated with `?` because it has type `Result<_, Box<(dyn std::error::Error + Send + Sync + 'static)>>`
|
= help: the trait `Sized` is not implemented for `dyn std::error::Error + Send + Sync`
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required for `Box<dyn std::error::Error + Send + Sync>` to implement `std::error::Error`
= note: required for `anyhow::Error` to implement `From<Box<dyn std::error::Error + Send + Sync>>`
tokenizers 的错误类型是 Box<dyn std::error::Error + Send + Sync>,这个 Box 本身没有实现 std::error::Error,anyhow 的 From 就用不上。函数返回 candle_core::Result 时同样不行,报的是 the trait From<Box<dyn std::error::Error + Send + Sync>> is not implemented for candle_core::Error,后面列出了 candle_core::Error 能从哪些类型转过来(std::io::Error、SafeTensorError、ParseIntError 等)。
改法是显式转一下,candle 自己的例子程序就是这么写的(candle-examples 里有 65 个文件用了 map_err(E::msg),E 是 anyhow::Error):
let tok = Tokenizer::from_file(&path).map_err(anyhow::Error::msg)?;
vocab 49152
Error: No such file or directory (os error 2)
第一行是读本地的 SmolLM2-135M-Instruct 分词器,第二行是文件不存在时的报错:里面连文件名都没有,同样值得加一句 .context(..)。
try/except 对 match
PyTorch 里 try: ... except RuntimeError as e: 接住任何失败。candle 的 Err 是普通的值,用 match 或 if let Err(e) 处理,不会跳出调用栈:
for (tag, r) in cases {
match r {
Ok(t) => println!("{tag:22} ok {:?} {:?}", t.dtype(), t.dims()),
Err(e) => println!("{tag:22} Err: {e}"),
}
}
上一节的报错表就是这个循环打印的。
两个要注意的地方:
- 不是所有失败都是
Err。 整数除以 0、debug 构建下的整数溢出(见整数运算)、整数张量的exp、Device::new_metal序号越界都是 panic,?和match接不住,进程直接退出。源码卷的错误列了实测到的几种。 - 按变体
match不可靠。 打开RUST_BACKTRACE=1后,错误被包进Error::WithBacktrace,matches!(e, Error::ShapeMismatchBinaryOp { .. })从true变成false;加过context的错误也多了一层。按错误种类分支时先剥掉包装,做法见错误里看不出是哪一行。PyTorch 的except RuntimeError不受环境变量影响。
小结
- candle 的张量方法和
+ - * /几乎都返回Result;漏写?是 E0599 编译错误,rustc 会指出该加在哪。 &a + &b + &c能链着写(左边的Result也实现了运算符),&x * 2.0 + 1.0不行,中间加?或用affine。- 形状、dtype、设备三类报错格式固定,
{kind} mismatch in {op}, lhs: .., rhs: ..;PyTorch 的 f32 + f64 会提升,candle 报错。 - candle 的错误只有一行,不带位置;在调用处
.context("…"),用{:#}打印整条链。 tokenizers的错误要.map_err(anyhow::Error::msg)才能?。- panic(整数除以 0、溢出、越界的设备序号)不是
Err;按变体match之前先剥包装。