乐于分享
好东西不私藏

NCCL 源码解析:从硬件拓扑到集合通信选型的完整链路

NCCL 源码解析:从硬件拓扑到集合通信选型的完整链路

NCCL 源码解析:从硬件拓扑到集合通信选型的完整链路

一、引言:NCCL 为什么是 GPU 通信的标杆?

要回答"NCCL 凭什么成为 NVIDIA GPU 集合通信的事实标准",答案藏在一个关键设计里:NCCL 先把整台机器看成一张图,再在这张图上决定如何通信。

拓扑系统正是这一思想的完整落地。它的核心任务是回答四个层层递进的问题:

  1. 1. 硬件长什么样?(拓扑发现)—— 从 sysfs 与 NVML 自动探测 PCIe/NVLink 拓扑,或通过 NCCL_TOPO_FILE 用 XML 手工覆盖,最终构建出 ncclTopoSystem 硬件图。
  2. 2. 数据能怎么走?(路径计算)——数据从 GPU A 到 GPU B 能走哪几条路?BFS 如何预计算路径带宽与类型?
  3. 3. 该选哪种算法?(性能调优)——面对同一份拓扑,为什么有时选 Ring、有时选 Tree 或 NVLS?调优模型如何按消息大小与硬件特性决定算法、协议与通道数?
  4. 4. 如何落地?(连接建立)——抽象的搜索结果如何变成每个 rank 真实的收发对端与传输连接?

四个环节环环相扣,前一个的输出正是后一个的输入。读完全文,即可自然衔接第 4 章的通信管线。


二、系统概览:一条三段流水线

拓扑系统在通信器初始化期间分三个阶段运行,形成一条清晰的流水线:

硬件发现(发现) → 路径计算(算路) → 图搜索(寻路)
  • • 拓扑发现ncclTopoGetSystem() 读 sysfs / NVML / XML,构建出类型化的 ncclTopoSystem 图。
  • • 路径计算ncclTopoComputePaths() 对每个节点预计算到各类目标的最优路径(带宽、跳数、类型),填进路由表。
  • • 图搜索ncclTopoSearch() 递归搜索最优通信图(环 / 树 / NVLS),同时"扣除带宽"保证多流共存。

这三段流水线的设计哲学是职责单一:三段只读写同一份图数据、不碰硬件,便于分段调优与排查。


二、核心数据结构:一张"硬件地图"的骨架

拓扑系统的数据地基由三层结构构成:系统(system)→ 节点(node)→ 链接/路径(link/path)

1. ncclTopoSystem —— 整机硬件图

ncclTopoSystem 是整台机器的"硬件地图",把硬件组件按类型分抽屉存放:

struct ncclTopoSystem {
    int
 systemId;                          // 主机唯一 ID
    uint64_t
 hostHashes[NCCL_TOPO_MAX_NODES]; // 各主机哈希
    int
 nHosts;                            // 主机数量
struct ncclTopoNodeSet nodes[NCCL_TOPO_NODE_TYPES];
 // 按类型分组的节点
    float
 maxBw;                           // 每通道最大带宽
    float
 totalBw;                         // 总可用带宽
    int
 inter;                             // 跨节点(1) 或 机内(0)
};

它维护十种节点类型的独立列表:GPU(0)、PCI(1)、NVS(2)、CPU(3)、NIC(4)、NET(5)、GIN(6)、RMA(7)、DEV(8)、CXB(9)。

maxBw / totalBw 提前汇总出两条全局带宽数字,让后续算法不必重数硬件就能拿数值做决策;inter 标志直接点明是机内还是跨机通信,第一时间影响算法选型。

2. ncclTopoNode —— 图上的站点

每个硬件组件由 ncclTopoNode 表示,它既是图顶点,又携带邻接表与预计算路由表:

