深度解析塑造未来的技术文章。

显存不够用怎么办:GPU VRAM 溢出实战指南

GPU 显存是 AI 与图形负载的主要瓶颈。详解统一内存、显存卸载与 NVMe 交换的底层原理。

显卡溢出的发光数据顺着下方的存储盘洒落

机器学习里最常见的报错,往往不是 Python 的 traceback,也不是 shape 不匹配,而是 CUDA out of memory。模型太大、batch size 太高,或者中间激活值放不进 GPU 的显存(VRAM)。RTX 4090 有 24 GB 显存,而 70B 参数的模型用半精度加载需要约 140 GB。数字对不上,堆更大的显卡也只是把问题往后推,显存 80 GB 的 H100 同样装不下最大的模型。

但如果能用系统内存甚至 NVMe 存储来透明地扩展 GPU 显存呢?NVIDIA 的 Greenboost 以及类似项目正是这么做的,利用内存层级(显存 → 系统内存 → SSD)去跑那些按理说装不进你硬件的负载。性能上的取舍是真实存在的,但对很多场景来说出乎意料地可以接受。要理解这是怎么实现的,得先搞清楚 GPU 内存层级本身。

为什么 GPU 内存和 CPU 内存不一样

CPU 和 GPU 内存服务于本质不同的访问模式。CPU 负载对延迟敏感,单个线程需要某份数据时会阻塞等待它到达。CPU 缓存的设计目标,就是在随机访问模式下把延迟降到最低。

GPU 负载则对吞吐量敏感:成千上万个线程各自需要数据,GPU 可以在线程之间切换来掩盖延迟。GPU 内存(数据中心卡上是 HBM,消费级显卡上是 GDDR)是为带宽设计的,每秒交付海量数据,哪怕单次访问比 CPU 缓存命中慢得多。

Memory bandwidth comparison (approximate):
RTX 4090 GDDR6X:     1,000 GB/s
H100 HBM3:           3,350 GB/s
DDR5 System RAM:       50 GB/s
PCIe 5.0 x16:          64 GB/s (theoretical max)
NVMe SSD:               7 GB/s
The bandwidth cliff between VRAM and system RAM is ~20x.
Between VRAM and NVMe it's ~140x.
This is why naive offloading to system RAM kills performance —
you're trying to feed a 1,000 GB/s appetite through a 50 GB/s straw.

正是这个带宽差距,使得把 GPU 内存“虚拟化”(像 CPU 把数据从磁盘换页到内存那样,把数据换页到系统内存)行不通。CPU 处理缺页中断可能只慢 10 倍左右;而 GPU 访问系统内存而非显存时,带宽会下降约 20 倍。对于带宽受限的负载(大多数 ML 推理都是如此)来说,这就意味着 20 倍的减速。

显存卸载究竟如何工作

诀窍不在于把系统内存当成慢速显存来用,而是在 GPU 需要之前,提前把数据从系统内存预取到显存里,让传输延迟被计算过程掩盖掉。这是所有实用显存扩展技术背后的关键思路。

神经网络推理是按层顺序进行的。GPU 在处理第 5 层时,就已经知道下一个要处理的是第 6 层。一个聪明的卸载系统可以在第 5 层计算的同时,开始把第 6 层的权重从系统内存传到显存。如果计算耗时比传输长(大层通常如此),传输就被完全掩盖了,GPU 从不会停顿。

# Conceptual overlap of compute and transfer
# (simplified pseudocode)
def inference_with_offloading(model, input_data):
# Only 2 layers fit in VRAM at a time
# Rest are in system RAM
for i, layer in enumerate(model.layers):
# Start async transfer of NEXT layer while computing current
if i + 1 < len(model.layers):
async_transfer_to_gpu(model.layers[i + 1])
# Compute on current layer (GPU is busy, transfer happens in parallel)
output = layer.forward(input_data)
# Evict current layer from VRAM (it's done)
transfer_to_ram(layer)
# Wait for next layer transfer to complete (usually already done)
sync_transfer()
input_data = output
return output

这种流水线方式对推理很有效,因为计算图是可预测的,你确切知道接下来需要哪些权重。训练则更难,因为反向传播需要前向传播时的激活值,数据移动模式因此复杂得多。

实践中的几种做法

CUDA 统一内存

NVIDIA 的 CUDA 统一内存(Unified Memory)创建了一个横跨显存和系统内存的单一地址空间。CUDA 运行时会根据访问模式在两者之间自动迁移页面。当 GPU 访问系统内存中的页面时,会触发缺页中断,然后把该页迁移到显存。

