- 人工智能
- 大模型
- 预训练
- 分布式训练
- 模型优化
- 深度学习
【免费下载链接】modded-nanogpt
NanoGPT (124M) in 90 seconds
本文基于 modded-nanogpt 仓库 speedrun 纪录 2025-09-21_DropAttn/README.md 展开,系统讲解这次把 124M 参数 GPT-2 训练时间推进到151.5 秒的纪录是如何达成的:删除第一个注意力层、将训练步数从 1645 提升到 1680、把验证期的所有长注意力窗口扩展到 20 个 block,并新增iteration_extension参数用于在最终学习率与窗口配置下追加训练步数。读完本文你将理解"哪些层可以安全删掉"背后的可解释性依据、如何用独立于步数的扩展阶段隔离超参影响,以及 Muon 优化器处理 1D 标量参数时的实际行为。
一、本次纪录的改动总览
该纪录建立在此前所有 speedrun 改进(含 PR #130)之上,核心改动共四项:
| 改动 | 内容 | 收益 |
|---|---|---|
| 删除首个注意力层 | Block中 layer 0 不再实例化CausalSelfAttention | 减少一个[768, 768]注意力矩阵的前向/反向开销 |
| 增加步数 | 训练步数从 1645 → 1680 | 换取更低损失 |
| 扩展验证窗口 | 验证时所有长窗口统一扩展到 20 个 block | 损失约 -0.001,约等于 10 步(≈1 秒)的提升 |
新增iteration_extension参数 | 指定在最终 lr 与 ws 下继续训练的步数 | 损失约 -0.001,且大幅简化后续调参 |
从仓库源码看,这次"删除首个注意力层"的拓扑选择已经固化在当时的训练脚本 records/track_1_short/2025-09-21_DropAttn/06350b97-4a98-47da-90c1-3b957af8af6c.txt 的Block定义中:
class Block(nn.Module): def __init__(self, dim: int, head_dim: int, num_heads: int, layer_idx: int): super().__init__() # skip attention of blocks.7 (the 8th layer) by @YouJiacheng self.attn = CausalSelfAttention(dim, head_dim, num_heads) if layer_idx != 7 and layer_idx != 0 else None # skip MLP blocks for first MLP layer by @EmelyanenkoK self.mlp = MLP(dim) if layer_idx != 0 else Nonelayer_idx != 0表明第一个(layer 0)注意力层被直接置为None,前向时跳过;同时 layer 0 的 MLP 也在更早的纪录中被删除(layer_idx != 0),从而形成了"首个 block 只做残差缩放与注入、不承担任何子层变换"的拓扑。
二、为什么可以安全删除第一个注意力层
文档给出了三个相互支撑的理由,这正是"删除层"这一激进操作的可解释性基础:
- 首个注意力层对 induction heads 没有实质贡献:通过观察训练完成后的模型权重(观察文章见仓库纪录背景),发现第一个注意力层没有产生有意义的 induction head 行为,属于"训练后可用性"极低的冗余计算。
- PR #120 已删除第一个 MLP:此前纪录把第一个 MLP 删掉后,模型前两个 block 之间出现了"两个注意力层之间没有中间变换"的结构——注意力层的输出直接喂给下一个注意力层,进一步削弱了首层注意力的存在意义。
- PR #130 的 smear 模块增强了 token 间信息传递:smear 模块(见 2025-09-18_Smear/README.md)以极轻量的方式实现了"把前一个 token 的 embedding 掺入当前 token"(近似
token + 0.07 * prior_token),这恰恰承担了首层注意力中"注意前一个 token"这类低阶功能,使模型不再需要首层注意力做这件事。
从源码层面看,Block.forward中if self.attn is not None与if self.mlp is not None的条件执行让这种稀疏拓扑非常廉价:跳过某层时,该 block 只做x = lambdas[0] * x + lambdas[1] * x0的残差缩放,不触发任何 GEMM。同时,这一设计也考虑了优化器的形状分桶:注释中明确写着"2 matrices x 12 layers = 24 total, which is divisible by 8 GPU world size",即保留 12 个 MLP 槽位、每槽 2 个矩阵,共 24 个[768, 2816]矩阵,恰好能被 8 卡 world size 整除,从而保持 Muon 的 reduce-scatter/all-gather 分片均匀。
三、iteration_extension:把"尾段微调"从总步数中解耦
3.1 为什么需要它
在引入该参数之前,lr(学习率)与ws(注意力窗口)都与总步数强绑定:改动步数会牵动整个后半段训练的行为。文档给出了一个直观例子——如果把步数从 1645 增加到 1655,那么第 1000 步处看到的损失就会突然变化,导致很难判断"损失变化究竟是步数增加造成的,还是其它改动造成的"。这给隔离变量影响带来了巨大困难。
iteration_extension的解法是:在主线训练结束后,追加一段以最终 lr 与最终 ws 运行的扩展阶段。主线步数的任何改动都不会再波及扩展阶段的行为,扩展步数单独可调,从而:
- 带来约 0.001 的损失提升;
- 让针对步数的微调(sweep)变得容易得多。
3.2 在仓库中的现代形态
这一机制在仓库演进中保留了下来。当前 track_1_short/config.py 中:
num_scheduled_iterations: int = int(os.environ.get("NUM_SCHEDULED_ITERATIONS", "1122")) num_extension_iterations: int = 20而 track_1_short/schedule.py 中:
self.total_steps = self.scheduled_iterations + extension_iterations训练阶段表(TRAINING_STAGES)的最后一项就是扩展阶段,其duration为None(不占主线比例),lr_mul被注释为"not used(lr sits at the floor)",并以较小的 batch(8 * 2048 * 8)与更长的窗口(window_sizes=(6, 13))运行——这正是"在最终 lr 与最终 ws 下继续训练"这一设计意图的延续。可见iteration_extension已经从小步追加演化为独立的"扩展阶段"概念,但其核心思想(延长尾段、不触碰主线调度)一脉相承。
四、验证窗口扩展到 20:让最终验证更接近模型真实能力
本次改动把验证期的所有长窗口(long window)统一扩展到 20 个 block(每 block 128 token,即 2560 token 的注意力视野),换来约 -0.001 的验证损失,折算约 10 步、1 秒的训练收益。
这与仓库此前的 YaRN 验证扩展实验一脉相承。2025-09-10_Yarn/ReadMe.md 曾记录"将最终验证窗口从 11 扩展到 13 带来约 0.0015 的损失提升",并观察到奇数窗口表现更佳(ws_short = ws_long // 2的整除行为对不同奇偶窗口不同)。本次纪录把验证窗口进一步推到 20,配合 YaRN 的频率缩放(attn_scale *= 0.2 * math.log(new_window / old_window) + 1),在验证时对更长上下文做无损扩展,从而在"训练窗口较小、验证窗口较大"的配置下更充分地展示模型潜力。
在当前的 track_1_short/config.py 中,这一机制固化为WS_POST_YARN_EXT = 20,注释为 "The final validation extends the long attention window to this many blocks (record #360)",而当前模型 model/gpt.py 的LONG_WINDOW_LAYERS = (3, 10)定义哪些层使用长窗口。可以推断:本次纪录(长窗口层带final_bm = ws_final_layer * args.block_size,对应bm_sizes中下标 4 与 11 的final_bm槽位)正是"验证时把长窗口层放大"这一做法的早期源头。
五、Future Opportunities:Muon 分片的填充浪费
文档还指出了下一步优化方向,这需要结合 Muon 的实现来理解。
5.1 现状:10 个注意力变量 vs 22 个 MLP 变量
删除首个注意力层后,模型中注意力变量(每个[4, 768, 768],即合并的 QKVO 四矩阵)总数降到10,而 MLP 变量(每个[768, 2816],c_fc与c_proj各 12 个)为22。在 Muon 中,同一形状的参数被分到同一 group,并跨 world_size 做 reduce-scatter 后各自计算 Newton-Schulz 正交化,再 all-gather 回全量参数(见 track_1_short/optim/anvil.py 的分片模式)。
文档指出:attention 梯度计算按 16 槽位分片时,10 个真实变量只占 10/16,意味着6/16 的计算落在 padding(全零填充)token 上——这是纯浪费。反观 MLP 侧 22/24,浪费只有 2/24。
5.2 设想:把注意力变量并入 MLP 分片
文档提出的设想是:若能把 2 个注意力变量移入 MLP 分片,使得 MLP 变成 24/24、attention 变成 8/8(即各分片均无 padding),则两块 GEMM 与 NS 迭代的利用率都能打满。这在当时的拓扑下是一个明确的系统级优化空间——它不改变模型结构,只改变优化器按形状分桶的边界,属于"零损失、省时间"的类型。
六、Muon 对 1D 标量变量的行为研究
本次纪录还顺带做了一个优化器层面的实证:
- 当前 attention gates 与 smear gate 这类 1D 标量参数被传给了 Muon;
- 从实现粗看,Muon 的 Newton-Schulz 正交化在 1D 变量上近似退化为
F.normalize(x, p=2, dim=-1)——即无论梯度多大,每一步的步长都被归一化到大致相同的距离,于是 Muon 对 1D 变量来说变成了"对先前梯度做指数平滑、且每步长度近似恒定"的更新器; - 把这类变量换到 Adam 上,实测约 0.5 秒的运行时增加,且损失无改善;
- 直接用
F.normalize(x, p=2, dim=-1)替换 Newton-Schulz 处理这些变量,表现反而略差。
文档作者明确表示对背后的理论尚无完整解释("I do not understand the theory here yet"),但经验上当前方案性能良好。这是一个典型的"先有实证、后补理论"的工程决策,也提醒读者:优化器与参数维度的匹配需要逐个形状实测,不能仅凭理论推断。值得注意的是,仓库在后续演进中(如 2025-10-24_NorMuon、2025-11-10_CautiousWD 等纪录)持续围绕 Muon 的标量参数处理与权重衰减展开调优,说明这个方向在 speedrun 的收益曲线上一直很有价值。
七、验证方法:单样本 t 检验 + 均值标准差
纪录的验证方法论非常严谨,采用多次独立运行 + 统计检验,而不是单次跑分:
import scipy.stats import torch accs = [3.2786, 3.2798, 3.2762, 3.2781, 3.2778, 3.2801, 3.2774, 3.2772, 3.2777, 3.2789] times = [151.559, 151.526, 151.516, 151.527, 151.606, 151.771, 151.546, 151.547, 151.44 , 151.872] print("p=%.4f" % scipy.stats.ttest_1samp(accs, 3.28, alternative="less").pvalue) # p=0.0005 print("acc:", torch.std_mean(torch.tensor(accs))) # acc: (tensor(0.0012), tensor(3.2782)) print("time:", torch.std_mean(torch.tensor(times))) # time: (tensor(0.1305), tensor(151.5910))关键点:
- 单样本单尾 t 检验:
ttest_1samp(accs, 3.28, alternative="less")检验"这批运行的均值是否显著低于 3.28"。得到p=0.0005,远小于 0.05,说明相对 3.28 的基线(此前纪录约 3.2790,见 2025-09-18_Smear/README.md 的统计)改进在统计上显著; - 损失均值 3.2782、标准差 0.0012:10 次运行非常稳定,证明改动不是单次运行的噪声;
- 耗时均值 151.591 秒、标准差 0.1305 秒:整个训练流程的耗时抖动被控制在约 0.13 秒内,这也是 speedrun 竞速能精确对比的前提。
文档同时说明:验证运行完成后,代码的语法与命名做了轻度重构(Code syntax/naming was lightly refactored after performing validation runs),因此发布版脚本与验证版逻辑一致、仅组织方式更整洁。
八、总结与延伸阅读
这次 151.5s 纪录的本质是"三管齐下":
- 结构上:用可解释性依据(induction head 观察 + smear 模块接管低阶功能)安全删除首个注意力层,让 124M 模型的注意力计算进一步向必要功能收敛;
- 调度上:通过
iteration_extension把尾段微调与主线步数解耦,使后续调参可以隔离变量;同时把验证长窗口扩到 20,让验证损失更接近模型真实能力; - 系统上:指出 Muon 形状分片中 attention 侧 10/16 的 padding 浪费,并验证了 1D 标量参数在 Muon 下的实际行为。
想深入跟进这一系列演进,可以继续阅读仓库内的相关纪录:2025-09-18_Smear/README.md(smear 模块)、2025-09-10_Yarn/ReadMe.md(YaRN 窗口调度与验证扩展);调度与参数的现代实现见 track_1_short/schedule.py 与 track_1_short/config.py;稀疏层拓扑在后续纪录中不断演化,当前 11 层模型的层级配置(NO_ATTN_LAYERS、LONG_WINDOW_LAYERS等)可参见 track_1_short/model/gpt.py 顶部常量区,从中可以清楚看到"哪些层跑注意力、哪些层只跑 MLP、哪些层两者皆无"最终如何被系统化地表达与调度。
- 人工智能
- 大模型
- 预训练
- 分布式训练
- 模型优化
- 深度学习
【免费下载链接】modded-nanogpt
NanoGPT (124M) in 90 seconds
相关推荐
modded-nanogpt 153.9s 纪录拆解:异步数据分片预取、首分片部分索引与最终验证窗口扩展
modded nanogpt 153.9s 纪录拆解:异步数据分片预取、首分片部分索引与最终验证窗口扩展 本篇文章以 modded nanogpt Track
人工智能大模型预训练分布式训练模型优化深度学习Partial Key Offset 让 NanoGPT 提速:modded-nanogpt 128.8s 世界纪录中的注意力机制改造实录
Partial Key Offset 让 NanoGPT 提速:modded nanogpt 128.8s 世界纪录中的注意力机制改造实录 导读 本篇文章围绕
人工智能大模型预训练分布式训练模型优化深度学习modded-nanogpt 突破 3 分钟大关:Long-Short 滑动窗口注意力、Attention Scale 与 Adam epsilon 调优实战
modded nanogpt 突破 3 分钟大关:Long Short 滑动窗口注意力、Attention Scale 与 Adam epsilon 调优实战
人工智能大模型预训练分布式训练模型优化深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考