ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VLLM框架大模型部署显存检测与OOM排查实战指南

VLLM框架大模型部署显存检测与OOM排查实战指南

部署大模型的时候,我最怕看到的就是这类提示:CUDA out of memory。尤其用VLLM框架做推理服务时,它启动阶段就会做显存检测和显存预分配,一旦模型权重加上KV Cache的预估体积超过实际可用显存,经常直接拒绝启动,或者跑几个请求后莫名其妙OOM。最近帮团队把一个70B量级的模型从单卡改成多卡部署,前前后后因为显存检测的问题折腾了好几天,这里把整个思路、计算方法和排查链路整理出来,给准备用VLLM框架部署大模型的朋友做个参考。

如果你刚要接触这个领域,可以先记住一句话:VLLM框架下的显存检测不是看“模型文件多大”,而是看“权重、KV Cache、激活值、CUDA Context”四个部分在显存里怎么分账。下面我从预算计算开始,一路讲到底层检测机制、监控手段和一次完整的排查案例。

1. 部署前的显存账本:先把模型“胃口”算清楚再启动

1.1 权重显存:FP16/BF16/INT8 到底怎么换算

模型权重占用的显存是最直观的一笔账,公式非常简单:

权重显存 ≈ 模型参数量 × 每个参数占用的字节数

以常见的7B模型为例,如果用FP16(半精度)加载,每个参数占2字节,权重显存就是7 × 10^9 × 2,约14GB。13B模型FP16约26GB,70B模型FP16约140GB。这里有个很多人会忽略的细节:VLLM框架默认加载权重时,会尽量保持模型训练时的精度格式。如果你的模型是BF16格式保存的,那加载后每个参数同样占2字节;如果是FP32格式,每个参数占4字节,显存直接翻倍。

所以部署前第一件事,先用torch.load或模型库的配置文件确认一下权重的dtype。别再只看“7B模型14GB”这种粗略数字,实际部署时可能因为load_format设置不同而出现巨大差异。VLLM框架里,load_format参数支持auto、pt、safetensors、gguf等选项,默认auto会按保存格式加载,如果你明确知道权重是INT8量化过的,那么每参数占用1字节,7B模型只剩约7GB,这个数字会直接影响后续的KV Cache预留空间。

1.2 KV Cache:VLLM框架里最容易被低估的显存吞金兽

如果说权重显存是“明账”,KV Cache就是“暗账”。普通推理框架每生成一个token,都要把历史token的Key和Value缓存下来,避免重复计算。VLLM框架的核心卖点PagedAttention,本质上就是用类似操作系统分页的方式管理这块缓存,但无论怎么优化,KV Cache的总大小依然和序列长度、并发数强相关。

单条请求KV Cache占用可以按这个公式估算:

单个token的KV Cache字节数 = 2(K和V两组) × 层数 × KV头数 × 头维度 × 每个元素字节数

举个例子,一个30亿参数量级的模型,假设有28层、20个KV头、头维度128、FP16存储,那么每个token的KV Cache就是2 × 28 × 20 × 128 × 2,约286KB。如果设置max-model-len=4096,单条请求最多需要4096 × 286KB,约1.1GB。这还只是单条请求。如果同时有16条并发请求,峰值情况下KV Cache就要预留17GB以上。

这就是为什么VLLM框架启动时报显存不足时,很多人第一反应是“模型只有5GB,为什么显存需要20GB”——因为KV Cache按最坏情况预留了。VLLM框架中与之相关的两个参数是max-model-len和max-num-seqs,它们分别限制单条序列的最大长度和同时处理的序列数。很多建议文档说“按需调整”,但实际部署时,这两个值直接影响KV Cache的显存预分配,调大任何一个,显存检测阶段的“水位”都会显著上升。

1.3 激活值和CUDA Context:看不见的显存消耗

权重加KV Cache之外,还有两部分显存消耗容易漏算。第一部分是激活值(Activation),推理过程中每一层transformer的中间结果都会临时占显存,虽然吞吐量级远小于训练,但当max-num-seqs很大时,激活值累积起来也有几百MB到1GB。第二部分是CUDA Context和PyTorch运行时本身的开销,每个进程初始化CUDA环境大约会占300MB到800MB,多卡并行时每张卡都有这份开销。

所以完整的显存预算公式应该是:

总显存需求 ≈ 权重显存 + KV Cache预留 + 激活值 + CUDA Context开销

VLLM框架启动时的显存检测,做的事情基本就是“当前可用显存 - 权重显存 - CUDA Context”,然后把剩余空间按比例(gpu-memory-utilization参数)划给KV Cache池。这个逻辑理解透之后,后面的配置就不会瞎猜了。

