第8板块·第3节:设备间拷贝与点对点(P2P)传输

学习目标

学完本节你将能够:

  • 理解多 GPU 环境下数据在设备间传输的基本方式
  • 掌握 cudaMemcpyPeercudaMemcpyPeerAsync 的使用方法
  • 了解点对点(P2P)传输的硬件支持和性能特点
  • 使用 cudaDeviceEnablePeerAccess 启用设备间直接访问
  • 分析 P2P 传输与主机中转传输的性能差异
  • 在多 GPU 程序中合理选择数据传输路径

1. 多 GPU 数据传输的两种方式

当数据需要在两个 GPU 之间移动时,有两种基本方式:

1.1 主机中转(Staging through Host)

复制代码
GPU0 → 主机内存 → GPU1

使用两次 cudaMemcpy 完成:

复制代码
cudaMemcpy(h_buf, d_data0, size, cudaMemcpyDeviceToHost);   // GPU0 → 主机
cudaMemcpy(d_data1, h_buf, size, cudaMemcpyHostToDevice);   // 主机 → GPU1

特点:

  • 需要分配主机缓冲区
  • 经过 PCIe 两次,带宽减半
  • 所有支持 CUDA 的 GPU 都可以使用

1.2 点对点直接传输(P2P)

复制代码
GPU0 → GPU1(通过 PCIe 或 NVLink)

使用 cudaMemcpyPeercudaMemcpyPeerAsync

复制代码
cudaMemcpyPeer(dst, dstDevice, src, srcDevice, size);

特点:

  • 不需要主机缓冲区
  • 如果硬件支持 P2P,可以直接通过 PCIe 或 NVLink 传输,性能更高
  • 需要检查硬件是否支持,并启用 P2P 访问

2. 检查 P2P 支持与启用

2.1 检查设备属性

复制代码
int deviceCount;
cudaGetDeviceCount(&deviceCount);

for (int i = 0; i < deviceCount; i++) {
    cudaDeviceProp prop;
    cudaGetDeviceProperties(&prop, i);
    printf("Device %d: %s, PCIe Gen %d, NVLink %d\n",
           i, prop.name, prop.pciBusID, prop.nvlink);
}

关键属性:

  • prop.unifiedAddressing:是否支持统一寻址
  • prop.managedMemory:是否支持统一内存
  • prop.concurrentManagedAccess:是否支持并发统一内存访问
  • prop.asyncEngineCount:异步引擎数量
  • prop.nvlink:是否支持 NVLink(较新 CUDA 版本)

2.2 检查 P2P 能力

复制代码
int canAccessPeer = 0;
cudaDeviceCanAccessPeer(&canAccessPeer, device0, device1);
if (canAccessPeer) {
    cudaSetDevice(device0);
    cudaDeviceEnablePeerAccess(device1, 0);   // 允许 device0 访问 device1
}

cudaDeviceCanAccessPeer 返回 1 表示两个设备之间可以启用 P2P。但即使支持,某些情况下也可能通过主机中转(例如跨 PCIe 根复合体)。

3. P2P 传输 API 的使用

3.1 同步传输 cudaMemcpyPeer

复制代码
cudaError_t cudaMemcpyPeer(void* dst, int dstDevice,
                           const void* src, int srcDevice,
                           size_t count);
  • dst:目标设备指针
  • dstDevice:目标设备 ID
  • src:源设备指针
  • srcDevice:源设备 ID
  • count:字节数

调用前必须确保两个设备之间的 P2P 访问已启用(或当前设备上下文正确)。

3.2 异步传输 cudaMemcpyPeerAsync

复制代码
cudaError_t cudaMemcpyPeerAsync(void* dst, int dstDevice,
                                const void* src, int srcDevice,
                                size_t count, cudaStream_t stream);
  • 在指定流中异步执行,便于与计算重叠。

3.3 完整示例:两 GPU 间数据传输

复制代码
#include <cstdio>
#include <cuda_runtime.h>

#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(EXIT_FAILURE); \
        } \
    } while (0)

