一台 GPU 上的十六天计算
单次长运行
ANDREA-120M 在 RTX 4090 上需要约 23 天(FP16,6 步/分钟,200K 步)。墙上电源、内核崩溃、代理崩溃和故意配置变更都会在该窗口内发生。没有检查点,单个小故障就会丢弃整个运行。
v1 因为一个错误(lr=0.001 过于激进)丢失了 27K 步,因为没有检查点比启动点更近。v2 吸取了该教训:检查点间隔降至每 100 步,并且 CUDA 信号处理器保证在 SIGTERM 时写入检查点。
三个角色
检查点同时承担三个工作:
1. 恢复点。 进程死亡、机器重启、内核崩溃:从最新的 step_NNNNNN.bin 恢复。
2. 优雅切换。 第 112,619 步:更改课程而无需重新训练。SIGUSR1 强制干净检查点,代理停止,新上限与下限提交,CUDA 从保存点在新策略下恢复。
3. 审计分叉。 在相同起始权重比较两个配置:复制一个检查点,向前运行两个分歧分支,观察哪个收敛。
每 100 步提供约 17 分钟的训练时间(以 6 步/分钟计算),在此之间进行一次写入。100 步也与 sample_every 匹配:每个检查点对应一次新的样本审计,且每个样本审计对应一个可恢复点。
一个文件的三种角色
一个文件中的五个区域
格式
每个 step_NNNNNN.bin 打包了五个连续区域:
[int32 step] [int32 n_params] [n_params x float32 weights] [n_params x float32 m] [n_params x float32 v]
逐区域解析
头部(总共 8 字节)。 一个 32 位步数告诉我们这个快照位于训练过程中的哪个位置;一个 32 位参数计数告诉我们后续三个数组各自的大小。
权重(n_params x 4 字节)。 每个学习参数,扁平化排列。顺序与模型的参数迭代器匹配:令牌与位置嵌入,然后是每层的注意力与 MLP 权重,最后是输出头。
Adam 一阶矩 m(n_params x 4 字节)。 过去梯度的 EMA(beta1 = 0.9)。形状与权重相同。AdamW 恢复所需。
Adam 二次矩 v (n_params x 4 字节)。 过去平方梯度的 EMA (beta2 = 0.999)。与权重形状相同。AdamW 恢复所需。
总大小
总字节数 = 8 + 12 x n_params。ANDREA-12M (1280万参数):磁盘上 154 MB (147 MB 四舍五入)。ANDREA-120M (~1.2亿参数) FP32:~1.44 GB。三个相同形状的数组,背靠背堆叠,加上一个微小的头部。
为什么保存 m & v
Vanilla Adam 通过 m & v 跟踪每个参数的学习率。在检查点写入时丢弃它们,恢复运行将从零动量和零方差估计开始,相当于一步学习率为 0,然后突然斜坡。损失激增;模型可能脱离当前盆地。保存 m & v 使恢复与从未停止的基线位元等价(除了数据加载器随机性)。
Sizing One Checkpoint
SIGTERM & SIGUSR1
为什么 CUDA 处理信号
训练作为一个长期运行的前台进程。当代理或操作员希望 GPU 停止时,会向 CUDA 引擎发送一个信号。没有处理程序,默认的 SIGTERM 会立即终止进程:飞行中的梯度计算被丢弃,自上次检查点以来的最新权重丢失。有了处理程序,引擎会先写入检查点,然后干净地退出。
SIGTERM:写入并退出
由停止按钮、systemctl stop 或父代理的 kill 发送。CUDA 完成当前步骤,将 step_NNNNNN.bin 写入磁盘,然后退出。从此状态恢复只需最新的 .bin:除了飞行中的部分步骤外,没有工作丢失。
SIGUSR1:写入并继续
由操作员或代理脚本按需发送。CUDA 完成当前步骤,写入 step_NNNNNN.bin,然后像什么都没发生一样继续训练。有用场景:就在配置更改前触发审计点;在已知良好时刻捕获权重;将检查点与外部样本质量评估运行对齐。
波兰转轴序列(步骤 112,619)
1. 操作员向 CUDA 发送 SIGUSR1。在下一个 100 步边界(步骤 112,700)写入检查点。
2. 操作员停止代理。
3. .samples.json 和 .state.json 被归档(样本日志和 bandit 状态作为历史记录保存)。
4. .loss.json 保持原位。 累积训练历史;永不归档。
5. 代理在新上限和下限下重启。
6. CUDA 从 step_112700.bin 恢复,使用新的 bandit 但完整的权重、m 和 v。
损失历史在切换点连续不断。样本日志干净重置。Bandit 在新策略下获得全新开始。
选择信号
累积训练历史
三个侧车文件
在每个检查点旁边,代理会在运行目录中维护三个 JSON 侧车文件:
- .loss.json -- 每个步骤一条记录,永久保存。到运行结束时约 200,000 条记录。累积训练历史。
- .samples.json -- 最近生成的样本,用于审计。在抛光转折时重置。
- .state.json -- 匪徒臂拉动、EMA 奖励、阶段计数器。在抛光转折时重置。
什么会重置,什么会持久化
波兰转折是策略变更,而不是运行重置。模型的权重、m、v 和损失历史都保持不间断连续。强盗的累积奖励不继续:新的上限和下限定义了不同的策略,强盗必须在新策略下从干净状态重新学习。
为什么 .loss.json 保持不变
损失历史作为运行的审计轨迹。每一条关于 ANDREA-120M 的已发布声明(110K 步时的损失 EMA、波兰转折恢复、112K 步时的收敛)都追溯到此文件中的条目。在阶段之间归档 .loss.json 将迫使读者拼凑碎片来重建运行;保持其仅追加且未触碰的状态保留了来源性。
僵尸臂教训
第 112,619 步在 .state.json 中发现了一个 repo-docstrings 臂,它携带着来自先前运行的权重 1.546。强盗状态在早前的重启中被保留,但数据源已不可用,导致产生了扭曲探索计数的僵尸拉取。教训:强盗状态允许在重启中以令人惊讶的方式漂移。损失历史是运行整个生命周期中必须保持未触碰的唯一文件。
一条统御一切的规则
在各阶段之间自由归档 .samples.json 和 .state.json。绝不归档 .loss.json。最新的 .loss.json 始终是规范的训练历史。
应用规则
构建了什么 & 为什么
五个决策
1. 节奏:每 100 步。 恢复点粒度 ~17 分钟。与 sample_every 对齐,因此每个检查点对应一个新鲜样本审计。
2. 格式:头部 + 3 个数组。 最小化:8 字节头部告诉我们每个尾随数组的大小。没有元数据膨胀。当 m & v 被保存时,实现位等价的恢复。
3. 信号:SIGTERM & SIGUSR1。 两个角色,两个信号。默认 systemd 关机通过 SIGTERM 获取干净检查点;按需审计点通过 SIGUSR1 获取干净检查点而不停止。
4. 损失连续性:永不归档。 累积训练历史在抛光转向、重启和策略变更中持续存在。整个运行的一个审计轨迹。
5. Bandit 状态:允许重置。 Bandit 策略在同一时间仅在一个配置下运行。抛光转向重置;损失历史继续。两个不同生命周期共享同一运行目录。
本课与什么内容相连
- 活动 23 (grow_a_language_model_sample_audit)。sample_every 节奏与检查点节奏匹配;两者每 100 步触发一次。
- 活动 24 (grow_a_language_model_microgpt_to_andrea)。v1 崩溃、v2.5 补丁、v3 抛光转向都需要干净的检查点才能运行。
- 活动 10 (grow_a_language_model_adamw)。在检查点中保存 m 和 v 很重要,因为 AdamW 的更新规则依赖于两者。丢弃它们并恢复会导致分歧。
最后一个工程真理
代码比作者持久。基础设施比建造者持久。一个简单的检查点格式比每一种承诺跳过保存优化器状态的复杂恢复方案都持久。节省字节;保存运行。