Rust in Everything
基于 tenferro-cpu 0.8.0

real value must be finite when casting to i32

主线位置:张量主线的第二步「计算」,浮点转整数(JAX 的 astype(jnp.int32)),错误要到执行时才出来。

版本:JAX 0.12.0.1(CPU,x64)、NumPy 2.5.4、tenferro-rs 0.8.0(CPU,native)
flowchart TB
  z["log of 0"] --> ninf["-inf"]
  ninf --> ja["JAX astype"]
  ja --> imin["int32 最小值"]
  ninf --> tc["tenferro cast"]
  tc --> b["建图 编译 准备 通过"]
  b --> err["run 返回错误"]
  ninf --> fix["clamp 加 where"]
  fix --> tc2["cast 成功"]

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

现象

按数量级分桶,log|z| 取整当桶号,z 里恰好有一个 0:

z = jnp.array([8.0, 0.0, 0.5])
jnp.log(jnp.abs(z)).astype(jnp.int32)
log(|z|) = [2.0794415416798357, -inf, -0.6931471805599453]
log(|z|).astype(int32) = [2, -2147483648, 0]

JAX 不报错,-inf 变成 int32 的最小值。别的非有限值也一样,jit 前后、NumPy 都给出同样的数:

jnp astype(int32) [2, -2, 0, 2147483647, 2147483647]
jit astype(int32) [2, -2, 0, 2147483647, 2147483647]
np astype(int32)  [2, -2, 0, 2147483647, 2147483647]

(输入 [2.7, -2.7, nan, inf, 1e20];NumPy 关掉了 invalid 警告。)NaN 变 0,超范围的值饱和到边界。

tenferro 照写:

let z = TracedTensor::from_vec_col_major(vec![3], vec![8.0_f64, 0.0, 0.5])?;
let bins = z.abs()?.log()?.cast(DType::I32)?;
let p = GraphCompiler::new().compile(&bins)?;
let pr = rt.prepare(&p, &[], &[eng.target()])?;
rt.run(&mut [&mut eng], &pr, &[])
graph build: ok, dtype I32
compile: ok
prepare: ok
run: Err: cast: invalid argument value: real value must be finite when casting to i32, got -inf
  TensorRuntime(Validation { op: "cast", source: InvalidArgument { argument: "value", message: "real value must be finite when casting to i32, got -inf" } })

前三步都通过,执行时才失败。NaN 和有限但超范围的值也一样:

cast I32 [NaN]: Err(cast: invalid argument value: real value must be finite when casting to i32, got NaN)
cast I32 [2147483648.0]: Err(cast: invalid argument value: real value 2147483648 is out of i32 range)

出处

检查在 CPU 内核里,对每个值做一次:validate_real_cast_to_i32

fn validate_real_cast_to_i32(value: f64) -> crate::Result<()> {
    if !value.is_finite() {
        return Err(invalid_cast_value(format!(
            "real value must be finite when casting to i32, got {value}"
        )));
    }
    if value < i32::MIN as f64 || value > i32::MAX as f64 {
        return Err(invalid_cast_value(format!(
            "real value {value} is out of i32 range"
// ...

转 I64 有同样的一份。截断(2.7 → 2、-2.7 → -2)和 JAX 一样,只有「没有合理整数可给」的值,tenferro 选择报错而不是编一个数。

📝NOTE
没有蠢问题

问: cast 的 dtype 在建图时就定了,为什么不在建图或 prepare 时报?

答: 能不能转取决于数值,不取决于形状和 dtype。建图、编译、prepare 都只看元数据;-inf 是 log(0) 在执行时算出来的,内核拿到数据才知道。所以这类错误永远在 run 里出现,而且只在数据里真有非有限值的那一次出现,测试数据干净时一直不会触发。

修法

在 cast 之前,用 JAX 的那套规则把值整理好:先 clamp 到 i32 的范围(±inf 变成边界),再把 NaN 换成 0(v == v 只在 NaN 上为假):

let lo = TracedTensor::from_vec_col_major(vec![3], vec![i32::MIN as f64; 3])?;
let hi = TracedTensor::from_vec_col_major(vec![3], vec![i32::MAX as f64; 3])?;
let zero = TracedTensor::from_vec_col_major(vec![3], vec![0.0_f64; 3])?;
let safe = |v: &TracedTensor| -> R<TracedTensor> {
  let clamped = v.clamp(&lo, &hi)?;
  let is_num = v.compare(v, CompareDir::Eq)?; // false only for NaN
  Ok(TracedTensor::where_select(&is_num, &clamped, &zero)?)
};
safe(&z.abs()?.log()?)?.cast(DType::I32)?
safe(log|z|).cast(I32): [2, -2147483648, 0]
safe([NaN, inf, -2.7]).cast(I32): [0, 2147483647, -2]

两行和 JAX 的数逐个相同。这只是复刻 JAX 的约定;log(0) 的桶号是 -2147483648 多半也不是想要的,更好的做法是在 log 之前把 0 单独分出来,按问题本身决定它归哪一桶。

怎么确认没中招

  • 每个 cast 到整数之前问一句:输入里能不能出现 log(0)、0/0、溢出?能的话就要先处理。
  • 测试数据里故意放一个 0、一个 NaN、一个很大的数,跑到 run 为止,而不是只建图。
  • 错误发生在 run,返回的是 Err,不会 panic;调用方要把它当成数据问题处理,而不是当成程序写错了。

小结

  • JAX、NumPy 的 astype(int32) 把 NaN 变 0、±inf 和超范围值饱和到边界,静默继续。
  • tenferro 的 cast 在执行时逐值检查,遇到非有限值或超范围值返回 real value must be finite when casting to i32 或 out of i32 range;建图、编译、准备都不报。
  • 先 clamp 再用 where_select 换掉 NaN,然后 cast;更好的是在源头决定这些值该怎么归类。

💬 关联讨论

发起讨论
加载中...