GPU P2P 数据传输机制详解

核心问题:P2P是否意味着不经过CPU?

简短回答:是的,但传输路径不只是PCIe。


1. 有P2P vs 无P2P 的区别

1.1 有P2P支持(直接传输)

GPU0 VRAM ──────────> GPU1 VRAM
          直接DMA传输

特点:

  • 不经过CPU内存
  • 不经过CPU参与数据搬运
  • 使用GPU的DMA引擎直接传输
  • 只需一次拷贝

代码路径:

// rocvirtual.cpp
if (p2pAllowed) {
  // 直接 GPU-to-GPU 拷贝
  result = blitMgr().copyBuffer(*srcDevMem, *dstDevMem, 
                                srcOrigin, dstOrigin, size);
}

1.2 无P2P支持(staging buffer方式)

GPU0 VRAM ──> CPU内存 ──> GPU1 VRAM
           (staging buffer)
           两次拷贝

特点:

  • 需要CPU内存作为中转(staging buffer)
  • 需要两次拷贝:GPU0→CPU、CPU→GPU1
  • 性能损失显著
  • ⚠️ CPU不参与数据搬运(仍使用DMA),但需要CPU内存

代码路径:

// rocvirtual.cpp - 无P2P时的回退路径
else {
  // 同步当前队列,因为P2P staging使用设备队列进行传输
  releaseGpuMemoryFence();
  
  amd::ScopedLock lock(dev().P2PStageOps());
  
  // 获取staging buffer
  Memory* dstStgMem = static_cast<Memory*>(
      dev().P2PStage()->getDeviceMemory(*cmd.source().getContext().devices()[0]));
  Memory* srcStgMem = static_cast<Memory*>(
      dev().P2PStage()->getDeviceMemory(*cmd.destination().getContext().devices()[0]));
  
  // 两步传输
  // 步骤1: GPU0 → CPU staging buffer
  result &= srcDevMem->dev().xferMgr().copyBuffer(*srcDevMem, *dstStgMem, 
                                                   srcOrigin, stageOffset, cpSize);
  
  // 步骤2: CPU staging buffer → GPU1
  result &= dstDevMem->dev().xferMgr().copyBuffer(*srcStgMem, *dstDevMem, 
                                                   stageOffset, dstOrigin, cpSize);
}

Staging Buffer分配:

// rocdevice.cpp::Device::init()
// 只在没有任何P2P支持时才分配staging buffer
if ((devices.size() >= 1) && !p2p_available) {
  amd::Buffer* buf = new (*glb_ctx_) amd::Buffer(
      *glb_ctx_, 
      CL_MEM_ALLOC_HOST_PTR,  // CPU可见内存
      kP2PStagingSize          // 默认1MB
  );
  if ((buf != nullptr) && buf->create()) {
    p2p_stage_ = buf;  // 全局staging buffer
  }
}

2. P2P传输的物理路径

2.1 通过PCIe(最常见)

     CPU
      |
  PCIe Switch
   /       \
GPU0      GPU1

传输路径: GPU0 → PCIe Switch → GPU1
特点:

  • 不经过CPU
  • 速度受限于PCIe带宽(16-32 GB/s)
  • 延迟较XGMI高

2.2 通过XGMI/Infinity Fabric(AMD高端卡)

GPU0 ←─XGMI─→ GPU1

传输路径: GPU0 ↔ GPU1(专用高速互连)
特点:

  • 完全不经过PCIe
  • 速度可达 200-400 GB/s
  • 延迟极低(类似GPU内部内存访问)

代码中如何区分:

// rocdevice.cpp::Device::findLinkInfo()
hsa_status_t status = Hsa::agent_memory_pool_get_info(
    bkendDevice_, pool, 
    HSA_AMD_AGENT_MEMORY_POOL_INFO_NUM_LINK_HOPS,  // 查询hop数
    &hops
);

// hops = 1: 直接连接(可能是XGMI)
// hops > 1: 通过中间节点(可能是PCIe switch)

2.3 Large BAR场景

如果GPU支持Large BAR(Resizable BAR):

CPU可以直接访问GPU的全部VRAM(不只是256MB窗口)

但这不影响GPU-to-GPU P2P传输,Large BAR主要优化CPU↔GPU访问。

检测代码:

