4. 通信原语与库:NCCL基础、All-Reduce、All-Gather、Reduce-Scatter、Broadcast
多卡协同,说白了就是让一堆GPU高效地“聊天”。
你想想看,单卡训练模型,数据量大了显存放不下,计算时间也长。多卡并行是出路,但卡与卡之间怎么交换数据?这就得靠通信原语和库了。
我个人习惯把通信原语比作“集体舞的固定动作”。每个GPU都知道自己该在什么时候、跟谁、交换什么数据。而NCCL,就是NVIDIA官方编排这些动作的“舞蹈教练”。
4.1 NCCL:多卡通信的基石
NCCL全称是NVIDIA Collective Communications Library。嗯,名字很长,但核心就一句话:它帮你把多卡通信的脏活累活全干了。
我在项目中遇到过最头疼的事,就是自己手写多卡同步逻辑。用MPI?太重了。用Socket?你得处理丢包、重传、拓扑感知……后来换成NCCL,世界清净了。
NCCL的优势在于:
- 硬件感知:它知道GPU之间是走NVLink、PCIe还是InfiniBand,自动选最优路径。
- 极致优化:内部用了很多底层trick,比如RDMA、GPUDirect,延迟极低。
- 易用性:你只需要调用几个API,剩下的交给它。
4.2 All-Reduce:最常用的集体舞
All-Reduce是分布式训练里出现频率最高的操作。没有之一。
它的目标很简单:每个GPU上的数据,经过All-Reduce后,所有GPU都得到相同的结果(通常是求和或求平均)。
举个例子:你有4张卡,每张卡上有一个数字[1, 2, 3, 4]。做All-Reduce求和后,每张卡都得到[10, 10, 10, 10]。
为什么会这样?因为每个位置上的数字都加了一遍。
在NCCL里,调用All-Reduce就一行代码:
ncclAllReduce(sendbuff, recvbuff, count, ncclFloat, ncclSum, comm, stream);
参数含义很直观:发送缓冲区、接收缓冲区、元素个数、数据类型、操作类型(求和)、通信器、CUDA流。
4.3 All-Gather:收集所有人的数据
All-Gather和All-Reduce有点像,但结果不同。
All-Gather不求和,它只是把每个GPU上的数据“拼”在一起,然后分发给所有人。
假设你有4张卡,每张卡上有一个向量:卡0有[A],卡1有[B],卡2有[C],卡3有[D]。All-Gather之后,每张卡都得到[A, B, C, D]。
这个操作在什么场景下用?
- Batch Normalization的同步:需要收集所有卡的均值和方差。
- 模型并行中的参数同步:某些层需要全局信息。
NCCL调用方式:
ncclAllGather(sendbuff, recvbuff, sendcount, ncclFloat, comm, stream);
注意:recvbuff的大小必须是sendbuff的N倍(N是GPU数量)。
4.4 Reduce-Scatter:先求和,再分发
Reduce-Scatter是All-Reduce的“前身”。很多高性能的All-Reduce实现,底层就是先做Reduce-Scatter,再做All-Gather。
它的逻辑是:
- 把数据分成N块(N是GPU数量)。
- 每个GPU负责对其中一块做Reduce(比如求和)。
- 最后每个GPU只保留自己负责的那一块结果。
举个例子:4张卡,每张卡有数据[1,2,3,4]。Reduce-Scatter后:
- 卡0得到[1+1+1+1] = [4]
- 卡1得到[2+2+2+2] = [8]
- 卡2得到[3+3+3+3] = [12]
- 卡3得到[4+4+4+4] = [16]
你看,每个GPU只保留了部分结果。这为后续的All-Gather做好了准备。
NCCL调用:
ncclReduceScatter(sendbuff, recvbuff, recvcount, ncclFloat, ncclSum, comm, stream);
4.5 Broadcast:一对多广播
Broadcast是最简单的通信原语。一个GPU(称为root)把自己的数据发给所有其他GPU。
场景很明确:
- 初始化参数:把模型参数从卡0广播到所有卡。
- 同步随机种子:确保所有卡用相同的随机数生成器初始状态。
NCCL调用:
ncclBroadcast(buff, buff, count, ncclFloat, root, comm, stream);
注意:sendbuff和recvbuff可以是同一个缓冲区。因为root既发送也接收,其他卡只接收。
4.6 如何选择?一张表说清楚
| 原语 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
| All-Reduce | 每卡一份数据 | 每卡得到全局Reduce结果 | 梯度同步 |
| All-Gather | 每卡一份数据 | 每卡得到所有数据拼接 | 同步BN统计量 |
| Reduce-Scatter | 每卡一份数据 | 每卡得到部分Reduce结果 | All-Reduce的中间步骤 |
| Broadcast | root有一份数据 | 所有卡得到相同数据 | 参数初始化 |
嗯,到这里你应该对这四个原语有了清晰的认识。我个人觉得,All-Reduce是重中之重,你至少得把它用熟。其他几个,遇到具体场景再查文档也来得及。
最后提醒一句:NCCL的通信是异步的,记得用cudaStreamSynchronize或者ncclGroupEnd来确保操作完成。我曾经因为忘了同步,读到了未就绪的数据,调试了一整个下午。