ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RDMA与NCCL架构解析:从AllReduce卡顿到跨机通信优化实践

RDMA与NCCL架构解析:从AllReduce卡顿到跨机通信优化实践

上周帮某实验室搭一套四节点GPU训练环境,单机内用NVLink通信一切正常,一旦跑到跨机的AllReduce就明显卡顿。当时我第一反应是网卡带宽不够,后来把整个链路重新梳理一遍才发现:问题不在于网络有多快,而在于数据从GPU显存走到网卡的路径上到底经过了多少次搬运。这也是我今天想聊的主题——RDMA在NCCL中的整体架构,以及在实际环境里如何验证、如何优化。

这篇文章适合正在做分布式训练、大模型多机推理,或者被跨机通信效率折磨过的朋友。我会从NCCL为什么要依赖RDMA讲起,把NCCL内部的分层结构、通道与队列对的对应关系、一条消息从显存到远端显存的完整旅程都拆开看,最后配合NCCL_DEBUG日志和nccl-tests工具,给出一套可以直接照搬的验证和调优方法。

1. 为什么跨机通信会成为训练瓶颈:先弄清楚问题出在哪

1.1 集合通信到底有多频繁:AllReduce的同步压力

分布式训练里的通信不是偶尔发生一次,而是每训练一步、每处理一个batch都要来一轮。以最常见的数据并行为例,每个GPU各自算完前向和反向之后,必须把梯度在所有GPU之间做一次AllReduce,拿到全局平均梯度后才能更新参数。换句话说,通信频率和训练步数是同一个量级,模型越大、batch越大,梯度张量越大,一次AllReduce需要搬运的数据量就越吓人。

我见过一个比较典型的场景:某模拟项目在一个四机环境上跑百亿参数模型,单机八卡。每个rank的梯度张量动辄几百MB到几个GB,一次AllReduce要在一个step内完成。单机内GPU之间走NVLink或PCIe,带宽能到600GB/s甚至更高,但跨机只剩两条网卡,每条100GbE带宽满打满算也只有12.5GB/s。这两个数量级差太远了,通信一旦落到网络侧,整个训练就被拖到网卡的极限上。

还有一个大家常忽略的点:集合通信的延迟是“木桶效应”的。AllReduce要求所有rank都把数据准备好,任何一个节点的网络慢一点,整个同步都要等它。所以跨机环境下,网络不只是带宽问题,延迟的稳定性同样决定训练能否线性扩展。这也是为什么TCP/IP在这种场景里几乎撑不住——它的协议开销太大,延迟抖动也太明显。

1.2 传统TCP/IP协议栈为什么扛不住

拿普通TCP/IP做GPU跨机通信,数据要走的路径大概是这样的:GPU显存里的数据先通过PCIe或者NVLink拷到主机内存,再拷贝进内核的socket缓冲区,内核协议栈帮你拆包、加TCP头、走拥塞控制、处理ACK中断,到对端之后还得一层层剥开,最后从内核缓冲区再拷回GPU显存。

这中间至少多了三到四次内存拷贝,每次拷贝都要占用CPU负载,而且整条链路没有一跳是“直通”的。更麻烦的是,TCP的中断处理在高频大流量下会让CPU飙到很高,训练进程本身还需要CPU去发布kernel、处理框架逻辑,两边抢资源,性能立刻崩。

所以RDMA进场的理由很简单:把“搬运”这件事从CPU和内核手里解放出来,让网卡自己完成数据从GPU显存到对端GPU显存的所有动作。

2. RDMA为什么快一个数量级:零拷贝、内核绕过与CPU卸载

2.1 三条硬核机制逐条拆

RDMA的核心优势可以归结成三句话:零拷贝、内核绕过、CPU卸载。这三件事单独拆开来看都不神秘,但合在一起就能把数据传输延迟从几十微秒压到几微秒。

零拷贝的意思最直接:数据不需要在内核缓冲区和用户缓冲区之间来回倒腾,发送方直接让网卡通过DMA读取GPU显存里的数据,接收方也直接让网卡把数据DMA写入对端GPU显存。整个链路中,数据从源显存到目的显存只被搬运一次,中间不存在中间缓冲。

内核绕过指的是用户态程序通过verbs接口直接给网卡下发工作请求(Work Request),不需要经过内核协议栈。网卡硬件里维护着发送队列和接收队列,CPU只需要把描述符写到队列里,剩下的工作全由网卡完成。

