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。