PCIe与DMA原理:PCIe拓扑、DMA传输机制、GPU与网卡/存储设备的数据通路
核心原理:PCIe拓扑结构与地址域
PCIe(Peripheral Component Interconnect Express)是GPU与外部设备通信的物理基础。理解PCIe拓扑,首先要抓住三个关键角色:Root Complex(根复合体,RC)、Switch(交换器)和Endpoint(端点设备)。RC位于CPU侧,是PCIe域的管理者,负责生成事务请求、处理地址映射和中断路由;Endpoint是实际的功能设备,如GPU、NVMe SSD、网卡(NIC);Switch则负责在多个设备之间扩展连接并转发数据包,它本身不产生事务,只做路由。
在拓扑中,每个设备通过Link(链路)连接,链路宽度有x1、x4、x8、x16等,每条lane(通道)为全双工串行差分对。GPU通常以x16链路直连RC或通过Switch上行,以获得最大带宽。一个典型的服务器拓扑中,CPU0的RC下挂两个GPU和一个NVMe Switch,CPU1的RC下挂网卡和另一个GPU,两个CPU之间通过QPI/UPI互联。这种结构决定了数据从GPU到网卡可能跨越多个PCIe桥接器,每次跨越都会引入额外的延迟和带宽竞争。
PCIe采用基于地址的路由,所有事务(Memory Read/Write、Message等)都携带地址。这里必须区分两个地址域:CPU物理地址域和PCIe总线地址域。RC内部维护一张地址映射表,将CPU物理地址窗口映射到下游设备的BAR(Base Address Register)空间。GPU的显存、网卡的寄存器、NVMe的控制队列都被映射到不同的PCIe地址窗口。DMA操作的本质,就是让Endpoint设备直接访问这些地址窗口,而不需要CPU介入逐字节拷贝。
DMA传输机制:从CPU拷贝到引擎搬运
在没有DMA的年代,GPU要从网卡获取数据,CPU必须执行一系列load和store指令,先从网卡FIFO读取到寄存器,再写入显存。这种方式称为PIO(Programmed I/O),它占用CPU核心、效率极低。DMA(Direct Memory Access)改变了这一局面:DMA引擎(在GPU内部或网卡内部)可以独立于CPU,直接发起PCIe读写事务,将数据从源地址搬运到目的地址。
一次典型的DMA传输包含以下步骤:
- 描述符准备:CPU在内存中构造一个DMA描述符,包含源地址、目的地址、传输长度、控制标志。对于GPU与网卡通信,源地址通常是GPU显存的PCIe总线地址,目的地址是网卡Ring Buffer的总线地址。
- 门铃写入:CPU向设备的Doorbell寄存器写入一个值,通知设备“描述符已就绪”。这个写入操作本身也是一个PCIe Memory Write事务。
- 设备搬运:网卡DMA引擎读取描述符,解析地址和长度,然后发起PCIe Memory Read(从GPU显存读数据)和Memory Write(写入自身FIFO或片上缓存),整个过程不需要CPU参与数据移动。
- 完成通知:传输完成后,设备通过MSI-X中断或完成队列元素(CQE)通知CPU。CPU只需处理完成事件,而不是逐字节拷贝。
关键点在于:DMA传输的源和目的地址都必须是PCIe总线地址。如果GPU显存要作为DMA源,其地址必须被映射到某个PCIe BAR窗口,并且该窗口对下游设备可见。现代GPU通过Base Address Register(BAR)暴露显存窗口,网卡或NVMe控制器可以直接向该BAR发起读写。此外,IOMMU(Input/Output Memory Management Unit)负责将设备发起的DMA地址翻译为真实的物理地址,提供地址保护。在虚拟化场景中,IOMMU还负责设备直通时的地址隔离。
DMA描述符示例(简化):
struct dma_desc {
uint64_t src_addr; // GPU显存PCIe地址 0x8000_1000
uint64_t dst_addr; // 网卡Ring Buffer地址 0xB000_2000
uint32_t length; // 4096 字节
uint32_t flags; // 方向: 读/写, 中断使能
};
案例/场景:GPUDirect RDMA的数据通路
以GPUDirect RDMA(GDR)为例,它允许网卡直接读写GPU显存,绕过CPU和主机内存。传统路径是:网卡 → 主机内存 → CPU拷贝 → 显存,数据经过两次PCIe传输和一次内存拷贝。GDR的路径是:网卡DMA引擎 → PCIe Switch → GPU显存,数据只经过一次PCIe传输。
具体到数据流:
- 发送方向(GPU → 网卡):GPU内核执行完计算,数据驻留在显存。CPU调用
ibv_post_send,其中sge(Scatter-Gather Element)的地址字段填写GPU显存的PCIe总线地址(通过cuMemGetAddressRange或ibv_reg_mr注册)。网卡收到门铃后,其DMA引擎直接发起对GPU显存的Memory Read,数据经PCIe Switch流入网卡,封装为RDMA报文发出。 - 接收方向(网卡 → GPU):网卡收到RDMA数据,DMA引擎直接向GPU显存发起Memory Write。GPU显存地址在接收描述符中预先指定。数据到达显存后,GPU内核通过
cudaStreamSynchronize感知数据就绪。
这个过程中,PCIe Switch的带宽分配至关重要。如果多个GPU同时与网卡通信,Switch内部仲裁器需要公平调度。实测中,使用GDR的单条RDMA Write带宽可以达到接近PCIe Gen4 x16的理论上限(约25GB/s),而传统路径通常只能达到12~15GB/s,且CPU占用率从80%降至5%以下。
| 路径类型 | 数据经过节点 | PCIe跳数 | CPU参与 | 相对带宽 |
|---|---|---|---|---|
| 传统拷贝 | 网卡→主机内存→CPU拷贝→显存 | 2次(网卡↔内存, 内存↔显存) | 高(逐字节拷贝) | 1x |
| GDR | 网卡→PCIe Switch→显存 | 1次 | 仅提交/完成 | 1.8~2.2x |
误区/复盘:DMA地址与同步陷阱
误区一:认为DMA传输不需要CPU参与。实际上,DMA虽然不搬运数据,但描述符的构造、门铃写入、完成轮询仍然需要CPU。如果CPU被其他任务抢占,门铃写入延迟会导致DMA启动变慢。在高性能场景中,常使用CPU忙等门铃或写合并(Write Combining)来降低延迟。
误区二:忽略PCIe地址与CPU物理地址的区别。在GDR中,sge.addr必须填写GPU显存映射到PCIe域的总线地址,而不是CPU虚拟地址。如果误填CPU虚拟地址,网卡DMA会访问错误的内存区域,导致数据损坏或页错误。正确做法是使用cuMemHostRegister或ibv_reg_mr时指定IBV_ACCESS_LOCAL_WRITE,并获取对应的lkey和addr。
误区三:忽视DMA完成同步。DMA是异步操作,数据到达显存后,GPU内核不能立即读取,必须通过cudaStreamSynchronize或cudaEventQuery确保DMA完成。在RDMA场景中,网卡发出完成通知(CQE)后,还需确认GPU侧可见性——因为PCIe写事务可能被Switch缓冲,GPU的L2缓存可能尚未失效。此时需要插入__threadfence_system()或使用原子操作刷新缓存。
复盘教训:在一次多GPU训练中,我们使用GDR让网卡直接写入GPU0显存,但GPU1通过NVLink读取GPU0显存时出现数据不一致。原因在于:网卡DMA写GPU0显存后,GPU0的L2缓存未失效,GPU1通过NVLink读到的是旧数据。解决方案是在DMA完成后,GPU0执行一次cudaDeviceSynchronize并配合cuCtxSynchronize,确保L2缓存行失效。这个案例说明,DMA传输不仅涉及PCIe路径,还涉及缓存一致性协议。
最后,复盘PCIe拓扑设计时,应遵循以下原则:将高频通信的设备放在同一Switch下,减少跨RC的UPI跳数;为GPU预留独立x16链路,避免与NVMe争抢带宽;在BIOS中启用Above 4G Decoding和SR-IOV,确保DMA地址范围覆盖完整64位地址空间。这些细节决定了DMA能否真正发挥硬件性能。