Rust in Everything
基于 tenferro-runtime 0.8.0

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 之前自己检查。

💬 关联讨论

发起讨论
加载中...