3. GPUDirect RDMA概述

任务目标

本案例面向首次接触GPUDirect RDMA的开发者,目标是理解其核心工作原理:数据如何在GPU显存与远端网卡之间传输,以及它相比传统RDMA路径(经CPU与主机内存中转)究竟消除了哪些拷贝与延迟。案例通过一个可运行的模拟程序,对比两种路径的DMA描述符构建过程,帮助你将抽象概念落到具体代码上。

工作原理概述

传统RDMA发送流程中,GPU数据必须先由GPU拷贝到主机内存(cudaMemcpy),再由CPU驱动网卡从主机内存读取并发送。GPUDirect RDMA则允许网卡通过PCIe总线直接访问GPU显存地址,省去了主机内存中转。关键在于网卡需要知道GPU显存的物理地址(而非虚拟地址),并据此构建DMA描述符。本案例模拟程序聚焦于这一核心差异:传统路径使用主机虚拟地址,GPUDirect路径使用经过映射后的GPU总线地址。

环境与依赖

  • 操作系统:Linux(建议Ubuntu 20.04+)
  • 编译器:g++(支持C++11)
  • 无需真实GPU或RDMA硬件,本案例为纯软件模拟

完整实现代码

#include <iostream>
#include <cstdint>
#include <cstring>
#include <vector>

// 模拟DMA描述符:描述一次数据传输的源地址、长度与方向
struct DmaDescriptor {
    uint64_t src_addr;   // 源地址(物理或总线地址)
    uint32_t length;     // 传输字节数
    uint32_t flags;      // 位0: 0=传统路径, 1=GPUDirect路径
};

// 模拟GPU显存分配:返回一个假的"显存虚拟地址"
uint64_t mock_gpu_malloc(size_t bytes) {
    static uint64_t fake_gpu_heap = 0x2A00000000ULL;
    uint64_t addr = fake_gpu_heap;
    fake_gpu_heap += bytes;
    return addr;
}

// 模拟cudaMemcpyDtoH:将GPU数据拷贝到主机内存(传统路径必需)
void mock_cuda_memcpy_dtoh(uint64_t dst_host, uint64_t src_gpu, size_t bytes) {
    std::cout << "  [模拟] cudaMemcpyDtoH: GPU[" << std::hex << src_gpu
              << "] -> Host[" << dst_host << "] " << std::dec << bytes
              << " bytes\n";
}

// 模拟获取GPU显存的总线地址(GPUDirect RDMA核心步骤)
uint64_t mock_get_gpu_bus_address(uint64_t gpu_virt_addr) {
    // 真实场景由CUDA驱动返回PCIe总线地址;此处用固定偏移模拟
    return gpu_virt_addr + 0x80000000ULL;
}

// 模拟网卡构建DMA描述符
DmaDescriptor build_dma_descriptor(uint64_t src, uint32_t len, bool gpu_direct) {
    DmaDescriptor desc;
    desc.src_addr = src;
    desc.length = len;
    desc.flags = gpu_direct ? 1 : 0;
    return desc;
}

// 传统RDMA路径:GPU -> 主机内存 -> 网卡DMA
DmaDescriptor traditional_rdma_path(uint64_t gpu_buf, uint32_t len) {
    std::cout << "[传统路径] 发送GPU数据需两步:\n";
    // 步骤1: 分配主机内存(模拟固定地址)
    uint64_t host_buf = 0x10000000ULL;
    // 步骤2: GPU到主机拷贝
    mock_cuda_memcpy_dtoh(host_buf, gpu_buf, len);
    // 步骤3: 网卡从主机内存DMA读取
    std::cout << "  [模拟] 网卡从Host[" << std::hex << host_buf
              << "] 构建DMA描述符\n" << std::dec;
    return build_dma_descriptor(host_buf, len, false);
}

// GPUDirect RDMA路径:网卡直接访问GPU显存
DmaDescriptor gpu_direct_rdma_path(uint64_t gpu_buf, uint32_t len) {
    std::cout << "[GPUDirect路径] 网卡直接访问GPU显存:\n";
    // 步骤1: 获取GPU显存对应的总线地址
    uint64_t bus_addr = mock_get_gpu_bus_address(gpu_buf);
    std::cout << "  [模拟] GPU虚拟地址[" << std::hex << gpu_buf
              << "] -> 总线地址[" << bus_addr << "]\n" << std::dec;
    // 步骤2: 直接用总线地址构建DMA描述符
    return build_dma_descriptor(bus_addr, len, true);
}

int main() {
    const uint32_t data_len = 4096;
    // 模拟在GPU上分配一块发送缓冲区
    uint64_t gpu_send_buf = mock_gpu_malloc(data_len);
    std::cout << "=== 模拟发送 " << data_len << " 字节数据 ===\n";
    std::cout << "GPU显存虚拟地址: 0x" << std::hex << gpu_send_buf
              << "\n\n" << std::dec;

    // 传统路径
    DmaDescriptor desc_trad = traditional_rdma_path(gpu_send_buf, data_len);
    std::cout << "  传统DMA描述符: src=0x" << std::hex << desc_trad.src_addr
              << ", len=" << std::dec << desc_trad.length
              << ", flags=" << desc_trad.flags << "\n\n";

    // GPUDirect路径
    DmaDescriptor desc_gd = gpu_direct_rdma_path(gpu_send_buf, data_len);
    std::cout << "  GPUDirect DMA描述符: src=0x" << std::hex << desc_gd.src_addr
              << ", len=" << std::dec << desc_gd.length
              << ", flags=" << desc_gd.flags << "\n";

    // 对比结论
    std::cout << "\n=== 对比结果 ===\n";
    std::cout << "传统路径DMA源地址: 0x" << std::hex << desc_trad.src_addr
              << " (主机内存)\n";
    std::cout << "GPUDirect DMA源地址: 0x" << std::hex << desc_gd.src_addr
              << " (GPU总线地址)\n" << std::dec;
    std::cout << "GPUDirect省去了GPU->主机的拷贝步骤,网卡直接读取显存。\n";
    return 0;
}

编译与运行

g++ -std=c++11 -o gd_rdma_overview gd_rdma_overview.cpp
./gd_rdma_overview

验证方法与判定标准

程序输出应包含以下关键信息,满足即判定通过:

  • 传统路径输出包含cudaMemcpyDtoH模拟行,且DMA描述符的src地址为0x10000000(主机内存地址)。
  • GPUDirect路径输出包含GPU虚拟地址 -> 总线地址映射行,且DMA描述符的src地址为0x2A80000000(即0x2A00000000 + 0x80000000)。
  • 对比结果明确显示两条路径的DMA源地址不同,并指出GPUDirect省去了GPU到主机的拷贝。

核心机制图解

传统RDMA路径 GPU显存 主机内存 网卡 ①拷贝 ②DMA 两次内存访问,CPU参与调度 延迟高,占用主机内存带宽 GPUDirect RDMA路径 GPU显存 网卡 ①DMA直接读取 PCIe总线地址映射 零拷贝,绕过主机内存与CPU

常见问题

  • 为什么GPUDirect需要总线地址而非虚拟地址?网卡通过PCIe总线发起DMA,必须使用物理地址空间中的总线地址,而GPU驱动负责将显存虚拟地址转换为对应的总线地址。
  • 本模拟程序与真实环境的差异?真实场景需调用cudaHostGetDevicePointer或cuMemGetAddressRange等API获取映射关系,且需要GPU与网卡硬件支持及驱动配置。本程序仅用于理解数据路径差异。