深入GPU显存管理:从CUDA OOM原理到B300实测
2026/9/16 5:10:48 网站建设 项目流程

你在跑模型的时候,有没有被同一句话气到过:CUDA error: out of memory。我几乎每周都能看到群友或者同事发这句话。明明系统内存还剩几十G,显卡显存却先爆了;明明显卡本身很贵,显存分配却像玄学一样让人摸不着头脑。直到我开始认真研究GPU显存管理这件事,才意识到背后牵扯的东西一点都不简单:虚拟内存怎么映射、显存和内存之间怎么换页、驱动层的分配算法又是怎么工作的。

这篇文章我打算从第一性原理把GPU显存管明白,从最底层的“为什么显存会不够用”开始,一直讲到我在一台B300(Blackwell Ultra)上的实测过程和结论。如果你是做训练、推理部署或者AI Infra的人,这篇文章应该能帮你省掉不少排查OOM的时间。

1. 为什么我要折腾GPU显存这摊事

1.1 显存是AI时代的硬通货

显存这个东西,听起来就是“显卡上的内存”,但实际用起来会发现它比系统内存娇贵得多。容量决定你扛不扛得动一个大模型,带宽决定你每个token吐出来的速度。过去十年,GPU的计算能力翻了几十倍,但显存容量和带宽的增长却远远跟不上算力的膨胀,所以“显存墙”成了所有人绕不开的坎。

我最早是从2080Ti的那台机器开始接触大模型的,11GB显存,跑个7B模型勉强能塞进去,再往上就要做各种量化、切层、offload。后来换了A100,觉得80GB简直是天堂,结果没过多久又变成“刚好够用”。到了最近,我调了一台B300做测试,单卡288GB HBM3e,带宽快到离谱,但我依然不能用“显存够大”来回避管理问题。管好显存,不是大卡用户才需要的技能,而是所有跑GPU的人都能受益的基本功。

有人可能会问:显存是从哪里来的,是驱动随便给你划一块吗?当然不是。GPU里的显存分配方式和CPU的内存分配有着本质上的相似,但又有很大的不同。这套机制,就是GPU下的虚拟内存体系。理解它,才能真正理解为什么某些场景下显存会爆、某些情况下明明卡上没多少进程却还是分配失败。

1.2 一次OOM引发的思考

我印象很深的一次,是在一台64GB系统内存的机器上,想跑一个30B参数的量化模型。模型文件加载进来大概占18GB,按理说应该没问题,但模型初始化到GPU的时候直接OOM。原因是PyTorch默认要把参数复制到显存,显存只有12GB,哪怕系统内存还有40GB空闲,也用不上。

当时我本能地想到Windows用户常说的“设置虚拟内存”,心想:GPU是不是也能借用一部分系统内存当“虚拟显存”?后来深入一查,发现这事并没有看上去那么简单。CUDA确实提供了一套机制,让GPU可以访问系统和设备上的统一虚拟地址空间,也就是Unified Virtual Memory。基本思路就是用GPU的page fault机制,把显存中暂时用不到的数据换到系统内存里,需要时再换回来。

这个思路听起来很美,但换页的代价极其高昂。GPU要的数据需要经过PCIe总线或者NVLink传回显存,带宽比本地显存慢了不止一个数量级。所以实际工程里,很少会无脑开虚拟内存,而是通过各种显存管理策略来尽量少触发换页。这种“容量换速度”的取舍,是理解GPU显存管理的一个核心框架。

1.3 先厘清三个概念:显存、内存、虚拟内存

聊GPU显存之前,必须把几个容易混淆的名词分开。显存是GPU芯片直接访问的高带宽存储,物理上就焊在显卡上,离GPU最近的“手边仓库”。系统内存是CPU访问的主存,GPU要通过PCIe或者NVLink去读,带宽比显存低得多。虚拟内存是一个地址空间抽象,把物理上可能不连续的、甚至不存在的存储变成一块连续可寻址的空间。

CPU端的虚拟内存很成熟,就是大家熟悉的page、swap、MMU。GPU端的虚拟内存其实也类似,不过它是从CUDA的统一地址空间往下投影的。同一块显存物理页,可以被映射到进程的虚拟地址空间里,也可以被迁移到系统内存,再由GPU page fault触发换回。NVIDIA管这套东西叫Unified Memory,配合现代GPU自带的地址翻译和页表机制,可以让开发者写出“不需要关心数据在哪里”的代码。

