第2章:RDMA 通信原理与 verbs —— 从 NIC 微架构到 doorbell batch
拆解 RDMA 的 verbs 编程模型、NIC 微架构(WQE/CQE/Doorbell)、QP 状态机、one-sided vs two-sided、doorbell batching、RoCE/InfiniBand 选型,从硬件到软件全栈打底,覆盖读 FORD/Motor 等论文需要的全部基础
RDMA 真正的价值不在”快网卡”,而在 bypass 远端 CPU——这是分离式内存能成立的物理前提。本章是模块十三的硬件/编程模型基础章,深度覆盖你做 disaggregated memory 研究 / 优化所需的所有底层基础:NIC 微架构(WQE/CQE/Doorbell)、QP 状态机、atomic op 硬件实现、doorbell batching、错误码诊断、ConnectX 代际演进。读完这章,FORD/Motor/LOTUS 论文里的每一句”我们 batch 了 N 次 CAS”、“利用 one-sided READ 拉一整 cache-line”、“绕开 atomic-IOPS 瓶颈”,你都知道底下硬件在做什么。
📑 目录
- 1. RDMA 是什么:不是”快网卡”
- 2. Verbs 编程模型:QP、MR、CQ、PD 四件套
- 3. NIC 微架构:WQE / CQE / Doorbell 内部机制
- 4. QP 状态机:从 RESET 到 RTS
- 5. 传输模式:RC / UC / UD / XRC
- 6. one-sided vs two-sided:谁感知谁不感知
- 7. Atomic op 的硬件实现:chip-level vs memory-level
- 8. Memory Registration 深挖:pin / IOMMU / NIC TLB
- 9. Doorbell batching 与 inline:把 100ns 抠出来
- 10. CQ 轮询 vs 中断:延迟敏感系统的选择
- 11. RoCE v1/v2 vs InfiniBand:选型决策
- 12. NCCL 怎么用 RDMA:从 collective 到底层 verbs
- 13. 性能微基准:perftest 实战与数字解读
- 14. WC 错误码完全手册
- 15. HW counters 与 NIC 性能诊断
- 16. ConnectX 代际演进:5 → 6 → 7
- 17. 工程踩坑清单:十大新人陷阱
- 自我检验清单
- 参考资料
1. RDMA 是什么:不是”快网卡”
很多新人第一次看到 100GbE RDMA 网卡的反应是”哦,就是更快的以太网”。这个理解错过了 RDMA 最重要的特性:两层 bypass。
传统 TCP 路径:
app → syscall → kernel TCP stack → NIC driver → 网线 →
远端 NIC → driver → kernel TCP stack → syscall → app
RDMA 路径(one-sided):
app → 用户态 verbs → NIC HW → 网线 → 远端 NIC HW → 直接 DMA 到内存
↑
远端 CPU 完全不参与
两层 bypass:
- 本地 kernel bypass:用户态直接写 doorbell 寄存器触发 NIC,不进 syscall(节省 ~1 µs)
- 远端 CPU bypass(one-sided 才有):远端 NIC 直接读写内存,远端 CPU 不知情(节省一次远端调度 + cache miss)
🧠 关键洞察:让 disaggregated memory 成立的不是带宽,是远端 CPU bypass。MN(memory node)节点上甚至可以没有 CPU 参与事务路径——这正是 FORD/Motor 这类 DM 事务系统的物理前提。如果远端必须 CPU 介入,那 MN 就退化成”远程数据库”,失去了 disaggregated 的意义。
🍎 直觉比喻:TCP 是”打电话给保姆,让她去厨房拿东西”;RDMA one-sided 是”自己有钥匙,直接进对方厨房拿”。
| 维度 | TCP/IP | RDMA send/recv | RDMA one-sided |
|---|---|---|---|
| 单边延迟(8B) | ~30 µs | ~3 µs | ~2 µs |
| 远端 CPU 参与 | ✅ 必须 | ✅ 必须(post recv + poll) | ❌ 不参与 |
| 编程模型 | socket | verbs(消息语义) | verbs(内存语义) |
| 本地 syscall 数 | ~2(send + recv) | 0(纯 user-space) | 0 |
| 数据拷贝次数 | ~4(user→kernel→NIC→…) | 0(zero-copy) | 0 |
| 远端无 CPU 也能用 | ❌ | ❌ | ✅(MN 可以是 CXL pool 或 SmartNIC) |
数据来自 ConnectX-6 100GbE 实测,具体硬件代际会有差异。
⭕ 延伸:RDMA 的”零拷贝”对小消息体感不强,但对大数据流(KV-cache blocks、训练 gradient)直接省掉两端 4 次拷贝——这是为什么 GPUDirect RDMA + NCCL 能逼近线速的根本原因。
2. Verbs 编程模型:QP、MR、CQ、PD 四件套
RDMA 的编程接口叫 libibverbs,核心抽象是四个对象,加上一个隐含主角(NIC HW context)。
┌───────────────────────────────┐
│ Protection Domain (PD) │
│ ┌────────────┐ ┌──────────┐ │
│ │ Memory │ │ Queue │ │
│ │ Region (MR)│ │ Pair (QP)│ │
│ └────────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ Completion │ │
│ │ Queue (CQ) │ │
│ └────────────┘ │
└───────────────────────────────┘
2.1 PD (Protection Domain)
权限沙箱。所有 MR 和 QP 都属于某个 PD,跨 PD 的访问会被 NIC 硬件直接拒绝(返回 IBV_WC_LOC_PROT_ERR)。
实际生产中往往一个进程一个 PD 就够了——它的设计初衷是”多进程共享 NIC 时互相隔离”,在 disaggregated memory 系统中用得不多。
2.2 MR (Memory Region)
预先注册到 NIC 的物理页面,产出一对 (lkey, rkey):
lkey:本地 key,本地发起 op 时引用本地 bufferrkey:远端 key,告知远端”这片内存可被你访问”——key 加 addr 一起传给对端,对端用它发起 one-sided op
注册过程要做几件昂贵的事:
- Pin 物理页面(防止 OS 换页)
- 给 NIC 喂 IOMMU 映射(让 NIC 能 DMA 到这片物理地址)
- NIC 内部 TLB 分配条目
单次 ibv_reg_mr 大小决定时延:小 buffer ~µs,大 buffer(几 GB)~ms 量级。所以系统都是启动时一次性注册大块 MR,运行时不再 reg。详见 §8。
2.3 QP (Queue Pair)
发送队列 SQ + 接收队列 RQ 的组合。两个 endpoint 的 QP 配对后建立 connection。所有 op 在 QP 上排队。
QP 有生命周期状态机,详见 §4。
2.4 CQ (Completion Queue)
NIC 完成 op 后写一条 CQE(Completion Queue Entry),应用 poll CQ 拿到结果。
CQE 的关键字段:
struct ibv_wc {
uint64_t wr_id; // 应用自定义 id, 用于关联到原 WR
enum ibv_wc_status status; // SUCCESS / 各种错误
enum ibv_wc_opcode opcode; // RDMA_READ / RDMA_WRITE / SEND / ...
uint32_t byte_len;
uint32_t imm_data;
uint32_t qp_num;
// ... 其他字段
};
CQE 是有序的(同一 QP 的 op 完成顺序与发起顺序一致,RC 模式下),但不同 QP 之间无序。
2.5 最简化的 PostSend → Poll 流程
// 1. SGE(scatter-gather entry)指向本地 buffer
struct ibv_sge sge = { .addr = (uint64_t)local_buf,
.length = 64,
.lkey = mr->lkey };
// 2. Work Request:描述这次 op
struct ibv_send_wr wr = {
.wr_id = 0xdeadbeef, // 应用 id, 完成时回填到 wc.wr_id
.opcode = IBV_WR_RDMA_READ,
.send_flags = IBV_SEND_SIGNALED, // 我要 CQE
.sg_list = &sge, .num_sge = 1,
.wr.rdma.remote_addr = remote_addr,
.wr.rdma.rkey = remote_rkey
};
struct ibv_send_wr *bad_wr;
// 3. Post(写 doorbell 触发 NIC,完全用户态)
ibv_post_send(qp, &wr, &bad_wr);
// 4. Poll(等完成)
struct ibv_wc wc;
while (ibv_poll_cq(cq, 1, &wc) == 0) { /* spin */ }
// 5. 检查
if (wc.status != IBV_WC_SUCCESS) { /* handle error */ }
⭕ 生产代码会做的额外事:
- 用
IBV_WR_RDMA_READ_WITH_IMM等带 immediate data 的变种,顺路传 4B 元数据 send_flags设IBV_SEND_INLINE把小 payload 直接塞进 WR(见 §9)- 多个 WR 用链表(
wr.next = &next_wr)一次 post(doorbell batch,见 §9) selective signaling:不是每个 op 都要 CQE(见 §10)
3. NIC 微架构:WQE / CQE / Doorbell 内部机制
要做 RDMA 性能优化,必须理解 NIC 看到的是什么。这一节是绝大多数中文资料里没讲透的硬核部分。
3.1 NIC 顶层框图(ConnectX-6 简化)
Host PCIe
│
┌───────┴───────┐
│ PCIe 5.0 x16 │ ◄── 数据面 (DMA, MMIO 寄存器)
│ (~64 GB/s) │
└───────┬───────┘
│
┌───────▼─────────────────────────────────────────┐
│ NIC ASIC │
│ │
│ ┌─────────────┐ ┌────────────┐ │
│ │ TX Engine │ │ RX Engine │ │
│ │ - WQE │ │ - 入包解析 │ │
│ │ fetcher │ │ - DMA write│ │
│ │ - DMA read │ └────────────┘ │
│ │ - Pkt build │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────┐ │
│ │ Internal SRAM (有限!) │ │
│ │ - QP context (per-QP state) │ │
│ │ - MR cache (TLB) │ │
│ │ - WQE 缓冲 │ │
│ │ - CQ 缓冲 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Atomic Engine │ │
│ │ (CAS / FAA 处理单元) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Crypto / Transport offload │ │
│ └─────────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────┘
│ 100 GbE / 200 GbE
▼
网络
关键:NIC 内部 SRAM 是有限的(典型几十 MB)。QP 多了、MR 多了 → SRAM 装不下 → 要去 host 内存查(性能腰斩)。
3.2 WQE(Work Queue Element)结构
应用调 ibv_post_send 时,libibverbs 把 ibv_send_wr 翻译成硬件格式的 WQE,写到 SQ 的尾部。
ConnectX-6 mlx5 的 WQE 大致结构(简化):
WQE (单个 WR 占用 64-256B,具体看 op 类型):
+------+--------------+--------+----------------+--------+
| Ctrl | RDMA addr+key| LKEY | Local addr+len | Sig |
| 16B | (8+4)B | 4B | (8+4)B | 4B |
+------+--------------+--------+----------------+--------+
↑
opcode, signaled, inline, fence, ...
字段含义:
- Ctrl:opcode(READ/WRITE/CAS/…)、SIGNALED 标志、qp_num、wqe_size
- RDMA addr + rkey:远端地址 + key(one-sided op 才有)
- LKEY:本地 lkey
- Local addr + len:本地 SGE
- Sig:校验位
关键:WQE 写到的是 host 内存(SQ ring buffer),NIC 通过 PCIe DMA 读取。
3.3 SQ / RQ Ring Buffer
SQ (Send Queue) ring buffer (host 内存):
+-------+-------+-------+-------+ ... +-------+
| WQE 0 | WQE 1 | WQE 2 | WQE 3 | |WQE N-1|
+-------+-------+-------+-------+ ... +-------+
↑ ↑
pi (producer index) ci (consumer index)
(app 写) (NIC 读后更新)
- 应用写 WQE 后,必须更新 producer index (pi),NIC 才知道有新 WQE
- 更新 pi 的方式 = 写 NIC 的 doorbell 寄存器
3.4 Doorbell 机制
Doorbell 是 NIC 暴露给 host 的一个 MMIO 寄存器(每个 QP 一个,通常映射在 BAR 空间):
┌─────────────────────────────────────────┐
│ Host CPU │
│ │
│ 写 doorbell: │
│ *((uint64_t*)doorbell_addr) = pi << 8 | qp_num │
│ │
│ ↓ 这是一次 PCIe MMIO Write │
└─────────────┬────────────────────────────┘
│ ~80-150 ns 跨 PCIe
▼
┌─────────────────────────────────────────┐
│ NIC ASIC │
│ │
│ Doorbell received → schedule QP │
│ → DMA fetch WQE from SQ │
│ → process op │
└─────────────────────────────────────────┘
为什么 doorbell 写贵:
- PCIe Posted Write 本身 ~30-50 ns(单纯传输)
- 但写后必须 fence(确保 WQE 数据先落地、再写 doorbell)→ +30-50 ns
- NIC 调度延迟:~30-50 ns
- 总开销 ~80-150 ns
这就是为什么 batch 这么重要:一次 doorbell 触发 N 个 op,摊销开销到每个 op 仅 ~10-20 ns。
3.5 Doorbell Record(优化路径)
新版 NIC(ConnectX-5+)还提供 doorbell record(BlueFlame/UAR):
旧路径:
写 WQE 到 SQ → fence → 写 doorbell MMIO → NIC fetch WQE
BlueFlame 路径:
写完整 WQE 直接到 NIC 的 BAR(BlueFlame 寄存器)→ NIC 立即开工
(不需要 NIC 再 DMA 读 SQ)
适用场景:单 op 小延迟(比如延迟敏感 CAS)。但 BlueFlame 一次只能塞一个 WQE,所以对 batch 不友好——生产中一般 batch 走传统 SQ + doorbell,延迟敏感单 op 走 BlueFlame。
3.6 CQE 结构
NIC 完成 op 后,写一条 CQE 到 CQ ring buffer(host 内存):
CQE (mlx5 格式, 64B):
+--------+--------+--------+--------+--------+--------+----+
| qp_num | wr_id | opcode | status | byte_ | imm_ |... │
| 4B | 8B | 1B | 1B | len 4B| data 4│ │
+--------+--------+--------+--------+--------+--------+----+
↑ ↑
IBV_WC_* IBV_WC_SUCCESS / 各种 error code
CQE 写完后,NIC 更新 CQ producer index,应用 poll 时检查这个 index 增加。
⭕ 优化:NIC 可以合并 CQE 写入(几个 op 一次 DMA),减少 PCIe 流量。但这种合并对应用透明。
3.7 关键 SRAM 数据结构
NIC 内部维护的关键状态:
| 数据 | 大小 | 影响 |
|---|---|---|
| QP context | 每 QP 几 KB | QP 数量 > NIC 容量 → context cache miss → 性能腰斩 |
| MR cache(TLB) | 几千-几万条 | MR/MTT miss → 多一次 host mem fetch |
| WQE 预取缓冲 | 几 KB | 影响 batch 处理效率 |
| CQ 缓冲 | 几 KB | CQE 写延迟影响 poll |
ConnectX-6 单卡推荐:
- QP 数 < 100K(超过会显著降速)
- 同时活跃 MR < 几百个(每个 MR 对应若干 TLB 条目)
- 用 hugepage(2MB / 1GB)减少 TLB 压力
🌟 结论:NIC 不是”无状态网卡”,它内部有大量 cache 和 SRAM 状态。RDMA 性能优化的本质,是让 NIC 内部 cache 不 miss。
4. QP 状态机:从 RESET 到 RTS
QP 不是一创建就能用,要走一套状态机:
┌─────────┐
│ RESET │ ibv_create_qp 后的初始态
└────┬────┘
│ ibv_modify_qp(IBV_QPS_INIT)
│ - 设 PD, port_num, qkey, access_flags
▼
┌─────────┐
│ INIT │ 本地配置就绪
└────┬────┘
│ ibv_modify_qp(IBV_QPS_RTR) Ready to Receive
│ - 设 dest_qp_num, dest_lid/gid, path_mtu, rq_psn,
│ max_dest_rd_atomic, min_rnr_timer, ah_attr
│ - 此时本地 RX 通路就绪,可以收对方的包
▼
┌─────────┐
│ RTR │ 能收,但不能主动发
└────┬────┘
│ ibv_modify_qp(IBV_QPS_RTS) Ready to Send
│ - 设 sq_psn, max_rd_atomic, timeout, retry_cnt, rnr_retry
▼
┌─────────┐
│ RTS │ 完全工作(可发可收)
└────┬────┘
│ (发生错误)
▼
┌─────────┐
│ ERR │ 出错,需要 RESET 重建
└─────────┘
4.1 关键字段:必须从对端拿到
为了从 INIT 进入 RTR,必须知道对端 QP 的几个信息:
dest_qp_num:对端 QP 号dest_lid(IB)或dest_gid(RoCE):对端网卡地址psn(packet sequence number):双方序列号
这些信息必须通过带外通道交换(verbs 本身没有 connection setup 协议)。常见做法:
- TCP/socket:小集群直接开个 TCP listen,exchange 完关
- memcached:中型集群(CREST/Motor 用这个)
- RDMA-CM:对应 CM(Connection Manager)的标准协议
- etcd / consul:大型集群用服务发现
4.2 Memcached 交换协议(以 CREST 为例)
Node A (准备 QP):
create_qp() → 拿到本地 qp_num, psn
↓
memcached SET "node_A_qp_info" = {qp_num, psn, gid}
↓
memcached GET "node_B_qp_info" → 拿到 B 的信息
↓
modify_qp(INIT → RTR) — 用 B 的信息
↓
modify_qp(RTR → RTS)
↓
memcached SET "node_A_ready" = 1
↓
等待 GET "node_B_ready" == 1
↓
开始通信
⚠️ 同步陷阱:如果 A 已经 RTS 但 B 还在 RTR,A 发的包会被 B 当作错误包丢弃。必须双方都到 RTS 之后再开始收发——这就是为什么需要 node_X_ready 这个二次同步。
4.3 错误恢复
QP 进入 ERR 状态后,必须 RESET 重建——不能直接从 ERR 跳回 RTS。重建步骤:
- ibv_modify_qp 到 RESET
- ibv_modify_qp 到 INIT
- 重新交换 QP 信息(双方都重建了,QPN 可能变)
- 走 INIT → RTR → RTS
🌟 生产经验:RDMA 应用必须有 QP 重建机制——长跑必然出错。Mooncake、Motor 等系统都有专门的 fault-tolerance 协议。
5. 传输模式:RC / UC / UD / XRC
QP 有四种传输模式,选错了直接吞掉性能或正确性:
| 模式 | 全称 | 可靠 | 有连接 | 支持 op | 代表用途 |
|---|---|---|---|---|---|
| RC | Reliable Connected | ✅(NIC 自动重传) | ✅(1-1 配对) | READ/WRITE/SEND/CAS/FAA | DM 事务、KV pool(95% 系统) |
| UC | Unreliable Connected | ❌ | ✅ | WRITE/SEND(不支持 READ/CAS!) | NCCL 部分场景 |
| UD | Unreliable Datagram | ❌ | ❌(无连接,可一对多) | SEND/RECV(payload ≤ MTU) | 集群发现、广播 |
| XRC | eXtended RC | ✅ | ✅ | 同 RC | 共享 SRQ 的多进程场景 |
5.1 关键事实
- one-sided READ 和 atomic op 只在 RC 上可用。所以做 disaggregated memory 几乎必走 RC——这一点没得选
- UD 的 MTU 上限是网络 MTU(典型 4096 字节),大于 MTU 必须自己分片
- UC 比 RC 快约 5-10%(省掉 ack 路径),但 NCCL 之外几乎不用
- XRC 主要解决”M 个进程 × N 个进程”导致 QP 数爆炸的问题——多进程共享一个 SRQ,QP 数从 M×N 降到 M+N
🌟 结论:FORD/Motor/Mooncake/AdaptX 的 QP 全是 RC,这章后面所有讨论默认 RC。
5.2 RC 的 QP 爆炸问题
N 个 CN × M 个 MN 时,要建 N×M 条 QP(因为 RC 是一对一)。当 CN/MN 都很多时:
- 每条 QP 至少占几 KB NIC SRAM
- ConnectX-6 单卡能撑 ~100K QP,超过会触发 QP cache miss(性能腰斩)
- 大集群(几百 CN × 几十 MN)就容易撞墙
缓解办法:
- SRQ(Shared Receive Queue):多个 QP 共享一个 recv 队列,recv side 内存节省
- XRC:一个 send QP 的对应,可以发给多个 receive XRC SRQ
- DCT(Dynamically Connected Transport,Mellanox 私有):动态连接,QP 不再固定 1-1
⭕ 生产经验:小集群(<32 节点)直接 RC + N×M;大集群(>100 节点)用 DCT 或 XRC。Mooncake 在大规模部署时用了 DCT。
6. one-sided vs two-sided:谁感知谁不感知
RDMA op 分两类:
| 类别 | op | 远端 CPU 参与 | 典型用途 |
|---|---|---|---|
| one-sided | READ / WRITE / CAS / FAA | ❌ 不参与 | DM 事务、disaggregated KV、远端 hash 表查询 |
| two-sided | SEND / RECV | ✅ 远端必须 post recv | RPC、消息队列、connection setup |
6.1 one-sided 四种 op
- READ(rkey, addr, len):从远端读 len 字节到本地 buffer。远端无感知,无 CPU 参与
- WRITE(rkey, addr, len):往远端写 len 字节。默认应用层无 ack(NIC 之间有 ack 保证可靠传输,但应用不感知)。变种 WRITE_WITH_IMM:写完顺带触发对端 CQE
- CAS(rkey, addr, expected, new):8B 原子比较交换,远端无感知。这是 OCC 锁的关键
- FAA(rkey, addr, value):8B fetch-and-add,远端无感知
6.2 two-sided 的代价
SEND/RECV 是消息语义:
// 接收方必须先 post recv,提供 buffer 给 NIC 准备落地
ibv_post_recv(qp, &recv_wr, &bad);
// 发送方
ibv_post_send(qp, &send_wr, &bad);
// → NIC 把 payload 直接 DMA 到接收方 pre-posted buffer
// → 接收方 CQ 收到 RECV completion(知道收到一条消息)
如果接收方没有可用 recv WR,发送方会触发 RNR(Receiver Not Ready) 错误,默认 NIC 会重试若干次后 abort。生产中常见做法:
- 维护一个长 recv 队列(比如 1024 个 pre-posted recv buffer)
- 每消费一个 recv 后立即 re-post(防止队空)
- 用 SRQ(Shared Receive Queue):多个 QP 共用一个 recv 队列
6.3 选择决策
🌟 结论:FORD 选 one-sided CAS 而不用 send/recv,是因为远端不需要任何 CPU/线程参与就能完成”获取锁 + 读数据”两步——这把 MN 从”另一个数据库”压成”一段裸内存”,disaggregation 才成立。
但 one-sided 也不是免费午餐:
- 数据布局必须 NIC-friendly:远端 NIC 不会做指针追逐,所有元数据必须 inline 在 cache-line 内(FORD 的 cache-line lock 设计就是为这个)
- 远端不能做计算:聚合、过滤这种”轻量计算”在远端 CPU 上几乎免费,但 one-sided 必须把全部数据拉过来 CN 做。这是 LOTUS 把锁迁回 CN 的动因
- CAS 只能 8B:超过 8B 的原子操作没有,只能用”锁 + 读 + 改 + 写 + 解锁”组合实现
| 场景 | 推荐 op | 理由 |
|---|---|---|
| 读远端 hash bucket | READ | 远端无负担 |
| 取锁 | CAS | 原子 + 远端无感知 |
| 提交事务计数 | FAA | 原子 + 不冲突 |
| 控制面信令(很少) | SEND/RECV | 远端要解析消息 |
| 长 RPC 协议 | SEND/RECV | 消息边界天然 |
| 集群广播(发现服务) | UD SEND | 一对多 |
7. Atomic op 的硬件实现:chip-level vs memory-level
DM 事务系统离不开 atomic op,理解硬件实现决定能不能正确预测瓶颈。这一节是被论文反复提及但很少展开的内容。
7.1 两种 atomic 实现路径
Chip-level atomic(NIC 内部完成):
CN 发 CAS 请求 →
NIC 收到 →
NIC 自己读 host 内存(或 NIC SRAM cache)→
NIC 内部比较 + 替换 →
NIC 写回 host 内存(或 cache)→
返回结果
- NIC 整个原子操作期间锁住该 cache-line
- 不经过 host CPU
- 限制:host 端访问该 cache-line(CPU read/write)与 NIC atomic 的一致性如何保证?老硬件:不保证(不能混用)
- 现代实现:NIC 与 CPU 走 PCIe coherent 协议(ATS/ATC + IOMMU),硬件保证一致
Memory-level atomic(原子操作下放到内存控制器):
CN 发 CAS 请求 →
NIC 收到 →
NIC 通过 PCIe 发 atomic 请求给 host 内存控制器 →
内存控制器原子完成 →
返回结果
- 走 PCIe atomic transaction(PCIe 3.0+ 支持)
- 完全跟 CPU 一致(CPU 同时做 atomic 也安全)
- 但 PCIe atomic 的硬件支持参差,某些 CPU+NIC 组合下会”软件 emulate”——直接崩到 1/100 性能
7.2 ConnectX 系列 atomic 演进
| NIC 代际 | atomic 实现 | 理论 IOPS 上限 | 实际中文社区报告 |
|---|---|---|---|
| ConnectX-3 | 主要 chip-level | ~1 M ops/s | ~0.5-1M |
| ConnectX-4 | 同 | ~1 M | ~1M |
| ConnectX-5 | chip-level + 部分硬件改进 | ~1.5 M | ~1.5M(LOTUS 论文背景) |
| ConnectX-6/6-Dx | 大幅优化 + ATS support | 5-10 M | 5-10M(FORD/Motor 主要测试) |
| ConnectX-7 | 进一步优化 | 估计 15-20 M | 暂少公开数据 |
7.3 atomic op 的延迟构成
单个 8B CAS:
CN 发起 ~0.1 µs (post + DMA fetch WQE)
网络往返 ~1-2 µs (单 LAN, 100GbE)
NIC atomic 单元处理 ~0.2-0.3 µs
host 内存 read+write ~0.1-0.2 µs (cached)
返回 CQE ~0.1 µs
────────────────────
总计 ~2-3 µs
vs 简单 8B READ:
~1.5-2 µs
CAS 比 READ 慢约 1 µs,主要在 atomic 单元串行化。
7.4 atomic-IOPS 上限的根因
为什么 ConnectX-6 是 5-10M?
- NIC 内部 atomic 单元的处理流水线限制
- atomic 期间 cache-line 锁住 → 同一 cache-line 的并发 atomic 不能并行(必须串行)
- 不同 cache-line 可以并行,但全 NIC atomic 单元也有总数上限
热点单 cache-line 的 CAS 因为不能并行,实际上限会更低(单 cache-line 估计 1-2M)。这正是 FORD 把锁/版本/数据放同一 cache-line 后还会有 hotspot 问题的原因——hot record 仍卡在单 cache-line atomic 串行。
7.5 工程含义
🧠 关键洞察:atomic-IOPS 不是均匀分布的——同一 cache-line 的 atomic 远低于跨 cache-line atomic 的总和。这意味着:
- 均匀分散负载 → 接近 5-10M
- hot key 集中 → 单 key 上限 ~1-2M(单 cache-line)
- 超过这个上限就 retry storm
FORD/Motor 的 cache-line 对齐设计在 cold path 上有效,但 hot key 上仍受限——LOTUS / AdaptX 必须从协议层绕开。
⭕ 优化路径:
- Padding:把热点 record 的锁字段散开到不同 cache-line(代价:多次 RDMA op)
- Lock striping:逻辑上一个锁,物理上 N 个 stripe,CN 选其中一个(代价:协议复杂)
- Lease-based:不是每次都 CAS,先获取一段租约,租约期内独占(代价:租约管理)
8. Memory Registration 深挖:pin / IOMMU / NIC TLB
ibv_reg_mr 是最容易出问题的 verbs 之一,因为它牵涉三层硬件:OS 页表、IOMMU、NIC TLB。
8.1 注册的内部步骤
ibv_reg_mr(pd, addr, len, flags)
│
├─ Pin pages (mlock-style):防止 OS 换页,直到 dereg
│
├─ Get physical addresses for [addr, addr+len)
│
├─ Program IOMMU:让 NIC 能用 IOVA → 物理地址
│ └─ 没设 iommu=pt 时,这一步可能跳过 / 错误
│
├─ Allocate NIC TLB entry:NIC 内部需缓存映射(MTT, Memory Translation Table)
│
└─ Return (lkey, rkey)
8.2 NIC 的 MTT 机制
NIC 内部有一个 MTT(Memory Translation Table)——存”IOVA → host 物理地址”的映射。
NIC 处理 RDMA op:
收到 (rkey, addr, len)
↓
用 rkey 查 MTT,得到对应物理地址段
↓
DMA 读/写
MTT 是 NIC 内部 SRAM 的稀缺资源:
- ConnectX-6 大概有几万到几十万条 MTT entry
- 一个 MR 可能占用很多 entry(取决于 page size)
8.3 Page size 的影响
- 4 KB page:1 GB MR 占用 256K MTT entry → 容易超 NIC 容量
- 2 MB hugepage:1 GB MR 占用 512 MTT entry → 节省 500×
- 1 GB hugepage:1 GB MR 占用 1 entry → 节省 256K×
🌟 生产经验:所有大型 RDMA 系统都用 hugepage——FORD/Motor/Mooncake 全是。
# 启用 1GB hugepage
echo 32 | sudo tee /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
mount -t hugetlbfs none /mnt/huge -o pagesize=1G
应用 mmap hugepage:
fd = open("/mnt/huge/buffer", O_CREAT | O_RDWR, 0666);
ftruncate(fd, size);
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
mr = ibv_reg_mr(pd, buf, size, ACCESS_FLAGS);
8.4 为什么注册这么贵
- Pin 大段内存 = 几十 ms(
mlock系统调用 + 内核遍历) - IOMMU 编程 = µs-ms(取决于硬件)
- NIC MTT 编程 = µs/entry × N entry
总耗时:
- 1 GB MR + 4KB page = 几秒(很慢!)
- 1 GB MR + 1GB hugepage = ~ms
⚠️ 生产中绝不在 critical path 调 ibv_reg_mr——只在启动时做一次。
8.5 实战策略
策略 1:启动时 reg 大块,运行时分配
// 启动时:
mr = ibv_reg_mr(pd, big_buffer, 32 GB, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | ...);
// 运行时:用 sub-allocator 在 big_buffer 上切
void *obj = my_allocator.alloc(big_buffer, size);
// 用 mr->lkey 访问 obj 即可,无需新 reg
FORD/Motor/CREST 全部走这条路。
策略 2:On-Demand Paging (ODP)
新版 OFED 支持 ODP——MR 可以不立即 pin,运行时 NIC 触发 page fault 再 fetch。代价是首次访问慢、复杂度高,生产慎用。
8.6 失败模式
⚠️ 三大经典坑:
- MR 大小超过物理 RAM → ibv_reg_mr 返回 ENOMEM,但更可能是先 OOM kill。生产中要先估准 mr_size(CREST 的
mr_size=32配置就是这么来的) - IOMMU 没设 passthrough → reg 返回成功,但 RDMA READ 拿到全 0 数据(WC 显示 SUCCESS)。这个是本系列踩过最阴险的坑——表面没错,数据全错
- rkey 泄露给不可信对端 → 远端可以用这个 rkey 任意读写你的 MR 范围。生产代码绝不在公网传 rkey,只在内网带外通道(memcached / tcp)交换
8.7 rkey 安全模型
rkey 是唯一的访问凭证——一旦泄露,持有者可在 MR 范围内任意读写。
攻击模型:
攻击者拿到 rkey → 用任意 RDMA op 访问该 MR 范围
→ 没有进一步认证!
生产保护:
- rkey 只在带外通道(TCP socket 加密、memcached 不暴露公网)交换
- 用 IDE(Integrity & Data Encryption)加密 RDMA 流量(CXL 也用同样的 IDE)
- 多租户隔离:每个租户独立 PD + 独立 rkey 域
9. Doorbell batching 与 inline:把 100ns 抠出来
NIC 触发一次 op 的本质是:应用写 NIC 的 doorbell 寄存器(MMIO),NIC DMA 读 work request → 执行 → 写 CQE。MMIO 写本身要 ~80-150 ns,如果一次只发一个 op,这部分占总延迟很可观。
9.1 Doorbell batching
一次写多个 WR 到本地内存(用 wr.next 链),然后只敲一次 doorbell,NIC 一次性处理整批。
struct ibv_send_wr wr1, wr2, wr3, *bad;
wr1.next = &wr2;
wr2.next = &wr3;
wr3.next = NULL;
// 一次 ibv_post_send,内部只敲一次 doorbell
ibv_post_send(qp, &wr1, &bad);
时序对比:
单个 op: WR1 → ring → MMIO → NIC poll → DMA → ... [80 ns + ...]
batch=8: WR1..WR8 → ring → MMIO → NIC poll → DMA(8x) → ... [80 ns + 8x 数据]
收益:批 8 时 doorbell 摊销到每个 op 只有 10 ns,延迟下降 ~30%、吞吐上升 5-10×。
9.2 Inline data
对小写,把 payload 直接塞进 WR 描述符里(IBV_SEND_INLINE flag),NIC 拿到 WR 时数据已经在,省一次 DMA(~80 ns)。代价:WR 描述符变大,batch 内总字节数受限(典型 inline cap 220-256 字节,可在 QP 创建时配)。
wr.send_flags = IBV_SEND_INLINE | IBV_SEND_SIGNALED;
sge.addr = (uint64_t)small_payload; // 8B 锁字、版本号
sge.length = 8;
// NIC 看到 INLINE,直接拷数据进 WR 描述符
9.3 Selective signaling
每个 SIGNALED op 都会写一条 CQE(花费 64B DMA + 应用 poll 时间)。对批量 op,只让最后一个 signaled,前面的 unsignaled:
for (int i = 0; i < N - 1; i++)
wrs[i].send_flags = 0; // unsignaled, 不产 CQE
wrs[N-1].send_flags = IBV_SEND_SIGNALED; // 最后一个 signaled
应用只 poll 最后一个 CQE,前面的 op 完成隐含确认(RC 模式有序保证)。节省 N-1 次 CQE 写入和 poll——延迟敏感系统(FORD/Motor)的标配优化。
⚠️ 限制:unsignaled WR 必须有上限(默认 max_send_wr / 2),否则 NIC 内部 outstanding queue 满 → 后续 post 阻塞。生产中每 N 个 op 强制一次 signaled,N 通常 8-32。
9.4 综合效果
🌟 结论:FORD/Motor/CREST 这类系统的 commit phase 都是 doorbell-batched + inline + selective signaling 三件套混用——一个 commit 同时写 N 条 record 的版本号 + 锁,batch 后总延迟就是单 op 延迟而不是 N×。
| 优化 | 单 op 延迟下降 | 适用条件 |
|---|---|---|
| Doorbell batch=8 | -30%(摊销 doorbell) | 批量 op,延迟可微容忍 |
| Inline send | -80 ns | payload < 220 B |
| Selective signaling | -50 ns(部分 op 不写 CQE) | 不需要每个都 poll |
| Hugepage MR | -100 ns(减少 TLB miss) | 大 MR + 随机访问 |
| Unreliable transport(UC/UD) | -50 ns | 应用层有重试机制 |
⚠️ 失败模式:batch 太大会导致 head-of-line blocking——批内某个 op 失败时整批要重做,同时 CQ 写入也变粗粒度,影响延迟敏感场景。生产中 batch 大小一般 4-16,不超过 32。
10. CQ 轮询 vs 中断:延迟敏感系统的选择
收完成事件有两种模式:
10.1 Busy-poll(纯轮询)
while (1) {
int n = ibv_poll_cq(cq, BATCH, wcs);
for (int i = 0; i < n; i++) handle(wcs[i]);
}
- ✅ 延迟最低(~100ns 拿到完成事件)
- ✅ 无 syscall、无线程切换
- ❌ CPU 100% 占用——一个 worker 线程绑一个核 spin
- ❌ 多核 CPU 利用率难提升(每核都在 spin)
FORD/Motor/Mooncake/AdaptX 全是 busy-poll——延迟敏感,CPU 不省。
10.2 中断驱动 (Event channel)
ibv_req_notify_cq(cq, 0); // 让 NIC 在下次 CQE 写入时发中断
poll(&pfd, 1, -1); // 等中断
ibv_get_cq_event(channel, &ev_cq, &ctx);
ibv_poll_cq(cq, BATCH, wcs);
ibv_ack_cq_events(ev_cq, 1);
- ✅ CPU 可让出
- ❌ 唤醒延迟 ~5-10 µs(中断 + 调度 + 缓存冷)
- 适合低 QPS、延迟可容忍场景(比如管理面、低频心跳)
10.3 混合模式
- Adaptive polling:先 spin 一段时间(几十 µs),还没事件就降级到中断
- NAPI-style:有事件时连续 poll 到队空,空闲时降级
- 实现复杂,但能兼顾延迟与 CPU 利用率
🌟 生产经验:DM 事务和 LLM 推理的 critical path 几乎全 busy-poll,管理面和后台任务才用中断。
11. RoCE v1/v2 vs InfiniBand:选型决策
RDMA 协议族有三条:
Application (libibverbs)
│
┌──────────────┴──────────────┐
│ │
InfiniBand RoCE (RDMA over Converged Ethernet)
(专用网络) ├── RoCE v1: L2 over Ethernet (同 LAN)
└── RoCE v2: L3 over UDP/IP (可跨子网)
11.1 三者协议栈差异
| 维度 | InfiniBand | RoCE v1 | RoCE v2 |
|---|---|---|---|
| 物理层 | IB switch + IB cable | 标准以太网 | 标准以太网 |
| 链路层 | IB LRH | Ethernet + IB GRH | Ethernet |
| 网络层 | IB GRH | IB GRH | UDP over IPv4/v6(端口 4791) |
| 路由 | IB subnet manager | L2 only | L3(可跨 VLAN/子网) |
| 拥塞控制 | IB credit-based(无 PFC 也工作) | PFC 必须 | DCQCN(基于 ECN)+ PFC |
| 部署复杂度 | 高(专用 fabric) | 低-中 | 中(以太网 + 拥塞配置) |
| 大集群成熟度 | 顶级(NVIDIA HPC) | 退场 | 高(超大规模 AI 集群) |
| 跨 DC 跨 region | 困难 | 不行 | 可以(L3 路由) |
| 价格 | 较贵(专用) | (逐渐淘汰) | 便宜(以太网生态) |
RoCE v1 已基本退场(L2 限制太大),实际选型只剩 IB 和 RoCE v2。
11.2 RoCE v2 的拥塞控制要点
RoCE v2 性能对拥塞控制配置极度敏感,配错的 RoCE v2 比配对的 IB 慢 5-10×。
PFC(Priority Flow Control):链路层暂停帧。某段链路拥塞时,下游发 PAUSE 让上游停发。问题:
- PFC storm:多跳传播,某节点慢传染整网
- Head-of-Line blocking:暂停优先级 X 时,该优先级所有流量停
- Deadlock:循环 PAUSE 路径形成死锁
DCQCN(Data Center QCN):基于 ECN 的端到端拥塞控制,降速发送方。配合 PFC 使用,目标是让 PFC 几乎不触发(只做 last-resort)。
🌟 配置经验(微软 RDMA over Converged Ethernet 经验):
- ECN 阈值要小(K_min = 几 KB,K_max = 几十 KB)
- DCQCN 速率降速参数 alpha = 1/16 起步
- PFC 只在 RoCE 优先级队列上启用(不影响普通 TCP)
- buffer 充足(交换机至少 16MB shared buffer)
11.3 GID 和 LID 的区别
- LID(Local ID):InfiniBand 地址,16-bit,subnet manager 分配
- GID(Global ID):128-bit(类似 IPv6),InfiniBand 跨子网或 RoCE 上用
- gid_index:网卡上有多个 GID(对应不同 IP / VLAN),用 index 选哪个
⚠️ 常见配错:RoCE v2 用 InfiniBand 的 GID index(默认 0)→ 连接建不起。一般 RoCE v2 用 gid_index = 3(实际 IP 那个),需要 show_gids 工具确认。
11.4 选型决策
- 同机柜 + 单租户 + 性能极致 → InfiniBand(微软 Azure HPC、阿里 PAI 大规模训练、HPC 集群)
- 跨机柜 + 多租户 + 与现有以太网共存 → RoCE v2(Meta、字节超大规模 AI 集群主流)
- AWS p5/Azure HBv4/GCP A3 → 都是定制化(EFA / IB / RoCE 自研变种)
⭕ CloudLab(本系列实验载体)给的是 RoCE v2 + ConnectX-6 Dx,主要因为这是大规模学术集群的事实标准。
12. NCCL 怎么用 RDMA:从 collective 到底层 verbs
NCCL(NVIDIA Collective Communication Library)是 GPU 训练用的集合通信库,提供 ncclAllReduce、ncclAllGather 等高层接口。底层调用栈:
ncclAllReduce
└─ Channel-based ring/tree algorithm
└─ NCCL P2P transport
├─ NVLink (同节点 GPU 间, ~600 GB/s)
├─ PCIe (退化路径, ~30 GB/s)
└─ NET plugin
└─ ibv_post_send (RDMA)
└─ ConnectX HW
12.1 关键设计
- GPU Direct RDMA:NIC 直接 DMA 到 GPU HBM,绕过 host RAM(否则 GPU→DDR→NIC→对端 NIC→DDR→GPU,bandwidth 减半)。需 NIC + GPU 同一 PCIe root complex 或 NVLink 域
- Ring vs Tree algorithm:Ring 在低节点数下更高效(单 hop 简单),Tree 在大规模(>32 GPU)对 reduce 更友好
- 多 channel 并行:一次 AllReduce 拆成 N 个 channel 并行跑(N=2-16),各自走独立 QP,提升带宽并行度
- bucket 攒梯度:不是每个 layer gradient 单独 allreduce,而是攒到 ~25MB bucket 再一次 allreduce(摊销启动开销)
12.2 内部通信骨架
某次 AllReduce 16MB tensor:
→ 切成 8 个 2MB chunk
→ 每个 chunk 通过 ring 一圈(N 节点 = N-1 hops × 2 = ~2(N-1) 通信)
→ 每个 hop = ibv_post_send 一个 chunk + ibv_poll_cq
→ 用 immediate data 传 chunk 序号
🍎 直觉比喻:NCCL 就像快递公司的”统一调度”,而你裸调 ibv 是”自己开车送”。统一调度对常见路线(标准 collective)很优,但你的特殊需求(比如 disaggregated KV 这种”多对一拉取”)它没有现成路由。
12.3 为什么 NCCL 在某些场景慢于裸 verbs
- NCCL 的 ring 在拓扑不均(8 卡节点 + 跨机)情况下会按”最大公约数”切分,慢卡拖快卡
- 裸 verbs 可以做应用层调度(如 Mooncake transfer engine 直接 ibv_post_send 多个独立 QP),不受 collective 抽象约束
- NCCL 启动开销(channel 建立、bucket 攒)在小消息场景占比高
- NCCL bucket 默认 25MB,小 tensor 等不齐 bucket → 延迟突增
12.4 选型经验
- 标准模型训练(同构集群、AllReduce/AllGather 主导)→ NCCL,不要轮重造
- 推理 KV-cache 跨节点搬运、推荐 embedding lookup、disaggregated 数据库 → 裸 verbs(通信模式不是 collective)
- MoE 训练的 expert routing → 裸 verbs(每次目标节点不同,collective 不匹配)
13. 性能微基准:perftest 实战与数字解读
perftest(github.com/linux-rdma/perftest)是 RDMA 调优的”瑞士军刀”。
13.1 三个最常用工具
# Server 端(被测远端)
ib_read_bw -d mlx5_2 -i 1 -F --report_gbits
# Client 端(发起方)
ib_read_bw -d mlx5_2 -i 1 -F --report_gbits <server_ip>
| 工具 | 测什么 | 健康指标(ConnectX-6 100GbE) | 异常情况 |
|---|---|---|---|
| ib_send_bw | two-sided 带宽 | 95-98 Gb/s | <80 Gb/s → MTU 没改大 / PFC 配置 |
| ib_read_bw | one-sided READ 带宽 | 90-95 Gb/s | <70 Gb/s → 远端 PCIe 瓶颈 / IOMMU |
| ib_atomic_bw | CAS/FAA IOPS | 5-10 M ops/s | <2M → atomic 走 software emulation |
| ib_write_bw | one-sided WRITE 带宽 | 95-98 Gb/s | <80 Gb/s → 同 send_bw |
| ib_send_lat | 单 op 延迟 | ~1-2 µs | >5 µs → 启用了中断 / 不是 polling |
| ib_atomic_lat | CAS 延迟 | ~2 µs | >10 µs → CAS 路径未硬件 offload |
13.2 典型 ib_read_bw 输出解读
#bytes #iterations BW peak[Gb/sec] BW average[Gb/sec] MsgRate[Mpps]
65536 5000 94.83 94.65 0.18
- 65536 字节包,5000 次,平均 94.65 Gb/s(健康)
- MsgRate 0.18 Mpps = 每秒 18 万次 op(64KB 包)
- 注意:小包(64B)时 BW 会很低,但 MsgRate 高——衡量 IOPS 看 MsgRate
13.3 ib_atomic_bw 的特殊性
#bytes #iterations BW peak[Mops/sec]
8 1000000 7.32
ConnectX-6 atomic 上限 5-10 Mops/s。这是 DM 事务系统理论的 atomic-IOPS 天花板——ConnectX-5 时代是 ~1.5M(LOTUS 提锁分离的 motivation),ConnectX-6 提升一个数量级但仍是瓶颈。
13.4 ib_read_bw 略低于 send_bw 的原因
READ 是 CN 发请求 → MN 回数据(一次往返,数据回流方向占用入向带宽)。SEND 是单向(应用层无 ack 给 wire,只在协议层有最小确认),所以 SEND 比 READ 略快。
差距通常 1-3 Gb/s,不是 bug——这是协议本身的开销,不是配置问题。
13.5 实战微基准检查清单
新集群上线时,按顺序跑这些验证:
ibv_devinfo -d mlx5_2:看 active_mtu, state PORT_ACTIVE, phys_state LINK_UPib_send_bw:两节点跑,期望 95+ Gb/sib_read_bw:期望 90+ Gb/sib_atomic_bw:期望 5+ Mopsib_send_lat:期望 ~1-2 µs- 多对节点测试(3-4 节点之间两两测,看是否一致)
任何一项明显异常,先别跑应用,先解决底层。
14. WC 错误码完全手册
ibv_wc.status 不是 SUCCESS 时,至少有 30+ 种错误——本节给一份诊断手册。
14.1 高频错误码表
| 错误码 | 原因 | 诊断方向 |
|---|---|---|
IBV_WC_SUCCESS | 成功 | (无) |
IBV_WC_LOC_LEN_ERR | 本地长度错(SGE > MR 范围) | 检查 SGE.length 与 MR 边界 |
IBV_WC_LOC_QP_OP_ERR | 本地 QP op 错(QP 状态不对) | QP 不是 RTS,或 op 不被该 QP 类型支持 |
IBV_WC_LOC_PROT_ERR | 本地保护错(lkey 错误 / PD 不匹配) | 检查 lkey + PD |
IBV_WC_WR_FLUSH_ERR | 之前的 WR 失败,后续被刷 | 找前面的真正错误 |
IBV_WC_MW_BIND_ERR | Memory window 绑定错 | 一般用不到 |
IBV_WC_BAD_RESP_ERR | 远端响应格式错 | NIC 间协议错 / 兼容问题 |
IBV_WC_LOC_ACCESS_ERR | 本地访问权限错 | MR 没有 LOCAL_WRITE access |
IBV_WC_REM_INV_REQ_ERR | 远端拒绝 — 请求无效 | rkey 错 / 远端 QP 状态不对 |
IBV_WC_REM_ACCESS_ERR | 远端访问拒绝 | 远端 MR 没有 REMOTE_READ/WRITE access |
IBV_WC_REM_OP_ERR | 远端操作错 | 远端 NIC 内部错 |
IBV_WC_RETRY_EXC_ERR | 重试超限 | 网络断 / 远端 QP 进 ERR / 远端 NIC 拥塞 |
IBV_WC_RNR_RETRY_EXC_ERR | RNR 重试超限 | 远端没 post recv |
IBV_WC_LOC_RDD_VIOL_ERR | RDD 违反 | (RD 模式专属,常用 RC 不见) |
IBV_WC_REM_INV_RD_REQ_ERR | 远端 RD 请求无效 | (同上) |
IBV_WC_REM_ABORT_ERR | 远端中止 | 远端取消了操作 |
IBV_WC_INV_EECN_ERR | EECN 错 | (RD 模式专属) |
IBV_WC_INV_EEC_STATE_ERR | EEC 状态错 | (RD 模式专属) |
IBV_WC_FATAL_ERR | NIC 致命错 | NIC 进入 reset 状态,需要重启 |
IBV_WC_RESP_TIMEOUT_ERR | 响应超时 | 网络 / 远端慢 |
IBV_WC_GENERAL_ERR | 通用错 | 看 vendor_err 字段 |
14.2 三个最常见的诊断套路
套路 1:IBV_WC_RETRY_EXC_ERR (远端 QP 没起来)
- 检查远端 QP 是否到 RTS
- 检查双方 QPN/PSN 是否匹配
- 检查 GID/LID 是否对(RoCE v2 用 gid_index=3 这种)
- 用
ibv_devinfo看 phys_state
套路 2:IBV_WC_REM_ACCESS_ERR (远端权限错)
- 检查 MR 注册时的 access_flags 是否包含 IBV_ACCESS_REMOTE_READ / WRITE / ATOMIC
- 检查 rkey 是不是错的(可能传错 MR 的 rkey)
套路 3:IBV_WC_LOC_PROT_ERR (本地保护错)
- 检查 SGE.lkey 与 MR.lkey 是否匹配
- 检查 SGE.addr + length 是否在 MR 范围内
- 检查 QP 与 MR 是否在同一 PD
14.3 vendor_err 字段
ibv_wc.vendor_err 是 NIC 厂商定义的扩展错误码,比 status 更细。Mellanox NIC 上可以查 mlx5 source code 或文档。
🌟 生产经验:遇到 RDMA 错误,先看 status,再看 vendor_err,最后用 perftest 回退到底层验证——按这个顺序大部分问题 30 分钟内能定位。
15. HW counters 与 NIC 性能诊断
NIC 暴露大量 hw_counters,理解它们是性能诊断的关键。
15.1 Mellanox hw_counters 路径
# 路径
ls /sys/class/infiniband/mlx5_2/ports/1/hw_counters/
# 关键计数器:
rx_read_requests # 入向 READ 请求数(MN 角度)
rx_write_requests # 入向 WRITE 请求数
rx_atomic_requests # 入向 atomic 请求数
out_of_buffer # 入向但没 recv buf 的次数(RNR)
duplicate_request # 收到重复请求(对端重传)
15.2 计数器读法
# 看每秒 IOPS
while true; do
cur=$(cat /sys/class/infiniband/mlx5_2/ports/1/hw_counters/rx_read_requests)
echo "$(date +%s) $cur"
sleep 1
done
差分两次值即可得到每秒 IOPS。
15.3 AdaptX Loop B 的信号源
AdaptX Loop B 就是用这套机制——MN publisher 每 5ms 读 hw_counters,把 NIC IOPS publish 到 RDMA MR slot,CN 拉取后用作 actuator 决策。
15.4 PCIe counter 与 NIC
PCIe 层的瓶颈也能监控:
# 通过 sysfs 看 PCIe link
lspci -vvv -s <bdf>
# Look for: LnkCap (capability) vs LnkSta (current state)
# 如果 LnkSta < LnkCap → 链路 degraded
15.5 perfquery 工具
# 看 IB port counters
perfquery -P 1 -a
# 关键:
# PortXmitData / PortRcvData (字节)
# PortXmitDiscards (链路丢包)
🌟 生产经验:任何”为什么我的 RDMA 慢”问题,先把 hw_counters / perfquery 跑一遍——80% 的 case 在这里能看出端倪。
16. ConnectX 代际演进:5 → 6 → 7
DM 事务论文经常提”我们用的是 ConnectX-6”——理解代际差异,你才知道为什么 LOTUS 在 ConnectX-5 上才必要、FORD 在 ConnectX-6 才能跑到接近线速。
| 代际 | 物理速率 | 单卡 atomic IOPS | 单 op 延迟 | 主要新特性 | 典型论文 |
|---|---|---|---|---|---|
| ConnectX-3 | 56 Gb/s | ~1 M | ~3 µs | 基本 RDMA | FaRM era |
| ConnectX-4 | 100 Gb/s | ~1 M | ~2.5 µs | 改进 cache | FaRM/早期 FORD 复现 |
| ConnectX-5 | 100 Gb/s | ~1.5 M | ~2 µs | 部分 atomic 优化 | LOTUS 论文背景 |
| ConnectX-6 / 6 Dx | 100/200 Gb/s | 5-10 M | ~1.5-2 µs | atomic 大幅优化、ATS、PCIe Gen4 | FORD/Motor 主要测试 |
| ConnectX-7 | 200/400 Gb/s | 估计 15-20 M | ~1-1.5 µs | PCIe Gen5、更大 SRAM | 2026+ 论文起步 |
| ConnectX-8 (ROADMAP) | 400/800 Gb/s | TBD | TBD | TBD | 早期采样 |
16.1 关键演进点
- ConnectX-5 → 6:atomic IOPS 5-10× 跃迁。这是 FORD 能跑到线速、AdaptX 5ms 反馈窗口可达的硬件前提
- ConnectX-6 → 7:PCIe Gen4 → Gen5 翻倍,带宽友好;atomic 进一步优化但比例小
- 每一代都加 SmartNIC 化:Bluefield-3 集成 ARM 核,部分逻辑可下放到 NIC
16.2 实战意义
读 RDMA 论文时,先看实验平台的 NIC 代际:
- ConnectX-5 上的实验数字 → atomic IOPS 1.5M 是上限,出现 hotspot 几乎是必然
- ConnectX-6 上的实验数字 → atomic IOPS 5-10M,但 hot key 上限仍是 ~1-2M(单 cache-line atomic 串行限制)
- ConnectX-7 上的实验数字 → 数据更宽松,但 hot key 上限改善有限
🌟 判断一篇论文的”加速”是否真有意义:看它是不是在 hot regime 上改善——cold regime 上的”接近线速”在 ConnectX-6 上几乎是 free lunch。
17. 工程踩坑清单:十大新人陷阱
按踩坑频率排序:
- 用了 mlx5_0 不是 mlx5_2:control 网 1Gbps,实验网 100Gbps。所有性能问题第一时间检查
ib_dev_id - IOMMU 没设 passthrough:RDMA READ 返回全 0 但 WC SUCCESS。
/etc/default/grub加iommu=pt+ reboot - MTU 太小:active_mtu 默认可能 256,带宽腰斩。改成 1024 或 4096
- MR 太大爆内存:
mr_size=64在 192GB 节点上 reg 不出来。先估准 - memcached 绑 127.0.0.1:CN 远程连不上。手动起
-l 0.0.0.0 - rkey 没在带外通道交换好:CN 不知道 MN 的 rkey 就发 op,触发 protection error
- CQ 不够大:默认 cq_size 太小,溢出后 op 丢。建 CQ 时给足大小(典型 65536)
- selective signaling 漏写最后一个 SIGNALED:unsignaled queue 满后 post_send 阻塞,看上去像死锁
- PD 跨进程共享假设错误:多进程要各自 ibv_alloc_pd,不能共享句柄
- 用 InfiniBand 模式跑 RoCE 网卡:
gid_idx设错(InfiniBand 用 0,RoCE v2 通常 1-3),连不通且报错信息晦涩
🌟 最重要的预防措施:新集群第一次跑前,把 §13 的 perftest 全过一遍——基础不通时跑应用是浪费时间。
✅ 自我检验清单
- 两层 bypass:能解释 RDMA 相对 TCP 的两个根本优势,以及哪个对 DM 系统更关键
- 四件套:能徒手画 QP/MR/CQ/PD 的关系
- NIC 微架构:能讲清 WQE / CQE / Doorbell 三者的关系
- QP 状态机:能默写 RESET → INIT → RTR → RTS 各阶段需要的字段
- 传输模式:能解释为什么 DM 事务必须用 RC 而不是 UC
- op 选择:面对一个新场景,能判断该用 READ / WRITE / CAS / send/recv
- atomic 实现:能讲清 chip-level vs memory-level atomic 的差异
- atomic-IOPS 上限:能讲清”单 cache-line atomic 串行”为什么 hot key 上限只 1-2M
- MR 注册成本:能讲清”为什么要启动时一次 reg 大块” + “为什么用 hugepage”
- doorbell batch:能算出 batch=8 相比 batch=1 的延迟和带宽收益
- selective signaling:能解释为什么不是每个 op 都要 signaled
- busy-poll vs 中断:能讲清两种模式的 trade-off 与适用场景
- RoCE 选型:能讲清在什么条件下 RoCE v2 不如 InfiniBand
- NCCL 调用栈:能从 ncclAllReduce 一路追到 ibv_post_send
- perftest 解读:看一份 ib_read_bw 输出能识别瓶颈
- WC 错误诊断:看到 IBV_WC_RETRY_EXC_ERR 能立即想到三个可能原因
- ConnectX 代际:能讲清为什么 LOTUS 在 ConnectX-5 上做”锁分离”特别有意义
- 十大坑:至少能默写其中 5 个并解释原因
📚 参考资料
概念入门
- RDMA Aware Networks Programming User Manual —— Mellanox/NVIDIA:官方手册 —— libibverbs 编程权威说明
- Anuj Kalia 个人主页 / CMU 系列论文 —— RDMA 系统研究综合入口
- The Linux Kernel RDMA Subsystem —— driver 视角的 RDMA(对硬件最透彻)
关键论文
- FaRM: Fast Remote Memory(Dragojević et al., NSDI’14):USENIX 链接 —— RDMA OCC 的奠基,所有后续 DM 事务系统的”祖宗”
- Design Guidelines for High Performance RDMA Systems(Kalia et al., USENIX ATC’16):USENIX 链接 —— one-sided/two-sided 选择的实证文献,入门必读 ⭐
- FaSST: Fast, Scalable and Simple Distributed Transactions(Kalia et al., OSDI’16):USENIX 链接 —— two-sided RPC 反例的开创工作
- Datacenter RDMA Networks(Mittal et al., SIGCOMM’18):RoCE v2 拥塞控制(DCQCN)实证
- RDMA over Commodity Ethernet at Scale(Guo et al., SIGCOMM’16):微软 RoCE v2 大规模部署经验
- HPCC: High Precision Congestion Control(Li et al., SIGCOMM’19):阿里超大规模 RDMA 拥塞控制
- Lessons from RDMA / RoCE Operational Experience(各类 NSDI / SIGCOMM tutorials) —— 工程视角
行业讨论
- Mellanox Bluefield SmartNIC 官方文档 —— SmartNIC 与 RDMA 的结合
- Microsoft Azure RDMA 工程博客 —— 大规模生产经验
- NVIDIA 开发者博客 RDMA 系列 —— 包括 GPU Direct、ConnectX 演进
- The Linux Kernel Mailing List(linux-rdma) —— driver 层问题的最权威讨论
框架文档
- libibverbs / RDMA Core:github.com/linux-rdma/rdma-core
- NCCL 官方文档:docs.nvidia.com/deeplearning/nccl
- perftest:github.com/linux-rdma/perftest
- DPDK + RDMA(rte_eal) 文档 —— 用户态高性能网络栈的另一选择
- OFED 用户手册 —— Mellanox 官方,详细 verbs API
硬件参考
- ConnectX-6 Dx Adapter Card Datasheet —— 实测的具体型号
- Mellanox mlx5 driver source(Linux kernel drivers/net/ethernet/mellanox/mlx5/) —— 看 doorbell / WQE 实际编码
下一章我们看 CXL 这条更年轻的路径,理解它如何把”远端”从网络压回总线。