简介:nccl-tests是NVIDIA官方维护的NCCL集合通信测试套件,专为CUDA开发者、高性能计算工程师和集群运维人员设计,用于验证NCCL库的安装正确性并量化多GPU、多节点间的通信性能。套件覆盖all_reduce、broadcast、all_gather、reduce_scatter、alltoall等主流集合通信操作,既可校验结果正确性,也可输出带宽与延迟数据,帮助定位通信瓶颈或对比不同拓扑、不同NCCL版本的效果。压缩包共15个文件,主体为7个cu测试源码和2个公共头文件,配合2个Makefile可灵活指定CUDA_HOME、NCCL_HOME构建,另含README与性能说明文档,包体仅27KB,适合快速部署。构建时支持MPI模式,通过MPI=1 MPI_HOME=...即可扩展至多进程、多线程和多CUDA设备测试,满足单机多卡到多节点集群的评估需求。当前已有4611人学习,适合具有一定CUDA基础、正在调试或优化NCCL通信性能的开发者。
1. NCCL 测试是分布式训练的体检报告:跑不跑得通先看它
跑大模型训练,最怕的不是显存不够,而是集群看着一切正常,实际集合通信带宽只有标称值的三分之一。NCCL 测试(nccl-tests)就是用来在开工前给通信链路做体检的工具:编译后得到一组 all_reduce_perf、all_gather_perf 这样的可执行文件,跑一遍就能拿到不同报文大小下的带宽和延迟。它能帮你确认驱动、拓扑、网卡配置是否真的对齐,适合集群管理员、训练平台开发者和做分布式性能优化的工程师。别把性能问题都归成玄学,先用 nccl-tests 排除通信底噪,再谈模型优化。
2. 编译装好 nccl-tests:最小构建流程与测试前的环境确认
2.1 事前确认:NCCL、CUDA 与驱动的版本匹配
nccl-tests 本身不是一个独立的通信库,它是挂在 NCCL 之上的一层测试程序。编译时它要找到 NCCL 的头文件和动态库,运行时则要能加载到版本匹配的库。很多人第一步就翻车在版本上:驱动太老、CUDA 太新、NCCL 库路径不对,编译一把过,运行时报错一堆。
我一般会先做三件事:查驱动支持的 CUDA 版本、查 nvcc 版本、查 NCCL 库是否存在。命令如下:
# 查看驱动声称支持的 CUDA 版本 nvidia-smi | grep "CUDA Version" # 查看编译工具链版本(注意 nvcc 与驱动允许的版本可以不同,但运行时一般取二者较低兼容级别) nvcc --version # 确认 NCCL 库位置,通常在 /opt/nccl 或系统默认目录 ls /opt/nccl/lib/libnccl.so* 2>/dev/null ls /usr/lib/x86_64-linux-gnu/libnccl.so* 2>/dev/null逻辑上要理清这一点:驱动决定 GPU 能不能用,nvcc 编译出的二进制决定依赖哪些 CUDA 运行时符号,NCCL 库决定集合通信算子的实现。三者不是同一个东西,但必须是兼容的一组。最省事的办法是全部由同一次安装的 CUDA 工具包提供,比如用包管理器统一安装的 CUDA 和 NCCL;如果 NCCL 是单独安装的,后续 make 时一定要指定NCCL_HOME。
提示:运行时找不到 libnccl.so,多半是
LD_LIBRARY_PATH没包含 NCCL 的 lib 目录,或者系统还没执行ldconfig。编译期能过不能说明运行期没问题。
2.2 从源码构建 nccl-tests 的标准步骤
nccl-tests 的构建比大多数 C/C++ 项目简单,核心就是一个 Makefile,不需要 cmake。常见做法是拿到源码后放到固定目录,然后通过NCCL_HOME和CUDA_HOME两个变量告诉构建系统去哪里找依赖。下面这段是我常用的构建流程:
mkdir -p /opt/nccl-tests && cd /opt/nccl-tests # 把下载到的源码包解压到当前目录,strip-components=1 去掉顶层目录 tar -xzf nccl-tests-src.tar.gz --strip-components=1 # 编译所有测试算子;NCCL_HOME 指向 NCCL 安装根目录,CUDA_HOME 指向 CUDA 安装根目录 make NCCL_HOME=/opt/nccl CUDA_HOME=/usr/local/cuda -j$(nproc)几个参数值得说明:NCCL_HOME必须指向包含include/nccl.h和lib/libnccl.so的目录,不能只写到/opt/nccl/lib;CUDA_HOME用于找cuda_runtime.h和libcudart,如果 CUDA 装在/usr/local/cuda,不写也能猜出来,但显式给出更稳。-j$(nproc)是并行编译参数,机器核多的时候能省时间。
如果你后面一定得用 MPI 方式跑多机测试,可以在 make 命令后面加MPI=1。首次编译我不建议加,因为一旦 MPI 实现没装好,编译报错会干扰你对核心逻辑的判断——先让单机版跑通,再扩到多机。
编译成功的标准是build/目录下出现一组可执行文件:all_reduce_perf、all_gather_perf、reduce_perf、broadcast_perf、reduce_scatter_perf、alltoall_perf等。日常用得最多的是all_reduce_perf,训练里梯度同步基本就是 all_reduce,所以后面所有示例都围绕它展开。
2.3 跑一条最小命令确认工具可用
编译完别急着上大报文,先用一条小测试确认工具本身能跑。常见做法是单机先做 all_reduce,数据量从 8 字节开始,翻倍到 128MB,用 8 张卡跑 5 次热身和 5 次正式迭代:
cd /opt/nccl-tests ./build/all_reduce_perf -b 8 -e 128M -f 2 -g 8 -w 5 -n 5说明一下我为什么这样定参数:-b 8是起始报文大小,这里故意设成最小的 8 字节,让程序先走一遍完整流程;-e 128M是结束报文大小,首次验证没必要测到 GB 级;-f 2表示从 8 字节开始每次乘 2,这样中间会覆盖 8B、16B……一直到 128MB。-g 8是参与通信的 GPU 数量,要求机器上至少能看到 8 张卡;-w 5是 warmup 迭代次数,用来让 CUDA context、NCCL 通信域都热起来;-n 5是正式测试迭代次数,次数太少结果抖动大,第一次跑通用 5 次就够了。
如果命令报错说找不到 CUDA 设备,先检查nvidia-smi是否能列出 GPU,再用CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7明确指定设备集合再跑一次。第一次测试的目标不是拿好看的数字,而是确认工具可执行、能看到两行结果表格(out-of-place 和 in-place)。
3. 单机多卡基准:all_reduce 带宽测试与关键参数拆解
3.1 b、e、f 三个参数决定流量模型
刚上手的人最容易把 nccl-tests 当成一个“点一下就跑”的黑盒子,其实它的参数就是测试流量模型本身。-b -e -f三个参数组合在一起,决定了这次测试模拟的是“小报文延迟敏感”还是“大报文带宽敏感”。
我给出一个常用的参数表,方便你做选择:
| 参数 | 作用 | 常用取值 | 说明 |
|---|---|---|---|
| -b | 起始报文大小 | 8、128K、128M | 起始值越小,覆盖的通信模式越全 |
| -e | 结束报文大小 | 128M、256M、512M | 大模型梯度同步常看 128M 以上 |
| -f | 每个 size 的翻倍系数 | 2 或 0 | 0 表示只测固定大小一次 |
| -g | 参与通信的 GPU 数 | 1~8 | 单机多卡时与物理卡数一致 |
| -w | warmup 迭代次数 | 5~10 | 太少会导致首次 CUDA 初始化计入时间 |
| -n | 正式迭代次数 | 5~20 | 需要稳定中位数时加大到 20 |
举例来说,大模型训练中梯度 all_reduce 的报文通常在几百 KB 到几十 MB,我一般用-b 128M -e 128M -f 0固定测 128MB 大包;如果要做延迟敏感的小报文扫描,就用-b 8 -e 1M -f 2。这里有个常见的误用:有人为了省时间把-b直接设成 128M,结果小报文场景下的通信延迟问题完全没暴露。
-w和-n的关系也要注意。warmup 部分不计入最终统计,它专门用来让 NCCL 完成通信域初始化、分配 buffer、建立连接。这个冷启动过程在第一次迭代里往往异常慢,如果-w设置太小,第一次迭代的时间会污染整体结果。
3.2 正确读 out-of-place / in-place 结果
跑完一条命令后,终端会输出每个报文大小对应的两行结果。这两行分别叫 out-of-place 和 in-place,很多人不看工具说明,只盯着第一行看,容易得出错误结论。
out-of-place 是指输入和输出 buffer 是两块独立内存,in-place 则是输入输出共用一块内存,后者省了一次数据拷贝。实际训练框架里梯度同步大多是就地更新,所以 in-place 那一行对应用更有参考价值;out-of-place 反映的是最坏情况或者说更通用的通信开销。
结果表里的三列需要分清:time是单次通信耗时,单位微秒;algbw是算法带宽,等于报文大小除以时间,代表“应用层视角的吞吐”;busbw是总线带宽,它把 all_reduce 在 ring 算法下每张卡要收发的倍数算进去了,计算公式大致是 algbw 乘以2*(n-1)/n,其中 n 是参与卡数。
我判断一个集群跑没跑到位,先看 in-place 的 busbw,再看 algbw。busbw 更接近硬件总线的真实利用率,algbw 更接近你在训练里能感知到的收益。如果 algbw 低而 busbw 高,说明算法本身在数据搬运上有额外开销,这是正常的;如果两者都低,才需要往硬件方向排查。
注意:8 卡 all_reduce 的 busbw 理论值大约是单卡点对点带宽的 1.7 倍左右,不是 8 倍。拿 8 倍去要求结果,属于对集合通信的理解偏差,会让任何人误判自己的集群。
3.3 用 nbus 数量判断拓扑利用程度
单机多卡的结果怎么判断好坏,不能光看绝对数字,要先看物理拓扑。最直接的工具是nvidia-smi topo -m,它会输出一张 GPU 之间的互连矩阵:
nvidia-smi topo -m矩阵里的关键标记含义大致如下:NV表示两张卡之间有 NVLink 直连,这是最快的路径;PIX表示经过同一个 PCIe switch;PXB表示经过 PCIe bridge;SYS表示要跨 CPU 走系统总线,这是单机里最慢的路径。
nccl-tests 的结果里没有直接输出拓扑,但它会根据每对 GPU 之间的路径自动选择算法。你在解读带宽时,先看矩阵里有多少NV连接,再对应检查结果。正常情况下,全 NVLink 互联的 8 卡在 128MB 报文下的 busbw 应该接近硬件标称水平,比如常见平台的几百 GB/s;如果矩阵显示不少SYS路径,那 busbw 落在 PCIe 带宽附近也是正常的,过度要求反而说明你没看拓扑。
我之前遇到过一次“8 卡带宽只有预期一半”的案例,查到最后发现是两张卡分别挂在两颗 CPU 的 PCIe root 上,跨 CPU 通信走了系统总线。拓扑决定上限,nccl-tests 只是把上限测出来给你看。
4. 多机多卡测试:网络栈验证与慢节点识别
4.1 hostfile 写法与进程布局
多机测试比单机多了一个维度:网络。第一步是写好 hostfile,它决定 MPI 进程在哪些节点上启动、每个节点允许启动几个进程。常见写法如下:
# hostA 上启动 8 个进程,hostB 上启动 8 个进程 hostA slots=8 hostB slots=8这里slots=8的含义是该节点允许同时运行的 MPI 进程数,一般与节点上的 GPU 数一致。随后用 mpirun 启动 16 个进程做 2 节点 all_reduce:
cd /opt/nccl-tests mpirun --hostfile hostfile -np 16 \ -x NCCL_DEBUG=INFO \ -x NCCL_SOCKET_IFNAME=eth0 \ ./build/all_reduce_perf -b 128M -e 128M -f 0 -g 1 -w 5 -n 20注意这里的-g 1与单机场景不同:mpirun 已经启动了 16 个进程,每个进程持有一张卡,所以每个进程内部只操控 1 个 GPU。如果你在多机命令里还写-g 8,程序会在每个进程里尝试找 8 张卡,立刻出错。
NCCL_SOCKET_IFNAME=eth0是告诉 NCCL 用哪块网卡做 Socket 通信。多机节点一般有多块网卡,管理网和高速计算网往往混在一起,不指定接口名时 NCCL 可能选到千兆管理口,带宽惨不忍睹。先用ip link查看节点上的接口名,再把它填进来。
NCCL_DEBUG=INFO在第一次多机测试时必须开。它会在输出里打印 NCCL 版本、检测到的网卡、建立连接的过程,以及最后选择了哪个算法。
4.2 先用 ring 再上 tree,区分算法开销
NCCL 在多机环境下会自动选择通信算法,逻辑大致是:小报文走延迟更友好的路径,大报文走带宽更优的路径。但自动选的不一定在你的网络上最优,所以多机排查时我习惯手动固定算法,把算法和硬件分开看。
先强制 ring 算法跑一遍,再用 tree 算法跑一遍:
mpirun --hostfile hostfile -np 16 \ -x NCCL_ALGO=RING \ -x NCCL_DEBUG=INFO \ ./build/all_reduce_perf -b 256M -e 256M -f 0 -g 1 -w 5 -n 20 mpirun --hostfile hostfile -np 16 \ -x NCCL_ALGO=TREE \ -x NCCL_DEBUG=INFO \ ./build/all_reduce_perf -b 256M -e 256M -f 0 -g 1 -w 5 -n 20对比两轮结果的 busbw,能快速区分问题到底出在算法选择还是物理链路。如果 ring 明显优于 tree,说明网络延迟对 tree 的层次通信不友好;如果两者都很差,问题大概率在网卡协商、驱动或交换机链路。
再补一个链路验证手段:设NCCL_IB_DISABLE=1强制走 Socket/TCP 路径,与默认路径对比。正常配置下,禁用高速网络后带宽应该明显下降;如果禁用前后几乎没差别,说明之前 NCCL 压根没用上高速网卡,要么是网卡驱动没识别,要么是接口名选错。
4.3 带宽偏低时如何快速定位是网卡还是拓扑
多机带宽偏低,常见的怀疑对象有三个:物理链路、NCCL 配置、主机拓扑。我有一套固定的排查顺序,先确认链路,再确认 NCCL 是否选中了正确的网卡,最后看 CPU 和 GPU 的亲和性。
第一步,用系统工具确认物理链路速率。ethtool eth0能看协商速率是不是预期值,比如预期 100Gbps 却协商成 40Gbps,问题直接就定位在物理链路或光模块上。这一步不做,后面所有 NCCL 参数调整都是瞎猜。
第二步,回看NCCL_DEBUG=INFO的日志。输出里会列出 NCCL 实际使用的网卡名称和地址族,确认它选的确实是高速计算网。这里是最容易踩坑的环节:机器上有eth0和eth1,NCCL 默认可能选了前者,而前者只是管理口。
第三步,看 CPU 亲和性。多机节点常见的 NUMA 架构下,GPU 挂在某颗 CPU 上,网卡也挂在某颗 CPU 上。如果 MPI 进程被调度到另一颗 CPU,跨 NUMA 访问网卡会显著增加延迟和拷贝开销。常见做法是在 mpirun 启动时绑定 CPU 亲和性,或者用numactl --cpunodebind固定进程到对应 NUMA 节点。很多“测出来莫名其妙少一半带宽”的案例,最后都是栽在亲和性上。
5. NCCL 测试避坑:5 条高频翻车记录与排查路径
5.1 多机测试中途 Communication timeout
现象:mpirun 启动后,前几个小报文测试正常,到大报文时进程直接 abort,日志里出现 communication timeout 相关字样,有时连结束码都不给一个。
原因:NCCL 建立连接和传输数据有默认超时。多机环境下只要有一个节点响应慢,比如 ssh 免密没配好、对方节点负载过高、共享文件系统卡住,超时就会被触发。很多情况下不是网络不通,而是慢。
解决:先确认两个节点之间 ssh 免密可用、同一个用户名和路径都存在;然后适当调大超时,常见做法是把超时时间从默认值调大到 300000 毫秒量级,给慢网络留足缓冲。同时把NCCL_DEBUG=INFO打开,看卡在哪一步;如果每次都在同一阶段超时,把该阶段涉及的网卡和链路单独测试。
5.2 同一台机器结果忽高忽低
现象:同一个命令连续跑三次,busbw 一会 90GB/s,一会 120GB/s,再跑又回到 95GB/s。单机多卡都有这种抖动,多机更明显。
原因:warmup 不充分、warmup 与正式迭代之间间隔太近导致 GPU 降频、CPU 被其他任务抢占、PCIe 链路因为供电或散热降速。许多人只跑 3 次取平均,正好把抖动当成真实结果。
解决:固定 CPU 亲和性,适当提高-w到 10 左右;正式迭代-n至少 20,然后取中位数而不是平均值。中位数对个别极端值不敏感,比平均更贴近“这台机器的真实水平”。如果你在共享集群上测,先确认没有其他任务占用 GPU 和内存带宽,否则数据没有可比性。
5.3 单卡带宽正常,多卡聚合不增反降
现象:点对点测速显示单卡链路没问题,但 8 卡 all_reduce 的总带宽只有单卡的 2 倍左右,远低于预期。
原因:这里多半是 busbw 的理解偏差。ring 算法下多卡 all_reduce 的 busbw 与卡数不是线性关系,8 卡时有效总线带宽大约是单卡点对点带宽的 1.7 倍左右;如果再扣除拓扑里SYS路径的代价,这个数字还会更低。另一种情况是 8 张卡共用一组 PCIe root,PCIe switch 出口成为瓶颈,这在双卡之外的所有并发场景都会发生。
解决:先用nvidia-smi topo -m看矩阵,分清哪些卡是NV直连、哪些跨 CPU;拿 matrix 里的拓扑结构对标 nccl-tests 的实测数字。如果是共用 PCIe switch 导致的瓶颈,能做的就是调整任务负载,让通信尽量发生在NV直连的卡之间,而不是期望换 NCCL 参数能突破物理上限。
5.4 NVLink 字样出现但带宽仍走满网卡
现象:单机测试中NCCL_DEBUG=INFO显示检测到 NVLink,但 8 卡 all_reduce 的 busbw 数值接近单机网卡带宽,明显不符合 NVLink 的表现。
原因:测试进程看到的 GPU 顺序可能被环境变量重排了。CUDA_VISIBLE_DEVICES如果被人为设置成不连续或不按物理顺序的编号,相邻 rank 对应的两张卡可能根本不是 NVLink 直连,而是分别挂在两颗 CPU 上,通信走系统总线甚至外网卡。
解决:测试前先清掉或显式设置CUDA_VISIBLE_DEVICES,让它按物理拓扑顺序排列;再用nvidia-smi topo -m核对 rank 顺序与拓扑矩阵是否一致。我习惯在跑测试前写一个小脚本,把 GPU 的 PCI bus id 和拓扑矩阵打印出来,作为当轮测试的附件存档。否则换一台机器重排后,结果变化都不知道原因。
5.5 进程全堆在第一个节点上
现象:mpirun 显示-np 16,但测试输出里 node count 是 1,或者第二个节点完全没有参与通信。
原因:hostfile 中第二个节点的 slots 写错、SSH 认证失败导致 mpirun 回退到单机模式,或者 MPI 实现的默认调度策略把进程都分配给了第一个节点。这个错误很隐蔽,因为程序不报错,只是结果变成单机带宽。
解决:先用一条轻量命令验证进程分布,比如mpirun --hostfile hostfile -np 16 hostname,看输出的 16 行主机名是否分布在两个节点。这一步通过后再跑真实测试。后续如果换过机器的 SSH key,优先重跑这个检查,它能快速暴露绝大多数多机启动问题。
6. 把 nccl-tests 变成日常巡检:基线脚本与结果对比技巧
跑通了不代表完事,真正有价值的是建立一份性能基线。我的习惯是每次硬件变更、驱动升级、网络调整后,固定跑同一套命令,把结果存成带时间戳的文件,之后拿当前结果和基线比对,偏差超过 10% 就自动告警。
下面是一个单机 8 卡最小基线脚本,固定测 256MB 报文,重复 5 轮取中位数:
#!/bin/bash cd /opt/nccl-tests LOG=/tmp/busbw_$(date +%Y%m%d_%H%M%S).log for round in 1 2 3 4 5; do ./build/all_reduce_perf -b 256M -e 256M -f 0 -g 8 -w 10 -n 20 \ | awk '/256M/ {print $5}' >> "$LOG" done # 中位数:sort 后取第 3 行,5 轮对应 NR==3 busbw=$(sort -n "$LOG" | awk 'NR==3 {print $1}') base=90 # 基线数值,单位 GB/s,按你的机器实测填写 echo "round5 median busbw=$busbw baseline=$base" # 低于基线的 90% 时脚本返回非 0,方便接入告警 awk -v b="$busbw" -v base="$base" \ 'BEGIN { if (b < base * 0.9) exit 1; else exit 0 }'脚本里的$5对应all_reduce_perf输出里 in-place 行的 busbw 列。我第一次写这个脚本时踩了个坑:没先看输出列顺序,直接取第一列,把自己的数据量和 algbw 当成了带宽。设置基线数值时,建议拿同一台机器连续 10 次结果的中位数乘以 0.9 作为告警线,不要太贴近峰值,否则一个小抖动就让整个巡检系统变成狼来了。
日志文件建议完整保存,而不仅是存中位数。每次测试保留原始输出,保留当时用的命令、CUDA_VISIBLE_DEVICES 和nvidia-smi topo -m结果。这样未来带宽衰减时,你能知道是换了驱动、改了拓扑顺序还是网卡降速导致的。
某次排障我印象很深:一个集群训练忽然变慢,看起来哪里都正常,翻了一个多月前的基线记录才发现 128MB 报文的 busbw 掉了三成。后来查到是光模块松动导致链路协商降速。如果没有基线,那种问题会被归类为“训练负载波动”而错过。我的习惯是:任何一次硬件变更后的第一件事就是重跑一遍基线脚本,把结果和上一次放到一起对比,确认没有负优化再上层业务。希望帮到你。
本文还有配套的精品资源,点击获取