理解这三者的区别,是后面所有调优的起点。我见过太多人把Windows的页面文件设置和GPU显存混为一谈,以为加大系统虚拟内存就能让显存大的模型跑起来,其实完全不是一回事。系统虚拟内存解决的是系统内存压力,GPU可不认Windows那套swap。

2. 从第一性原理看GPU显存体系

2.1 大模型到底吃掉了多少显存

要理解什么样的显存才是够用的,得先算一笔账。以7B参数模型为例,如果用FP16保存,单权重就是2字节,权重本身大约需要14GB。但推理阶段不只有权重,还有KV Cache,有中间激活值,有临时缓存。把这些加起来,7B模型实际跑推理时通常会超过16GB。要是训练,还要叠加梯度和优化器状态,显存需求会再膨胀3到5倍,这就是为什么当年用几张A100训练一个13B模型都费劲。

我做测试时喜欢用一个粗糙的估算公式:推理显存约等于参数量的2到3倍(以FP16为基准),训练显存约等于参数量的12到16倍。注意这只是量级估算,具体还要看序列长度、batch size和框架实现。比如我用FP16跑一个30B模型,推理启动时分配至少60GB,如果你把KV Cache预分配也打开,轻松破70GB。

了解这个量级后,你就能明白为什么显存管理这么重要。大模型时代,显存永远是稀缺资源,模型永远比你手头的卡大那么一丢丢,剩下就看你怎么腾挪。

2.2 HBM为什么这么贵:带宽、容量、成本三角

显存家族里,消费级显卡多用GDDR6/GDDR6X,数据中心用的则是HBM,最新一代是HBM3e。HBM之所以贵,是因为它把多个DRAM die垂直堆叠在一起,通过硅通孔和底部的base die连接,再和GPU封装在同一块基板上。制造难度高、良率低,成本自然水涨船高。

为什么要花这么大代价做HBM?最核心的驱动力是带宽。AI推理和训练是大规模访问数据的任务,GPU里几千个核心疯狂计算,如果显存喂数据的速度跟不上,算力再强也白搭。HBM通过超宽总线获得极高的带宽,比如H100的HBM3就有3.35TB/s,B300这一代用HBM3e更是接近8TB/s。相比之下,系统内存哪怕走DDR5,带宽也只有几十GB/s,完全不是一个数量级。

带宽决定了算力的上限,容量决定了模型的边界,成本决定了预算的规模。这个三角关系就是显存管理的底层约束。理解它之后,你就知道为什么低显存方案总在尝试“绕过带宽瓶颈”,比如量化就是用更少的位宽减少数据搬运量,offload则干脆把不常用的数据丢到低速存储上。

2.3 显存不够时的三条路:量化、换出、虚拟内存

当显存和模型的供需关系失衡,工程上就分化出三条路线。第一条是量化,把FP16降成INT8、INT4甚至更低位,模型体积直接缩小,代价是有精度损失,而且推理时要额外做反量化计算,在某些场景反而变慢。第二条是换出,把权重或KV Cache按需搬到内存,用PCIe带宽换显存容量,这个做法的典型代表是各种CPU offload库。第三条就是虚拟内存,把地址空间做得比物理显存大,靠缺页中断自动搬运数据。

三条路线不是互斥的。我在实测和日常使用中,经常看到它们叠加用:量化降低基础容量,offload把冷数据挪走,再靠框架的动态管理来兜底。那虚拟内存到底是什么时候启动?决策权其实在驱动和运行时手里,开发者能控制的,主要是CUDA的memory pool、异步分配和memory advice这些细粒度接口。

3. 虚拟内存在GPU上是怎么落地的

3.1 CUDA的统一内存与托管内存

NV在CUDA里直接提供了一套托管内存接口,核心是cudaMallocManaged。你用这个接口分配出来的内存,既可以被CPU访问,也可以被GPU访问,物理页面会在两者之间自动迁移。更厉害的是,这套接口和CPU侧虚拟内存思路一脉相承:每个进程有一个统一的虚拟地址空间,GPU和CPU共享同一张页表区域。

