学习 RDMA 的基本知识。
基本概念
一些基础概念:
PD
用来实现隔离性的。不能跨 PD 访问。CQ
QP
在创建 QP 的时候,CQ 和 PD 会关联起来。1
2
3
4
5
6
7
8
9
10
11ibv_context:代表一块 RDMA 设备
│
├── PD A
│ ├── MR A
│ └── QP A ─────┐
│ ├── CQ X
├── PD B │
│ ├── MR B │
│ └── QP B ─────┘
│
└── CQ XQPN
QP 的编号Port
HCA上的物理或逻辑端口编号GID
表示 RoCE 的 IP/网络地址LID
通常为 0
一些实现:
- InfiniBand 可以理解为很屌的专有实现
- RoCE 在以太网上的实现
RDMA Write 的基本链路
A 通过 rdma 往 B 写操作的流程,A 大概是:
- 通过 ibv_get_device_list、ibv_open_device、ibv_free_device_list 打开设备。
- 通过 ibv_query_gid 获得 gid。
- 通过 ibv_alloc_pd、ibv_create_cq 创建 pd 和 cq。
- 通过 posix_memalign 分配 a_buffer,并把它注册到 pd 中得到一个 mr。
- 创建一个 ibv_create_qp,需要指定一个 ibv_qp_init_attr。里面的 max_send_wr 指定了发送队列的深度。
- 调用 ibv_modify_qp,让 A 的 QP 从 RESET 到 INIT。
- 创建一个控制面的 fd 和套接口,链接到 B,把 A 的 qp 的 qp_num、a_psn、gid_text 发送过去。
- 从控制面的 fd 去读 B 的 qpn、psn。gid_text、buffer 的地址和 rkey。
- 用这些信息,设置 A 的 ibv_qp_attr,调用 ibv_modify_qp,把状态设置为 RTR。
- 用 ibv_modify_qp 把 A 的状态再设置为 RTS。
- 从控制面等待 B 也是设置好自己的 QP。
- 构造一个 ibv_sge,描述要发送的 buffer。构造一个 ibv_send_wr 封装这个 sge,然后设置 wr.rdma 字段,就是 b 那边的 address 和 rkey。
- 调用 ibv_post_send 发送。
- 使用一个循环,不停调用 ibv_poll_cq 去查询发送的情况。
- 清理:
- 关闭控制面 fd
- 销毁 qp、mr、buffer、cq、pd、context。
B 大概是:
- 和 A 一样。
- 启动控制面,listen 一个地址。
- 在 A 链接之后,得到 control_fd。
- 先从 control_fd 获得 A 那边的信息。
- 然后把自己这边的额信息发送过去。
- 用 ibv_modify_qp 切换到 RTR 状态。
- 用 ibv_modify_qp 切换到 RTS 状态。
- 告诉 A 自己已经 READY 了。
- 从控制面直到 A 已经完成了传输。
- 调用 __sync_synchronize
- 发送 OK 给 A。后面就销毁掉。
控制面的交互大概是:
B 启动 TCP Server
1
2B: listen()
B: accept()A 连接 B
1
A: connect()
A 把自己的 QP 信息发给 B
1
2A -> B:
<a_qpn> <a_psn> <a_gid>B 把自己的 QP 信息和 MR 授权发给 A
1
2B -> A:
<b_qpn> <b_psn> <b_gid> <b_buffer_address> <b_rkey>A 和 B 分别设置自己的 QP
1
RESET -> INIT -> RTR -> RTS
B 告诉 A,B 已经准备完成
1
2B -> A:
READYA 执行一次 RDMA Write
1
A buffer --RDMA Write--> B buffer
A 使用:
- 本地 a_buffer、length、a_lkey
- 远端 b_buffer_address、b_rkey,目的是能够获得授权写入 B buffer
A 轮询 CQ,确认 RDMA Write 完成,然后通知 B
1
2A -> B:
DONEB 检查自己的 buffer,验证成功后回复 A
1
2B -> A:
OKA 和 B 关闭 TCP 连接并清理 RDMA 资源
注意,READY、DONE、OK 是这个 demo 自己定义的控制消息,不是 verbs API。原因是这里是单边语义,B 的 CPU 不参与复制,所以需要控制面协助通知 B 是否已经完成。
KVCache 的传输用什么语义比较好?单边语义的有几个好处:
- 数据直接进入目标 KV Cache 地址,不需要中间接收 buffer。
- 目标端不需要为每个数据块调用 ibv_post_recv()。
- 目标端不需要持续处理 Receive CQ、补充 Receive WR。
- 不会因为 Receive Queue 没有槽位而发生 RNR。
- 源端可以把大量 TransferEntry 批量转成 RDMA Write WR。
- 目标 CPU 不参与每一次数据搬运,适合大块 GPU/Host 内存复制。