/ AI Infra
[Infra-2] NCCL
A first-person walkthrough of NCCL: its role in distributed GPU systems, common collectives, topology-aware communication, multi-node paths, and practical debugging.
我一开始看 NCCL,并不是因为我想从零写一个通信库,而是因为多卡训练和大模型推理里很多性能问题,最后都会绕回同一个问题:卡和卡之间到底是怎么交换 tensor 的?
在单卡上,问题通常还比较直观。模型参数、activation、梯度、KV cache 大多都在一张卡里流动,瓶颈更多来自算子、显存、调度或者 batch 组织。但一旦进入多卡、多机,事情就变了。每一张卡只负责一部分计算,算完以后必须把结果同步、聚合、拼接或者重新分发。这个时候,通信就不再是背景音,而是系统性能的一部分。
NCCL 就是在这个位置出现的。
我现在更愿意把它理解成:NVIDIA GPU 集群里真正负责搬 tensor 的高速通信引擎。上层框架可以是 PyTorch、Megatron、vLLM 或 TensorRT-LLM;并行方式可以是 data parallel、tensor parallel、pipeline parallel 或 expert parallel;但只要底层是 NVIDIA GPU,并且涉及多卡 collective,NCCL 往往就在通信路径上。
1. NCCL 在系统里到底是什么#
NCCL 的全称是 NVIDIA Collective Communications Library,读作 Nickel。它不是训练框架,也不是任务调度器,而是一个面向多 GPU、多节点 GPU 的 collective communication library。
它提供的常见通信原语包括:
AllReduceBroadcastReduceAllGatherReduceScatterAllToAllGather / ScatterSend / Recv
如果把一个多卡系统拆成几层,我会这样放置 NCCL:
应用层:
PyTorch / vLLM / Megatron / TensorRT-LLM
并行策略层:
Data Parallel / Tensor Parallel / Pipeline Parallel / Expert Parallel
分布式通信 API:
torch.distributed / ProcessGroup
通信库:
NCCL / CNCL / RCCL / oneCCL
硬件互联:
NVLink / NVSwitch / PCIe / InfiniBand / RoCE / EthernetNCCL 位于分布式通信 API 和硬件互联之间。上层写的是:
dist.all_reduce(tensor)底层真正发生的可能是:
GPU0 -> GPU1 -> GPU2 -> ...
NIC0 -> switch -> NIC1所以我看 NCCL 时,关注点不是“这个库有什么 API”这么简单,而是它怎样把一个高层 collective 调用,映射到底层拓扑、链路、网卡和通信算法上。
2. 为什么深度学习需要 NCCL#
最典型的例子是数据并行训练。
假设我有 8 张 GPU,每张卡处理不同的 mini-batch。反向传播结束后,每张卡都会得到一份梯度:
GPU0: grad_0
GPU1: grad_1
GPU2: grad_2
...
GPU7: grad_7为了让 8 张卡上的模型参数保持一致,我需要把这些梯度求和或求平均:
avg_grad = (grad_0 + grad_1 + ... + grad_7) / 8然后每张卡都拿到同一份 avg_grad,再各自更新参数。这个过程通常就是 AllReduce。
在大模型推理里,通信同样绕不开。比如 tensor parallel size 等于 4 时,一个线性层的权重可能被切成 4 份:
完整矩阵 W 被切成:
GPU0: W0
GPU1: W1
GPU2: W2
GPU3: W3每张卡只计算一部分输出:
GPU0: y0 = xW0
GPU1: y1 = xW1
GPU2: y2 = xW2
GPU3: y3 = xW3下一步就要通过 AllGather、AllReduce 或 ReduceScatter 把结果拼起来、加起来,或者继续分片传递下去。
这也是为什么在 vLLM 这类推理框架里,NCCL/CNCL 这类通信库的表现会直接影响:
- tensor parallel 的扩展效率;
- prefill 和 decode 的延迟;
- 高并发吞吐;
- P99 latency 和 ITL;
- 多机部署时的跨节点性能。
如果每一层 forward 都要等一次通信,那么通信延迟就会直接进入单 token latency。模型越大、并行越细、跨节点比例越高,这件事就越明显。
3. 我理解 NCCL 时最先抓住的四个概念#
3.1 Rank#
rank 可以理解成分布式系统里的进程编号。通常一个 rank 绑定一张 GPU。
比如双机 16 卡:
Node0:
GPU0 -> rank 0
GPU1 -> rank 1
...
GPU7 -> rank 7
Node1:
GPU0 -> rank 8
GPU1 -> rank 9
...
GPU7 -> rank 15很多分布式 bug 的第一步,其实就是把 rank、local rank、device id 的关系理清楚。
3.2 World Size#
world_size 表示总 rank 数。
双机 16 卡就是:
world_size = 16如果我以为系统里有 16 个 rank,但实际上某台机器只启动了 7 个 worker,那后面初始化或者 collective 很可能会卡住。
3.3 Local Rank#
local_rank 是当前机器内部的 GPU 编号。
例如第二台机器上的第 3 张卡:
global rank = 10
local rank = 2这个概念在写启动脚本和绑定设备时很关键。很多时候报错不是通信算法错了,而是 rank 映射和物理设备映射不一致。
3.4 Communicator#
NCCL 会为一组参与通信的 rank 建立 communicator。我会把它理解成这批 GPU 之间的通信上下文。
后面的 AllReduce、AllGather、ReduceScatter 等操作,都在这个 communicator 上发生。
4. 常见 collective:我怎么记它们#
NCCL 里有很多 collective,但日常分析训练和推理性能时,我最常遇到的是下面几个。
4.1 AllReduce:所有人贡献,所有人拿结果#
AllReduce 是我最先需要掌握的 collective。它的语义很简单:每个 rank 提供一份输入,所有输入做规约,然后每个 rank 都拿到同一份结果。
例如 4 张卡:
GPU0: [1, 2]
GPU1: [3, 4]
GPU2: [5, 6]
GPU3: [7, 8]做 sum allreduce 后,每张卡都得到:
[16, 20]在 DDP 训练里,它常用于同步梯度。在 tensor parallel 里,它常用于合并分片计算结果。
4.2 Broadcast:一个人发,所有人收#
Broadcast 是从一个 root rank 把数据复制给所有 rank。
GPU0 有参数 W
GPU1 没有
GPU2 没有
GPU3 没有
Broadcast 后:
GPU0: W
GPU1: W
GPU2: W
GPU3: W它适合初始化模型参数、同步配置,或者从主 rank 分发状态。
4.3 Reduce:所有人贡献,一个人拿结果#
Reduce 和 AllReduce 很像,区别是只有 root rank 拿到规约结果。
AllReduce: 所有 rank 都拿到结果
Reduce: 只有 root rank 拿到结果如果只是统计某个全局指标,或者只需要主进程汇总结果,Reduce 就更贴近语义。
4.4 AllGather:每个人一段,所有人拿全集#
AllGather 是把每个 rank 的数据收集起来,然后让每个 rank 都拿到完整拼接结果。
GPU0: A
GPU1: B
GPU2: C
GPU3: D
AllGather 后:
GPU0: [A, B, C, D]
GPU1: [A, B, C, D]
GPU2: [A, B, C, D]
GPU3: [A, B, C, D]在 tensor parallel 里,这个操作非常常见。例如列并行线性层之后,每张卡拿到一部分 hidden states,下一步可能就需要拼回完整 hidden states。
4.5 ReduceScatter:先规约,再分片#
ReduceScatter 可以理解成:
先 Reduce,再 Scatter它先把所有 rank 的数据做规约,再把规约后的结果切成不同分片发给不同 rank。
我觉得它最有用的记法是:
ReduceScatter + AllGather ≈ AllReduce在 ZeRO、FSDP、Megatron 风格的训练优化里,ReduceScatter 很常见,因为它可以把通信和显存分片结合起来。
4.6 AllToAll:每个人都给每个人发不同的数据#
AllToAll 的语义是每个 rank 都向其他 rank 发送不同的数据,同时也从其他 rank 接收不同的数据。
它在 MoE 里尤其重要。token 需要被路由到不同 expert,dispatch 和 combine 都可能涉及类似 AllToAll 的通信。这个操作对网络拓扑和交换结构非常敏感,也很容易成为 MoE 的主要瓶颈。
5. NCCL 为什么能快#
NCCL 的目标不是简单做到“能通信”,而是尽量吃满底层硬件带宽。
我理解它的高性能,主要来自三件事。
5.1 拓扑感知#
NCCL 会关心 GPU 之间的物理连接关系:
GPU-GPU 是否通过 NVLink 直连?
GPU-GPU 是否经过同一个 PCIe Switch?
GPU-GPU 是否跨 CPU Socket?
GPU-NIC 距离远不远?这些信息会影响 NCCL 选择通信路径。
例如同一台 8 卡机器里,有些 GPU 之间是 NVLink,有些需要跨 PCIe root complex,有些 GPU 离 NIC 更近。NCCL 会尽量构建更合理的 ring、tree 和 channel,避免所有流量都挤在低效路径上。
5.2 Ring 和 Tree#
Ring 是 AllReduce 里最经典的算法之一。假设 4 张卡组成一个环:
GPU0 -> GPU1 -> GPU2 -> GPU3 -> GPU0Ring AllReduce 通常可以拆成两个阶段:
ReduceScatter:
每张卡把数据切块,在环上流动并逐步累加
AllGather:
每张卡把最终分片继续传递,让所有卡得到完整结果Ring 的优势是大消息下带宽利用率高,缺点是步数和 rank 数相关,小消息 latency 不一定最优。
Tree 更像树形规约:
GPU0
/ \
GPU1 GPU2
/ \
GPU3 GPU4Tree 的优势是延迟相对低,适合小消息或中等消息;但在大消息下,它不一定比 ring 更能吃满带宽。
5.3 多 channel 并行#
NCCL 通常不会只走一条通信路径,而是把数据切成多个 chunk,用多个 channel 并行传输。
一个大 tensor
-> 切成多个 chunk
-> 多条通信通道并发传输
-> 最后聚合所以 NCCL 性能会和拓扑、message size、rank 数、NIC 数量强相关。不是所有机器都能靠同一个环境变量调好,也不是所有 collective 都会在同一个算法上表现最好。
6. 单机和多机通信的差别#
单机多卡时,NCCL 主要处理 GPU 到 GPU 的通信:
GPU <-> GPU底层路径可能是:
NVLink / NVSwitch
PCIe P2P
PCIe Switch
CPU root complex如果机器里有 NVLink 或 NVSwitch,通信性能通常会明显优于普通 PCIe。尤其是 tensor parallel 这种高频同步场景,TP group 最好尽量放在机内高速互联里。
多机时,通信会变成两层:
机内通信:
GPU <-> GPU
机间通信:
GPU <-> NIC <-> 网络 <-> NIC <-> GPU例如双机 16 卡的一次 AllReduce,逻辑上是 16 个 rank 一起做,但底层可能会分层优化:
第一层:每台机器内部 8 卡先聚合
第二层:两台机器之间通过网卡交换
第三层:每台机器内部再分发我现在看多机性能时,会先把瓶颈分成两类:
单机瓶颈:
GPU-GPU 拓扑,例如 NVLink / PCIe / NUMA
多机瓶颈:
NIC、交换机、RDMA、TCP、跨节点带宽、路由和过订阅很多时候,单机内通信已经很快,真正拖慢的是跨节点链路。这个时候继续盯着 GPU 算力看,方向就偏了。
7. PyTorch 和 vLLM 里我怎么遇到 NCCL#
在 PyTorch 里,我通常不会直接写 NCCL C API,而是通过 torch.distributed 间接调用。
import torch.distributed as dist
dist.init_process_group(
backend="nccl",
rank=rank,
world_size=world_size,
)
dist.all_reduce(tensor)这里的:
backend="nccl"表示 PyTorch 的 collective 操作底层交给 NCCL 处理。
在 vLLM 这类推理框架里,NCCL 主要出现在多卡并行推理场景。比如:
--tensor-parallel-size 4这意味着一个模型层可能被切到 4 张卡上。每一层 forward 过程中,可能会出现:
Linear 分片计算
-> AllReduce / AllGather
Attention 分片计算
-> AllReduce / AllGather
MLP 分片计算
-> AllReduce / ReduceScatter如果通信慢,表现出来就是:
每层 forward 等待通信
decode 每 token latency 上升
高并发吞吐下降
P99 ITL 变差如果是 pipeline parallel,通信更多是 send / recv activation,也就是上一段 pipeline 把 hidden states 发给下一段。
如果是 MoE,通信重点就会转向 AllToAll。这时我会特别关注网络对分带宽、跨节点比例、expert 放置和 token dispatch 的数据分布。
8. 调优和排查时,我最常用的 NCCL 环境变量#
NCCL 提供了很多环境变量。日常排查时,我最常先看这几个。
export NCCL_DEBUG=INFO打印 NCCL 初始化、拓扑、通信路径等信息。
export NCCL_SOCKET_IFNAME=eth0指定 TCP socket 使用哪个网卡。多机通信时,如果机器上有多张网卡,这个变量非常重要。
export NCCL_IB_HCA=mlx5_0,mlx5_1指定使用哪些 InfiniBand / RDMA 网卡。
export NCCL_CROSS_NIC=0控制多 NIC 场景下 ring/tree 是否跨不同 NIC。它不一定永远应该开或关,具体要看网络拓扑。
export NCCL_P2P_DISABLE=1禁用 GPU P2P 通信。这个变量通常只用于定位问题,不建议随便作为长期配置。
export NCCL_IB_DISABLE=1禁用 IB/RDMA,让 NCCL 退回 TCP socket。这个常用于判断 RDMA 路径是否有问题。
这些变量的使用原则是:先观察,再假设,再做对照实验。比如我怀疑 RDMA 有问题,就可以临时禁用 IB 看性能和错误形态是否变化;但如果只是盲目复制环境变量,很容易把性能调得更差。
9. NCCL 最常见的问题:hang#
NCCL 最典型的问题不是报一个清清楚楚的异常,而是卡住。
我排查 hang 时,会先看下面几类问题。
9.1 有 rank 没进入 collective#
例如:
if rank == 0:
dist.all_reduce(x)只有 rank 0 调用了 all_reduce,其他 rank 没调用,这就很容易 hang。正确的方式是所有 rank 都进入同一个 collective:
dist.all_reduce(x)9.2 不同 rank 的 tensor shape 不一致#
rank 0: tensor shape = [1024]
rank 1: tensor shape = [2048]这类问题有时不会优雅报错,而是表现成卡住、超时或结果异常。
9.3 dtype 不一致#
rank 0: float16
rank 1: bfloat16collective 要求参与通信的 tensor 类型一致。shape、dtype、count 这些条件都要对齐。
9.4 world size 或 rank 配置错误#
比如我以为是双机 16 卡:
world_size = 16但某台机器只启动了 7 个进程,最终只有 15 个 rank 加入。初始化阶段或者第一次 collective 就可能卡住。
9.5 网卡选错#
多机时如果 NCCL_SOCKET_IFNAME 或 RDMA 网卡选错,常见表现包括:
连接不上
初始化很慢
通信超时
性能极差这种问题很像“框架坏了”,但本质通常是网络路径、路由、网卡选择或防火墙配置不对。
10. NCCL 性能怎么看#
我通常会用 nccl-tests 看基础性能,比如 AllReduce:
./build/all_reduce_perf -b 8 -e 1G -f 2 -g 8这里最常见的两个指标是 algbw 和 busbw。
algbw 是 algorithm bandwidth:
algbw = 逻辑数据量 / 通信耗时它回答的是:从用户视角看,这个 collective 完成得有多快。
busbw 是 bus bandwidth:
busbw = 折算到底层总线上的带宽它会考虑 collective 算法中的实际数据搬运量,更接近底层链路利用情况。
我一般这样看:
小包看 latency
大包看 bandwidth比如:
8B - 1KB: latency 主导
1MB - 1GB: bandwidth 主导这和推理场景也能对应起来。decode 阶段有时通信 tensor 不大,小消息延迟很关键;prefill 阶段 tensor 更大,带宽更关键。
11. 从 NCCL 类比 CNCL#
我现在做 CNCL 相关场景时,NCCL 仍然很有参考价值。
严格来说:
NCCL 是 NVIDIA GPU / CUDA 生态的通信库
CNCL 是寒武纪硬件生态中类似角色的通信库但很多概念可以对应起来:
| NVIDIA GPU 体系 | Cambricon 体系 | 作用 |
|---|---|---|
| CUDA | CNRT / runtime | 设备运行时 |
| NCCL | CNCL | 多卡 collective 通信 |
| CUDA kernel | BANG kernel | 设备算子 |
| NVLink / PCIe / IB | 芯片互联 / PCIe / 网络 | 物理互联 |
backend="nccl" | 对应 backend / CNCL | 分布式后端 |
所以当我分析双机 16 卡通信时,可以借用 NCCL 的思路理解 CNCL:
Ray / vLLM 负责进程编排
PyTorch distributed / vLLM distributed executor 负责分布式调用
CNCL 负责加速卡间 collective 通信
GLOO 可能负责 CPU 侧 rendezvous / 控制面通信
机器之间真正的数据面通信依赖网卡、路由、RDMA/TCP 和 CNCL 后端实现这里有一个很重要的区分:GLOO_SOCKET_IFNAME=eth0 更偏控制面或 rendezvous,它不等价于“所有大 tensor 都通过 GLOO 传”。在这类硬件生态里,大 tensor 的 collective 通信更可能由 CNCL 处理。
12. 我最后怎么记 NCCL#
如果只留一句话,我会这样记:
NCCL/CNCL 不是训练框架,也不是调度框架。
它是多卡之间真正搬 tensor 的高速通信引擎。在 vLLM 多机多卡场景里,Ray 负责把 worker 拉起来,PyTorch distributed 或 vLLM executor 负责发起分布式调用,而 NCCL/CNCL 负责让 worker 之间真正交换张量。
真正做性能分析时,我会按这个顺序拆问题:
- 先看 collective 类型,是
AllReduce、AllGather、ReduceScatter还是AllToAll。 - 再看 rank 映射和拓扑,是机内、跨 socket,还是跨节点。
- 再看消息大小,是 latency 主导还是 bandwidth 主导。
- 再看环境变量和网络路径,确认网卡、RDMA、P2P 有没有走到预期路径。
- 最后看上层框架是否改变了通信次数、分片方式或同步点。
多卡系统的性能问题,经常不是某一层单独造成的。它可能从并行策略开始,在通信库里放大,最后被硬件拓扑定下上限。NCCL 的价值就在于,它让我能把这些层连起来看,而不是只盯着某一条日志或者某一个吞吐数字。
参考资料#
- NVIDIA NCCL User Guide, Overview: https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/overview.html
- NVIDIA NCCL User Guide, Collective Operations: https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html
- NVIDIA NCCL User Guide, Environment Variables: https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html
- PyTorch Documentation, Distributed Data Parallel: https://docs.pytorch.org/docs/stable/notes/ddp.html