4. RDMA读GPU内存
任务目标
本案例实现一个最小可复现的RDMA读操作:客户端(主动方)通过InfiniBand RDMA Read从服务端(被动方)的GPU显存中读取数据。服务端将数据写入GPU显存并注册为可读内存区域,客户端发起单边RDMA Read操作直接读取该数据,无需服务端CPU参与数据拷贝。
环境与依赖
- 硬件:InfiniBand HCA(支持RDMA Read)、NVIDIA GPU(同一节点或不同节点均可)
- 软件:Linux内核含InfiniBand驱动、libibverbs、librdmacm、CUDA Toolkit(≥11.0)
- 编译依赖:
cuda_runtime.h、infiniband/verbs.h、rdma/rdma_cma.h - 连接方式:服务端监听,客户端主动连接后发起RDMA Read
核心实现
代码包含服务端与客户端两个角色,通过rdma_cm建立连接,交换QP信息与GPU内存的RKey。服务端将数据写入GPU显存并注册为可读内存区域,客户端发起单边RDMA Read操作直接读取该数据,无需服务端CPU参与数据拷贝。服务端使用cudaMalloc分配显存,cudaMemcpy写入数据,再通过ibv_reg_mr注册该显存区域。客户端使用ibv_post_srq接收请求,ibv_post_read发起RDMA Read。为简化流程,代码使用同步轮询完成事件处理,不使用异步回调。
关键完整代码
// rdma_read_gpu.c — 编译: mpicc -o rdma_read_gpu rdma_read_gpu.c -lrdmacm -libverbs -lcuda
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <infiniband/verbs.h>
#include <rdma/rdma_cma.h>
#include <cuda_runtime.h>
#define DATA_SIZE 4096
#define BUFFER_SIZE (DATA_SIZE + 4096) // 对齐
struct conn_info {
uint64_t addr;
uint32_t rkey;
uint32_t qp_num;
};
static int post_read(struct rdma_cm_id *id, uint64_t remote_addr, uint32_t rkey,
void *local_buf, uint32_t length, struct ibv_mr *mr) {
struct ibv_sge sge = {.addr = (uint64_t)local_buf, .length = length, .lkey = mr->lkey};
struct ibv_send_wr wr = {
.opcode = IBV_WR_RDMA_READ,
.send_flags = IBV_SEND_SIGNALED,
.wr.rdma.remote_addr = remote_addr,
.wr.rdma.rkey = rkey,
.sg_list = &sge,
.num_sge = 1
};
struct ibv_send_wr *bad_wr = NULL;
return ibv_post_send(id->qp, &wr, &bad_wr);
}
static int run_server(struct rdma_cm_id *listen_id) {
struct rdma_cm_id *conn_id = NULL;
struct rdma_conn_param param = {0};
struct rdma_conn_param accepted = {0};
struct ibv_mr *gpu_mr = NULL;
char *gpu_buf = NULL;
struct conn_info local_info, remote_info;
int ret = 0;
if (rdma_get_request(listen_id, &conn_id)) { perror("get_request"); return -1; }
// 分配GPU内存并写入数据
if (cudaMalloc(&gpu_buf, BUFFER_SIZE) != cudaSuccess) { fprintf(stderr, "cudaMalloc failed\n"); return -1; }
char *host_data = malloc(DATA_SIZE);
memset(host_data, 'R', DATA_SIZE);
if (cudaMemcpy(gpu_buf, host_data, DATA_SIZE, cudaMemcpyHostToDevice) != cudaSuccess) {
fprintf(stderr, "cudaMemcpy failed\n"); free(host_data); return -1;
}
free(host_data);
// 注册GPU显存
gpu_mr = ibv_reg_mr(conn_id->pd, gpu_buf, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ);
if (!gpu_mr) { perror("reg_mr"); return -1; }
// 交换地址信息
local_info.addr = (uint64_t)gpu_buf;
local_info.rkey = gpu_mr->rkey;
local_info.qp_num = conn_id->qp->qp_num;
param.private_data = &local_info;
param.private_data_len = sizeof(local_info);
param.responder_resources = 1;
param.initiator_depth = 1;
param.retry_count = 7;
if (rdma_accept(conn_id, ¶m)) { perror("accept"); return -1; }
struct rdma_cm_event *event;
if (rdma_get_cm_event(listen_id->channel, &event)) { perror("get_event"); return -1; }
if (event->event != RDMA_CM_EVENT_ESTABLISHED) { fprintf(stderr, "Unexpected event\n"); return -1; }
memcpy(&remote_info, event->param.conn.private_data, sizeof(remote_info));
rdma_ack_cm_event(event);
// 等待客户端完成读取
struct ibv_wc wc;
while (rdma_get_recv_comp(conn_id, &wc) != 0) { /* 轮询 */ }
if (wc.status != IBV_WC_SUCCESS) { fprintf(stderr, "WC error: %s\n", ibv_wc_status_str(wc.status)); ret = -1; }
// 验证数据(读回GPU内存检查)
char *verify = malloc(DATA_SIZE);
cudaMemcpy(verify, gpu_buf, DATA_SIZE, cudaMemcpyDeviceToHost);
int ok = 1;
for (int i = 0; i < DATA_SIZE; i++) if (verify[i] != 'R') { ok = 0; break; }
printf("Server: data %s\n", ok ? "OK" : "MISMATCH");
free(verify);
rdma_disconnect(conn_id);
ibv_dereg_mr(gpu_mr);
cudaFree(gpu_buf);
rdma_destroy_id(conn_id);
return ret;
}
static int run_client(struct rdma_cm_id *id) {
struct rdma_conn_param param = {0};
struct conn_info local_info, remote_info;
char *local_buf = malloc(DATA_SIZE);
struct ibv_mr *local_mr = NULL;
struct ibv_wc wc;
int ret = 0;
memset(local_buf, 0, DATA_SIZE);
local_mr = ibv_reg_mr(id->pd, local_buf, DATA_SIZE, IBV_ACCESS_LOCAL_WRITE);
if (!local_mr) { perror("reg_mr"); return -1; }
local_info.addr = 0; // 客户端不需要暴露内存
local_info.rkey = 0;
local_info.qp_num = id->qp->qp_num;
param.private_data = &local_info;
param.private_data_len = sizeof(local_info);
param.responder_resources = 1;
param.initiator_depth = 1;
param.retry_count = 7;
if (rdma_connect(id, ¶m)) { perror("connect"); return -1; }
struct rdma_cm_event *event;
if (rdma_get_cm_event(id->channel, &event)) { perror("get_event"); return -1; }
if (event->event != RDMA_CM_EVENT_ESTABLISHED) { fprintf(stderr, "Unexpected event\n"); return -1; }
memcpy(&remote_info, event->param.conn.private_data, sizeof(remote_info));
rdma_ack_cm_event(event);
// 发起RDMA Read
if (post_read(id, remote_info.addr, remote_info.rkey, local_buf, DATA_SIZE, local_mr)) {
perror("post_read"); ret = -1; goto out;
}
// 等待完成
if (rdma_get_send_comp(id, &wc) != 0) { perror("get_send_comp"); ret = -1; goto out; }
if (wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "Read WC error: %s\n", ibv_wc_status_str(wc.status)); ret = -1; goto out;
}
// 验证读取结果
int ok = 1;
for (int i = 0; i < DATA_SIZE; i++) if (local_buf[i] != 'R') { ok = 0; break; }
printf("Client: read %d bytes, data %s\n", DATA_SIZE, ok ? "OK" : "MISMATCH");
out:
rdma_disconnect(id);
ibv_dereg_mr(local_mr);
free(local_buf);
return ret;
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <server|client> <addr>\n", argv[0]);
return 1;
}
struct rdma_cm_id *listen_id = NULL, *conn_id = NULL;
struct rdma_event_channel *ec = rdma_create_event_channel();
if (!ec) { perror("create_event_channel"); return 1; }
if (strcmp(argv[1], "server") == 0) {
if (rdma_create_id(ec, &listen_id, NULL, RDMA_PS_TCP)) { perror("create_id"); return 1; }
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(7471)};
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (rdma_bind_addr(listen_id, (struct sockaddr *)&addr)) { perror("bind"); return 1; }
if (rdma_listen(listen_id, 1)) { perror("listen"); return 1; }
printf("Server listening on port 7471\n");
run_server(listen_id);
rdma_destroy_id(listen_id);
} else {
if (rdma_create_id(ec, &conn_id, NULL, RDMA_PS_TCP)) { perror("create_id"); return 1; }
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(7471)};
inet_pton(AF_INET, argv[2], &addr.sin_addr);
if (rdma_resolve_addr(conn_id, NULL, (struct sockaddr *)&addr, 2000)) { perror("resolve_addr"); return 1; }
if (rdma_resolve_route(conn_id, 2000)) { perror("resolve_route"); return 1; }
run_client(conn_id);
rdma_destroy_id(conn_id);
}
rdma_destroy_event_channel(ec);
return 0;
}
运行与验证
- 编译:
mpicc -o rdma_read_gpu rdma_read_gpu.c -lrdmacm -libverbs -lcuda(若无mpicc可用gcc替代) - 启动服务端:
./rdma_read_gpu server 0.0.0.0(监听端口7471) - 启动客户端:
./rdma_read_gpu client <服务端IP> - 通过条件:服务端输出
Server: data OK,客户端输出Client: read 4096 bytes, data OK,且两端均无错误信息
关键说明
- 服务端使用
cudaMalloc分配显存,ibv_reg_mr注册时需指定IBV_ACCESS_REMOTE_READ权限,否则客户端无法读取 - 客户端通过
IBV_WR_RDMA_READ操作码发起单边读,服务端CPU不参与数据拷贝 - 代码使用同步轮询等待完成事件(
rdma_get_send_comp/rdma_get_recv_comp),实际工程中应使用异步事件通道 - 若GPU与HCA不在同一NUMA节点,性能会下降,但不影响功能正确性