拓冰建站拓冰建站
首页 / 资讯中心 / 正文

KV cache泄漏被忽视:nvidia-smi为何对vLLM显存问题视而不见?

部署过 vLLM 的人应该都有过这种经历GPU 显存看着还剩十几个 G但服务突然 OOM或者长对话到某一轮之后响应变得极慢重启之后又恢复如初。如果只用nvidia-smi观察你会觉得一切都很正常——显存没满、温度正常、进程还在但线上问题就是反复出现。这篇文章要讲的核心判断是nvidia-smi对 vLLM 的 KV cache 泄漏基本是失明的。它能看到进程占用了多少显存却看不到 KV cache 的 block 是否在合理分配、释放、复用。Kvcachescope 这类工具的出现正是为了补上这一块视野。读完这篇文章你会理解 KV cache 为什么是大模型推理时的显存大头泄漏到底指什么如何在线上复现并定位问题以及怎么把监控维度从显存总量升级到KV cache block 状态。1. 为什么说 nvidia-smi 对 KV cache 泄漏是失明的先做一个类比。nvidia-smi看到的显存占用相当于整栋楼门口的总水表它只知道楼里一共用了多少水但不知道水是流进了正常的房间还是在某条管道里漏掉了又或者热水器和冷水管之间出现了内循环。vLLM 的 KV cache 管理恰好就是这栋楼里的管道系统。它的内部结构非常复杂包括按 block 分配的空闲池、正在被请求占用的活跃 block、被抢占后暂时保留的 block、前缀缓存占用的共享 block以及因为分配策略和碎片化而无法使用的空洞。这些细节全部发生在 vLLM 进程内部对外部监控工具来说是不可见的。所以你会看到一种奇怪的现象从nvidia-smi看进程的显存占用曲线非常平稳但从业务层面看服务的吞吐在下滑延迟在上升甚至开始报 out of memory。真正的问题不是 GPU 显存总量不够而是 vLLM 内部的 KV cache 空间出现了逻辑泄漏——可用 block 越来越少但进程级显存占用没有明显变化。我们可以从监控维度上做一个对比观察维度nvidia-smivLLM 日志与 metricsKvcachescope 这类 KV cache 专用工具进程级显存总量能看到不一定直接暴露不作为主要目标GPU 利用率能看到不一定直接暴露不作为主要目标KV cache 总 block 数看不到部分能看到能看到空闲 block 数量看不到部分能看到能看到每个请求占用的 block看不到难直接对应能看到前缀缓存命中情况看不到部分能看到能看到碎片化程度看不到难判断能看到泄漏定位完全失明难以量化核心能力这就是标题里说的 blind不是nvidia-smi坏了而是它的视角停留在 GPU 硬件层面离 vLLM 的 KV cache 管理层隔了好几层。2. 先把 KV cache 说清楚它为什么又大又难管KV cache 的英文全称是 Key-Value Cache。Transformer 模型在解码时每生成一个新 token都需要重新计算当前序列里所有历史 token 的注意力分数。为了不把历史 token 的 Key 和 Value 全部重算一遍推理框架会把它们缓存下来这就是 KV cache。用一句话概括KV cache 是牺牲显存、换推理速度的空间换时间设计。KV cache 的大小与模型结构直接相关我们可以按下面的简化估算公式来理解KV cache 大小 ≈ 层数 × KV 头数 × 头维度 × 序列长度 × 请求数 × 每个 KV 值占用的字节数这里的每个 KV 值占用的字节数取决于精度。例如 FP16 是 2 字节INT8 是 1 字节。不同模型使用的注意力机制MHA、GQA、MQA会影响 KV 头数GQA 的 KV 头数通常远小于 Q 头数所以显存占用会在这一步出现明显差异。下面给一个 Python 计算脚本把公式变成可以直接运行的代码# 文件路径estimate_kv_cache.py # 功能估算一个请求在给定序列长度下占用的 KV cache 显存 # 注意这是简化估算实际 vLLM 中还有 block 对齐、额外显存开销结果需按比例修正 def estimate_kv_cache_memory( num_layers: int, num_kv_heads: int, head_dim: int, seq_len: int, num_requests: int, bytes_per_item: int 2, # FP16 为 2INT8 为 1 ) - int: 返回估算的 KV cache 显存单位字节 kv_per_token num_layers * num_kv_heads * head_dim * 2 # Key 和 Value 各一份 kv_per_sequence kv_per_token * seq_len total kv_per_sequence * num_requests * bytes_per_item return total # 以某个 7B/8B 级别模型为例参数来自模型配置可自行替换 num_layers 28 num_kv_heads 4 head_dim 128 seq_len 8192 num_requests 16 total_bytes estimate_kv_cache_memory( num_layersnum_layers, num_kv_headsnum_kv_heads, head_dimhead_dim, seq_lenseq_len, num_requestsnum_requests, ) print(f估算 KV cache 显存: {total_bytes / 1024**3:.2f} GB) print(f单个请求 8K token 上下文占用: {total_bytes / num_requests / 1024**3:.2f} GB)如果你把上面的num_requests从 16 调到 64结果会非常惊人请求越多、上下文越长KV cache 的显存增长越快而且不是线性增长是请求数 × 序列长度两维同时放大的增长。这带出了一个核心矛盾KV cache 不像静态模型权重那样一次分配就固定不变它是在线动态变化的。vLLM 为了管理这种动态变化引入了基于 block 的分配机制。KV cache 不再是连续的一大块显存而是被切成固定大小的 block一个 block 通常可以容纳固定数量的 token。当请求不断生成新 token 时vLLM 从空闲 block 池里取 block请求结束时block 应该回到空闲池等待复用。这套机制的难点在于block 的分配和释放是高频操作一个活跃请求的每一步生成都可能触发并发请求之间会竞争空闲 block出现抢占preemption时被踢出的请求 block 不会立刻释放需要重新调度前缀缓存prefix caching可能让多个请求共享一部分 block统计口径更复杂。从运维角度看我们无法通过 GPU 显存总量去反推这些内部状态。这就是为什么需要一个 KV cache 专用的观察工具。3. Kvcachescope 到底补上了哪块视野Kvcachescope 从名字上就能看出定位Scope 是观察范围KV Cache 是对象合起来就是观察 KV cache 的显微镜。这类工具的价值不在于替代nvidia-smi而在于把 vLLM 内部的 KV cache 状态暴露出来。它可以回答三个以往很难回答的问题当前到底有多少 KV cache block 可用是哪个请求、哪类会话结构占用了大量 block当服务性能下降时是因为显存总量不够还是因为 block 被低效占用、碎片化、命中率下降如果只看表面很容易误以为显存还够说明 KV cache 没问题。但 KV cache 的问题和显存总量不是一回事。实际项目里一个典型的故障轨迹是这样的服务刚启动时一切正常业务方开始压测发送大量长上下文请求某个请求因异常中断连接断开但在 vLLM 内部对应的 seq 状态没有及时清理或者是前缀缓存不断累积共享 block 越来越多但被其他不相关请求命中不了服务开始频繁抢占preemption每次抢占都要重新计算吞吐骤降最终新请求无法申请到足够 block返回 OOM。在这个过程中nvidia-smi显示的显存占用可能一直平稳因为 vLLM 在启动时通常会按照配置预留一块较大的显存区域内部 block 池的收缩扩张不会立刻在进程级显存上反映出来。Kvcachescope 这类工具会从 vLLM 内部读取 BlockManager 的状态得到每个 block 的归属、每个序列的 block 列表、空闲 block 数量、前缀缓存命中情况、碎片率等指标。这些信息才是 KV cache 调优和故障定位真正需要的数据。由于该项目是近期以 Show HN 形式发布的开源项目具体安装方式和 API 可能快速变化我这里不写死命令。更合适的做法是在下面的章节中先讲清楚不依赖任何特定工具的通用排查方案再说明接入这类工具的通用思路。这样即使项目版本迭代方法论依然成立。4. 环境准备与通用手术刀nvidia-smi 之外的三个视角在真正定位 KV cache 泄漏之前先要明确一件事我们需要从三个层面观察一个正在运行的 vLLM 服务。第一个层面是硬件层也就是nvidia-smi能看到的内容进程显存占用、GPU 利用率、温度、功耗。第二个层面是推理框架层也就是 vLLM 日志和 metrics 暴露出来的内容请求数、preemption 计数、cache 命中率等。第三个层面是资源分配器层也就是 PyTorch caching allocator 或 CUDA 图分配器内部的状态。下面我们先搭建一个最小实验环境。4.1 环境要求一台装有 NVIDIA GPU 的 Linux 服务器驱动正常Docker 或 Python 虚拟环境用于运行 vLLMvLLM 以及一个开源模型本文以 Qwen 系列为例但代码思路适用于其他模型Python 3.8安装requests、pynvml等辅助库。启动一个 vLLM 服务时建议把关键参数显式写出来而不是全部用默认值docker run --rm --gpus all \ -p 8000:8000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --ipchost \ vllm/vllm-openai:latest \ --model Qwen/Qwen3-8B \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --max-num-seqs 8参数说明--gpu-memory-utilization 0.85表示 vLLM 最多可以使用 85% 的显存这是保护 GPU 驱动、显示输出和其他进程的常见做法--max-model-len 8192限制单请求最大上下文长度这里只是一个示例值需要以模型实际支持范围为准--max-num-seqs 8限制最大并发序列数避免请求全部打进来瞬间压垮 KV cache 池--ipchost在一些容器环境中用于增加共享内存防止 tokenizer 或 DataLoader 工作进程出现共享内存不足问题。需要特别说明的是vLLM 版本迭代很快部分参数在新版本中可能改名或默认行为变化。建议以你实际使用的 vLLM 版本--help输出为准。4.2 视角一硬件层监控命令启动服务后开一个终端窗口用下面的脚本每 2 秒采样一次显存# 文件路径watch_gpu.sh # 功能每隔 2 秒打印 GPU 显存和利用率摘要 while true; do nvidia-smi --query-gputimestamp,memory.used,memory.total,utilization.gpu \ --formatcsv,noheader,nounits sleep 2 done这个脚本会持续输出 GPU 显存总量和使用量。它能帮我们确认进程是否在涨显存但对 KV cache 内部的 block 状态无能为力。4.3 视角二vLLM 日志与 metricsvLLM 启动时会打印模型配置、显存使用计划、KV cache block 数量等信息。要重点关注以下内容日志开头的 KV cache 显存预留信息block size 和总 block 数量每次请求结束后的统计信息preemption 相关警告。同时vLLM 兼容 OpenAI API 的服务通常会在/metrics路径暴露 Prometheus 格式的指标。生产环境中可以用 Prometheus 采集再用 Grafana 展示。这里做一个轻量的手工检查curl -s http://localhost:8000/metrics | grep -E ^vllm | head -50如果只输出少量指标说明你的 vLLM 版本把部分指标放到了其他命名空间下重点观察包含cache、block、preemption、running这几个关键词的指标。4.4 视角三PyTorch caching allocator如果 KV cache 分配走的是 PyTorch 的 caching allocator我们可以在 Python 进程内调用内存摘要。实际操作上可以在 vLLM 服务启动前注入一段探针或者用调试模式单独跑推理脚本。下面是一个独立脚本展示如何在一个使用 GPU 的 Python 进程里获取 allocator 视角的内存摘要# 文件路径show_memory_summary.py # 功能展示 PyTorch caching allocator 视角的 GPU 内存状态 # 使用方式在业务进程内或调试进程中按需打印不要直接附加到生产进程 import torch # 触发 CUDA 初始化让 allocator 完成初始化 torch.cuda.init() current_allocated torch.cuda.memory_allocated() reserved torch.cuda.memory_reserved() max_allocated torch.cuda.max_memory_allocated() print(f当前分配: {current_allocated / 1024**3:.2f} GB) print(f预留显存: {reserved / 1024**3:.2f} GB) print(f历史最大分配: {max_allocated / 1024**3:.2f} GB) # 打印完整摘要包含 cached_blocks、reserved_segments 等内部状态 print(torch.cuda.memory_summary(abbreviatedFalse))这里有一个关键点nvidia-smi看到的显存占用往往更接近torch.cuda.memory_reserved()的范围而不是torch.cuda.memory_allocated()。这也就是为什么服务内部实际只用了很少的 KV block但nvidia-smi的显存数字依旧很大。5. 用一次完整实验演示显存没满但 KV cache 不再工作下面我们通过一次可重复的实验理解 KV cache 逻辑泄漏的场景。实验不做任何危险操作只观察服务行为。5.1 实验目标让一个 vLLM 服务连续处理多轮长对话请求观察三个现象nvidia-smi显示的进程显存是否变化vLLM 内部 KV cache 状态如何恶化请求最终是否出现 OOM 或性能骤降。5.2 压测脚本下面脚本会模拟多轮对话每一轮都在原始输入后面追加新的用户内容让序列长度不断增加。# 文件路径simulate_long_chat.py # 功能模拟多轮长对话请求持续增加输入的 token 长度 # 使用方式python simulate_long_chat.py import time import requests BASE_URL http://localhost:8000/v1/chat/completions conversation [ {role: system, content: 你是一个耐心的助手请详细回答每个问题。}, ] # 每轮追加一段文本让序列长度显著增长 for round_idx in range(10): user_text f这是第 {round_idx} 轮的补充信息请结合上面的对话继续分析 user_text 我们需要非常仔细地讨论一个技术问题 * 50 conversation.append({role: user, content: user_text}) payload { model: Qwen/Qwen3-8B, messages: conversation, max_tokens: 256, temperature: 0.7, } try: start_t time.time() resp requests.post(BASE_URL, jsonpayload, timeout120) cost time.time() - start_t if resp.status_code ! 200: print(f第 {round_idx} 轮失败HTTP {resp.status_code}: {resp.text[:200]}) break data resp.json() content data[choices][0][message][content] usage data.get(usage, {}) print(f第 {round_idx} 轮成功 | 耗时 {cost:.2f}s | fprompt_tokens{usage.get(prompt_tokens)} | fcompletion_tokens{usage.get(completion_tokens)}) except Exception as exc: print(f第 {round_idx} 轮异常: {exc}) break time.sleep(1)运行方式python simulate_long_chat.py此时另开一个终端运行显存采样bash watch_gpu.sh5.3 通过 vLLM 日志观察 preemption在多轮请求打到一定程度后vLLM 日志中很可能出现 preemption 相关记录。这表示新的 token 申请不到足够的 KV cache block系统必须把一部分已缓存的历史 block 丢弃或重算。preemption 次数上升时即使nvidia-smi显示显存没有变化服务的实际可用 KV cache 空间也已经缩水。这是 KV cache 逻辑泄漏最典型的表现之一。5.4 用 pynvml 从进程外读取显存上面实验更多是服务端视角。我们再写一个独立的 Python 脚本用pynvml从外部分钟级采样显存并记录时间点方便和业务日志对齐# 文件路径sample_gpu_with_pynvml.py # 功能从进程外读取 GPU 显存并记录到 CSV # 使用方式python sample_gpu_with_pynvml.py 2 100 import csv import sys import time from datetime import datetime from pynvml import ( nvmlInit, nvmlDeviceGetHandleByIndex, nvmlDeviceGetMemoryInfo, nvmlDeviceGetUtilizationRates, ) def main(): interval int(sys.argv[1]) if len(sys.argv) 1 else 2 count int(sys.argv[2]) if len(sys.argv) 2 else 100 output gpu_samples.csv nvmlInit() handle nvmlDeviceGetHandleByIndex(0) with open(output, w, newline) as f: writer csv.writer(f) writer.writerow([timestamp, used_mb, free_mb, total_mb, gpu_util]) for _ in range(count): mem nvmlDeviceGetMemoryInfo(handle) util nvmlDeviceGetUtilizationRates(handle) now datetime.now().isoformat() writer.writerow([ now, mem.used // 1024**2, mem.free // 1024**2, mem.total // 1024**2, util.gpu, ]) f.flush() time.sleep(interval) print(f采样完成结果写入 {output}) if __name__ __main__: main()运行方式python sample_gpu_with_pynvml.py 2 100这个脚本的价值在于把显存数据落到文件里你可以用 Excel、pandas 或任何可视化工具画曲线和时间轴对齐。5.5 实验结论如果实验顺利你大概率会看到以下结果nvidia-smi或pynvml的显存占用曲线没有明显上升甚至是一条平台线服务在第 N 轮请求后开始变慢preemption 增多某些请求开始报错提示无法分配足够的 KV cache block重启服务后一切恢复。这不是模型的 bug也不是 vLLM 简单的显存泄漏而是 KV cache 空间在高并发、长上下文场景下的正常但难观测的消耗过程。Kvcachescope 这类工具的价值就是把这个过程可视化让难观测变成可观测。6. 接入 Kvcachescope 的通用思路Kvcachescope 因为刚发布不久安装方式可能有变化。我不写死命令而是给出一个面对这类工具时通用的接入思路你可以拿着这个思路去对照官方仓库的 README。6.1 确认工具的采集模式KV cache 监控工具一般有两种模式旁路采集进程外拉取 vLLM 的 metrics 或日志做二次聚合应用内探针在 vLLM 进程内注册回调直接读取 BlockManager 状态。你先确认 Kvcachescope 是哪种模式。如果是旁路采集不需要改动 vLLM 启动命令如果是应用内探针通常需要在启动 vLLM 的 Python 入口中增加几行初始化代码。6.2 通用接入步骤无论工具具体怎么实现接入步骤大概率是下面几步安装工具建议在独立虚拟环境中安装确认 vLLM 服务 metrics 端口或日志路径运行工具观察能否读到 KV cache block 数据用压测脚本制造长上下文请求观察面板数据变化将历史数据保存和nvidia-smi曲线对比。6.3 一个示意性的探针写法下面代码是示意性质不代表 Kvcachescope 的真实 API。目的是让你理解探针模式大概长什么样# 文件路径kv_cache_probe_demo.py # 功能探针模式接入的通用示意代码 # 注意这段代码不是 Kvcachescope 的真实 API只用于理解接入思路 import time # 假设工具提供了这样的接口具体名称以仓库文档为准 # from kvcachescope import attach_vllm_probe # 初始化探针传入 vLLM 服务进程或 engine 实例 # probe attach_vllm_probe(engine_instance) # 每隔 5 秒打印一次 KV cache block 状态 for _ in range(12): # state probe.get_kv_cache_state() # print(state.summary()) time.sleep(5)如果你的目标是在生产环境中使用务必先看官方文档确认 API再到测试环境验证不要直接把示意代码复制到生产。6.4 给团队的落地建议Kvcachescope 这类工具更适合放在压测和问题定位阶段使用不适合作为唯一的生产告警源。生产环境仍然建议以 vLLM 自带 metrics、请求延迟、错误率和 preemption 计数为主KV cache 专用工具作为深入排查的辅助手段。团队接入时建议分三步走测试环境跑通在压测环境部署工具确认它能看到 KV cache block 变化建立基线记录不同并发、不同上下文长度下的 block 占用基线关联告警当 preemption 抬升或 cache 命中率下降时用工具回放现场。7. 常见问题与排查思路下面整理几个与 KV cache、vLLM 部署、nvidia-smi相关的高频问题。问题现象可能原因排查方式解决方案nvidia-smi报错 has failed because it couldnt communicate with the nvidia driverNVIDIA 驱动模块未加载、内核模块版本不匹配执行 lsmodgrep nvidia看驱动模块是否存在容器内nvidia-smi正常但 vLLM 识别不到 GPU容器缺少 GPU 运行时参数检查 docker run 命令是否带--gpus all增加--gpus all并确认 NVIDIA Container Toolkit 已安装nvidia-smi显存没满但新请求报 OOMKV cache block 耗尽或碎片化查看 vLLM 日志中的 preemption 记录拉取 metrics调低--max-num-seqs适当调低--max-model-len观察 KV cache 命中率服务运行几小时后吞吐明显下降长上下文请求累积preemption 增加前缀缓存占用增长对比nvidia-smi曲线和 vLLM 日志通过工具查看哪些请求占用了无法复用的 block必要时滚动重启--enable-prefix-caching开启后感觉没有效果请求前缀不相同或版本默认行为不同检查 metrics 中 cache 相关计数确认请求的 system prompt 是否一致按版本文档确认参数是否默认开启修改--gpu-memory-utilization后启动失败预留显存过低或过高模型加载空间不足查看 vLLM 启动日志中的内存估算日志从 0.8 开始逐步调整避免一次顶满某些版本遇到和chunk_size相关的异常行为Prompt 分块、编码或缓存粒度相关的参数变化查看该版本 release notes 和 issue 区优先回退到稳定版本或在官方 issue 中搜索相同报错这里要特别提醒一点对生产环境做任何显存相关配置变更前先备份当前启动命令和配置在测试环境验证并准备好快速回滚手段。显存参数一旦设置错误最容易出现的不是服务不可用而是服务能启动但压测几分钟后才暴露问题。8. 最佳实践让 KV cache 可观测、可预期、可回收结合上面的实验和排查思路下面给出几条在工程项目中能直接落地的建议。8.1 不要顶满显存gpu-memory-utilization不要设置成 0.98 或 0.99。vLLM 确实可以在高百分比下运行但一旦出现 KV cache 碎片化或突发长请求你就没有缓冲空间。更稳妥的做法是先按 0.85 起步观察 preemption 指标如果长期无 preemption再小幅上调。8.2 把 KV cache 监控接入 Prometheus 体系生产环境不要靠肉眼盯 nvidia-smi。至少要做到采集 vLLM 的/metrics配置 preemption、cache 命中率、运行中请求数等关键告警把nvidia-smi的显存数据也采集进来方便故障时对齐时间线。8.3 区分显存不够和KV cache 被低效占用当服务 OOM 或性能下降时先判断属于哪一类显存总量确实不够压低并发、减少上下文长度、升级硬件KV cache 被低效占用检查长连接请求、未释放的会话状态、前缀缓存命中率、碎片率。Kvcachescope 这类工具的核心应用场景是第二种判断。8.4 建立请求级观测能力比进程级显存监控更贴近问题的是请求级观测每个请求的 prompt token 数和 completion token 数每个请求实际分配的 block 数请求结束后 block 是否回到空闲池达到一定序列长度后preemption 是否开始出现。如果能把这些数据和业务日志关联定位泄漏的速度会快很多。8.5 安全与权限提醒如果 KV cache 专用工具提供强制释放 block或手动清理缓存的能力要格外谨慎。这类操作会影响在线推理服务甚至导致正在处理的请求直接失败。建议只在测试环境尝试生产环境需要走变更评审流程。9. 总结与后续学习方向这篇文章真正想传达的点是不要把nvidia-smi的显存数字当成 KV cache 健康状态的指标。它适合观察硬件资源水位但看不见 vLLM 内部的 block 分配、空闲、碎片化和 preemption 状态。Kvcachescope 这类工具的出现意味着社区开始把 KV cache 当作一等公民来观测这对部署 vLLM 的团队来说是一个很及时的补充。下一步你可以这样做用文中的压测脚本在你自己的 vLLM 服务上复现一次 KV cache 空间耗尽过程用watch_gpu.sh和sample_gpu_with_pynvml.py采集显存数据观察它和业务表现的脱节查看 Kvcachescope 官方仓库的 README确认它当前支持哪种接入方式把 vLLM 的 metrics 接入 Prometheus建立 preemption 和 cache 命中率的告警。KV cache 是一个越深入越有意思的领域。继续研究的方向可以是 vLLM 的 BlockManager 源码、PagedAttention 的实现原理、prefix caching 的命中边界以及在不同并发模型下 KV cache 碎片率的差异。先让问题可观测才能让问题可解决。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门