举个最简单的例子:

float *data; cudaMallocManaged(&data, N * sizeof(float)); // 在CPU侧初始化 for (int i = 0; i < N; i++) data[i] = i; // 内核里直接访问 kernel<<<blocks, threads>>>(data, N); cudaDeviceSynchronize();

你没看错,不需要手动cudaMemcpy。第一次GPU去读data的时候,会触发缺页,驱动把对应的页从系统内存搬到显存;如果显存不够,也会把一些页踢回去。开发者视角的代码瞬间清爽,但代价就一个字:慢。系统内存和显存之间搬一个页,走PCIe可能要几微秒,多搬几次整个内核的时间就全耗在等数据上了。

实际项目里,我很少把cudaMallocManaged用在性能敏感的主路径上,更多是把它当作快速开发的原型。想真正驾驭它,需要配合cudaMemPrefetchAsync预取或者cudaMemAdvise给驱动提示,把换页时机前置到计算开始前。没有经验的人直接跑,性能大概率会崩。

3.2 显存与系统内存之间的换页机制

统一内存背后的核心机制就是GPU page fault。现代数据中心GPU都支持完整的虚拟内存功能,每个进程可以有最多256TB的统一虚拟地址空间。第一次访问某段地址,如果对应页面不在显存,就会触发一个page fault,驱动接管之后把页面从系统内存拉进来,同时更新页表。

值得留意的是,GPU的page fault处理方式和CPU有显著区别。CPU的page fault由内核的缺页异常处理程序接管,路径很成熟。GPU的page fault则是靠驱动固件配合,通过NVLink或PCIe向CPU发出请求,在设备端也要维护一套地址翻译缓存,类似CPU的TLB。这套系统越复杂,出问题的点就越多,比如地址映射出现了问题、TLB没有失效、显存碎片化严重,都可能导致性能诡异或者直接报错。

我在实战里遇到过一种比较隐性的问题:用cudaMallocManaged分配了一大批数组,代码逻辑上没问题,但GPU访问时频繁触发换页,导致kernel的运行时间从2毫秒飙到200毫秒。如果只看报错根本看不出来,必须盯着nvidia-smi的显存变化和nvprof的page fault计数才能定位。这也是为什么我一直强调,虚拟内存是“最后的兜底方案”,不是“性能方案”。

3.3 ComfyUI Dynamic VRAM 与低显存运行工具的原理

近两年各种“低显存运行模型”的教程非常火,尤其ComfyUI的Dynamic VRAM方案,几乎被当成神器。其实它的底层思路和虚拟内存异曲同工,只不过控制点不在CUDA驱动层,而在框架层。ComfyUI有个选项叫“GPU memory management”,开启之后它会预先给显存设置一个目标上限,当分配超过上限时,不再直接申请新显存,而是把历史中间结果从显存挪到内存,或者在下次用到时再换回来。

这类工具本质上就是在复刻:容量不足时,把冷数据换到低速存储,尽量把热数据留在显存。区别是它让你在可读性更强的配置界面里控制策略。比如说,你可以把Low VRAM模式理解为“激进换出模式”,把Normal模式理解为“按需换出”,把No VRAM limit理解为“完全信任驱动”。组件的元数据缓存、模型权重缓存、潜空间图像缓存,分别对应着异构的存储层级。

但这类方案也有明显局限。第一,它换出的单位是完整的tensor,比虚拟内存的粗粒度换页要大得多。第二,它需要工作负载本身有清晰的冷热边界,像ComfyUI这种图生图流程,中间状态生命周期很短,很适合;但你要是跑一个长时间训练任务,所有数据都热,框架层换出一点用没有。第三,通用推理框架里,比如vLLM、SGLang,它们的管理策略更复杂,涉及PagedAttention和KV Cache的按需分页,换出旧页时还要考虑和CUDA graph的兼容性。

3.4 Windows虚拟内存配置与GPU显存的关系

看到热搜里一堆“win11虚拟内存配置错误”“16g内存虚拟内存设置多少”的问题,我必须在这里说清楚:Windows的虚拟内存(页面文件)和GPU显存,物理上可以说是两套东西。Windows页面文件解决的是系统内存不够的问题,它可以把数据暂存到磁盘。GPU上的CUDA、显存管理,走的是NVIDIA驱动内部分配逻辑,并不会因为你把Windows的虚拟内存从8GB改成32GB就自动多出显存。