CPU卸载则体现在传输层的可靠性上。RDMA网卡自己处理ACK、重传、拥塞控制,CPU可以彻底不碰数据面。这跟TCP完全相反——TCP的拥塞控制、滑动窗口全都在内核里跑,每个包都要消耗CPU周期。

可以这样理解:TCP是让快递每到一个中转站都重新装卸一遍,RDMA是快递车从发货仓库直接开到收货仓库,连货都不落地。这也是为什么很多场景下RDMA不只是快,而是CPU占用还能同时降下来。

2.2 IB、RoCE、iWARP三种落地形态怎么选

RDMA的实现并不只有一种网络形态,大体上分三类:InfiniBand(IB)、RoCE、iWARP。NCCL对IB和RoCE的支持最成熟,iWARP相对小众一些。我用下面这张表做个直接对比。

形态承载网络部署成本典型延迟适用场景
InfiniBand专用IB交换机高,需要专网最低,约1-2微秒高性能计算集群
RoCE v2普通以太网交换机中,需要无损配置约2-5微秒分布式训练/数据中心
iWARP通用TCP网络低较高,依赖TCP栈兼容性优先的场景

RoCE在实际训练集群里非常流行,因为它能复用现成的以太网交换机和线缆,成本比IB低很多。但RoCE有个前提:交换机必须支持无损以太网,也就是要开启PFC(优先级流控)和合适的ECN,否则RoCE在高负载下会出现丢包,性能直接断崖。

我自己第一次碰RoCE的时候以为把网卡驱动装上就行,结果一压测就是掉包,后来查了一圈才发现交换机的PFC配置没开。所以RoCE并不是“RDMA的廉价平替”那么简单,它需要你在网络设备层面一起配合。

3. NCCL的整体架构:看RDMA在哪个位置干活

3.1 从API到连接:NCCL的分层设计

NCCL本身提供了一组集合通信的API,比如ncclAllReduce、ncclBroadcast、ncclAllGather,上层框架像PyTorch的DDP就是通过NCCL来做梯度同步的。但很多人不知道的是,NCCL内部并不是直接调RDMA的verbs接口,它自己也有抽象分层。

从外往里看,大致是四层。最外层是集合通信原语API;再往里是算法层,负责实现Ring、Tree这类具体的通信拓扑;然后是连接层,管理一对一的点对点连接;最底层才是传输驱动,按实际环境选择NVLink、PCIe P2P、共享内存或者RDMA网络。

RDMA处于最底层,但它不是孤立的。NCCL会优先在同机GPU之间尝试NVLink P2P,只有数据真正跨机时才会走RDMA。所以NCCL的架构本质上是一个按距离和带宽分层决策的系统:能近就不远,能直连就不转发。

这个设计有个很重要的好处:同一套上层算法和API,可以同时覆盖单机八卡和跨机几十上百卡的环境。上层不用关心底下到底走NVLink还是走RDMA,NCCL在建立连接时自动给你选好。

3.2 通道(Channel)与RDMA资源的对应

NCCL里有一个很核心的概念叫Channel,中文常翻译成“通道”。每个通道对应一条独立的通信路径,一个Ring或一棵Tree都在某个通道上流转。

在跨机场景下,通道数量和网卡资源有直接对应关系。NCCL在初始化时会为每个通道建立一组网络连接,每个连接背后就是一个RDMA的队列对(QP)。你可以理解成:每条跨机数据通路,都占用了网卡上的一对传输队列。

NCCL默认会根据GPU数量和网卡数量去决定开多少个通道。通道多了,通信可以并行跑,封顶的吞吐才会高;通道太少,即使网卡带宽够也发挥不出来。这也是为什么高网卡配置下你要检查NCCL是否真的识别到了所有网卡,否则通信并行度会白白打折。

每个通道内的连接还有一个细节:NCCL会建立一对“发送”和“接收”方向独立的QP,并且通信数据的buffer要在初始化阶段就注册到RDMA网卡的内存区域里。注册的意思是告诉网卡:这块GPU显存物理地址可以直接DMA读写。注册完成后,网卡才真正拿到这些地址的访问权。

3.3 NCCL初始化时到底做了什么

NCCL初始化阶段值得单独展开,因为很多问题都出在这。它第一步会用TCP做Bootstrap,也就是各个rank之间先通过普通TCP网络互相发现,交换GPU拓扑、网卡信息、节点地址。这一阶段不传大数据,只做控制面通信,因此走TCP完全没问题。

