GPT-2 124M GitHub 代码训练 | AI生成和翻译

Home 2026.06

问题: 基于 lzwjava/zz 中的训练笔记和配置/脚本,提供GPT-2 124M在GitHub代码上训练运行的全面技术总结。


GPT-2 124M 在 GitHub 代码上的训练 —— 全面总结

1. 流水线概览

三段式流水线,全部在 lzwjava/zz 中自定义构建:

codeparrot/github-code (HuggingFace)
        ↓ download_github_code.py
   38 个 parquet 分片(共 1126 个),约 27 GB
        ↓ tokenize_github_code.py
   282 个 .npy 分片(通过 tiktoken 使用 GPT-2 BPE)
   140 个训练分片 + 1 个验证分片
        ↓ train.py + config/train_github_code_124m.py
   nanoGPT 训练循环,427k 步

数据集: codeparrot/github-code 是一个大规模开源代码数据集。你下载了 1126 个 parquet 分片中的 38 个,覆盖约 140 亿 token,分布在 140 个训练分片中。验证分片是 1 个文件——很可能是保留的语言或分片索引,提供了一个干净的、类似分布外 (OOD) 的验证信号。


2. 模型架构

标准 GPT-2 124M —— 未作修改:

超参数
n_layer 12
n_head 12
n_embd 768
block_size 1024
dropout 0.0
bias False
总参数量 ~1.63亿(1.24亿非嵌入参数)

参数量差异(命名的 1.24亿 与 总参数量 1.63亿):嵌入表大小为 50257 × 768 ≈ 3860万 参数,不计入“124M”标签。前向传播为标准因果 transformer:token + 位置嵌入 → 12 个仅解码器注意力块 → LM 头(权重与嵌入绑定)。bias=False 遵循 GPT-3 论文的发现,即偏置在大规模下不必要。


3. 训练配置

批次几何:

micro_batch = 4 个序列 × 1024 token = 4096 token
grad_accum  = 8
有效批次大小 = 4096 × 8 = 32768 token/步

32768 token/步与 Karpathy 在 nanoGPT Shakespeare/OpenWebText 运行中使用的有效批次大小相同——对于单 GPU 来说是一个可靠的选择。

优化器(GPT-3 风格 AdamW):

lr            = 6e-4        # 峰值学习率
min_lr        = 6e-5        # 通过余弦调度 10 倍衰减
warmup_iters  = 2000        # 线性预热
max_iters     = 427000
lr_decay_iters = 427000     # 整个运行期间完全余弦衰减
weight_decay  = 0.1
beta1, beta2  = 0.9, 0.95  # GPT-3 默认值
grad_clip     = 1.0

余弦衰减贯穿全部 427k 步——学习率在结束时恰好达到 min_lr,这是标准做法(无冷却阶段)。这正是 GPT-3/Chinchilla 风格运行的做法。

Chinchilla 框架: Chinchilla 对 124M 参数的最优训练量约为 25 亿 token。你在 140 亿 token 上训练,这相对于朴素 Chinchilla 比例是约 5.6 倍的过度计算。这是为了推理效率而有意为之——一个过度训练的较小模型在服务时运行得更快(“Llama 哲学”)。但也意味着训练后期梯度信号减弱,这解释了为什么你在约 7 万步后看到 val_loss 增加


4. 硬件与吞吐量

指标
GPU RTX 4070 12GB
显存使用 ~5050 MiB
功耗 ~208W
温度 ~65°C
每步时间 平均 ~636 ms/步,稳态 621 ms
吞吐量 ~51900 token/秒
MFU 14.44%

MFU 分析: 在 RTX 4070 上运行 nanoGPT 且使用 torch.compile 时,14.44% 的模型 FLOP 利用率对于单个消费级 GPU 来说是典型的。RTX 4070 的理论 BF16 吞吐量约为 165 TFLOPS。GPT-2 124M 的前向+反向传播大约需要 6 × N × D = 6 × 140 亿 × 1.63 亿 ≈ 1.37 × 10^19 总 FLOPs。与峰值 MFU 的差距由以下原因解释:

