Ultra-Scale 容错
分布式训练 · 2024

Ultra-Scale 训练的容错:419 次意外中断,为什么有效训练时间还能高于 90%

Llama 3 405B 在最多 16,384 张 H100 上的 54 天预训练快照中记录到 419 次意外中断,仍报告高于 90% 的有效训练时间。checkpoint 把恢复变成“回到最近快照 + 重跑”,代价是写带宽、存储和一致性。

Meta Llama Team · arXiv 2407.21783v3 原文 ↗ nanotron ↗
TL;DR · 30 秒抓住核心

万卡训练的故障不是例外:Llama 3 405B 的 54 天快照里,466 次作业中断,其中 419 次是意外中断;按这个计数折算,集群平均每 3.09 小时就有一次意外中断(本页按 54 天 ÷ 419 折算,不是论文给出的 MTBF 指标)。同步训练里,单卡故障可能要求整个作业重启。

checkpoint 把恢复变成“回到最近快照 + 重跑”。频率越高,丢失的工作越少,但每次停顿和写入量越大;同步 checkpoint 的停顿落在关键路径上,异步 checkpoint 把磁盘写入挪到后台,但快照复制仍要占时间。

Llama 3 的实测是:在 16,384 张 H100 的 54 天预训练快照、419 次意外中断下,有效训练时间仍高于 90%,只有 3 次需要重大人工干预。论文没有报告 checkpoint 间隔和单次停顿,所以不能从 90% 反推出这两个参数。

本页用一个简化账本把 trade-off 算清楚:有效时间 ≈ 1 − I/(2·MTBF) − C/I。它不替代论文数据,只用来解释为什么“更频繁”不一定更好。

419
54 天内的意外中断次数(另有 47 次计划中断)
>90%
有效训练时间:有用训练时间 / 总耗时
58.7%
GPU 问题占全部意外中断的比例
01

规模上来后,故障从“意外”变成“日常”

读完这节你应该知道:万卡训练的故障间隔有多短,为什么一次单卡故障会放大成整个作业的重启。

Llama 3 405B 的训练最多用到 16,384 张 H100(Table 4)。论文对这套集群的判断很直接:16K GPU 训练的复杂度和潜在故障场景超过他们运维过的更大 CPU 集群;训练是同步的,单张 GPU 故障可能要求整个作业重启(§3.3.4)。

论文给出一个 54 天的预训练快照:期间共 466 次作业中断,47 次是计划中断(固件升级、配置或数据更新),419 次是意外中断(§3.3.4)。把 419 次摊到 54 天,得到 3.09 小时/次的集群级平均间隔(本页折算,不是论文给出的 MTBF 指标)。换句话说,按这个快照,集群平均每 3 小时左右就要处理一次意外中断。

MTBFcluster = 54 天 × 24 h / 419 = 3.09 h

按 §3.3.4 的 419 次意外中断折算;这是“集群级”间隔,不是单卡 MTBF。

故障来源并不集中在“GPU 坏了”这一项。Table 5 的类别里,Faulty GPU 148 次、GPU HBM3 Memory 72 次、Software Bug 54 次、Network Switch/Cable 35 次、Host Maintenance 32 次;GPU 相关类别合计仍是最大头,论文正文写 GPU 问题占 58.7%(§3.3.4)。

这就是万卡容错的基本矛盾:在 16K 卡的尺度上,故障不再是低频例外;同步训练又把单点故障的恢复粒度放大到整个作业。checkpoint 不是“可选优化”,而是把故障损失限制在一个可计算窗口内的基础设施。

02

同步 checkpoint 把恢复变成“回到快照再重跑”

读完这节你应该知道:最直接的方案为什么有效,又为什么会在万卡规模上撞到停顿和写带宽。

最直接的容错方案是周期性 checkpoint:所有 rank 停下来,把模型、优化器、学习率调度器、RNG 和数据读取位置写进共享存储,然后继续训练。故障发生后,作业重启,加载最近一次完整 checkpoint,从那个 iteration 继续,未 checkpoint 的 step 全部重跑。

Llama 3 也做周期性 checkpoint;论文未说明它是同步还是异步。§3.3.1 写明:checkpoint 保存每张 GPU 的模型状态,单卡范围 1 MB 到 4 GB;目标是缩短 checkpoint 期间的 GPU 停顿,同时提高 checkpoint 频率以减少恢复后丢失的工作。这两句话本身就是一对矛盾:频率越高,丢失越少,但停顿和写入次数越多。

