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下的 lossbackward会报does not require grad;tenferro 返回Ok,Gradients为空,叶子的grad()是None。 backward只检查输出是不是标量,不检查它是否带梯度。backward之前查tracks_grad(),并把空梯度当成错误处理。