struct ncclTopoNode {
    int
 type;                    // 节点类型
    int64_t
 id;                  // 唯一 ID: (systemId << 56 | localId)
union {
                      // 按类型携带私有字段
struct {
 int dev; int rank; ... } gpu;  // GPU: rank、计算能力
struct {
 uint64_t pciId; float bw; int gdrSupport; ... } net; // NIC
struct {
 int arch; int vendor; ... } cpu;  // CPU 架构
struct {
 uint64_t device; } pci;
    };
    int
 nlinks;
struct ncclTopoLink links[NCCL_TOPO_MAX_LINKS];
 // 邻接表(最多 576 条)
struct ncclTopoLinkList* paths[NCCL_TOPO_NODE_TYPES];
 // 预计算路由表
    uint64_t
 used;               // 搜索状态位图
};
  • • links 是与它直连的邻居,最多 576 条,足以容纳 GB200-NVL72 这类高密度互联。
  • • paths 是按目标类型预计算的路由缓存,把"现场问路"变成"查表",图搜索时直接复用、大幅提速。

3. 链接(Link)与路径(Path)—— 一对关键抽象

这是读懂拓扑系统的一把钥匙:

类型
符号
典型带宽
含义
本地
LINK_LOC / PATH_LOC
5000.0
同一设备
NVLink
LINK_NVL / PATH_NVL
12–40.1
直接 NVLink
NVLink(桥接)
PATH_NVB
多跳 NVLink
芯片间
LINK_C2C / PATH_C2C
Grace-Hopper C2C
PCIe
LINK_PCI / PATH_PIX
12.0
Gen3 x16
PCIe(交换机)
PATH_PXB
多个 PCIe 桥
CPU 互联
LINK_SYS / PATH_PHB/SYS
QPI/UPI
网络
LINK_NET / PATH_NET
可变
基于 NIC

链接是物理事实,路径是逻辑结论。 一条多跳路径的带宽与类型由组成它的链接推导出来,遵守两个"最":

  • • 带宽取 min(瓶颈处最小带宽决定整条路径带宽)
  • • 路径类型取 max(最差的一条链路决定整条路径质量等级)

NCCL 特意区分二者,是为了如实计入代价:比如穿过 PCIe 桥或 NUMA 互联会带来 TLP 开销与带宽折价。把路径固化成 PATH_LOC → PATH_NET 这套"数值越小越优"的有序枚举后,后续 BFS 与图搜索只需做数值比较即可排序路径优劣,把昂贵的硬件语义压缩成了便宜的枚举比较。


三、拓扑发现:从"裸硬件"到"能决策的图"

拓扑发现回答的是**"地图从哪来"**。ncclTopoGetSystem() 要么读 XML 现成图纸,要么现场实测重建。

发现流程

先查 NCCL_TOPO_FILE 覆盖?
  ├─ 有 → 读 XML
  └─ 无 → 自动检测(sysfs + NVML)
  • • XML 覆盖优先:私有集群、模拟器或想修正驱动探测偏差时,用户给 XML 就以其为准。
  • • 自动检测回退:读 sysfs 设备树 + NVML 探测。
  • • 两条来源共用同一套建图入口,改的是数据源头、不变的是图模型,上层逻辑完全无感。

自动检测的三路取证

当未提供 XML 文件时,系统按以下方式自动检测拓扑:

  1. 1. CPU 架构:检测厂商(Intel/AMD/ARM/Zhaoxin)与型号,设置基础 CPU 间带宽。
  2. 2. PCI 层级:遍历 PCIe 树,包含对 BCM Gen4 交换机的专门处理。
  3. 3. GPU 邻近性:识别 GPU 相对于 PCIe 交换机与 NUMA 节点的位置。

历史回眸:2.0 重写时立下的"自动认路"起点。更早的 NCCL 更像手工编排,连接方案靠配置与经验;自 2.0 起,拓扑发现 + 路径搜索成了"自动选路"的核心,靠 sysfs/NVML 就能把整台机器读成一张图。如今 NCCL_TOPO_FILE 的 XML 覆盖,仍是这套自动发现上留的一扇"人工后门"。