之后NCCL会根据拓扑信息决定使用哪些传输方式:同机GPU之间开P2P连接,跨机就建立RDMA连接。建立RDMA连接时要交换的信息包括QP编号、GID地址、内存区域key(lkey/rkey)等。这些信息交换完成后,数据面就直接走RDMA了,TCP连接在后面基本派不上用场。

所以如果你想确认NCCL有没有走RDMA,关键看初始化阶段的日志里,网络连接的建立模式是IB/RoCE还是Socket。如果是Socket,说明NCCL认为当前环境不支持RDMA,退化成了TCP传输。

4. 一次AllReduce的完整旅行:从显存出发再回到显存

4.1 Ring算法下消息如何流转

拿最常见的Ring AllReduce举例。假设有4个节点,每个节点上有一个rank参与通信,所有rank通过RDMA连成一个虚拟环。整体通信分两步:第一步是reduce-scatter,每个rank把自己的数据切成4块,然后从环上相邻rank接收数据块并做累加;第二步是allgather,每个rank把已经reduce完的块继续往环上传,直到所有rank都拿到完整结果。

这个算法的优势是每个rank只跟自己的前驱和后继各通信一次,网络负载均衡,带宽利用率高。而RDMA正好匹配这种模式:NCCL把每个相邻rank之间的点对点连接都建成了RDMA QP,数据走的就是类似直连快递的路径。

在一台四机环境里,如果配置正确,你会发现每个rank的发送队列里有三份数据块等着往下一跳发,同时接收队列也在从上一跳收数据,整个流水线是重叠的。通信延迟被数据块切分和流水线隐藏了,这也是为什么Ring能做到高吞吐的原因之一。

4.2 GPUDirect RDMA如何完成显存到网卡的零拷贝DMA

这里要展开一个关键机制:GPUDirect RDMA。它做的事情是让网卡绕过主机内存,直接访问GPU显存地址。没有它,网卡再快也没用,因为数据还得先从显存拷到主机内存,这本身就是一次额外搬运。

开启GPUDirect RDMA之后,发送路径变成了:GPU kernel写完梯度后,网卡通过DMA描述符直接读取显存中指定地址的数据,拆包组包后发送到链路。接收路径则反过来:网卡解析完数据后,把payload直接DMA写入对端显存的目标地址。整条链路里,CPU和主机内存完全不碰数据。

要支持这个能力,需要有配套的内核模块来做GPU显存和网卡DMA地址的映射,同时NCCL也会在建立连接时把显存buffer注册到网卡。如果这些条件不满足,NCCL会检测到不支持GPUDirect RDMA,然后退回“GPU显存 → 主机内存 → 网卡”的间接传输。虽然还是RDMA,但多了一条PCIe搬运,延迟会高,带宽也会受影响。

所以判断“是否走了RDMA”只是第一层,确认“RDMA是否走了GPUDirect”才是性能优化的关键一步。这两个东西在日志里都能看出来,下面一节讲具体怎么查。

5. 用日志和工具确认你的NCCL确实在走RDMA

5.1 打开NCCL_DEBUG看传输类型

NCCL内置了非常详细的调试日志,打开方式很简单,设置环境变量NCCL_DEBUG=INFO,如果还想看网络子系统的更多细节,可以再加上NCCL_DEBUG_SUBSYS=NET。我用一个较细的组合来跑:

export NCCL_DEBUG=INFO export NCCL_DEBUG_SUBSYS=NET mpirun -np 16 -hostfile hosts ./your_training_script.py

跑起来之后,日志里会刷出一大堆NCCL INFO开头的行。重点看两个地方。第一处是初始化时每个rank打印的通信拓扑信息,它会把节点内/节点间的连接类型写出来。第二处是网络连接建立日志,如果看到类似NCCL INFO NET/IB的行,说明已经通过IB或RoCE建立了RDMA连接。

如果日志里出现的是NCCL INFO NET/Socket,那就要警惕了:NCCL认为当前没有可用的RDMA网卡,回落到了TCP socket传输。Socket传输不是不能用,但性能跟RDMA完全不是一个档位。

我在实际排查中习惯把日志重定向到文件里,然后直接grep关键字段:

mpirun -np 16 -hostfile hosts ./your_training_script.py 2>&1 | tee nccl.log grep -E "NET/IB|NET/Socket|Using (IB|socket)" nccl.log | head -n 40

这样一眼就能看到通信路径属于哪一类。对于GPUDirect的确认,可以再打开NCCL_DEBUG=TRACE级别,日志会包含更细的DMA/注册信息。不过TRACE日志量非常大,四机环境下几分钟就能刷出几GB,建议只在短时间测试时使用。

5.2 用nccl-tests实测

