type annotations needed(assert_eq 一个空形状)
主线位置:eager 主线的
backward()之前。标量损失的形状是空切片[],测试里常常先断言它;断言本身和 tenferro 无关,但 tenferro 的依赖图决定了它能不能编译。eager 主线见 eager AD。
flowchart TB lit["空数组 元素类型未知"] --> need["找 usize 的 PartialEq"] need --> core["core 的 PartialEq usize"] need --> sj["serde_json 的 PartialEq Value"] ein["文件里用到 einsum"] --> sj core --> amb["两个候选 推断失败"] sj --> amb fix["is_empty 或写明 usize"] --> ok["编译通过"]
实验 crate exp-tenferro8/pit1-shape(依赖同 base:runtime、cpu、tensor、ad、linalg、einsum、fft 都是 0.8.0),bin shape_plain、shape_einsum、shape_einsum_real、shape_fix,debug;M2 Max、macOS 27、rustc 1.99.0。
现象
eager 求一个标量损失,再断言它的形状是空的:
use tenferro_ad::{EagerRuntime, Tensor};
use tenferro_cpu::CpuEngine;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = EagerRuntime::new()?;
let mut engine = CpuEngine::serial();
let x = runtime.variable_from(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0])?)?;
let loss = runtime.with_eager_session(&mut engine, |s| {
let p = s.mul(&x, &x)?;
s.reduce_sum(&p, Some(&[0]))
})?;
assert_eq!(loss.shape(), &[]);
println!("loss shape {:?}", loss.shape());
Ok(())
}
shape_plain 能编译能跑:
loss shape []
换成真实的写法,损失用 einsum 算(use tenferro_einsum::EagerSessionEinsumExt;,s.einsum(&[&x, &x], "i,i->")),同一行断言编不过,另一个普通 Tensor 的同样断言也编不过:
error[E0282]: type annotations needed
--> src/bin/shape_einsum_real.rs:10:3
|
10 | assert_eq!(loss.shape(), &[]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
error[E0283]: type annotations needed
--> src/bin/shape_einsum_real.rs:10:3
|
10 | assert_eq!(loss.shape(), &[]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
|
= note: multiple `impl`s satisfying `usize: PartialEq<_>` found in the following crates: `core`, `serde_json`:
- impl PartialEq for usize;
- impl PartialEq<serde_json::value::Value> for usize;
= note: required for `[usize]` to implement `PartialEq<[_; 0]>`
= note: 1 redundant requirement hidden
= note: required for `&[usize]` to implement `PartialEq<&[_; 0]>`
error[E0283]: type annotations needed
--> src/bin/shape_einsum_real.rs:12:3
|
12 | assert_eq!(t.shape(), &[]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
...
error: could not compile `pit1-shape` (bin "shape_einsum_real") due to 3 previous errors
(t 是 Tensor::from_vec_col_major(vec![], vec![1.0_f64])?。)不用 einsum 也一样:在 shape_plain 里只加一行 use tenferro_einsum as _;(shape_einsum),报的是同样的 E0282 和 E0283。改动的是一行 use,出错的是一行与之无关的测试代码。
出处
不在 tenferro 里,在 Rust 的类型推断。&[] 的类型是 &[_; 0],元素类型 _ 只能从「[usize] 要和 [_; 0] 比较」这个约束里推,也就是找 usize: PartialEq<_> 的实现。只有标准库的 impl PartialEq for usize 时,唯一的候选让 _ = usize;编译器能看见的 crate 里一旦另有 impl PartialEq<Value> for usize,候选变成两个,推断失败。
这个实现来自 serde_json。在 0.8.0 的依赖图里只有一条路把它带进来(cargo tree -i serde_json -e normal):
serde_json v1.0.154
└── omeco v0.2.6
└── tenferro-einsum v0.8.0
└── pit1-shape v0.1.0 (...)
einsum 依赖 omeco 做收缩规划,omeco 把 serde_json 列在普通依赖里(omeco/Cargo.toml),用来读写和 Julia 的 OMEinsumContractionOrders.jl 兼容的 JSON 收缩树。0.8.0 默认的规划已经是 tenferro 自己的贪心算法,不调 omeco(见 einsum),但依赖还在。
为什么只在依赖里出现还不够,要在文件里「用到」einsum 才出错:rustc 只加载代码里实际引用到的 crate 和它们的依赖,没被引用的 crate 里的 trait 实现不参加推断。shape_plain 所在的 crate 同样依赖 tenferro-einsum,但那个 bin 只引用 tenferro_ad 和 tenferro_cpu,serde_json 没有被加载。
上游教程 eager_autodiff_pytorch_style.rs 在 0.8.0 改成了 assert!(loss.shape().is_empty());。改动在提交 bc25b4bd 里,说明写的是「loss.shape() compared with &[] became ambiguous once the 0.11 graph exposes serde_json's PartialEq<Value> for usize」,指的是当时 CubeCL 0.11 的依赖图。CubeCL 后来随 WebGPU 一起删掉了,那条路已经不在;omeco 这条路从 0.7.1 起一直在。
修法
不和空字面量比,或者把类型写出来:
assert!(loss.shape().is_empty());
assert_eq!(loss.shape(), &[] as &[usize]);
assert_eq!(t.shape(), &[] as &[usize]);
loss shape [], t shape []
(shape_fix,就是 shape_einsum_real 改了这三行。)assert_eq!(loss.shape().len(), 0) 也行。tenferro 的公开 API 里形状都是 &[usize] / Vec<usize>,只有和字面量比较时会遇到。
怎么确认没中招
- E0283 的 note 里出现
multiple impls satisfying usize: PartialEq<_>和一个与你代码无关的 crate 名(serde_json),就是这一类。 assert_eq!(x.shape(), &[3])这类非空字面量没事:整数字面量只能推成整数类型,serde_json::Value不是候选。- 写库的测试时假设下游会把
serde_json带进来:空的切片字面量一律带类型。
小结
assert_eq!(x.shape(), &[])靠「usize: PartialEq<_>只有一个实现」推出元素类型;serde_json给usize多加了一个。- 0.8.0 里
serde_json经 omeco 跟着tenferro-einsum进来;文件里一用到 einsum,原本能编译的断言就报 E0282 / E0283。 - 不是 tenferro 的 bug。写
is_empty()或&[] as &[usize],上游教程也改成了is_empty()。