Rust in Everything
基于 burn 0.22.1

mask_where 照搬 torch.where,结果正好互补

主线位置:张量主线的「计算」一步:按布尔掩码在两个张量之间逐元素选值。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB
  tw["torch.where c, x, y"] --> sem1["c 真取 x 否则 y"]
  mw["y.mask_where c, x"] --> sem2["在 y 上把 c 真换成 x"]
  sem1 --> same["两者相同"]
  sem2 --> same
  bad["x.mask_where c, y"] --> inv["结果互补"]

实验 crate pita-t 的 p08,PyTorch 侧 p08_where.py。debug 构建。平台 M2 Max、macOS 27、rustc 1.99.0。

PyTorch 的写法

自己写一个 leaky ReLU(或者任何「正的一半这样算、负的一半那样算」的分段函数):

x = torch.tensor([-2.0, -0.5, 0.0, 1.0, 3.0])
leaky = torch.where(x > 0, x, 0.01 * x)
torch.where(x > 0, x, 0.01*x) [-0.019999999552965164, -0.004999999888241291, 0.0, 1.0, 3.0]
F.leaky_relu                  [-0.019999999552965164, -0.004999999888241291, 0.0, 1.0, 3.0]

torch.where(cond, a, b):条件、条件为真时的值、条件为假时的值。

搬到 burn 的现象

burn 里和 torch.where 对应的是张量方法 mask_where(mask, value)。按参数出现的顺序翻译,x 放到点号前面:

let pos = x.clone().greater_elem(0.0);
let wrong = x.clone().mask_where(pos.clone(), x.clone() * 0.01);
x.mask_where(x > 0, 0.01*x) [-2.0, -0.5, 0.0, 0.01, 0.03]

正数被缩小了 100 倍,负数原样通过,正好是想要的结果的补集。形状、dtype 都对,不会报错,训练也能跑,只是激活函数反了。

原因

mask_where 的文档是「Update the given tensor with the value tensor where the mask is true」:

    pub fn mask_where(self, mask: Tensor<D, Bool>, value: Self) -> Self {
        Self::new(K::mask_where(
            self.primitive,
            mask.primitive,
            value.primitive,
        ))
    }

它是一次「改写」:从 self 出发,掩码为真的位置换成 value。所以 self 扮演的是 torch.where 的第三个参数(条件为假时的值),value 是第二个参数。逐字翻译把两者对调了。它的标量版本 mask_fill 也是同样的读法(a.mask_fill(mask, v) 对应 a.masked_fill(mask, v),这一个和 PyTorch 的方法同序)。burn 0.22.1 里没有 torch.where 那样「条件在前、两个分支并列」的自由函数,origin/main(2026-10-28)上也没有。各种掩码操作的对照表见切片与索引的最后一节。

改法

接收者是条件为假时的值:

// torch.where(c, a, b)  ==  b.mask_where(c, a)
let right = (x.clone() * 0.01).mask_where(pos.clone(), x.clone());
// 或者保留 x 在前面,把条件取反
let flipped = x.clone().mask_where(pos.bool_not(), x.clone() * 0.01);
println!("activation::leaky_relu      {:?}", fv(activation::leaky_relu(x, 0.01)));
(0.01*x).mask_where(x > 0, x) [-0.02, -0.005, 0.0, 1.0, 3.0]
x.mask_where(!(x > 0), 0.01*x) [-0.02, -0.005, 0.0, 1.0, 3.0]
activation::leaky_relu      [-0.02, -0.005, 0.0, 1.0, 3.0]

两边各把 torch.where 的结果写成 JSON(burn 侧先把 f32 转成 f64 再打印,避免最短表示和完整位数的差别),compare.py 容差 0:

leaky                    n=  5  max|diff|=0.00e+00
worst=0.00e+00 tol=0e+00 OK

误差为 0:两边做的是同样的 f32 乘法 0.01 * x,再逐元素选值,没有求和顺序的差别。反向验证:把写反的结果写进 JSON,同一条命令报 leaky ... max|diff|=2.97e+00、MISMATCH,退出码 1(3.0 对 0.03)。

能用现成激活函数的地方直接用:burn 的 activation::leaky_relu、activation::relu 等不需要自己写掩码。

怎么检查

  • 搬代码时把 torch.where(c, a, b) 机械地改写成 b.mask_where(c, a),不要按「哪个张量是主角」去放接收者。

  • 单元测试的输入要同时有满足和不满足条件的元素,并且两个分支的值要能区分(上面 x 和 0.01 * x 在 x = 0 处相同,只看 0 是看不出来的)。

  • 如果代码里有很多处,写一个和 torch.where 同序的辅助函数,搬代码时只改函数名:

    /// torch.where(c, a, b), same argument order
    fn where_<const D: usize>(c: Tensor<D, Bool>, a: Tensor<D>, b: Tensor<D>) -> Tensor<D> {
      b.mask_where(c, a)
    }
    
    where_(x > 0, x, 0.01*x)      [-0.02, -0.005, 0.0, 1.0, 3.0]
    

小结

  • torch.where(c, a, b) 在 burn 里是 b.mask_where(c, a):接收者是条件为假时的值。
  • 照参数顺序写成 a.mask_where(c, b) 结果正好互补,不报错。
  • 改对之后和 PyTorch 逐位相同(误差 0);测试输入要两个分支都覆盖。

💬 关联讨论

发起讨论
加载中...