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工具或自行编写对应的接收逻辑。