05 - 第一次完整预训练与复盘
前面四篇产出的是代码,这一篇产出的是一次真实的训练记录。
需要说明这一篇的性质:它现在是模板,不是记录。 表格里的数字要等真开卡跑完才能填。之所以先写出来,是因为该记什么必须在开跑之前就定好——训练跑起来之后再想「早知道该记一下这个」,就得重跑。
前置:01 到 04 篇的验收清单全部通过。
一、开跑前的最后检查
前面每篇都有验收清单,这里只列跳过了就会烧掉真金白银的那几条。
| 检查项 | 出自 | 跳过的后果 |
|---|---|---|
train.bin 随机位置 decode 回文本通顺 | 01 篇六节 | 数据是乱的,17 小时全废 |
val.bin 的文档不在 train.bin 里 | 01 篇 5.7 | 验证 loss 偏低,误判模型变好 |
| 参数量对得 上 502,193,664 | 02 篇 8.1 | 模型结构写错 |
| 初始 loss 在 10.37 附近 | 02 篇 8.3 | 偏低说明因果掩码失效,模型在抄答案 |
| 因果性测试通过 | 02 篇 8.4 | 同上,且这是唯一能主动发现它的手段 |
| 杀掉进程能从 checkpoint 恢复且曲线接得上 | 03 篇十节 | 实例被回收时无法续跑 |
| checkpoint 路径指向持久化云盘 | 03 篇 8.3 | 实例回收后 checkpoint 一起没 |
| 多卡 loss 曲线与单卡重合 | 04 篇六节 | 种子没错开,四张卡在算同样的数据 |
最后两条最容易漏,也最贵。 前者的代价是整轮训练白跑,后者的代价是花了四倍的钱得到单卡的效果。
二、跑起来之后盯什么
2.1 六个指标
| 指标 | 记录频率 | 健康的样子 |
|---|---|---|
| 训练 loss | 每 10 步 | 从 10.37 持续下降 |
| 验证 loss | 每 200 步 | 跟随训练 loss,不明显背离 |
| grad norm | 每 10 步 | 稳定小幅波动,不长期贴着裁剪阈值 |
| 学习率 | 每 10 步 | 与 03 篇 5.3 打印的曲线一致 |
| tokens/s | 每 10 步 | 稳定,用来估剩余时间 |
| MFU | 每 10 步 | 30% 以上;低于 30% 查瓶颈 |
另外每次存 checkpoint 时记一下峰值显存和已消耗卡时。前者用来判断还能不能加大 batch,后者用来对成本。
2.2 日志要落盘,不要只打屏
torchrun 的输出在实例被回收时一起消失。把日志重定向到持久化云盘上,和 checkpoint 放一起:
torchrun --standalone --nproc_per_node=4 train_ddp.py --mode ddp \
2>&1 | tee /mnt/persistent/logs/run_$(date +%Y%m%d_%H%M).log
复盘时要回看的是曲线,不 是最后一屏。
三、loss 曲线的四种形状
绝大多数训练问题在 loss 曲线上有特征,而且前两百步就能看出来。开跑之后先盯着前两百步,不对就立刻停,别等 17 小时。
对应的诊断:
| 形状 | 最可能的原因 | 先查什么 |
|---|---|---|
| ① 健康 | 正常 | 继续,但仍要看 grad norm |
| ② 中途发散 | loss 忘了除 grad_accum;学习率太高;没做梯度裁剪 | 03 篇 3.3、5.4、第六节 |
| ③ 完全不降 | 学习率被设成 0;忘了 optimizer.step();warmup 步数写成了总步数 | 03 篇五节 |
| ④ 降得异常快 | 因果掩码失效,模型在抄答案 | 02 篇 8.4 因果性测试 |
第四种最危险,因为它伪装成好消息。02 篇 8.3 说过:初始 loss 明显低于 10.37 是同一个问题的早期信号。
四、翻车现场与处置
按「你会先看到什么」组织,而不是按原因分类。
| 你看到的现象 | 大概率原因 | 处置 |
|---|---|---|
| 启动就 OOM,只有卡 0 显存满 | 忘了 torch.cuda.set_device(local_rank) | 04 篇 2.2 |
| 训练卡死,四张卡利用率 100% 但不动 | 某些 rank 少调了一次 collective | 设 TORCH_NCCL_BLOCKING_WAIT=1 让它报错,见 04 篇 5.3 |
| MFU 只有百分之十几 | 数据加载跟不上 | 检查 pin_memory;调大 micro_batch |
| 屏幕刷四份重复日志 | 没判断 rank == 0 | 04 篇 2.2 |
| 恢复 checkpoint 后 loss 跳一下 | 只存了权重,没存优化器状态 | 03 篇 8.1 |
| 恢复后学习率不对 | step 没存进 checkpoint | 同上 |
| 存 checkpoint 时进程被杀,旧的也没了 | 没做原子写 | 03 篇 8.2 |
中途发现要改超参怎么办。 如果只是学习率偏高,可以从最近的 checkpoint 恢复、调低学习率继续,不必从头。但如果是数据或模型结构的问题,从头重跑是唯一选择——这也是为什么第一节那张检查表要在开跑前全过一遍。
五、跑完要填的表
5.1 基本信息
| 项 | 值 |
|---|---|
| 开跑时间 | 待填 |
| 硬件 | 待填(型号、卡数、NVLink 还是 PCIe) |
| 并行方式 | 待填(DDP / FSDP) |
| 实际总步数 | 待填 |
| 实际总耗时 | 待填 |
5.2 性能实测
对照 03 篇 9.3 那张「耗时与 MFU 对应关系」表填:
| 指标 | 估算值 | 实测值 |
|---|---|---|
| tokens/s | 163,399 | 待填 |
| MFU | 39.5% | 待填 |
| 单步耗时 | 6.42 s | 待填 |
| 峰值显存 | 8 GB 静态 + 激活 | 待填 |
| 4 卡相对单卡加速比 | 3.7~3.9×(04 篇 4.2 预期) | 待填 |
估算与实测的偏差本身就是复盘内容。 差得多说明账算错了或有瓶颈,这比数字本身更值得记。
5.3 效果
| 指标 | 值 |
|---|---|
| 初始 loss | 待填(应 ≈ 10.3735) |
| 最终训练 loss | 待填 |
| 最终验证 loss | 待填 |
| 对应困惑度 | 待填(08 篇 4.1,PPL = exp(loss)) |
5.4 成本
按 00 篇 3.4 的式子 填:
| 项 | 卡时 | 金额 |
|---|---|---|
| 数据准备(CPU,不算 GPU) | — | 待填 |
| 冒烟测试(TinyStories) | 待填 | 待填 |
| 多卡基准测试(04 篇 4.1) | 待填 | 待填 |
| 正式预训练 | 待填 | 待填 |
| 翻车重跑 | 待填 | 待填 |
| 合计 | 待填 | 待填 |
00 篇按 150~200 卡时做的预算,实际花了多少、超没超、超在哪一项,是这张表要回答的。
5.5 翻车记录
每次翻车记三行:现象、原因、损失的卡时。这是整篇复盘里对下一次最有用的部分——第四节那张表就是这么积累出来的。
六、验收
- 训练跑完,最终 checkpoint 在持久化存储上
- 完整日志已落盘,能画出 loss 曲线
- 5.1 到 5.5 五张表填完
- 估算与实测的偏差已解释
- 用 08 篇的方法算出困惑度
- 拿 base 模型生成一段文本,确认它在说人话(哪怕内容是错的)
最后一条是最直观的验收。0.5B 训 10B token 的 base 模型不会回答问题(06 篇 0.2 节讲了为什么),但它续写出来的文本应该语法通顺、词能搭上。如果还是乱码,说明前面某处有问题,别急着进 06 篇。