简介:面向需要验证NCCL集合通信性能与正确性的CUDA开发者,这套nccl-tests源码包提供了all_reduce、broadcast、all_gather等典型操作的测试基准。可结合MPI实现多进程多节点扩展,支持多线程及每线程多设备,适合HPC与深度学习训练场景的性能调优。资源共15个文件,以.cu实现、.h头文件为主,另有Makefile构建脚本与README、PERFORMANCE文档,完整覆盖从编译到结果解读的流程。压缩包仅27KB,轻量易用。已有4602人学习。通过阅读源码与文档,可掌握NCCL测试的构建参数(CUDA_HOME、NCCL_HOME、MPI开关),理解各类集合操作的验证逻辑,并用于对比不同环境下的通信带宽与延迟表现,是NCCL开发与集群调优的实用参考。 做分布式训练最怕遇到那种“看起来在跑、速度就是上不去”的哑巴问题:模型代码没问题,GPU 利用率也不低,但一上多卡就感觉训练节奏不对。遇到这种情况,我第一反应不是去调超参,而是先拿 nccl-tests 给这台机器或者这批集群做一次通信体检。这套工具虽然名字不起眼,但在排查多卡通信瓶颈、验证集群网络配置、评估不同拓扑下的集合通信性能时,几乎是最直接、最不容易扯皮的手段。这篇文章就围绕 nccl-tests 的用法、输出解读和实战排查链路展开,给刚接触分布式训练,或者已经在集群上被通信问题折磨过几轮的工程师一些参考。
1. 为什么分布式训练的“哑巴问题”要靠 nccl-tests 来开口说话
1.1 通信开销在分布式训练里的真实占比
很多人对 NCCL 的性能没有直观概念,总觉得反正 GPU 算得快,通信慢一点也能接受。但实际上,在 8 卡甚至几十卡规模上做大模型训练时,每次梯度同步都要把几十 GB 甚至上百 GB 的张量在卡之间搬一遍。以 AllReduce 为例,假设单次同步数据量是 1 GB,8 卡 A100 的 NVLink 全互联大约能提供 400 GB/s 以上的总线带宽,那么理想情况下一次同步也要好几毫秒。如果通信链路配置不对,这个数字翻三四倍很正常。
更可怕的是,通信时间不是一次性开销。一个训练 step 里通常有多次 AllReduce、AllGather、ReduceScatter,这些时间累积起来,轻则让训练吞吐掉 20%,重则让 GPU 空转等数据,利用率直接崩塌。这时候如果只盯模型代码,很难发现问题;但单独把通信层拿出来测,问题马上现原形。
1.2 为什么不能用线上训练任务直接测
有人会问:我直接看训练日志里的吞吐不就行了?为什么要单独跑 nccl-tests?
原因很简单:线上任务里计算和通信是交叠的,GPU 在等通信的时候可能还在做前向或反向计算,最终吞吐表现包含了很多干扰因素。测出来的数字是“计算+通信”的混合结果,根本分不清到底是哪一环拖后腿。而且训练脚本里通常还有数据加载、梯度裁剪、参数更新等一堆逻辑,任何一个环节出问题都会污染结论。
nccl-tests 的价值就是做“控制变量”:把通信从整个训练流程里剥离出来,用最干净的方式反复压测某个集合通信原语,得到一份可复现、可对比的基准数据。集群验收、硬件故障排查、网络配置变更后的回归验证,都适合用这套工具做标准动作。
2. 从安装到跑通:nccl-tests 最快上手路径
2.1 编译坑位:别小看这个 make
nccl-tests 的编译本身不复杂,依赖项也就 CUDA、make、g++ 这些,但有几个坑很容易让人卡壳。
首先是 MPI 的问题。如果想测多机多卡,必须编译带 MPI 支持的版本。推荐在编译时显式指定 MPI 和 NCCL 的路径:
make MPI=1 CUDA_HOME=/usr/local/cuda NCCL_HOME=/usr/local/nccl -j$(nproc)我见过不少人漏掉NCCL_HOME,结果编译出来的测试程序链接到了系统自带的旧版 nccl 库,测出来的数据奇奇怪怪。编译完之后可以用ldd build/all_reduce_perf看一眼链接的libnccl.so来自哪里,这是排查诡异数据的第一步。
如果实在没有 MPI 环境,也能编译不带 MPI 的版本。这种情况下单机多卡可以用-g参数让单进程管理多个 GPU 来测,但多机测试就别想了,老老实实装一个 OpenMPI 或者 MPICH,这是跑通一切的前提。
另外一个小细节:编译时如果报找不到cuda_runtime.h之类的头文件,多半是CUDA_HOME没指对,或者 CUDA toolkit 本身没装全。别急着折腾编译器,先用nvcc --version确认 CUDA 环境正常。
2.2 一条最常用的测试命令
单机 8 卡,测 AllReduce 大消息带宽,我常用的命令是:
mpirun -np 8 ./build/all_reduce_perf -b 128M -e 8G -f 2 -g 1 -w 20 -n 100参数的含义用一张表列清楚:
| 参数 | 含义 | 我的建议 |
|---|---|---|
-b | 起始消息大小(字节) | 大带宽测试从 128M 或 256M 起 |
-e | 结束消息大小(字节) | 一般到 8G 或 16G 足够 |
-f | 消息大小的递增倍率 | 2 就是每次翻倍,覆盖性好 |
-g | 每个进程使用的 GPU 数 | 常规分布式训练是一进程一卡,设为 1 |
-w | 预热轮数 | 20 轮左右,别省 |
-n | 正式测试迭代次数 | 100 次以上数据才稳定 |
-t | 每进程线程数 | 默认就行,不建议乱调 |
如果是第一次跑,可以先减小消息范围快速验证环境通不通,比如-b 8 -e 512K -f 2,几十秒就出结果。确认没问题再上大规模压力测试。
跑的过程中建议开一个nvidia-smi盯着看,正常情况是所有 GPU 的利用率都在跳动,显存占用不高。如果发现某张卡始终没动静,说明拓扑识别或者进程绑定出问题了,这种问题后面会专门讲排查方法。
3. 别只盯着数字:algbw 和 busbw 这么读才有用
3.1 两个带宽指标到底在算什么
第一次跑完 nccl-tests,很多人盯着输出表格里的algbw和busbw发呆,不知道这两个到底有什么区别。简单说:
algbw(algorithm bandwidth)是算法层面的带宽,等于消息大小除以该原语实际花费的时间。它体现的是“你这个集合通信操作整体做下来有多快”。busbw(bus bandwidth)是把通信操作在所有 GPU 之间的数据搬运量折算到单条总线上的等效带宽,更能反映硬件链路的真实压力。
为什么需要busbw?因为不同集合通信原语的算法特性不同,直接比algbw会让不同卡数的结果失去公平性。以 AllReduce 的 Ring 算法为例,n 张卡做一次 AllReduce,每张卡实际要搬走和搬入的数据量大约是消息大小的 2(n-1)/n 倍,所以总线带宽大约是算法带宽乘以 2(n-1)/n。这也是为什么你在输出里能看到busbw比algbw高出一截。
打个比方:一条马路上的实际车流量,和每个路口完成一轮交通调度的效率是两个概念。algbw说的是路口调度效率,busbw说的是马路本身承压了多少流量。排查硬件瓶颈,看busbw更靠谱。
3.2 判断健康与否的体感阈值
不同硬件平台、不同原语,健康数值差异很大。我不打算给一套死数字,因为这和驱动版本、拓扑结构强相关,但几个体感阈值可以作为参考:
| 场景 | 参考值 |
|---|---|
| 8 卡 A100 80GB,AllReduce,消息 1GB 以上 | busbw 在 400 GB/s 以上基本健康 |
| 8 卡 H100,AllReduce,消息 1GB 以上 | busbw 在 600 GB/s 以上基本健康 |
| 4 卡 4090,AllReduce | 别指望 NVLink 级别的带宽,几十 GB/s 左右也正常 |
| 2 机 16 卡 A100,跨机 AllReduce | 除 NVLink 外,还要看 IB 链路带宽,比如 200Gbps IB 实测约 23 GB/s 理论上限 |
另外记住一个原则:小消息看延迟,大消息看带宽。消息大小在 8K、16K 这种量级时,真正重要的是完成时间,也就是输出的time列,而不是带宽。因为小消息的瓶颈在同步开销、内核启动延迟和网络 RTT,带宽再高也救不了延迟。
拿busbw做比较时要保证两边硬件环境和脚本参数一致,否则就是关公战秦琼。我自己习惯把所有测试结果按“日期+机型+驱动版本+卡数+消息大小”命名的 CSV 存起来,几次变更之后回头对比,数据会告诉你很多直觉看不出来的问题。
4. 从测试结果反向排查:一个典型的性能瓶颈定位过程
4.1 单机结果减半的排查链路
假设场景:8 卡 A100 单机,跑 AllReduce,128M 以上消息的busbw只有 200 GB/s 上下,怎么都提不上去。
第一步别急着怀疑卡坏了,先看拓扑识别是否正常,运行:
nvidia-smi topo -m正常 8 卡 A100 应该是 8 个 GPU 之间全部 NVLink 互联,输出里大多是NV#标记。如果看到大量PIX、PXB或者SYS,说明 GPU 没有正确走 NVLink,数据很可能绕道 PCIe 了,带宽砍半甚至更低就不奇怪。
第二步开 NCCL 调试日志,看看实际选择了什么算法和传输方式:
NCCL_DEBUG=INFO mpirun -np 8 ./build/all_reduce_perf -b 1G -e 1G -g 1 -n 5日志里会输出 NCCL 版本、拓扑信息、选用的算法(Ring/Tree/CollNet)和使用的传输方式(P2P、SHM、NET/IB)。单机环境下如果看到transport不是 P2P 或 SHM,而是 NET,说明 NCCL 可能把本机通信当成跨机通信来处理了,这就是典型的环境变量或拓扑识别问题。
第三步检查 P2P 是否被禁用。有些虚拟化环境或者某些 PCIe 配置下,NCCL 默认会禁用 P2P 直连,导致同一台机器内的卡通信也要经过主机内存中转。可以尝试:
NCCL_P2P_LEVEL=NVLink mpirun -np 8 ./build/all_reduce_perf -b 1G -e 1G -g 1 -n 50如果加了NCCL_P2P_LEVEL=NVLink之后性能立刻上去了,说明原来的 P2P 配置有问题。反之如果报错,就说明当前环境根本没法做 GPU 间直连,问题在硬件或驱动层。
还有个容易被忽略的干扰项:GPU 动态频率和温度。长时间跑负载后 GPU 降频,也会让测试数据变差。排障时最好顺手锁一下时钟:
nvidia-smi -lgc 1410测完再恢复:
nvidia-smi -rgc数据会稳定很多。
4.2 多机跨节点瓶颈的几点判断
多机场景更复杂,我习惯把问题分层拆开。先单机测一遍,再双机测一遍,如果单机数字正常,双机明显掉档,那么问题大概率出在跨机链路。
跨机通信常见瓶颈包括:
| 现象 | 可能原因 | 验证方法 |
|---|---|---|
| 双机性能只有单机一半还低 | GPUDirect RDMA(GDR)没开 | 查看 NCCL 日志里是否有 NET/IB 和 GDR 字样 |
| 网络吞吐上不去 | 链路协商速率不对 | ibstat或ethtool eth0看端口速率 |
| 时好时坏,波动明显 | 网络拥塞或丢包重传 | ethtool -S看rx_dropped、tx_dropped |
| 跨机带宽正常但延迟偏高 | 网络拓扑层次太深 | 用ib_write_bw或pingpong测点对点延迟 |
一个很实用的交叉验证:跨机点对点测试和集合通信测试结合。如果pt2pt的点对点带宽正常,但 AllReduce 不行,那问题多半在 NCCL 的算法选择或者多路径协作上;如果点对点本身就不行,那直接排查网络链路。
另外,多机场景下NCCL_DEBUG=INFO日志里会列出每个 rank 使用的网卡和 IP。我遇到过两次问题,都是因为两个节点上的网卡排序不一致,导致 NCCL 走了慢速网卡,改一下NCCL_SOCKET_IFNAME指定正确的网卡名就好了。
5. 进阶玩法:按场景组合原语与参数
5.1 不同通信原语对应训练中的哪些环节
nccl-tests 提供的不只是all_reduce_perf这一个程序,它覆盖了 NCCL 几乎全部常用集合通信原语。不同原语在真实训练里的意义不一样,做基准测试时不能只跑 AllReduce:
all_reduce_perf:传统 DDP 里的梯度全局归约同步,通信量的核心来源。reduce_scatter_perf:ZeRO/FSDP 风格的分片梯度归约,每张卡只留自己负责的那一份。all_gather_perf:ZeRO/FSDP 参数更新后的全量收集,和 reduce_scatter 往往是成对出现的。alltoall_perf:MoE 模型里 expert parallel 做 token 交换时的主要通信模式。pt2pt:点对点 send/recv,流水线并行各 stage 之间传输 activation 和梯度时会用到。
如果你的训练脚本用的是 FSDP,那单测 AllReduce 其实不够全面,应该把reduce_scatter_perf和all_gather_perf一起跑掉。很多 FSDP 训练慢的问题,恰恰就是 ReduceScatter 的跨机链路没调好。
5.2 参数组合的实际测试建议
nccl-tests 优点之一是参数可玩性很高,但玩过头也会误判。我的习惯是固定一套“标准套餐”作为日常健康检查,另搞一套“深度套餐”专门排查问题。
标准套餐只覆盖大消息带宽和一个小消息延迟:
# 大消息带宽 mpirun -np 8 ./build/all_reduce_perf -b 128M -e 8G -f 2 -g 1 -w 20 -n 100 # 小消息延迟 mpirun -np 8 ./build/all_reduce_perf -b 8 -e 512K -f 2 -g 1 -w 20 -n 500深度套餐则会加入不同协议和算法的交叉对比。NCCL 支持通过环境变量强制指定某个协议或算法,比如:
NCCL_PROTO=Simple NCCL_ALGO=Ring mpirun -np 8 ./build/all_reduce_perf -b 1G -e 1G -g 1 NCCL_PROTO=LL128 NCCL_ALGO=Tree mpirun -np 8 ./build/all_reduce_perf -b 1G -e 1G -g 1不同协议(Simple、LL、LL128)在小消息和大消息上的表现差异很大,而不同算法(Ring、Tree)在单机和多机、消息大小变化时各有胜负。搞清楚当前环境在什么组合下最优,等于给训练任务做了一次通信层面的预筛选。
另外,测试脚本里最好也记录-t线程数和-g进程 GPU 数,因为线程数会影响小消息的延迟表现,进程数和 GPU 数的比例决定了通信域的大小。这些都是影响结果解读的上下文,缺了后面很难复盘。
6. 顺带说说源码:想知道“数字怎么算出来”就往这两处看
6.1 nccl-tests 的结构和计时逻辑
如果你不满足于会跑命令,想搞清楚 nccl-tests 到底怎么得到那些数字,直接翻源码。项目结构很清晰,src目录下每个集合通信原语对应一个perf文件,比如all_reduce_perf.cxx、all_gather_perf.cxx等。这些文件的核心逻辑非常相似:初始化 NCCL communicator,然后循环执行“计时 + 调用对应的集合通信原语 + 校验结果”的流程。
计时部分有两种方式:一种是 CUDA event 计时,适合大消息;另一种是 CPU 端高精度时钟计时。最终报告里显示的time是多次迭代的平均值和最大值。源码里busbw的计算公式直接写在报告函数里,搜索busbw就能看到不同原语的折算系数是怎么算出来的。我第一次找的时候还特意核对了 AllReduce 的系数,确认就是前面提到的2*(n-1)/n。
6.2 任务提交与 NCCL 库的边界
有一些人逛 nccl-tests 源码是为了给自己的测试工具加功能,这时候你会注意到 nccl-tests 本身做的事其实很“薄”:它负责准备输入数据、计时、校验输出,但真正的通信执行全部在 NCCL 库内部完成。NCCL 库会把一次集合通信操作拆解成 task 并追加(append)到对应的 channel 上,由 GPU 端 kernel 依次消费执行。如果你对这块感兴趣,直接去 NCCL 源码里搜task append、ncclGroupStart这类机制,能看到它和 nccl-tests 之间的分工边界。
说实话,对大多数用户而言,不需要深入读通 NCCL 全部源码,但弄清楚这个边界有助于跳出“换参数试一下”的玄学式排障。比如当你怀疑问题是出在驱动层还是通信库层时,可以用 nccl-tests 在不同 NCCL 版本下分别跑一遍,数字摆出来,问题归属基本就清楚了。我在排查过一次“换驱动后跨机带宽异常”的问题后,现在每次升级驱动或 NCCL,都会顺手跑一遍基准数据归档,等出了问题再回头翻记录,比临时抱佛脚强太多。
最后分享一个小经验:nccl-tests 跑出的原始数据只是第一层,真正有价值的是把数据放到时间轴上做对比。同一套硬件,今天测的数值和三个月后测的数值放在一起,很多时候能提前暴露链路老化、驱动版本回退、网卡固件异常这类不容易察觉的问题。把它当作集群基础设施的“体检报告”来维护,比单纯当成一次性测试工具要有用得多。
本文还有配套的精品资源,点击获取