Rust in Everything
基于 tenferro-ad 0.8.0

no_grad 下 backward 成功,却没有梯度

主线位置:变换主线的「grad」一步,eager 写法:backward 把梯度写进叶子。

版本:PyTorch 2.15.0(CPU,单线程)、tenferro-rs 0.8.0(CPU,native)
flowchart TB
  ng["no_grad 里算 loss"] --> pt["torch backward"]
  pt --> raise["RuntimeError"]
  ng --> tb["tenferro backward"]
  tb --> okk["Ok 空 Gradients"]
  okk --> none["x.grad 是 None"]
  ng --> chk["先查 tracks_grad"]

实验代码:PyTorch 脚本一个(torch.set_num_threads(1)),tenferro 程序 p13_no_grad_backward(EagerRuntime,CpuEngine::serial(),release)。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

PyTorch 里,在 no_grad 里算出的 loss 不带计算图,对它 backward 直接报错:

x = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float64, requires_grad=True)
with torch.no_grad():
  loss = (x * x).sum()
loss.backward()
loss.requires_grad = False
backward: RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

这条报错经常救人:评估代码里的 no_grad 不小心包住了训练步,第一步就停下。tenferro 的 eager 写法一模一样:

let x = rt.variable_from(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0])?)?;
let loss = {
  let _guard = rt.no_grad();
  rt.with_eager_session(&mut eng, |s| {
    let m = s.mul(&x, &x)?;
    s.reduce_sum(&m, None)
  })?
};
loss.backward(&mut eng)
loss = [14.0]  loss.tracks_grad = false
loss.backward(): Ok, Gradients.len() = 0
x.grad = None

backward 返回 Ok,梯度表是空的,x 的梯度槽位还是 None。如果后面的更新写成「有梯度就减,没有就跳过」,训练循环会一直跑、参数一直不变。

出处

EagerTensor::backward 只检查输出是不是标量,然后从种子 1 开始反向:

    pub fn backward(&self, engine: &mut dyn ExecutionEngine) -> Result<Gradients> {
        if !self.shape().is_empty() {
            return Err(Error::NonScalarGrad {
                shape: self.shape().to_vec(),
            });
        }

        let value = self.to_tensor(&mut *engine)?;
        let seed = one_like_tensor(&value, &mut *engine)?;
        self.backward_from_seed(engine, seed)
    }

backward_from_seed 收集所有 requires_grad 的叶子,对每个求 VJP,得到 None 的(输出不依赖它)就跳过。no_grad 下算出的 loss 不记录对 x 的依赖,于是每个叶子都是 None,结果是一张空表,不是错误。它不像 PyTorch 那样先看「输出本身有没有梯度函数」。no_grad、detach 和 PyTorch 的对照见 backward 的「no_grad 与 detach」一节;梯度槽位的累加规则见 tenferro 卷的常见坑第二次 backward 后梯度翻倍。

修法

在 backward 之前查输出的 tracks_grad(),把「没有梯度」当成错误;loss 要在 no_grad 之外算:

if !loss.tracks_grad() {
  println!("refusing backward: loss does not track gradients");
}
let loss2 = rt.with_eager_session(&mut eng, |s| {
  let m = s.mul(&x, &x)?;
  s.reduce_sum(&m, None)
})?;
let g = loss2.backward(&mut eng)?;
refusing backward: loss does not track gradients
loss2.tracks_grad = true
loss2.backward(): Gradients.len() = 1, x.grad = Some([2.0, 4.0, 6.0])

实际代码里那一行 println! 应该是 return Err(..)。no_grad 的守卫只在它的作用域里生效(上面用一个块限定),评估代码把它放在自己的块里,别让它包住训练步。

怎么确认没中招

  • backward 返回的 Gradients 查一下 len():训练步里它应该等于要更新的参数个数,为 0 就是中招了。
  • 更新参数时,参数的 grad() 是 None 要报错,而不是跳过。
  • 搜一遍 no_grad() 的守卫,看它的生命周期覆盖了哪些代码。

小结

  • PyTorch 对 no_grad 下的 loss backward 会报 does not require grad;tenferro 返回 Ok,Gradients 为空,叶子的 grad() 是 None。
  • backward 只检查输出是不是标量,不检查它是否带梯度。
  • backward 之前查 tracks_grad(),并把空梯度当成错误处理。

💬 关联讨论

发起讨论
加载中...