int main() {
    int deviceCount;
    CUDA_CHECK(cudaGetDeviceCount(&deviceCount));
    if (deviceCount < 2) {
        printf("Need at least 2 GPUs\n");
        return -1;
    }

    int dev0 = 0, dev1 = 1;
    // 检查 P2P 支持
    int canAccessPeer = 0;
    CUDA_CHECK(cudaDeviceCanAccessPeer(&canAccessPeer, dev0, dev1));
    printf("P2P between GPU0 and GPU1: %s\n", canAccessPeer ? "Yes" : "No");

    if (!canAccessPeer) {
        printf("P2P not supported, exiting.\n");
        return -1;
    }

    // 启用 P2P 访问
    CUDA_CHECK(cudaSetDevice(dev0));
    CUDA_CHECK(cudaDeviceEnablePeerAccess(dev1, 0));
    CUDA_CHECK(cudaSetDevice(dev1));
    CUDA_CHECK(cudaDeviceEnablePeerAccess(dev0, 0));

    const int N = 1 << 20;
    size_t bytes = N * sizeof(float);
    float *d_data0, *d_data1;
    CUDA_CHECK(cudaSetDevice(dev0));
    CUDA_CHECK(cudaMalloc(&d_data0, bytes));
    CUDA_CHECK(cudaSetDevice(dev1));
    CUDA_CHECK(cudaMalloc(&d_data1, bytes));

    // 初始化 d_data0(省略)
    // ...

    // P2P 传输:GPU0 → GPU1
    CUDA_CHECK(cudaMemcpyPeer(d_data1, dev1, d_data0, dev0, bytes));

    printf("P2P copy completed.\n");

    // 清理
    CUDA_CHECK(cudaSetDevice(dev0));
    CUDA_CHECK(cudaFree(d_data0));
    CUDA_CHECK(cudaSetDevice(dev1));
    CUDA_CHECK(cudaFree(d_data1));

    return 0;
}

4. P2P 与主机中转的性能对比

4.1 性能差异来源

  • P2P 传输:数据直接从 GPU0 到 GPU1,只经过一次总线。如果两个 GPU 通过 NVLink 连接,带宽远高于 PCIe。
  • 主机中转:需要两次 PCIe 传输(GPU0→主机,主机→GPU1),带宽减半,且主机内存带宽可能成为瓶颈。

4.2 实验设计

编写程序分别测量:

  • P2P 传输带宽(使用 cudaMemcpyPeer
  • 主机中转带宽(使用两次 cudaMemcpycudaMemcpyAsync

使用 cudaEvent 计时,并计算有效带宽。

NVLink 是 NVIDIA 的高速互联技术,提供比 PCIe 高数倍的带宽和更低的延迟。在支持 NVLink 的系统上,P2P 传输性能极大提升。

复制代码
// 使用 CUDA 的 NVLink 属性(较新 CUDA 版本)
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, deviceId);
// prop.nvlink 可能表示支持的 NVLink 数量

更详细的拓扑信息可以通过 nvidia-smi topo -m 命令查看。

5.2 对 P2P 的影响

  • 如果两个 GPU 通过 NVLink 连接,cudaDeviceCanAccessPeer 返回 true,且带宽较高。
  • 如果没有 NVLink,P2P 也可能通过 PCIe 工作,但带宽受 PCIe 限制。

6. 代码演示:P2P 与主机中转带宽对比

复制代码
#include <cstdio>
#include <cuda_runtime.h>
#include <chrono>

#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(EXIT_FAILURE); \
        } \
    } while (0)

