Rust in Everything
基于 tenferro-runtime 0.8.0

x[-1] 取到的是第一个元素

主线位置:张量主线的第二步「计算」,用整数张量当下标取元素(gather)。

版本:JAX 0.12.0.1(CPU,x64)、tenferro-rs 0.8.0(CPU,native)
flowchart TB
  i["下标 -1"] --> jw["JAX 加 n 得 2"]
  jw --> jc["夹到 0..2"]
  jc --> j12["取到 12"]
  i --> tc["tenferro 直接夹"]
  tc --> t10["取到 10"]
  i --> fix["先 where 加 n"]
  fix --> tc2["再 gather"]
  tc2 --> j12

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

现象

x = [10, 11, 12]。JAX 里负下标是 NumPy 的「从末尾数」,jit 里也一样,下标算出来是负的也一样:

x = jnp.array([10.0, 11.0, 12.0])
x[-1]; x[jnp.array([1, -1, -4, 5])]; jax.jit(lambda i: x[i])(-1); x[jnp.argmax(x) - 3]
x[-1] = 12.0  x[jnp.array([1, -1, -4, 5])] = [11.0, 12.0, 10.0, 12.0]
jit(lambda i: x[i])(-1) = 12.0
x[jnp.argmax(x) - 3] = 12.0

tenferro 没有 x[i] 语法,取元素写成 StableHLO 式的 gather,下标照搬:

let gcfg = GatherConfig {
  offset_dims: vec![],
  collapsed_slice_dims: vec![0],
  start_index_map: vec![0],
  index_vector_dim: 1,
  slice_sizes: vec![1],
};
let idx = TracedTensor::from_tensor_concrete_shape(Tensor::from_vec_col_major(vec![4, 1], vec![1_i64, -1, -4, 5])?)?;
x.gather(&idx, gcfg)?
gather x[[1, -1, -4, 5]]: [11.0, 10.0, 10.0, 12.0]

-1 取到了 10,第一个元素。不报错,也没有警告。正的越界下标 5 两边都夹到最后一个(12),所以只有负下标这一项和 JAX 不同。

出处

两边都按 StableHLO 的规则把越界的起点夹进 [0, n − 窗口],差别在夹取之前:JAX 的 x[i] 在 lowering 成 gather 之前先把 i < 0 换算成 i + n;tenferro 的 TracedTensor::gather 直接拿下标去夹,文档写着「an out-of-range or negative index is clamped into [0, dim - window] rather than rejected」。于是 -1 和 -4 都变成 0。dynamic_slice、take_along_axis 同样如此,NumPy、JAX、tenferro 三方的完整对照见整数运算的下标一节,夹取的实现和它的来历见 tenferro 卷的越界下标不报错,取到边上的值。

修法

在 gather 之前做 JAX 做的那一步:小于 0 的下标加 n。下标在图里算出来时,用比较加 where_select:

fn wrap_negative(idx: &TracedTensor, n: i64) -> R<TracedTensor> {
  let shape = idx.concrete_shape()?.to_vec();
  let len: usize = shape.iter().product();
  let zero = TracedTensor::from_tensor_concrete_shape(Tensor::from_vec_col_major(shape.clone(), vec![0_i64; len])?)?;
  let nn = TracedTensor::from_tensor_concrete_shape(Tensor::from_vec_col_major(shape.clone(), vec![n; len])?)?;
  let neg = idx.compare(&zero, CompareDir::Lt)?;
  Ok(TracedTensor::where_select(&neg, &(idx + &nn)?, idx)?)
}
wrapped indices: [1, 2, -1, 5]
gather x[wrap([1, -1, -4, 5])]: [11.0, 12.0, 10.0, 12.0]

结果和 JAX 的 [11, 12, 10, 12] 逐项相同:-4 换算后是 -1,仍然越界,再被夹到 0;5 被夹到 2。这也是 JAX 的行为,要的是 NumPy 那样报 IndexError,就得在主机端自己查范围(JAX 一侧可用 checkify,见整数运算)。下标在主机上时,直接在 Rust 里 if i < 0 { i + n } 更省事。

怎么确认没中招

  • 搜一遍移植过来的代码里所有负数常量下标(-1、-2)和「可能为负」的下标计算(argmax(x) - k、i - offset)。
  • 测试里放一个负下标,期望值按 JAX 的语义写(最后一个元素),而不是只测正的下标。
  • 结果只在边界元素上出错,平均值、范数这类统计量很可能看不出来。

小结

  • JAX 的负下标先换算成 i + n 再夹取;tenferro 的 gather、dynamic_slice、take_along_axis 直接夹取,-1 取到第一个元素。
  • 两边都不报越界,正的越界下标两边结果相同,所以只有负下标会静默不同。
  • 修法:gather 之前把负下标加 n(图里用 compare 加 where_select,主机上直接改)。

💬 关联讨论

发起讨论
加载中...