性能调优:bucket size / overlap / fused optimizer
11.1 调优前先量化"瓶颈在哪"
打开 ds_config.json:
{
"wall_clock_breakdown": true,
"steps_per_print": 50,
"flops_profiler": {
"enabled": true,
"profile_step": 5,
"module_depth": -1,
"top_modules": 3,
"detailed": false,
"output_file": null
}
}
跑 100 步后能看到一份 breakdown:
step= 50 fwd= 240ms bwd= 380ms step= 95ms
allgather= 60ms (24% of fwd)
reduce_scatter= 110ms (28% of bwd)
optim_step= 95ms
诊断思路:
| 慢在哪 | 优先看哪些字段 |
|---|---|
| fwd 占比大 | attention / FlashAttention / TP=? / activation checkpointing |
| bwd_inner(实际反向) | 同上 |
| bwd_allreduce / reduce_scatter | bucket size、overlap_comm、网络带宽 |
| optim_step 大 | fused optimizer、cpu offload? |
| allgather 大(stage 3) | prefetch_bucket_size、stage3_max_live_parameters |
11.2 通信 bucket:reduce / allgather 的"批"
DS 通信不是一个张量一个张量地发,而是凑齐一个 bucket(≈ 几百 MB)后一次发出。bucket 太小 → call 多,启动开销大;bucket 太大 → 显存被占;最佳值通常 $2 \times 10^8 \sim 5 \times 10^8$。
| 字段 | 含义 | 建议 |
|---|---|---|
reduce_bucket_size | 反向 reduce-scatter 桶 | 5e8 |
allgather_bucket_size | stage 1/2 all-gather 桶 | 5e8 |
stage3_prefetch_bucket_size | stage 3 prefetch 桶 | "auto" |
stage3_max_live_parameters | 同时驻留显存的参数 bytes | 1e9 |
stage3_max_reuse_distance | 多大距离内的参数复用 cache | 1e9 |
11.3 overlap_comm:必须开
不开 overlap,整个 step 串行执行:
$$T_\text{step} = T_\text{compute} + T_\text{comm}$$
开 overlap:
$$T_\text{step} \approx \max(T_\text{compute}, T_\text{comm}) + \text{少量开销}$$
差距通常 30%+。任何场景都建议开:
"zero_optimization": {
"stage": 3,
"overlap_comm": true,
"contiguous_gradients": true
}
11.4 fused optimizer
DS 内置的 fused 实现比 PyTorch 自带快 1.5–3 倍:
| 原 PyTorch | DS fused 对应 |
|---|---|
torch.optim.Adam | deepspeed.ops.adam.FusedAdam |
torch.optim.AdamW | 同上(自动加 weight decay) |
| CPU Adam(offload) | deepspeed.ops.adam.DeepSpeedCPUAdam |
torch.optim.Lamb | deepspeed.ops.lamb.FusedLamb |
用法:直接在 ds_config 写 "optimizer": {"type": "AdamW"},DS 自动选 FusedAdam(如果 op 已编);offload 时自动选 CPUAdam。不需要你手写 import。
ds_report 显示 FusedAdam: [NO],说明 op 没编。第 2 章 2.3 节里 DS_BUILD_FUSED_ADAM=1 重装一次。
11.5 activation checkpointing
大模型激活内存约:
$$M_\text{act} \approx B \cdot L \cdot H \cdot (\text{constant})$$
$B$ batch、$L$ sequence length、$H$ hidden size。开 checkpointing 后激活内存降到 $\approx \sqrt{L}$ 倍数级。代价:反向时要重做一次 forward,训练时间 +25%–35%。
三种打开方式:
| 地方 | 怎么写 |
|---|---|
| HF Trainer | TrainingArguments(gradient_checkpointing=True) |
| 原生 DS | ds_config 加 activation_checkpointing 段 |
| 模型内部 | model.gradient_checkpointing_enable()(HF 模型) |
同时开多个不会冲突,DS 内部去重。
11.6 torch.compile 加持
PyTorch 2.x 的 compile() 能给训练带 10–30% 提速。DS 0.14+ 支持:
{
"torch_compile": {
"enabled": true,
"backend": "inductor"
}
}
注意:
- 第一次 step 会卡几分钟编译;
- 跟 ZeRO-3 + activation checkpoint 同开偶有 bug,多看 issue;
- 动态 shape(变长 sequence)会触发重编译,建议配合 packing 把长度齐整。
11.7 autotuning:让 DS 帮你调
DS 自带 autotuner:
{
"autotuning": {
"enabled": true,
"fast": true,
"results_dir": "autotune_results"
}
}
启动命令加 --autotuning run:
deepspeed --autotuning run --num_gpus 8 train.py --deepspeed --deepspeed_config ds_config.json
它会扫一组合理的 micro batch / accum / zero stage 组合,跑短训练测吞吐,给一份最佳推荐。第一次接触新硬件时极有用。
11.8 一份"性能体检"清单
| 序 | 检查 | 怎么修 |
|---|---|---|
| 1 | ds_report 里 FusedAdam / CPUAdam 都 [OKAY]? | 否则重装编译 op |
| 2 | 开了 overlap_comm? | 必开 |
| 3 | 开了 contiguous_gradients? | 必开 |
| 4 | bucket size 在 5e8 量级? | 过小会拖;过大占显存 |
| 5 | 开了 activation_checkpointing? | 大模型必开,HF Trainer 用 gradient_checkpointing |
| 6 | 用 bf16 而非 fp16? | A100/H100 用 bf16 |
| 7 | 没有 offload 也能装下? | 能不 offload 就不 offload |
| 8 | num_micro_batches ≥ 4 × PP? | 降低 pipeline bubble |
| 9 | flash_attn 装好? | Attention 大头 |
| 10 | 样本长度齐整 / 开 packing? | 否则显存波动大 |
| 11 | NCCL_DEBUG=INFO 查 P2P / NVLink 是否启用? | 跨机 InfiniBand 必查 |
| 12 | autotuner 跑过一次? | 大改架构 / 换卡时建议跑 |
11.9 实战吞吐参考(A100 80G)
| 模型 / 配置 | tokens / sec / GPU |
|---|---|
| 7B + bf16 + ZeRO-2 | ~4500 |
| 7B + bf16 + ZeRO-2 + flash_attn | ~5500 |
| 13B + bf16 + ZeRO-2 | ~2800 |
| 13B + bf16 + ZeRO-3 | ~2200 |
| 13B + bf16 + ZeRO-3 + offload optim | ~1600 |
| 13B + bf16 + ZeRO-3 + offload optim + ZenFlow | ~2000(blog 数据,未本机复测) |
| 13B + bf16 + ZeRO-3 + DeepCompile | ~2600(blog 数据,未本机复测) |
| 70B + bf16 + ZeRO-3 + offload optim+param | ~250 |
| 70B + bf16 + ZeRO-3 + offload + SuperOffload | ~380(blog 数据,未本机复测) |
具体数字会因模型架构、batch size、context length 浮动 ±30%;用作"我的训练是不是远低于预期"的参考标尺。
11.10 Coordinator 八个 timer 的实战用法
ch05.11.3 列了 Stage 3 PartitionedParameterCoordinator 的八个 timer。wall_clock_breakdown: true 时它们会逐步打印——下表给出"这个 timer 大代表哪类问题":
| timer | 大时意味着 | 第一反应调什么 |
|---|---|---|
FORWARD_FETCH_SUBMIT | 每次 forward 进入 module 都要新提一次 all-gather(没命中 prefetch) | 查 trace 是否 INVALID(ch05.11.1);模型里去掉 rank 分支 |
FORWARD_FETCH_WAIT | 提交了但还在等带宽 | 升网络(IB / NVLink);减小 reduce_bucket_size 提早释放 |
FORWARD_PREFETCH_SUBMIT | 预取量大(不一定坏,但占带宽) | 调小 stage3_prefetch_bucket_size |
FORWARD_ALL_GATHER | 实际 all-gather 字节大 | 升 stage3_param_persistence_threshold(小参数不切,省 all-gather) |
BACKWARD_FETCH_WAIT | 反向时也要再 gather 一遍参数 | 开 contiguous_gradients;保证 overlap_comm |
BACKWARD_PREFETCH_SUBMIT | 反向预取频繁 | 同上;考虑提高 stage3_max_live_parameters |
BACKWARD_ALL_GATHER | 反向 all-gather 大头 | checkpointing 用 partition_activations 配合 |
| "optim_step" (BF16/FP16 wrapper) | step 本身慢 | 用 FusedAdam;offload 时检查 cpu_adam 是否真的编了 |
一个经验顺序:先看 *_WAIT 大不大;其次看 ALL_GATHER 大不大;最后看 SUBMIT。WAIT 大几乎都是带宽 / 拓扑问题,调字段救不回来。
11.11 ZeRO++(HPZ 分层 + 量化通信)
ZeRO++ 的核心 idea:节点内通信便宜(NVLink ~ 600 GB/s)、节点间通信贵(IB ~ 50 GB/s)。所以——
- 把 ZeRO-3 的 all-gather 拆成两层:节点内 all-gather + 节点间 broadcast,节点间数据量减少;
- 节点间传输前量化:权重 int8、梯度 int4,带宽再省 4×。
对应字段(ds_config):
| 字段 | 含义 | 典型值 |
|---|---|---|
zero_optimization.zero_hpz_partition_size | "节点内"分区数;一般 = 节点的 GPU 数(如 8) | 8 |
zero_optimization.zero_quantized_weights | 跨节点传 weight 时 int8 量化 | true(多机时开) |
zero_optimization.zero_quantized_gradients | 跨节点传 grad 时 int4 量化 | true(多机时开) |
陷阱:
- 单机不要开:HPZ 分区数 = WORLD_SIZE 时跟原 ZeRO-3 等价,但多绕一层 wrapper 反而慢;
- 量化梯度会引入数值误差,对 fp16 训练影响明显(bf16 一般 OK);
- 跨架构 checkpoint 不可移植 —— 量化路径写下的 checkpoint 只能在 ZeRO++ 下加载。
11.12 DeepCompile:把 ZeRO 和 torch.compile 真正打通
torch.compile 单独用不难,但跟 ZeRO-3 的"参数动态 all-gather / release" 配合不是显然的——参数地址会变,graph 重编频率高。DeepCompile(deepspeed/compile/)的工作是把 ZeRO 的 hook 行为转成编译器能理解的 IR pass。
| 字段 | 含义 |
|---|---|
compile.deepcompile: true | 启用 |
compile.free_activation | 编译期分析后激进释放激活(比 ckpt 更细粒度) |
compile.offload_activation | 激活也搬 CPU |
compile.offload_opt_states | 和 ZeRO offload 协同(避免重复搬) |
compile.offload_parameters | 同上 |
compile.double_buffer | 双缓冲通信,进一步藏延迟 |
compile.passes | 启用哪些 pass,如 ["z1", "z3", "autosp"] |
关键源码:compile/backend.py:44 的 GraphOrder 跟踪 frame ID 与图执行顺序;line 79 register_compile_pass(name, fn) 注册 custom pass;line 95 launch_compile_passes(global_steps) 按 step 触发 pass。pass 调度逻辑在 compile/list_schedule.py。
启用前置:PyTorch 2.x 带 torch._dynamo、装好 functorch、AOT autograd 可用。第一步 step 会卡几分钟编译。
11.13 Autotuner 搜索空间
deepspeed/autotuning/autotuner.py:42 的 Autotuner 在跑一组短训练实验找最佳配置。搜索维度(你不写也会自动展开):
| 维度 | 枚举范围 |
|---|---|
| ZeRO stage | 0 / 1 / 2 / 3 |
offload_optimizer.device | none / cpu (/ nvme) |
offload_param.device | none / cpu |
| micro_batch_size_per_gpu | 1, 2, 4, 8, ...(直到 OOM) |
| gradient_accumulation_steps | 满足总 batch 的几个值 |
三种 tuner(autotuning/tuner/):
- gridsearch:穷举(小空间 OK);
- random:随机抽样(大空间 + 时间紧);
- modelbased:用代理模型(Gaussian Process / RF)逐步收敛;
配 "max_evals": 30 控制最多跑多少组合。结果写到 results_dir,里面 exps_dir/exps_*.json 是每次实验的 config,results.csv 是吞吐对比。第一次接触新硬件时极有用——比手调省两天。