1. 缘起:为什么好好的 TCP/IP 突然不够用了
我在做分布式存储那几年,最头疼的问题不是磁盘慢,而是网络慢。明明服务器里装的是闪存盘,单机随机读能做到几十万 IOPS,结果一走到网络层,整条链路的 IOPS 直接掉到几千。问题出在哪?CPU、内存、网卡三套体系在互相拖后腿——数据从应用程序的内存到对端应用程序的内存,要经过用户态缓冲、内核协议栈、网卡驱动、中断处理,这一路上的拷贝和上下文切换,往往比真正在网线上传输的时间还长。
后来我转到 AI 训练集群的运维,发现这个矛盾被放得更大了。GPU 跑一轮迭代只要几十毫秒,但梯度同步时如果把数据送到 TCP 协议栈里走一圈,网络开销可能比计算本身还高。说白了,当网络从“偶尔传点小文件”变成“每时每刻都在流动的海量数据”时,TCP/IP 那个“内核代收代发”的模型就成了瓶颈。这时候,RDMA(Remote Direct Memory Access,远程直接内存访问)才真正从冷门技术变成了救星。
RDMA 的核心诉求非常直白:让一台机器的应用可以直接读写另一台机器的内存,不经过操作系统内核,不经过对方 CPU,也不要中间一堆拷贝。它解决的痛点,说白了就是网络数据传输里的“中间商赚差价”。这篇文章我会从最底层的内存搬运讲起,一直聊到它在 AI 集群网络里怎么落地,全程用我实际踩过的坑和验证过的方式来讲,适合正在做分布式存储、高性能计算或者大模型训练的工程师参考。
2. 传统网络路径到底慢在哪
2.1 一次数据发送的内在流程
先别急着上 RDMA,你得先理解传统网络为什么慢,才知道 RDMA 省掉了什么。以最简单的 TCP 发送为例,应用层调用 send() 发送数据,实际操作可不像你想象的那样直接把数据丢到网卡里就完事。
数据要先从应用程序的用户态缓冲区复制到内核态的 socket 发送缓冲区,这个拷贝是必须的,因为内核不能直接信任用户态传进来的指针。然后 TCP 协议栈开始干活:切分、加 TCP 头、加 IP 头,计算校验和,再转给网卡驱动。网卡驱动把数据从内核缓冲区搬运到网卡自带的 DMA 描述符环里,网卡才能把数据真正发出去。这个过程中,还要经过多次上下文切换——应用态切到内核态,处理完再切回来,每一次切换都是纯开销。
接收方向更离谱。数据到达网卡后,网卡先要通过中断告诉 CPU:“有数据来了。”CPU 得暂停手上的活儿,去处理这个中断,把数据从网卡 DMA 到内核缓冲区,然后软中断去处理 TCP 包,确认序号、重组数据,最后再把数据从内核缓存复制到用户态缓冲区,应用才能拿到。这是一条“应用 → 内核 → 网卡 → 网络 → 网卡 → 内核 → 应用”的链路,数据被复制了至少四次,上下文切换了至少四次,CPU 参与到了每一次搬运中。
2.2 真正拖后腿的其实是内存拷贝
很多初学者以为网络慢是因为链路带宽不够,实际上对于 40GbE、100GbE 的高速网络,链路易损率已经很低了,真正拖后腿的是内存拷贝。一次内存拷贝的吞吐量再高,也经不住来回倒腾的次数太多。当网络带宽从 1GbE 提升到 100GbE 时,CPU 需要搬运的数据量同步提升了 100 倍,但 CPU 的主频和内存带宽却没跟上这个节奏。结果就是,网络带宽翻倍,CPU 先被榨干了,应用层看到的吞吐量根本跑不满链路。
我做性能测试时遇到过一个经典场景:两台 25GbE 网卡的服务器,用 iperf 打 TCP 流量,只能跑到 6-7Gbps,CPU 单核直接飙到 100%。链路本身完好无损,但协议栈把单核 CPU 耗尽了。这时候你加再大的带宽也没用,瓶颈已经不在链路上,而在 CPU 参与数据搬运的能力上。RDMA 的思路就是从根本上把 CPU 从数据搬运这条路上赶走,让网卡自己完成大部分工作。
2.3 类比解释:快递员 vs 直达专线
用个通俗的类比来理解这个转变。传统网络就像你寄快递:你把东西打包好(用户态缓冲区),快递员上门取件(内核拷贝),送到中转站分拣(协议栈处理),再运到另一个城市的中转站(网络传输),最后快递员派送(内核拷贝到用户态),收件人才能拿到包裹。整个过程每个环节都要有人经手,每一手都是时间。
RDMA 相当于修了一条两地之间的点对点高速传送带,你的包裹直接从你的仓库送到对方仓库,全程没有快递员经手,没有中转站分拣。这听着很爽,但代价是:你得提前告诉传送带系统“我的仓库在哪、多大、什么时候能开仓接货”——这就是 RDMA 里“注册内存”和“建立连接”的核心思想。
3. RDMA 的三种落地形态和它们的脾气
3.1 InfiniBand、RoCE、iWARP,到底选哪个
RDMA 不只是某一种实现,而是一族技术的统称。目前市面上主要能看到三种形态:InfiniBand、RoCE(RDMA over Converged Ethernet,融合以太网上的 RDMA)、iWARP(Internet Wide Area RDMA Protocol,互联网广域 RDMA 协议)。它们的共同点是都支持 RDMA 语义,但底层承载的链路不同,脾气也完全不一样。
InfiniBand 是从头到尾为 RDMA 设计的专用网络,硬件、协议、交换机全部是定制化,性能最好,延迟可以做到亚微秒级别,但也有个明显的缺点——贵。一套 InfiniBand 交换机加 HCA 卡(Host Channel Adapter,主机通道适配器)的价格比同规格以太网设备贵不少,而且技术栈相对封闭,跟传统以太网设备不互通。适合预算充足、追求极致性能的 HPC 中心。
RoCE 是在标准以太网上实现 RDMA 的协议,分 RoCEv1 和 RoCEv2 两个版本。RoCEv1 在二层以太网运行,没法跨网段路由;RoCEv2 把 RDMA 报文封装进 UDP/IP 包,可以走三层路由,灵活性大大提升。目前绝大多数 AI 集群选的都是 RoCEv2,因为可以复用现成的以太网交换机和运维体系,成本低一大截,性能虽然不如 InfiniBand 那么极致,但也足够喂饱主流 GPU 的通信需求。
iWARP 则是把 RDMA 语义实现在 TCP 之上,优点是兼容性最好,缺点是它绕不开 TCP 协议栈的处理开销,性能上限不如前两者,现在用的场景越来越少了。如果完全没有特殊需求,我建议直接考虑 RoCEv2,这也是当下性价比最高、生态最成熟的路线。
3.2 RDMA 背后的核心机制:队列对、完成队列、零拷贝
不管底层是哪种链路,RDMA 的逻辑模型都是一样的。应用程序要和远端通信,首先要创建一组队列对(Queue Pair,QP),QP 里面包含一个发送队列和一个接收队列。应用把要发送的数据描述成“工作请求”(Work Request,WR),丢给发送队列,然后网卡自己按照描述去内存里取数据、封装、发送,整个过程中应用不需要再参与。
对端接收时,网卡把数据直接 DMA 到预先注册好的内存区域,然后在接收队列里记录一个“工作完成”(Work Completion,WC)事件。应用通过轮询完成队列(Completion Queue,CQ)来感知数据是否到达。换句话说,发送方和接收方做的事情都非常简单:发就是“提交一个描述”,收就是“轮询一个事件”,真正搬数据的是网卡硬件。
这个逻辑里最关键的机制是“零拷贝”。RDMA 要求应用在初始化阶段就向网卡注册一块内存区域,注册时会把这块内存的物理地址映射到网卡的地址空间里。这样网卡在收发数据时,直接用 DMA 读写这块物理内存,不需要先把数据搬到内核缓冲,也不需要 CPU 在中间做一次中转。配合“内核旁路”(kernel bypass),数据从应用态内存直接进入网卡,再从网卡直接进入对端应用态内存,全程没有一次系统调用,没有一次上下文切换。
3.3 到底怎么选:一个快速决策表
很多人问我,做 AI 训练到底用 InfiniBand 还是 RoCE?我给一个非常务实的判断标准:
| 维度 | InfiniBand | RoCEv2 |
|---|---|---|
| 延迟 | 极低,亚微秒级 | 低,与 IB 差距在可接受范围 |
| 生态兼容 | 专用,与以太网不互通 | 完全兼容现有以太网设施 |
| 成本 | 高,交换机和网卡都贵 | 性价比高,可用普通交换机 |
| 拥塞控制 | 硬件原生支持 | 依赖 ECN/PFC 等以太网机制配合 |
| 适用场景 | HPC 中心、对延迟极敏感的场景 | AI 训练集群、分布式存储、通用高性能网络 |
如果你是在现有机房里搭 AI 训练集群,大概率会选 RoCEv2。它不完美,尤其在丢包控制上比较娇气,但只要配置得当,RoCEv2 完全能满足大模型训练对网络吞吐和延迟的要求。
4. 实操:在 Linux 服务器上快速搭一套 RDMA 环境
4.1 硬件选型和驱动安装注意事项
实操之前先确认硬件。RDMA 不是装个驱动就能用,物理网卡必须支持 RDMA 能力。NVIDIA 的 ConnectX 系列(从 ConnectX-4 到 ConnectX-7)是当前支持 RoCEv2 最成熟的网卡,Intel 的 E810 系列也支持但生态相对没那么完善。采购时务必确认网卡型号支持 RoCE,部分标着“支持 RDMA”的低端网卡只支持 iWARP,买回来再折腾就麻烦了。
驱动方面,NVIDIA 网卡用的是 mlx5 驱动,官方叫 MLNX_OFED(OpenFabrics Enterprise Distribution)。安装时注意和内核版本、发行版版本的兼容性,建议直接按官方软件仓库的指引来装。装完用ibstat或ibv_devinfo验证一下驱动状态,看到 state 为 Active 就说明网卡已经被正确驱动起来了。
4.2 注册内存区域:一个最简单的实现
RDMA 的零拷贝依赖内存注册,这是所有后续操作的前提。在 Linux 上,我们可以通过 libibverbs 库调用ibv_reg_mr()函数注册内存区域。参数有三个核心信息:内存起始地址、长度、访问权限。权限至少得包含IBV_ACCESS_LOCAL_WRITE和IBV_ACCESS_REMOTE_WRITE,否则对方没法往你的内存里写数据。
注册完成后,系统会返回一个lkey(本地键)和rkey(远端键)。lkey是本地使用,网卡在本地 DMA 读写这块内存时要用;rkey要告诉给对端,对端拿着这个键才能直接往你的内存里写。这就相当于你把自家仓库的钥匙给了对方,但只给了一把限定权限的钥匙,不能打开其他房间。
这里有个非常容易踩的坑:注册的内存区域在 RDMA 通信期间绝对不能释放,也不能改变它的用途。一旦注册了,这段内存在虚拟地址、物理地址上都要保持稳定。否则网卡正在 DMA 时内存被换页或者释放,轻则数据错乱,重则直接 segfault,而且这种问题非常难复现,排查起来让人抓狂。
4.3 建立连接和交换信息:握手过程简化版
RDMA 连接建立比 TCP 要复杂一些,因为它不仅要交换 IP 地址和端口,还要交换 QP 编号、内存区域信息、rkey 等信息。因为 RDMA 控制面是走软件建立的,数据面才走硬件直通。最简单的用法是在两台机器上用rdma_cm库(RDMA Communication Manager,RDMA 通信管理器)来建立连接。
用rdma_cm写程序时,服务器端流程是:创建 RDMA CM ID、绑定地址、监听、等待连接、接受连接。客户端流程是:创建 RDMA CM ID、解析地址、连接。连接建立后,两端都能通过事件回调拿到对端的 QP 信息,这时候再用ibv_create_qp()创建数据面需要的 QP,把 QP 信息通过已有的 CM 通道交换给对方。
我自己做性能验证时,会先跑一遍官方的 perftest 工具来确认链路状态。这个工具包含了ib_write_bw(写带宽测试)、ib_send_bw(双边收发测试)、ib_read_lat(读延迟测试)等现成工具,不需要自己写代码就能快速摸清一对网卡跑到什么水平。
4.4 验证 RDMA 生效的四个命令
建完环境,先别急着写复杂代码,用这组命令快速验证:
# 查看网卡状态,确认 link 是 Up,state 是 Active ibstat # 查看 RDMA 设备能力,确认支持 RoCEv2 ibv_devinfo -v | grep -i roce # 测试两个节点间的 RDMA 写带宽(服务端先跑,客户端后跑) # 服务端 ib_write_bw -d mlx5_0 # 客户端 ib_write_bw -d mlx5_0 192.168.1.2 # 查看 RDMA 连接是否建立成功 rdma statistic showibstat输出里的 state 为 Active、physical state 为 LinkUp 是基本要求。如果看到 Down 或者 INIT,先检查网线、光模块,再看驱动日志。ib_write_bw的结果如果单线程跑到接近线速,说明 RDMA 链路已经通,而且性能正常发挥。rdma statistic show能看到当前活跃的 QP 数和收发字节统计,用于确认程序运行时确实走了 RDMA 路径。
5. AI 集群里 RDMA 到底扮演什么角色
5.1 大模型训练中的通信模式:AllReduce 和 AlltoAll
大模型训练的通信模式,说得直白点,就是 GPU 之间不断在“交换梯度”和“交换激活值”。数据并行训练时,每个 GPU 各自计算一部分 batch 的梯度,然后需要把所有 GPU 上的梯度加起来得到全局梯度,这个操作叫 AllReduce。模型并行(比如张量并行、流水线并行)时,GPU 之间需要互相传递中间激活值,这就是 AlltoAll、AllGather 等集合通信操作。
通信量有多大?以 1750 亿参数的大模型为例,单次梯度同步就要交换几十 GB 级别的数据。如果网络是传统的 TCP/IP,光是把这些梯度从 GPU 内存搬到 CPU 内存再走网络,就得消耗可观的 CPU 资源和额外延迟。这也是为什么你能看到大模型训练集群里,GPU 之间的互联都是动辄 400Gbps 的 InfiniBand 或 RoCEv2 网络——不是炫技,是通信量已经大到不走 RDMA 根本跑不动。
5.2 GPU Direct RDMA:跳过 CPU 那一哆嗦
AI 集群里 RDMA 还有个杀手级特性叫 GPU Direct RDMA(GDR)。普通的 RDMA 路径是:GPU 显存 → CPU 内存 → 网卡 → 对端网卡 → CPU 内存 → 对端 GPU 显存,中间还是有一道 CPU 内存的转手。GDR 直接把网卡的 DMA 引擎指向 GPU 显存的物理地址,数据从 GPU 显存直接到网卡,再从网卡直接进对端 GPU 显存,CPU 内存完全不参与。
要实现 GDR,硬件层面要求网卡和 GPU 在同一棵 PCIe 树上,或者至少通过高性能 PCIe Switch 互连。软件层面要启用 GPU Direct 相关的驱动参数,比如 NVIDIA 官方文档里的NV_P2P_ENABLE相关环境变量。配置完成后,NCCL(NVIDIA Collective Communications Library,NVIDIA 集合通信库)会自动检测到 GDR 能力,你不需要改代码,就可以在测试日志里看到类似[0] plugin: gdr, support: 1这样的输出。
GDR 带来的性能提升非常可观。我实测过一个 8 卡 A100 节点,启用 GDR 后 AllReduce 带宽比纯 RDMA 路径提升 30% 以上,延迟降低了 20% 微秒级别。效果虽然不是数量级的飞跃,但在大模型训练这种动不动跑几百轮的场景下,每轮省下来的时间累积起来非常可观。
5.3 AI 集群中 RDMA 的拓扑设计要点
AI 训练集群的 RDMA 网络拓扑,最常见的是两层 spine-leaf 结构。叶子层连接 GPU 服务器,Spine 层做无阻塞转发,保证任意两台服务器之间的通信都不经历链路共享。无论是 InfiniBand 还是 RoCEv2,拓扑设计的第一原则都是:不要让通信路径上出现拥塞热点,尤其是不能让东西向流量和南北向流量争抢链路。
对于 RoCEv2 网络,特别要注意 PFC(Priority Flow Control,优先级流量控制)和 ECN(Explicit Congestion Notification,显式拥塞通知)的配合。RoCEv2 对丢包极其敏感,一个包丢失就可能引发 TCP 级别的重传风暴,带宽直接断崖式下跌。所以要在交换机上为 RDMA 流量划分独立的优先级队列,启用 PFC 做无损保障,同时配合 ECN 做拥塞反馈,让发送端动态调整速率。这一套配置如果只设 PFC 不设 ECN,拥塞时容易引发 PFC 风暴,导致全网链路被反向压力堵死。
6. RDMA 调优的一线经验和踩坑记录
6.1 带宽起不来的四个常见原因
跑 RDMA 第一件事不是看配置文档,而是先确认链路是不是真的“无损”。我遇到过太多次性能上不去的案例,最后查来查去,问题都不是出在 RDMA 本身,而是底层以太网的过载丢包。
原因一是 MTU 不一致。RoCEv2 依赖大 MTU 来提升带宽效率,建议全链路设置 4096 字节的 MTU。如果交换机上跑了默认 1500 字节的 MTU,会导致 RDMA 报文被分片,带宽直接减半。这个问题的排查很简单,在服务器上ping -M do -s 8972到对端,能通就说明 MTU 没问题。
原因二是 PFC 没配好。PFC 要求从接入交换机到核心交换机的每个端口,对某个优先级队列都要启用无损配置。只要有任何一个端口漏配,流量经过那个端口时一拥塞就开始丢包,RDMA 的表现就是吞吐忽高忽低、延迟剧烈跳动。
原因三是网卡的队列数不够。每个 QP 需要消耗网卡的硬件队列资源,队列资源不足时,程序创建的 QP 会被拒绝,或者排队时间过长,导致应用侧吞吐上不去。确认方法是通过ibv_devinfo查看max_qp、max_cq等参数,和程序实际创建的数量对比。
原因四是 CPU 和网卡的中断绑核没做。虽然 RDMA 的数据面不依赖 CPU,但在控制面(创建 QP、事件处理)以及大流量初次建立连接时,CPU 还是需要介入的。如果网卡中断都打在同一个 CPU 核上,控制面的响应就会变慢,表现为并发连接数一高延迟就飙升。
6.2 拥塞控制参数怎么调才不踩雷
RoCEv2 网络最核心的调优就是拥塞控制。你需要重点盯三个参数:ECN 的tcp_threshold、PFC 的xoff_threshold、网卡侧的gid_index设置。其中最容易出错的是交换机的xoff_threshold——这个值设得太小,PFC 会频繁触发,导致大量反压帧占满链路;设得太大,缓冲区溢出又会在拥塞还没反馈回来之前就丢包。
一个比较稳妥的取值办法:先忽略 ECN,把 PFC 的 xoff 阈值设置为交换机端口缓存的一半左右,跑一发ib_write_bw,看吞吐曲线是否平滑;然后逐步调低阈值,观察吞吐和延迟的 trade-off,找到一个稳定点。启用 ECN 后,网卡侧要开启roce.cc相关的拥塞控制算法,Mellanox 卡通常用的是 DCQCN,NVIDIA 新版驱动里也支持 Timely 等算法。
这些参数没有一组是万能模板,因为不同交换机型号的缓存大小、不同网卡芯片的响应延迟都不一样。我建议把调参过程当作一个实验流程来做:先固定其他变量,一次只动一个参数,记录性能表现,再做下一组测试。这样折腾出来的配置,才是真正适合你那套环境的。
6.3 多轨训练场景下 RDMA 配合 NCCL 的注意事项
在大模型训练里,NCCL 是实际调用 RDMA 的最上层库。NCCL 环境变量里和 RDMA 相关的有几个值得关注:NCCL_IB_DISABLE(是否禁用 InfiniBand)、NCCL_IB_GID_INDEX(设置 RoCEv2 的 GID 索引)、NCCL_IB_TIMEOUT(数据包传输超时参数)、NCCL_IB_RETRY_CNT(重传次数)。
最常见的坑是NCCL_IB_GID_INDEX设置不对。RoCEv2 里这个值通常要设为 3,对应 IPv4 的 GID 类型。如果没设对,NCCL 启动时要么报找不到设备,要么性能忽上忽下。另一组容易踩的坑是NCCL_IB_TIMEOUT设置太短。大集群下流量高峰时延迟会波动,超时设置太短会导致 NCCL 误判链路故障,频繁触发重新建连,训练直接卡死。我一般的做法是NCCL_IB_TIMEOUT=22,再配合NCCL_IB_RETRY_CNT=7,给 RDMA 链路足够的时间自我恢复。
还有一点务必注意:NCCL 启用 RDMA 时,建议把其他网络流量(管理网络、存储网络)和训练流量隔离开来,不要让它们共享同一个物理网卡。训练节点的管理口、存储口、RDMA 口要分开独立硬件,否则存储备份等日常操作会瞬间打爆 RDMA 的 QoS 队列,拖垮整个训练任务。
7. 常见问题排查速查表
| 问题现象 | 可能原因 | 排查命令与解决方案 |
|---|---|---|
| ib_write_bw 带宽远低于线速 | MTU 不一致 / PFC 未配置 | ping 大包测 MTU;检查交换机接口 qos 配置 |
| 多节点训练时 AllReduce 卡住 | NCCL_IB_TIMEOUT 设置太短 | 调大超时参数;检查是否有网卡降速事件 |
| 偶发延迟尖峰,吞吐抖动 | 拥塞控制参数不匹配 | 检查 ECN 阈值,用 ib_write_bw 加多流压测观察 |
| 创建 QP 失败,资源不足 | 网卡队列资源耗尽 | ibv_devinfo 查看 max_qp;减少 QP 数或换高阶网卡 |
| 启动时报 ibv_fork_init 相关错误 | 进程有 fork 行为 | 在初始化 RDMA 前调用 ibv_fork_init() 或避免 fork |
| GDR 没生效,NCCL 日志显示 gdr=0 | GPU 和网卡不在同 PCIe switch 下 | 检查拓扑;lspci -tv 确认链路关系 |
表格里这些场景都是我在实际维护里真真切切碰到过的,不是从文档里抄来的“理论问题”。尤其第一条,MTU 不匹配导致的带宽骤降,我见过最离谱的一版是机房网络同事在核心交换机上调了 MTU,没有同步调整接入层,结果整个训练集群的 RDMA 吞吐从 190Gbps 掉到 90Gbps,排查了整整一个下午,最后用ping -M do一测才发现问题。
8. 回到内存搬运本身的一点心得
聊了这么多,最后想分享一个我个人的观察。很多人一提到 RDMA 就觉得这是网络工程师的活,和自己没什么关系。但我在做 AI 集群的过程中越来越觉得,RDMA 的核心价值恰恰不在网络,而在“内存语义”——它把网络通信简化成了一次跨节点的内存读写,让分布式系统的编程模型变得干净了。
我自己在调优时最受益的一个习惯是:遇到性能问题,永远先回到数据路径上画一遍“数据到底搬了几次”。很多看似复杂的问题,比如带宽翻不上去、延迟抖动,追根到底都是数据路径上多了一道无谓的拷贝,或者某个环节的缓冲配置不对。RDMA 的价值就是用硬件把这条路径压缩到最短,而你作为使用者和维护者,要做的事情就是别再利用软件把它人为拉长。
如果你正在搭建自己的第一套 RDMA 环境,我的建议是从最简单的两台机器跑通ib_write_bw开始,先让链路通、让性能曲线稳定下来,再去碰 NCCL、GDR、多轨拓扑这些复杂玩意儿。先学会看底层数据,再往上层发展,你后面踩坑的概率会小很多。