2. VLLM框架显存检测的三个层面:启动检查、运行监控、报错反馈

2.1 启动阶段:gpu-memory-utilization是怎么“圈地”的

gpu-memory-utilization是VLLM框架里最核心的显存比例参数,默认0.9,意思是“最多把90%的显存用于模型权重和KV Cache池”。注意,这个参数不是“预先占满90%显存”,而是限定了KV Cache cache池的上限。VLLM框架在初始化时,会先计算当前卡可用显存,然后乘以这个比例,得到“可分配总显存”。如果模型权重加载后剩余空间不够KV Cache池的最小页大小,就会报错。

实际操作中,我遇到过好几次“单卡80GB显存但gpu-memory-utilization设0.9仍启动失败”的情况。检查后发现,机器的显存并不是全部可用,比如另一块显卡的驱动预留、其他进程占用、甚至显卡型号本身支持的最大显存和实际空闲显存不一致。VLLM框架检测用的是torch.cuda.mem_get_info()拿到的实时可用值,这个值在启动那一刻才确定。如果你的卡上已经有一个推理进程占了20GB,VLLM框架检测到的“总可用”是剩下部分,再乘0.9,自然不够用。

建议:第一次启动时,把gpu-memory-utilization设成0.5或0.6,先跑通,看日志里给了多少KV Cache池空间,再逐步调高。别一上来就0.9,那样很容易触发启动期的显存检测错误,尤其是有多张卡并行时,每张卡剩余可用显存可能不一致,一张卡OOM就会拖垮整个进程。

2.2 运行阶段:KV Cache管理器如何动态调整显存水位

启动时预分配好KV Cache池,不等于运行中不会出问题。VLLM框架的PagedAttention会把KV Cache池分成固定大小的页(block),每个block默认16个token。请求进来后,管理器按需分配页;请求结束,页被回收。这个过程是动态的,所以显存检测不能只看启动那一刻。

运行中真正危险的情况是:KV Cache池虽然分配够了,但实际并发请求的token总数超过了池子容量。VLLM框架遇到这种情况不会直接OOM,而是会触发“抢占”(preemption),把低优先级请求的KV Cache暂时换出到CPU,腾出空间给新请求。但是如果CPU换出也来不及,或者池子本身配置太小,就会在某些极端请求组合下报torch.OutOfMemoryError。

我见过一个案例:某团队部署时把max-num-seqs设成256,但KV Cache池只按64个并发序列预留。运行初期看起来没事,因为实际并发很低。突然来了一个长文档分析任务,单条序列token数非常高,再加上其他请求,KV Cache池瞬间被打满,管理器反复做抢占,最终触发了CUDA OOM。所以运行阶段的“显存检测”比启动阶段更需要动态监控。

2.3 报错信息里的显存线索:从日志定位问题来源

VLLM框架的报错大致分三类,从日志中可以直接判断是哪种显存问题。

第一类是启动期的torch.OutOfMemoryError: CUDA out of memory,一般发生在engine = LLMEngine.from_engine_args阶段。这类错误最直接,通常就是模型权重加最小KV Cache池都超过显存上限。解决思路是降低gpu-memory-utilization、开启量化、减少模型并行度。

第二类是运行期的CUDA error: out of memory,可能伴随NCCL通信错误。这类错误除了显存不足,还要检查分布式集合通信环境,比如多卡并行时每张卡负载不均。VLLM框架默认张量并行会把KV Cache头拆分到多卡,但如果某张卡上其他进程占用过多,这张卡就会成为瓶颈。

第三类是ValueError: Not enough memory to create KV cache pool。这是VLLM框架自己检测到显存不足后主动抛出的,属于“显存检测”的典型拦截点。日志里通常会给出当前可用显存、模型占用量、KV Cache池要求的最小字节数,看到这个就说明配置参数需要回调。

所以,看到报错先别急着改参数,先打开日志看是哪个阶段触发的。VLLM框架的日志做得已经比较清晰,关键是区分“权重装不下”和“KV Cache预留过多”。

3. 用数据说话:实战中如何监控和预判显存占用

3.1 nvidia-smi 之外:用 pynvml 拿到实时显存曲线

显示显存占用最直接的是nvidia-smi,但命令行看的是进程级别总显存,反映不了KV Cache池内部使用情况。我自己习惯写一个小脚本,用pynvml定时采样,记录每张卡的used和free,再结合VLLM框架日志里的KV Cache使用率一起看。

下面是一个简单的采样脚本,部署时我常挂在后台跑:

import pynvml import time pynvml.nvmlInit() handle = pynvml.nvmlDeviceGetHandleByIndex(0) while True: mem = pynvml.nvmlDeviceGetMemoryInfo(handle) util = pynvml.nvmlDeviceGetUtilizationRates(handle) print(f"time={time.time():.0f} used={mem.used/1024**3:.2f}GB " f"free={mem.free/1024**3:.2f}GB util={util.gpu}%") time.sleep(5)

这个脚本的价值在于,当KV Cache池打满、开始抢占时,nvidia-smi看到的显存总量可能没有明显增长,但实际推理吞吐已经掉了一半。你只有把used变化趋势和请求QPS对应起来,才能定位瓶颈到底在计算侧还是KV Cache池供给侧。

3.2 通过VLLM框架日志和 Metrics API 观测 KV Cache 使用率

VLLM框架启动时会打印一行关键日志,类似这样:

KV cache size: 28.64 GB, max_tokens: 8192, max_seqs: 64

这行日志给出的KV cache size就是当前配置下显存检测后的KV Cache池上限。运行过程中,如果你启动了--enable-metrics,VLLM框架会暴露Prometheus指标,其中最常用的是:

  • vllm:num_requests_running:当前正在处理的请求数
  • vllm:gpu_cache_usage_perc:KV Cache池使用百分比
  • vllm:num_preemptions:发生抢占的次数

我一般在压测时把gpu_cache_usage_perc拉到90%以上观察是否出现延迟尖刺。如果这个指标长期接近100%且num_preemptions在涨,说明KV Cache池不够宽,需要增大gpu-memory-utilization或减少max-model-len、max-num-seqs。

3.3 一张表模拟不同量级模型的显存估算

下面这张表是我自己常用的估算模板,按FP16权重、单条全序列长度4096、并发32来算。实际数值会因层数、头数不同有浮动,但可以帮你在启动前快速判断“这张卡够不够”。

模型量级权重显存(FP16)单token KV Cache4096长度单序列KV Cache32并发KV Cache峰值加激活值和Context约需
1B2GB约70KB约280MB约9GB12GB左右
7B14GB约512KB约2GB约64GB80GB左右
13B26GB约800KB约3.2GB约102GB130GB左右
70B140GB约2MB约8GB约256GB400GB以上

从表里能明显看出,模型权重只是“起步价”,KV Cache往往才是决定显存机型选择的因素。这也是为什么很多人部署70B模型时必须上多卡,因为单卡即使能塞下权重,KV Cache池也会被压到极小,并发能力约等于没有。用VLLM框架时,与其追求“跑起来”,不如算清楚“能支撑多少并发”,否则服务上线后才是噩梦的开始。

4. 典型案例复盘:一次因显存检测不准确导致的反复OOM

4.1 现象:同一条启动命令,有时候能跑,有时候直接崩

背景是部署一个13B量级的模型,单卡80GB A100,初始配置是--gpu-memory-utilization 0.9 --max-model-len 4096 --max-num-seqs 64。第一次启动成功,压测到20并发时,日志开始出现CUDA out of memory。奇怪的是,同样的命令重启几次,有时能正常跑,有时刚启动就报错。

这种“薛定谔的显存”最让人头疼。第一反应是代码问题,但VLLM框架启动流程是确定性的,除非机器上有其他进程在抢显存。于是先跑了nvidia-smi,发现每次启动前可用显存都不一样:有时75GB,有时只有50GB。进一步排查,发现是同一台机器上其他团队偶尔会跑训练任务,占掉20多GB。所以VLLM框架启动时检测到的“可用显存”是随机的,0.9乘以不同的基数,得到完全不同的KV Cache池大小。这不是框架的bug,是我没有预留足够的安全边际。

4.2 排查链路:从日志、监控数据到参数调整的完整过程

我把排查链路按顺序写下来,供大家参考:

  1. 先看VLLM框架启动日志里的Available device memory和KV cache size。记录这次实际可用显存是多少,KV Cache池分配了多少,然后和理论值对比。
  2. 用nvidia-smi dmon -c 60监控60秒内的显存变化,确认是否有其他进程在周期性抢占。
  3. 查看当前卡上所有进程的PID和显存占用,发现一个训练任务占用了约20GB。
  4. 把gpu-memory-utilization降到0.8,同时把max-num-seqs从64降到32,max-model-len从4096调到2048。这样即使其他进程占掉20GB,剩余可用显存也足够支撑KV Cache池。
  5. 启动后再次从日志确认KV cache size,确保池子大小能支撑32并发和2048序列长度。
  6. 跑一轮压测,观察gpu_cache_usage_perc,确认它稳定在70%以下,不再触发抢占。

