add: incompatible shapes: lhs=[3], rhs=[4]
主线位置:traced 主线的第一步和最后一步:运算只记图,执行时才算。形状在哪一步确定、
broadcast_binary怎么放行,见 TracedTensor 的「形状在什么时候确定」。
flowchart TB
x["符号叶子 x"] --> op["x*x + w"]
w["具体叶子 w 长 4"] --> op
op --> b["建图 通过"]
b --> c["编译 通过"]
c --> r{"运行时 x 的长度"}
r -->|3| err["add 形状不符"]
r -->|4| ok["结果"]
实验 crate exp-tenferro8/pit2-exp,bin p21_deferred,CPU(CpuEngine::serial()),debug,M2 Max、macOS 27、rustc 1.99.0。
现象
x 是符号形状的叶子(带 3 个元素的默认数据),w 是长度 4 的具体叶子,y = x * x + w:
let x = TracedTensor::from_tensor_symbolic_shape(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0])?)?;
let w = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64; 4])?;
let y = (&(&x * &x)? + &w)?;
println!("build: ok, y.try_concrete_shape() = {:?}", y.try_concrete_shape());
let p = c.compile(&y)?;
// 打印输出值的形状元数据,再 prepare + run,输入用默认值
build: ok, y.try_concrete_shape() = None
compile: ok, output shape metadata = Ok("[Exact(Max(InputDim { input_idx: 0, axis: 0 }, Const(4)))]")
run defaults: Err("run: add: incompatible shapes: lhs=[3], rhs=[4]")
- 建图、编译、
prepare都通过(run:前缀是实验代码加的,标明失败的是哪一步)。编译器给输出的长度是Max(InputDim 0, Const(4)):它把这次加法当成了能广播的,没有记下「两边必须一样长」。 - 错误在
run时由 CPU 的add内核报出,只说add,不说是图里的哪个加法、哪一行代码。程序里有几十个加法时,这条消息几乎没法定位。 - 同一个编译好的程序,喂一个长度 4 的
x就正常:
x index = Some(0)
run with x of length 4: Ok([2.0, 5.0, 10.0, 17.0])
同样的推迟也发生在 slice 上。对长度 3 的输入取 1..5,报错的阶段取决于叶子是哪一种:
concrete [3] .slice(1..5): Err: TracedTensor::slice (GraphBuild): invalid slice bounds: start=1, end=5, axis_len=3
placeholder .slice(1..5): build ok
compile_with_input_specs([3]): Err: shape_infer (Compile): invalid slice bounds: start=1, end=5, axis_len=3
symbolic leaf (3 elements) .slice(1..5): build ok
compile: ok
run: Err("run: slice: invalid argument configuration: limit 5 on axis 0 exceeds dimension size 3")
具体形状的叶子在建图那一行报;占位符要等 compile_with_input_specs 给出形状,在编译时报;带数据的符号叶子编译时也不知道长度,到执行时才由内核报,措辞也换了一套。
出处
二元运算先经过 broadcast_binary:
if a.shape_hint == b.shape_hint && a.rank == b.rank {
return Ok((a.clone(), b.clone()));
}
if (try_concrete_shape(a).is_none() || try_concrete_shape(b).is_none()) && a.rank == b.rank {
return Ok((a.clone(), b.clone()));
}
let a_shape = concrete_shape(a)?;
let b_shape = concrete_shape(b)?;
let target = broadcast_shape(&a_shape, &b_shape)
.map_err(|err| graph_broadcast_error("broadcast_binary", err))?;
只要有一边不是具体形状、两边秩相同,就原样放行,既不检查也不插入广播。符号形状的长度只有执行时才知道,建图时确实无法比较;但放行之后也没有留下一条运行期的约束,检查就落到了内核上。两边都是具体形状时走后面的 broadcast_shape,长度对不上当场报错。slice 的三个阶段同理:建图时有形状就查,编译期的 shape_infer 用绑定的形状再查一次,都没有就交给执行时的内核。
修法
长度在建图时就知道的,用具体形状的叶子,错误出现在写错的那一行:
let xc = TracedTensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0])?;
let y = &(&xc * &xc)? + &w;
concrete build: Err: broadcast_binary (GraphBuild): incompatible shapes: lhs=[3], rhs=[4]
真的需要可变长度时:
- 让和符号输入运算的其他张量也从同一个输入派生(同一个叶子算出来的值,长度表达式都是同一个
InputDim),不要混进写死长度的具体叶子。 - 能用占位符加
compile_with_input_specs的,用它:形状在编译期绑定,错误至少出现在编译时(代价是换长度要重新编译,见 has no runtime input binding)。 - 在调用
run之前,自己按程序的约定检查输入的长度。运行时只在内核里查,查到的时候前面的运算已经算过了。
怎么确认没中招
- 搜
from_tensor_symbolic_shape和input_symbolic_shape,看它们参与的每个二元运算,另一边是不是写死了长度。 - 编译后打印输出的形状元数据(
program.program().value_metadata(out)):出现Max(InputDim .., Const(..))这样的表达式,就说明编译器把一处本该相等的长度当成了可广播。 - 测试里至少用两种不同的长度跑同一个程序。
小结
- 一边是符号形状、秩相同时,traced 的二元运算建图不查形状,编译和
prepare也通过,run时才由内核报add: incompatible shapes: lhs=[3], rhs=[4]。 - 编译器把输出长度记成
Max(InputDim, Const),没有记下相等约束;报错只给运算名,不给位置。 - 同一个越界
slice:具体叶子在建图时报,占位符在编译时报,符号叶子在执行时报。长度已知就用具体形状;可变长度时让相关张量从同一个输入派生,并在run之前自己检查。
💬 关联讨论
发起讨论加载中...