RDMA WITH GPU MEMORY VIA DMA-BUF
RDMA WITH GPU MEMORY VIA DMA-BUF
Jianxin Xiong, Intel Corporation
目录
- RDMA 概述
- RDMA与系统内存
- RDMA与GPU内存
- DMA-BUF 概述
- DMA-BUF API (导出方)
- DMA-BUF API (导入方)
- 使用 DMA-BUF 进行 GPU 内存 RDMA
- 内存注册工作流
- GPU 软件变更
- RDMA 驱动程序变更
- RDMA 驱动程序变更 (续)
- RDMA 库变更
- RDMA 库变更 (续)
- OFI 变更
- 状态与未来工作
RDMA 概述
RDMA与系统内存
-
RDMA 是 “DMA + 网络”
- RDMA 操作结合了发起方(Initiator)和目标方(Target)的 DMA 操作。
- Write 操作: 发起方进行 DMA 读,目标方进行 DMA 写。
- Read 操作: 发起方进行 DMA 写,目标方进行 DMA 读。
- RDMA 操作结合了发起方(Initiator)和目标方(Target)的 DMA 操作。
-
DMA 需要对内存进行正确的设置
- 内存页(Memory pages)需要被“钉住”(pinned),以防止被交换到磁盘。
- 使用总线地址(Bus addresses)进行寻址。
- 这些设置通常在“内存注册”(memory registration)时完成。
- 对于系统内存中的用户空间缓冲区,通常涉及以下内核函数调用:
get_user_pages()sg_alloc_table() / sg_set_page() / sg_next() / ...dma_map_sg()
RDMA与GPU内存
-
GPU 内存是本地(local)的
- 网络接口控制器(NIC)驱动无法直接“钉住”GPU内存。
- NIC驱动不知道GPU内存的DMA地址。
-
需要 NIC 驱动和 GPU 驱动之间的协作
-
Mellanox 的 Peer-Direct 方案
- 这是为内核 RDMA 核心设计的插件接口。
- 每个 GPU 驱动提供一个插件模块。
- 当内存被注册时,系统会逐一查询这些插件,直到某个插件声明对该内存的所有权。
- 该方案仅在 Mellanox OFED (MOFED) 中可用。
-
我们能否有一个非专有的上游解决方案?
- 我们的提议是使用 dma-buf。
DMA-BUF 概述
Dma-buf 是 Linux 内核中的一种标准机制,用于在不同设备驱动程序之间共享缓冲区。
其工作流程如下:
* 导出方 (Exporter): 拥有内存分配的驱动程序。它创建一个 dma-buf 对象并导出一个文件描述符(fd)。
* 导入方 (Importer): 需要访问该内存的驱动程序。它通过文件描述符获取 dma-buf 对象的引用,将其附加(attach)到自己的设备上,并将其映射(map)为设备可访问的DMA地址。
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 操作。
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_FDIB_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()。
- 采用非 fd 版本的实现,并将
RDMA 库变更
- 向 Verbs API 添加两个新函数
ibv_reg_mr_fd和ibv_rereg_mr_fd。- 同样,与非 fd 版本相比,这些函数有两个额外参数
ibv_mr_fd_type和fd。
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 的相关变更推送到上游。