发现管线的解耦设计

发现管线把"读硬件"与"转图"拆成两个模块:

  • • xml.cc:负责跟操作系统与驱动打交道——读 sysfs、解析 XML、问 NVML。
  • • topo.cc:只认 XML 这一种输入,把它翻译成类型化图结构。

两者用 XML 中间表示当契约。XML 元素按物理层级组织——NUMA CPU 为根、PCI 子树为枝、GPU/NIC 为叶,为 sysfs 采集与 NVML 查询的结果提供统一容器。这种解耦让"换采集来源"与"改图模型"只需动一边。

关键发现手段

  • • sysfs PCI 拓扑发现:遍历 /sys/devices/system/node/ 下的每个 NUMA 节点生成 <cpu> 元素;读每个 PCI 设备的 classvendordevice、链路宽度与速率,作为 XML 属性存储。CPU 架构由编译期宏(__x86_64____aarch64__ 等)判定。
  • • NVML GPU 与 NVLink 发现:sysfs 只见 PCIe,NVLink 这类私有总线还得靠 NVML 问 GPU 驱动。NVML 报出活动 NVLink lane、C2C 互连、MNNVL 的 fabric clique 结构数据,补上 GPU 之间的高速私网。
  • • PCI 类别码到节点类型的映射:PCI 类别码是设备的"身份证前缀"——0x060400→PCI 桥/交换机、0x068000→NVLink 交换机、0x03xxxx→GPU、0x02xxxx→NIC。

四、路径计算与图搜索:从"静态地图"到"可用路线"

路径计算与图搜索是把"静态地图"变成"可用路线"的两步:先 BFS 预计算最优路径并缓存,再做带带宽预留的递归图搜索。

第一步:BFS 路径发现

ncclTopoComputePaths() 对每个 GPU/NIC/CPU 节点执行广度优先搜索,预计算所有节点对之间的最短路径,填充 node->paths[type] 路由表。

BFS 算法关键特性:

特性
实现
说明
队列管理
两个交替的 ncclTopoNodeList
潮水式扩散
带宽计算
std::min(path->bw, link->bw)
瓶颈处最小带宽
路径类型
std::max(path->type, newType)
最差链路获胜
NVB 路由
通过 DEV 节点限制中间跳
防止桥接路径无限兜圈

BFS 以 GPU 或 NET 为起点向外扩散,每跨一跳用 min(累计带宽, 链路带宽) 收紧带宽,一旦发现路径类型更优或跳数更少就更新路由。

路径计算的入口点还要处理"走不通"的四种拐弯:

情形
动作
GPU P2P 禁用
改经 CPU 路由
传输不可用
标记为 PATH_NET
GDR 不可用
改经 CPU 路由
PXN 机会
添加中间 GPU(通过 NVLink 代理获得更优带宽)

它们合并成一条"先试最优、不行就降级"的兜底逻辑,保证总能给出可用路径。

第二步:递归图搜索 + 带宽预留

图搜索通过递归探索 GPU 与 NIC 的排列来寻找最优通信模式,由三个函数组成:

函数
用途
ncclTopoSearchRec
入口点,分派到 GPU 或 NET 搜索
ncclTopoSearchRecNet
为节点间尝试不同的起始 NIC
ncclTopoSearchRecGpu
为一个通道按顺序递归尝试 GPU

带宽预留是核心约束。 搜索使用 ncclTopoFollowPath() 遍历图,边走边从链路"扣除"带宽,若路径无效或拥塞则"回退"(rewind)。

命题 3.1(单对最优 ≠ 全局可行):BFS 阶段求出的每对节点最优路径是"孤立最优"——一旦多条通信流共享同一条链路,各自的最优就会互相挤占。因此图搜索必须在走每一步时预留(扣除)链路带宽:某条链路被扣成负就说明该猜想的组合过度订阅了硬件,必须 rewind 回退换路。这一"先求单对最优、再做全局共存"的两段式设计,正是 NCCL 多通道并行通信能跑满有效带宽而不错配流量的根本保证。

