Rust in Everything
基于 tenferro-runtime 0.8.0

整数乘 0.5 原样返回

主线位置:张量主线的第二步「计算」,整数张量乘一个浮点系数。
版本:JAX 0.12.0.1(CPU,x64)、PyTorch 2.15.0(CPU,单线程)、tenferro-rs 0.8.0(CPU,native)
flowchart TB
  k["I64 -7, 7"] --> jm["JAX 乘 0.5"]
  jm --> jf["float64 -3.5, 3.5"]
  k --> sr["scale_real 0.5"]
  sr --> rd["0.5 舍成 1"]
  rd --> same["-7, 7 原样"]
  k --> fix["先 cast F64"]
  fix --> jf

实验代码:JAX / PyTorch 脚本一个,tenferro 程序 p07_scale_real_int(traced,CpuEngine::serial(),release)。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

JAX 和 PyTorch 里整数数组乘 Python 浮点数,结果是浮点数组:

k = jnp.array([-7, 7], dtype=jnp.int64)
k * 0.5; k * 0.4; torch.tensor([-7, 7]) * 0.5
int64 * 0.5: float64 [-3.5, 3.5]
int64 * 0.4: float64 [-2.8000000000000003, 2.8000000000000003]
torch int64 * 0.5: [-3.5, 3.5]

tenferro 里「张量乘一个 Rust 标量」的方法是 scale_real,dtype 一页把它对应到 JAX 的 Python 标量(弱类型)。搬过来:

let k = TracedTensor::from_tensor_concrete_shape(Tensor::from_vec_col_major(vec![2], vec![-7_i64, 7])?)?;
k.scale_real(0.5)?
I64.scale_real(0.5): (I64, [-7, 7])
I64.scale_real(0.4): (I64, [0, 0])
I64.scale_real(1.5): (I64, [-14, 14])
I64.scale_real(2.5): (I64, [-21, 21])

没有报错,dtype 仍是 I64。乘 0.5 原样返回,乘 0.4 全变成 0,乘 1.5 等于乘 2,乘 2.5 等于乘 3。

出处

TracedTensor::scale_real 按输入的 dtype 把系数落成一个常量:

        let op = match self.dtype {
            DType::F64 => StdTensorOp::constant(factor),
            DType::F32 => StdTensorOp::constant(factor as f32),
            DType::I32 => StdTensorOp::constant(round_real_to_i32_for_op("scale_real", factor)?),
            DType::I64 => StdTensorOp::constant(round_real_to_i64(factor)?),
// ...

整数分支经 round_real_to_i64_for_op 做 finite_real_scalar(op, value)?.round():只拒绝非有限值和超出 i64 的值,不拒绝小数。f64::round 把 .5 舍向远离 0 的一侧,所以 0.5 → 1、2.5 → 3。

「系数随张量的 dtype」对 F32 正是 JAX 弱类型的效果(f32 * 0.1 留在 f32);JAX 的弱类型对整数却不一样:int * 0.5 提升成浮点。scale_real 没有区分这两种情况。这条规则在整数运算的「整数乘浮点系数」一节有完整的对照。

修法

先转成浮点再乘。I64 到 F64 是提升规则允许的方向,cast 和 convert 都可以:

k.cast(DType::F64)?.scale_real(0.5)?
k.convert(DType::F64)?.scale_real(0.4)?
I64.cast(F64).scale_real(0.5): (F64, [-3.5, 3.5])
I64.convert(F64).scale_real(0.4): (F64, [-2.8000000000000003, 2.8000000000000003])

和 JAX 逐位相同。乘一个 F64 张量(而不是标量)也会把结果提升成 F64,效果一样。大于 2⁵³ 的整数转 F64 会丢精度,和 JAX 的 int64 * 0.5 一样。

怎么确认没中招

  • 对整数张量调用 scale_real 时,系数必须本来就是整数;搜一遍代码里所有 scale_real,看接收者的 dtype。
  • 打印结果的 dtype:乘了一个小数结果却还是 I32 / I64,就是中招了。
  • scale_real(0.5) 原样返回、scale_real(0.4) 归零,这两个值放进测试最容易暴露问题。

小结

  • scale_real 保持输入的 dtype,整数输入时系数先按 f64::round 舍成整数:乘 0.5 原样返回,乘 0.4 得 0。
  • JAX 和 PyTorch 的 int * 0.5 提升成浮点。
  • 整数乘小数前先 cast(DType::F64)(或 convert)。

💬 关联讨论

发起讨论
加载中...