存储侧的压力是实打的。Llama 3 的存储 fabric 基于 Tectonic,240 PB、7,500 台 SSD 服务器,持续吞吐 2 TB/s、峰值 7 TB/s;论文特别指出,checkpoint 写入是高度突发的,会在短时间内打满存储 fabric(§3.3.1)。

同步方案还有两个结构性代价:

工程侧可以对照 Hugging Face Ultra-Scale Playbook 的 “Lessons learned on benchmarking”:他们遇到 Slurm 强制终止作业、进程清理失败、作业无限挂起,并把大量时间花在缩短集群重启和 idle time 上。这是工程资料,不是论文实验数字;本页只在说明“恢复流程本身也是成本”时引用。

所以“同步 checkpoint + 重跑”不是错,而是把问题从“故障不可控”变成了“窗口可计算”:剩下的问题是怎么让窗口更小、让停顿更短。

03

把状态切片、异步写出,checkpoint 才能跟上故障率

读完这节你应该知道:一个可恢复的 checkpoint 里到底有什么,分布式框架怎么分片和写元数据,异步写又把停顿挪到了哪里。

以下实现细节来自 PyTorch、Megatron、DeepSpeed 源码与 nanotron 文档;论文数字只来自 Llama 3 §3.3.1、§3.3.4、Table 4、Table 5。

checkpoint 里不只有权重

一个能“原地续训”的 checkpoint 至少包含:模型参数、优化器状态(例如 Adam 的 m/v 与 fp32 master weights)、学习率调度器、RNG 状态、数据加载器位置、当前 iteration。少任何一项,恢复后都不是同一条训练轨迹。

三个框架的保存入口都体现了这一点。DeepSpeed 的 save_checkpoint 保存 module state、optimizer states、LR scheduler 和 client_state,并要求所有进程一起调用——每个进程要写自己的 master weights 和 optimizer states(deepspeed/runtime/engine.py:4914)。Megatron 的 save_checkpoint 接收 model、optimizer、opt_param_scheduler、dataloader iterator 和 iteration(megatron/training/checkpointing.py:640)。Nanotron 的文档把 checkpoint 拆成 model/、optimizer/、lr_scheduler/、random/ 和一个 checkpoint_metadata.json,后者记录 version、topology 和恢复所需元数据(nanotron docs/docs.md)。

Llama 3 给了一个尺度感:单张 GPU 的模型状态在 1 MB 到 4 GB 之间(§3.3.1)。论文只写 model state,未说明是否含优化器状态 推断;405B 模型的完整 checkpoint 只会更大。

分片:每个 rank 只写自己那一块

PyTorch Distributed Checkpoint(DCP)的 save 明确按 SPMD 工作:每个 rank 只保存本地 shard,不把所有状态 gather 到 rank 0(torch/distributed/checkpoint/state_dict_saver.py:89)。checkpoint 的元数据记录每个 tensor 的 chunk offsets/sizes、planner data 和 storage data(torch/distributed/checkpoint/metadata.py:154;chunk 结构见 metadata.py:24)。加载时按元数据把 shard 映射回当前模型的切分。

Megatron 的 dist_checkpointing 把“sharded state dict”同时当作保存输入和加载指南:load 的文档说,加载需要现有的 sharded state dict 作为 guidance(megatron/core/dist_checkpointing/serialization.py:78);保存和加载分别由 TorchDistSaveShardedStrategy 和 TorchDistLoadShardedStrategy 实现(megatron/core/dist_checkpointing/strategies/torch.py:684、:924)。非 sharded 的公共状态在 current format 里作为 common_state ShardedObject 存进 checkpoint,legacy 才走 common.pt(serialization.py:212)。

DeepSpeed 的 ZeRO checkpoint 按 DP rank 写文件,文件名形如 zero_pp_rank_<dp>_mp_rank_<mp>_optim_states.pt(deepspeed/runtime/engine.py:4358;前缀常量见 deepspeed/checkpoint/constants.py:41)。加载时先读 latest 文件拿到 tag,再按文件加载(deepspeed/runtime/engine.py:4426)。

Nanotron 的文档则直接承认:checkpoint 不是 topology-agnostic,因为 random states 和 sharded tensors 都和切分方式绑定;他们计划用 checkpoint_reshape 解决(nanotron docs/docs.md)。这解释了弹性训练为什么难:换卡数不是“多开几个进程”,而是要把旧切分映射到新切分。

异步:把磁盘写入挪出关键路径

