vLLM显存监控:nvidia-smi看不到的KV cache泄漏与KVCacheScope实战
之前在给一个内部推理服务做压测时遇到了一个比较隐蔽的问题nvidia-smi显示显存占用几乎打满但业务接口的实际吞吐却不高换用不同的并发参数后显存占用也没有如期释放。当时第一反应是“有显存泄漏”但翻遍代码、排查请求日志始终没定位到具体是哪个环节在吃显存。后来才意识到问题其实出在 vLLM 的 KV cache 上——而nvidia-smi根本看不透这一层。这也是本文想聊清楚的核心问题为什么 vLLM 部署大模型时nvidia-smi对 KV cache 的分配、复用和“泄漏”几乎是盲的以及一个专门面向这个场景的监控工具 KVCacheScope 是怎么把问题摊开来看的。无论你是刚用 vLLM 部署 Qwen3 这类大模型的新手还是正在排查线上推理服务显存异常的老手这篇文章都会给你一套从原理到实践的完整思路。1. 背景与核心概念1.1 KV cache 是什么KV cacheKey-Value cache是大模型推理加速的关键机制。自回归生成过程中模型每生成一个 token都需要基于之前所有 token 的 Key 和 Value 计算注意力权重。如果不做缓存每次生成都要重新计算前面所有 token 的 K/V 矩阵做了缓存之后这部分计算结果可以直接复用大幅降低重复计算。以一个长度为 2048 的输入为例在未启用 KV cache 的情况下生成第 2048 个 token 时需要重新计算前 2047 个 token 的注意力结果计算量会随序列长度近似平方增长。而启用 KV cache 后只有新 token 的 K/V 需要计算耗时主要集中在新 token 的增量计算上。KV cache 的显存占用可以按下面的方式做粗略估算KV cache 显存 ≈ 2 × 层数 × 隐藏层维度 × 序列长度 × 精度字节数例如一个 7B 模型层数 32、隐藏层维度 4096使用 FP16 精度2 字节单条序列长度 20482 × 32 × 4096 × 2048 × 2 ≈ 1.07 GB注意这里没有区分 K 和 V 的大小差异也没有计算 attention 头数的影响实际会略有出入。但它能说明一个问题KV cache 和模型权重一样都是显存大户。1.2 nvidia-smi 的“盲区”在哪里nvidia-smi是 NVIDIA 官方提供的 GPU 状态查看工具也是很多开发者监控显存的第一选择。它能显示GPU 总显存和已用显存显存温度、功耗运行中的进程及其显存占用但对 vLLM 来说nvidia-smi看到的已用显存和 KV cache 的真实使用情况并不是一回事。原因在于 vLLM 在启动时会通过gpu_memory_utilization参数预先申请一大块显存作为推理内存池。举例来说你设置gpu_memory_utilization0.9vLLM 会把 90% 的显存一次性纳入自己的管理范围其中一部分给模型权重另一部分留给 KV cache。在nvidia-smi里这块显存从 vLLM 进程启动的那一刻就已经显示为“已占用”了。之后请求进来了、KV cache 逐步被填满nvidia-smi的数字基本不会变化请求结束了、KV cache 应该被释放复用nvidia-smi的数字还是那个数。这就是标题中“nvidia-smi is blind”的含义它只能看到 CUDA 上下文占用了多少显存无法感知 vLLM 内部的 KV cache 分配器当前实际分配了多少 block、还有多少 block 空闲、哪些 block 没有被及时释放。1.3 为什么需要单独的 KV cache 监控工具既然nvidia-smi看不到 KV cache 的细节那么线上服务如果出现“显存泄漏”就非常难排查。这里的泄漏不一定是传统意义上的内存泄漏而更可能是KV cache block 没有被回收导致可用 KV cache 空间越来越少prefix cache前缀缓存占用了大量 block但实际命中率不高某些异常的请求把 KV cache block 长期占住不放长连接或流式请求异常中断后缓存资源没有正确释放。KVCacheScope 这个工具就是把视角从“GPU 显存进程级占用”下沉到“vLLM 内部缓存池分配情况”让你能直观看到 KV cache 的容量、用量、命中率以及是否存在异常驻留。2. 环境准备与版本说明KVCacheScope 属于社区监控工具和 vLLM 本身的版本迭代速度相比功能接口可能随版本发生变化。部署前建议先确认下面几项基础环境。2.1 运行环境建议项目建议配置GPUNVIDIA 显卡支持 CUDA显存建议 16GB 以上操作系统Ubuntu 20.04 / 22.04或其他 Linux 发行版驱动NVIDIA 驱动 470Python3.9 及以上CUDA11.8 或 12.x 均可取决于 PyTorch 和 vLLM 版本vLLM0.4.x 到 0.6.x 等常用版本均可但需注意接口差异如果你本机已经用 vLLM 成功部署过模型说明基础环境基本没问题。KVCacheScope 更多是作为“附加监控层”接入不要求重新搭建推理环境。2.2 确认 vLLM 运行状态在接入监控前先用一两条命令确认 vLLM 服务是否正常# 查看 vLLM 进程是否在运行 ps aux | grep vllm # 查看监听端口vLLM 默认监听 8000 ss -lntp | grep 8000确认服务正常后再用一个简单请求验证基本推理功能curl http://localhost:8000/v1/completions \ -H Content-Type: application/json \ -d { model: qwen3, prompt: 你好介绍一下你自己, max_tokens: 64 }能正常返回结果后再继续接监控工具。如果这个基础请求都不通先解决服务问题再谈监控。2.3 关于 nvidia-smi 驱动报错不少人在部署时遇到过下面这个报错nvidia-smi has failed because it couldnt communicate with the nvidia driver. Make sure that the latest NVIDIA driver is installed and running.这个错误意味着nvidia-smi工具本身无法和内核态的 NVIDIA 驱动通信。常见原因包括系统内核升级后NVIDIA 驱动模块没有重新编译安装容器运行时 GPU 参数配置不正确宿主机驱动没有正确映射到容器内驱动和 CUDA 版本不匹配。如果遇到这个错误优先检查宿主机驱动状态# 查看内核模块是否加载 lsmod | grep nvidia # 查看驱动版本 cat /proc/driver/nvidia/version确认驱动没问题后再检查容器启动参数是否正确。这个报错和 KVCacheScope 本身无关但经常会成为监控链路不工作的前置原因所以建议先排除。3. 核心原理拆解vLLM 显存管理机制要理解 KVCacheScope 为什么有用得先知道 vLLM 的显存分配机制。下面按顺序拆开讲。3.1 vLLM 启动阶段的显存预分配vLLM 在启动时会读取gpu_memory_utilization配置决定预申请多少比例的 GPU 显存。默认值通常是 0.9也就是 90%。这 90% 的显存大致分为几块模型权重把模型权重加载到 GPU激活值activation空间前向推理过程中临时产生的中间张量KV cache 池用于存放 KV cache block 的显存区域其他的 CUDA 上下文开销。vLLM 会先计算模型权重和激活值的大致占用再把剩余空间尽可能全部划给 KV cache 池。所以你在nvidia-smi里看到 vLLM 进程占用几十 GB 显存不代表 KV cache 真的用了这么多只能说 vLLM 把这块显存“圈”起来了。这种预分配策略的优点是推理过程中不需要频繁向 CUDA 申请释放显存速度和稳定性都更好。缺点是外部工具无法从进程级显存数字判断 KV cache 的真实使用情况。3.2 PagedAttention 与 block 管理vLLM 使用的核心显存优化方案是 PagedAttention。它借鉴了操作系统虚拟内存的分页思想把 KV cache 划分成固定大小的 block而不是为每条序列分配一整块连续显存。每条序列的 KV cache 只在实际填充到某个 block 时才真正占用一个 block 的显存空间。不同序列可以共享已经计算过的 prefix block。这样就能实现更细粒度的显存管理prefix cache 复用按需分配而不是一次分配到底。KV cache block 的管理由 vLLM 内部的 CacheEngine 和 BlockManager 负责。它们维护了 block 的分配、引用、释放和复用逻辑。问题就在于这些逻辑运行在 vLLM 进程内部外部无法直接看到。一旦某个 block 的引用计数异常导致它长期不被释放外部观察者只会发现“显存好像被什么东西吃掉了”但没有办法定位是哪一个 block、属于哪一条序列、什么时候分配的。3.3 为什么会出现 KV cache 泄漏或不可释放从实际生产经验来看“KV cache 泄漏”通常有几类原因。第一类流式请求异常中断当客户端发送流式请求后中途断开vLLM 服务端可能无法及时感知连接已失效导致这条请求对应的 KV cache block 一直处于“被使用”状态。如果这类情况反复出现可用 block 池就会被“僵尸请求”慢慢耗尽。第二类prefix cache 命中率过低启用了 prefix cache 后如果请求的公共前缀很短、或者前缀变化频繁缓存命中率会很低。此时 prefix cache 区虽然占用了大量 block但对吞吐没有实际贡献看起来就像“缓存膨胀”。第三类vLLM 自身版本 bug搜索热词中有一条很值得注意“vllm 0.23.0 chunk_size bug”。虽然这里所说的0.23.0版本号可能有误vLLM 的常用版本线一般是 0.4.x、0.5.x、0.6.x但版本迭代中出现缓存管理相关 bug 是真实存在的。比如某些版本在特定前缀前缀分配策略下cache block 无法完全回收或者 chunked prefill 模式下chunk 大小设置不当导致 KV cache 分配异常。所以当显存异常时首先要确定 vLLM 版本再对照官方 changelog 和 issue 列表排查是否有已知问题。盲目调参往往浪费时间。3.4 KVCacheScope 的监控思路KVCacheScope 的做法是直接读取 vLLM 运行时内部的 KV cache 状态。它要回答的问题包括KV cache 总容量是多少个 block当前已经分配了多少个 block其中多少 block 被正常请求占用多少 block 被 prefix cache 占用分配后没有释放的 block 占比是多少每一条请求占用了多少 block它一般通过两种方式接入读取 vLLM 日志或统计接口暴露的缓存指标作为可观测性插件关联到 vLLM 的 CacheEngine 内部状态。由于不同版本的 vLLM 内部实现差异较大具体接入方式得看工具版本对应的说明。下面是常见的功能示意不代表具体 API 就是如此# 伪代码示意 KVCacheScope 的监控数据结构 kv_cache_status { num_total_blocks: 4096, num_free_blocks: 1024, num_active_blocks: 2048, num_prefixed_blocks: 512, num_leaked_blocks: 512, }这种方式比nvidia-smi的粗粒度显存信息精准得多。它能直接看出“缓存池还有多少余量”而不是“GPU 显存还有多少余量”。4. 完整实战KVCacheScope 监控 vLLM KV cache下面进入实际操作。我们以一套常见的 vLLM 部署环境为例演示接入 KVCacheScope、查看 KV cache 状态、定位异常占用的完整流程。你需要先有一个正在运行的 vLLM 服务。这里为了统一讲解假设你的服务已经通过 Docker 或裸进程方式启动且模型是 Qwen3 系列8B 或 27B 均可。4.1 启动 vLLM 服务先启动一个最基础的 vLLM 服务。如果你已经有服务在跑这步可以跳过。docker run --rm --gpus all -p 8000:8000 \ -v /models:/models \ vllm/vllm-openai:latest \ --model /models/qwen3-8b \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --enforce-eager说明--gpu-memory-utilization 0.9vLLM 最多预占 90% 显存--max-model-len 8192最大序列长度直接影响 KV cache 容量规划--enforce-eager禁用 CUDA graph便于排查问题时观察显存变化生产环境可以不设置。如果你没有 Docker 环境也可以用裸 Python 方式python -m vllm.entrypoints.openai.api_server \ --model /models/qwen3-8b \ --gpu-memory-utilization 0.9 \ --max-model-len 8192启动成功后vLLM 会打印类似下面的日志INFO: Running vLLM with 1 GPU. Maximum concurrency for serving requests 8 INFO: Maximum KV cache size 65536 blocks INFO: Number of blocks per request 2048其中Maximum KV cache size就是当前配置下 KV cache 池的总 block 数。Number of blocks per request是单条最长序列可能占用的 block 上限。4.2 确认 KV cache 状态在接入 KVCacheScope 之前我们先用 vLLM 自带的方式确认一下服务状态。vLLM 的/metrics接口会暴露部分指标比如curl -s http://localhost:8000/metrics | grep -E vllm:(num_requests|num_preemptions|prompt_tokens_total|generation_tokens_total)不过这些指标大多是请求维度的KV cache block 级别的数据是否完整暴露要看版本。这也是 KVCacheScope 存在的原因之一标准指标不够细。4.3 安装 KVCacheScopeKVCacheScope 建议以独立进程方式部署避免对推理进程产生额外干扰。一般通过 pip 安装即可pip install kvcachescope由于项目仍处于快速迭代阶段具体包名和依赖以仓库 README 为准。安装后可以先查看命令行帮助kvcachescope --help如果命令找不到可以尝试用python -m kvcachescope运行python -m kvcachescope --help4.4 配置监控参数KVCacheScope 通常需要知道 vLLM 服务的地址和端口。假设 vLLM 服务运行在localhost:8000可以用类似下面的方式启动监控kvcachescope --endpoint http://localhost:8000 \ --interval 5 \ --output terminal这里只是一个示例具体参数名可能会随版本调整。核心思路是--endpointvLLM 服务地址--interval轮询间隔秒--output输出方式terminal表示在终端打印。如果你的 vLLM 服务跑在容器里宿主机的监控工具直接访问映射端口即可。4.5 用 KVCacheScope 观察状态启动后终端会周期性打印类似下面的信息[2025-01-10 14:30:01] total_blocks32768 free_blocks12800 active_blocks16256 prefixed_blocks2600 leaked_blocks1112 [2025-01-10 14:30:06] total_blocks32768 free_blocks12000 active_blocks17000 prefixed_blocks2700 leaked_blocks1068关键字段含义字段含义total_blocksKV cache 池总 block 数基本不会变化free_blocks空闲可用 block 数active_blocks正在被请求占用的 block 数prefixed_blocks被 prefix cache 占用的 block 数leaked_blocks疑似泄漏、无法释放的 block 数正常情况下free_blocks会随着请求进入而减少、请求结束后回升。如果free_blocks持续下降、leaked_blocks持续上升说明缓存回收有问题。4.6 构造压力场景验证监控为了验证 KVCacheScope 的监控效果可以写一个简单的并发脚本向 vLLM 发起大量流式请求并在请求中途强制断开模拟异常场景。import asyncio import httpx async def stream_request(url: str, prompt: str, disconnect_after: float 0.5): timeout httpx.Timeout(None, connect5.0) async with httpx.AsyncClient(timeouttimeout) as client: async with client.stream(POST, url, json{ model: qwen3, prompt: prompt, max_tokens: 2048, stream: True, }) as response: start_time asyncio.get_event_loop().time() async for line in response.aiter_lines(): if line.startswith(data:): # 模拟客户端中途断开 if asyncio.get_event_loop().time() - start_time disconnect_after: await response.aclose() break async def main(): sem asyncio.Semaphore(16) async def limited(prompt): async with sem: await stream_request( http://localhost:8000/v1/completions, prompt, disconnect_after0.3 ) tasks [limited(f测试请求 {i} 你好请写一篇长文章 * 50) for i in range(50)] await asyncio.gather(*tasks) if __name__ __main__: asyncio.run(main())这个脚本会并发发起 50 个流式请求每个请求都在 0.3 秒后强制断开。正常情况下vLLM 最终会回收这些中断请求占用的 KV cache block。如果回收不及时KVCacheScope 会显示free_blocks明显下降并伴随leaked_blocks上升。运行脚本的时候建议开着 KVCacheScope 观察最好再配合watch -n 1 nvidia-smi对比看看。你会注意到nvidia-smi的显存占用始终在一个固定区间浮动基本不变KVCacheScope 的free_blocks和leaked_blocks会出现明显波动。这个对比能直观说明为什么排显存问题不能只看nvidia-smi。4.7 结果说明与初步判断如果实验结束后KVCacheScope 显示free_blocks回到实验前的水平说明 vLLM 正常回收了中断请求的缓存。如果free_blocks明显低于实验前并且长时间不恢复说明确实存在“缓存泄漏”或“缓存回收延迟”。此时可以继续观察是prefixed_blocks增长还是leaked_blocks增长是某条特定的请求路径导致还是所有请求都受影响vLLM 日志中是否出现 preemption抢占或 eviction驱逐相关记录结合这些信息才能判断是应用层问题、vLLM 配置问题还是 vLLM 自身 bug。5. 常见问题与排查思路KV cache 监控过程中有几类问题是高频出现的。这里整理成表格和步骤说明方便你直接对照。5.1 高频问题速查表问题现象常见原因解决思路nvidia-smi显示显存占满但请求吞吐不高vLLM 预分配了显存池KV cache 池并没有真正用完用 KVCacheScope 查看active_blocks和free_blocks的真实情况free_blocks持续下降不回弹存在异常请求占用缓存未释放检查是否有流式请求中断、长连接未关闭leaked_blocks持续上升vLLM 版本存在 cache block 回收 bug升级或回退 vLLM 版本查看官方 issueprefixed_blocks占用过高prefix cache 命中率低调整 prefix cache 策略或增加公共前缀长度KV cache 总容量太小gpu_memory_utilization配置偏低或max-model-len设置过大平衡显存利用率和序列长度上限nvidia-smi has failed because...NVIDIA 驱动和内核模块不匹配重装或重新编译 NVIDIA 驱动检查容器--gpus all参数请求多时显存直接 OOM并发过高超过 KV cache 池容量降低 max concurrency或限制max_num_seqs5.2 显存异常排查六步法如果你在 vLLM 部署中碰到显存异常建议严格按下面顺序排查不要一上来就怀疑“显存泄漏”。第一步确认 nvidia-smi 驱动正常先执行nvidia-smi如果报驱动通信错误先解决驱动问题。第二步确认基础推理功能正常用curl发送一个简单请求确认模型能正常返回结果。第三步记录基线指标启动 KVCacheScope 或查看 vLLM 日志记录空闲状态下的total_blocks、free_blocks基线。第四步复现问题用并发脚本、或线上真实流量复现显存异常。观察free_blocks和leaked_blocks的变化。第五步定位是“总量不足”还是“回收失败”如果free_blocks在请求结束后能恢复说明只是并发高峰期的缓存总量规划不足如果free_blocks持续下降且不恢复说明回收逻辑有问题如果prefixed_blocks占比异常高优先排查 prefix cache 命中情况。第六步针对性解决总量不足调整gpu_memory_utilization、max-model-len、max_num_seqs回收失败升级 vLLM 版本、加长客户端超时断开时间、在应用层正确处理流式请求中断prefix cache 命中率低调整公共 prompt 结构或关闭 prefix cache。5.3 KV cache 命中率优化搜索热词里有“vllm如何优化大模型的缓存命中率”这里多说几句。prefix cache 的命中率直接决定缓存空间的有效性。命中率越高越多的请求可以直接复用已计算的 KV cache block省去重复 prefill 的计算量和显存占用。常见优化方式统一公共 prompt 前缀在 prompt 最前面放固定的系统提示词或角色设定尽量保证多条请求的公共前缀一致合理设置 max-model-len不要把 max-model-len 设置得过大否则单条序列可能占用过多 block缓存池碎片化严重避免频繁改变 prompt 模板如果每次请求都在公共前缀里带上随机信息或时间戳prefix cache 基本无法命中调整 prefix cache 相关参数不同版本的 vLLM 对 prefix cache 的开关和策略不同确认你的版本是否默认开启。从 KVCacheScope 的prefixed_blocks可以看到 prefix cache 实际占用的 block 数量。如果这一项数值很大但请求耗时没有明显下降说明命中率不高缓存更多是“占着位子不干活”。5.4 关于 vLLM 版本 bug 的处理建议vLLM 更新速度快0.23.0 这类版本号是否真实存在我没有去逐一核实但“某个版本存在 chunk_size 或 cache block 管理 bug”这类事件并不罕见。遇到疑似版本 bug 时正确的做法是确认当前 vLLM 版本python -c import vllm; print(vllm.__version__)到 vLLM 官方 GitHub 仓库搜索相关 issue查看 Changelog确认是否有涉及 CacheEngine、BlockManager、prefix cache 的修复在测试环境升级或回退版本复测显存表现不要在生产环境直接升级先跑回归测试。6. 最佳实践与工程建议6.1 监控指标体系建议把显存监控分为三层而不是只依赖nvidia-smi监控层工具核心指标GPU 硬件层nvidia-smi、DCGM显存温度、功耗、总显存占用进程层nvidia-smi、NVIDIA Management LibraryvLLM 进程显存占用vLLM 内部缓存层KVCacheScope、vLLM metricstotal_blocks、free_blocks、active_blocks、prefixed_blocks、leaked_blocks只有第三层指标才能真实反映 KV cache 的健康状态。生产环境接入 Prometheus Grafana 时建议把这几个 block 指标都纳入采集范围。6.2 缓存策略配置建议vLLM 的显存和缓存参数存在相互制约关系。配置时考虑以下几点gpu_memory_utilization不宜过低也不宜过高。过低会导致 KV cache 池过小并发能力受限过高会增加显存紧张风险尤其在集成其他 GPU 任务时容易 OOMmax-model-len要依据业务真实序列长度设计。设置过大会让每条请求可能占用的 block 数上限变大降低并发能力max_num_seqs可以限制同时处理的序列数避免突发流量打爆缓存池如果业务场景中多轮对话居多建议启用并优化 prefix cache充分利用 prompt 相同前缀的缓存收益。6.3 应用层优雅处理流式请求根据 4.6 节的实验流式请求中断是导致 KV cache 回收延迟的常见诱因。在应用层做几件事能显著减少这类问题设置合理的读取超时不要让客户端无限期等待流式响应捕获连接异常在客户端代码中处理httpx.ReadTimeout、asyncio.CancelledError等异常确保中断后关闭响应体服务端做好心跳检测如果有条件在流式输出中定期发送心跳 token 或注释数据让服务端感知连接存活状态监控长连接数量如果同时存在的流式连接数远超预期优先排查客户端是否没有正确关闭连接。6.4 生产环境变更安全建议这里多强调一句凡是涉及 vLLM 版本升级、显存参数调整、缓存策略变更都建议先在测试环境用同样的模型和压测脚本验证再灰度发布到生产。不要在生产环境直接改gpu_memory_utilization后重启服务不要把线上流量直接打到新版本 vLLM 上验证缓存回收逻辑。特别是当你怀疑 KV cache 泄漏、需要升级 vLLM 版本时先记录线上服务的 KV cache 基线指标升级后再对比这样能客观评估版本变更是否真的解决了问题。6.5 从“事后排查”走向“事前预警”KVCacheScope 这类工具最大的价值不是帮你在问题发生后定位根因而是让你建立事前预警能力。建议设置几条监控告警规则free_blocks低于总 block 数的 10% 持续 5 分钟告警leaked_blocks持续上升且 30 分钟内没有回落告警请求失败率上升同时free_blocks急剧下降告警prefix cache 占用超过总 block 的 50%但平均首 token 延迟没有下降告警提示缓存策略可能失效。有了这些规则KV cache 问题就能在影响业务之前被发现。7. 总结与后续学习方向本文从nvidia-smi的监控盲区出发解释了 vLLM 预分配显存机制和 KV cache block 管理的原理介绍了 KVCacheScope 定位 KV cache 泄漏的思路并给出了从环境准备、监控接入、异常模拟到问题排查的完整流程。重点包括KV cache 是 vLLM 推理加速的核心机制也是显存占用的重要组成部分vLLM 通过gpu_memory_utilization预先申请显存池导致nvidia-smi无法反映 KV cache 真实使用情况KVCacheScope 通过观察 block 分配、释放、复用情况让 KV cache 泄漏可见显存异常排查要遵循“驱动检查 → 基线记录 → 异常复现 → 分类定位 → 针对性解决”的路径prefix cache 命中率优化需要从 prompt 结构、参数配置、版本选择多个角度入手。下一步可以继续学习 vLLM 的 PagedAttention 论文理解 block 管理和 prefix cache 的算法细节也可以深入 vLLM 源码看看 CacheEngine 在具体版本中的实现方式这样在遇到版本 bug 时能更快定位问题代码。如果条件允许结合自己的线上业务做一次 KV cache 压力测试和参数调优会理解得更扎实。回到开头那个排查经历后来我们在 KVCacheScope 里清楚看到某个内部服务的流式接口没有正确处理客户端断连导致请求对应的 KV cache block 长期驻留。修复应用层连接管理后缓存回收恢复正常吞吐也明显回升。这个教训总结下来就是一句话vLLM 部署不只看 GPU 显存数字更要看 KV cache 内部状态。希望这篇文章能让你少走一些弯路。