日志只能告诉你“走没走RDMA”,但性能到底怎么样,还得用nccl-tests压一下。这套测试工具是NCCL官方配套的,里面最常用的是all_reduce_perf和all_gather_perf,编译好之后用法很简单:

mpirun -np 16 -hostfile hosts \ /path/to/nccl-tests/build/all_reduce_perf \ -b 128M -e 8G -f 2 -g 1

参数含义:-b是起始size,-e是结束size,-f是倍增系数,-g表示每个GPU跑一个进程。-b 128M -e 8G -f 2的意思是消息从128MB开始,每次翻倍,一直测到8GB。

跑完后的输出表里有两列需要重点看:Algo bandwidth是算法带宽,Bus bandwidth是总线带宽。AllReduce算法本身相当于每个rank要发两轮数据,所以Bus bandwidth通常是Algo bandwidth的两倍左右。在多机正常走RDMA的情况下,消息尺寸够大时Bus bandwidth应该能逐步逼近网卡聚合带宽的理论值。

如果消息小的时候带宽低,那是正常的,毕竟有握手延迟存在。但如果消息上到GB级带宽还上不去,那就不要看网卡标称值了,先去确认GPUDirect是否真的生效,再看网卡驱动版本和交换机流控配置。

还有一个小技巧:用time参数可以看每个size下实际花了多少时间。如果你发现带宽曲线随着消息增大出现断崖,基本可以断定中间发生了某层回退,比如从GPUDirect掉到了普通拷贝路径。

6. 我在实际环境里踩过的坑和调优路径

6.1 RoCE能ping通却不走RDMA——GID index与MTU的坑

踩过最经典的一个坑是RoCE网络明明可以ping通,网卡驱动也正常,但NCCL日志里始终是NET/Socket。排查了很久才发现是RoCE的GID index配置不对。

RoCE在建立QP时需要一个GID(Global Identifier),网卡上可能有多个GID条目,NCCL默认拿的索引可能不是RoCE v2那个。解决办法是显式指定:

export NCCL_IB_GID_INDEX=3

不同网卡和驱动版本下默认值不同,可以先自己查询网卡的GID列表再配。另外,RoCE的MTU设置也要和交换机对齐。MTU不一致时,大包直接丢,小包能过,表现就是小消息正常、大消息传输时性能剧烈抖动。

我当时的排查顺序是:先看dmesg里有没有网卡丢包记录,再用ib_write_bw这类perftest小工具测裸流性能,最后再回到NCCL日志。如果perftest本身就跑不满,说明是底层网络的问题;如果perftest正常但NCCL不行,才需要怀疑NCCL自身的配置。

6.2 多网卡负载均衡与GDR Level

另一个容易踩的点是NCCL没有把多网卡并行用起来。很多机器装了不止一张网卡,但NCCL默认可能只挑一张来用。想让它按你的拓扑来,可以显式指定:

export NCCL_IB_HCA=mlx5_0,mlx5_1 export NCCL_SOCKET_IFNAME=eth0,eth1

NCCL_IB_HCA是告诉它用哪些IB/RoCE设备,NCCL_SOCKET_IFNAME是告诉它Bootstrap阶段用哪些TCP接口。这里建议HCA列表和socket接口列表一一对应,避免控制面走错网卡。

还有一个参数是NCCL_NET_GDR_LEVEL,它控制GPUDirect RDMA的使用强度。常见的是0到5几个档位,大致含义如下表:

取值行为
0关闭GPUDirect RDMA,数据经主机内存中转
1-4针对单网卡/多网卡、单进程/多进程做不同层级的控制
5全部走GPUDirect RDMA,最大程度依赖GPU显存直通

在配套驱动完整的情况下,建议直接设成5,让NCCL不要在传输路径上做额外的拷贝降级。但注意,如果把GDR打开后机器出现GPU和网卡之间的PCIe带宽竞争,反而可能比不开更慢,这时候就要做实测对比。

我自己的习惯是先跑一组默认配置的数据,再分别改NCCL_IB_GID_INDEX、NCCL_NET_GDR_LEVEL、NCCL_IB_HCA,每次只改一个变量,对比nccl-tests的输出。这样不仅能确认哪个参数在起作用,还能留下完整的调优记录。分布式训练的通信链路牵涉GPU驱动、网卡固件、交换机配置和NCCL版本多个层面,任何一个环节掉了链子,表面都是“训练变慢”这一种表现,但根因可能差得很远。把链路拆细、把日志看懂,比盲目加网卡更有意义。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进