引言
在 CPU 上,“读一个内存地址”是教科书里有标准答案的问题:经过 TLB 和页表把虚拟地址翻译成物理地址,再按 L1、L2、L3、DRAM 的顺序逐级下探,纳秒级完成。但把主语换成 GPU,这件事的复杂度立刻上了一个台阶——因为数据可能根本不在显卡上,而显卡自己也有一套完整的内存管理系统。最近,做 AI 推理基础设施的 Doubleword 发了一篇博客《What happens when a GPU reads memory》,把这条路径完整拆了一遍。问题看似基础,答案却直接指向今天 AI 推理性能的核心矛盾。
一次读取要过的三道关
顺着文章的思路,一次 GPU 内存读取大致要闯三关。
第一关:地址翻译。 现代 GPU 和 CPU 一样工作在虚拟地址空间里。自 CUDA 引入统一虚拟寻址(UVA)以来,主机和设备上的指针都落在同一个地址空间中,由 GPU 自己的 MMU 和页表负责翻译。页表本身常驻显存,TLB 未命中就意味着多跑几趟显存;为了缓解 TLB 压力,GPU 普遍使用 2MB 大页。这也是为什么内核里数据的摆放与对齐会实打实地影响性能,翻译开销从来不是免费的。
第二关:搬运数据。 对独立显卡来说,如果数据还躺在主机内存里,就必须跨过 PCIe 总线。这类传输由专门的 DMA 拷贝引擎执行,而主机那一端的内存状态决定了快慢:普通分页内存不能被硬件直接访问,驱动得先把数据复制进一块锁页的中转缓冲区;只有用 cudaMallocHost 之类接口分配的锁定内存,才能让 DMA 直达。带宽的量级差异更关键:PCIe 4.0 x16 约 32GB/s、5.0 约 64GB/s,而 H100 的 HBM 带宽在 3TB/s 以上——显存与总线之间差着一到两个数量级,凡是频繁跨芯片搬数据的负载,瓶颈几乎注定在总线上。
第三关:缺页与迁移。 CUDA 的 Unified Memory 把“两个地址空间”进一步抹平:开发者只管拿到一个指针,数据在主机与设备之间由缺页机制按需迁移,GPU 侧的页错误会触发驱动把页面搬过 PCIe。这极大简化了编程,但缺页迁移的代价比一次本地读取高几个数量级,密集缺页会带来明显抖动。实践中可以用预取(cudaMemPrefetchAsync)和内存提示压低缺页频率;而在 Grace Hopper 这类通过 NVLink-C2C 把 CPU 与 GPU 连成一致性系统的平台上,迁移成本被大幅摊薄,“统一内存”才真正接近它当初的承诺。
为什么此刻值得关心
这篇文章出现的时机并不偶然。模型体积超过单卡显存已是常态:权重放不下就得卸载到主机内存甚至 NVMe,KV cache 存在哪里直接决定吞吐;显卡厂商推 HBM、推一致性互连,应用层做预取和分层缓存,本质上都是在同一条路径上做文章。对做推理服务的人来说,理解“GPU 到底是怎么读到数据的”,约等于理解自己系统的性能上限来自哪里——Nsight 系列工具里那些 PCIe 吞吐与 TLB 相关的计数器,量的正是这条路径。
简短点评
这类“基础问题”式的技术文章最见功力:它不教你任何新 API,却能改变你看待整台机器的方式。抽象总会泄漏,而内存层次是最常泄漏的那一层——尤其在显存按字节计较的 AI 时代,一次读取背后页表、DMA 与缺页的接力,本身就是性能与瓶颈的全部故事。无论你是写 CUDA 内核,还是搭建推理服务,这篇文章都值得放进书签。