4. NVLink与PCIe混合通信性能对比

任务说明

本案例在包含NVLink与PCIe两种互联的混合拓扑中,分别使用两种路径完成相同大小的数据块传输,测量并对比带宽与延迟。目标是通过可复现的基准测试,量化NVLink相对PCIe的性能优势,并验证两种路径在混合拓扑中的实际可用性。

测试拓扑与数据布局

采用双GPU节点(GPU0与GPU1),两者同时具备NVLink直连和PCIe Switch上行连接。数据布局为:GPU0显存中生成源数据缓冲区,GPU1显存中分配目标缓冲区,传输方向固定为GPU0→GPU1。通过CUDA事件计时,分别记录NVLink路径(cudaMemcpyPeerAsync经NVLink)与PCIe路径(经PCIe Switch)的传输耗时。

GPU 0 显存: 源缓冲区 (src, 256MB) GPU 1 显存: 目标缓冲区 (dst, 256MB) NVLink 直连 (路径 A) PCIe Switch Gen4 x16 (路径 B) PCIe 下行 PCIe 上行 计时方式: cudaEventRecord 包裹 cudaMemcpyPeerAsync (同步后取 elapsed) 路径A: NVLink直连 | 路径B: PCIe Switch转发 | 数据方向: GPU0 → GPU1

代码实现

以下代码使用CUDA P2P API完成混合拓扑下的双路径传输基准测试。程序首先查询设备间P2P支持状态,然后分配显存缓冲区并填充源数据,分别通过NVLink(cudaMemcpyPeerAsync默认路径)与PCIe(通过cudaMemcpyPeerAsync配合cudaCtxResetPersistingL2Cache控制,或使用cudaMemcpyPeerAsync并指定非P2P访问方式)执行传输。为区分两条路径,NVLink路径使用设备间直接访问(需启用P2P),PCIe路径则通过禁用P2P后的staged传输(经主机内存中转)来模拟PCIe Switch路径。计时采用CUDA事件,带宽按传输字节数除以耗时计算。

// nvlink_pcie_bench.cu
// 编译: nvcc -o bench nvlink_pcie_bench.cu
// 运行: ./bench  (需双GPU且支持P2P)

#include <cstdio>
#include <cstdlib>
#include <cuda_runtime.h>
#include <chrono>

#define CHECK_CUDA(call) do { \
    cudaError_t err = (call); \
    if (err != cudaSuccess) { \
        printf("CUDA错误 %s:%d: %s\n", __FILE__, __LINE__, cudaGetErrorString(err)); \
        exit(EXIT_FAILURE); \
    } \
} while(0)

// 传输数据块大小 (字节)
const size_t DATA_SIZE = 256 * 1024 * 1024;  // 256MB

// 执行一次传输并返回耗时(ms)
float run_transfer(int src_dev, int dst_dev, void* src, void* dst, 
                   size_t bytes, bool use_p2p) {
    // 设置当前设备为源设备
    CHECK_CUDA(cudaSetDevice(src_dev));
    
    // 根据路径类型启用/禁用P2P
    if (use_p2p) {
        // NVLink路径: 启用P2P访问
        int can_access = 0;
        CHECK_CUDA(cudaDeviceCanAccessPeer(&can_access, src_dev, dst_dev));
        if (!can_access) {
            printf("错误: GPU%d 无法P2P访问 GPU%d\n", src_dev, dst_dev);
            exit(EXIT_FAILURE);
        }
        CHECK_CUDA(cudaDeviceEnablePeerAccess(dst_dev, 0));
    } else {
        // PCIe路径: 禁用P2P, 强制走staged拷贝(经主机内存)
        // 若之前已启用, 先禁用
        cudaDeviceDisablePeerAccess(dst_dev);  // 忽略错误, 可能未启用
    }
    
    // 创建CUDA事件用于计时
    cudaEvent_t start, stop;
    CHECK_CUDA(cudaEventCreate(&start));
    CHECK_CUDA(cudaEventCreate(&stop));
    
    // 预热 (避免首次调用开销)
    CHECK_CUDA(cudaMemcpyPeerAsync(dst, dst_dev, src, src_dev, bytes));
    CHECK_CUDA(cudaDeviceSynchronize());
    
    // 正式计时
    CHECK_CUDA(cudaEventRecord(start));
    CHECK_CUDA(cudaMemcpyPeerAsync(dst, dst_dev, src, src_dev, bytes));
    CHECK_CUDA(cudaEventRecord(stop));
    CHECK_CUDA(cudaEventSynchronize(stop));
    
    float ms = 0.0f;
    CHECK_CUDA(cudaEventElapsedTime(&ms, start, stop));
    
    // 清理: 若启用了P2P则禁用
    if (use_p2p) {
        CHECK_CUDA(cudaDeviceDisablePeerAccess(dst_dev));
    }
    
    CHECK_CUDA(cudaEventDestroy(start));
    CHECK_CUDA(cudaEventDestroy(stop));
    
    return ms;
}