那为什么低显存场景下,有人调整页面文件后确实变顺了?因为当你使用CPU offload或者统一内存换页时,数据会真实地写到系统内存,如果系统内存也不够,OS层面才会动用页面文件。所以Windows虚拟内存间接帮到了“把模型权重换到内存”这条路径。我给过的经验值是:内存16GB配4到8GB页面文件,32GB配8到16GB页面文件;如果你的任务主要是offload大模型,系统内存又偏紧,可以把页面文件稍微放大到系统内存的1.5倍,但别贪。设置太大,磁盘IO会拖垮整体性能。

真正想要提升GPU的显存容量,靠谱做法还是:减少占用(量化)、搬走冷数据(offload)、或者直接买更大显存的卡。别把希望全押在Windows的虚拟内存开关上。

4. B300实测:显存管理的极限压力测试

4.1 测试环境与实测思路

B300是Blackwell Ultra架构,这一代最吸引我的单卡288GB HBM3e,公开资料给到的显存带宽大概在8TB/s量级。这个容量和带宽意味着它可以相当宽松地放下几百亿参数模型的FP16权重,甚至可以在单卡内跑大Batch的推理。

我这台机器的软件环境是:Ubuntu 22.04,NVIDIA驱动和CUDA 12.8,PyTorch 2.6,容器里跑的是比较新的NGC PyTorch镜像。实测思路分四步:第一步,观察基础显存占用和分配行为;第二步,加载一个比较大的模型,逐步观测显存分配曲线;第三步,用MIG把大卡切成多个实例,看隔离效果;第四步,把虚拟内存和换出方案打开,对比在不同开启状态下推理性能的变化。整个过程核心就是回答一个问题:B300这么大显存,是不是真的可以让程序员忽略顯存管理?

4.2 nvidia-smi 观测显存分配

刚拿到这台机器,我先用nvidia-smi看基础信息。大多数人只看右上角Memory-Usage那一栏,但我想看到更精细的状态,比如计算实例、内存频率和实际已用显存,用的是基础命令:

nvidia-smi --query-gpu=name,memory.total,memory.used,memory.free,clocks.mem --format=csv

输出结果会看到名称、总显存、已用显存、空闲显存和内存时钟。值得一提的是,空闲显存并不等于实际可分配显存。驱动会保留一部分显存给上下文、内核模块和显示输出,也存在显存池预分配的情况。尤其在使用CUDA图形API时,底层会顺带预留一部分显存给命令缓冲区和资源状态。所以有一次我明明看到free内存还有30GB,打开一个新context时却突然清理出一堆缓存来,就是因为显存池的预留逻辑在起作用。

真正的显存管理细节,还是得靠CUDA运行时层的接口。PyTorch里可以用torch.cuda.memory_summary()拿到当前进程内部的显存分配明细,包括缓存块、剩余可分配空间和保留显存。我习惯把nvidia-smi和进程内分配两个视图一起看,前者反映物理状态,后者反映逻辑状态,两边经常对不上,尤其有多个进程抢显存的时候。

4.3 加载72B模型:我观察到的显存分配曲线

我在B300上直接用PyTorch加载了一个72B左右规模的模型,FP16权重,不量化,看看会发生什么。代码很简单,就是:

import torch from transformers import AutoModelForCausalLM torch.cuda.set_per_process_memory_fraction(0.95) model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-72B-Instruct", torch_dtype=torch.float16).to("cuda") torch.cuda.reset_peak_memory_stats()

一边加载,一边每0.5秒采样一次显存。曲线很有意思:启动阶段会先预留一小块空间给CUDA context和cuDNN,然后权重文件从磁盘流式加载到CPU,再逐步复制到GPU显存。当模型复制完成,显存占用会有一个明显的平台期,这时候再推理,KV Cache和激活值又开始缓慢爬升。

288GB的显存对付这个模型相当轻松,权重大概144GB,估算的KV Cache如果按8k序列长度和32层计算,大约10GB左右,整个推理过程峰值占用不到160GB,还有一大半余量。B300在显存容量上的确是个怪兽。

