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 选择报错而不是编一个数。
问: 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;更好的是在源头决定这些值该怎么归类。