NCCL通信原语:AllReduce、AllGather、ReduceScatter、Broadcast的原理与适用场景
聊到NCCL,绕不开的就是这几个核心通信原语。说白了,它们就是多卡通信的"基本动作"。我在项目中见过不少同学,API调得飞起,但一问底层原理就含糊了。嗯,这其实挺危险的——你不懂原语,就不知道怎么选,更不知道怎么调优。
今天我把这四个原语掰开揉碎讲清楚。每个我都会聊三个点:它干了什么、它怎么干的、什么时候用它。
AllReduce:最常用的"求和"操作
AllReduce,顾名思义,就是所有节点都拿到最终结果。最常见的是求和操作——每张卡算出一份梯度,最后大家手里都有一份完整的梯度总和。
它的核心逻辑是这样的:
- 每张卡输入一个向量(比如梯度)
- 所有卡的向量做逐元素求和(或取最大值等)
- 最终每张卡都得到相同的结果
重要:AllReduce 是分布式训练中使用频率最高的原语。PyTorch DDP 的梯度同步,底层就是调用的 AllReduce。
我个人的习惯是,在调试分布式训练时,第一个排查的就是 AllReduce 的带宽利用率。如果跑不满,后面基本不用看了。
AllReduce 的实现方式
NCCL 内部用了多种算法来实现 AllReduce。最常见的是Ring AllReduce。它的思路很巧妙:
- 把数据切成 N 块(N 等于卡数)
- 每张卡只和前后邻居通信
- 先做 ReduceScatter,再做 AllGather
你想想看,这样每张卡只需要发 N-1 次数据,而且每次数据量只有 1/N。带宽利用率非常高。
小技巧:如果你用 8 卡机器,Ring AllReduce 的通信量是 2*(N-1)/N * 数据量。N 越大,效率越高。所以 8 卡比 4 卡划算。
AllGather:把碎片拼成完整图
AllGather 做的事情很简单:每张卡把自己的一块数据广播给所有人,最后每张卡都拥有完整的数据集。
举个例子:假设你有 4 张卡,每张卡存了 1/4 的参数。AllGather 之后,每张卡都拿到了全部参数。
它的适用场景很明确:
- 模型并行中的参数同步
- 数据并行中的全量梯度收集
- 需要全局信息的计算(比如 BatchNorm 的全局统计量)
我曾经在一个 64 卡的项目里,发现 AllGather 成了瓶颈。排查了半天,原来是数据切分不均匀,导致某些卡等了好久。嗯,这个问题其实很隐蔽。
ReduceScatter:先归约,再分发
ReduceScatter 是 AllReduce 的前半段。它的逻辑是:每张卡只负责归约一部分数据,然后把结果留在自己手里。
说白了,就是:
- 每张卡把数据切成 N 块
- 第 i 张卡负责归约所有卡的第 i 块数据
- 归约结果只保留在第 i 张卡上
这个原语在模型并行中特别有用。比如你做了张量并行,每张卡只存了一部分参数。反向传播时,你只需要把梯度归约到对应的卡上,不需要全量 AllReduce。
注意:ReduceScatter 的输出是分散的。如果你需要每张卡都有完整结果,后面还得接 AllGather。这两个合起来就是 AllReduce。
Broadcast:一对多的"广播"
Broadcast 是最简单的原语:一张卡把数据发给所有其他卡。
它的典型场景:
- 初始化参数广播(主卡初始化,其他卡同步)
- 模型保存/加载时的参数分发
- 控制信号的传递
Broadcast 的实现也很直接。NCCL 会用树形拓扑或者环形拓扑来做。我个人建议,如果数据量不大(比如几 MB 以内),直接用 Broadcast 就行。数据量大了,还是用 AllGather 更划算。
四种原语的对比
| 原语 | 输入 | 输出 | 通信量 | 典型场景 |
|---|---|---|---|---|
| AllReduce | 每卡一份数据 | 每卡一份归约结果 | 2*(N-1)/N * 数据量 | 梯度同步(DDP) |
| AllGather | 每卡一份数据 | 每卡一份完整数据 | (N-1)/N * 数据量 | 参数同步、全局统计量 |
| ReduceScatter | 每卡一份数据 | 每卡一份部分归约结果 | (N-1)/N * 数据量 | 张量并行、模型并行 |
| Broadcast | 一张卡有数据 | 所有卡都有数据 | 数据量 | 参数初始化、控制信号 |
核心知识体系图
下面这张图展示了四个原语之间的关系和它们在分布式训练中的位置:
如何选择正确的原语?
这个问题我经常被问到。我的建议很简单:
- 做梯度同步 → AllReduce(DDP 默认就是这个)
- 收集所有卡的参数 → AllGather(比如做 checkpoint 时)
- 模型并行中的梯度归约 → ReduceScatter(只归约自己需要的那部分)
- 初始化或控制信号 → Broadcast(简单直接)
避坑指南:我曾经在 32 卡集群上,用 Broadcast 去同步大模型参数(几个 GB)。结果网络直接被打满了,训练速度慢得像蜗牛。后来换成 AllGather,带宽利用率直接翻倍。所以记住:小数据用 Broadcast,大数据用 AllGather。
好了,这四个原语的核心内容就这些。你只要理解了它们各自的特点和适用场景,写分布式代码时就不会抓瞎了。记住一点:通信原语的选择,直接决定了你的训练效率。