Rust in Everything
基于 tenferro-runtime 0.8.0

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> 让编译器拦下。

💬 关联讨论

发起讨论
加载中...