跳转至

第 3 章 数据并行与梯度同步

开场问题:单卡能跑,但要三年才训完。加卡最直接的办法是什么,它的极限在哪?

3.1 最简单的扩展

每张卡保存一份完整模型,各自读不同的数据算出各自的梯度,然后所有卡把梯度求和(AllReduce)并除以卡数,最后各自用同一份平均梯度更新参数。因为初始权重相同、每步用的梯度也相同,各卡的参数始终保持一致,不需要额外同步。这就是 DDP。

它的数学含义很简单:N 张卡做数据并行,等价于单卡跑一个 N 倍大的 batch。所以数据并行不改变训练结果,只改变你多快能跑完。

通信量也容易算:每步每张卡要把全部梯度发出去再收回来,AllReduce 的实际传输量约为 2 × 参数量 × 精度字节。8B 模型 bf16 梯度是 16 GB,每步每卡要搬约 32 GB——这个数字是后面所有并行策略的比较基准。

3.2 全局 batch 是算法超参,不是系统参数

全局 batch = 每卡 micro-batch × 梯度累积步数 × 数据并行卡数。

这里有个新手最常踩的坑:卡数从 64 加到 256,如果其他两项不变,全局 batch 也跟着变成 4 倍,学习率、warmup 步数、总步数全都不再匹配,这已经不是原来那个实验了。正确做法是先确定目标全局 batch(由算法和数据量决定),再反过来分配卡数和累积步数。

batch 也不是越大越好。超过某个"临界批量"后,梯度已经足够准,再增大只是让同样的梯度信息重复计算,token 效率下降。工业上的做法是把全局 batch 定在临界点附近,剩下的算力用来加卡缩短 wall-clock,而不是继续放大 batch。

3.3 梯度桶与通信重叠

朴素实现是等整个反向传播结束再统一 AllReduce,此时 GPU 完全空闲等网络。DDP 的做法是把参数按反向顺序分成若干 bucket(默认 25 MB 量级),某个 bucket 里的梯度全部就位就立刻发起该桶的 AllReduce,同时 GPU 继续算前面的层。

于是通信被藏在计算后面。桶太小,每次通信的数据量不足以摊薄启动开销,且 kernel 数量暴增;桶太大,最后一个桶要等反向全部结束才能发,藏不住。这是第 2 章"通信与计算重叠"在最简单场景下的具体形态。

3.4 为什么加卡不是线性加速

四个常见原因,排查时按这个顺序看:通信占比随卡数上升(AllReduce 的延迟项与卡数相关);数据管线跟不上,GPU 在等数据;CPU 侧的 Python 调度和 kernel launch 成为瓶颈(小模型尤其明显);集群里有慢卡,同步训练下所有人都得等它。

报告扩展效率时必须固定全局 batch,比较 step 时间的变化。只说"加卡之后总吞吐上升了"没有意义——那是因为 batch 也变大了。

核心问答

Q:DDP 省显存吗? 不省。每张卡都有完整的权重、梯度和优化器状态,它只提高吞吐。省显存是下一章的事。

Q:某张卡慢 10% 会怎样? 整个训练慢 10%。同步训练每步都有全局同步点,最慢的卡决定 step 时间,所以慢节点(straggler)比偶发崩溃更难受——它不报错。

Q:梯度累积和加卡等价吗? 对梯度而言基本等价(都是在更多样本上平均),但累积是串行的,不缩短 wall-clock;加卡是并行的。两者常配合使用,共同凑出目标全局 batch。

Q:什么时候只用 DDP 就够? 模型加上梯度和优化器状态能装进单卡、数据管线喂得饱的时候。它实现最简单、故障面最小,不要为了"显得专业"去开更复杂的并行。