3. 梯度同步机制:AllReduce 算法详解(Ring AllReduce、Tree AllReduce)
好,咱们接着聊。上一节我们把数据并行的整体流程捋了一遍,你知道了每个GPU算完自己的梯度后,得有个“汇总”的步骤。这个步骤,就是梯度同步。说白了,就是把所有卡上的梯度加起来,求个平均,然后分发给每一张卡。
那问题来了:怎么高效地完成这个“求和再分发”的操作?这就引出了我们今天的主角——AllReduce 算法。
我个人习惯把 AllReduce 看作是分布式训练的“交通枢纽”。它不通畅,整个训练就得堵死。我早期做多卡训练时,就吃过这个亏,以为卡多就一定快,结果通信开销直接把加速比拉成了负数。嗯,这里要注意,选对 AllReduce 算法,比单纯堆硬件重要得多。
3.1 什么是 AllReduce?
AllReduce 是一个集合通信操作。它的目标很简单:每个节点都把自己的数据贡献出来,最终每个节点都拿到所有数据的规约结果。最常见的规约操作就是求和(SUM),当然也有求最大值、最小值等。
在数据并行里,我们做的就是梯度求和,然后除以卡数,得到平均梯度。
你想想看,如果只有两张卡,那好办,互相传一下就行。但如果有8张、16张、甚至64张卡呢?如果每张卡都跟其他所有卡通信,那就是一个全连接的网络,通信量是 O(N²),卡一多,网络直接爆炸。
所以,我们需要更聪明的算法。
3.2 朴素 AllReduce:参数服务器模式
在讲主流算法之前,我先提一嘴最朴素的方式——参数服务器。这个模式我早期做项目时用过,结构很简单:
- 有一个中心节点(参数服务器),所有工作节点把梯度发给它。
- 参数服务器求和后,再把结果广播回去。
这个模式的问题很明显:中心节点会成为瓶颈。带宽是有限的,所有卡都往一个点灌数据,网卡先扛不住。我曾经在一个16卡的项目里试过,结果参数服务器的网卡直接跑满,其他卡都在干等。所以,现在大规模训练基本不用这个了。
3.3 Ring AllReduce:最流行的方案
Ring AllReduce 是目前最主流的算法。NVIDIA 的 NCCL 库默认就用的它。为什么流行?因为它把通信负载均匀地分摊到了每一张卡上,没有瓶颈。
它的原理,我拆成两步来讲:
3.3.1 第一步:Scatter-Reduce(分散-规约)
假设我们有 N 张卡,每张卡上的梯度张量被分成 N 个块(chunk)。
然后,卡与卡之间形成一个逻辑上的环。每张卡只跟它的前后邻居通信。
具体操作是这样的:
- 每张卡把自己第 i 个块发给下一张卡。
- 同时,从上一张卡接收第 i-1 个块。
- 收到后,把接收到的块和自己的对应块相加。
- 重复 N-1 次。
做完这一步,每张卡上都有一个块,这个块是所有卡对应块的累加和。但其他块呢?还在别的卡上。
3.3.2 第二步:AllGather(全收集)
现在,每张卡都持有一个“全局求和后的块”。我们需要把这些块分发给所有卡。
操作跟第一步类似,只是把“相加”换成了“覆盖”:
- 每张卡把自己持有的全局块发给下一张卡。
- 同时,从上一张卡接收它持有的全局块。
- 直接覆盖本地对应的块。
- 重复 N-1 次。
做完这一步,每张卡上都拥有了完整的、求和后的梯度张量。
你算算通信量:每张卡在每一步都发送和接收一个块。总共 2*(N-1) 步,每步传输的数据量是总数据量的 1/N。所以总通信量是 2*(N-1)/N * 总数据量。当 N 很大时,趋近于 2倍的总数据量。这比参数服务器的 N 倍要好太多了。
3.4 Tree AllReduce:另一种思路
Tree AllReduce 是另一种经典方案。它把通信组织成一棵树的结构。
也分两步:
3.4.1 Reduce 阶段(自底向上)
叶子节点把自己的梯度发给父节点。父节点收到所有子节点的梯度后,求和,再向上传递。最终,根节点持有完整的梯度总和。
3.4.2 Broadcast 阶段(自顶向下)
根节点把平均后的梯度广播给子节点,子节点再往下传,直到所有叶子节点都收到。
Tree AllReduce 的优点是延迟低。因为通信步数是 O(log N),而 Ring 是 O(N)。在卡数非常多(比如几百上千张)时,Tree 的延迟优势就体现出来了。
但它的缺点是:根节点和靠近根节点的链路会成为瓶颈。根节点要处理所有数据的汇总和分发,带宽压力大。
- Ring AllReduce: 带宽利用率高,适合带宽受限、卡数适中的场景(如单机8卡)。
- Tree AllReduce: 延迟低,适合卡数极多、对延迟敏感的场景(如大规模跨机训练)。
3.5 核心逻辑流程图
下面我用一张 SVG 图,把 Ring AllReduce 的两步流程画出来。你一看就明白数据是怎么流动的了。
3.6 实际工程中的选择
在实际工程里,我们一般不用自己实现 AllReduce。NVIDIA 的 NCCL 库已经封装好了。你只需要在 PyTorch 里调用 torch.distributed.all_reduce 就行。
但了解底层原理,能帮你更好地调优。比如:
- 如果你的网络带宽是瓶颈(比如千兆以太网),Ring AllReduce 是首选。
- 如果你的卡间延迟很高(比如跨地域训练),Tree AllReduce 可能更合适。
- NCCL 2.0 之后,还引入了 Hierarchical AllReduce,它在机内用 Ring,机间用 Tree,结合了两者的优点。我最近的项目就在用这个,效果不错。
NCCL_ALGO 来强制 NCCL 使用特定的算法。比如 export NCCL_ALGO=Ring。但一般情况下,让 NCCL 自动选择就好,它内部有启发式算法。
好了,关于梯度同步的 AllReduce 算法,我们就聊到这里。核心就两点:Ring 均匀分摊负载,Tree 降低延迟。理解它们,你就能在遇到通信瓶颈时,知道该往哪个方向去优化。