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到主机的拷贝。
核心机制图解
常见问题
- 为什么GPUDirect需要总线地址而非虚拟地址?网卡通过PCIe总线发起DMA,必须使用物理地址空间中的总线地址,而GPU驱动负责将显存虚拟地址转换为对应的总线地址。
- 本模拟程序与真实环境的差异?真实场景需调用
cudaHostGetDevicePointer或cuMemGetAddressRange等API获取映射关系,且需要GPU与网卡硬件支持及驱动配置。本程序仅用于理解数据路径差异。