梯度如何穿过计算图
把 backward、optimizer.step 和训练系统放在同一张图里
你可以熟练调用工具包,却仍不确定训练究竟改变了什么。这一章只补上那张必要的图:前向保存什么,反向返回什么,参数又在哪一步发生变化。
读完这一章,你应该能够
- 手算一条计算图中每条边的局部梯度
- 解释反向传播与梯度下降的不同职责
- 识别梯度累积、mask 和 detach 导致的训练异常
本章目录 · 0 个小节
一步训练,其实是三件不同的事
先给结论:前向传播算输出和损失,反向传播算损失对参数的导数,优化器根据导数更新参数。 loss.backward() 本身通常不会改变参数。调用优化器后,下一次前向才用上新参数。PyTorch 的 autograd 在前向构建操作图,再根据链式法则累积叶子张量梯度。官方文档与源码入口
样本 x、目标 y 与当前参数 θ
前向:保存需要的中间结果,得到损失 L
反向:从 L 出发,沿计算图累积 ∂L/∂θ
优化器:结合梯度、学习率与优化器状态,更新 θ
下一批数据:使用新参数重新构建图
这里的“反向”不是把句子倒着读,也不是重新生成一个答案。它是在回答:刚才的误差,如果某个参数稍微改变,会增加还是减少、改变多少?
用一个参数,把整张图算出来
设输入 ,目标 ,模型 ,当前 。损失选最简单的平方误差:
前向得到:乘法输出 1,加法输出 1,误差 -4,损失 8。接着反向走同样的边:
| 节点 | 本节点的局部导数 | 从后面传回的梯度 | 送给前一节点的梯度 |
|---|---|---|---|
| 1 | -4 | ||
| -4 | -4 | ||
| 对 z、b 都为 1 | -4 | z 与 b 各得到 -4 | |
| 对 w 为 x=2 | -4 | w 得到 -8 |
所以 ,。取学习率 0.1,梯度下降更新为 。下一次预测变成 3,损失变成 2。减去负梯度,参数反而增大。 打开梯度实验室,按前向、反向、更新三个按钮逐步确认。
为什么一条路径相乘,多条路径相加
想象同一个参数被两个分支使用:一个影响注意力输出,另一个影响辅助损失。沿一个分支逐层“变化率相乘”;如果两条路径都影响最终损失,就把两条贡献相加。反向自动微分因此需要拓扑顺序,并累计到同一个叶子的梯度中。
一个极小反例:,第一条路径返回 ,第二条返回 1,最后是 。若实现用赋值覆盖而不是累加,就会静默丢掉一条路径。这也是教学自动微分实验需要测试共享子图的原因。
矩阵形式没有改变原则。对于 ,若 、、输出梯度 ,则 ,。shape 是读反向代码时最实用的第一项检查。
backward 不等于梯度下降
课程分析:梯度是测量局部变化率的结果;SGD、AdamW 等优化器决定如何使用它。梯度算得完全正确,也可能因为学习率过高而训练失败。对于这个 toy,Hessian 的非零特征值为 ;沿有效方向,固定步长梯度下降需要 才收敛。界限只属于当前二次目标,不能搬成 LLM 学习率。
把交互实验的学习率调到 0.5,会观察到震荡发散。此时“再算精确一点梯度”解决不了更新步长问题;换成真实训练,要把梯度异常、优化器状态异常与数据异常分别检查。
# 教学 PyTorch 对照;本章纯标准库实验无需安装 torch
import torch
w = torch.tensor(0.5, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)
optimizer = torch.optim.SGD([w, b], lr=0.1)
optimizer.zero_grad(set_to_none=True)
loss = 0.5 * (w * 2 + b - 5).square()
loss.backward() # w.grad=-8, b.grad=-4;参数未改变
optimizer.step() # w=1.3, b=0.4
zero_grad 清理上一轮累计结果。它的位置要与 microbatch 累积策略配套:每个 microbatch 都清理,会破坏累积;更新后一直不清理,又会把跨更新的梯度混在一起。PyTorch autograd
从一条图走到真实训练 step
生产模型有数十亿参数,但仍然执行同一组职责。变复杂的是中间激活存在哪里、何时重算、梯度在哪个 rank、哪些 token 应该计入 loss。OLMo-core 的 TransformerTrainModule.train_batch 展示有效标签统计、microbatch 与反向入口;优化器步骤独立执行。这是透明实现对照,不是对其模型能力排名的判断。固定源码与调用链
| 机制 | 为何出现 | 对计算图和更新的影响 |
|---|---|---|
| Activation checkpointing | 激活显存不足 | 反向时重新计算部分前向,换显存但增加算量 |
| 梯度累积 | 单卡放不下整个 batch | 多个 microbatch 累积梯度后只更新一次 |
| 数据并行 | 多卡处理不同样本 | 梯度需要归约,保证副本执行相同更新 |
| 参数分片 | 参数与 optimizer state 太大 | 前向/反向前收集参数,反向后归约并分片梯度 |
| Token mask | padding、工具观测不该直接成为优化动作 | 决定哪些位置参与损失及分母 |
特别留意平均方式:先对每个不同长度的样本取平均,再对样本平均,不等于对全 batch 有效 token 统一平均。这不是一个无关紧要的数值误差,而是改变样本权重。
Agent RL 里的梯度为什么不会穿过终端
环境执行工具、运行测试、返回奖励,通常不在 PyTorch 可微图内。策略梯度使用采样动作的 log-prob 和 advantage,改变以后选中这些动作的概率;它不需要对“编译器”求导。
教学目标可写为 。 把工具观察等非策略 token 排除;这里假定 A 已作为固定权重,不让梯度穿过 advantage。真实 PPO/GRPO 还包含 old log-prob、概率比、clipping、KL 等机制。Agent rollout 中的 response mask
这个连接很重要:反向图只包含策略更新所需的张量计算,训练数据流还包含远处的 CPU 环境、验证器和轨迹存储。下一步阅读一条 rollout 如何变成训练样本。
运行、核对,然后故意破坏它
python3 -m labs.run --experiment gradients
python3 -m unittest discover -s tests -p 'test_*.py'
labs/gradients.py 是本站原创的标量自动微分教学实现。输入和输出都是标量节点;实验把解析梯度与中央有限差分相比较。它没有 GPU tensor、并行反向引擎、mixed precision 或 Adam 状态;不要把其运行时间当成生产性能。
预期观察:初始 loss=8,w 梯度=-8、b 梯度=-4,学习率 0.1 更新后的 loss=2。小步长中央差分应与自动微分相近。差分步长过小会受浮点消减误差影响,过大则不是局部导数。
| 现象 | 首先检查 | 可验证动作 |
|---|---|---|
| 参数一直没变 | 是否执行 optimizer.step、是否加入 optimizer | 保存更新前后参数差范数 |
| 梯度为 None | 图是否被 detach/no_grad 打断、是否叶子 | 检查 requires_grad 与 grad_fn |
| 梯度越来越大 | 是否忘了清理、loss分母改变 | 固定同一批重放并记录逐层范数 |
| loss下降却没有任务收益 | loss覆盖的token是否符合目标 | 可视化mask并检查独立评测 |
| 单卡好、多卡坏 | 归约与有效token分母是否一致 | 对齐一个等价全局batch的梯度 |
工程任务:用共享参数构造 ,写出两条反向路径;再把一个长度为 1、另一个长度为 100 的样本放在同一 batch,解释两种平均损失的权重差异。你能够画出并验证这两件事,就已经拥有阅读后续训练代码所需的核心直觉。
把这一章放进实验里
调整变量,观察机制,再运行本地 Python 实验验证。
本章证据与源码
引用代表相应来源支持的结论,不意味着公开了完整训练配方。
官方事实PyTorch:Autograd mechanicsPyTorch · stable 文档快照源码观察OLMo-core:Transformer 的 token 归一化与训练微批allenai · 0589a034f721源码观察verl:多轮 Agent rollout 的标准化接口volcengine · 8e03c039f9a7留下你的工程判断
记录一个还没有解释清楚的现象,或下一次实验要控制的变量。笔记仅保存在当前浏览器。
自动保存在本机 · 可在“我的学习笔记”导出