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
2
3
4
5
6
7
8
9
10
11
Host A                                      Host B

CPU / GPU CPU

│ RDMA WRITE

RNIC A =================================> RNIC B

│ DMA

Remote Memory

假设 B 已经有一块内存:

1
0x70000000 ~ 0x70000fff

B 先把这块内存注册给 RNIC,并把:

1
2
remote_addr = 0x70000000
rkey = 0x123456

告诉 A。

之后 A 就可以发起:

1
RDMA WRITE

让自己的 RNIC 直接把数据写进:

1
B: 0x70000000

整个数据传输过程中:

1
B CPU 不需要调用 recv()

这就是所谓的 one-sided


单边和双边的区别

双边操作典型是:

1
SEND / RECV

要求:

1
2
3
4
5
A:
post_send()

B:
post_recv()

也就是说双方都要参与。

流程类似:

1
2
3
4
5
6
A SQ                        B RQ

SEND WQE -------------> RECV WQE


B Buffer

而单边:

1
2
3
A SQ                      B Memory

RDMA WRITE WQE ---------> 直接写入

B 不需要为这一条 WRITE 提前放一个 Receive WQE。

所以:

1
2
3
SEND/RECV = two-sided

READ/WRITE = one-sided

2. 什么是 QP?

QP 全称:

1
Queue Pair

也就是:

一对 Work Queue。

一个 QP 最基本包含:

1
2
3
4
5
6
7
8
9
          QP

┌─────────────────────┐
│ │
│ SQ RQ │
│ │
│ Send Queue Receive │
│ Queue │
└─────────────────────┘

即:

1
QP = SQ + RQ

其中:

SQ:Send Queue

放主动发起的任务。

例如:

1
2
3
4
SEND
RDMA WRITE
RDMA READ
Atomic

这些 WR 最终都会进入 SQ。


RQ:Receive Queue

主要服务于:

1
SEND / RECV

例如 B 希望接收 A 发来的 SEND:

B 需要提前:

1
post_recv()

放一个 Receive WQE 到:

1
B.RQ

3. CQ 是什么?

除了 QP,还通常会有:

1
2
CQ
Completion Queue

CQ 用来告诉软件:

某个 Work Request 完成了。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
A CPU

│ ibv_post_send()

A SQ



RNIC

│ RDMA WRITE

Network


B Memory

完成以后

A RNIC


A CQ


WC
Work Completion

CPU 可以:

1
ibv_poll_cq(...)

检查完成状态。

因此常见关系是:

1
2
3
4
5
6
7
8
9
        QP
┌──────────────┐
│ │
SQ RQ
│ │
└──────┬───────┘


CQ

严格来说:

1
CQ 不属于 QP

而是 QP 在创建时指定自己的:

1
2
send_cq
recv_cq

多个 QP 甚至可以共享一个 CQ。


4. 单边 RDMA 需要 RQ 吗?

这是一个非常重要的问题。

对于:

1
2
RDMA WRITE
RDMA READ

数据传输本身不需要远端 RQ。

例如:

1
2
3
4
5
6
7
8
9
A                                   B

SQ RQ
│ │
│ RDMA WRITE │
│ │
└───────────────> B memory │
X
不消费 Receive WQE

因此可以说:

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
2
3
4
5
6
7
8
9
10
11
12
Local:

SQ


RNIC


Remote Memory

+
CQ 返回完成结果

因此对于纯 RDMA WRITE:

1
2
3
4
A SQ
A CQ

B 注册好的 Memory Region

基本就足够完成数据传输了。

不过还是要注意:

1
2
RQ 是 QP 的一部分,
只是这次 RDMA WRITE 没有用它。

6. WR 和 WQE 是什么关系?

软件层面通常操作的是:

1
2
WR
Work Request

例如 libibverbs 中:

1
struct ibv_send_wr

应用程序构造 WR:

1
2
3
4
5
6
7
8
WR

│ ibv_post_send()

QP Send Queue


WQE

可以简单理解为:

1
2
3
WR = 软件告诉 RDMA 驱动"我要做什么"

WQE = RNIC 真正能够读取和执行的队列项

概念上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Application

│ WR

libibverbs / Driver


WQE


SQ


RNIC

7. 我要发一个 RDMA WRITE,WR 里要填什么?

假设:

1
A 要把自己的 buffer 写到 B 的 buffer。

基本需要两类信息:

1
2
3
本地信息
+
远端信息

7.1 本地信息

A 需要告诉 RNIC:

1
2
3
数据在哪?
数据多长?
这块内存是否有权限访问?

对应一个 SGE:

1
2
3
4
5
struct ibv_sge {
uint64_t addr;
uint32_t length;
uint32_t lkey;
};