DCP 的 async_save 先做 staging:把 state_dict 复制到 staging storage(默认 CPU 内存),然后在独立线程里调用 save(torch/distributed/checkpoint/state_dict_saver.py:221)。staging 的目的就是制造一个“training-safe”的快照:复制完成后,训练继续更新参数,不会影响正在序列化的那份数据(torch/distributed/checkpoint/staging.py:40)。

Megatron 的 --async-save 走 dist_checkpointing.save(..., async_sharded_save=True, ...)(megatron/training/checkpointing.py:945);如果上一次异步保存还没结束,它会打印警告,建议增大 checkpoint 间隔(checkpointing.py:696)。这说明异步不是无限提速:写入队列会堆积,后台写和下一轮训练会争 CPU 内存、网络和存储带宽。

把三种方案放在一起看:同步 checkpoint 的停顿约等于写完整状态的时间;异步 checkpoint 的停顿约等于 staging 复制的时间,磁盘写入与后续训练 overlap;但异步需要双缓冲/额外 CPU 内存,而且写不完时仍会反压。checkpoint 频率的上限,最终由“写带宽 + 可用内存”决定,而不是由愿望决定。

04

停顿与丢失工作量的账本:间隔越短,写放大越贵

读完这节你应该知道:把 MTBF、checkpoint 间隔、单次停顿和总时长放在一起,怎么算预期故障数、平均丢失工作和有效训练时间。

把故障看成随机到达,并假设 checkpoint 间隔固定为 I 小时、每次 checkpoint 让训练停顿 C 小时、集群平均无故障时间为 MTBF 小时。故障落在两次 checkpoint 之间时,平均要丢掉半个间隔的工作量;checkpoint 本身也要占时间。于是有效训练时间比例可以写成:

Teff = 1 − I / (2 × MTBF) − C / I

第一项是平均丢失工作量,第二项是 checkpoint 停顿;推导假设故障均匀落在间隔内,忽略恢复时间与故障间的相关性。

对 I 求导,最优间隔满足 I* = √(2 × C × MTBF)。这个式子说明:checkpoint 越便宜(C 越小),最优间隔越短;MTBF 越长,间隔也可以放长。但两边都是“按本页模型”的结论,不是论文的实测参数。

下面的计算器把四个输入放在一起。MTBF 和总时长的默认值来自 §3.3.4 的 419 次/54 天折算;checkpoint 间隔和停顿是可调假设,不是论文报告值。

预期故障数
419.42
平均丢失/次
0.50 h
checkpoint 总开销
64.80 h
有效训练时间
78.82%
有效训练 丢失工作量 checkpoint 停顿
本页简化账本 · 非论文实测
总丢失工作量 209.71 h;checkpoint 总开销 64.80 h;有效训练时间 78.82%。按该模型,故障数 = T / MTBF,平均丢失 = I / 2,checkpoint 开销 = (T / I) × C。忽略恢复时间、排队时间和相关性故障。
手算 A:同步 1 h / 3 min MTBF=3.09 h, T=1296 h

预期故障数 = 1296 / 3.09 = 419.4175 → 显示 419.42。

平均丢失/次 = 1.00 / 2 = 0.50 h;总丢失 = 419.4175 × 0.50 = 209.7087 h → 209.71 h。

checkpoint 次数 = 1296 / 1.00 = 1296;总开销 = 1296 × 0.05 = 64.80 h。

有效比例 = (1296 − 209.7087 − 64.80) / 1296 × 100 = 78.8188% → 78.82%。

JS 在相同输入下显示:419.42 / 0.50 h / 64.80 h / 78.82%。

手算 B:异步 0.28 h / 45 s MTBF=3.09 h, T=1296 h

预期故障数 = 1296 / 3.09 = 419.4175 → 419.42。

平均丢失/次 = 0.28 / 2 = 0.14 h;总丢失 = 419.4175 × 0.14 = 58.7184 h → 58.72 h。

checkpoint 次数 = 1296 / 0.28 = 4628.5714(连续近似,实际次数取整);总开销 = 4628.5714 × 0.0125 = 57.8571 h → 57.86 h。

有效比例 = (1296 − 58.7184 − 57.8571) / 1296 × 100 = 91.0050% → 91.00%。

JS 在相同输入下显示:419.42 / 0.14 h / 57.86 h / 91.00%。

注意这不是对 Llama 3 的复算:论文没有报告 I 和 C,也没有把 419 次中断逐次对应到丢失的 step。这个账本只回答“如果 MTBF 是 3.09 小时,间隔和停顿取这些值,损失大概是多少”。

05

恢复流程:检测、重启、加载、回滚,每一步都要能自动完成