代价模型里那些看似随意的常数也全是真实的硬件哲学:

  • • 浮点精度SUB_ROUND 宏处理
  • • PCI 开销:Intel CPU 的 P2P 有 6/5 开销
  • • GPU 读取开销:前 Ampere GPU 为 1/8 反向带宽
  • • POWER NVLink:POWER CPU 上的完全反向带宽

关键细节

  • • time 预算:搜索维护 time 预算防止无限循环——排列空间随节点数指数膨胀,超时就采纳当前最佳结果收工。宁可给出次优解,也不阻塞启动。
  • • GPU 评分排序cmpScore 依次优先 interBw(最高)、interPciBw、interNhops(最低)、intraBw、intraNhops。把"大概率更优"的 GPU 放前面,预算耗尽时资源优先留给最优组合。
  • • 路径校验(P2P 与 GDR 检查)
    • • P2P 校验 ncclTopoCheckP2p:检查主机哈希、MNNVL 支持、路径距离与 NCCL_P2P_LEVEL,再叠加 NVML P2P 状态。
    • • GDR 校验 ncclTopoCheckGdr:检查 NIC/GPU 的 GDR 支持、读模式兼容性与拓扑距离。
  • • 图比较与选择ncclTopoCompareGraphs:优先总带宽(nChannels * bwIntra),然后最小化跳数。

五、性能调优与算法选择:时间公式驱动的决策

找到最优图之后,NCCL 必须选择最佳执行策略。调优模型(ncclTopoTuneModel)选择三样东西:

  1. 1. 算法:Ring、Tree、CollNet 或 NVLS。
  2. 2. 协议:Simple、LL(低延迟)、LL128。
  3. 3. 通道数:独立通信通道的数量。

核心决策公式

time = lat * latCount + nBytes / (1000 * bw)

命题 3.2(路径选择即性能决策):选哪种路径(NVLink/PCIe/网络)在拓扑发现期就已锁定性能上限——从 PATH_LOC 到 PATH_NET 的等级决定了可行的算法与带宽天花板,调优模型不会突破它,只会在其之上按消息大小报价。因此一次初始化期的选型,决定了该通信器此后每一次集合调用的性能上限。

两阶段架构

  1. 1. 图搜索阶段:为每种算法计算带带宽与路径信息的拓扑图(回答"理论上能开多快")。
  2. 2. 调优模型阶段:基于硬件特性,为每个集合操作/算法/协议组合计算有效带宽与延迟(回答"实际该选谁")。

调优常量架构

NCCL 为不同 GPU 架构(Volta、Ampere、Hopper、Blackwell)维护硬件特定的调优常量 ncclTunerConstants_t。示例(Hopper):

常量
描述
baseLatencies[RING][SIMPLE]
基础 Ring/Simple 延迟
8.4 us
hwLatencies[NVLINK][RING][SIMPLE]
NVLink 环形延迟
3.4 us
llMaxBws[HOPPER][0]
单节点 LL 最大带宽
141.0 GB/s
perChMaxTreeBws[HOPPER][1]
双节点 Tree 每通道带宽
41.4 GB/s

带宽计算:按协议"打折"

每个协议都有固定的"有效载荷比",调优模型据此算出真实可用带宽:

算法
LL 协议
LL128 协议
Simple 协议
Ring
因 2 倍数据传输限制 50% 效率
约 92%(120/128 有效载荷比)
完整总线带宽
Tree
因串行化约 1/3.8 效率
7/9(单节点)或 120/128(多节点)
完整总线带宽
NVLS
效率因子 Hopper=0.85、Blackwell=0.74
CollNet
factor = ppn / nChannels

每个系数背后都有一条物理原因:LL 的 2 倍数据放大、LL128 的 120/128 载荷、NVLS 的通道数与 AllReduce 双向搬运等。这些折扣让"看起来带宽很高"的算法在模型里现出原形,避免选错。