但这不代表没有细节问题。为了保障并发推理,我尝试把max_memory设置成一个偏小的值,想用虚拟内存或者offload来兜底:

max_memory = {0: "40GiB", "cpu": "160GiB"} model = AutoModelForCausalLM.from_pretrained(..., device_map="auto", max_memory=max_memory)

推理照样能跑,但速度肉眼可见地掉下来,因为每处理几条请求,GPU就在等待权重或KV Cache从内存换回显存,PCIe的方向来回切换也让延迟变得很不稳定。这个现象印证了我在前面说的一点:虚拟内存能救容量,但不能救速度。真正在B300这种卡上生产运行,是宁可把Batch做大、把KV Cache池配好,也不要轻易依赖换页。

4.4 MIG切分与显存隔离实测

B300支持MIG(Multi-Instance GPU),把物理GPU切分成多个独立的GPU实例,每个实例有隔离的SM、显存和内存带宽。对大模型推理来说,这是特别实用的功能:一张288GB的卡,按需求切成多个7GB或者更小的实例,可以同时跑不同模型,互不干扰。

我实测把B300切成了几个计算实例,用MIG命令:

nvidia-smi mig -cgi 6,6,6,6 -C

拿到几个大小不一的实例,每个实例的显存独立,跑同一个模型互不影响。好处很明显:不同团队的作业可以共享一张卡,不会因为一个人的显存暴涨影响别人。之前在一个共享集群上,经常有人把显存全占满,别人一个作业都提交不进去。MIG在隔离维度上解决了这个问题。

但也要注意,MIG切分的实例越小,带宽和总SM数量越受限制。如果切得太碎,跑大模型反而会因为缺带宽而变慢。另外,MIG开启后,统一内存、CUDA graph这些特性会有一定限制,部分功能在实例模式下不支持。生产环境里得权衡清楚,是追求大容量单任务,还是追求多任务隔离。B300这种大卡,做推理服务时切成几个中等尺寸的实例通常是合理选择。

4.5 低显存运行大模型的实测对照

虽然B300本身显存很大,但为了写这篇内容,我还是故意模拟了一下“显存不够”的场景,把max_memory调低,观察低显存模型的运行。我用量化后的INT4模型,配合CPU offload和层间调度,做了一组对照。

对照组1:INT4量化模型直接放显存,显存占用约为FP16的38%左右,速度不差。 对照组2:INT4量化模型,权重放在CPU,靠框架的自动换入换出。速度明显慢,但能跑。 对照组3:FP16权重,开启CUDA统一内存,完全交给驱动换页。能跑,但性能波动剧烈,出现过单次token延迟从50ms飙到3s的情况。

结论很清楚:低显存运行模型有很多办法,但每条路都在用延迟换容量。最实用的是先把模型量化到INT4或INT8,让基本盘变小,再用offload调整冷热数据。全局的方案不如分层:热数据留在显存,冷数据放内存,真正很冷的干脆放磁盘。

另外,低显存场景还有一个很值得做的优化:手动管理CUDA memory pool,给推理服务预留一块固定显存,避免动态分配导致的碎片化和慢分配。在B300这种卡上,显存池更大,碎片问题相对缓和,但原理一样。把一次推理用到的所有tensor都尽量复用,比频繁申请释放要好得多。

5. 显存过载与故障排查实录

5.1 显存检测Mats:判断是不是卡本身坏了

显存管理做多了,会遇到一种很诡异的情况:明明模型和代码都没问题,但训练时不时报错,或者渲染出奇怪的错误结果。排除软件问题之后,就该怀疑物理显存了。NVIDIA的Mats(Memory Advanced Test Software)是我在这种时候会用的检测工具,它可以在驱动层面对显存颗粒进行遍历读写测试,定位到具体的Bank和通道问题。

Mats通常需要进入一个特定的测试环境,加载专用内核模块,然后对显卡执行压力测试,输出每个显存通道的错误位置。Mats能测出很多常规工具看不出的问题,比如某颗显存颗粒在高温下读写不稳定,或者HBM堆栈内部有坏的TSV连接。这类硬件故障往往表现为间歇性错误,应用层crash日志里只有“uncorrectable ECC error”之类的信息。

