shape mismatch in add
主线位置:算子主线的第一步。
a.matmul(&b)?在碰到Storage之前,Tensor::matmul先检查形状;逐元素的add、mul也在同一层检查。错误值的结构见错误。
flowchart TB
op["x + bias"] --> chk{"形状完全相同"}
chk -->|否| err["shape mismatch in add"]
chk -->|是| run["后端逐元素计算"]
fix["broadcast_add"] --> bs["先算广播形状"]
bs --> view["broadcast_as 视图"]
view --> run
实验 crate exp-candle/pa-pit,bin p01_shape,CPU,debug;M2 Max、macOS 27、rustc 1.99.0。
现象
从 PyTorch 或 NumPy 过来,最顺手的写法是给每一行加一个偏置:
let x = Tensor::zeros((2, 3), DType::F32, &dev)?;
let bias = Tensor::zeros(3, DType::F32, &dev)?;
show("x + bias", &x + &bias);
x + bias: Err(shape mismatch in add, lhs: [2, 3], rhs: [3])
x.add(&bias): Err(shape mismatch in add, lhs: [2, 3], rhs: [3])
x.broadcast_add(&bias): Ok [2, 3]
x + 1.0: Ok [2, 3]
x - col: Err(shape mismatch in sub, lhs: [2, 3], rhs: [2, 1])
x.broadcast_sub(&col): Ok [2, 3]
x.broadcast_add(&bad): Err(shape mismatch in broadcast_add, lhs: [2, 3], rhs: [2])
col 是 (2, 1),bad 是 (2)。标量可以直接加(x + 1.0),两个张量只要形状不完全相同就报错,哪怕按广播规则合法。最后一行是真正不合法的广播,报错的 op 名换成了 broadcast_add。
matmul 也一样。一批序列 (batch, seq, in) 乘一个二维权重 (in, out):
h.matmul(&w): Err(shape mismatch in matmul, lhs: [4, 7, 3], rhs: [3, 5])
h.broadcast_matmul(&w): Ok [4, 7, 5]
reshape-matmul-reshape: Ok [4, 7, 5]
(2,3,4,5).matmul((3,2,5,6)): Ok [2, 3, 4, 6]
(2,3,4,5).broadcast_matmul((3,2,5,6)): Err(shape mismatch in broadcast_matmul, lhs: [2, 3], rhs: [3, 2])
倒数第二行反过来:批维 (2, 3) 和 (3, 2) 明明不同,matmul 却成功了,结果的批维取左边的 (2, 3)。
出处
逐元素二元运算由 binary_op! 宏生成,第一行就是 same_shape_binary_op:
pub(crate) fn same_shape_binary_op(&self, rhs: &Self, op: &'static str) -> Result<&Shape> {
let lhs = self.shape();
let rhs = rhs.shape();
if lhs != rhs {
Err(Error::ShapeMismatchBinaryOp {
lhs: lhs.clone(),
rhs: rhs.clone(),
op,
}
.bt())
} else {
Ok(lhs)
}
}
广播只在 broadcast_* 系列里做:broadcast_binary_op! 先用 Shape::broadcast_shape_binary_op 算出目标形状,再把需要扩展的一边 broadcast_as 成同形状的视图(步长为 0,不复制),最后调普通的 add。x + 1.0 能过,是因为和 f64 相加走的是 affine,不经过形状检查(见 DType 与 Device)。
Tensor::matmul 要求两边秩相同,批维只比较乘积:
let batching: usize = a_dims[..dim - 2].iter().product();
let batching_b: usize = b_dims[..dim - 2].iter().product();
if k != k2 || batching != batching_b {
Err(Error::ShapeMismatchBinaryOp {
lhs: self.shape().clone(),
rhs: rhs.shape().clone(),
op: "matmul",
}
.bt())?
}
所以 (4, 7, 3) 乘 (3, 5) 因秩不同被拒;(2, 3, ..) 乘 (3, 2, ..) 因 6 == 6 被放行,后端把两边都当成 6 个矩阵按顺序配对,第 i 个配第 i 个。方法的文档写的是「self - A tensor with dimensions b1, b2, ..., bi, m, k」「rhs - ... b1, b2, ..., bi, k, n」,即批维逐个相同;检查比文档宽。
批维的这项检查是 PR #93(「Sketch the Falcon model.」)顺手加的,提交说明的最后一条是「Stricter shape checking in matmul.」。之前 matmul 只比较 k != k2,批维完全不看;这次加上了 batching != batching_b,两个乘积都由同一处代码算出,直接拿来比较。0.11.0 里这几行没有变。这个提交早于本地仓库最早的 tag,git tag --contains 只能查到 0.5.1。
修法
逐元素运算换成 broadcast_*:broadcast_add、broadcast_sub、broadcast_mul、broadcast_div,还有 broadcast_maximum、broadcast_minimum、broadcast_eq 这类比较。
(batch, seq, in) 乘二维权重有两种写法,上面的输出里都成功了:
// 1. broadcast_matmul: rhs is broadcast to (4, 5, ..) and made contiguous
let y = h.broadcast_matmul(&w)?;
// 2. fold the batch into rows, one 2-D matmul, unfold
let y = h.reshape((4 * 7, 3))?.matmul(&w)?.reshape((4, 7, ()))?;
broadcast_matmul 会把被广播的一边 contiguous(),真的复制出 batch 份权重;源码里留着「TODO: Avoid concretising the broadcasted matrixes via contiguous.」。第二种写法不复制,candle-nn 的 Linear::forward 对连续输入就是这么做的,注释写着「we avoid using a broadcasted matmul as it is much slower than the standard matmul for the cuda and cpu backends」。直接用 Linear 最省事(权重是 [out, in],见常用层)。
批维 (2, 3) 对 (3, 2) 这种情况不会报错,只能自己防:批维来自不同来源时,乘之前比较 a.dims()[..rank - 2] 和 b.dims()[..rank - 2],或者改用 broadcast_matmul,它逐维计算广播形状,上面的输出里它报了错。
怎么确认没中招
shape mismatch in <op>里的<op>是add/sub/mul/div/matmul(不带broadcast_)时,先看两边秩是否相同、是否只差一个长度为 1 的维度:这类都是缺了broadcast_。带broadcast_前缀的才是形状真的不兼容。- 批量
matmul的结果形状只看左边。写一行assert_eq!(a.dims()[..r - 2], b.dims()[..r - 2])比事后查数值便宜。 - 加载权重时报的
shape mismatch for <名字>是另一回事(文件里的形状和模型要的不同),见 VarBuilder。
小结
- candle 的
+、-、*、/和matmul都要求形状相同,不做隐式广播;要广播就写broadcast_*。 - 批量乘二维权重:
broadcast_matmul(会复制权重),或 reshape 成二维再乘(Linear的做法)。 matmul的批维只比较乘积,(2, 3)对(3, 2)不报错,结果形状跟左边。