
你或许觉得,训练大模型就像在家煮粥——把数据和代码丢进锅里,等几天就出锅了。直到你在真实集群上跑过一次,才发现这更像是高空走钢丝:几百张 GPU 同时工作,任何一张卡因为过热、驱动崩溃、网络闪断而掉线,整个训练进程就可能戛然而止。而且这种故障不是小概率事件——卡越多,训练时间越长,概率就越接近 1。
我最早接触分布式训练时天真地以为,训练框架应该有某种魔法,能让训练在故障后自动接着跑。结果第一次遇到卡挂,日志直接报错退出,我一脸懵:不是说好的容错吗?后来才明白,分布式训练的容错不是开箱即用的魔法,而是一套你必须理解并主动配置的机制。
为什么挂卡是必然事件,而不是偶然事件
先算一笔账。假设一张 GPU 卡在 30 天内故障的概率是 1%(这已经是相当乐观的假设,实际数据中心里 GPU 故障率通常更高),那么 256 张卡跑 7 天,至少有一张卡出问题的概率是:
1 - (1 - 0.01)^(256 × 7/30) ≈ 1 - 0.9999^59 ≈ 44%
等等,这个数字还不够夸张。再把故障率调高到 2%,训练时间拉到 14 天,256 张卡的概率就变成:
1 - (1 - 0.02)^(256 × 14/30) ≈ 1 - 0.98^119 ≈ 91%
商用数据中心里,GPU 的年故障率(AFR)通常在 1% 到 5% 之间,而且故障并不是均匀分布的——一批次有问题的卡可能集中爆发。所以结论很直白:规模越大、训练越长,故障就不是 if,而是 when。这也是为什么所有大模型训练框架都把故障容错当成一等公民,而不是事后补救。
检查点:定时存档,但这远没有你想的那么简单
容错的第一层是检查点(checkpoint),就是把训练过程中的模型参数、优化器状态、当前步数保存到持久化存储。训练挂了之后,从最近的检查点重新加载,而不是从零开始。
听起来很简单?但这里有几个你一定会踩的坑。
- 检查点不等于只存模型权重。如果你用 Adam 这类带动量项的优化器,还需要保存优化器状态(momentum 和 variance)。只存权重的话,恢复之后相当于换了一个优化器状态重新开始,训练效果会莫名其妙变差。
- 同步 AllReduce 框架下,保存所有卡的数据是错的。传统做法是每张卡都保存自己的分片,但这样要保存的参数量是模型参数量的 N 倍。DeepSpeed 和 Megatron-LM 的做法是让每张卡只保存自己分到的分片,配合分布式优化器状态,把检查点体积从数百 GB 压到跟模型大小相当。
- 写到共享存储的带宽是瓶颈。几百张卡同时写检查点,如果共享文件系统带宽不够,会造成所谓 "检查点风暴"——每 10 分钟卡顿一次,训练效率大打折扣。所以实际系统里要错峰写入,或者用异步检查点:先拷到本地内存或 NVMe,再后台慢慢刷到持久化存储。
你可以把检查点想象成游戏存档。问题是,这个存档并不是每一秒都在存——存得太频繁,训练被拖慢;存得太稀疏,挂了之后回退太多步数。现在的实践通常是:每小时存一次,异步写入,不阻塞训练。但即便这样,你还是面临另一个问题:谁来发现卡挂了?
心跳与超时:让系统自己发现异常
检查点只是 "攒血量",真正让系统具备恢复能力的是故障检测。分布式训练框架里,每个进程都会定期向协调者(或者通过分布式 KV store 如 etcd)发送心跳。如果某个 worker 超过预定时间没上报,协调者就判定它已死亡,触发恢复流程。
这个机制看起来直白,但有一个微妙的权衡:超时时间设多长?
设短了,一次网络抖动(比如交换机短暂拥塞导致的 RPC 延迟)就会误杀一个健康的 worker,造成不必要的重启。设长了,真正的故障要很久才能被发现,浪费时间。而且注意,在同步训练中,只要有一张卡挂了,其他所有卡都会因为等待它的梯度而卡住——整个训练已经停了,只是没人通知你。
这里有个很多初学者理解错的地方:分布式训练框架的 "容错" 并不是检测到故障后让训练无缝继续,而是检测到故障后,先把整个训练停下来,再用健康的节点重新拉起一个训练进程,从最近的检查点恢复。也就是说,容错的粒度是 "分钟级中断 + 自动重启",而不是 "零中断热迁移"。明白了这一点,很多设计就好理解了。
弹性训练:把容错从运维脚本变成框架内置能力
早期人们在 PyTorch 上做容错,方式是写一堆外部脚本:监听进程退出码、手动重启、加载 checkpoint。这套方案能跑,但非常脆弱——启动命令不一致、环境变量丢失、rank 混乱、不同 worker 加载的 checkpoint 步数不一致……全是坑。
新一代方案是把弹性(elasticity)内建到训练框架里。PyTorch 2.0 之后官方推荐的 torch.distributed.elastic(简称 torchelastic)就是干这个的。它的核心抽象叫 rendezvous:一组 worker 通过它互相发现、同步、达成一致。
# 用 torchelastic 启动训练
python -m torch.distributed.run --nproc-per-node=8 --rdzv-backend=etcd --rdzv-endpoint=etcd:2379 train.py
这个命令跟普通的 torch.distributed.launch 长得很像,但区别在于:torchelastic 的 agent 会监控 worker 进程,发现死亡后自动重启,并且通过 rendezvous 让新加入的 worker 跟存量 worker 对齐世界大小(world size),重新分配 rank。训练代码里需要做的,就是每个 rank 在启动时通过 load_checkpoint 恢复到同一状态。
值得注意的一个细节:弹性训练的 world size 是可变的吗?理论上 rendezvous 允许节点数变化(这就是 "弹性" 的含义),但当前大多数实际训练脚本里,数据并行(DDP)的 world size 是在代码里写死的,变了就会出问题。所以今天的 torchelastic 主要价值是 固定 world size 下的自动故障恢复,而不是真正意义上的动态伸缩。Hugging Face 的 accelerate 库也封装了这套机制,你在配置里指定 --rdzv-backend,就能获得自动重启能力。
DataLoader 重放:一个隐藏的恢复陷阱
很多人把 checkpoint 保存好就以为万事大吉,但你可能会在恢复后遇到一个诡异的问题:训练 loss 看起来正常,但收敛速度明显变慢,或者效果变差。
问题出在数据加载器上。分布式训练中,每个 rank 用自己的 DataLoader 按顺序读取数据分片。如果只恢复模型参数和优化器状态,没有恢复 DataLoader 的状态(比如当前 epoch 已经读到了哪个文件、哪个 offset),那么恢复后的训练就会从该 epoch 的开头重新读数据。
这意味着什么?一部分数据被重复学习了,而另一部分可能永远没有被学到。在样本量小的情况下,重复数据会放大某些样本的影响,让模型学歪。
解决办法有几种:一是把 DataLoader 的迭代位置和随机数种子也存进 checkpoint;二是使用基于数据索引的重放机制,让模型按全局样本索引读取,恢复时直接从断点索引继续。这些细节在 PyTorch 官方文档里没有集中说明,但实践中你一定会遇到。
那部署层面呢?Kubernetes 的介入
如果你用的是 Kubernetes 集群,那么容错又多了一个层次。Kubernetes 本身就有 Pod 重启策略和 liveness probe,可以自动重启崩溃的容器。但这里有个重要区别:Kubernetes 重启的是容器,而分布式训练需要的是 "训练作业级" 的协调。
如果只是简单地把每个训练 worker 作为独立 Pod 运行,Kubernetes 发现某个 Pod 挂掉后重启它,但这个新 Pod 的 rank 和网络地址都变了,其他 worker 并不知道——训练照样失败。所以真正的实践是:要么用 Kubeflow 的 Training Operator,它会管理整个 job 的生命周期,并在 worker 故障时重新调度整个 job、从 checkpoint 恢复;要么在 Pod 之上再跑 torchelastic,让框架自己处理 worker 重启。
顺带一提,NVIDIA 的 NeMo 框架和 Hugging Face 的 transformers Trainer 都内置了 "重启即恢复" 的能力:你只需要把训练命令封装成可重复执行的脚本,框架检测到异常退出后自动重新拉起,并在启动时加载最新的 checkpoint。这个模式被称为 resilient training,理念是:不要试图阻止故障,而是让启动变成一件廉价且幂等的事。
一张表看清:各方案能容什么错
| 方案 | 能恢复什么 | 恢复粒度 | 代价 |
|---|---|---|---|
| 检查点 + 手动重启 | 任何进程崩溃 | 分钟级 | 需要人工介入,易出错 |
| torchelastic 自动重启 | 进程崩溃、节点掉线 | 分钟级 | 需配置 rendezvous,DataLoader 可能重复读 |
| Kubernetes 故障自愈 | 容器崩溃、节点宕机 | 分钟级 | 需要 job 级协调,否则 rank 错乱 |
| Megatron/DeepSpeed 异步 checkpoint | 训练中断后快速恢复 | 秒级加载 | 需要共享存储带宽,代码侵入 |
我的最后一课:故障恢复是训练策略的一部分,不是事后补救
我想强调的是,别把容错当作一个 "万无一失的保障",它更像是训练策略的一部分,需要你提前设计。每次故障恢复后,模型从哪一步继续?学习率调度器(scheduler)的状态要不要恢复?数据顺序会不会被打乱?这些都会影响最终结果。
我现在每次开启大规模训练前,都会先做一次 故障演练:故意 kill 掉一个 worker,看系统能不能在规定时间内自动恢复,恢复后的 loss 跟故障前是否衔接。这比什么监控都管用。
回到开头的问题:几百张 GPU 跑一周必然有卡挂,怎么自动恢复?答案是:把故障当成常态,把恢复当成流程。检查点存档、心跳检测、弹性重启、DataLoader 重放,每一层都有自己的职责,缺一层你的训练就可能在某个深夜静悄悄地失败。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/1470.html