4. CUDA与RDMA集成
任务目标
编写一个CUDA程序,在GPU内核中生成数据,然后通过RDMA(InfiniBand)将数据发送到远端主机。本案例聚焦于CUDA与RDMA的集成方式:使用GPU Direct RDMA技术,使网卡能够直接访问GPU显存,避免数据经过主机内存的拷贝。
环境依赖
- NVIDIA GPU(支持GPUDirect RDMA,如P100/V100/A100等)
- Mellanox InfiniBand网卡(如ConnectX-5及以上)
- CUDA Toolkit 10.0+
- MLNX_OFED驱动(包含libibverbs、librdmacm)
- Linux系统,已安装gcc、make
核心实现
以下代码实现:GPU内核生成递增数据 → 通过cudaHostAlloc分配主机锁页内存 → 使用GPUDirect RDMA将数据从GPU显存直接发送到远端。关键点在于使用cudaHostAlloc配合cudaHostAllocMapped标志,使主机内存映射到GPU地址空间,从而允许网卡通过RDMA直接读写该内存区域。
// rdma_cuda_send.c - GPUDirect RDMA发送示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cuda_runtime.h>
#include <infiniband/verbs.h>
#include <rdma/rdma_cma.h>
#include <arpa/inet.h>
#define DATA_SIZE 4096 // 4KB数据
// GPU内核:生成递增数据
__global__ void generate_data(int *data, int size) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < size) {
data[idx] = idx * 2; // 生成偶数序列
}
}
// 检查CUDA错误
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA error at %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(1); \
} \
} while(0)
// 检查RDMA操作返回码
static int post_send(struct ibv_qp *qp, uint64_t addr, uint32_t rkey,
uint32_t length, uint32_t imm_data) {
struct ibv_sge sge;
struct ibv_send_wr wr, *bad_wr = NULL;
memset(&sge, 0, sizeof(sge));
sge.addr = addr;
sge.length = length;
sge.lkey = 0; // 使用GPUDirect时lkey由设备分配
memset(&wr, 0, sizeof(wr));
wr.wr_id = 1;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_SEND_WITH_IMM;
wr.send_flags = IBV_SEND_SIGNALED;
wr.imm_data = htonl(imm_data);
return ibv_post_send(qp, &wr, &bad_wr);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <server_ip> <port>\n", argv[0]);
return 1;
}
const char *server_ip = argv[1];
int port = atoi(argv[2]);
// 1. 分配GPU可访问的主机锁页内存
int *h_data = NULL;
CUDA_CHECK(cudaHostAlloc((void**)&h_data, DATA_SIZE * sizeof(int),
cudaHostAllocMapped));
// 2. 获取设备指针(GPUDirect RDMA需要)
int *d_data = NULL;
CUDA_CHECK(cudaHostGetDevicePointer((void**)&d_data, h_data, 0));
// 3. 在GPU上生成数据
generate_data<<<1, 256>>>(d_data, DATA_SIZE);
CUDA_CHECK(cudaDeviceSynchronize());
// 4. 初始化RDMA连接
struct rdma_cm_id *cm_id = NULL;
struct rdma_event_channel *ec = NULL;
struct rdma_cm_event *event = NULL;
struct ibv_pd *pd = NULL;
struct ibv_mr *mr = NULL;
struct ibv_qp *qp = NULL;
ec = rdma_create_event_channel();
if (!ec) { perror("rdma_create_event_channel"); return 1; }
if (rdma_create_id(ec, &cm_id, NULL, RDMA_PS_TCP)) {
perror("rdma_create_id"); return 1;
}
// 5. 解析服务器地址
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, server_ip, &addr.sin_addr);
// 6. 连接到服务器
if (rdma_resolve_addr(cm_id, NULL, (struct sockaddr*)&addr, 2000)) {
perror("rdma_resolve_addr"); return 1;
}
rdma_get_cm_event(ec, &event);
rdma_ack_cm_event(event);
if (rdma_resolve_route(cm_id, 2000)) {
perror("rdma_resolve_route"); return 1;
}
rdma_get_cm_event(ec, &event);
rdma_ack_cm_event(event);
// 7. 创建QP
struct ibv_qp_init_attr qp_attr;
memset(&qp_attr, 0, sizeof(qp_attr));
qp_attr.qp_type = IBV_QPT_RC;
qp_attr.sq_sig_all = 1;
qp_attr.cap.max_send_wr = 16;
qp_attr.cap.max_recv_wr = 16;
qp_attr.cap.max_send_sge = 1;
qp_attr.cap.max_recv_sge = 1;
if (rdma_create_qp(cm_id, NULL, &qp_attr)) {
perror("rdma_create_qp"); return 1;
}
qp = cm_id->qp;
pd = cm_id->pd;
// 8. 注册内存区域(GPUDirect RDMA关键步骤)
mr = ibv_reg_mr(pd, h_data, DATA_SIZE * sizeof(int),
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
if (!mr) { perror("ibv_reg_mr"); return 1; }
// 9. 连接服务器
if (rdma_connect(cm_id, NULL)) {
perror("rdma_connect"); return 1;
}
rdma_get_cm_event(ec, &event);
rdma_ack_cm_event(event);
// 10. 发送数据(网卡直接从GPU显存读取)
if (post_send(qp, (uint64_t)h_data, mr->rkey,
DATA_SIZE * sizeof(int), 0x1234)) {
fprintf(stderr, "post_send failed\n");
return 1;
}
// 11. 等待发送完成
struct ibv_wc wc;
while (ibv_poll_cq(qp->send_cq, 1, &wc) == 0) {
usleep(100);
}
printf("Data sent successfully via GPUDirect RDMA\n");
printf("First 5 values: %d, %d, %d, %d, %d\n",
h_data[0], h_data[1], h_data[2], h_data[3], h_data[4]);
// 12. 清理
ibv_dereg_mr(mr);
rdma_destroy_qp(cm_id);
rdma_disconnect(cm_id);
rdma_destroy_id(cm_id);
rdma_destroy_event_channel(ec);
CUDA_CHECK(cudaFreeHost(h_data));
return 0;
}
编译与运行
# 编译(需要安装MLNX_OFED并设置环境)
gcc -o rdma_cuda_send rdma_cuda_send.c \
-I/usr/local/cuda/include -L/usr/local/cuda/lib64 \
-lcudart -libverbs -lrdmacm \
-Wl,-rpath,/usr/local/cuda/lib64
# 在服务器端启动接收程序(需先运行接收端)
# 发送端执行
./rdma_cuda_send 192.168.1.100 7471
验证方法
运行程序后,观察以下判定条件:
- 程序输出
Data sent successfully via GPUDirect RDMA,说明发送流程完成 - 输出前5个值为
0, 2, 4, 6, 8,验证GPU内核生成的数据正确 - 在服务器端使用
ibv_rc_pingpong或自定义接收程序确认收到数据 - 使用
nvidia-smi查看GPU显存使用,确认数据在GPU中生成
注意:本示例为客户端发送端代码,需要配合一个RDMA接收端程序才能完成完整的数据传输验证。接收端可使用标准ibv_rc_pingpong工具或自行编写对应的接收逻辑。