多卡微调代码模型,并行组合策略对长序列稳定性的几个实测结论
实测跑了几组多卡微调代码模型的配置,结论先放前面:在长序列(8K 以上)场景下,张量并行(TP)优先、数据并行(DP)兜底的组合对训练稳定性最友好;纯 DP 在超长序列时容易出现显存碎片和 loss 尖刺,而 TP 过大又会把通信延迟放大到不可接受的程度。下面按具体实验数据展开。
我测了哪些组合
硬件环境:4 台 8×A100 80GB 节点,节点内 NVLink 600GB/s,节点间 InfiniBand HDR 200Gbps。模型用 CodeLlama-13B 和 DeepSeek-Coder-33B,微调数据是 8K~32K 长度的代码补全与文档生成混合语料,框架基于 Megatron-DeepSpeed。
测了三类组合:
- 纯 DP:TP=1,DP=32,PP=1,ZeRO-2/3 分别测过
- TP+DP 混合:TP=2/4/8,DP 对应 16/8/4,PP=1,序列并行开启
- TP+PP+DP:TP=4,PP=2,DP=4,主要用于 32K 场景
训练超参统一:global batch 128,micro batch 2,AdamW,lr 2e-5,bf16,flash-attn 2.5.6,序列并行使用 ring attention(DeepSpeed Ulysses 实现)。
结论一:TP=2 是 8K~16K 长度下的稳定性甜点
在 8K 长度、CodeLlama-13B 上,TP=2+DP=16 的配置跑 5000 步,loss 曲线全程无尖刺,最大单步 loss 波动不超过均值 ±0.8。而纯 DP=32(ZeRO-3)在同一数据上,第 1800 步左右出现一次 loss 从 2.1 跳到 4.7 的尖刺,之后缓慢恢复;显存监控显示尖刺前后有 5 次 OOM 重试触发了 offload 降级,原因大概率是长序列激活在 DP 切分下碎片化分配。
16K 长度时纯 DP 的问题更明显:ZeRO-3 需要把优化器状态也切分到 32 卡,每张卡上的激活片段按序列维度分配,ring attention 的通信拓扑在 DP 组内产生额外的 all-to-all 开销。实测 16K、纯 DP 的每步耗时比 TP=2+DP=16 慢了 18%,并且 3000 步内出现 3 次 loss 尖刺。
TP=2 的优势在于:序列并行把长序列的激活切到 2 张卡上,单卡激活显存减半,碎片化程度大幅降低;同时 TP 组内通信走 NVLink,在 2 卡规模下 all-gather/reduce-scatter 的延迟几乎可以忽略(实测 1.2ms/层以内)。13B 模型 TP=2 后单卡显存占用从 61GB 降到 38GB,留给激活和碎片的空间充裕得多。
结论二:TP=4 在 32K 长度开始暴露通信瓶颈,需要引入 PP
32K 长度的测试我上了 DeepSeek-Coder-33B。先试 TP=4+DP=8,单步耗时达到 11.3 秒(对比 16K 时 TP=2 是 4.7 秒)。剖开一看,TP 组内 4 卡的 ring attention 通信占了单步耗时的 41%,其中跨节点的 2 路通信(因为 4 卡跨 2 个节点)走 InfiniBand,all-to-all 的延迟从 NVLink 的 0.9ms 飙到 6.8ms。
引入 PP 后(TP=4+PP=2+DP=4),单步耗时降到 8.9 秒。PP 把 32K 序列切成两个 16K 的 pipeline stage,每个 stage 内部的 TP 通信量减半,通信和计算的重叠率提高。但 PP 也有代价:pipeline bubble 让 GPU 利用率从 91% 掉到 84%,而且需要重写 data iterator 来配合 micro-batch 的流水调度。
这里有个实测细节值得单独说:PP=2 时如果不开 --num-layers-per-virtual-pipeline-stage(Megatron 的 interleaved pipeline),bubble 会额外多出 7%~9%。开了 interleaved schedule 后,33B 模型按 24 层一个 virtual stage 切,bubble 控制在 5% 以内。
结论三:ZeRO-3 + TP 叠加时,reduce_bucket_size 和 allgather_bucket_size 必须手动调
这个坑花了我们两天。Megatron-DeepSpeed 默认的 bucket size 是 5e8(500MB),在 32K 长度、TP=4 的配置下,ZeRO-3 的参数 allgather 会把 33B 模型的 660 亿参数按 bucket 切分后打散到 DP=8 的组里。默认 bucket 太大,导致 allgather 通信与 TP 的 reduce-scatter 在同一时间窗口竞争 InfiniBand 带宽,实测出现周期性 loss 震荡(每 200 步一个 ±1.5 的波动,周期和 bucket 轮转一致)。
把 reduce_bucket_size 和 allgather_bucket_size 都降到 2e8(200MB)后,通信粒度变细,和 TP 通信的错峰效果变好,周期震荡消失。这个参数在 8K/16K 场景下用默认值没问题,但 32K 会触发,原因和 ring attention 的通信模式变化有关,具体机理我还没完全搞清楚,但数值上稳定了。
另外 ZeRO-2 在 TP≥2 时表现比 ZeRO-3 差:ZeRO-2 不切分优化器状态,33B 模型 + 32K 序列下,TP=4 的单卡显存峰值到 76GB,频繁触发显存碎片整理(碎片率 23%),比 ZeRO-3 的 58GB 高不少。所以长序列 + TP 混合时,优先 ZeRO-3。
结论四:纯 DP 在超长序列下不是不能用,但需要手动做序列切块
如果坚持纯 DP(比如卡数限制没法做 TP),有个 workaround:在数据预处理阶段把长序列切成固定长度的块,块之间用注意力掩码隔断。我们试过 32K 序列切成 4×8K 块,纯 DP=32 + ZeRO-3,loss 稳定性和 TP=2 接近,但代价是长距离依赖被切断,代码生成任务上 eval 的 pass@1 掉了 2.3 个百分点。
这个方案适合长文档摘要这类局部依赖为主的任务,不适合代码生成这种需要跨文件引用、全局符号解析的场景。代码模型对长距离依赖的敏感度比文本模型高,我们在 DeepSeek-Coder 上对比过:同样是 32K 切块,文本任务的退化在 0.5% 以内,代码任务退化超过 2%。
结论五:CUDA_LAUNCH_BLOCKING=1 对稳定性排查有奇效,但别开着跑正式训练
长序列 + 多卡并行时,异步 launch 的报错往往延迟几十步才出现,定位极难。我们在排查一次 500 步后出现的非法内存访问时,开了 CUDA_LAUNCH_BLOCKING=1 让报错精确指向了 ring attention 的 forward kernel 里一个 offset 计算。定位到问题后关掉这个变量,训练速度恢复。
正式训练千万别开,单步耗时会慢 3~5 倍。它只适合 debug 阶段。
实际推荐的配置速查
| 序列长度 | 模型规模 | 推荐并行组合 | 关键参数 |
|---|---|---|---|
| ≤4K | ≤13B | 纯 DP + ZeRO-3 | 默认即可 |
| 8K~16K | 13B~33B | TP=2 + DP + ZeRO-3 | 开启序列并行 |
| 16K~32K | 33B | TP=4 + PP=2 + DP + ZeRO-3 | interleaved pipeline,bucket 降到 2e8 |
| >32K | ≥33B | TP=8 + PP≥2 + ZeRO-3 | 需要跨节点 TP,带宽需 ≥200Gbps |
最后补充一句:这些结论基于 A100 + InfiniBand 环境。如果换成 H100 + NVLink 4.0 或 HDR 以上的网络,TP=4 的通信瓶颈会缓解,TP 甜点可能右移到 4~8;如果网络只有 100Gbps 或更低,TP 超过 2 就要谨慎。
常见问题
为什么 TP=2 比 TP=1(纯 DP)更稳定,明明通信更多了?
稳定性问题出在显存碎片化而不是通信量。纯 DP 时,长序列的激活按序列维度切分到每张卡上,ring attention 在 DP 组内做 all-to-all,每张卡的激活片段大小和地址分布不均匀,长期训练后碎片率升高,触发 OOM 重试和 loss 尖刺。TP=2 把激活切到 2 张卡上,单卡激活量减半,碎片化概率大幅下降,而 TP 组内的额外通信走 NVLink,延迟很低,净收益为正。
TP 和序列并行是什么关系?需要单独开吗?
在 Megatron-DeepSpeed 里,序列并行是 TP 的附属功能。开启 TP≥2 后,Attention 部分的激活会自动按序列维度切分,这就是 sequence parallelism。不需要单独配置,但要注意 ring attention 的实现选择(Ulysses 还是 ring),不同实现对长序列的通信模式不同。
多机训练时 TP 跨节点好还是限制在单节点内好?
尽量限制在单节点内。TP 的通信是同步且频繁的(每层多次 all-gather/reduce-scatter),跨节点走 InfiniBand 的延迟是 NVLink 的 5~8 倍,对单步耗时影响巨大。如果模型太大必须跨节点 TP,优先考虑 PP 来减少 TP 规模,或者升级到 200Gbps 以上的网络。
长序列训练时 loss 每隔固定步数震荡一次,是什么原因?
优先查 ZeRO 的通信 bucket 和 TP 通信的带宽竞争。默认 bucket size 在长序列下过大,导致 ZeRO 的 allgather 和 TP 的 reduce-scatter 周期性撞车。把 reduce_bucket_size 和 allgather_bucket_size 从默认 5e8 降到 2e8 通常能解决。