// rocdevice.cpp::iterateGpuMemoryPoolCallback()
hsa_amd_memory_pool_access_t tmp{};
Hsa::agent_memory_pool_get_info(dev->cpu_agent_info_->agent, pool,
                                HSA_AMD_AGENT_MEMORY_POOL_INFO_ACCESS, &tmp);

if (tmp == HSA_AMD_MEMORY_POOL_ACCESS_NEVER_ALLOWED) {
  dev->info_.largeBar_ = false;  // 不支持Large BAR
} else {
  dev->info_.largeBar_ = ROC_ENABLE_LARGE_BAR;
}

3. 性能对比

3.1 不同传输方式的性能

传输方式典型带宽延迟CPU参与
XGMI P2P200-400 GB/s~1-2 μs❌ 不参与
PCIe 4.0 P2P~32 GB/s~5-10 μs❌ 不参与
PCIe 3.0 P2P~16 GB/s~5-10 μs❌ 不参与
Staging Buffer<10 GB/s~50-100 μs⚠️ 需要CPU内存
CPU拷贝<5 GB/s>100 μs✅ CPU执行memcpy

3.2 实测示例(MI250X,4卡XGMI拓扑)

# P2P带宽测试
$ rocm-bandwidth-test

# 同一GPU内拷贝
GPU0 → GPU0: 1600 GB/s (HBM内部)

# XGMI P2P
GPU0 → GPU1: 380 GB/s  (直接XGMI连接)
GPU0 → GPU2: 340 GB/s  (1-hop XGMI)

# 无P2P(如果禁用)
GPU0 → GPU1: 8 GB/s   (通过staging buffer)

4. CPU是否参与?详细说明

4.1 有P2P:CPU完全不参与数据传输

// GPU发起DMA传输,CPU只负责提交命令
hipMemcpyPeerAsync(dst_gpu1, gpu1, src_gpu0, gpu0, size, stream);
  ↓
提交DMA命令到GPU0的命令队列
  ↓
GPU0的DMA引擎直接将数据发送到GPU1
  ↓
传输过程中CPU可以做其他事情
  ↓
GPU1完成信号通知CPU(可选)

CPU角色:

  • 提交传输命令
  • 等待完成信号(如果同步)
  • 不参与实际数据搬运

4.2 无P2P:CPU仍然不搬运数据,但需要CPU内存

// 仍使用DMA,但分两步
步骤1: GPU0 DMA → CPU staging buffer
  ↓
步骤2: CPU staging buffer → GPU1 DMA

关键点:

  • CPU内存作为中转站(pinned memory)
  • 数据搬运仍由GPU的DMA引擎完成
  • CPU不执行memcpy,只是内存空间被借用

对比真正的CPU拷贝:

// 这才是CPU真正参与数据搬运
void* tmp = malloc(size);
hipMemcpy(tmp, gpu0_ptr, size, hipMemcpyDeviceToHost);  // DMA
memcpy(tmp2, tmp, size);  // CPU执行,极慢!
hipMemcpy(gpu1_ptr, tmp2, size, hipMemcpyHostToDevice); // DMA

5. 如何判断是否使用了P2P?

5.1 编程方式检查

#include <hip/hip_runtime.h>

int gpu0 = 0, gpu1 = 1;
int canAccess = 0;

// 检查是否支持P2P
hipDeviceCanAccessPeer(&canAccess, gpu0, gpu1);

if (canAccess) {
    printf("GPU %d 可以 P2P 访问 GPU %d\n", gpu0, gpu1);
    
    // 启用P2P访问
    hipSetDevice(gpu0);
    hipDeviceEnablePeerAccess(gpu1, 0);
    
    // 现在可以直接传输
    hipMemcpyPeer(dst_gpu1, gpu1, src_gpu0, gpu0, size);
} else {
    printf("不支持P2P,将使用staging buffer\n");
}

5.2 系统级检查

# 查看拓扑信息
rocminfo | grep -A 10 "Link Type Info"

# 输出示例(支持P2P):
# Link Type: XGMI
# Numa Distance: 15
# Weight: 15
# Bandwidth: 400000 (MB/s)

# 输出示例(不支持P2P):
# Link Type: N/A
# Access: No

5.3 运行时监控

# 使用rocprof监控P2P传输
rocprof --hip-trace ./my_app

# 查看日志
# 有P2P: CopyDeviceToDevice (direct)
# 无P2P: CopyDeviceToHost + CopyHostToDevice

