本页对应 phase1-124m/04_gpt2_124m.py。架构内核和前几关一模一样(注意力 + FFN + 残差 + LayerNorm),
变的是规模和一整套"让大模型训得稳、训得动"的训练工程。这一页把"配置怎么涨 → 真数据怎么喂 →
显存不够怎么办 → 学习率怎么排 → 最后收敛到哪"五步拆开,每步亲手拨一个旋钮。
第 3 关那个字符级小 GPT(nanoGPT 风格)和这一关的 GPT-2 124M,零件清单完全一样 —— 区别只是每个数字往上跳了一大截。点下面两个按钮,看同一张配置表里哪一列被点亮,以及每行涨了多少倍。
| 超参 | 字符级玩具(03) | GPT-2 124M(04) |
|---|
GPTConfig.vocab_size)。原因纯粹是硬件:
GPU 的矩阵乘对"维度是 2 的幂 / 128 的倍数"算得更快。多出来的 50304-50257=47 行
永远不会作为真实 token 出现,学不到也无害,白拿一点速度。
" the"、"ing"),
50304 个 token 覆盖了真实英文,所以同样长度能塞下多得多的信息。架构没变,只是"最底层的字典"换大了。
训练数据是 FineWeb-Edu:
先用 GPT-2 的 BPE 把文本切成 token id,再把上百亿个 id 存成一批
shard
(.npy 文件)。训练时 DataLoaderLite 一片片顺序读、读完换下一片。点「▶ 开始喂数据」看指针滑过。
.npy(int token id);DataLoaderLite.next_batch 切出 B×T+1 个连续 token,前 B×T 个做输入、后移一位做标签。读完一片换下一片。DataLoaderLite 同一时刻只 np.load 一片到内存,指针 pos 在片内滑动,
到头了 cur_shard+1 换下一片(循环)。这样无论数据多大,内存占用是常数。
prepare_fineweb.py 里用 GPT-2 的 tokenizer 完成。
GPT-2 的等效大 batch 是 524288token/步(total_batch_size,219)。
单卡一次只喂得下一个微 batch:8micro_batch 条序列 × 1024seq_len = 8192 token。
办法:连喂若干个微 batch、把梯度累加进"蓄水槽",攒满了才 optimizer.step() 走一步。拖滑块看要攒几次。
524288 个 token 的损失求平均。代码里
loss = loss / grad_accum 再 loss.backward():每个微 batch 贡献 1/N,
累加 N 次正好等于"在一个大 batch 上算一次平均梯度"。不除的话梯度会被放大 N 倍,等于偷偷把学习率乘了 N。
学习率不是常数。前 warmup_steps热身 步从 0 线性爬到峰值
6e-4max_lr,之后余弦平滑退火到
6e-5min_lr(= max_lr × 0.1),末尾保持不变。
拖两个滑块,曲线实时精确重画(就是 get_lr 这个纯函数,不是示意)。
get_lr 里就是 step < warmup_steps 时
max_lr × (step+1) / warmup_steps 这条线性斜坡。
coeff = 0.5×(1+cos(π·ratio)),把进度 ratio 从 0→1 平滑映射成系数 1→0,
再插值到 [min_lr, max_lr] 之间。
betas=(0.9, 0.95)(GPT 系比默认 0.999 更小的二阶动量)、eps=1e-8、fused=True(把更新融合成一个 CUDA kernel 提速)。
weight_decay=0.1 还分组:只对 2 维参数(矩阵、嵌入)做衰减,bias 和 LayerNorm 的增益/偏置不衰减
(configure_optimizers 里的 decay / nodecay 两组)。另外每步还做梯度裁剪把范数截到 1.0,防偶发巨梯度炸训练。
随机初始化时,模型对 50257 个 token 完全瞎猜,loss ≈ ln(50257) ≈ 10.82(代码开训前会打印这个基线)。
训练让它一路下降。本项目没有保存逐 step 的 loss,只有两次跑各自的终点 —— 下面是这两个真实端点,
以及它们对应的数据量差 33×。
torch.compile(把模型编译成更快的 kernel)、Flash-Attention
(F.scaled_dot_product_attention,不显式建 T×T 矩阵)。它们让同样的训练更快更省,但 loss 曲线该到哪还是到哪 —— 所以本页只点到为止。