Rust in Everything
基于 burn 0.22.1

Int 的 / 向零截断,不是 //

主线位置:张量主线的「计算」一步,Int 张量上的整数除法和取余。

版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB
  hab["PyTorch rel // 2"] --> port["burn rel / 2"]
  port --> q["-3 / 2 得 -1"]
  rem["burn rel % 2"] --> r["-3 % 2 得 1"]
  q --> bad["q*2 + r 不等于 rel"]
  r --> bad
  bad --> fix["(rel - rel%2) / 2"]

实验 crate pita-t(burn =0.22.1,feature std、flex、ndarray、autodiff、optim、linalg)的 p01,PyTorch 侧 p01_floor_div.py。debug 构建。平台 M2 Max、macOS 27、rustc 1.99.0。

PyTorch 的写法

相对位置分桶(relative position bucket)、把时间差换成周期数、把扁平下标拆成行列,都是整数 // 加 %。相对位置一定有负数:

rel = torch.arange(-4, 5)  # relative positions j - i
b = 2
q, r = rel // b, rel % b
print("q*2 + r == rel", bool(((q * b + r) == rel).all()))
rel         [-4, -3, -2, -1, 0, 1, 2, 3, 4]
rel // 2    [-2, -2, -1, -1, 0, 0, 1, 1, 2]
rel % 2     [0, 1, 0, 1, 0, 1, 0, 1, 0]
q*2 + r == rel True

// 向下取整,% 跟除数同号,两者配套,q·b + r = a 对每个元素都成立。

搬到 burn 的现象

逐字翻译:

let rel = Tensor::<1, Int>::arange(-4..5, &device);
let q = rel.clone() / 2;
let r = rel.clone() % 2;
let back = q.clone() * 2 + r.clone();
let ok: bool = back.clone().equal(rel.clone()).all().into_scalar();
rel        [-4, -3, -2, -1, 0, 1, 2, 3, 4]
rel / 2    [-2, -1, -1, 0, 0, 0, 1, 1, 2]
rel % 2    [0, 1, 0, 1, 0, 1, 0, 1, 0]
q*2 + r == rel false  (q*2 + r = [-4, -1, -2, 1, 0, 1, 2, 3, 4])

没有报错。商在 -3、-1 两个位置变了:PyTorch 是 -2、-1,burn 是 -1、0。分桶的结果是 0 号桶多收了 -1,-1 号桶多收了 -3,靠近 0 的桶比别的桶宽。取余一列和 PyTorch 一模一样,所以只对照 % 的输出,或者只用非负的输入测,都发现不了。

原因

rel / 2 落到 Div<E> 的实现,再到 div_scalar;Flex 的 int_div_scalar 直接用 Rust 的整数除法:

    fn int_div_scalar(lhs: IntTensor<Flex>, rhs: Scalar) -> IntTensor<Flex> {
        // ...
        int_scalar_op(lhs, rhs.to_i64().unwrap(), |a, b| a / b)
    }

Rust 的 / 向零截断。% 走 Rem<E> 和 remainder_scalar,Flex 的 int_remainder_scalar 调的是 remainder_int,注释写着「Python/PyTorch-style remainder」,在余数和除数异号时加上除数。商按 torch.div(rounding_mode="trunc") 算,余数按 % 算,两套规则拼在一起,商和余数异号的位置上 q·b + r 差一个 b。

张量除张量(a / b)和除标量是同一种取整。四个后端怎么实现、main 上有没有改,见整数运算;那一页的结论是 Flex、ndarray、Metal 实测一致,origin/main(2026-10-28)没有改动。

改法

burn 0.22.1 没有向下取整的整数除法(floor_divide 一类的名字在源码里搜不到)。用跟除数同号的余数把商凑出来:a - a % b 一定是 b 的整数倍,截断除法在这里是精确的。

let floor_q = (rel.clone() - r.clone()) / 2;
let ok: bool = (floor_q.clone() * 2 + r).equal(rel).all().into_scalar();
(rel - rel%2)/2 [-2, -2, -1, -1, 0, 0, 1, 1, 2]  invariant true

和 PyTorch 的 rel // 2 逐个相同。整数运算没有舍入,两边应当完全一样,这里也是。反过来要 torch.fmod 的语义,写 a - (a / b) * b。两种公式和 compare.py 的对比(容差 0、误差 0)见整数运算的「改法」一节。

怎么检查

  • 在搬过来的代码里搜所有 Int 张量上的 /,问一句:输入会不会是负数?位置差、时间差、坐标偏移、对称区间的 arange 都会。
  • 单元测试的输入一定带负数,并且断言恒等式 q * b + r == a(上面 equal(..).all().into_scalar() 那一行)。只有 % 和 / 同时按 PyTorch 的 // 语义算,它才成立。
  • 不要用 .float() / b 再 .floor() 绕:绝对值超过 2²⁴ 的整数在 f32 里不精确,理由见整数运算的「没有蠢问题」。

小结

  • burn 的 Int 除法向零截断(对应 torch.div(rounding_mode="trunc")),取余跟除数同号(对应 %);rel // 2 照搬成 rel / 2,负数一侧的商大 1,不报错。
  • 要 //,写 (a - a % b) / b;要 fmod,写 a - (a / b) * b。
  • 测试里放负数,断言 q * b + r == a。

💬 关联讨论

发起讨论
加载中...