RDMA WITH GPU MEMORY VIA DMA-BUF

Jianxin Xiong, Intel Corporation

目录

RDMA 概述

RDMA与系统内存

  • RDMA 是 “DMA + 网络”

    • RDMA 操作结合了发起方(Initiator)和目标方(Target)的 DMA 操作。
      • Write 操作: 发起方进行 DMA 读,目标方进行 DMA 写。
      • Read 操作: 发起方进行 DMA 写,目标方进行 DMA 读。
  • DMA 需要对内存进行正确的设置

    • 内存页(Memory pages)需要被“钉住”(pinned),以防止被交换到磁盘。
    • 使用总线地址(Bus addresses)进行寻址。
    • 这些设置通常在“内存注册”(memory registration)时完成。
    • 对于系统内存中的用户空间缓冲区,通常涉及以下内核函数调用:
      • get_user_pages()
      • sg_alloc_table() / sg_set_page() / sg_next() / ...
      • dma_map_sg()

Page 3: RDMA与系统内存工作流程图
Page 3: RDMA与系统内存工作流程图

RDMA与GPU内存

  • GPU 内存是本地(local)的

    • 网络接口控制器(NIC)驱动无法直接“钉住”GPU内存。
    • NIC驱动不知道GPU内存的DMA地址。
  • 需要 NIC 驱动和 GPU 驱动之间的协作

  • Mellanox 的 Peer-Direct 方案

    • 这是为内核 RDMA 核心设计的插件接口。
    • 每个 GPU 驱动提供一个插件模块。
    • 当内存被注册时,系统会逐一查询这些插件,直到某个插件声明对该内存的所有权。
    • 该方案仅在 Mellanox OFED (MOFED) 中可用。
  • 我们能否有一个非专有的上游解决方案?

    • 我们的提议是使用 dma-buf。

Page 4: RDMA与GPU内存协作示意图
Page 4: RDMA与GPU内存协作示意图

DMA-BUF 概述

Dma-buf 是 Linux 内核中的一种标准机制,用于在不同设备驱动程序之间共享缓冲区。

其工作流程如下:
* 导出方 (Exporter): 拥有内存分配的驱动程序。它创建一个 dma-buf 对象并导出一个文件描述符(fd)。
* 导入方 (Importer): 需要访问该内存的驱动程序。它通过文件描述符获取 dma-buf 对象的引用,将其附加(attach)到自己的设备上,并将其映射(map)为设备可访问的DMA地址。

Page 6: DMA-BUF 概述图
Page 6: DMA-BUF 概述图

DMA-BUF API (导出方)

  • 创建一个新的 dma-buf 对象
    使用 dma_buf_export() 函数,并传入 dma_buf_export_info 结构体。该结构体包含 dma_buf_ops,定义了对该缓冲区的操作。
struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info);

struct dma_buf_export_info {
    const char *exp_name;
    struct module *owner;
    const struct dma_buf_ops *ops; // 操作函数集
    size_t size;
    int flags;
    struct dma_resv *resv;
    void *priv;
};

dma_buf_ops 结构体中定义了多种回调函数,其中加粗的为强制实现项:

struct dma_buf_ops {
                                   /* 加粗为强制实现项 */
    bool cache_sgt_mapping;
    bool dynamic_mapping;
    int (*attach)(struct dma_buf *, struct dma_buf_attachment *);
    void (*detach)(struct dma_buf *, struct dma_buf_attachment *);
    struct sg_table * (*map_dma_buf)(struct dma_buf_attachment *, enum dma_data_direction);
    void (*unmap_dma_buf)(struct dma_buf_attachment *, struct sg_table *, enum dma_data_direction);
    void (*release)(struct dma_buf *);
    int (*begin_cpu_access)(struct dma_buf *, enum dma_data_direction);
    int (*end_cpu_access)(struct dma_buf *, enum dma_data_direction);
    int (*mmap)(struct dma_buf *, struct vm_area_struct *vma);
    void *(*map)(struct dma_buf *, unsigned long);
    void (*unmap)(struct dma_buf *, void *);
    void *(*vmap)(struct dma_buf *);
    void (*vunmap)(struct dma_buf *, void *vaddr);
};
  • 关联文件描述符
    使用 dma_buf_fd() 函数将 dma-buf 对象与一个文件描述符关联起来。