读完这节你应该知道:一次故障从被检测到训练恢复,中间经过哪些步骤,每一步依赖什么状态。

训练时间 → C0 C1 C2 C3 C4 故障 丢失工作量 平均 ≈ I / 2 回滚到 C3 checkpoint 间隔 I
checkpoint 每隔 I 小时写一次;故障落在 C3 与 C4 之间,恢复时回滚到 C3,C3 之后、故障之前的 step 需要重跑。示意图不按真实时间比例。

把 Llama 3 的描述和三个框架的接口放在一起,恢复可以拆成六步:

  1. 检测。NCCL watchdog/heartbeat 超时、通信卡死、进程退出。Llama 3 使用 PyTorch 内置的 NCCL flight recorder,把 collective metadata 和 stack trace 写进 ring buffer,并在 watchdog/heartbeat 超时时自动 dump(§3.3.4)。
  2. 定位与隔离。判断是单卡、节点、网络还是软件;把坏节点从调度池里摘掉,避免重启后再次撞上。Llama 3 还提到 straggler 排查:单张慢卡可能表现为“通信正常但很慢”(§3.3.4)。
  3. 重启作业。同步训练的恢复粒度是整个 job;调度器重新分配节点,所有 rank 重新初始化进程组和并行组(§3.3.4)。
  4. 加载 checkpoint。DeepSpeed 先读 latest 拿 tag(deepspeed/runtime/engine.py:4426);Megatron 用当前模型的 sharded state dict 作为加载指南(megatron/core/dist_checkpointing/serialization.py:78);DCP 用元数据里的 chunk offsets/sizes 把保存时的 shard 映射到当前切分(torch/distributed/checkpoint/metadata.py:24)。
  5. 回滚到最近完整 checkpoint。恢复 iteration、优化器状态、LR scheduler、RNG 和数据读取位置;没有 checkpoint 的 step 重跑。丢失的工作量就是“故障时刻 − 最近 checkpoint 时刻”。
  6. 健康检查后再放回集群。确认 checkpoint 完整、节点通过诊断、通信库没有残留错误;否则下一次故障可能更早到来。

这条链路里,最容易被低估的是第 4 步和第 6 步:加载不是“读一个文件”,而是按当前拓扑重组 shard;健康检查不是“能启动就行”,而是要把静默错误和慢节点挡在训练之外。

06

Llama 3 的 54 天:419 次意外中断下仍高于 90% 有效训练

读完这节你应该知道:论文在什么规模、什么时间窗口、用什么口径报告了容错效果,以及哪些参数它没有报告。

规模与配置:Llama 3 405B,最多 16,384 张 H100(Table 4)。Table 4 同时给出 MFU:8,192 卡、DP=64 时 43%;16,384 卡、DP=128 时 41%;16,384 卡、CP=16、序列 131,072 时 38%。卡数翻倍后 MFU 从 43% 到 41%,差 2 个百分点(43 − 41);16,384 卡的长序列配置比 8,192 卡的 43% 低 5 个百分点(43 − 38)。论文把 16K 卡上 MFU 的小幅下降归因于保持全局 tokens/batch 不变时每 DP 组的 batch 变小(§3.3.2)。

Table 4 · Llama 3 405B 预训练的并行配置与 BF16 MFU(照录原表)

GPUsTPCPPPDPSeq. Len.Batch/DPTokens/BatchTFLOPs/GPUBF16 MFU
8,1928116648,1923216M43043%
16,38481161288,1921616M40041%
16,384816168131,0721616M38038%

容错结果:在 54 天的预训练快照里,466 次作业中断,47 次计划中断,419 次意外中断;有效训练时间高于 90%;约 78% 的意外中断归因于已确认或疑似的硬件问题;GPU 问题占 58.7%;只有 3 次需要重大人工干预(§3.3.4)。论文把有效训练时间定义为“有用训练时间 / 总耗时”,并说明自动化维护(固件、Linux 内核升级)至少每天造成一次中断(§3.3.4)。

Table 5 · 54 天内意外中断的根因分类(照录原表;Faulty GPU 行的 148 与 30.1% 存在算术对账问题,见下方折叠块)

ComponentCategoryInterruption Count% of Interruptions
Faulty GPUGPU14830.1%
GPU HBM3 MemoryGPU7217.2%
Software BugDependency5412.9%
Network Switch/CableNetwork358.4%
Host MaintenanceUnplanned Maintenance327.6%
GPU SRAM MemoryGPU194.5%
GPU System ProcessorGPU174.1%
NICHost71.7%
NCCL Watchdog TimeoutsUnknown71.7%
Silent Data CorruptionGPU61.4%
GPU Thermal Interface + SensorGPU61.4%
SSDHost30.7%
Power SupplyHost30.7%
Server ChassisHost20.5%
IO Expansion BoardHost20.5%
DependencyDependency20.5%
CPUHost20.5%
System MemoryHost20.5%