也就是:

1
2
3
addr
length
lkey

例如:

1
2
3
4
5
A local buffer:

addr = 0x10000000
length = 4096
lkey = 0xaaaa

含义:

addr

1
本地数据起始地址

length

1
传多少字节

lkey

RNIC 用来验证:

1
这个 QP 是否有权限 DMA 访问这块本地内存

8. 为什么要有 lkey?

因为不能让 RNIC 随便 DMA 任意虚拟地址。

应用需要先:

1
ibv_reg_mr(...)

注册 Memory Region:

1
2
3
4
5
6
7
8
9
10
11
Memory

│ ibv_reg_mr

MR
┌───────────┐
│ addr │
│ length │
│ lkey │
│ rkey │
└───────────┘

其中:

1
lkey

主要用于本地 RNIC 访问本地 Memory Region。


9. RDMA WRITE 还需要远端信息

A 还必须知道:

1
2
B.remote_addr
B.rkey

WR 中类似:

1
2
wr.wr.rdma.remote_addr = remote_addr;
wr.wr.rdma.rkey = rkey;

因此:

1
2
3
4
5
6
7
8
9
10
11
12
RDMA WRITE WQE

┌─────────────────────────┐
│ opcode = RDMA_WRITE │
│ │
│ local_addr │
│ local_length │
│ local_lkey │
│ │
│ remote_addr │
│ remote_rkey │
└─────────────────────────┘

10. 还要填什么?

一个典型 Send WR 还会包含:

1
2
3
4
wr_id
opcode
send_flags
SGE

例如:

1
2
3
4
5
6
7
8
9
10
struct ibv_send_wr wr = {};

wr.wr_id = 123;
wr.opcode = IBV_WR_RDMA_WRITE;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.send_flags = IBV_SEND_SIGNALED;

wr.wr.rdma.remote_addr = remote_addr;
wr.wr.rdma.rkey = rkey;

因此可以总结成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WR

├── 做什么?
│ └── opcode

├── 本地数据在哪?
│ ├── local_addr
│ ├── length
│ └── lkey

├── 远端写到哪?
│ ├── remote_addr
│ └── rkey

├── 完成以后怎么识别?
│ └── wr_id

└── 是否产生 CQE?
└── send_flags

11. 接收方 B 要提供什么?

这是单边 RDMA 中最容易问到的问题。

对于 RDMA WRITE,B 最重要的是提供:

1
2
remote_addr
rkey

通常流程是:

1
2
3
4
5
6
7
8
9
10
11
12
B:

malloc buffer


ibv_reg_mr()


得到 MR

├── addr
└── rkey

然后:

1
2
B ----------------------> A
remote_addr + rkey

这种信息交换本身通常走:

1
2
3
4
TCP
RDMA SEND/RECV
RPC
控制面协议

而不是”凭空知道”。


12. B 要不要把 length 给 A?

从 RDMA WRITE WQE 本身来说:

A 写多少字节,是由 A 本地 SGE 中的 length 决定的。

因此 WQE 里的远端部分主要是:

1
2
remote_addr
rkey

没有一个:

1
remote_length

字段。

但是实际协议里,B 通常仍然需要告诉 A:

1
这块 buffer 有多大。

例如:

1
2
3
4
5
B 告诉 A:

remote_addr = 0x70000000
rkey = 1234
capacity = 4096

这样 A 才知道不能写:

1
8192 bytes

否则可能越界。

所以需要区分:

1
2
3
4
硬件 WQE 必须字段:

remote_addr
rkey

和:

1
2
3
软件协议通常也会交换:

buffer length / capacity

13. 把 WR 转成一个具体例子

假设:

1
2
3
4
5
6
7
8
9
A local buffer:

0x10000000
4 KB

B remote buffer:

0x70000000
4 KB

A 有:

1
2
3
local_addr = 0x10000000
length = 4096
lkey = 0x1111

B 给 A:

1
2
remote_addr = 0x70000000
rkey = 0x2222

于是 A 创建:

1
2
3
4
5
6
7
8
9
10
11
12
                   RDMA WRITE WR

┌────────────────────────────────────────┐
│ opcode = RDMA_WRITE │
│ │
│ local addr = 0x10000000 │
│ length = 4096 │
│ lkey = 0x1111 │
│ │
│ remote addr = 0x70000000 │
│ rkey = 0x2222 │
└────────────────────────────────────────┘

提交:

1
ibv_post_send(qp, &wr, &bad_wr);

14. RDMA WRITE 的完整流程

现在把 QP 加进来。

假设:

1
2
A = Initiator
B = Target

Step 0:两边先建立 QP

两边都创建:

1
2
QP_A
QP_B

对于 RC:

1
Reliable Connected

需要建立:

1
QP_A <------------> QP_B

连接关系。

例如会交换:

1
2
3
QPN
PSN
LID / GID

等连接信息。


Step 1:B 注册远端内存

1
2
3
4
5
6
7
8
9
10
11
12
13
B CPU


malloc()


Buffer


ibv_reg_mr()


MR

得到:

1
2
addr = 0x70000000
rkey = 0x2222

Step 2:B 把远端信息告诉 A

控制面:

1
2
3
4
5
6
B                         A

remote_addr
rkey
buffer size
-------------------->

至此 A 知道:

1
我要往 B 的哪里写。

15. Step 3:A 创建并提交 RDMA WRITE WR

A 构造:

1
2
3
4
5
6
7
8
9
10
WR

opcode = RDMA_WRITE

local_addr
length
lkey

remote_addr
rkey

然后:

1
ibv_post_send(qp_A, &wr, ...);

逻辑上:

1
2
3
4
5
6
7
8
A Application

│ ibv_post_send()

QP_A.SQ

│ WQE

WQE 被放进:

1
QP_A 的 Send Queue

16. Step 4:Doorbell

仅仅把 WQE 写进 SQ 还不够。

需要告诉 RNIC:

SQ 的 producer 状态更新了,有新的 WQE 可以执行。

于是 CPU 或 GPU 会:

1
ring doorbell

例如:

1
2
3
4
5
CPU

│ write doorbell

RNIC

RNIC 于是知道:

1
QP_A.SQ 中出现了新的任务。

17. Step 5:A RNIC 获取 WQE

1
2
3
4
5
6
7
A RNIC


读取 QP_A.SQ


解析 WQE

发现:

1
opcode = RDMA_WRITE

并解析:

1
2
3
4
5
6
local_addr
length
lkey

remote_addr
rkey

18. Step 6:A RNIC DMA 读取本地内存

A RNIC 根据:

1
local_addr + lkey

访问:

1
A Memory

例如:

1
2
3
4
5
6
A Memory
0x10000000

│ PCIe DMA READ

A RNIC

也就是说:

1
CPU 不负责 memcpy

数据由 RNIC DMA 取得。


19. Step 7:通过网络发给 B

RNIC 将数据封装成 RDMA 网络包:

1
2
3
4
5
6
7
8
A RNIC

│ RDMA WRITE packets

Network


B RNIC

包里会包含足够的信息,让 B RNIC 找到目标 MR,例如:

1
2
3
remote virtual address
rkey
payload

20. Step 8:B RNIC 验证 rkey

B RNIC 收到以后:

1
2
3
4
5
6
7
B RNIC

├── 检查 rkey

├── 检查访问权限

└── 检查地址是否合法

例如确认:

1
0x70000000

确实属于对应 MR。


21. Step 9:B RNIC DMA 写入 B 内存

验证成功:

1
2
3
4
5
6
7
B RNIC

│ PCIe DMA WRITE

B Memory

0x70000000

于是:

1
2
3
4
5
6
7
8
9
A buffer
0x10000000


│ RDMA WRITE


B buffer
0x70000000

整个数据面过程中:

1
2
B CPU 没有调用 recv()
B RQ 没有消费 WQE

22. Step 10:完成通知

对于可靠连接 RC,远端 RNIC 会按照协议完成可靠传输确认。

最终 A RNIC 可以在:

1
A CQ

中生成一个 CQE。

然后 A:

1
ibv_poll_cq(...)

得到:

1
Work Completion

于是知道:

1
这条 RDMA WRITE 已经完成。

完整路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
              Host A                              Host B

Application

│ post_send

QP_A.SQ

│ WQE

RNIC A

│ DMA READ

A Memory


└────────────┐


RNIC A

│ RDMA WRITE

=================│================================ Network


RNIC B

│ check rkey
│ DMA WRITE

B Memory


RNIC A


A CQ


Work Completion

注意:

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
2
3
4
5
PUT

最终底层实现

RDMA WRITE

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Application

put(dst_remote, src_local, size)




Runtime / Communication Library




ibv_post_send()

opcode = IBV_WR_RDMA_WRITE




QP.SQ

所以如果讨论的是普通 RDMA/InfiniBand:

PUT 通常不是另一种硬件传输协议,而是上层 API 对”把数据写到远端”这一动作的称呼,底层通常映射为 RDMA WRITE。


24. RDMA WRITE 和 PUT 的 Step 3 分别是什么?

如果老师把:

1
WRITE

和:

1
PUT

分开讲,通常可以这样理解。


RDMA WRITE 的 Step 3

用户直接构造 RDMA WRITE WR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Application

│ create WR


opcode = RDMA_WRITE