int dma_buf_fd(struct dma_buf *dmabuf, int flags);

DMA-BUF API (导入方)

  • 检索 dma-buf 对象
    通过文件描述符 fd 获取 dma-buf 对象。
struct dma_buf *dma_buf_get(int fd);
void dma_buf_put(struct dma_buf *dma_buf);
  • 将设备附加到 dma-buf
    导出方可以检查其后端存储是否可被 dev 设备访问。
struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dma_buf, struct device *dev);
// 动态附加
struct dma_buf_attachment *dma_buf_dynamic_attach(struct dma_buf *dma_buf, struct device *dev, bool allow_dynamic, void *priv);
void dma_buf_detach(struct dma_buf *dmabuf, struct dma_buf_attachment *attach);
  • 映射到 DMA 地址
    此时,导出方需要确定后端存储的位置并钉住页面。
struct sg_table *dma_buf_map_attachment(struct dma_buf_attachment *attach, enum dma_data_direction direction);
void dma_buf_unmap_attachment(struct dma_buf_attachment *attach, struct sg_table *sg_table, enum dma_data_direction direction);
  • CPU 访问函数
int dma_buf_begin_cpu_access();
int dma_buf_end_cpu_access();
void *dma_buf_kmap();
void dma_buf_kunmap();
int dma_buf_mmap();
void *dma_buf_vmap();
void dma_buf_vunmap();

使用 DMA-BUF 进行 GPU 内存 RDMA

内存注册工作流

下图展示了使用 dma-buf 进行 GPU 内存 RDMA 注册的完整流程:
1. 应用程序 (Application) 调用 GPU 库 (GPU library) 来分配 GPU 内存。
2. GPU 库返回内存地址、大小以及一个代表该内存的 文件描述符 (fd)
3. 应用程序调用 RDMA 库 (RDMA library) (例如 OFI 或 Verbs) 的内存注册函数 (如 ibv_reg_mr_fd),并将文件描述符 fd 传入。
4. 调用链最终到达内核态的 RDMA 驱动 (RDMA driver)
5. 在内核中,GPU 驱动 作为 导出方 (exporter),将 GPU 内存导出为 dma-buf 对象。
6. RDMA 驱动 作为 导入方 (importer),通过文件描述符导入该 dma-buf,并将其映射为可供 NIC 进行 点对点 DMA (peer-to-peer DMA) 的物理地址。
7. 这样,NIC 就可以通过 PCIe 直接访问 GPU 内存,实现 RDMA 操作。

Page 10: 内存注册工作流图
Page 10: 内存注册工作流图

GPU 软件变更

  • 许多现有的 GPU 驱动已支持 Dma-buf

    • 作为 DRM / GEM / PRIME 框架的一部分。
    • 例如,可以通过 ioctl() 访问 /dev/dri/card<n>
    • 相关命令:
      command function
      DRM_IOCTL_MODE_CREATE_DUMB 分配一个 "dumb" 缓冲区
      DRM_IOCTL_I915_GEM_CREATE 分配一个 "GEM" 缓冲区
      DRM_IOCTL_PRIME_HANDLE_TO_FD 获取 dma-buf 文件描述符
  • 当前的 GPU 驱动实现可能未针对 P2P 访问进行优化。

  • 用户空间库需要提供接口来检索 dma-buf fd

    • 作为已分配内存对象的属性(例如,作为 IPC 句柄)。
    • 应用程序不希望直接调用 ioctl

RDMA 驱动程序变更

  • 核心变更: 支持通过专门的 ib_umem_get() 导入 dma-buf 作为用户内存。
    • 引入新函数 ib_umem_dmabuf_get() 来处理基于文件描述符的 dma-buf 内存。
// 原有函数
struct ib_umem *
ib_umem_get(
    struct ib_ucontext *ucontext,
    unsigned long addr,
    size_t size,
    int access);
