torch.autograd 是 PyTorch 的自动微分引擎:前向计算时动态构建计算图,调用
backward()时沿图按链式法则求梯度。掌握 requires_grad / grad_fn 的跟踪规则与计算图「前向记录、反向释放」的生命周期,是理解训练循环与排查梯度异常的前提。
核心机制:requires_grad 与 grad_fn
autograd 的跟踪由两个 Tensor 属性驱动(Tensor 结构见 Tensor 与 Storage;Variable 已并入 Tensor):
requires_grad:为True时其参与的运算被记录。torch.nn 的nn.Parameter默认开启,这就是「参数可训练」的全部实现。grad_fn:非叶子 Tensor 指向创建该运算的反向节点(AddBackward0、MulBackward0等);叶子节点(直接创建、无计算历史)则为None。
import torch
x = torch.tensor([1.0, 2.0], requires_grad=True) # 叶子,被跟踪
w = torch.tensor([3.0, 4.0], requires_grad=True) # 叶子
y = x * w # 非叶子,y.grad_fn = <MulBackward0>
z = y.sum() # 非叶子,z.grad_fn = <SumBackward0>
print(x.is_leaf, x.grad_fn) # True None
print(y.is_leaf, y.grad_fn) # False <MulBackward0 object>requires_grad 具有传染性:只要输入中有一个被跟踪,输出就被跟踪。x.detach() 返回脱离计算图的新 Tensor(共享存储),是切断跟踪的标准手段。叶子判定的完整规则(no_grad 产物、detach 产物同为叶子)与 grad_fn 链的手动走查,见 动态计算图与叶子节点。
backward 与链式法则
backward() 的本质是 vector-Jacobian 乘积(VJP):对输出 与向量 计算 ,沿图反向逐节点乘 Jacobian,即链式法则:
标量 loss 直接 loss.backward();非标量 Tensor 必须传入同形梯度作为 :
z.backward() # 标量,v 默认为 1
y.backward(torch.ones_like(y)) # 非标量,显式传 v反向完成后,梯度累积到叶子节点的 .grad 属性;非叶子节点的 .grad 恒为 None(除非对其调用 .retain_grad())。
动态图 vs 静态图
PyTorch 是 define-by-run:计算图随 Python 执行逐运算构建,每次前向都是一张新图,控制流(if/for/递归)即原生 Python 语义,可直接 print、打断点。静态图框架(TensorFlow 1.x)先声明整图再执行,换取全图优化空间,代价是调试困难、控制流笨重,也难以适配变长输入。完整对比见 深度学习框架对比。
计算图生命周期
理解「前向记录、反向释放」能解释大多数报错:
前向:每个可导运算创建反向节点,持有其输入的引用 → 图即 grad_fn 链
反向:从 loss 沿链式法则回溯,逐节点计算并释放缓冲- 默认即释放:backward 后中间缓冲即释放,再次反传报
Trying to backward through the graph a second time;需重复反传(GAN 复用判别器前向)时传retain_graph=True。 - 图随前向重建:每轮前向生成新图,backward 前必须有当轮前向;跨 step 复用旧 loss 会拿到过期图。
- 持有即占用:把每层激活存进 list 等引用会阻止图被回收,是隐式显存泄漏的常见来源。
生命周期的四拍步进图解(建图 → 回流释放 → 再反传报错 → 新图)见 动态计算图与叶子节点。
no_grad 与 inference_mode
推理场景应关闭图构建,二者都通过将模式压入线程局部栈实现:
torch.no_grad() | torch.inference_mode() | |
|---|---|---|
| 图构建 | 关闭 | 关闭 |
| 版本计数器 | 仍维护 | 跳过 |
| 输出 Tensor | 普通 Tensor,可参与后续训练 | InferenceTensor,被 autograd 记录时直接报错 |
| 性能 | 一般 | 更快(省 view tracking 与 version counter 开销) |
纯评估、benchmark 与部署用 inference_mode();输出要回到训练图(EMA 更新、伪标签)用 no_grad()。二者均可作装饰器或 context manager。
常见坑
in-place 操作。autograd 靠 _version 版本计数检测改写:叶子节点 in-place(x += 1 且 requires_grad=True)直接报错;非叶子 in-place 覆盖反向所需值时,backward 报 one of the variables needed for gradient computation has been modified by an inplace operation。判据:可 out-of-place 就不 in-place;relu_ 等官方 API 已确保不破坏反向依赖。
梯度累积。.grad 是累加而非覆盖:标准循环必须 optimizer.zero_grad()(见 torch.optim);小显存模拟大 batch 正是利用该语义,多次前向反传后再 step():
for i, (x, y) in enumerate(loader):
loss = criterion(model(x), y) / accum_steps
loss.backward() # 梯度累积到 .grad
if (i + 1) % accum_steps == 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 裁剪防爆炸
optimizer.step()
optimizer.zero_grad() # 不清零则下轮继续累加梯度裁剪在 backward 之后、step() 之前执行——裁剪与更新是 torch.optim 一侧的职责。
double backward:对梯度再求导(WGAN-GP 梯度惩罚)时用 backward(create_graph=True),使梯度计算本身建图、可再反传;开销约翻倍,仅高阶梯度场景需要。
梯度如何变成参数更新,见 torch.optim;autograd 在分层架构中的位置见 PyTorch 架构总览。