优点是对应用透明:你的 CUDA 代码不需要自己管理数据放置。缺点是缺页中断代价很高,而且运行时的迁移启发式策略不一定契合应用的访问模式。对于神经网络推理这种可预测的负载,显式管理通常表现更好。

逐层卸载

Hugging Face Accelerate、DeepSpeed ZeRO-Inference,以及 llama.cpp 的 --mmap 参数,都实现了显式的逐层卸载。它们只在显存中保留当前活跃的层,其余层则从系统内存或磁盘流式读取。模型不需要整体放进显存,只需要能容纳一两层就行。

这正是在消费级硬件上跑 70B 模型的实际原理。4 位量化的 70B 模型总共需要约 40 GB,但任何单层只需要约 1–2 GB。有 24 GB 显存再加上预取,你就能以适度的性能损失把它跑起来,可能比完全装进显存慢 30%–50%。

把 NVMe 当作扩展内存

最激进的做法是把 NVMe SSD 当作 GPU 内存的第三层。它的带宽远不及显存(约 7 GB/s 对比约 1,000 GB/s),但容量基本无限。一块 4 TB 的 NVMe 硬盘只要 200 美元,就能同时存下几十个大模型。

像 NVIDIA 的 Greenboost 这类项目就是这样实现的:GPU 内存系统透明地延伸到 NVMe,并通过智能预取减轻带宽限制的影响。对于 GPU 大量时间都在计算(而不只是搬数据)的推理负载,NVMe 的延迟可以通过计算重叠被完全掩盖。

性能高度依赖具体负载。计算密集型操作(大矩阵乘法)能很好地掩盖传输延迟;访存密集型操作(比如长上下文的注意力机制)则不行。实践中,NVMe 卸载最适合小 batch size 的大模型批量推理,这恰好也是本地 LLM 推理的典型场景。

Apple 统一内存的优势

Apple Silicon 的统一内存架构走的是完全不同的路线:直接消除显存与内存的区别。CPU 和 GPU 共享同一块物理内存池。根本不存在“卸载”,因为根本没有分离,GPU 访问的正是 CPU 使用的那块内存。

这并没有解决带宽问题。M 系列的内存带宽(M4 Max 约 400 GB/s)仍低于独立显卡,但它消除了让独立显卡卸载变慢的 PCIe 瓶颈。一台 128 GB 统一内存的 Mac 可以把 70B 模型完整放进 GPU 可访问的内存里,无需任何卸载开销。

取舍在于:对于能完整装进独立显卡显存的负载,它的峰值吞吐更低;但对于装不下的负载,它的表现要好得多。对于大模型推理,当模型超出独立显卡显存时,Apple Silicon 往往比带卸载的独立显卡更快,尽管它的原始算力更弱。

这对开发者意味着什么

如果你在构建使用 GPU 的应用,比如 ML 推理、图形或科学计算,显存限制会以具体的方式影响你的架构决策。

  • 先摸清你的工作集大小。分析 GPU 内存用量时,关注的不是峰值分配,而是任意时刻的工作集。如果峰值是 48 GB,但单次操作最多只需要 8 GB 的活跃数据,卸载就能跑得很好。如果某个操作确实需要同时占用 48 GB,那就得换更大的 GPU。
  • 根据访问模式选择卸载策略。顺序访问(逐层推理)配合预取效果很好;随机访问(大上下文上的注意力)则不然。先搞清楚你的负载属于哪种模式。
  • 量化通常比卸载更划算。把模型从 FP16 降到 INT4,显存占用可减少 4 倍,质量影响有限。卸载到系统内存虽然对质量没有影响,但内存节省有限,还会增加延迟。先做量化,再考虑卸载。
  • batch size 是你的调优旋钮。更大的 batch 需要更多显存,但能更好地摊薄开销;更小的 batch 占用更少显存,但每秒处理的条目也更少。当你接近显存上限时,减小 batch size 是最简单的修复办法。
  • 留意显存碎片。CUDA 的显存分配会随时间产生碎片,尤其是输入长度可变的时候。你可能总共还有 8 GB 空闲,却找不到一块连续的 2 GB 空间。PyTorch 的 torch.cuda.memory_stats() 可以查看碎片情况。torch.cuda.empty_cache() 或许有帮助,但它不是万能药。

在大规模计算负载中,GPU 显存始终是瓶颈,模型增长的速度总是快过显存。但管理这一瓶颈的工具(统一内存、智能卸载、多层缓存)正变得越来越成熟。“放不进显存”已不再是硬性门槛,而是一种性能上的取舍,而且越来越可控。