数据截至 (上游 commit 3adf61e154c3)
02 · 训练循环:300 行里的完整预训练
这一章讲什么:
train.py的 337 行如何覆盖「从零训一个 GPT-2」的全部环节——配置、分布式、混合精度、梯度累积、学习率调度、评估存档。它是一张「完整训练循环零件清单」的最小参考实现。
1. 它要解决的小问题
「训练 = 前向、反传、更新」这句话漏掉了真实训练里的全部杂事:
- 超参有 40 个,怎么管理才不烦?
- 8 张卡怎么一起训?
- 显存装不下大 batch 怎么办?
- 混合精度训练怎么不丢精度?
- 学习率什么时候开始降、降到多少?
- 训到一半断了怎么办?
- 怎么知道 GPU 有没有被喂饱?
train.py 每问给一个 5~20 行的答案。本章按执 行顺序走一遍。
2. 执行顺序总览
脚本从上到下就是初始化流水线,最后进入一个 while True 主循环(train.py:255):
① 默认超参写成全局变量 (train.py:32-74)
② exec configurator.py → 配置文件/命令行覆写 (train.py:77)
③ DDP 初始化: rank/world_size/设备 (train.py:82-100)
④ TF32 + autocast 上下文 (train.py:107-112)
⑤ get_batch 定义 (train.py:116-131)
⑥ 建模型: scratch / resume / gpt2 三选一 (train.py:149-188)
⑦ GradScaler + AdamW + torch.compile + DDP 包装 (train.py:196-212)
⑧ while True: 设 LR → 定期评估存档 → N 个 micro-step → clip → step
3. 机制一:穷人配置系统(configurator.py)
小问题: 不想引入 Hydra/argparse 这类配置框架,又要支持「配置文件 + 命令行覆盖」。
nanoGPT 的野路子: 超参就是脚本顶部的全局变量;train.py:77 用 exec(open('configurator.py').read()) 把覆盖逻辑直接执行在当前全局命名空间里。
configurator.py 对命令行参数分两类处理(configurator.py:20-47):
- 不含
=的参数 → 当成配置文件路径,整个文件 exec 进来(configurator.py:28)。所以config/train_gpt2.py就是一个普通 Python 文件,里面直接写batch_size = 12。 --key=value→literal_eval尝试解析成 Python 字面量,再断言类型与原全局变量一致后覆写(configurator.py:31-45)。
坑: 类型断言 type(attempt) == type(globals()[key])(configurator.py:42)意味着 --dropout=0 会被拒(int ≠ float,默认是 0.0)——命令行要写 --dropout=0.0。作者自己在文件头承认这是「Probably a terrible idea」,但省了所有配置样板代码(configurator.py:1-15)。