延迟计算:按算法形态累加

延迟的核心不是"查表",而是按算法形态把一个逻辑公式直接累加进 comm->latencies[coll][a][p]

  • • Ring(nsteps - nInterSteps) * intraLat + nInterSteps * interLat——节点内步数按 intraLat 计价、跨节点步数按更贵的 interLat 计价。AllReduce 的跨节点步数 nInterSteps = 2*(nNodes-1)(环形片段首尾各过一遍网,双向)。
  • • Tree2 * ((nRanks/nNodes - 1) * intraLat + log2(nNodes) * interLat)——节点内线性叠加,节点间按树高对数缩放;×2 源于双树结构。
  • • NVLS:直接置为intraLat(不是累加)——单节点 NVLS 一次 NVLink 读写即完成,几乎无拓扑步数;多节点才补 interLat。= 与 += 的区别本身就泄露了不同算法的步数形态。
  • • CollNet Direct2 * (min(1, nRanks/nNodes - 1) * intraLat + (nRanks/nNodes - 1) * 0.4) + interLat——0.4us 为仲裁串行化开销。

计算实例:各情形下 time 的修正

运行期按消息大小、拓扑与流水线深度做几个条件修正:

情形
影响项
计算
组合不可用(bw==0)
直接 time = -1 弃权
Tree + 中等消息
bw
bw *= treeCorrectionFactor[protocol][logSize]
NVLS_TREE + 中等消息
bw
复用同一张 treeCorrectionFactor
Ring + Simple + 多节点大消息
lat
lat *= 1.9
(前 Ampere)或 1.4(Ampere+)补偿"平台效应"
latCount
Ring 用 numPipeOps;其余算法按批数 DIVUP(numPipeOps, 批量)

最后代入 time = lat * latCount + nBytes / (1000 * bw):前一项是"启动成本",后一项是"传输成本",选最小者。

算法启用/禁用与用户覆盖

  • • 禁用逻辑:NVLS_TREE 单节点禁用;CollNet 开关关闭时禁用;LL128 要求同质计算能力与特定路径类型(Hopper+ 通常为节点内 PATH_NVB 与节点间 PATH_PXN)。
  • • 环境变量覆盖NCCL_ALGO(算法)、NCCL_PROTO(协议)、NCCL_NTHREADS(线程数)、NCCL_LL128_NTHREADS(LL128 线程数)。
  • • Tuner 插件 API:通过 ncclTuner_t 接口支持外部调优插件,NCCL_TUNER_PLUGIN 加载,插件实现 init/getCollInfo/finalize 三个回调,可改写成本表或覆盖通道数——"换脑不换骨架"。

六、连接建立与通道配置:把决策落实为真实连接

最后阶段把抽象的搜索结果映射为具体的 rank 到 rank 连接,分两个主要阶段:

本地"打草稿"(preset) → 全局协调"定稿"(postset)

1. 拓扑预设阶段 ncclTopoPreset

各台机器自己先摆好桌牌,只动本地通道字段,不碰网络、不等别人。因为 Ring/Tree/CollNet/NVLS 的关系都可从拓扑图直接算出。

对每个通道,按算法填好"邻座名单":

通道字段
算法
描述
ring.prev
 / ring.next
Ring
环形中的前/后一个 rank
tree.up
 / tree.down[]
Tree
树中的父/子 rank
collnetChain.up
 / .down[]
CollNet Chain
链中的上/下游 rank
collnetDirect.*
CollNet Direct
Head rank 与对端连接
nvls.*
NVLS
NVLS 特定拓扑(虚拟端点)

此外,NVLS 需要从图中提取唯一的"head" rank 存入 nvlsHeads——它是协调多播组的唯一指挥,跨所有节点管理一个或多个 NVPT 通道。

2. 拓扑后置阶段 ncclTopoPostset

"集体对表":用 allTopoRanks 数组从所有 rank 收集拓扑信息,再连接节点间拓扑。

