核心问题: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 P2P | 200-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. 总结
-
P2P = GPU直接传输,不经过CPU
- 数据传输由GPU DMA引擎完成
- CPU只负责提交命令,不参与数据搬运
- CPU在传输过程中可以做其他工作
-
传输路径可能是:
- ✨ XGMI/Infinity Fabric(最快,200-400 GB/s)
- 🔧 PCIe(较快,16-32 GB/s)
- 都不经过CPU核心
-
无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)
241

被折叠的 条评论
为什么被折叠?



