label 106 has extents 1 and 3
主线位置:einsum 线的第 3 步(执行)。CPU 上三个以上 F32 / F64 / C32 / C64 输入的收缩整条交给单独发布的 cpueinsum,tenferro 只给收缩顺序;两个输入走 tenferro 自己的路。两条路的分工见 einsum 和 CPU provider。
flowchart TB
src["a 的 j 长 1 b 的 j 长 3"] --> n{"几个输入"}
n -->|两个| own["tenferro 执行器"]
own --> bc["先广播到树的尺寸"]
bc --> ok2["算出结果"]
n -->|三个以上| ce["cpueinsum"]
ce --> chk["同一标签长度必须相同"]
chk --> err["label 106 has extents"]
fix["先 broadcast_in_dim"] --> ok3["三个输入也能算"]
实验 crate exp-tenferro8/pit1-pit,bin p13_bcast_fwd,release;CpuEngine::serial();M2 Max、macOS 27、rustc 1.99.0。
现象
a 是 [2, 1],b 是 [3, 2],c 是 [2, 2]。j 在 a 里长 1、在 b 里长 3。两个输入时 einsum 按 NumPy 规则广播;加上第三个输入,同样的广播就不行了:
let mut engine = CpuEngine::serial();
[&a, &b].einsum("ij,jk->ik", &mut engine)
[&a, &b, &c].einsum("ij,jk,kl->il", &mut engine)
concrete ij,jk->ik Ok([2, 2] [6.0, 12.0, 15.0, 30.0])
concrete ij,jk,kl->il Err(CPU einsum: backend failure: label 106 has extents 1 and 3)
debug: Tensor(BackendSource { op: "CPU einsum", source: Shape(Extent { label: 106, first: 1, second: 3 }) })
eager ij,jk,kl->il Err(CPU einsum: backend failure: label 106 has extents 1 and 3)
concrete ...ij,...jk,...kl->...il Err(CPU einsum: backend failure: label 4294967295 has extents 1 and 4)
106 是字符 j 的码值;4294967295(u32::MAX)是省略号盖住的第一个轴的标签([1,2,3]、[4,3,2]、[4,2,2] 三个输入,批维 1 对 4)。eager 的 einsum(EagerRuntime::new() 加 with_eager_session,输入不求导)报的是同一个错误。
traced 入口又是另一种态度:不写省略号时,两个输入也在编译期拒绝;写了省略号,两个输入能算,三个输入在运行时报 cpueinsum 的错:
traced ij,jk->ik Err(compile: extension family "tenferro.einsum.v1" shape constraint at instruction None failed: Const(1) (1) Equal Const(3) (3))
traced ...ij,...jk->...ik Ok([4, 2, 2])
traced ...ij,...jk,...kl->...il Err(prepare/run: CPU einsum: backend failure: label 4294967295 has extents 1 and 4)
出处
三个以上输入时,tenferro 的执行器 eager_einsum_exec 先问 cpu_concrete::supports:
pub(crate) fn supports(session: &mut dyn Engine, inputs: &[TensorRead<'_>]) -> bool {
inputs.len() >= 3
&& inputs.first().is_some_and(|input| {
matches!(
input.dtype(),
DType::F32 | DType::F64 | DType::C32 | DType::C64
)
})
&& with_cpu_engine(session, |_| ()).is_some()
}
为真时原始输入连同收缩树直接交给 cpueinsum 0.9.0。cpueinsum 要求同一个标签在所有操作数上长度相同,不同就是 ShapeError::Extent:
/// A label has two different extents.
#[error("label {label} has extents {first} and {second}")]
Extent {
两个输入不走这条路。tenferro 自己的执行器第一步是 broadcast_to_tree_sizes,把每个输入广播到收缩树记录的尺寸,再一步步收缩;交给 cpueinsum 之前没有对应的这一步。规划这一步接受广播(标签尺寸按「相等或其中一个是 1」合并),执行时却交给了一个不做广播的库。这是 0.8.0 的回归,上游 bug:0.7.1 同样的三输入广播前向能算(只是梯度形状错,见 einsum 的梯度形状不对)。
traced 两个输入的编译错误来自扩展算子的 infer_output_meta:载荷里的 allow_broadcast 只在写了省略号时为真,否则对同一标签的两个尺寸 require_equal。三个入口的这几种态度并排见 einsum。
PR #2004(首次发布于 v0.8.0)的第一个提交 04fed1d2 把 CPU 后端改成调用方持有的 engine,提交说明里有一条:「dot_general, grouped GEMM and N-ary concrete einsum delegate to cpueinsum prepared plans」。tenferro 从此只给 cpueinsum 一个收缩顺序,中间标签、暂存区和每一步的计划都是 cpueinsum 的。长度 1 广播是更早的 PR #1705(v0.4.0)随省略号加进来的,由 tenferro 自己的执行器在收缩前补齐;委托给 cpueinsum 之后,三个以上输入就绕开了补齐这一步。
修法
在 einsum 之前把广播做出来,或者去掉长度 1 的轴、照实写下标,或者先把两个输入收缩掉:
let wide = a.broadcast_in_dim(&[2, 3], &[0, 1], &mut engine)?;
[&wide, &b, &c].einsum("ij,jk,kl->il", &mut engine)?;
let flat = a.reshape(&[2], &mut engine)?;
[&flat, &b, &c].einsum("i,jk,kl->il", &mut engine)?;
let ab = [&a, &b].einsum("ij,jk->ik", &mut engine)?;
[&ab, &c].einsum("ik,kl->il", &mut engine)?;
fix broadcast_in_dim, ij,jk,kl->il Ok([2, 2] [6.0, 12.0, 15.0, 30.0])
fix reshape, i,jk,kl->il Ok([2, 2] [6.0, 12.0, 15.0, 30.0])
fix two steps, (ij,jk->ik),kl->il Ok([2, 2] [6.0, 12.0, 15.0, 30.0])
(broadcast_in_dim、reshape 是 TensorSessionOpsExt 的方法。)三种结果相同,也和两个输入时的广播结果一致(c 是单位阵)。第二种最好:a 本来就和 j 无关,下标里写 i 而不是 ij,没有广播,也省掉一次物化。要求导时同样适用,而且能躲开上一页的梯度形状 bug。
怎么确认没中招
- 错误是
BackendSource { op: "CPU einsum", source: Shape(Extent { .. }) }:把label换回字符(char::from_u32),找出这个标签在哪个输入上长 1。4294967295一类接近u32::MAX的标签是省略号的轴。 - 同一个式子拆成两个输入能算、三个输入报错,就是这一类。
- 写库代码时不要依赖 einsum 的隐式长度 1 广播,无论几个输入。
小结
- CPU 上三个以上 F32 / F64 / C32 / C64 输入整条交给 cpueinsum;它要求同一标签长度相同,长度 1 广播报
label .. has extents 1 and ..。 - 两个输入时 tenferro 自己的执行器先广播,能算;traced 不写省略号时连两个输入也在编译期拒绝。
- 0.8.0 的回归,上游 bug。先
broadcast_in_dim,更好的是去掉长度 1 的轴并改写下标。