ZeRO-3 开启 CPU offload 后训练吞吐骤降,通常不是某一个配置项写错了,而是 offload 本身把 CPU 放进了训练的关键路径。DeepSpeed 的参数、梯度和优化器状态可以分开 offload,很多人打开的是全量 CPU offload,于是每个训练步骤都要在 CPU 与 GPU 之间搬运接近模型规模的数据。先确认实际加载的配置里到底是哪些 offload 生效,再按“先关 optimizer offload、再增加有效 batch、再调缓存参数”的顺序处理。
ZeRO-3 CPU offload 导致吞吐骤降时,先检查是否同时开启了 offload_optimizer 和 offload_param。通常建议先关闭 optimizer offload,保留参数 offload,配合 pin_memory 和更大的有效 batch。若显存不允许关闭,则不要追求吞吐恢复,改为减少 dataloader 争抢并调大 stage3_max_live_parameters。每改一项,小步验证 loss 与 step 时间。
先确认实际生效的 offload 对象
训练脚本可能同时读 JSON、命令行参数或在 Python 里构造配置;config 文件里的内容不一定是最终效果。启动训练前,先单独打印一次最终配置的 zero_optimization 子项:
python -c "import json; print(json.load(open('ds_config.json'))['zero_optimization'])"这一步要看三个字段:offload_optimizer.device 是否等于 cpu、offload_param.device 是否等于 cpu、pin_memory 是否为 true。如果打印结果里没有 offload 字段,说明当前训练没有启用 CPU offload,吞吐下降的原因要到数据加载或分布式通信里去找,而不是继续调 offload。
optimizer offload 和 param offload 的开销形态不同。param offload 会在每一层前向和反向时把参数从 CPU 取回;micro-batch 数量越多,这种重复取参的固定开销越高。optimizer offload 主要在优化器更新阶段搬运优化器状态,频率通常是每个 optimizer step 一次。两者混在一起时,先关掉优化器 offload 更容易看出 param offload 的剩余影响。
按顺序只改一个变量
如果显存还有余量,第一步先删除 offload_optimizer,或把 device 改成 none,仅保留参数 offload。DeepSpeed 配置看起来是这样:
{ "zero_optimization": { "stage": 3, "offload_param": { "device": "cpu", "pin_memory": true }, "overlap_comm": true, "contiguous_gradients": true, "reduce_bucket_size": 500000000 } }关闭 optimizer offload 后,Adam 的一阶和二阶动量会按 ZeRO-3 切分存放在 GPU 上,显存占用会明显增加。如果这一改直接 OOM,需要退回原配置,改用下一段里的方式在保留 optimizer offload 的情况下优化。
如果必须保留 optimizer offload,先把 pin_memory 设为 true,让 CPU 内存页固定下来,拷贝过程不容易被换页打断。然后调大 micro-batch,显存不够就先开启 gradient checkpointing,再调大 micro-batch。梯度累积能降低 optimizer 更新频率,但减不掉每层参数 allgather 的开销,所以不能只靠梯度累积解决 param offload 的重复取参。
如果显存有余量,还可以小幅调大 stage3_max_live_parameters 和 stage3_max_reuse_distance,让刚用过的参数在 GPU 上多留一会儿,减少下次取出。这个值超过显存容量会直接 OOM,每次只小幅调整,并观察 step 时间。
把 dataloader 的内存争抢降下来
CPU offload 让训练进程持续占用内存带宽,而 dataloader 的 num_workers 和 prefetch_factor 开得过高时,多个 worker 会同时做张量拷贝,和 offload 抢内存带宽。可以先从 num_workers=4、prefetch_factor=2 开始试,之前越高,改善空间越明显。如果数据读取本身不是瓶颈,这个改动不会带来可感知变化,需要换回原值。
验证和回滚
每次只改一个配置项,记录 baseline 的 step 时间和 loss 曲线。建议用 50 到 200 步作为对比窗口,太短看不到稳定差异,太长会浪费算力。训练时用 nvidia-smi 看显存占用和 GPU 利用率,用 free -g 看 CPU 内存余量;如果空闲 CPU 内存持续降到接近物理容量,说明进程开始使用 swap,此时 offload 的吞吐下降会更明显,需要先降低内存占用而不是继续调 offload。
优化是否真的可用,最终要看两条:loss 曲线没有异常,step 时间比改前明显下降。关闭 optimizer offload 会在更新阶段增加 GPU 显存占用,只能作为速度优先的选择;如果模型规模很大,显存不允许,就只能保留 CPU offload,并把目标定为“吞吐降得不那么严重”,而不是“恢复到未 offload 的水平”。