不得不提醒一句,Mats对操作环境有一定要求,跑一遍通常要几十分钟甚至更久,而且错误的解读需要经验。普通用户如果只是花屏或者驱动掉,可以先用nvidia-smi -q查看ECC错误计数,如果持续增长再考虑深度检测。

5.2 OOM排查的基本流程

遇到CUDA out of memory,我建议先按这个流程走:第一步,打开第二个终端,跑watch -n 0.5 nvidia-smi看哪个进程占了多少显存;第二步,在Python里打印torch.cuda.memory_summary(),看你当前进程到底预分配了多少显存池、实际tensor占用多少;第三步,检查是否有历史context没释放,比如多次调用CUDAGraphtorch.cuda.memory._record_memory_history()记录的历史分配。

经常出现的情况是,进程内memory_allocated并不高,但memory_reserved已经接近显存上限。这是因为PyTorch的显存分配器会从驱动那边一次性预留很大一块,再用自己的算法切分给Tensor。如果你的模型对显存峰值有严格要求,可以通过PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True来让显存段尽量扩展,减少预分配浪费。

双GPU或者多卡场景也常常在这里出问题。比如有经验的开发者会在推理启动时对所有可见GPU设置torch.cuda.set_device,但没留意数据和模型是否真的在对应设备上,结果主卡显存爆满,从卡空闲。用nvidia-smi一眼就能看出来,但没经验的人往往盯着代码看半天。

5.3 跨卡调度与双GPU显存管理

在B300这类机器上,一个节点可能两张卡甚至更多,跨卡调度是绕不开的话题。最简单的方式是CUDA_VISIBLE_DEVICES=0,1指定可见卡,配合device_map="auto"让transformers自动切分模型。切分之后,每一层的权重分布在多张卡上,前向传播时会有跨卡通信,如果卡间走的是NVLink,速度还挺可观;如果是跑在PCIe上,通信开销就可能成为瓶颈。

做推理服务时,很多团队喜欢把两张卡各自独立跑一个模型实例,而不是把模型拆分到两张卡上。这样显存管理最简单,故障隔离也最好。只有在单卡放不下完整模型时,才考虑跨卡切分。我在B300上就发现,由于单卡288GB足够大,跨卡切分的需求反而比A100时代少了很多,优先都是做成多副本。

跨卡还需要注意一个小坑:显存的prefetch和统一内存在多卡之间会放大开销。UVM的页面如果跨卡共享,会涉及多GPU之间的同步和页表复制,很容易让性能雪崩。所以多卡场景,我通常关闭UVM,老老实实做显式复制。

5.4 虚拟内存配置与常见误区

最后再集中回应一下“win11虚拟内存配置错误”“虚拟内存怎么查看使用”这类热搜问题。很多人以为显存不够,把Windows虚拟内存调大就能跑大模型,实际上是对概念有误解。Windows虚拟内存不会变成显存,CUDA也看不到Windows的页面文件。不过,你在使用“把参数放到CPU内存再换入显存”这种offload方案时,确实会占用系统内存,足量的页面文件可以在系统内存吃紧时避免进程崩溃。

怎么查看虚拟内存使用量?Windows的任务管理器“性能”页签里有“提交”一项,Linux下用free -h看Swap。如果Swap经常被用满,说明系统内存严重不足,这时加大页面文件或增加物理内存才有意义。否则,调来调去也就是个心理安慰。

我给非专业用户的建议永远是:如果你的目标只是“让显存不大的显卡也能跑模型”,优先用现成的vLLM、llama.cpp、ComfyUI的Low VRAM模式,它们已经把底层策略调得相当成熟了,别再手动折腾虚拟内存了。

最后再分享一点实测之外的体会

这套测试做下来,我最深的感受是:显存管理不是一个“开关”,而是一整套策略的组合。大显存不代表不需要管理,B300虽大,但只要你把并发堆上去,KV Cache照样能把288GB吃个精光。反过来,小显存也未必不能跑大模型,前提是你要接受换页的延迟代价,并且愿意做量化、换出和显存池优化这些取舍。

我自己的习惯是,凡是上线部署的推理服务,都在代码里把显存监控写进去,定时记录smi信息与进程内分配差异,这样一旦线上出问题,回看曲线就能定位是硬件、驱动还是应用层的问题。这个小习惯帮我排查过好几次隐患,也希望对你有点用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询