torch.compile 配合 BF16 在此发挥了关键作用,否则你可能会得到 8-10% 的 MFU。


5. 损失曲线与过拟合分析

步数 val_loss 说明
0 10.87 随机初始化(ln(vocab) ≈ ln(50257) ≈ 10.8 ✓)
14k 5.87 前期快速下降
25k ~3.6 语法结构开始出现
70k 2.08 最佳泛化
325k 2.79 过拟合逐渐显现
427k 3.47 最终检查点(已退化)

损失从第 70k 步到第 427k 步增加是关键信号。保存的 ckpt.pt 是最终步数,而非最佳 val_loss——你应当重载大约第 60-80k 步的检查点以获得最佳模型。一种 best_val_loss 检查点保存策略(仅在 val_loss 改善时保存)本可以捕捉到这一点。

为什么 val_loss 在训练损失下降的情况下反而增加? 典型的过拟合 + 学习率衰减导致的分布偏移:

在约 5.6 倍 Chinchilla 时出现的发散证实:在此数据集大小下,模型在第 70k 步左右已经饱和。剩余的 357k 步是梯度下降在记忆化而非泛化上的努力。


6. 总计算预算

总 token 数:140 亿
计算量(前向+反向):~6 × 140 亿 × 1.63 亿 = 1.37 × 10^19 FLOPs
实际时间:75.4 小时
GPU 小时:75.4 GPU 小时(RTX 4070)
RTX 4070 TDP:200W,实际:208W
能耗:75.4h × 0.208 kW ≈ 15.7 kWh

与 GPT-2 原始训练对比:OpenAI 使用 256 个 TPU v3 训练 GPT-2 1.5B。你用一块消费级 GPU 在 3 天内训练了 124M。相对成本说明了计算资源如何变得民主化。


7. 模型实际学到的东西

在 val_loss ~2.08(最佳)时,模型生成的代码具有:

在 val_loss 3.47(最终)时,生成质量在未见代码上实际上更差,尽管经过了更多训练步骤——模型内化的训练数据与验证分片之间的分布差距是真实存在的。


8. 经验教训 / 下一步

当前运行的即时修复:

# 在 train.py 中添加:仅在 val_loss 改善时保存检查点
if val_loss < best_val_loss:
    best_val_loss = val_loss
    torch.save(checkpoint, 'ckpt_best.pt')

对于下一次运行——修复过拟合:

  1. 在第 ~80k 步提前停止 —— 你有 140 亿 token,但模型在约 25 亿(Chinchilla)时饱和。压缩相同的学习率调度,训练至 80k 步(约 26 亿 token)。

  2. 添加 dropout —— dropout=0.1 将减缓过拟合,但会损失约 5% 的吞吐量。

  3. 更多验证多样性 —— 1 个验证分片信号弱。使用 3-5 种不同语言的分片作为验证集,以捕捉语言特定的过拟合。

  4. 扩大规模 —— 你用于 GPT-2 760M 的 AMI MI300X 可以在约 6-8 小时内以 Chinchilla 最优方式训练这个 124M 模型。或者用它在这个 140 亿 token 数据集上训练一个 760M 模型:6 × 140 亿 × 7.6 亿 ≈ 6.4 × 10^19 FLOPs —— 在 MI300X 上约需 12-15 小时,而在 4070 上则需要数月。

  5. 正确评估 —— 在 HumanEval 代码片段上的困惑度或简单补全上的 pass@k 可以提供超越 val_loss 的具体代码质量指标。


总结: 完整的端到端训练。75 小时,140 亿 token,消费级硬件,自定义流水线。真正的胜利在于基础设施 —— 下载 → 分词 → 训练 → 检查点均正常工作。模型在第 ~70k 步达到峰值(val_loss 2.08),后期退化是在固定数据集上训练到 Chinchilla 的 5.6 倍且无正则化情况下的预期结果。下一次运行:提前停止 + 压缩调度 = 在 1/5 的时间内获得更好的模型。


Back Donate