int main() {
    int deviceCount;
    CUDA_CHECK(cudaGetDeviceCount(&deviceCount));
    if (deviceCount < 2) {
        printf("Need at least 2 GPUs\n");
        return -1;
    }
    int dev0 = 0, dev1 = 1;
    int canPeer = 0;
    CUDA_CHECK(cudaDeviceCanAccessPeer(&canPeer, dev0, dev1));
    if (!canPeer) {
        printf("P2P not supported\n");
        return -1;
    }
    CUDA_CHECK(cudaSetDevice(dev0));
    CUDA_CHECK(cudaDeviceEnablePeerAccess(dev1, 0));
    CUDA_CHECK(cudaSetDevice(dev1));
    CUDA_CHECK(cudaDeviceEnablePeerAccess(dev0, 0));

    const int N = 1 << 26;   // 64M floats = 256 MB
    size_t bytes = N * sizeof(float);
    float *d_data0, *d_data1, *h_buf;
    CUDA_CHECK(cudaSetDevice(dev0));
    CUDA_CHECK(cudaMalloc(&d_data0, bytes));
    CUDA_CHECK(cudaSetDevice(dev1));
    CUDA_CHECK(cudaMalloc(&d_data1, bytes));
    CUDA_CHECK(cudaMallocHost(&h_buf, bytes));   // 页锁定主机内存

    // 初始化 d_data0(省略)

    // 计时 P2P 传输
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 10; i++) {
        CUDA_CHECK(cudaMemcpyPeer(d_data1, dev1, d_data0, dev0, bytes));
    }
    CUDA_CHECK(cudaDeviceSynchronize());
    auto end = std::chrono::high_resolution_clock::now();
    double p2p_ms = std::chrono::duration<double, std::milli>(end - start).count() / 10.0;
    double p2p_bandwidth = (bytes / 1e9) / (p2p_ms / 1e3);   // GB/s

    // 计时主机中转传输
    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 10; i++) {
        CUDA_CHECK(cudaMemcpy(h_buf, d_data0, bytes, cudaMemcpyDeviceToHost));
        CUDA_CHECK(cudaMemcpy(d_data1, h_buf, bytes, cudaMemcpyHostToDevice));
    }
    CUDA_CHECK(cudaDeviceSynchronize());
    end = std::chrono::high_resolution_clock::now();
    double staging_ms = std::chrono::duration<double, std::milli>(end - start).count() / 10.0;
    double staging_bandwidth = (bytes / 1e9) / (staging_ms / 1e3);

    printf("P2P bandwidth: %f GB/s\n", p2p_bandwidth);
    printf("Staging bandwidth: %f GB/s\n", staging_bandwidth);
    printf("Speedup: %.2fx\n", p2p_bandwidth / staging_bandwidth);

    // 清理
    CUDA_CHECK(cudaSetDevice(dev0));
    CUDA_CHECK(cudaFree(d_data0));
    CUDA_CHECK(cudaSetDevice(dev1));
    CUDA_CHECK(cudaFree(d_data1));
    CUDA_CHECK(cudaFreeHost(h_buf));
    return 0;
}

预期结果:P2P 带宽通常高于主机中转(尤其有 NVLink 时),可能快 1.5~3 倍。

7. 课后练习

练习1:检查 P2P 支持

编写程序,枚举系统中所有 GPU 对,检查它们之间的 P2P 支持情况,并输出拓扑矩阵。

练习2:P2P 异步传输

使用 cudaMemcpyPeerAsync 将数据在两个 GPU 间传输,同时执行一个 Kernel,观察是否能够重叠。

练习3:性能对比

在你的多 GPU 环境上运行本节的带宽对比程序,记录 P2P 和主机中转的带宽,并分析你的硬件拓扑(使用 nvidia-smi topo -m)。

练习4:统一内存 vs P2P

比较使用统一内存(cudaMallocManaged)与 P2P 传输在多 GPU 间共享数据的性能差异。思考各自的适用场景。

练习5:错误处理

故意在不启用 P2P 的情况下调用 cudaMemcpyPeer,观察错误码,并编写正确的检查流程。

8. 下一步

下一节将进入 内存池与异步拷贝优化,你将学习:

  • 使用内存池减少分配/释放开销
  • 异步拷贝与流的高级应用
  • CUDA 的内存压缩与缓存管理
  • 多 GPU 环境下的内存管理策略
相关推荐
Omics Pro1 小时前
新型条件传输模型!虚拟细胞扰动预测
数据库·人工智能·算法·机器学习·自然语言处理
武子康1 小时前
两台机器跑 vLLM,什么时候才值得引入 Ray?
人工智能·llm·agent
AI技趣星球1 小时前
ChatGPT 杀进 Word:免费版可用,AI 办公落地进入深水区
人工智能
Raas1001 小时前
MAI Gateway(魔芋企业级AI网关)功能全解:AI网关支持流式输出吗?一文看懂AI网关能力矩阵
大数据·人工智能·gateway·mai gateway·企业级网关
wangqiaowq1 小时前
AI-Native(AI 原生) 指从产品、架构、交互到组织流程,从第一天起就以 AI 为核心来设计,而不是在传统软件上后加一个 AI 功能。
人工智能
AI深栈1 小时前
第 14 章 · LangGraph4j 入门:AI Agent 什么时候该上状态图
java·人工智能
学电子她就能回来吗1 小时前
把自然语言变成可制造 CAD:开源 SolidWorks Automation Skill 的工程化实践
人工智能·python·自动化·solidworks·mcp
爱奥尼欧1 小时前
【C++】map、set 的底层是什么?红黑树五条性质、插入旋转到模拟实现
开发语言·数据结构·c++
广西生活网1 小时前
启元机器人接入腾讯 WorkBuddy 具身智能打通虚实服务新链路
人工智能