Rust in Everything
基于 tenferro-runtime 0.8.0

ContractionScalar::one: dtype mismatch: expected I64, actual F32

主线位置:直接计算主线的第 4 步。matmul 落到 dot_general,CPU engine 先构造累加用的系数(alpha = 1、beta = 0),再交给收缩 provider;provider 只有 F32 / F64 / C32 / C64 四种(见 CPU provider)。

flowchart TB
  t["I64 张量 matmul"] --> dot["execute_dot_allocated"]
  dot --> one["ContractionScalar::one"]
  one --> err["dtype mismatch 写反"]
  dot -.-> nice["带修法的 unsupported"]
  nice -.-> never["走不到"]
  fix["convert F64 再 cast 回"] --> ok["结果正确"]

实验 crate exp-tenferro8/pit1-pit,bin p16_int,release;CpuEngine::serial();M2 Max、macOS 27、rustc 1.99.0。

现象

计数矩阵、邻接矩阵这类数据天然是整数。用 I64 张量做矩阵乘和 einsum:

let mut engine = CpuEngine::serial();
let a = Tensor::from_vec_col_major(vec![2, 2], vec![1_i64, 2, 3, 4])?;
let b = Tensor::from_vec_col_major(vec![2, 2], vec![5_i64, 6, 7, 8])?;
a.matmul(&b, &mut engine)
i64 matmul: Err(ContractionScalar::one: dtype mismatch: expected I64, actual F32)
  debug: Validation { op: "ContractionScalar::one", source: DTypeMismatch { expected: I64, actual: F32 } }
i64 einsum ij,jk->ik: Err(ContractionScalar::one: dtype mismatch: expected I64, actual F32)
i64 einsum ij,jk,kl->il: Err(ContractionScalar::one: dtype mismatch: expected I64, actual F32)
i32 matmul: Err(ContractionScalar::one: dtype mismatch: expected I32, actual F32)

程序里根本没有 F32 张量,报错却说「期望 I64,实际是 F32」,读起来像是某个输入被悄悄转成了 F32。不做收缩的整数运算都正常:

i64 add: [6, 8, 10, 12]
i64 einsum ij->ji: Ok([1, 3, 2, 4])
i64 einsum ij,ij->ij: Ok([5, 12, 21, 32])
i64 einsum ij->: Ok([10])

转置、逐元素积、全部求和都能算,只有要走 dot_general 的收缩不行。

出处

CpuEngine 的 execute_dot_allocated 在判断 dtype 能不能收缩之前,先构造累加系数:

        let accumulation = DotGeneralAccumulation {
            lhs_conj,
            rhs_conj,
            alpha: ContractionScalar::one(dtype)?,
            beta: ContractionScalar::zero(dtype)?,
        };
        if matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64) {

tenferro-tensor 的 ContractionScalar::one 对整数返回的错误是借用了 DTypeMismatch:

            DType::I32 | DType::I64 | DType::Bool | DType::External(_) => Err(validation(
                "ContractionScalar::one",
                ValidationError::DTypeMismatch {
                    expected: dtype,
                    actual: DType::F32,
                },
            )),

expected 填的是输入的 dtype,actual 是一个写死的 F32,两个值的意思和字段名正好相反,也和别处的 DTypeMismatch(expected 是左操作数,见 dot_general: dtype mismatch)不一致。

tenferro-cpu 其实准备了一条好得多的错误:cpu_contraction_unsupported_dtype_message 给 I64 生成「CPU contraction providers support F32/F64/C32/C64; convert I64 to F64 before contraction」,有单元测试 contraction_unsupported_dtype_message_lists_recovery 断言这句话。它用在分配输出(allocate_dot_output)和写入已有输出的路径上;而分配新输出的路径先调了 ContractionScalar::one,? 提前返回,轮不到它。einsum 的两输入、三输入(整数不走 cpueinsum,cpu_concrete::supports 只认四种浮点和复数)最后都落到这里。错误信息误导,可以算上游 bug,0.8.0 上复现。

修法

收缩前转成 F64,算完再转回来:

let af = a.convert(DType::F64, &mut engine)?;   // 提升方向,checked convert 允许
let bf = b.convert(DType::F64, &mut engine)?;
let cf = af.matmul(&bf, &mut engine)?;
let ci = cf.cast(DType::I64, &mut engine)?;     // 变窄要用 cast
convert F64, matmul, cast I64: [23, 34, 31, 46]

(列主序的 [[23, 31], [34, 46]],就是 [[1,3],[2,4]] · [[5,7],[6,8]]。)F64 的尾数只有 53 位,绝对值超过 2^53 的整数和中间累加值会被舍入:

2^53+1 through F64: [9007199254740992] (want 9007199254740993)

计数和邻接矩阵通常远小于这个范围;真需要精确的大整数收缩,tenferro 的 CPU provider 做不了,要在外面自己写循环。

怎么确认没中招

  • 看到 ContractionScalar::one: dtype mismatch,把 expected 读成「输入的 dtype」:它是整数(I32、I64)或 Bool,就是这个坑,和 F32 无关。
  • 整数数据在进收缩之前统一 convert(DType::F64, ..);结果需要整数时 cast 回去,并确认数值不超过 2^53。

小结

  • CPU 上的收缩(matmul、dot_general、einsum 的两输入和多输入)只支持 F32 / F64 / C32 / C64;整数的逐元素、转置、求和不受影响。
  • 报错来自 ContractionScalar::one,expected 是输入 dtype、actual 写死成 F32;带修法提示的 unsupported_dtype 错误在这条路径上走不到。上游 bug,0.8.0 复现。
  • 转成 F64 收缩再 cast 回整数;注意 2^53 的精度上限。

💬 关联讨论

发起讨论
加载中...