
你在跑模型的时候有没有被同一句话气到过CUDA error: out of memory。我几乎每周都能看到群友或者同事发这句话。明明系统内存还剩几十G显卡显存却先爆了明明显卡本身很贵显存分配却像玄学一样让人摸不着头脑。直到我开始认真研究GPU显存管理这件事才意识到背后牵扯的东西一点都不简单虚拟内存怎么映射、显存和内存之间怎么换页、驱动层的分配算法又是怎么工作的。这篇文章我打算从第一性原理把GPU显存管明白从最底层的“为什么显存会不够用”开始一直讲到我在一台B300Blackwell 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/sB300这一代用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; // 内核里直接访问 kernelblocks, 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.04NVIDIA驱动和CUDA 12.8PyTorch 2.6容器里跑的是比较新的NGC PyTorch镜像。实测思路分四步第一步观察基础显存占用和分配行为第二步加载一个比较大的模型逐步观测显存分配曲线第三步用MIG把大卡切成多个实例看隔离效果第四步把虚拟内存和换出方案打开对比在不同开启状态下推理性能的变化。整个过程核心就是回答一个问题B300这么大显存是不是真的可以让程序员忽略顯存管理4.2 nvidia-smi 观测显存分配刚拿到这台机器我先用nvidia-smi看基础信息。大多数人只看右上角Memory-Usage那一栏但我想看到更精细的状态比如计算实例、内存频率和实际已用显存用的是基础命令nvidia-smi --query-gpuname,memory.total,memory.used,memory.free,clocks.mem --formatcsv输出结果会看到名称、总显存、已用显存、空闲显存和内存时钟。值得一提的是空闲显存并不等于实际可分配显存。驱动会保留一部分显存给上下文、内核模块和显示输出也存在显存池预分配的情况。尤其在使用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_dtypetorch.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_mapauto, max_memorymax_memory)推理照样能跑但速度肉眼可见地掉下来因为每处理几条请求GPU就在等待权重或KV Cache从内存换回显存PCIe的方向来回切换也让延迟变得很不稳定。这个现象印证了我在前面说的一点虚拟内存能救容量但不能救速度。真正在B300这种卡上生产运行是宁可把Batch做大、把KV Cache池配好也不要轻易依赖换页。4.4 MIG切分与显存隔离实测B300支持MIGMulti-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和层间调度做了一组对照。对照组1INT4量化模型直接放显存显存占用约为FP16的38%左右速度不差。 对照组2INT4量化模型权重放在CPU靠框架的自动换入换出。速度明显慢但能跑。 对照组3FP16权重开启CUDA统一内存完全交给驱动换页。能跑但性能波动剧烈出现过单次token延迟从50ms飙到3s的情况。结论很清楚低显存运行模型有很多办法但每条路都在用延迟换容量。最实用的是先把模型量化到INT4或INT8让基本盘变小再用offload调整冷热数据。全局的方案不如分层热数据留在显存冷数据放内存真正很冷的干脆放磁盘。另外低显存场景还有一个很值得做的优化手动管理CUDA memory pool给推理服务预留一块固定显存避免动态分配导致的碎片化和慢分配。在B300这种卡上显存池更大碎片问题相对缓和但原理一样。把一次推理用到的所有tensor都尽量复用比频繁申请释放要好得多。5. 显存过载与故障排查实录5.1 显存检测Mats判断是不是卡本身坏了显存管理做多了会遇到一种很诡异的情况明明模型和代码都没问题但训练时不时报错或者渲染出奇怪的错误结果。排除软件问题之后就该怀疑物理显存了。NVIDIA的MatsMemory 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没释放比如多次调用CUDAGraph或torch.cuda.memory._record_memory_history()记录的历史分配。经常出现的情况是进程内memory_allocated并不高但memory_reserved已经接近显存上限。这是因为PyTorch的显存分配器会从驱动那边一次性预留很大一块再用自己的算法切分给Tensor。如果你的模型对显存峰值有严格要求可以通过PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True来让显存段尽量扩展减少预分配浪费。双GPU或者多卡场景也常常在这里出问题。比如有经验的开发者会在推理启动时对所有可见GPU设置torch.cuda.set_device但没留意数据和模型是否真的在对应设备上结果主卡显存爆满从卡空闲。用nvidia-smi一眼就能看出来但没经验的人往往盯着代码看半天。5.3 跨卡调度与双GPU显存管理在B300这类机器上一个节点可能两张卡甚至更多跨卡调度是绕不开的话题。最简单的方式是CUDA_VISIBLE_DEVICES0,1指定可见卡配合device_mapauto让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信息与进程内分配差异这样一旦线上出问题回看曲线就能定位是硬件、驱动还是应用层的问题。这个小习惯帮我排查过好几次隐患也希望对你有点用。