一、为什么 RDMA 重新站到舞台中央
大模型训练把网络从"辅助组件"推成了"算力的一部分"。一个 175B 参数的模型做一次数据并行同步,AllReduce 的通信量在 GB 量级;当单卡算力增长放缓而集群规模持续扩张时,通信墙(communication wall)直接决定 MFU(Model FLOPs Utilization)。
传统 TCP/IP 路径的代价是结构性的:一次 send() 要经历用户态到内核态的拷贝、协议栈处理、再到网卡 DMA,中间还有中断与上下文切换。在 200Gbps 链路上打满线速,纯内核协议栈往往要吃掉多个 CPU 核。RDMA(Remote Direct Memory Access)的价值在于三件事同时成立:内核旁路(kernel bypass)、零拷贝(zero copy)、CPU 卸载(protocol offload)——数据从本地用户态内存直接落到远端用户态内存,CPU 只负责下发工作请求。
但 RDMA 不是"换一个 API 就变快"的银弹。它把复杂度从内核搬到了应用:内存必须先注册、连接必须显式管理、完成语义必须自己处理、拥塞控制交给网络。下面从语义、代码、到生产陷阱逐层拆开。
二、三种传输语义:先选对模型
RDMA 提供三类操作,选错语义是初学者第一个坑:
| 语义 | 是否需要对端 CPU 参与 | 是否知道地址 | 典型用途 |
|---|---|---|---|
| Send / Recv | 需要(对端必须预 Post Receive) | 否 | 控制面、小消息、带立即数通知 |
| Write | 不需要 | 需要远端 rkey + 虚拟地址 | 数据推送、梯度传输 |
| Read | 不需要 | 需要远端 rkey + 虚拟地址 | 拉取远端状态、参数服务器 |
关键差异在于 Write/Read 是单边操作(one-sided):远端 CPU 完全不参与,不产生中断,甚至不知道这次传输发生过。这是 RDMA 性能魔力的来源,也是调试噩梦的根源——远端进程崩溃了,你这边可能还在成功写入一块已经无人看管的内存。
传输类型(QP 类型)同样要选:
- RC(Reliable Connected):可靠、有序、支持全部语义。AI 训练的事实标准。
- UC(Unreliable Connected):不支持 Read,可能丢包。少用。
- UD(Unreliable Datagram):类似 UDP,支持多播。用于连接建立阶段的地址发现(如通过 UD 交换 RC 连接信息)。
三、Verbs 核心对象模型
在写代码之前,必须理解五个对象的关系:
Protection Domain (PD)
├── Memory Region (MR) → 注册后得到 lkey / rkey
└── Queue Pair (QP) = Send Queue (SQ) + Receive Queue (RQ)
└── 绑定到 Completion Queue (CQ)
- PD:隔离域,MR 和 QP 必须在同一 PD 内才能互相引用。
- MR:把一段虚拟内存钉住(pin)并交给网卡,返回 32 位
lkey(本地访问)和rkey(远端访问)。没有注册的缓冲区无法被 RDMA 访问——这是零拷贝的前提,也是最大的成本来源。 - QP:通信端点,等价于一条连接的收发队列。SQ 上放 Work Request(WR),RQ 上必须预先 Post Receive 缓冲区。
- CQ:完成队列,WR 完成后生成一个 CQE(Completion Queue Entry)。轮询 CQ 是唯一获知完成的方式,没有回调,没有中断(除非你主动 arm 一个)。
四、代码实战:建立一个 RC 连接并做单边写
下面是从零建立 RC QP 并做 RDMA Write with Immediate 的精简骨架。省略了设备枚举与错误处理,聚焦主干逻辑。
4.1 打开设备与注册内存
#include <infiniband/verbs.h>
struct ibv_context *ctx;
struct ibv_device **dev_list = ibv_get_device_list(NULL);
ctx = ibv_open_device(dev_list[0]); // 打开第一个 HCA
struct ibv_pd *pd = ibv_alloc_pd(ctx);
// 关键:缓冲区必须页对齐,否则注册后仍可能被限制访问粒度
size_t buf_size = 4 * 1024 * 1024;
void *buf;
posix_memalign(&buf, sysconf(_SC_PAGESIZE), buf_size);
memset(buf, 0, buf_size); // 必须先触碰,确保页已映射
struct ibv_mr *mr = ibv_reg_mr(
pd, buf, buf_size,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ
);
// mr->lkey 本地用,mr->rkey 发给对端用于单边操作
posix_memalign 与 memset 这两步不是形式主义。注册内存会触发缺页并把页钉死,未对齐的缓冲区可能导致网卡按页粒度访问时越界;未触碰的页在注册后如果发生写时复制(COW),物理页换掉而网卡仍持有旧页地址,就会出现静默数据损坏。这是 RDMA 最经典的隐蔽 bug。
4.2 创建 CQ 与 QP
struct ibv_cq *cq = ibv_create_cq(ctx, 1024, NULL, NULL, 0);
struct ibv_qp_init_attr qp_attr = {0};
qp_attr.send_cq = cq;
qp_attr.recv_cq = cq;
qp_attr.cap.max_send_wr = 512; // SQ 深度,直接决定在途请求数
qp_attr.cap.max_recv_wr = 512; // RQ 深度,必须 >= 对端并发 Send 数
qp_attr.cap.max_send_sge = 4; // 每个 WR 最多聚合几个散布段
qp_attr.cap.max_recv_sge = 4;
qp_attr.cap.max_inline_data = 256;// 小数据内联进 WR,省一次 DMA 读取
qp_attr.qp_type = IBV_QPT_RC; // 可靠连接
struct ibv_qp *qp = ibv_create_qp(pd, &qp_attr);
max_inline_data 是个被低估的优化项:小于该阈值的 payload 会随 WR 一起通过 PCIe 写入网卡,网卡无需再发起一次 DMA 回读主机内存,小消息延迟可降数百纳秒。
4.3 状态机迁移(最容易被忽视的环节)
QP 不是创建完就能用,必须严格按 RESET → INIT → RTR → RTS 迁移:
// RESET -> INIT
struct ibv_qp_attr attr = {0};
attr.qp_state = IBV_QPS_INIT;
attr.port_num = 1;
attr.pkey_index = 0;
attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ;
ibv_modify_qp(qp, &attr,
IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS);
// INIT -> RTR(Ready To Receive):此处填入对端信息
attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_4096; // RoCE 通常 1024/2048
attr.dest_qp_num = remote_qpn; // 对端 QP 号
attr.rq_psn = remote_psn; // 对端起始包序列号
attr.max_dest_rd_atomic = 16; // 允许对端的在途 Read 数
attr.min_rnr_timer = 12; // RNR NAK 重传间隔
attr.ah_attr.is_global = 1; // RoCEv2 必须走 GRH
attr.ah_attr.grh.dgid = remote_gid;
attr.ah_attr.grh.sgid_index= 1; // 注意:RoCEv2 常被误配为 0
attr.ah_attr.port_num = 1;
ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU |
IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER);
// RTR -> RTS(Ready To Send)
attr.qp_state = IBV_QPS_RTS;
attr.sq_psn = local_psn;
attr.timeout = 14; // 重传超时 ~4.096us * 2^14
attr.retry_cnt = 7; // 传输层重试次数
attr.rnr_retry = 7; // RNR 重试,0 表示无限重试
attr.max_rd_atomic = 16;
ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT |
IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC);
sgid_index 是 RoCEv2 部署的高频故障点。InfiniBand 一般用 index 0,而 RoCEv2 的 RoCEv1/UDP 封装 GID 表位置依赖驱动与配置,ibv_devinfo -v 里那个带 IPv4 映射地址的 GID 才是正确索引。配错的表现非常迷惑:QP 能建起来,但第一个包就丢,抓包看是 UDP 目的 MAC 不对。
4.4 发起单边写 + 立即数
// 对端必须预先 Post Receive,否则立即数无处安放 -> RNR NAK
static void post_recv(struct ibv_qp *qp, struct ibv_mr *mr) {
struct ibv_sge sge = { .addr = (uintptr_t)mr->addr,
.length = 64, .lkey = mr->lkey };
struct ibv_recv_wr wr = {0}, *bad = NULL;
wr.wr_id = 0x1234; // 自定义标识,会在 CQE 中原样返回
wr.sg_list = &sge;
wr.num_sge = 1;
ibv_post_recv(qp, &wr, &bad);
}
static void rdma_write_imm(struct ibv_qp *qp, struct ibv_mr *mr,
uint64_t remote_addr, uint32_t rkey) {
struct ibv_sge sge = { .addr = (uintptr_t)mr->addr,
.length = 4096, .lkey = mr->lkey };
struct ibv_send_wr wr = {0}, *bad = NULL;
wr.wr_id = 0x5678;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM; // 写 + 通知
wr.send_flags = IBV_SEND_SIGNALED; // 生成 CQE
wr.imm_data = htonl(0xCAFE);
wr.wr.rdma.remote_addr = remote_addr; // 对端 mr->addr
wr.wr.rdma.rkey = rkey; // 对端 mr->rkey
ibv_post_send(qp, &wr, &bad);
}
WRITE_WITH_IMM 是工程上最常用的组合:数据走单边写落到远端内存(不消耗对端 CPU),同时立即数触发一个 CQE 唤醒对端(对端仍需预 Post 一个 Receive 来接这个立即数)。数据传输零 CPU,通知一次中断,这个组合几乎是所有 RDMA 中间件(NCCL、UCX、RPC 框架)的通用范式。
4.5 轮询完成队列
struct ibv_wc wc;
int ne;
do {
ne = ibv_poll_cq(cq, 1, &wc);
} while (ne == 0);
if (ne < 0 || wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "CQE error: %s\n", ibv_wc_status_str(wc.status));
// 一旦出现 WC 错误,该 QP 基本进入不可用状态,必须重建
}
// wc.wr_id 对应你下发时的标识;wc.opcode 区分是 Send/Write/Recv
注意 ibv_poll_cq 返回值三种语义:>0 取到的 CQE 数,0 表示空,<0 表示出错。不要用 ne == 0 判断错误。另外,一旦某个 WR 产生错误 CQE,该 QP 通常会进入 error state,正确做法是整体重建而不是继续下发。
五、被低估的成本:内存注册
ibv_reg_mr 不是免费的。它要做三件事:锁页(mlock)、建立虚拟地址到物理地址的映射表(MTT)、把映射表写入网卡缓存。实测单次注册 4MB 内存约 5–20 微秒,且是同步阻塞操作。
这直接引出几条工程结论:
- 绝不收发路径上注册内存。必须做内存池 + MR 缓存,启动时注册大块,运行时从池里切分。
- 警惕注册缓存膨胀。长期运行的进程如果按请求粒度缓存 MR,物理页被钉死的总量会超过 RLIMIT_MEMLOCK,表现为注册失败而非 OOM,很难归因。
- 考虑 ODP(On-Demand Paging)。现代网卡支持按需分页,注册时不再全量钉页,缺页由网卡触发
fault回调。它解决了灵活性与内存占用,但引入了不可预测的缺页延迟,高频小消息场景下得不偿失。 - 大页(HugePage)能显著降低 MTT 压力。同样 1GB 内存,4KB 页需要 26 万个表项,2MB 大页只需 512 个,网卡 TLB 命中率天差地别。
六、拥塞控制:RDMA 最危险的部分
RDMA 要求无损网络,而以太网上实现无损靠的是 PFC(Priority Flow Control)。问题在于 PFC 是逐跳暂停机制:当下游交换机缓冲区将满,会向上游发暂停帧。在大规模 fat-tree 拓扑下,这会产生 head-of-line blocking,严重时形成 PFC 死锁——多个交换机互相等待对方释放缓冲区,整个网络静默停摆。
生产级的解法是 DCQCN(Data Center Quantized Congestion Notification)三件套:
- ECN:交换机在队列超阈值时给报文打 Congestion Encountered 标记;
- CNP(Congestion Notification Packet):接收端收到 ECN 后回送 CNP 给发送端;
- 速率调节:发送端按 AI(加性增)/ MD(乘性减)调整 QP 发送速率,整个过程在网卡硬件完成。
部署 checklist:
# 1. 确认网卡工作在 RoCEv2 且开启 DCQCN
cma_roce_mode -d mlx5_0 -p 1 # 应输出 RoCE v2
mlxconfig -d <pci_addr> q | grep -i dcqcn
# 2. 交换机侧 ECN 阈值(以 SONiC/Cumulus 为例)
# ECN 标记阈值必须严格小于 PFC 触发阈值,否则来不及降速就已丢包
# 3. 统计验证:tx_pause 应接近 0,rp_cnp_ignored 不应持续增长
ethtool -S <iface> | grep -E 'pause|cnp|ecn'
一个实用的判据:如果 PFC 暂停帧持续增长,说明 ECN 没生效或阈值配错——DCQCN 调好了,PFC 应该只是最后的安全网,而不是常态。
七、AI 训练中的真实用法:NCCL 视角
在 NCCL 里,RDMA 不是用来做点对点 send/recv 的,而是承载 Ring AllReduce 的流水线。N 张卡构成一个环,每张卡把本地梯度切成 N-1 块,经过 2(N-1) 步完成规约。每一步的传输量是 total_grad / N,通信量与 N 无关——这是 ring 算法能线性扩展的原因。
关键工程细节:
- GPUDirect RDMA:网卡绕过主机内存,直接从 GPU 显存 DMA。省掉一次 GPU→CPU→GPU 的拷贝,实测能把 AllReduce 延迟降低 30%–40%。需要
nvidia-peermem内核模块,且网卡与 GPU 必须在同一 PCIe switch 下才能发挥最佳效果——NUMA/PCIe 拓扑决定性能上限。 - 多轨(multi-rail)聚合:单机 8 卡通常配 4–8 张网卡,NCCL 会把一条逻辑连接条带化到多张网卡上。这里最容易踩的坑是网卡与 GPU 的亲和性配错,导致数据跨 CPU socket 走 UPI,带宽被砍半。用
nvidia-smi topo -m核对NIC与GPU的亲和关系(应是PIX/PXB而非SYS)。 - 小消息走 Send,大消息走 Write:NCCL 内部对不同尺寸切换协议,避免小消息上单边语义的连接管理开销。
八、生产环境避坑清单
- RQ 深度必须足够。对端并发 Send 数超过
max_recv_wr会触发 RNR NAK,性能断崖式下跌。宁可开大。 - 连接建立用带外通道。QP 号、PSN、GID、rkey 通常需要一个 TCP 控制面交换。别指望 RDMA 自举。
- 永远校验 CQE status。
IBV_WC_SUCCESS之外的错误意味着 QP 已不可用。 - 监控 MTT 与 pinned memory。
/proc/<pid>/status的VmLck是被钉住的内存量,接近 RLIMIT_MEMLOCK 就是告警信号。 - 超时参数别照抄。
timeout=14适合低延迟无损网络,跨机房或丢包率高的环境要调大,否则重传风暴会把链路打满。 - 别在 RC 上做大规模扇出。N 个节点全互联需要 N² 条 QP,MR 与 QP 内存开销指数级增长。超大规模应考虑 UD 或共享连接的中间件(如 UCX)。
九、结语
RDMA 的本质是一次权衡的转移:它把协议栈复杂度、拷贝开销、CPU 占用从内核和驱动那里拿走,交给了应用程序员和运维。你换来的是 200Gbps 线速下 2–3 微秒的端到端延迟和接近零的 CPU 占用,代价是必须自己管理内存生命周期、连接状态机、拥塞响应和故障恢复。
对 AI 训练集群而言,这笔交易是划算的——通信效率直接换算成 MFU,而 MFU 换算成真金白银的训练成本。但对普通业务而言,除非你的瓶颈确实在网络延迟或 CPU 协议栈开销上,否则引入 RDMA 的复杂度很可能超过收益。
判断标准很简单:先做 profiling,确认中断、拷贝、上下文切换确实占据了你无法接受的开销,再考虑 RDMA。 否则你只是把一个简单的问题换成了复杂的问题。

发表评论 取消回复