6. 典型场景分析

场景1:单机4卡训练(XGMI连接)

GPU0 ←XGMI→ GPU1
  ↑           ↑
 XGMI       XGMI
  ↓           ↓
GPU2 ←XGMI→ GPU3

传输特性:

  • 所有GPU间P2P: ✅
  • 传输路径: 全部通过XGMI
  • 不经过CPU: ✅
  • 不经过PCIe: ✅(XGMI专用通道)
  • 性能: 极佳(200-400 GB/s)

场景2:双GPU工作站(PCIe连接)

    CPU
     ↓
  PCIe Switch
   ↙     ↘
GPU0    GPU1

传输特性:

  • GPU间P2P: ✅
  • 传输路径: PCIe Switch
  • 不经过CPU: ✅(不经过CPU内存和CPU核心)
  • 经过PCIe: ❌(通过PCIe但不经过CPU)
  • 性能: 中等(16-32 GB/s)

场景3:集成GPU + 独立GPU

CPU
 ├─ iGPU (集成显卡)
 └─ PCIe → dGPU (独立显卡)

传输特性:

  • GPU间P2P: ❌(通常不支持)
  • 传输路径: 使用staging buffer
  • CPU内存参与: ✅(但CPU核心不搬运数据)
  • 性能: 差(<10 GB/s)

7. 关键代码片段

7.1 P2P检测和启用

// hip_peer.cpp::hipDeviceEnablePeerAccess()
hipError_t hipDeviceEnablePeerAccess(int peerDeviceId, unsigned int flags) {
  int deviceId = hip::getCurrentDevice()->deviceId();
  int canAccess = 0;
  
  // 检查硬件是否支持
  if ((hipSuccess != canAccessPeer(&canAccess, deviceId, peerDeviceId)) || 
      (canAccess == 0)) {
    HIP_RETURN(hipErrorInvalidDevice);
  }
  
  // 在HSA层启用P2P
  amd::Device* device = g_devices[deviceId]->asContext()->devices()[0];
  amd::Device* peer_device = g_devices[peerDeviceId]->asContext()->devices()[0];
  
  // 关键:授权peer访问当前设备的内存
  peer_device->enableP2P(device);
  
  return hipSuccess;
}

7.2 实际传输路径选择

// rocvirtual.cpp::submitCopyMemory()
bool VirtualGPU::submitCopyMemory(amd::CopyMemoryCommand& cmd) {
  // 检查是否允许P2P
  bool p2pAllowed = (srcDevMem->dev().P2PAccessAllowed(*dstDevMem)) &&
                    (dstDevMem->dev().P2PAccessAllowed(*srcDevMem));
  
  if (p2pAllowed) {
    // 直接GPU-to-GPU传输(一步到位)
    result = blitMgr().copyBuffer(*srcDevMem, *dstDevMem, 
                                  srcOrigin, dstOrigin, size);
  } else {
    // 使用staging buffer(两步传输)
    releaseGpuMemoryFence();
    amd::ScopedLock lock(dev().P2PStageOps());
    
    Memory* stageMem = static_cast<Memory*>(dev().P2PStage()->...);
    
    // 步骤1: GPU0 → staging
    result &= srcDevMem->dev().xferMgr().copyBuffer(*srcDevMem, *stageMem, ...);
    
    // 步骤2: staging → GPU1
    result &= dstDevMem->dev().xferMgr().copyBuffer(*stageMem, *dstDevMem, ...);
  }
}

8. 总结

  1. P2P = GPU直接传输,不经过CPU

    • 数据传输由GPU DMA引擎完成
    • CPU只负责提交命令,不参与数据搬运
    • CPU在传输过程中可以做其他工作
  2. 传输路径可能是:

    • XGMI/Infinity Fabric(最快,200-400 GB/s)
    • 🔧 PCIe(较快,16-32 GB/s)
    • 不经过CPU核心
  3. 无P2P时的回退机制:

    • 使用CPU内存作为staging buffer
    • 仍然用DMA传输(不是CPU memcpy)
    • 但需要两次拷贝,性能大幅下降

📊 性能优先级:

XGMI P2P (400 GB/s) > PCIe P2P (32 GB/s) > Staging Buffer (8 GB/s) > CPU memcpy (5 GB/s)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值