int main() {
    int dev_count = 0;
    CHECK_CUDA(cudaGetDeviceCount(&dev_count));
    if (dev_count < 2) {
        printf("需要至少2块GPU, 当前检测到 %d 块\n", dev_count);
        return EXIT_FAILURE;
    }
    
    // 使用设备0和设备1
    const int dev0 = 0, dev1 = 1;
    
    // 查询设备属性
    cudaDeviceProp prop0, prop1;
    CHECK_CUDA(cudaGetDeviceProperties(&prop0, dev0));
    CHECK_CUDA(cudaGetDeviceProperties(&prop1, dev1));
    printf("GPU0: %s\n", prop0.name);
    printf("GPU1: %s\n", prop1.name);
    
    // 检查P2P支持
    int can_access = 0;
    CHECK_CUDA(cudaDeviceCanAccessPeer(&can_access, dev0, dev1));
    printf("P2P支持 (GPU0→GPU1): %s\n", can_access ? "是" : "否");
    if (!can_access) {
        printf("错误: 当前GPU组合不支持P2P, 无法测试NVLink路径\n");
        return EXIT_FAILURE;
    }
    
    // 分配显存
    void *src = nullptr, *dst = nullptr;
    CHECK_CUDA(cudaSetDevice(dev0));
    CHECK_CUDA(cudaMalloc(&src, DATA_SIZE));
    CHECK_CUDA(cudaSetDevice(dev1));
    CHECK_CUDA(cudaMalloc(&dst, DATA_SIZE));
    
    // 初始化源数据 (在GPU0上填充)
    CHECK_CUDA(cudaSetDevice(dev0));
    unsigned char* h_data = (unsigned char*)malloc(DATA_SIZE);
    for (size_t i = 0; i < DATA_SIZE; i++) {
        h_data[i] = (unsigned char)(i % 251);
    }
    CHECK_CUDA(cudaMemcpy(src, h_data, DATA_SIZE, cudaMemcpyHostToDevice));
    free(h_data);
    
    // 目标缓冲区清零
    CHECK_CUDA(cudaSetDevice(dev1));
    CHECK_CUDA(cudaMemset(dst, 0, DATA_SIZE));
    
    // 执行多次测试取平均
    const int ITERATIONS = 5;
    float nvlink_ms = 0.0f, pcie_ms = 0.0f;
    
    printf("\n开始基准测试 (数据大小: %.0f MB, 迭代次数: %d)\n", 
           DATA_SIZE / (1024.0*1024.0), ITERATIONS);
    
    for (int i = 0; i < ITERATIONS; i++) {
        // NVLink路径
        float t1 = run_transfer(dev0, dev1, src, dst, DATA_SIZE, true);
        nvlink_ms += t1;
        
        // PCIe路径 (禁用P2P)
        float t2 = run_transfer(dev0, dev1, src, dst, DATA_SIZE, false);
        pcie_ms += t2;
        
        printf("迭代 %d: NVLink=%.3f ms, PCIe=%.3f ms\n", i+1, t1, t2);
    }
    
    nvlink_ms /= ITERATIONS;
    pcie_ms /= ITERATIONS;
    
    // 计算带宽 (GB/s)
    double nvlink_bw = (double)DATA_SIZE / (nvlink_ms / 1000.0) / 1e9;
    double pcie_bw = (double)DATA_SIZE / (pcie_ms / 1000.0) / 1e9;
    
    printf("\n========== 测试结果 ==========\n");
    printf("NVLink路径: 平均耗时 %.3f ms, 带宽 %.2f GB/s\n", nvlink_ms, nvlink_bw);
    printf("PCIe路径:   平均耗时 %.3f ms, 带宽 %.2f GB/s\n", pcie_ms, pcie_bw);
    printf("--------------------------------\n");
    printf("NVLink/PCIe 带宽比: %.2fx\n", nvlink_bw / pcie_bw);
    printf("NVLink/PCIe 延迟比: %.2fx (越小越好)\n", nvlink_ms / pcie_ms);
    
    // 验证数据完整性
    CHECK_CUDA(cudaSetDevice(dev1));
    unsigned char* verify = (unsigned char*)malloc(DATA_SIZE);
    CHECK_CUDA(cudaMemcpy(verify, dst, DATA_SIZE, cudaMemcpyDeviceToHost));
    
    bool ok = true;
    for (size_t i = 0; i < DATA_SIZE; i++) {
        if (verify[i] != (unsigned char)(i % 251)) {
            printf("数据校验失败 @ offset %zu\n", i);
            ok = false;
            break;
        }
    }
    free(verify);
    
    if (ok) {
        printf("数据完整性校验: 通过\n");
    } else {
        printf("数据完整性校验: 失败\n");
    }
    
    // 清理
    CHECK_CUDA(cudaSetDevice(dev0));
    CHECK_CUDA(cudaFree(src));
    CHECK_CUDA(cudaSetDevice(dev1));
    CHECK_CUDA(cudaFree(dst));
    
    return ok ? EXIT_SUCCESS : EXIT_FAILURE;
}

编译与运行

在具备双GPU且支持NVLink的服务器上执行以下命令:

nvcc -o bench nvlink_pcie_bench.cu
./bench

验证方法与判定标准

程序运行后应输出GPU型号、P2P支持状态、每次迭代的耗时以及最终的平均带宽对比。判定标准如下:

  • 功能正确性:程序正常结束且输出“数据完整性校验: 通过”,说明两条路径均成功完成数据传输且数据无损坏。
  • 路径区分有效性:NVLink路径(启用P2P)与PCIe路径(禁用P2P)的耗时存在显著差异,NVLink路径耗时明显更短。
  • 性能对比合理性:在支持NVLink的平台上(如A100、V100等),NVLink带宽应显著高于PCIe Gen4 x16的理论带宽(约32GB/s),实测NVLink带宽通常可达PCIe的2~5倍。若NVLink带宽低于PCIe,需检查拓扑连接或驱动配置。
  • 可重复性:多次运行结果应保持稳定,带宽波动不超过±10%。

注意:实际性能数值取决于具体GPU型号、NVLink版本(NVLink2/3/4)、PCIe代际及系统拓扑,本案例不预设具体数值,以实际运行输出为准。