调整前后的核心变化在于:不再把全部可用的显存都押在KV Cache上,而是预留了至少10GB的缓冲空间。这里的“缓冲”不是为了好看,而是为了应对其他进程突然占用、以及VLLM框架运行时激活值带来的小尖峰。

4.3 根因分析与修复:为什么显存“检测”到的数值不等于实际可用

这个问题背后有个更本质的点:VLLM框架的显存检测基于torch.cuda.mem_get_info()返回的瞬时值,这个值代表“当前时刻”的全局空闲显存。但gpu-memory-utilization控制的是“框架最多能使用多少比例的当前空闲显存”,无法防止其他进程在框架启动后继续申请显存。换句话说,VLLM框架检测的是一个“没有第三方竞争”的理想状态。

同一台机器上共享GPU的场景下,这种显存检测机制天然有竞争风险。解决方案不是期望框架自动解决,而是在部署层加约束:要么用容器或资源管理系统把GPU隔离,要么在启动命令里把gpu-memory-utilization设到合适的值,留出足够的“不归我管”的显存。我后来给服务加了一个启动前的检查脚本,先统计当前GPU可用显存,低于安全阈值就直接拒绝启动,把“随机性”挡在门外。

5. 跳出默认配置:让显存检测与分配策略更聪明的几招

5.1 合理设置max-model-len和max-num-seqs,把KV Cache花在刀刃上

很多朋友部署服务时,习惯把max-model-len设成模型最大支持长度,比如32K、64K。这个想法没错,但对于显存预算来说,长序列意味着KV Cache按最坏情况预留。你的业务如果90%的请求都在2K以内,把max-model-len设为4K,单条请求的KV Cache直接减少一个量级,剩下显存可以支撑更多并发。

max-num-seqs同理。它代表“同时处理的序列数上限”,设太大时KV Cache池会被切得很碎。我一般用这个公式倒推:

max-num-seqs × max-model-len × 单token KV Cache字节数 ≤ KV Cache池可用空间的70%

这里的70%是经验值,给动态变化和抢占留出余地。比如上面13B模型,单token KV Cache约800KB,max-model-len=2048时单条序列约1.6GB,如果KV Cache池只有40GB,那么max-num-seqs不宜超过17。很多人把并发设成128,结果KV Cache池根本接不住,运行一小时就开始OOM,就是这个原因。

5.2 张量并行和量化对显存检测结果的影响

多卡部署时,VLLM框架的张量并行会把模型权重和KV Cache头均匀切到各卡。比如2卡跑70B模型,每卡权重70GB,而不是140GB。但要注意,激活值和CUDA Context在每张卡上仍然完整存在,所以每卡的总显存需求并不是简单的“总需求除以卡数”。而且,多卡并行时每张卡的可用显存可能不同,VLLM框架做显存检测时是以“最卡脖子的那张卡”为基准。如果一张卡上还有残留进程占着10GB,整个服务能用的KV Cache池就要以这张卡的剩余显存为准。

量化则是从源头降低权重的字节数。INT8量化后权重从2字节变成1字节,70B模型从140GB降到70GB,2卡就能跑。VLLM框架加载量化模型后,显存检测里的模型权重占用会相应变小,KV Cache池空间更大。不过量化后模型精度会有轻微损失,建议先用离线评测脚本验证一批业务场景的准确率再决定是否上线。

5.3 容器内存限制下的显存预留策略

如果你用容器部署,还要考虑显存与内存的关系。VLLM框架在KV Cache池不够时会把部分请求换出到CPU内存,这个机制依赖宿主机可用内存。如果容器的内存限制很紧,换出操作会失败或卡死。我在实际部署中发现,容器内存最好设为显存池上限 + 模型权重在CPU上的副本大小 + 4GB,否则极端情况下会触发更隐蔽的“间接OOM”。

顺便提一个容易被忽略的点:VLLM框架的分布式初始化会占用一点CPU内存和显存,多卡时尤其明显。启动前用free -g看一眼系统内存,别让容器内存限制卡住NCCL通信的临时buffer分配。

我在实际部署中发现,显存检测问题从来不是单点问题,它背后的核心是“预算意识”。用VLLM框架时,默认参数能跑通不代表能扛住生产流量。最稳妥的做法是:先按公式算出理论显存,再结合启动日志和监控指标做一轮压测,把gpu-memory-utilization、max-model-len、max-num-seqs三个参数当作同一套系统来调,不要单独动任何一个。最后再分享一个小技巧:每次调整参数后,记录一下启动日志中的KV cache size和压测时的gpu_cache_usage_perc,几组数据对比下来,你对这台机器的显存“水位”心里就有数了,部署任何模型都不容易翻车。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进