3、分布式Checkpoint框架:基于PyTorch Distributed的State Dict保存、Megatron-LM与DeepSpeed的Checkpoint机制
分布式训练里,Checkpoint 从来不是「把模型参数存个盘」那么简单。
你想想看,当你的模型分布在几十甚至几百张 GPU 上,每张卡只持有模型的一部分——要么是数据并行的一份副本,要么是模型并行中的一个切片,要么是流水线并行中的一个 stage。这时候,怎么保证存下来的东西能完整恢复?
我最早踩这个坑是在一次 64 卡的多机训练里。训练跑了三天,突然节点掉线,我心想没事,有 Checkpoint。结果恢复的时候发现,因为数据并行的 rank 映射没对齐,加载的参数全乱了。嗯,从那以后,我对分布式 Checkpoint 的敬畏心就上来了。
3.1 PyTorch Distributed 的 State Dict 保存:最基础的方案
PyTorch 原生的 torch.save(model.state_dict(), path) 在单卡场景下没问题。但到了分布式环境,你得自己处理 rank 的协调。
最朴素的做法是这样:
import torch
import torch.distributed as dist
def save_checkpoint(model, optimizer, epoch, path):
if dist.get_rank() == 0:
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
}, path)
dist.barrier()
这段代码的问题很明显——它假设所有 rank 的 state dict 是一样的。这在纯 DDP(Distributed Data Parallel)下成立,因为 DDP 会同步梯度,模型参数在各 rank 间保持一致。
但一旦你用了模型并行或流水线并行,每个 rank 的 state dict 就不同了。这时候,你需要每个 rank 各自保存自己的那份:
def save_distributed_checkpoint(model, optimizer, epoch, path_prefix):
rank = dist.get_rank()
local_path = f"{path_prefix}_rank_{rank}.pt"
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'rank': rank,
}, local_path)
dist.barrier()
恢复的逻辑也简单:
def load_distributed_checkpoint(model, optimizer, path_prefix):
rank = dist.get_rank()
local_path = f"{path_prefix}_rank_{rank}.pt"
checkpoint = torch.load(local_path, map_location=f'cuda:{rank}')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
return checkpoint['epoch']
这个方案的好处是简单直接,坏处是文件数量随 GPU 数量线性增长。你训练 1024 张卡,就得管理 1024 个文件。说实话,这在实际运维中挺头疼的。
3.2 Megatron-LM 的 Checkpoint 机制:模型并行的救星
Megatron-LM 是我个人非常推崇的一个框架。它在 Checkpoint 设计上做了很多工程化的考量。
Megatron-LM 的核心思路是:每个模型并行组内的 rank 共享一个 Checkpoint 文件。它通过 mp_rank_XX 的命名规则来区分不同模型并行分片的文件。
举个例子,假设你有 4 张卡,模型并行度(tensor parallelism, TP)为 2,数据并行度(DP)为 2。那么:
- TP group 0(rank 0, 1)保存为
mp_rank_00.pt - TP group 1(rank 2, 3)保存为
mp_rank_01.pt
每个文件里存的是该 TP group 内所有 rank 的 state dict。恢复时,每个 TP group 加载自己的文件,然后按 rank 切分。
Megatron-LM 的 Checkpoint 代码里有一个关键函数 save_checkpoint,它的流程大致是:
- 只在每个 TP group 的 master rank(通常是 rank 0)上执行保存操作。
- 收集该 group 内所有 rank 的 state dict。
- 打包成一个字典,写入文件。
- 其他 rank 等待 barrier。
关键设计: Megatron-LM 把 optimizer state 也按模型并行分片存储。这意味着恢复时 optimizer 的 momentum、variance 等状态也能精确还原。这一点对长训练任务至关重要。
我自己的经验是,Megatron-LM 的 Checkpoint 在千卡规模下表现稳定。但有一个坑:如果你改了模型并行度(比如从 TP=2 改成 TP=4),旧的 Checkpoint 就没法直接用了。你得写一个转换脚本,把旧的分片重新切分。这个转换过程我做过一次,说实话,挺折腾的。
3.3 DeepSpeed 的 Checkpoint 机制:ZeRO 优化的最佳拍档
DeepSpeed 的 Checkpoint 设计跟它的 ZeRO 优化器深度绑定。ZeRO 把 optimizer state、gradient 和 parameter 分散到各个 rank 上,所以 Checkpoint 也必须感知这种分布。
DeepSpeed 提供了两个层次的 Checkpoint:
- Engine Checkpoint:保存整个训练引擎的状态,包括模型、优化器、学习率调度器等。
- Client Checkpoint:只保存用户自定义的状态,比如数据加载器的进度。
Engine Checkpoint 的保存接口很简单:
model_engine.save_checkpoint(save_dir, tag="latest")
但背后的逻辑相当复杂。DeepSpeed 会根据 ZeRO 的 stage 来决定如何保存:
| ZeRO Stage | 保存策略 | 文件数量 |
|---|---|---|
| Stage 1 | optimizer state 分片,每 rank 保存自己的部分 | 等于 rank 数 |
| Stage 2 | optimizer state + gradient 分片 | 等于 rank 数 |
| Stage 3 | optimizer state + gradient + parameter 分片 | 等于 rank 数 |
恢复时,DeepSpeed 会自动从所有 rank 的文件中重建完整的模型状态。这个过程对用户是透明的。
个人建议: 使用 DeepSpeed 时,一定要设置 save_latest 参数为 True。这样每次保存都会更新一个符号链接指向最新的 Checkpoint。训练意外中断后,你可以直接用 load_checkpoint 恢复,不用手动指定 tag。这个小习惯帮我省了不少时间。
DeepSpeed 还有一个很实用的功能:异步保存。它会把 Checkpoint 的写入操作放到后台线程执行,不阻塞训练。这在 IO 成为瓶颈的大规模训练中特别有用。
不过要注意,异步保存时如果训练进程崩溃,正在写的 Checkpoint 可能会损坏。我一般会在关键节点(比如每个 epoch 结束)用同步保存,中间步骤用异步保存。
3.4 三种方案的对比与选型
说了这么多,我们来个直观的对比:
| 特性 | PyTorch 原生 | Megatron-LM | DeepSpeed |
|---|---|---|---|
| 适用并行策略 | 纯 DDP | TP + PP + DP | ZeRO + DP |
| 文件数量 | 等于 rank 数 | 等于 TP group 数 | 等于 rank 数 |
| 恢复速度 | 快(每卡独立加载) | 中等(需 group 内聚合) | 慢(需全量重建) |
| 存储开销 | 高(每卡存完整副本) | 中(分片存储) | 低(ZeRO 分片) |
| 易用性 | 简单 | 中等 | 简单(接口封装好) |
怎么选?我的经验是:
- 如果你只是做数据并行,用 PyTorch 原生就够了,别引入额外依赖。
- 如果你用 Megatron-LM 做模型并行,就用它自带的 Checkpoint 机制,别自己造轮子。
- 如果你用 DeepSpeed 的 ZeRO,那必须用 DeepSpeed 的 Checkpoint,因为只有它知道怎么正确保存和恢复分片状态。
3.5 一张图看懂分布式 Checkpoint 框架
下面这张图展示了三种框架在分布式 Checkpoint 中的核心差异:
3.6 实战中的几个坑
最后,分享几个我实际踩过的坑:
- 文件系统压力:千卡规模下,所有 rank 同时写文件,文件系统可能扛不住。我建议用
dist.barrier()错峰写入,或者用 DeepSpeed 的异步保存。 - Checkpoint 校验:保存后一定要做一次加载验证。我有一次因为磁盘空间不足,文件写了一半,训练中断后恢复才发现 Checkpoint 损坏。后来我加了一个校验步骤:保存后立即加载,对比 loss 是否一致。
- 版本兼容:PyTorch、Megatron-LM、DeepSpeed 的版本升级可能导致 Checkpoint 不兼容。我习惯在 Checkpoint 文件名里带上框架版本号,比如
checkpoint_v1.2.3.pt。 - 多节点训练:节点间网络抖动可能导致 Checkpoint 保存超时。我一般会设置
torch.distributed.distributed_c10d::_DEFAULT_TIMEOUT为 30 分钟,给足缓冲时间。
分布式 Checkpoint 没有银弹。你得根据你的并行策略、硬件环境和恢复需求来选。但不管选哪个,一定要在训练开始前就把 Checkpoint 的保存和恢复流程跑通。别等到训练跑了一周才发现恢复不了——那种感觉,我不想再体验第二次了。