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 的精度上限。