Table 5 的 18 行按原文转录;原表中 Faulty GPU 的计数与百分比不能同时成立,下方折叠块给出算术对账。本页不替原文改数。

Table 5 对账:148 与 30.1% 不能同时成立 按 419 次意外中断

原表 Faulty GPU 行:148 次,30.1%。按 419 次意外中断计算,30.1% × 419 ≈ 126 次;148 / 419 = 35.3%。两个数不能同时成立。

论文正文写 GPU 问题占 58.7%。把 Table 5 的 GPU 类别相加:

  • 用 126 计:126 + 72 + 19 + 17 + 6 + 6 = 246;246 / 419 = 58.71%,与 58.7% 对齐。
  • 用 148 计:148 + 72 + 19 + 17 + 6 + 6 = 268;268 / 419 = 63.96%,与 58.7% 对不上。

本页照录原表的 148 和 30.1%,不替原文改数;上面两组数只是把原表内部的算术关系摆出来。

三个反直觉点:

论文正文与 Table 5 未报告 checkpoint 间隔、单次 checkpoint 停顿和 checkpoint 总写入量;这些是本页账本的输入假设,不是论文实测值。

07

边界:写带宽、存储、一致性、弹性,一个都不能省

读完这节你应该知道:checkpoint 能解决什么、不能解决什么,以及在哪些条件下账本会失效。

写带宽与存储

Llama 3 的存储 fabric 是 240 PB、持续 2 TB/s、峰值 7 TB/s;checkpoint 写入高度突发,会在短时间打满 fabric(§3.3.1)。这意味着 checkpoint 频率不能只看“我想丢多少工作”,还要看“存储能不能在窗口内写完”。模型越大、优化器状态越多,单次 checkpoint 越大;保留多个历史 checkpoint 还会成倍占用容量。

写放大是结构性的:每次 checkpoint 都重写完整状态,即使大部分参数在两次 checkpoint 之间只变了一点点。分片并行写把带宽摊到多个 rank,但不减少总字节数;异步写把写入移出关键路径,也不减少总字节数,只是把压力挪到后台。

一致性

异步 checkpoint 的核心难点是“训练继续跑,但写出的必须是同一时刻的快照”。DCP 的做法是先 staging 到 CPU 内存,形成一个 training-safe 的副本,再在后台序列化(torch/distributed/checkpoint/staging.py:40)。如果直接在训练更新中的 tensor 上写,checkpoint 可能包含不同 step 的混合状态。写 checkpoint 期间发生故障,还可能留下不完整的目录;DeepSpeed 用 latest 文件指向完整 tag(deepspeed/runtime/engine.py:4426),Megatron/DCP 也各自有元数据和完整性校验。

弹性与拓扑变化

弹性训练允许作业在节点故障后缩容/扩容,但 checkpoint 的切分方式会变。Nanotron 文档明确说 checkpoint 不是 topology-agnostic,因为 random states 和 sharded tensors 都与 TP/DP/PP 切分绑定(nanotron docs/docs.md);换卡数需要 reshard 或 checkpoint_reshape。DCP 的元数据(chunk offsets/sizes)和 resharding 模块为换切分提供了基础(torch/distributed/checkpoint/resharding.py:1),Megatron 的 FullyParallelLoadStrategyWrapper 也支持按新切分加载(megatron/core/dist_checkpointing/strategies/fully_parallel.py:163);但“换拓扑”不是免费的,恢复时间会变长。

静默错误与慢节点

不是所有故障都会让作业崩掉。Llama 3 的 Table 5 里 Silent Data Corruption 6 次(1.4%);论文还提到“仍然工作但变慢”的 straggler,单张慢卡可以拖慢数千张卡(§3.3.4)。这类问题不会触发重启,却会悄悄拉低有效训练时间,甚至把错误写进 checkpoint。节点健康检查、NCCL flight recorder、通信超时和 straggler 排查是 checkpoint 之外的补丁。

最后,checkpoint 不能解决模型发散、数据管道 bug、相关性故障(例如网络设备同时影响多个 rank)和 checkpoint 文件本身的损坏。它把“故障后能否继续”变成工程问题,不把“故障不会发生”变成事实。