RDMA 单边操作:QP、WR/WQE 与 RDMA WRITE / PUT 流程
RDMA 单边操作:QP、WR/WQE 与 RDMA WRITE / PUT 流程
1. 什么叫单边 RDMA?
单边 RDMA(One-sided RDMA) 指的是:
发起方可以直接读写远端机器已经注册好的内存,而远端 CPU 不需要为这次数据传输主动执行
recv()、read()或write()。
典型的单边操作有:
RDMA WRITE:把本地内存的数据写到远端内存RDMA READ:把远端内存的数据读到本地内存- Atomic:远端原子操作,例如 Compare-and-Swap、Fetch-and-Add
例如:
1 | Host A Host B |
假设 B 已经有一块内存:
1 | 0x70000000 ~ 0x70000fff |
B 先把这块内存注册给 RNIC,并把:
1 | remote_addr = 0x70000000 |
告诉 A。
之后 A 就可以发起:
1 | RDMA WRITE |
让自己的 RNIC 直接把数据写进:
1 | B: 0x70000000 |
整个数据传输过程中:
1 | B CPU 不需要调用 recv() |
这就是所谓的 one-sided。
单边和双边的区别
双边操作典型是:
1 | SEND / RECV |
要求:
1 | A: |
也就是说双方都要参与。
流程类似:
1 | A SQ B RQ |
而单边:
1 | A SQ B Memory |
B 不需要为这一条 WRITE 提前放一个 Receive WQE。
所以:
1 | SEND/RECV = two-sided |
2. 什么是 QP?
QP 全称:
1 | Queue Pair |
也就是:
一对 Work Queue。
一个 QP 最基本包含:
1 | QP |
即:
1 | QP = SQ + RQ |
其中:
SQ:Send Queue
放主动发起的任务。
例如:
1 | SEND |
这些 WR 最终都会进入 SQ。
RQ:Receive Queue
主要服务于:
1 | SEND / RECV |
例如 B 希望接收 A 发来的 SEND:
B 需要提前:
1 | post_recv() |
放一个 Receive WQE 到:
1 | B.RQ |
3. CQ 是什么?
除了 QP,还通常会有:
1 | CQ |
CQ 用来告诉软件:
某个 Work Request 完成了。
例如:
1 | A CPU |
CPU 可以:
1 | ibv_poll_cq(...) |
检查完成状态。
因此常见关系是:
1 | QP |
严格来说:
1 | CQ 不属于 QP |
而是 QP 在创建时指定自己的:
1 | send_cq |
多个 QP 甚至可以共享一个 CQ。
4. 单边 RDMA 需要 RQ 吗?
这是一个非常重要的问题。
对于:
1 | RDMA WRITE |
数据传输本身不需要远端 RQ。
例如:
1 | A B |
因此可以说:
RDMA WRITE / READ 不依赖远端 RQ 中的 Receive WQE。
但不能简单地说:
1 | 单边 RDMA 的 QP 没有 RQ |
因为从 QP 结构上:
1 | QP = SQ + RQ |
RQ 仍然是 QP 的组成部分。
只是在纯单边通信场景下:
1 | RQ 可以基本不用。 |
所以更准确的说法是:
单边 RDMA 操作仍然通过 QP 发起,但 RDMA READ/WRITE 不需要远端提前 post receive,因此远端 RQ 不参与这次数据搬运。
5. 单边 RDMA 是不是只需要 SQ + CQ?
从操作路径来看,可以近似理解成:
1 | Local: |
因此对于纯 RDMA WRITE:
1 | A SQ |
基本就足够完成数据传输了。
不过还是要注意:
1 | RQ 是 QP 的一部分, |
6. WR 和 WQE 是什么关系?
软件层面通常操作的是:
1 | WR |
例如 libibverbs 中:
1 | struct ibv_send_wr |
应用程序构造 WR:
1 | WR |
可以简单理解为:
1 | WR = 软件告诉 RDMA 驱动"我要做什么" |
概念上:
1 | Application |
7. 我要发一个 RDMA WRITE,WR 里要填什么?
假设:
1 | A 要把自己的 buffer 写到 B 的 buffer。 |
基本需要两类信息:
1 | 本地信息 |
7.1 本地信息
A 需要告诉 RNIC:
1 | 数据在哪? |
对应一个 SGE:
1 | struct ibv_sge { |
也就是:
1 | addr |
例如:
1 | A local buffer: |
含义:
addr
1 | 本地数据起始地址 |
length
1 | 传多少字节 |
lkey
RNIC 用来验证:
1 | 这个 QP 是否有权限 DMA 访问这块本地内存 |
8. 为什么要有 lkey?
因为不能让 RNIC 随便 DMA 任意虚拟地址。
应用需要先:
1 | ibv_reg_mr(...) |
注册 Memory Region:
1 | Memory |
其中:
1 | lkey |
主要用于本地 RNIC 访问本地 Memory Region。
9. RDMA WRITE 还需要远端信息
A 还必须知道:
1 | B.remote_addr |
WR 中类似:
1 | wr.wr.rdma.remote_addr = remote_addr; |
因此:
1 | RDMA WRITE WQE |
10. 还要填什么?
一个典型 Send WR 还会包含:
1 | wr_id |
例如:
1 | struct ibv_send_wr wr = {}; |
因此可以总结成:
1 | WR |
11. 接收方 B 要提供什么?
这是单边 RDMA 中最容易问到的问题。
对于 RDMA WRITE,B 最重要的是提供:
1 | remote_addr |
通常流程是:
1 | B: |
然后:
1 | B ----------------------> A |
这种信息交换本身通常走:
1 | TCP |
而不是”凭空知道”。
12. B 要不要把 length 给 A?
从 RDMA WRITE WQE 本身来说:
A 写多少字节,是由 A 本地 SGE 中的
length决定的。
因此 WQE 里的远端部分主要是:
1 | remote_addr |
没有一个:
1 | remote_length |
字段。
但是实际协议里,B 通常仍然需要告诉 A:
1 | 这块 buffer 有多大。 |
例如:
1 | B 告诉 A: |
这样 A 才知道不能写:
1 | 8192 bytes |
否则可能越界。
所以需要区分:
1 | 硬件 WQE 必须字段: |
和:
1 | 软件协议通常也会交换: |
13. 把 WR 转成一个具体例子
假设:
1 | A local buffer: |
A 有:
1 | local_addr = 0x10000000 |
B 给 A:
1 | remote_addr = 0x70000000 |
于是 A 创建:
1 | RDMA WRITE WR |
提交:
1 | ibv_post_send(qp, &wr, &bad_wr); |
14. RDMA WRITE 的完整流程
现在把 QP 加进来。
假设:
1 | A = Initiator |
Step 0:两边先建立 QP
两边都创建:
1 | QP_A |
对于 RC:
1 | Reliable Connected |
需要建立:
1 | QP_A <------------> QP_B |
连接关系。
例如会交换:
1 | QPN |
等连接信息。
Step 1:B 注册远端内存
1 | B CPU |
得到:
1 | addr = 0x70000000 |
Step 2:B 把远端信息告诉 A
控制面:
1 | B A |
至此 A 知道:
1 | 我要往 B 的哪里写。 |
15. Step 3:A 创建并提交 RDMA WRITE WR
A 构造:
1 | WR |
然后:
1 | ibv_post_send(qp_A, &wr, ...); |
逻辑上:
1 | A Application |
WQE 被放进:
1 | QP_A 的 Send Queue |
16. Step 4:Doorbell
仅仅把 WQE 写进 SQ 还不够。
需要告诉 RNIC:
SQ 的 producer 状态更新了,有新的 WQE 可以执行。
于是 CPU 或 GPU 会:
1 | ring doorbell |
例如:
1 | CPU |
RNIC 于是知道:
1 | QP_A.SQ 中出现了新的任务。 |
17. Step 5:A RNIC 获取 WQE
1 | A RNIC |
发现:
1 | opcode = RDMA_WRITE |
并解析:
1 | local_addr |
18. Step 6:A RNIC DMA 读取本地内存
A RNIC 根据:
1 | local_addr + lkey |
访问:
1 | A Memory |
例如:
1 | A Memory |
也就是说:
1 | CPU 不负责 memcpy |
数据由 RNIC DMA 取得。
19. Step 7:通过网络发给 B
RNIC 将数据封装成 RDMA 网络包:
1 | A RNIC |
包里会包含足够的信息,让 B RNIC 找到目标 MR,例如:
1 | remote virtual address |
20. Step 8:B RNIC 验证 rkey
B RNIC 收到以后:
1 | B RNIC |
例如确认:
1 | 0x70000000 |
确实属于对应 MR。
21. Step 9:B RNIC DMA 写入 B 内存
验证成功:
1 | B RNIC |
于是:
1 | A buffer |
整个数据面过程中:
1 | B CPU 没有调用 recv() |
22. Step 10:完成通知
对于可靠连接 RC,远端 RNIC 会按照协议完成可靠传输确认。
最终 A RNIC 可以在:
1 | A CQ |
中生成一个 CQE。
然后 A:
1 | ibv_poll_cq(...) |
得到:
1 | Work Completion |
于是知道:
1 | 这条 RDMA WRITE 已经完成。 |
完整路径:
1 | Host A Host B |
注意:
1 | B.RQ 完全没有出现在数据路径中。 |
23. 那么 PUT 是什么?
这里需要特别区分。
在标准 InfiniBand Verbs 中,通常使用的术语是:
1 | IBV_WR_RDMA_WRITE |
也就是:
1 | RDMA WRITE |
Verbs 本身并没有一个独立的 IBV_WR_PUT 操作。
但是很多上层通信模型中会使用:
1 | PUT |
这个词。
PUT 的语义通常是:
把本地数据放到远端指定地址。
也就是:
1 | PUT(local_buffer, remote_buffer) |
从语义来看:
1 | PUT ≈ remote write |
因此很多 RDMA 系统中:
1 | PUT |
例如:
1 | Application |
所以如果讨论的是普通 RDMA/InfiniBand:
PUT 通常不是另一种硬件传输协议,而是上层 API 对”把数据写到远端”这一动作的称呼,底层通常映射为 RDMA WRITE。
24. RDMA WRITE 和 PUT 的 Step 3 分别是什么?
如果老师把:
1 | WRITE |
和:
1 | PUT |
分开讲,通常可以这样理解。
RDMA WRITE 的 Step 3
用户直接构造 RDMA WRITE WR:
1 | Application |
即:
发起方直接把一个
RDMA WRITE WQE放入本地 QP 的 SQ,随后 ring doorbell,RNIC 消费该 WQE。
PUT 的 Step 3
如果 PUT 是一个上层 API:
1 | put(remote_addr, local_addr, size) |
那么 runtime 首先需要:
1 | PUT |
之后:
1 | PUT 和 RDMA WRITE 的底层流程基本相同。 |
也就是说:
1 | Application PUT |
25. 从 QP 角度比较 WRITE 和 PUT
可以画成:
1 | RDMA WRITE: |
而 PUT:
1 | Application |
核心区别通常只在:
1 | API / programming abstraction |
而不是网络数据路径。
26. 单边 WRITE 时,两端 QP 分别在干什么?
这个问题值得单独说明。
假设:
1 | QP_A <======== RC connection ========> QP_B |
A 发 WRITE:
1 | QP_A.SQ |
但是 B:
1 | QP_B.RQ |
没有 Receive WQE 被消费。
可以理解为:
1 | RC QP Connection |
这里:
1 | B QP |
依然参与 RC 连接的协议状态,例如可靠性、packet sequence 等。
但:
1 | B.RQ |
不负责提供目标 buffer。
目标 buffer 来自:
1 | remote_addr + rkey |
27. 这和 SEND / RECV 最大区别在哪里?
SEND
A 只需要知道:
1 | 我要发送 local buffer |
并不知道:
1 | 最终写到 B 的哪个具体虚拟地址 |
B 必须提前:
1 | post_recv() |
例如:
1 | A B |
RDMA WRITE
A 明确知道:
1 | B.remote_addr |
因此:
1 | A B |
所以二者最大的区别可以理解为:
1 | SEND: |
28. 最后总结
什么叫单边 RDMA?
单边 RDMA 是:
发起方 RNIC 可以直接访问远端已经注册并授权的内存,远端 CPU 不需要参与每一次数据搬运。
典型操作:
1 | RDMA READ |
什么是 QP?
1 | QP = SQ + RQ |
SQ:
1 | 主动发送任务 |
RQ:
1 | 主要用于接收 SEND |
单边 READ/WRITE:
1 | 不需要远端提前 post_recv() |
因此:
1 | 远端 RQ 不参与数据搬运。 |
一个 RDMA WRITE WR 要有什么?
最核心的信息:
1 | RDMA WRITE WR |
即:
1 | 我要干什么? |
接收方要提供什么?
最核心:
1 | remote_addr |
实际系统通常还会告诉:
1 | buffer capacity |
这些信息一般通过控制面提前交换。
RDMA WRITE 完整数据路径
1 | A Application |
WRITE 和 PUT
在 InfiniBand Verbs 中:
1 | RDMA WRITE |
是真正的硬件 Work Request 类型。
而:
1 | PUT |
通常是上层编程模型中的语义:
1 | PUT |
因此大多数情况下:
PUT 是”我要把这段数据放到远端”的 API 语义,而 RDMA WRITE 是底层真正执行这个动作的 RDMA Verb。

