dot_general: dtype mismatch: expected F64, actual F32
主线位置:直接计算主线的第 4 步。
matmul在CpuEngine上落到dot_general,执行前先核对两个输入的 dtype。dtype 标签、提升格和动态Tensor的来历见数据模型。
flowchart TB
x["Tensor F64"] --> api{"哪个入口"}
y["Tensor F32"] --> api
api -->|直接运算| err["DTypeMismatch"]
api -->|eager| pe["执行前提升到 F64"]
api -->|traced| pt["建图时插入 cast"]
fix["engine 上先 convert"] --> ok["F64 matmul"]
实验 crate exp-tenferro8/pit1-pit,bin p06_dtype,release;TypedTensor 的编译错误引自 exp-tenferro8/tensor-cfail 的 dtype_mix(debug)。M2 Max、macOS 27、rustc 1.99.0。
现象
数据一半从 f32 的文件读进来,一半是 f64 的参数,都装进动态的 Tensor:
let mut engine = CpuEngine::serial();
let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
let y = Tensor::from_vec_col_major(vec![2, 2], vec![0.5_f32; 4])?;
x.matmul(&y, &mut engine)
direct matmul f64 x f32: Err(dot_general: dtype mismatch: expected F64, actual F32)
debug: Validation { op: "dot_general", source: DTypeMismatch { expected: F64, actual: F32 } }
direct matmul f32 x f64: Err(dot_general: dtype mismatch: expected F32, actual F64)
direct add f64 + f32: Err(add: dtype mismatch: expected F64, actual F32)
同样两个张量,交给 eager(EagerRuntime::constant_from 后在 with_eager_session 里 s.matmul)或 traced(TracedTensor::matmul 后编译运行)就能算:
eager matmul f64 x f32: Ok F64 [2.0, 3.0, 2.0, 3.0]
traced matmul f64 x f32: Ok F64 [2.0, 3.0, 2.0, 3.0]
于是一段在 eager 下验证过的计算,搬到直接计算(为了少一层记录)时就报错了。
用带类型参数的 TypedTensor<f64> 和 TypedTensor<f32> 写同样的乘法,编译期就过不去(a.matmul(&b, &mut engine),b 是 TypedTensor<f32>),错误是 E0308 expected &TypedTensor<f64>, found &TypedTensor<f32>,完整输出见数据模型。
出处
直接运算的 dtype 检查在 CpuEngine 的 execute_dot_allocated:
let dtype = lhs.dtype();
if rhs.dtype() != dtype {
return Err(crate::Error::dtype_mismatch(
"dot_general",
dtype,
rhs.dtype(),
));
}
expected 是左操作数的 dtype,actual 是右操作数的,所以反过来写 y.matmul(&x) 时两个值也对调了。写进已有输出的路径(dot_into)和逐元素运算(add 等)各自做同样的检查。直接运算的契约是「输入是什么 dtype 就按什么 dtype 算」,不替调用方决定精度。
eager 在执行前查一张提升计划:tenferro-ad 的 eager_input_promotion_plan 对 Add、Mul、DotGeneral 等二元运算返回 Pair { dtype: promote_dtype_for_binary_op(..) },把两个输入先转成公共类型。traced 在建图时做同样的事:TracedTensor::dot_general 最后调 apply_binary:
let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], "apply_binary")?;
// Insert Convert ops when an input dtype differs from the primitive input dtype.
let lhs = if lhs.dtype != input_dtype {
lhs.cast(input_dtype)?
} else {
lhs.clone()
};
两条路最后都落到 tenferro-tensor 的 promote_dtype 这张格(F32 和 F64 提升到 F64)。所以三个入口的差别不是「支持不支持混合 dtype」,而是「谁插入那次转换」:eager 和 traced 替你插,直接运算要你自己插。
修法
在同一个 engine 上先 convert,再算:
let y64 = y.convert(DType::F64, &mut engine)?;
let z = x.matmul(&y64, &mut engine)?;
convert then matmul: F64 [2.0, 3.0, 2.0, 3.0]
结果和 eager、traced 的一致。convert 只接受提升方向的转换;想把 f64 降成 f32,要用有损的 cast:
f64 -> f32 convert: Err(UnsupportedDTypeConversion { op: "convert", from: F64, to: F32, message: "checked convert only accepts conversions allowed by dtype promotion; use explicit cast for lossy dtype projection" })
f64 -> f32 cast: Ok(F32)
两者都是 TensorSessionOpsExt 的方法,在 tenferro_runtime::prelude 里,第二个参数是 &mut dyn Engine。更省事的办法是在边界上统一 dtype:读进来的 f32 数据在构造张量之前就转成 f64,或者整条计算都用 TypedTensor<T>,让编译器检查。
怎么确认没中招
- 错误是
Validation { source: DTypeMismatch { .. } },op是dot_general、add这类直接运算名:在调用点打印两边的dtype(),expected一侧是左操作数。 - 同一段代码 eager 能跑、直接计算报错,先怀疑 dtype。
- 能用
TypedTensor<T>的地方尽量用,混合 dtype 在编译期就是 E0308。
小结
- 直接运算不提升 dtype,
dot_general和逐元素运算遇到两种 dtype 直接报DTypeMismatch。 - eager(
eager_input_promotion_plan)和 traced(apply_binary)用同一张提升格自动插入转换。 - 修法:engine 上先
convert到公共类型;变窄用cast;或改用TypedTensor<T>让编译器拦下。