RDMA 学习

学习 RDMA 的基本知识。

基本概念

一些基础概念:

  • PD
    用来实现隔离性的。不能跨 PD 访问。

  • CQ

  • QP
    在创建 QP 的时候,CQ 和 PD 会关联起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ibv_context:代表一块 RDMA 设备

    ├── PD A
    │ ├── MR A
    │ └── QP A ─────┐
    │ ├── CQ X
    ├── PD B │
    │ ├── MR B │
    │ └── QP B ─────┘

    └── CQ X
  • QPN
    QP 的编号

  • Port
    HCA上的物理或逻辑端口编号

  • GID
    表示 RoCE 的 IP/网络地址

  • LID
    通常为 0

一些实现:

  • InfiniBand 可以理解为很屌的专有实现
  • RoCE 在以太网上的实现

RDMA Write 的基本链路

A 通过 rdma 往 B 写操作的流程,A 大概是:

  1. 通过 ibv_get_device_list、ibv_open_device、ibv_free_device_list 打开设备。
  2. 通过 ibv_query_gid 获得 gid。
  3. 通过 ibv_alloc_pd、ibv_create_cq 创建 pd 和 cq。
  4. 通过 posix_memalign 分配 a_buffer,并把它注册到 pd 中得到一个 mr。
  5. 创建一个 ibv_create_qp,需要指定一个 ibv_qp_init_attr。里面的 max_send_wr 指定了发送队列的深度。
  6. 调用 ibv_modify_qp,让 A 的 QP 从 RESET 到 INIT。
  7. 创建一个控制面的 fd 和套接口,链接到 B,把 A 的 qp 的 qp_num、a_psn、gid_text 发送过去。
  8. 从控制面的 fd 去读 B 的 qpn、psn。gid_text、buffer 的地址和 rkey。
  9. 用这些信息,设置 A 的 ibv_qp_attr,调用 ibv_modify_qp,把状态设置为 RTR。
  10. 用 ibv_modify_qp 把 A 的状态再设置为 RTS。
  11. 从控制面等待 B 也是设置好自己的 QP。
  12. 构造一个 ibv_sge,描述要发送的 buffer。构造一个 ibv_send_wr 封装这个 sge,然后设置 wr.rdma 字段,就是 b 那边的 address 和 rkey。
  13. 调用 ibv_post_send 发送。
  14. 使用一个循环,不停调用 ibv_poll_cq 去查询发送的情况。
  15. 清理:
    1. 关闭控制面 fd
    2. 销毁 qp、mr、buffer、cq、pd、context。

B 大概是:

  1. 和 A 一样。
  2. 启动控制面,listen 一个地址。
  3. 在 A 链接之后,得到 control_fd。
  4. 先从 control_fd 获得 A 那边的信息。
  5. 然后把自己这边的额信息发送过去。
  6. 用 ibv_modify_qp 切换到 RTR 状态。
  7. 用 ibv_modify_qp 切换到 RTS 状态。
  8. 告诉 A 自己已经 READY 了。
  9. 从控制面直到 A 已经完成了传输。
  10. 调用 __sync_synchronize
  11. 发送 OK 给 A。后面就销毁掉。

控制面的交互大概是:

  1. B 启动 TCP Server

    1
    2
    B: listen()
    B: accept()
  2. A 连接 B

    1
    A: connect()
  3. A 把自己的 QP 信息发给 B

    1
    2
    A -> B:
    <a_qpn> <a_psn> <a_gid>
  4. B 把自己的 QP 信息和 MR 授权发给 A

    1
    2
    B -> A:
    <b_qpn> <b_psn> <b_gid> <b_buffer_address> <b_rkey>
  5. A 和 B 分别设置自己的 QP

    1
    RESET -> INIT -> RTR -> RTS
  6. B 告诉 A,B 已经准备完成

    1
    2
    B -> A:
    READY
  7. A 执行一次 RDMA Write

    1
    A buffer --RDMA Write--> B buffer

    A 使用:

    • 本地 a_buffer、length、a_lkey
    • 远端 b_buffer_address、b_rkey,目的是能够获得授权写入 B buffer
  8. A 轮询 CQ,确认 RDMA Write 完成,然后通知 B

    1
    2
    A -> B:
    DONE
  9. B 检查自己的 buffer,验证成功后回复 A

    1
    2
    B -> A:
    OK
  10. A 和 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 内存复制。

SEDN/RECV

Reference