Rust in Everything
基于 tenferro-runtime 0.8.0

整数加 f32 变成了 f64

主线位置:张量主线的第一步「建数组」(字面量定 dtype)和第二步「计算」(两种 dtype 相遇)。
版本:JAX 0.12.0.1(CPU,x64)、PyTorch 2.15.0(CPU,单线程)、tenferro-rs 0.8.0(CPU,native)
flowchart TB
  lit["vec 1, 2, 3"] --> i32["I32"]
  i32 --> add["加 F32"]
  add --> jx["JAX torch: f32"]
  add --> tf["tenferro: F64"]
  tf --> rd["按 f32 读回报错"]
  i32 --> fix["先 cast 到 F32"]
  fix --> f32["F32"]

实验代码:JAX / PyTorch 脚本一个,tenferro 程序 p03_int_float(traced、eager 和直接运算各一次,CpuEngine::serial(),release)。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

单精度程序里,整数下标或计数和 float32 数据相加,JAX 和 PyTorch 都留在 float32:

i = jnp.array([1, 2, 3], dtype=jnp.int32)
f = jnp.array([0.5, 0.5, 0.5], dtype=jnp.float32)
print((i + f).dtype)
jax int32 + float32: float32 [1.5, 2.5, 3.5]
torch int32 + float32: torch.float32

搬到 tenferro,Rust 的整数字面量没写后缀,元素类型推成 i32;再和 f32 张量相加:

let it = TracedTensor::from_tensor_concrete_shape(Tensor::from_vec_col_major(vec![3], vec![1, 2, 3])?)?;
let f = TracedTensor::from_vec_col_major(vec![3], vec![0.5_f32; 3])?;
let y = (&it + &f)?;
let yv = eval(&y)?;               // 编译、准备、执行、读回
yv.as_slice::<f32>()
vec![1, 2, 3] -> I32
traced I32 + F32 -> F64
as_slice::<f32>(): Err(Tensor::as_slice: dtype mismatch: expected F32, actual F64)
as_slice::<f64>(): [1.5, 2.5, 3.5]
eager I32 + F32 -> F64
direct (I32+F32 result) * F32: Err(mul: dtype mismatch: expected F64, actual F32)

建图和计算都成功,结果是 F64。后面按 f32 读回时报错;结果再和 f32 张量在 engine 上直接运算(不提升 dtype 的那条路径)也报错。没有报错的路径上,整条单精度流水线从这里起变成了双精度,内存和时间翻倍。

出处

promote_dtype 的文档列着规则:

/// - I32/I64 + F32/F64 → F64 (F32 does not have enough mantissa bits for arbitrary integers)

理由写在括号里:f32 只有 24 位尾数,放不下任意的 i32。JAX 和 PyTorch 选的是「整数遇到浮点,取浮点那一边」,tenferro 选的是不丢整数精度。traced 和 eager 都按这条规则提升;engine 上的直接运算不提升,dtype 不同就报 dtype mismatch,那是 tenferro 卷的常见坑 dot_general: dtype mismatch。完整的提升表和 JAX、NumPy、PyTorch 的并排对照见 dtype。

另一半原因在字面量:tenferro 的 dtype 来自 Rust 的元素类型,vec![1, 2, 3] 是 Vec<i32>,没有 JAX 那种由全局开关决定的默认值。

修法

整数参与浮点运算前先转成要的浮点类型,或者一开始就写浮点字面量:

let y2 = (&it.cast(DType::F32)? + &f)?;
let i32f = TracedTensor::from_vec_col_major(vec![3], vec![1.0_f32, 2.0, 3.0])?;
I32.convert(F32): Err(TracedTensor::convert: unsupported dtype conversion from I32 to F32: checked convert only accepts conversions allowed by dtype promotion; use explicit cast for lossy dtype projection)
I32.cast(F32) + F32 -> F32 [1.5, 2.5, 3.5]
vec![1.0_f32, 2.0, 3.0] + F32 -> F32

I32 到 F32 在 tenferro 看来是有损的,convert 只接受提升规则允许的方向,拒绝了;要用 cast(dtype 的 astype 一节)。大于 2²⁴ 的整数转 F32 会舍入,这正是 tenferro 默认不这样做的原因;下标、计数这类小整数不受影响。

怎么确认没中招

  • 单精度程序里,在 traced 图的关键节点打印 .dtype():建图时就知道,不必执行。上面的 y.dtype() 在建图后就是 F64。
  • 读回用 as_slice::<f32>() 而不是 as_slice::<f64>():dtype 不对时这里会报错,而不是静默给出双精度的数。
  • 搜一遍没有后缀的整数字面量 vec![1, 2, ...],确认它们本来就想当整数用。

小结

  • tenferro 的 I32 / I64 加 F32 提升成 F64,JAX 和 PyTorch 都是 float32;Rust 不带后缀的整数字面量是 I32。
  • 结果在 traced 和 eager 里静默变成 F64,到按 f32 读回或在 engine 上直接运算时才报 dtype mismatch。
  • 整数先 cast(DType::F32),或直接写 _f32 字面量。

💬 关联讨论

发起讨论
加载中...