算法
连接函数
说明
RING
ncclTransportRingConnect()
为环形拓扑建立 P2P/NET 连接
TREE
ncclTransportTreeConnect()
建立树形拓扑连接
NVLS
ncclNvlsBufferSetup()
分配并注册 NVPT 多播缓冲区
NVLS_TREE
ncclNvlsTreeConnect()
结合节点内 NVPT 与节点间树形
COLLNET_CHAIN
ncclCollNetChainBufferSetup()
设置 CollNet 链缓冲区
COLLNET_DIRECT
ncclCollNetDirectBufferSetup()
设置 CollNet Direct 缓冲区
  • • connectRings:通过跨节点首尾相接,把各节点本地环焊成一条全局大环。
  • • connectTrees:用双树(tree0/tree1)织两张可能不同的树,既冗余又能吃满更多带宽。
  • • connectCollNet:星型(hub-and-spoke),head 连接所有本地对端及其他 head,shift 值错开负载避免热点。
  • • connectNvls:机内 NVSwitch 多播 + 机间树形叠加,端点用虚拟 rank ID(nRanks+1+h)单独编址。

通道数的多阶段确定

通道数由硬件能力、用户配置、算法需求三股力量共同拍板:

环境变量
默认值
描述
NCCL_MIN_NCHANNELS
0
最小通道数(强制复制)
NCCL_MAX_NCHANNELS
MAXCHANNELS
最大通道数(限制数量)
NCCL_UNPACK_DOUBLE_NCHANNELS
1
为 unpack 网络启用翻倍

通道复制场景(copyChannels 复制的是"拓扑模式"而非连接本身):

场景
触发条件
复制倍数
双树
始终
2x
CollNet 饱和
bwIntra > bwInter && nRanks > nNodes
1.5x
Hopper/Blackwell <8 PPN
minCompCap >= 90 && nNodes > 1 && bwIntra > 45
2x
Unpack 网络
netDeviceType == UNPACK && nNodes > 1
2x
用户最小值
ncclMinNchannels() > current
可变

子通信器(split/shrink 产物)被"锁"在父通信器的通道数之内,防止资源超分。


七、总结

拓扑系统完整回答了"NCCL 如何把一台机器变成可优化通信的图模型"这一根本问题。其精髓可以归结为一条主线:

硬件发现(3.8) → 路径计算与图搜索(3.9) → 性能调优与算法选择(3.10) → 连接建立与通道配置(3.11)
  1. 1. 数据地基(3.3):用 ncclTopoSystem / ncclTopoNode / Link+Path 三层结构,把硬件抽象成带类型的图,并把"物理直连"与"逻辑路由"分开建模。
  2. 2. 发现与建图(3.8):xml.cc 读 sysfs/NVML/用户 XML,topo.cc 把 XML 转成图——采集与建模解耦,XML 是统一的中间契约。
  3. 3. 路径与搜索(3.9):BFS 预计算最优路径缓存成路由表,递归图搜索通过"扣除带宽 + 回退重试"保证多通信流全局共存,而非孤立的单对最优。
  4. 4. 调优决策(3.10):核心公式 time = lat * latCount + nBytes / bw 一行驱动全部选择——算法结构决定延迟系数(Ring 线性、Tree 对数、NVLS 常数),协议决定带宽折扣(LL 半速、LL128 120/128、Simple 吃满)。
  5. 5. 连接落地(3.11):ncclTopoPreset 本地先摆好"邻座名单",ncclTopoPostset 全局协调焊接跨节点连接,最后按硬件/用户/算法三约束确定通道数。

一句话总结:拓扑系统是 NCCL 的"硬件根基"——它先把机器看成一幅图,再在这幅图上决定怎么通信。一次初始化期的选型,决定了该通信器此后每一次集合调用的性能上限,这正是为什么最贵重的经验测量值得固化在一次性初始化里、而运行期仅做一次 ncclTopoGetAlgoTime 比较的原因。