local_addr
length
lkey

remote_addr
rkey


│ post_send


QP.SQ

即:

发起方直接把一个 RDMA WRITE WQE 放入本地 QP 的 SQ,随后 ring doorbell,RNIC 消费该 WQE。


PUT 的 Step 3

如果 PUT 是一个上层 API:

1
put(remote_addr, local_addr, size)

那么 runtime 首先需要:

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT


查找远端地址对应信息

├── remote_addr
└── rkey


构造 RDMA WRITE WR


post 到 QP.SQ

之后:

1
PUT 和 RDMA WRITE 的底层流程基本相同。

也就是说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Application PUT


Runtime

│ translate

RDMA WRITE WR


QP.SQ


RNIC

25. 从 QP 角度比较 WRITE 和 PUT

可以画成:

1
2
3
4
5
6
7
8
9
10
11
12
13
RDMA WRITE:

Application

│ ibv_post_send(RDMA_WRITE)

QP.SQ


RNIC


Remote Memory

而 PUT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Application

│ put()

Runtime / Library

│ convert to RDMA WRITE

QP.SQ


RNIC


Remote Memory

核心区别通常只在:

1
API / programming abstraction

而不是网络数据路径。


26. 单边 WRITE 时,两端 QP 分别在干什么?

这个问题值得单独说明。

假设:

1
QP_A <======== RC connection ========> QP_B

A 发 WRITE:

1
2
3
4
5
QP_A.SQ

│ RDMA WRITE WQE

RNIC A

但是 B:

1
QP_B.RQ

没有 Receive WQE 被消费。

可以理解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
             RC QP Connection

A QP B QP

┌───────────────┐ ┌───────────────┐
│ │ │ │
│ SQ │ │ SQ │
│ │ │ │
│ WRITE WQE ----│--------------->│ RNIC handling │
│ │ │ │
│ RQ │ │ RQ │
│ │ │ │
│ │ │ unused │
└───────────────┘ └───────────────┘


B Memory

这里:

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
2
3
4
5
6
7
8
A                                 B

QP_A.SQ QP_B.RQ

SEND WQE -----------------------> RECV WQE


Buffer

RDMA WRITE

A 明确知道:

1
2
B.remote_addr
B.rkey

因此:

1
2
3
4
5
6
7
8
9
A                                 B

QP_A.SQ

RDMA WRITE WQE ----------------> registered MR
0x70000000

QP_B.RQ
X

所以二者最大的区别可以理解为:

1
2
3
4
5
6
7
SEND:

"B,你找一块提前准备好的 receive buffer 接住它。"

RDMA WRITE:

"B,我已经知道地址了,我直接写到 0x70000000。"

28. 最后总结

什么叫单边 RDMA?

单边 RDMA 是:

发起方 RNIC 可以直接访问远端已经注册并授权的内存,远端 CPU 不需要参与每一次数据搬运。

典型操作:

1
2
3
RDMA READ
RDMA WRITE
Atomic

什么是 QP?

1
QP = SQ + RQ

SQ:

1
2
3
4
5
主动发送任务
SEND
READ
WRITE
Atomic

RQ:

1
主要用于接收 SEND

单边 READ/WRITE:

1
不需要远端提前 post_recv()

因此:

1
远端 RQ 不参与数据搬运。

一个 RDMA WRITE WR 要有什么?

最核心的信息:

1
2
3
4
5
6
7
8
9
10
11
12
                 RDMA WRITE WR

Local Remote

local_addr remote_addr
length rkey
lkey

+
opcode
wr_id
send_flags

即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
我要干什么?
WRITE

从哪里读取?
local_addr + lkey

读多少?
length

写到哪里?
remote_addr

有没有权限?
rkey

接收方要提供什么?

最核心:

1
2
remote_addr
rkey

实际系统通常还会告诉:

1
buffer capacity

这些信息一般通过控制面提前交换。


RDMA WRITE 完整数据路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
A Application

│ WR

A QP.SQ

│ WQE

Doorbell


A RNIC

│ DMA read

A Memory


A RNIC

│ network

B RNIC

│ check rkey
│ DMA write

B Memory

B CPU 不参与
B RQ 不参与

WRITE 和 PUT

在 InfiniBand Verbs 中:

1
RDMA WRITE

是真正的硬件 Work Request 类型。

而:

1
PUT

通常是上层编程模型中的语义:

1
2
3
4
5
6
7
8
9
10
11
PUT

runtime

RDMA WRITE WR

QP.SQ

RNIC

Remote Memory

因此大多数情况下:

PUT 是”我要把这段数据放到远端”的 API 语义,而 RDMA WRITE 是底层真正执行这个动作的 RDMA Verb。