3. NVLink多GPU通信延迟测试

NVLink多GPU通信延迟测试 学习卡片

任务说明

本案例编写一个CUDA程序,测量多GPU系统中不同GPU对之间的点对点(P2P)通信延迟。程序将遍历所有GPU组合,对每对GPU执行小规模数据(如64字节)的往返传输并记录耗时,最终输出延迟矩阵,用于分析NVLink拓扑对通信延迟的影响。

实现方案

使用CUDA的P2P API(cudaDeviceEnablePeerAccess)建立GPU间直接通信通道,通过cudaMemcpyPeerAsync在设备间发起小数据块拷贝,配合CUDA事件精确计时。为消除启动开销影响,对每对GPU执行多次往返测试并取最小值。程序同时查询并输出每对GPU是否支持P2P及对应的拓扑连接类型(NVLink/PCIe),便于关联延迟数据与物理拓扑。

代码实现


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

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

// 获取两个设备之间的连接类型描述
const char* get_link_type(int dev_a, int dev_b) {
    struct cudaDeviceProp prop_a, prop_b;
    cudaGetDeviceProperties(&prop_a, dev_a);
    cudaGetDeviceProperties(&prop_b, dev_b);
    for (int i = 0; i < prop_a.numNumaNodes; i++) {
        for (int j = 0; j < prop_b.numNumaNodes; j++) {
            int link_type = 0;
            if (cudaDeviceGetP2PAttribute(&link_type,
                    cudaDevP2PAttrPerformanceRank, dev_a, dev_b) == cudaSuccess) {
                // 使用性能等级粗略判断:0为最高(通常NVLink)
                if (link_type == 0) return "NVLink";
            }
        }
    }
    return "PCIe";
}

// 测量一对GPU之间的往返延迟(微秒)
float measure_roundtrip_latency(int dev_src, int dev_dst, int iterations = 1000) {
    const size_t bytes = 64;  // 小数据量,聚焦延迟
    char *buf_src, *buf_dst;

    // 在两个设备上分配缓冲区
    cudaSetDevice(dev_src);
    cudaMalloc(&buf_src, bytes);
    cudaSetDevice(dev_dst);
    cudaMalloc(&buf_dst, bytes);

    // 初始化源数据
    cudaSetDevice(dev_src);
    cudaMemset(buf_src, 0xAB, bytes);

    // 建立P2P访问
    cudaSetDevice(dev_src);
    cudaDeviceEnablePeerAccess(dev_dst, 0);
    cudaSetDevice(dev_dst);
    cudaDeviceEnablePeerAccess(dev_src, 0);

    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    // 预热
    cudaMemcpyPeerAsync(buf_dst, dev_dst, buf_src, dev_src, bytes,
                        cudaMemcpyDeviceToDevice);
    cudaDeviceSynchronize();

    // 计时往返:src->dst 再 dst->src
    float min_time = 1e9f;
    for (int i = 0; i < iterations; i++) {
        cudaEventRecord(start, dev_src);
        cudaMemcpyPeerAsync(buf_dst, dev_dst, buf_src, dev_src, bytes,
                            cudaMemcpyDeviceToDevice);
        cudaMemcpyPeerAsync(buf_src, dev_src, buf_dst, dev_dst, bytes,
                            cudaMemcpyDeviceToDevice);
        cudaEventRecord(stop, dev_src);
        cudaEventSynchronize(stop);

        float ms = 0.0f;
        cudaEventElapsedTime(&ms, start, stop);
        if (ms * 1000.0f < min_time) min_time = ms * 1000.0f;  // 转微秒
    }

    // 清理
    cudaDeviceDisablePeerAccess(dev_dst);
    cudaSetDevice(dev_dst);
    cudaDeviceDisablePeerAccess(dev_src);
    cudaEventDestroy(start);
    cudaEventDestroy(stop);
    cudaFree(buf_src);
    cudaFree(buf_dst);
    cudaSetDevice(dev_src);

    return min_time;
}

int main() {
    int dev_count;
    cudaGetDeviceCount(&dev_count);
    if (dev_count < 2) {
        printf("需要至少2块GPU,当前检测到 %d 块\n", dev_count);
        return 1;
    }

    printf("检测到 %d 块GPU,NVLink P2P延迟矩阵(微秒,往返)\n\n", dev_count);
    printf("延迟\\拓扑   ");
    for (int j = 0; j < dev_count; j++) printf("   GPU%d    ", j);
    printf("\n");

    for (int i = 0; i < dev_count; i++) {
        printf("GPU%d  ", i);
        for (int j = 0; j < dev_count; j++) {
            if (i == j) {
                printf("   ---    ");
                continue;
            }
            // 检查P2P支持
            int can_access = 0;
            cudaDeviceCanAccessPeer(&can_access, i, j);
            if (!can_access) {
                printf(" 不支持  ");
                continue;
            }
            float lat = measure_roundtrip_latency(i, j);
            const char* link = get_link_type(i, j);
            printf("%6.2f(%s)", lat, link);
        }
        printf("\n");
    }

    // 输出拓扑提示
    printf("\n说明:延迟数值为多次往返最小值,括号内为连接类型。\n");
    printf("NVLink延迟通常显著低于PCIe,同交换机下不同GPU对延迟应接近。\n");
    return 0;
}

编译与运行


nvcc -arch=sm_80 -o nvlink_latency nvlink_latency.cu
./nvlink_latency

编译要求CUDA Toolkit 11.0+,运行环境需为多GPU服务器(如DGX系列或配备NVSwitch的HGX平台)。

验证方法

运行程序后观察输出矩阵:

  • 若GPU对之间通过NVLink连接,延迟应明显低于PCIe连接(通常NVLink往返延迟在1~3微秒量级,PCIe在5微秒以上);
  • 同一NVSwitch域内的GPU对延迟应基本一致,跨域或经PCIe的GPU对延迟会显著升高;
  • 程序输出的连接类型(NVLink/PCIe)应与nvidia-smi topo -m命令显示的拓扑一致。

判定标准:程序能正确枚举所有GPU组合,对支持P2P的GPU对输出非零延迟值,且延迟相对关系与物理拓扑相符(NVLink路径延迟低于PCIe路径)。

NVLink多GPU通信延迟测试拓扑与数据流 GPU 0 SM 0 GPU 1 SM 1 GPU 2 SM 2 GPU 3 SM 3 NVSwitch 交叉开关矩阵 延迟: 1.2μs 延迟: 1.3μs 延迟: 1.1μs 延迟: 1.2μs PCIe备用 PCIe备用 测量流程: 1. 枚举GPU对 2. 检查P2P支持 3. 分配64B缓冲区 4. 往返拷贝计时 5. 取最小延迟 6. 输出矩阵 图例 NVLink路径 PCIe路径 GPU节点

拓扑分析要点

程序输出的延迟矩阵可直接用于拓扑分析:

  • 若所有GPU对延迟接近(如1~2μs),说明所有GPU处于同一NVSwitch域,通信路径对称;
  • 若部分GPU对延迟明显偏高,可能跨越了不同的NVSwitch或经过CPU PCIe桥接;
  • 对比nvidia-smi topo -m输出的拓扑图,可验证延迟数据与物理连接的一致性。

该程序为诊断多GPU应用通信瓶颈提供了量化依据,可指导任务调度与数据放置策略优化。