764 lines
19 KiB
Markdown
764 lines
19 KiB
Markdown
# 第 14 课:学习率、梯度累积与混合精度
|
||
|
||
这一课只解决一个问题:MiniMind 的训练循环如何把多个 micro-batch 的 loss 变成一次稳定的参数更新。
|
||
|
||
## 目录
|
||
|
||
- [0. 本节主线](#l14-mainline)
|
||
- [1. 原理讲解](#l14-principle)
|
||
- [2. 源码阅读顺序图](#l14-reading-order)
|
||
- [3. MiniMind 源码走读](#l14-source-walkthrough)
|
||
- [4. 本节必须会写 / 暂时不要求](#l14-must-write)
|
||
- [5. 手写模块](#l14-handwrite)
|
||
- [6. 对齐测试](#l14-alignment-test)
|
||
- [7. 阶段组装](#l14-stage-assembly)
|
||
- [8. 本节检查](#l14-check)
|
||
- [9. 下一课](#l14-next)
|
||
|
||
<a id="l14-mainline"></a>
|
||
## 0. 本节主线
|
||
|
||
MiniMind 的一个训练 micro-step 是:
|
||
|
||
```text
|
||
取一个 batch
|
||
-> 把 input_ids / labels 放到 device
|
||
-> 按当前全局 step 设置 learning rate
|
||
-> autocast forward 得到 lm loss 和 aux loss
|
||
-> loss 除以 accumulation_steps
|
||
-> backward,把梯度累积到参数上
|
||
-> 每 accumulation_steps 个 micro-step 做一次 optimizer step
|
||
-> step 前 unscale + clip grad
|
||
-> step 后 scaler.update + zero_grad
|
||
-> epoch 结束时处理不足 accumulation_steps 的尾巴
|
||
```
|
||
|
||
这一课的核心不是“调用 `loss.backward()`”,而是看懂这四件事如何配合:
|
||
|
||
```text
|
||
learning rate schedule
|
||
gradient accumulation
|
||
mixed precision / GradScaler
|
||
gradient clipping + optimizer.step
|
||
```
|
||
|
||
<a id="l14-principle"></a>
|
||
## 1. 原理讲解
|
||
|
||
### 1.1 一个训练 step 到底更新了什么
|
||
|
||
模型训练的目标是让参数 $\theta$ 沿着 loss 下降的方向移动。
|
||
|
||
如果一个 batch 的训练损失是 $L(\theta)$,反向传播计算的是:
|
||
|
||
$$
|
||
g = \nabla_{\theta} L(\theta)
|
||
$$
|
||
|
||
优化器做的事可以粗略理解成:
|
||
|
||
$$
|
||
\theta_{\text{new}} = \theta_{\text{old}} - \eta \cdot \mathrm{Update}(g)
|
||
$$
|
||
|
||
这里:
|
||
|
||
| 符号 | 形状 | 含义 |
|
||
|---|---|---|
|
||
| $\theta$ | 每个参数自己的形状 | 模型参数 |
|
||
| $L$ | 标量 | 当前 batch 的训练损失 |
|
||
| $g$ | 和参数同形状 | 反向传播得到的梯度 |
|
||
| $\eta$ | 标量 | learning rate |
|
||
|
||
对 MiniMind 来说,训练 loss 通常是:
|
||
|
||
$$
|
||
L_{\text{train}} = L_{\text{lm}} + L_{\text{aux}}
|
||
$$
|
||
|
||
其中 $L_{\text{lm}}$ 是 next-token loss,$L_{\text{aux}}$ 是 MoE router 的辅助损失。非 MoE 模型里,`aux_loss` 通常就是 0。
|
||
|
||
### 1.2 学习率不是固定不变的
|
||
|
||
Learning rate 控制每次参数更新走多大一步。太大容易震荡,太小训练太慢。
|
||
|
||
MiniMind 的 `get_lr` 用的是余弦衰减,但不是衰减到 0,而是从初始学习率衰减到 10% 初始学习率。
|
||
|
||
公式是:
|
||
|
||
$$
|
||
\eta_s
|
||
= \eta_0 \left(0.1 + 0.45\left(1 + \cos\left(\frac{\pi s}{T}\right)\right)\right)
|
||
$$
|
||
|
||
其中:
|
||
|
||
| 符号 | 形状 | 含义 |
|
||
|---|---|---|
|
||
| $s$ | 标量 | 当前全局 micro-step |
|
||
| $T$ | 标量 | 总 micro-step 数 |
|
||
| $\eta_0$ | 标量 | 命令行传入的初始学习率 |
|
||
| $\eta_s$ | 标量 | 当前 step 实际使用的学习率 |
|
||
|
||
代入两个边界:
|
||
|
||
$$
|
||
\begin{aligned}
|
||
\eta_0^{\text{actual}}
|
||
&= \eta_0 \left(0.1 + 0.45(1 + 1)\right)
|
||
= \eta_0 \\
|
||
\eta_T
|
||
&= \eta_0 \left(0.1 + 0.45(1 - 1)\right)
|
||
= 0.1\eta_0
|
||
\end{aligned}
|
||
$$
|
||
|
||
也就是说,它从 `learning_rate` 平滑降到 `0.1 * learning_rate`。
|
||
|
||
### 1.3 梯度累积解决什么问题
|
||
|
||
显存放不下很大的 batch 时,可以用多个小 batch 模拟一个大 batch。
|
||
|
||
假设:
|
||
|
||
| 符号 | 形状 | 含义 |
|
||
|---|---|---|
|
||
| $B$ | 标量 | 单个 micro-batch 的 batch size |
|
||
| $K$ | 标量 | `accumulation_steps` |
|
||
| $B_{\text{eff}}$ | 标量 | 等效 batch size |
|
||
|
||
单卡非 DDP 时:
|
||
|
||
$$
|
||
B_{\text{eff}} = B \times K
|
||
$$
|
||
|
||
如果是 DDP,多卡训练还要乘以进程数 $W$:
|
||
|
||
$$
|
||
B_{\text{eff}} = B \times K \times W
|
||
$$
|
||
|
||
为什么要把 loss 除以 $K$?
|
||
|
||
如果第 $i$ 个 micro-batch 的原始 loss 是 $L_i$,MiniMind 用的是:
|
||
|
||
$$
|
||
\tilde{L}_i = \frac{L_i}{K}
|
||
$$
|
||
|
||
连续反向传播 $K$ 次后,参数上累积的梯度是:
|
||
|
||
$$
|
||
\begin{aligned}
|
||
g
|
||
&= \sum_{i=1}^{K}\nabla_{\theta}\tilde{L}_i \\
|
||
&= \sum_{i=1}^{K}\nabla_{\theta}\left(\frac{L_i}{K}\right) \\
|
||
&= \frac{1}{K}\sum_{i=1}^{K}\nabla_{\theta}L_i
|
||
\end{aligned}
|
||
$$
|
||
|
||
这相当于取了 $K$ 个 micro-batch 梯度的平均值,而不是把它们直接相加。否则 `accumulation_steps` 越大,梯度尺度越大,学习率也会被隐式放大。
|
||
|
||
### 1.4 混合精度做了什么
|
||
|
||
混合精度的目标是减少显存和加速计算。
|
||
|
||
MiniMind 的 forward 放在 `autocast_ctx` 里:
|
||
|
||
```text
|
||
with autocast_ctx:
|
||
res = model(...)
|
||
loss = ...
|
||
```
|
||
|
||
这表示某些矩阵乘法可以用 `float16` 或 `bfloat16` 执行,而不是全部用 `float32`。
|
||
|
||
但 `float16` 动态范围小,梯度可能 underflow。`GradScaler` 的思想是先把 loss 放大:
|
||
|
||
$$
|
||
L_{\text{scaled}} = c \cdot L
|
||
$$
|
||
|
||
反向传播得到放大后的梯度:
|
||
|
||
$$
|
||
g_{\text{scaled}} = c \cdot g
|
||
$$
|
||
|
||
真正更新前再除回来:
|
||
|
||
$$
|
||
g = \frac{g_{\text{scaled}}}{c}
|
||
$$
|
||
|
||
MiniMind 只在 `dtype == float16` 时启用 `GradScaler`。如果是 `bfloat16`,代码仍然调用 `scaler.scale(...)`,但 scaler 是 disabled 状态,本质上是 no-op。
|
||
|
||
### 1.5 为什么要先 unscale 再 clip grad
|
||
|
||
梯度裁剪限制梯度范数,避免一次异常梯度把参数推太远。
|
||
|
||
如果最大范数是 $C$,裁剪可以理解成:
|
||
|
||
$$
|
||
g_{\text{clipped}}
|
||
= g \cdot \min\left(1, \frac{C}{\lVert g \rVert_2}\right)
|
||
$$
|
||
|
||
如果用了 `GradScaler`,参数上的梯度可能还是放大后的 $g_{\text{scaled}}$。这时直接 clip,裁剪的就不是原始梯度。
|
||
|
||
所以 MiniMind 的顺序是:
|
||
|
||
```text
|
||
scaler.unscale_(optimizer)
|
||
-> clip_grad_norm_
|
||
-> scaler.step(optimizer)
|
||
-> scaler.update()
|
||
```
|
||
|
||
先 unscale,再 clip,最后 step。
|
||
|
||
### 1.6 变量形状
|
||
|
||
训练循环里最重要的张量形状是:
|
||
|
||
| 变量 | 形状 | 含义 |
|
||
|---|---|---|
|
||
| `input_ids` | `[B, S]` | token id |
|
||
| `labels` | `[B, S]` | 训练目标,忽略位置是 `-100` |
|
||
| `logits` | `[B, S, V]` | 每个位置对词表的预测 |
|
||
| `res.loss` | 标量 | causal LM loss |
|
||
| `res.aux_loss` | 标量 | MoE aux loss,非 MoE 时通常是 0 |
|
||
| `loss` | 标量 | `(res.loss + res.aux_loss) / accumulation_steps` |
|
||
| `param.grad` | 和参数同形状 | 当前累计梯度 |
|
||
|
||
注意:`loss` 是标量,不是 `[B, S]`。前面第 10 课已经讲过,cross entropy 会把有效 token 的 loss 聚合成一个标量。
|
||
|
||
<a id="l14-reading-order"></a>
|
||
## 2. 源码阅读顺序图
|
||
|
||
这节源码按这个顺序读:
|
||
|
||
```text
|
||
trainer_utils.get_lr
|
||
-> train_pretrain.py 参数
|
||
-> autocast / GradScaler 初始化
|
||
-> train_epoch 前半段:lr + forward + loss / accumulation_steps
|
||
-> train_epoch 中段:backward + unscale + clip + step + zero_grad
|
||
-> train_epoch 末尾:不足 accumulation_steps 的尾批处理
|
||
```
|
||
|
||
对应文件:
|
||
|
||
```text
|
||
trainer/trainer_utils.py
|
||
trainer/train_pretrain.py
|
||
```
|
||
|
||
先看 `get_lr`,是为了知道每个 step 用什么学习率。
|
||
再看 `train_epoch`,是为了知道 loss、梯度、optimizer step 如何串起来。
|
||
最后看初始化部分,是为了知道 `autocast_ctx` 和 `GradScaler` 是怎么决定的。
|
||
|
||
<a id="l14-source-walkthrough"></a>
|
||
## 3. MiniMind 源码走读
|
||
|
||
### 第 1 步:学习率公式
|
||
|
||
File: `trainer/trainer_utils.py:40-41`
|
||
|
||
Read this to understand: MiniMind 的 learning rate schedule 不是 PyTorch scheduler,而是每个 step 手动计算。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
def get_lr(current_step, total_steps, lr):
|
||
return lr*(0.1 + 0.45*(1 + math.cos(math.pi * current_step / total_steps)))
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- `current_step` 是当前训练进度。
|
||
- `total_steps` 是总训练步数。
|
||
- 返回值直接写入 optimizer 的 param group。
|
||
- 它是 cosine decay,终点是 `0.1 * lr`,不是 0。
|
||
|
||
### 第 2 步:训练参数决定 batch、lr、累积步数和裁剪阈值
|
||
|
||
File: `trainer/train_pretrain.py:87-95`
|
||
|
||
Read this to understand: 训练机制的关键超参都来自命令行参数。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
|
||
parser.add_argument("--batch_size", type=int, default=32, help="batch size")
|
||
parser.add_argument("--learning_rate", type=float, default=5e-4, help="初始学习率")
|
||
parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")
|
||
parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")
|
||
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- `batch_size` 是单个 micro-batch 的大小。
|
||
- `accumulation_steps` 决定多少个 micro-batch 做一次 optimizer update。
|
||
- `learning_rate` 是 schedule 的初始值。
|
||
- `dtype` 决定 autocast 用 `float16` 还是 `bfloat16`。
|
||
- `grad_clip` 决定梯度裁剪上限。
|
||
|
||
### 第 3 步:初始化 autocast 和 GradScaler
|
||
|
||
File: `trainer/train_pretrain.py:119-138`
|
||
|
||
Read this to understand: 混合精度由 `autocast_ctx` 和 `GradScaler` 两部分组成。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
device_type = "cuda" if "cuda" in args.device else "cpu"
|
||
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
||
autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)
|
||
...
|
||
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
|
||
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- CPU 上不用 autocast,直接是 `nullcontext()`。
|
||
- CUDA 上会进入 `torch.cuda.amp.autocast(dtype=dtype)`。
|
||
- `bfloat16` 只用 autocast,不启用 GradScaler。
|
||
- `float16` 同时使用 autocast 和 GradScaler。
|
||
- optimizer 是 AdamW。
|
||
|
||
### 第 4 步:每个 micro-step 设置学习率
|
||
|
||
File: `trainer/train_pretrain.py:24-34`
|
||
|
||
Read this to understand: 学习率不是 epoch 级别更新,而是每个 micro-step 更新。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
||
input_ids = input_ids.to(args.device)
|
||
labels = labels.to(args.device)
|
||
last_step = step
|
||
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
|
||
for param_group in optimizer.param_groups:
|
||
param_group['lr'] = lr
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- `step` 从 `start_step + 1` 开始,支持断点续训。
|
||
- `epoch * iters + step` 是跨 epoch 的全局 step。
|
||
- 每个 param group 都会被写入当前 lr。
|
||
- `input_ids` 和 `labels` 先搬到训练 device。
|
||
|
||
### 第 5 步:forward、loss 和梯度累积缩放
|
||
|
||
File: `trainer/train_pretrain.py:35-40`
|
||
|
||
Read this to understand: MiniMind 是先求完整 loss,再除以 `accumulation_steps`。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
with autocast_ctx:
|
||
res = model(input_ids, labels=labels)
|
||
loss = res.loss + res.aux_loss
|
||
loss = loss / args.accumulation_steps
|
||
|
||
scaler.scale(loss).backward()
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- `res.loss` 是语言模型主 loss。
|
||
- `res.aux_loss` 是 MoE 辅助 loss。
|
||
- `loss / accumulation_steps` 是为了让累积梯度保持平均尺度。
|
||
- `scaler.scale(loss).backward()` 兼容 float16 AMP;如果 scaler disabled,就等价于普通 backward。
|
||
|
||
### 第 6 步:到达累积步数后才更新参数
|
||
|
||
File: `trainer/train_pretrain.py:42-50`
|
||
|
||
Read this to understand: 不是每个 micro-batch 都调用 optimizer step。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
if step % args.accumulation_steps == 0:
|
||
scaler.unscale_(optimizer)
|
||
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
||
|
||
scaler.step(optimizer)
|
||
scaler.update()
|
||
|
||
optimizer.zero_grad(set_to_none=True)
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- 只有 `step % accumulation_steps == 0` 时才更新参数。
|
||
- `unscale_` 必须在 `clip_grad_norm_` 前面。
|
||
- `scaler.step(optimizer)` 内部会在 scaler enabled 时检查溢出。
|
||
- `optimizer.zero_grad(set_to_none=True)` 清掉已使用的梯度,为下一组累积做准备。
|
||
|
||
### 第 7 步:日志里的 loss 要乘回去
|
||
|
||
File: `trainer/train_pretrain.py:51-59`
|
||
|
||
Read this to understand: 训练内部用的是缩放后的 loss,但日志要展示原始尺度。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
current_loss = loss.item() * args.accumulation_steps
|
||
current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0
|
||
current_logits_loss = current_loss - current_aux_loss
|
||
current_lr = optimizer.param_groups[-1]['lr']
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- `loss` 已经除过 `accumulation_steps`。
|
||
- 打日志时乘回 `accumulation_steps`,得到原始训练 loss 尺度。
|
||
- `current_logits_loss` 是总 loss 减掉 aux loss。
|
||
- `current_lr` 从 optimizer 里读当前实际学习率。
|
||
|
||
### 第 8 步:epoch 结束时处理尾巴
|
||
|
||
File: `trainer/train_pretrain.py:75-80`
|
||
|
||
Read this to understand: 如果最后剩下不足 `accumulation_steps` 的梯度,也要做一次 optimizer step。
|
||
|
||
Code/config/template excerpt:
|
||
|
||
```python
|
||
if last_step > start_step and last_step % args.accumulation_steps != 0:
|
||
scaler.unscale_(optimizer)
|
||
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
||
scaler.step(optimizer)
|
||
scaler.update()
|
||
optimizer.zero_grad(set_to_none=True)
|
||
```
|
||
|
||
This code shows:
|
||
|
||
- 如果 epoch 里真的跑过 batch,`last_step > start_step`。
|
||
- 如果最后一个 step 不是累积边界,说明还有没 step 的梯度。
|
||
- 这时需要 flush tail,否则最后几个 micro-batch 的梯度会被丢掉。
|
||
|
||
<a id="l14-must-write"></a>
|
||
## 4. 本节必须会写 / 暂时不要求
|
||
|
||
必须会写:
|
||
|
||
1. MiniMind 的 cosine lr:
|
||
|
||
$$
|
||
\eta_s
|
||
= \eta_0 \left(0.1 + 0.45\left(1 + \cos\left(\frac{\pi s}{T}\right)\right)\right)
|
||
$$
|
||
|
||
2. 梯度累积的 loss 缩放:
|
||
|
||
$$
|
||
\tilde{L} = \frac{L}{K}
|
||
$$
|
||
|
||
3. optimizer step 判断:
|
||
|
||
$$
|
||
\text{should\_step}(s, K) =
|
||
\begin{cases}
|
||
\text{True}, & s \bmod K = 0 \\
|
||
\text{False}, & s \bmod K \ne 0
|
||
\end{cases}
|
||
$$
|
||
|
||
4. 尾批 flush 判断:
|
||
|
||
$$
|
||
\text{flush\_tail} =
|
||
(s_{\text{last}} > s_{\text{start}})
|
||
\land
|
||
(s_{\text{last}} \bmod K \ne 0)
|
||
$$
|
||
|
||
5. AMP update 顺序:
|
||
|
||
```text
|
||
scale loss
|
||
-> backward
|
||
-> unscale grad
|
||
-> clip grad
|
||
-> optimizer step
|
||
-> scaler update
|
||
-> zero grad
|
||
```
|
||
|
||
暂时不要求:
|
||
|
||
```text
|
||
1. DDP 梯度同步细节
|
||
2. checkpoint / resume 的完整状态恢复
|
||
3. torch.compile 对训练循环的影响
|
||
4. wandb / swanlab 日志工程
|
||
5. PPO / GRPO 里的 scheduler 版本
|
||
```
|
||
|
||
这一课只要求你能手写 pretrain/SFT 这类监督训练最小循环。
|
||
|
||
<a id="l14-handwrite"></a>
|
||
## 5. 手写模块
|
||
|
||
本节你要补的是:
|
||
|
||
```text
|
||
course/impl/core/train_loop.py
|
||
```
|
||
|
||
### 5.1 补 `cosine_lr`
|
||
|
||
接口:
|
||
|
||
```python
|
||
def cosine_lr(current_step: int, total_steps: int, base_lr: float) -> float:
|
||
...
|
||
```
|
||
|
||
对齐源码:
|
||
|
||
```text
|
||
trainer/trainer_utils.py:40-41
|
||
```
|
||
|
||
你要实现的行为:
|
||
|
||
- 输入 `current_step`、`total_steps`、`base_lr`。
|
||
- 输出和 MiniMind `get_lr` 完全一致的 float。
|
||
- 不要调用原项目的 `get_lr`,要自己写公式。
|
||
|
||
### 5.2 补 `scale_loss_for_accumulation`
|
||
|
||
接口:
|
||
|
||
```python
|
||
def scale_loss_for_accumulation(loss: torch.Tensor, accumulation_steps: int) -> torch.Tensor:
|
||
...
|
||
```
|
||
|
||
对齐源码:
|
||
|
||
```text
|
||
trainer/train_pretrain.py:35-40
|
||
```
|
||
|
||
你要实现的行为:
|
||
|
||
$$
|
||
\tilde{L} = \frac{L}{K}
|
||
$$
|
||
|
||
其中 $K$ 就是 `accumulation_steps`。
|
||
|
||
### 5.3 补 `should_step_optimizer`
|
||
|
||
接口:
|
||
|
||
```python
|
||
def should_step_optimizer(step: int, accumulation_steps: int) -> bool:
|
||
...
|
||
```
|
||
|
||
对齐源码:
|
||
|
||
```text
|
||
trainer/train_pretrain.py:42
|
||
```
|
||
|
||
你要实现的行为:
|
||
|
||
```text
|
||
step=1, K=4 -> False
|
||
step=2, K=4 -> False
|
||
step=3, K=4 -> False
|
||
step=4, K=4 -> True
|
||
```
|
||
|
||
### 5.4 补 `should_flush_tail`
|
||
|
||
接口:
|
||
|
||
```python
|
||
def should_flush_tail(last_step: int, start_step: int, accumulation_steps: int) -> bool:
|
||
...
|
||
```
|
||
|
||
对齐源码:
|
||
|
||
```text
|
||
trainer/train_pretrain.py:75
|
||
```
|
||
|
||
你要实现的行为:
|
||
|
||
```text
|
||
last_step=10, start_step=0, K=4 -> True
|
||
last_step=8, start_step=0, K=4 -> False
|
||
last_step=0, start_step=0, K=4 -> False
|
||
```
|
||
|
||
### 5.5 补 `train_one_epoch`
|
||
|
||
接口已经在骨架里。第一版只需要对齐监督训练主循环,不要写 DDP、wandb、checkpoint。
|
||
|
||
对齐源码:
|
||
|
||
```text
|
||
trainer/train_pretrain.py:24-80
|
||
```
|
||
|
||
建议实现顺序:
|
||
|
||
```text
|
||
1. 遍历 loader,得到 input_ids / labels。
|
||
2. 搬到 device。
|
||
3. 用 cosine_lr 设置 optimizer lr。
|
||
4. 进入 autocast_ctx。
|
||
5. 调 model(input_ids, labels=labels)。
|
||
6. loss = outputs.loss + outputs.aux_loss。
|
||
7. loss = scale_loss_for_accumulation(loss, accumulation_steps)。
|
||
8. scaler.scale(loss).backward()。
|
||
9. 如果 should_step_optimizer 为 True:
|
||
unscale -> clip -> step -> update -> zero_grad。
|
||
10. epoch 结束后,如果 should_flush_tail 为 True:
|
||
再执行一次 unscale -> clip -> step -> update -> zero_grad。
|
||
```
|
||
|
||
注意:
|
||
|
||
- 如果 `scaler is None`,你可以创建 `torch.cuda.amp.GradScaler(enabled=False)`,让 CPU 路径也能跑。
|
||
- 如果 `autocast_ctx is None`,可以用 `contextlib.nullcontext()`。
|
||
- `outputs.aux_loss` 可能是 0 标量,不要特殊跳过。
|
||
|
||
<a id="l14-alignment-test"></a>
|
||
## 6. 对齐测试
|
||
|
||
本节新增对齐测试:
|
||
|
||
```text
|
||
course/impl/tests/test_train_loop_mechanics.py
|
||
```
|
||
|
||
运行命令:
|
||
|
||
```bash
|
||
cd /home/sun/minimind
|
||
python course/impl/tests/test_train_loop_mechanics.py
|
||
```
|
||
|
||
现在还没有实现时,这个测试会因为 `NotImplementedError` 失败。等你补完前四个小函数后,它应该打印类似:
|
||
|
||
```text
|
||
lr_step_0_diff=0.000000000000
|
||
lr_step_1_diff=0.000000000000
|
||
...
|
||
scaled_loss=2.000000
|
||
optimizer_step_micro_steps=[4, 8]
|
||
train_loop_mechanics=passed
|
||
```
|
||
|
||
这个测试先不检查完整 `train_one_epoch`,因为完整训练循环需要模型、数据和 optimizer 共同参与。先把机制函数对齐,再进入阶段组装更稳。
|
||
|
||
另外可以观察原项目 tiny 训练 step:
|
||
|
||
```bash
|
||
cd /home/sun/minimind
|
||
python course/labs/trace_pretrain_step.py --batch_size 2 --max_length 48 --hidden_size 64 --num_hidden_layers 2
|
||
```
|
||
|
||
这个实验会打印:
|
||
|
||
```text
|
||
input_ids.shape
|
||
labels.shape
|
||
logits.shape
|
||
loss
|
||
aux_loss
|
||
lr
|
||
grad_norm_before_clip
|
||
max_first_param_delta_after_step
|
||
```
|
||
|
||
<a id="l14-stage-assembly"></a>
|
||
## 7. 阶段组装
|
||
|
||
本节完成后,Pretrain 阶段会多出训练循环核心能力:
|
||
|
||
```text
|
||
course/impl/core/train_loop.py::cosine_lr
|
||
course/impl/core/train_loop.py::scale_loss_for_accumulation
|
||
course/impl/core/train_loop.py::should_step_optimizer
|
||
course/impl/core/train_loop.py::should_flush_tail
|
||
course/impl/core/train_loop.py::train_one_epoch
|
||
```
|
||
|
||
它们以后会接到:
|
||
|
||
```text
|
||
course/impl/train_pretrain_impl.py
|
||
```
|
||
|
||
阶段目标是:
|
||
|
||
```text
|
||
dataset
|
||
-> DataLoader
|
||
-> CourseMiniMindForCausalLM
|
||
-> train_one_epoch
|
||
-> save course_pretrain checkpoint
|
||
```
|
||
|
||
当前 Pretrain 阶段还缺:
|
||
|
||
```text
|
||
1. 教学版 CausalLM 完整 forward/loss
|
||
2. 教学版 pretrain dataset 或 dataset 适配
|
||
3. checkpoint 保存和加载
|
||
4. tiny pretrain 脚本组装
|
||
```
|
||
|
||
第 14 课只先把训练循环的机械部分写清楚。
|
||
|
||
<a id="l14-check"></a>
|
||
## 8. 本节检查
|
||
|
||
1. MiniMind 的 `get_lr` 起点和终点分别是多少?
|
||
2. 为什么梯度累积时要把 loss 除以 `accumulation_steps`?
|
||
3. 如果 `batch_size=4`,`accumulation_steps=8`,单卡训练的等效 batch size 是多少?
|
||
4. `autocast` 和 `GradScaler` 分别解决什么问题?
|
||
5. 为什么 `clip_grad_norm_` 前要先 `scaler.unscale_(optimizer)`?
|
||
6. `step % accumulation_steps == 0` 控制的是什么?
|
||
7. 为什么 epoch 结束时还要处理不足 `accumulation_steps` 的尾巴?
|
||
8. 写出 `input_ids`、`labels`、`logits`、`loss`、`param.grad` 的形状。
|
||
|
||
<a id="l14-next"></a>
|
||
## 9. 下一课
|
||
|
||
第 15 课讲 checkpoint 与断点续训。
|
||
|
||
下一课要解决:
|
||
|
||
```text
|
||
保存哪些状态;
|
||
为什么只保存 model 不够;
|
||
optimizer / scaler / epoch / step 如何恢复;
|
||
SkipBatchSampler 如何跳过已经训练过的 batch;
|
||
普通权重文件和 resume checkpoint 有什么区别。
|
||
```
|