Hotdry.
ai-systems

PCIe BAR0 MMIO 驱动 GPU 直接 NVMe 读:70B 模型单卡推理的硬件寄存器级实现

深入解析通过 PCIe BAR0 MMIO 让 GPU 直接发起 NVMe 读操作的工程细节,涵盖寄存器编程模型、队列操作时序与流式推理关键参数。

在大语言模型推理场景中,如何在有限显存下运行超出显存容量的模型是一个核心挑战。传统方案依赖 CPU 在 NVMe 与 GPU 之间搬运数据,形成明显的性能瓶颈。gpu-nvme-direct 项目展示了另一种可能:让 CUDA 内核直接通过 PCIe BAR0 MMIO 发起 NVMe 命令,将 GPU 变身为自主的 I/O 处理器,从而在单张 RTX 3090 上实现 70B 模型的流式推理。

硬件架构与 PCIe 拓扑

该实现采用消费级硬件搭建:NVIDIA RTX 3090(GA102,sm_86,24GB)通过 PCIe x16 连接到 AMD Ryzen 7 5800X(Zen 3,AM4),主板为华硕 ROG STRIX B450-F GAMING II。测试用 NVMe 设备为 WD SN740 512GB(Gen4 x4,在 B450 上降级为 Gen3 x4),系统盘为 WD SN530 1TB(Gen3 x4)。

值得注意的是,NVIDIA 在 GeForce 系列显卡上禁用了原生 PCIe P2P DMA 功能,这意味着消费级 GPU 无法像专业计算卡那样直接通过 GPUDirect 访问 NVMe 设备。该项目采用了分级降级策略:Tier 1 模式下队列和数据缓冲区均位于主机固定内存(pinned memory),仅使用 GPU 的 MMIO 能力提交命令和轮询完成队列;Tier 2 需要对 NVIDIA 开源内核模块打补丁,才能将数据缓冲区放置在 GPU VRAM 中;Tier 3 则是完整的 BaM 架构,队列和数据均位于 GPU 显存。

从实测数据来看,SN740 在 Gen3 x4 模式下可持续读取 3.35 GB/s,达到了该接口理论带宽的 96%;SN530 则为 2.1 GB/s。

BAR0 MMIO 编程模型

理解 GPU 直接 NVMe 读操作的核心在于掌握 BAR0 空间与 NVMe 命令队列的工作机制。现代 NVMe 控制器通过 PCIe Base Address Register 0(BAR0)暴露一组内存映射寄存器,GPU 内核可以通过这些寄存器直接与控制器交互。

NVMe 控制器在 BAR0 中维护两类关键队列:提交队列(Submission Queue,SQ)和完成队列(Completion Queue,CQ)。提交队列是一个生产者 - 消费者环形缓冲区,生产者(此场景下为 GPU 内核)写入命令项,消费者(NVMe 控制器)读取并执行。每个命令项包含操作码、数据物理页寄存器列表(PRP)地址、起始 LBA 等字段。完成队列则记录每个命令的执行状态,使用相位位(phase bit)区分新旧条目。

GPU 内核执行一次 NVMe 读的完整时序如下:首先在内核栈或共享内存中构造 NVMe SQ 条目,通常是 64 字节的 READ 命令结构体,包含目标 LBA、读取扇区数、PRP 地址等;随后调用 __threadfence_system() 确保所有内存操作对 PCIe 可见;接着使用 PTX 指令 st.mmio.sys 向提交队列尾部门铃寄存器(Doorbell)写入新条目索引,通知控制器有未处理命令;最后使用 ld.mmio.sys 轮询完成队列头的相位位或状态字段,直至检测到对应命令完成。

这种设计的关键在于将传统 I/O 模型中的软件中断替换为 GPU 硬件轮询,避免了 CPU 上下文切换和内核调度开销。

流式推理参数与三层缓存策略

在 70B 参数模型的单卡推理场景中,显存仅够容纳模型的一小部分权重。该实现采用了三层缓存架构来最大化 I/O 效率:热层(hot layer)常驻 VRAM,温层(warm layer)驻留在主机固定内存,冷层(cold layer)位于 NVMe 设备。

具体到 70B Q6_K 量化模型,单层权重约为 15-20 GB,显然无法全部装入 24 GB 显存。流式推理利用了 Transformer 架构的层序特性:每一层计算仅需要当前层的权重和 KV 缓存,其他层可以卸载到存储设备。通过双缓冲(double-buffered streaming)技术,可以在当前层进行推理计算的同时预取下一层权重,实现计算与 I/O 的流水线重叠。

根据项目文档,关键参数包括:管道深度(pipeline depth)设为 32,即同时有 32 个 NVMe 读操作在 flight;MDTS(Maximum Data Transfer Size)限制为 1024 KB,单次 NVMe 命令最大传输量受控制器限制;读取粒度通常为 4 KB 扇区,以匹配 NVMe 的物理寻址单元。

在 70B Q6_K 模型上,该方案实测推理速度约为 0.2 tokens/s,相比传统的 mmap 方式提升了约 33 倍。这一数字虽然远低于高端服务器运行 FP16 权重的速度,但已证明消费级硬件配合精细的 I/O 调度能够实现可行的单卡大模型推理。

驱动配置与安全考量

要让 GPU 直接访问 NVMe 设备的 BAR0 空间,需要一系列系统级配置。首先必须禁用 IOMMU(Input-Output Memory Management Unit),否则 DMA 地址转换会阻断 GPU 与 NVMe 之间的直接通信。其次需要加载 VFIO(Virtual Function I/O)内核模块并将 NVMe 设备绑定到 vfio-pci 驱动,从而将设备从内核原生驱动中解绑,交给用户态程序直接控制。

项目提供的脚本 setup_vfio.sh 负责完成设备隔离的初始化工作。项目还使用了打补丁后的 NVIDIA DKMS 和 CUDA 头文件,以支持 cudaHostRegisterIoMemory 功能,该功能允许注册主机内存作为可 DMA 寻址的 IO 内存。

这种配置的局限性也很明显:它需要定制化的内核模块、非标准的驱动配置,且在生产环境中可能存在安全隔离问题。因此更适合作为研究原型或专用推理引擎,而非通用计算平台。

工程落地的关键阈值

如果希望在类似硬件上复现或优化该方案,以下参数值得特别关注:NVMe 设备的 MDTS 决定了单次 I/O 的最大粒度,消费级设备通常为 512 KB 或 1024 KB;队列深度(Queue Depth)影响吞吐量饱和点,PCIe Gen3 x4 带宽约为 3.5 GB/s,需要深度至少为 32 才能跑满带宽; PCIe 拓扑上,GPU 与 NVMe 应位于同一 root complex 或通过 PCIe switch 互联,跨 CPU 节点的访问会引入显著延迟;使用 Q6_K 或更激进的量化(如 Q4_K_M)可以显著减少单层数据量,从而降低 I/O 带宽需求。

该项目证明了一个关键论点:在显存受限时,将 NVMe 视为显存的延伸而非单纯的启动盘,配合细粒度的 BAR0 MMIO 编程,能够构建出端到端绕过 CPU 的推理数据路径。这为消费级硬件上的大模型部署提供了一条可操作的工程路径。

资料来源:该项目代码与文档托管于 GitHub(xaskasdf/gpu-nvme-direct),硬件配置与性能数据均来自项目 README。

查看归档