第 04 章 · 基础
自动求导:从计算图到反向传播
拆开 autograd 的黑盒:看懂动态计算图、叶子节点、梯度累积、钩子和自定义求导,真正掌握训练为何有效。
动态计算图如何记录运算
本节目标
本节围绕“动态计算图如何记录运算”展开。运行示例后查看 requires_grad、grad_fn 与 .grad,记录计算图和梯度在各步的变化。
清晰讲解
当输入需要梯度时,PyTorch 会在前向过程中记录产生结果的算子,形成运行时计算图。调用 backward 后,它按链式法则从标量损失反向传播。图是动态构建的,所以普通 Python 分支和循环依然可用。
核对“动态计算图如何记录运算”中的叶子节点、计算图连接、梯度累积和图释放时机。
代码示例
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x**3 + 2*x
y.backward()
print(x.grad) # 3*x^2 + 2 = 14
运行结果与观察
x=2 时梯度应为 14;它等于手算导数而不是当前函数值。修改函数后要先手算,再与 .grad 对照。
常见错误
- 误以为 PyTorch 先编译一张永久静态图;在前向中把 Tensor 转为 Python 数值后还期待梯度回传。
- 排查“动态计算图如何记录运算”时只看 loss,没有检查 requires_grad、grad_fn、.grad 或梯度是否意外累积。
与大模型方向的连接
Transformer 是大量可微算子的组合;你不必手写每层梯度,但必须知道哪些操作切断了图。
动手练习
把函数改成 sin(x) * x**2,手算导数并与 autograd 对照。
查看参考答案与验收点
验收:梯度值与手算或有限差分一致,并能说明“动态计算图如何记录运算”中计算图何时建立、累积和释放。
官方资料
叶子 Tensor、grad_fn 与 retain_grad
本节目标
本节围绕“叶子 Tensor、grad_fn 与 retain_grad”展开。运行示例后查看 requires_grad、grad_fn 与 .grad,记录计算图和梯度在各步的变化。
清晰讲解
用户创建且需要梯度的参数通常是叶子 Tensor,梯度默认累积在它的 .grad。中间结果是非叶子节点,虽然有 grad_fn,但默认不保留 .grad;调试时可调用 retain_grad。
核对“叶子 Tensor、grad_fn 与 retain_grad”中的叶子节点、计算图连接、梯度累积和图释放时机。
代码示例
import torch
x = torch.ones(3, requires_grad=True)
y = x * 2
y.retain_grad()
loss = (y**2).sum()
loss.backward()
print(x.is_leaf, y.is_leaf)
print(x.grad, y.grad)
运行结果与观察
x 是叶子、y 是非叶子;调用 retain_grad 后两者都能观察到梯度。删除它后 y.grad 应为空。
常见错误
- 把非叶子节点的空
.grad误判为梯度消失;滥用retain_grad导致额外内存。 - 排查“叶子 Tensor、grad_fn 与 retain_grad”时只看 loss,没有检查 requires_grad、grad_fn、.grad 或梯度是否意外累积。
与大模型方向的连接
检查某层激活梯度时,不要因为 .grad is None 就断言没有梯度流;先区分叶子与非叶子。
动手练习
打印一个两层 MLP 中参数和激活的 is_leaf、grad_fn。
查看参考答案与验收点
先把每个轴写成语义:例如 [B,T,C] 分别是批次、序列和隐藏维;再用 assert tensor.shape == (...) 固化预期。
官方资料
为什么每步都要清空梯度
本节目标
本节围绕“为什么每步都要清空梯度”展开。运行示例后查看 requires_grad、grad_fn 与 .grad,记录计算图和梯度在各步的变化。
清晰讲解
PyTorch 默认把新梯度加到现有 .grad 上,这便于梯度累积,但普通训练若不清空就会混入历史批次。推荐在反向前使用 optimizer.zero_grad(set_to_none=True)。
核对“为什么每步都要清空梯度”中的叶子节点、计算图连接、梯度累积和图释放时机。
代码示例
import torch
w = torch.tensor(1.0, requires_grad=True)
for _ in range(2):
(w * 3).backward()
print(w.grad) # 6:两次各累积 3
w.grad = None
(w * 3).backward()
print(w.grad) # 3
运行结果与观察
连续两次 backward 后梯度由 3 累积为 6;清空后再次反向回到 3,证明累积是默认行为。
常见错误
- 每个 micro-batch 都
step却称为梯度累积;累积时忘记缩放 loss。 - 排查“为什么每步都要清空梯度”时只看 loss,没有检查 requires_grad、grad_fn、.grad 或梯度是否意外累积。
与大模型方向的连接
梯度累积正是利用这一行为模拟更大 batch,但损失通常要除以累积步数。
动手练习
实现 4 个 micro-batch 累积后再更新一次参数,并验证更新幅度。
查看参考答案与验收点
用一个极小 batch 过拟合,并记录 loss、梯度范数和参数更新;断点恢复后比较下一步结果,而不只检查文件存在。
官方资料
停止梯度:no_grad、inference_mode 与 detach
本节目标
本节围绕“停止梯度:no_grad、inference_mode 与 detach”展开。运行示例后查看 requires_grad、grad_fn 与 .grad,记录计算图和梯度在各步的变化。
清晰讲解
no_grad 暂停记录梯度,适合验证;inference_mode 进一步关闭部分 autograd 开销,适合纯推理;detach 返回与原 Tensor 共享存储但脱离当前图的视图。三者目的相近,作用范围不同。
核对“停止梯度:no_grad、inference_mode 与 detach”中的叶子节点、计算图连接、梯度累积和图释放时机。
代码示例
import torch
x = torch.randn(2, 3, requires_grad=True)
with torch.no_grad():
a = x * 2
with torch.inference_mode():
b = x * 3
c = (x * 4).detach()
print(a.requires_grad, b.requires_grad, c.requires_grad)
运行结果与观察
三个结果的 requires_grad 均为 False;但 detach 仍可能共享存储,不能据此认定发生了数据复制。
常见错误
- 用
detach后原地修改共享数据;验证时忘记关闭梯度导致显存逐步上涨。 - 排查“停止梯度:no_grad、inference_mode 与 detach”时只看 loss,没有检查 requires_grad、grad_fn、.grad 或梯度是否意外累积。
与大模型方向的连接
生成阶段应避免构建反向图;记录指标时可 loss.detach(),防止日志列表无意持有整张计算图。
动手练习
写一个验证函数,确保模型切到 eval、关闭梯度并返回 Python 浮点指标。
查看参考答案与验收点
用一个极小 batch 过拟合,并记录 loss、梯度范数和参数更新;断点恢复后比较下一步结果,而不只检查文件存在。
官方资料
梯度钩子与调试梯度流
本节目标
本节围绕“梯度钩子与调试梯度流”展开。运行示例后查看 requires_grad、grad_fn 与 .grad,记录计算图和梯度在各步的变化。
清晰讲解
hook 能在梯度经过 Tensor 或模块时观察、记录甚至修改它。它适合诊断梯度爆炸、某层无梯度等问题,但生产代码要及时移除,避免额外开销和隐蔽副作用。
核对“梯度钩子与调试梯度流”中的叶子节点、计算图连接、梯度累积和图释放时机。
代码示例
import torch
x = torch.randn(4, requires_grad=True)
handle = x.register_hook(lambda grad: print('grad norm:', grad.norm().item()))
loss = (x**2).mean()
loss.backward()
handle.remove()
运行结果与观察
反向时只打印一次有限的梯度范数;移除 handle 后再次反向不应再触发该输出。
常见错误
- 在 hook 中返回错误形状;注册后从不 remove;日志同步 GPU 造成训练变慢。
- 排查“梯度钩子与调试梯度流”时只看 loss,没有检查 requires_grad、grad_fn、.grad 或梯度是否意外累积。
与大模型方向的连接
可按 Transformer 层记录梯度范数,判断深层梯度是否异常;不要每步打印完整梯度。
动手练习
给一个 nn.Linear 的权重注册 hook,只在梯度非有限时报警。
查看参考答案与验收点
用一个极小 batch 过拟合,并记录 loss、梯度范数和参数更新;断点恢复后比较下一步结果,而不只检查文件存在。
官方资料
自定义 autograd Function 何时需要
本节目标
本节围绕“自定义 autograd Function 何时需要”展开。运行示例后查看 requires_grad、grad_fn 与 .grad,记录计算图和梯度在各步的变化。
清晰讲解
大多数模型应组合已有算子,让 autograd 自动求导。只有接入自定义 CUDA 算子、特殊数值近似或需要手写反向时,才实现 torch.autograd.Function,并用 gradcheck 校验。
核对“自定义 autograd Function 何时需要”中的叶子节点、计算图连接、梯度累积和图释放时机。
代码示例
import torch
class Cube(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
ctx.save_for_backward(x)
return x**3
@staticmethod
def backward(ctx, grad_out):
(x,) = ctx.saved_tensors
return grad_out * 3 * x**2
x = torch.tensor(2., requires_grad=True)
Cube.apply(x).backward()
print(x.grad)
运行结果与观察
x=2 时自定义反向应得到梯度 12,并应继续通过双精度 gradcheck。
常见错误
- 在 forward 保存所有中间值造成显存浪费;反向漏乘上游梯度
grad_out。 - 排查“自定义 autograd Function 何时需要”时只看 loss,没有检查 requires_grad、grad_fn、.grad 或梯度是否意外累积。
与大模型方向的连接
高性能大模型算子可能有自定义 forward/backward;理解保存哪些中间量,就是理解它的显存代价。
动手练习
为 x^2 + x 写自定义 Function,并用双精度 gradcheck 验证。
查看参考答案与验收点
验收:梯度值与手算或有限差分一致,并能说明“自定义 autograd Function 何时需要”中计算图何时建立、累积和释放。
官方资料
章末资料
小测、项目与相关面试题
1. 为什么连续 backward 后 grad 会变大?
PyTorch 默认累积梯度,新梯度会加到现有 .grad。
2. 非叶子 Tensor 的 grad 为 None 是否代表无梯度?
不一定。非叶子默认不保留 .grad,可用 retain_grad 调试。