// 新增函数
struct ib_umem *
ib_umem_dmabuf_get(
    struct ib_ucontext *ucontext,
    unsigned long addr,
    size_t size,
    int dmabuf_fd,
    int access);
  • Uverbs: 为内存注册定义两个新的 uverbs 命令。
    • IB_USER_VERBS_CMD_REG_MR_FD
    • IB_USER_VERBS_CMD_REREG_MR_FD
    • 与非 FD 版本相比,这两个命令需要两个额外参数:
      • fd_type: 文件描述符的类型,允许未来扩展。
      • fd: 文件描述符。

RDMA 驱动程序变更 (续)

  • ib_device 结构中添加两个函数指针,用于与供应商驱动程序对接
struct ib_device {
    ......
    struct ib_mr * (*reg_user_mr_fd)(....., int fd_type, int fd, int acc, ..... );
    int (*rereg_user_mr_fd)(....., int fd_type, int fd, int acc, ..... );
};
  • 供应商 RDMA 驱动程序: 实现这两个函数是可选的
    • 仅当供应商驱动希望支持 dma-buf 时才需要。
    • 可以选择仅支持注册 (reg),而不支持重新注册 (rereg)。
    • 相应地设置 ib_dev->dev.uverbs_cmd_mask
    • 实现过程很直接:
      • 采用非 fd 版本的实现,并将 ib_umem_get() 替换为 ib_umem_dmabuf_get()

RDMA 库变更

  • 向 Verbs API 添加两个新函数
    • ibv_reg_mr_fdibv_rereg_mr_fd
    • 同样,与非 fd 版本相比,这些函数有两个额外参数 ibv_mr_fd_typefd
struct ibv_mr *ibv_reg_mr_fd (
    struct ibv_pd *pd,
    void *addr,
    size_t length,
    enum ibv_mr_fd_type,
    int fd,
    int access);
int ibv_rereg_mr_fd (
    struct ibv_mr *mr,
    int flags,
    struct ibv_pd *pd,
    void *addr,
    size_t length,
    enum ibv_mr_fd_type,
    int fd,
    int access);

RDMA 库变更 (续)

  • 添加两个 uverbs 命令函数以与内核驱动程序交互
int ibv_cmd_reg_mr_fd(....., int fd_type, int fd, int access, .....);
int ibv_cmd_rereg_mr_fd(....., int fd_type, int fd, int access, .....);
  • verbs_context_ops 结构中添加两个函数指针,用于与供应商库对接
struct verbs_context_ops {
    ......
    struct ibv_mr *(*reg_mr_fd)(....., enum ibv_mr_fd_type, int fd, int access );
    int (*rereg_mr_fd)(....., enum ibv_mr_fd_type, int fd, int access );
};
  • 在特定供应商的 RDMA 库 (provider) 中实现这两个函数
    • 只需调用 ibv_cmd_ 版本的函数即可。

OFI 变更

fi_mr_attr 结构中增加了新字段,以便在进行内存注册时可以传递文件描述符(fd)。

struct fi_mr_attr {
    ......
    enum fi_hmem_iface iface;   /* 用于内存分配的API */
    union {
        uint64_t reserved;
        ......
        int fd;
    } device;
};
  • 必须使用 fi_mr_regattr() 函数。
  • 提供商(Providers)需要识别这些字段并正确处理注册过程
  • 支持情况由 FI_HMEM 能力位(capability bit)来表示。

状态与未来工作

软件原型实现

一个软件原型已经完成,其特性如下:
- 基于上游 Linux 内核 5.6 和最新的用户空间 rdma-core 库。
- GPU:使用 i915 驱动程序的 Intel GPU。
- RDMA NIC:Mellanox ConnectX-4 EDR,使用上游驱动程序。

后续步骤

  • 将 RDMA 驱动程序的变更推送到上游 Linux 内核:
    • 第一个 RFC(征求意见)补丁集已发送至 linux-rdma 邮件列表并得到审阅。
    • 修订版的 RFC 补丁集正在开发中。
    • 这项工作依赖于 GPU 驱动程序能够通过 dma-buf 接口来固定(pin)设备内存,此功能目前尚未合并到上游。
  • 将 RDMA 库的变更推送到上游 rdma-core。
  • 将 OFI 的相关变更推送到上游。