ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

vLLM显存排障:为何nvidia-smi对KV cache泄漏“失明”?

vLLM显存排障:为何nvidia-smi对KV cache泄漏“失明”? 在给 vLLM 推理服务做显存排障时很多人第一反应都是敲nvidia-smi。但我在实际调优中遇到过一种很“诡异”的现象GPU 利用率已经掉到个位数nvidia-smi里显存占用却稳稳保持在高位甚至服务端已经报CUDA out of memory。反复用nvidia-smi观察看到的永远只是一个笼统的“进程占用显存量”完全分辨不出那部分高占用到底是模型权重、激活值还是 vLLM 为每轮请求预留的 KV cache。后来我基于 vLLM 的内存管理机制整理了一套排查方案并随手做了一个叫Kvcachescope的诊断脚本。本文就从这个问题出发讲清楚为什么nvidia-smi对 vLLM 的 KV cache 泄漏“失明”以及如何用更细颗粒度的工具定位泄漏、恢复显存、合理配置推理服务。文章会覆盖KV cache 的基本概念、vLLM 的显存管理方式、nvidia-smi的盲区原因、Kvcachescope 的完整实战步骤以及 vLLM 部署大模型过程中常见的显存问题排错思路。有基础的后端或运维同学可以直接跳到第 5 节看脚本新手建议从第 1 节开始。1. 背景nvidia-smi 能看到什么看不到什么1.1 我们先说 KV cache 是什么Transformer 模型做自回归生成时每生成一个 token都要基于之前的所有 token 重新计算注意力分布。如果不加缓存序列越长重复计算量越大推理速度会慢到不可接受。KV cache 就是把已经计算出来的 Key 和 Value 向量保存下来下一次生成新 token 时直接复用。简单理解KV cache 是用“显存空间”换“推理速度”。KV cache 的大小和模型结构强相关可以用下面这个公式估算KV Cache 大小 ≈ 层数 × 每层 KV 头数 × head_dim × 2 × 序列长度 × 单元素字节数其中2表示 Key 和 Value 两份数据。比如一个 27B 量级模型如果层数很深、KV heads 很大长上下文场景下 KV cache 可能占掉几十 GB 显存。1.2 nvidia-smi 只能看到总账nvidia-smi本身是 NVIDIA 提供的 GPU 状态查询工具可以看到 GPU 显存总容量、已用空间、空闲空间以及正在运行进程的显存占用。例如nvidia-smi --query-gpuindex,memory.total,memory.used,memory.free --formatcsv输出如下index, memory.total [MiB], memory.used [MiB], memory.free [MiB] 0, 81920, 74890, 7030可以看到 GPU 0 总显存 81GB 左右已用约 73GB空闲约 7GB。但如果你继续追问这 73GB 里有模型权重多少有激活值多少有 KV cache 多少哪些显存块已经被 vLLM 标记为空闲但进程仍然占用着这些信息nvidia-smi都无法回答。1.3 vLLM 的 KV cache 管理逻辑vLLM 引入了一种类似操作系统分页的内存管理机制PagedAttention。它把 KV cache 切成固定大小的 block按需分配给不同请求序列。这样做的好处是显存利用率更高不同请求可以共享前缀 block减少外部碎片。但这也带来一个问题vLLM 通常在初始化时就按gpu_memory_utilization比例预留一大块显存放入自己的 cache pool 中。对操作系统和nvidia-smi来说这块显存已经“被使用”了但 vLLM 内部可能只用了其中一部分 block。所以你会看到服务很空闲时nvidia-smi显存占用依然很高。这不是传统意义上的内存泄漏而是nvidia-smi的视角和 vLLM 的内部视角不一致。1.4 什么时候算真正的 KV cache 泄漏也有真正需要警惕的泄漏场景。比如vLLM 的 block 分配计数出错已结束请求的 block 没有归还到 free 列表每次请求都累积少量无法回收的碎片enable_prefix_caching开启后前缀缓存块没有按预期复用某些版本存在 chunked prefill 相关的显存回收 bugmax_num_seqs、max_num_batched_tokens配置不合理导致大量 block 被临时占用却长期不释放。这类问题用nvidia-smi看只会发现显存占用“只升不降”。想定位根因必须看到 vLLM 内部的block_manager状态。2. Kvcachescope 是什么能解决什么问题2.1 工具定位Kvcachescope不是一个替代nvidia-smi的工具。它更像一个“显存剖析器”从 vLLM 内部读取 key-value cache 的状态再结合nvidia-smi的外部视角输出一个更完整的显存使用画像。我开发它时主要想回答三个问题当前 vLLM 总共分配了多少 GPU block空闲 block 还有多少已用 block 有多少我的推理服务在压力测试中KV cache 是否出现了无法回收的增长趋势这些问题在标准nvidia-smi视图下都是盲区。2.2 与 nvidia-smi 的能力对比能力维度nvidia-smiKvcachescope总显存/已用/空闲能看到能看到进程级显存占用能看到能看到模型权重占用看不到可根据模型配置估算KV cache 总 block 数看不到能看到空闲 block 数看不到能看到已用 block 数看不到能看到每个请求占用 block看不到能粗略分析缓存命中率与 KV 占用关联看不到能辅助分析简单说nvidia-smi是“整栋楼的电表总表”Kvcachescope是“房间级的分电表”。2.3 适用场景vLLM 部署大模型后显存占用异常偏高长轮对话场景中显存随请求数量长期增长压测时需要量化 KV cache 的峰值和回落情况怀疑 vLLM 版本存在 chunked prefill 或 prefix caching 相关 bug需要快速采集证据。3. 环境准备与版本说明3.1 基础环境本文示例以常见的 Linux 环境为例版本仅供参考实际请根据你的项目环境调整。操作系统Ubuntu 20.04 或 22.04GPU 驱动建议 NVIDIA 驱动 535 或更新版本Python3.10 或 3.11vLLM安装与你的 CUDA 版本匹配的最新稳定版依赖库pynvml、pandas、matplotlib3.2 检查 NVIDIA 驱动在安装 vLLM 之前先确认nvidia-smi本身能正常运行nvidia-smi如果出现nvidia-smi has failed because it couldnt communicate with the nvidia driver说明驱动异常。可能原因包括系统更新后内核与驱动版本不匹配、驱动服务未加载、GPU 被误拔等。解决方案通常是重启机器或者重新安装匹配版本的 NVIDIA 驱动。生产环境操作前一定要先确认变更窗口和备份方案。3.3 安装依赖建议先创建独立的虚拟环境python3 -m venv vllm-env source vllm-env/bin/activate pip install --upgrade pip pip install pynvml pandas matplotlibvLLM 安装方式可以按官方文档走。CPU 版和 CUDA 版的依赖差异较大具体版本需要根据你的 GPU 驱动和 CUDA 版本判断。下面是一个通用的 pip 安装示意pip install vllm如果网络不稳定也可以使用镜像源pip install vllm -i https://pypi.tuna.tsinghua.edu.cn/simple注意不同 vLLM 版本内部数据结构差异很大下文脚本如果访问了scheduler.block_manager这类内部属性需要以你实际使用的版本为准。4. 核心原理为什么 nvidia-smi 对 KV cache 泄漏“失明”4.1 vLLM 预分配显存池机制vLLM 启动时会根据gpu_memory_utilization参数计算可用于 KV cache 的显存上限。例如设置为0.9意思是最多使用 90% 的显存作为 vLLM 的整体显存池。这一步是“预分配”的。也就是说vLLM 可能还没有真正处理任何请求nvidia-smi就已经看到很高的显存占用了。很多新手第一次启动 vLLM 时都会吓一跳觉得自己是不是“内存泄漏”了。这种预分配行为其实是 vLLM 为了减少运行时的显存分配开销。它一方面降低了显存碎片问题另一方面掩盖了真实的 KV cache 利用率。4.2 block 分配与切片vLLM 内部把 KV cache 切成一个个 block。每个 block 包含固定数量的 token 的 KV 数据。block_size是一个关键参数常见值为 16 或 32。可以用下面这个公式估算单个 block 的大小单 block 字节数 block_size × 层数 × 每层 KV 头数 × head_dim × 2 × dtype 字节数例如 FP16 每个元素占 2 字节。假设模型有 32 层KV 头数为 16head_dim 为 128block_size 为 16单 block 字节数 16 × 32 × 16 × 128 × 2 × 2 4 MB如果总显存预留了 60 GB 给 KV cache那么理论上大约有num_blocks ≈ 60 × 1024 / 4 ≈ 15360也就是大约 1.5 万个 block。vLLM 的block_manager会维护总 block 数、空闲 block 数和已分配 block 数。4.3 nvidia-smi 为什么看不到 KV cache 内部状态关键点在于nvidia-smi通过 NVMLNVIDIA Management Library读取的是 GPU 显存硬件层面的“页面占用”信息它不知道 CUDA 或 vLLM 内部把显存逻辑划分成了什么用途。换句话说当 vLLM 把 KV cache 池申请好之后这些显存页在硬件层就已经是“已分配”状态。即使 vLLM 内部标记某些 block 为空闲也并不会立刻调用cudaFree将显存释放给驱动。因为 vLLM 希望这些 block 能被下一个请求复用频繁释放和重新申请反而会降低性能。所以 nvidia-smi 看到的“占用”并不是缓存泄漏更多是“预留”。真正的泄漏发生在 vLLM 内部的空闲 block 数量持续下降、但请求量并没有继续增长的时候。5. 实战用 Kvcachescope 定位 vLLM KV cache 泄漏下面我会写一个精简版的 Kvcachescope核心思路是定时调用nvidia-smi采集进程级显存占用尝试从 vLLM 内部读取 block_manager 状态输出 KV cache 的 total / free / used 指标在压测过程中观察指标走势。5.1 项目结构kvcachescope/ ├── kvcachescope.py ├── requirements.txt └── README.mdrequirements.txt内容pynvml11.0 pandas2.0 matplotlib3.75.2 采集 nvidia-smi 数据先实现一个独立的 GPU 显存采集函数# 文件路径kvcachescope/kvcachescope.py import subprocess from datetime import datetime def get_gpu_memory(): 通过 nvidia-smi 查询 GPU 显存信息。 result subprocess.run( [ nvidia-smi, --query-gpuindex,memory.total,memory.used,memory.free, --formatcsv,noheader,nounits, ], capture_outputTrue, textTrue, checkTrue, ) lines result.stdout.strip().split(\n) gpus [] for line in lines: parts [item.strip() for item in line.split(,)] idx, total, used, free int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3]) gpus.append( { timestamp: datetime.now().isoformat(timespecseconds), gpu_index: idx, memory_total_mb: total, memory_used_mb: used, memory_free_mb: free, } ) return gpus if __name__ __main__: for gpu in get_gpu_memory(): print(gpu)这段代码可以直接运行输出类似{timestamp: 2025-06-01T12:00:00, gpu_index: 0, memory_total_mb: 81920, memory_used_mb: 74890, memory_free_mb: 7030}5.3 从 vLLM 读取 KV cache 状态vLLM 的内部架构在不同版本中变化较快因此下面的读取逻辑是“示例思路”不一定完全适配所有版本。核心思想是拿到LLMEngine的 scheduler再通过block_manager读取 block 数量。# 文件路径kvcachescope/kvcachescope.py节选 def inspect_llm_engine(engine): 从 vLLM engine 中读取 KV cache block 状态。 注意vLLM 不同版本内部属性名可能不同请以实际版本为准。 state {} try: scheduler engine.llm_engine.scheduler block_manager scheduler.block_manager num_total block_manager.num_total_gpu_blocks num_free block_manager.num_free_gpu_blocks num_used num_total - num_free state[kv_cache_total_blocks] num_total state[kv_cache_free_blocks] num_free state[kv_cache_used_blocks] num_used state[kv_cache_utilization] round(num_used / num_total, 4) if num_total else 0 except Exception as exc: state[error] f无法读取 block_manager: {exc} return state这里有一个非常重要的点num_used_blocks指的是“已经被请求占用的 block 数”而num_free_blocks是“可以立即分配给新请求的 block 数”。如果num_free_blocks持续下降且请求结束后没有回升就要怀疑是否发生了 KV cache 泄漏。5.4 定时采样的主脚本为了方便压测时观察趋势我把两部分采集整合成一个循环每隔指定秒数记录一次# 文件路径kvcachescope/kvcachescope.py节选 import time import csv def collect_loop(interval: int 5, output: str kvcache_trace.csv): rows [] print(开始采集 KV cache 状态按 CtrlC 停止。) try: while True: gpus get_gpu_memory() now datetime.now().isoformat(timespecseconds) for gpu in gpus: row { timestamp: now, gpu_index: gpu[gpu_index], memory_used_mb: gpu[memory_used_mb], memory_free_mb: gpu[memory_free_mb], } # 这里如果 engine 可用就加入 KV cache 指标 # row.update(inspect_llm_engine(engine)) rows.append(row) print(f{now} 已记录 {len(rows)} 行) time.sleep(interval) except KeyboardInterrupt: print(采集结束。) if rows: with open(output, w, newline) as f: writer csv.DictWriter(f, fieldnamesrows[0].keys()) writer.writeheader() writer.writerows(rows) print(f结果已保存到 {output})运行方式python kvcachescope.py --interval 5 --output trace.csv5.5 压测并观察趋势在另一个终端启动 vLLM 服务再用压测工具发请求。这里以curl简单模拟连续请求为例for i in $(seq 1 20); do curl -s http://localhost:8000/v1/completions \ -H Content-Type: application/json \ -d {model: qwen3-27b, prompt: 写一段关于显存优化的文章, max_tokens: 256} \ /dev/null sleep 1 done实际操作中推荐使用更正规的压测工具比如hey、wrk或 Locust控制并发和请求速率。压测结束后用 pandas 查看数据import pandas as pd df pd.read_csv(trace.csv) print(df.tail(10))关注两列memory_used_mb进程级显存占用memory_free_mb剩余显存。如果压力结束一段时间后memory_used_mb仍然没有回落且服务空闲时 KV cache 的free_blocks也没有恢复说明存在异常占用。5.6 画出趋势图可以用 matplotlib 快速可视化# 文件路径kvcachescope/plot_trace.py import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(trace.csv) df[timestamp] pd.to_datetime(df[timestamp]) plt.figure(figsize(10, 4)) plt.plot(df[timestamp], df[memory_used_mb], labelGPU memory used (MB)) plt.xlabel(time) plt.ylabel(MB) plt.title(vLLM GPU Memory Trend) plt.legend() plt.grid(True) plt.savefig(gpu_memory_trend.png, dpi100)生成图片后就能直观判断是否存在“只涨不降”的趋势。6. 泄漏修复与配置调优找到了 KV cache 显存不释放的问题后不能只靠重启服务硬扛下面几个方向需要重点排查。6.1 调整 gpu-memory-utilizationgpu_memory_utilization设置太高会导致显存池预留过大留给系统和其他进程的显存不足。比如显存 80GB模型权重已经占 35GB如果你设置0.95可用 KV cache 空间大概是(80 × 0.95 - 35) 41GB如果看起来比较紧张可以调到0.85或0.9。注意这个值不是越大越好还需要考虑 CUDA context、激活值、临时缓冲区等额外显存开销。启动 vLLM 时命令行示例python -m vllm.entrypoints.openai.api_server \ --model /models/qwen3-27b \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --max-num-seqs 32 \ --block-size 16 \ --enable-prefix-caching这里的模型路径和参数需要根据实际情况调整。6.2 合理设置 max-num-seqs 和 max-model-lenmax_num_seqs控制最多同时处理多少个序列。这个值过大会让大量请求同时申请 KV cache block瞬时显存压力很大。max_model_len如果设得过高即使实际请求没那么长vLLM 也会预留更多 KV cache 空间。建议先统计业务实际平均上下文长度和峰值长度把max_model_len设置为略高于峰值的值不要盲目拉满max_num_seqs从 16 或 32 开始压测逐步上调观察 KV cache 利用率。6.3 检查 prefix caching 配置--enable-prefix-caching开启后vLLM 会尝试在多轮请求中复用相同的前缀 block。如果命中率高KV cache 的消耗会显著降低。但要注意如果请求的前缀几乎不相同开启该功能不会带来明显收益反而可能因为缓存管理产生额外开销。Kvcachescope 可以从 block 数量变化间接观察命中率请求前缀相同且缓存命中新增used_blocks应该很少如果每次请求都大量新增 block说明前缀缓存没有命中。6.4 升级或回滚 vLLM 版本vLLM 迭代很快某些版本会引入显存管理 bug。例如热词中提到过的vllm 0.23.0 chunk_size bug本质上就是某个版本在 chunked prefill 场景下显存回收异常。遇到疑似泄漏问题可以先查当前 vLLM 的 release issue再决定升级到修复版本还是回滚到稳定版本。生产环境升级前建议用同一模型和同一压力脚本回归验证。6.5 不要依赖 torch.cuda.empty_cache很多人会用类似下面的代码“手动清显存”import torch torch.cuda.empty_cache()这个操作在普通 PyTorch 场景下可以回收一部分缓存但 vLLM 的 KV cache 是通过预分配显存池管理的empty_cache()通常无法把池子里的显存释放给nvidia-smi。更合理的做法是定位泄漏源找到对应请求的特征针对配置或版本做修复。7. 常见问题与排查思路下面把 vLLM 部署大模型和 KV cache 排查中常见的问题整理成一张表方便快速对照。问题现象常见原因解决思路nvidia-smi has failed because it couldnt communicate with the nvidia driver驱动异常或内核模块未加载重启机器或重新安装匹配驱动确保驱动版本与内核版本一致vLLM 启动后显存占用立刻很高vLLM 预分配显存池属于正常现象用 Kvcachescope 查看 free block 数量确认是否还有可用 block请求结束后nvidia-smi显存不降预备池设计KV cache block 等待复用观察 free block 是否回升结合压测判断并发升高后显存持续上涨不再回落KV cache block 回收异常或配置过大检查max_num_seqs和max_model_len升级 vLLM 版本启动时直接 OOMgpu_memory_utilization过高或模型权重占用估算不足调低 gpu-memory-utilization预留 CUDA context 空间长上下文请求很慢显存占用很高上下文长度超长KV cache 增长量过大限制 max-model-len或使用支持长上下文的优化模型开启 prefix caching 后命中率低请求前缀不一致缓存无法复用检查业务请求是否共享系统提示词或公共前缀多卡显存不均衡tensor parallel 切分配置不合理调整 tensor parallel size检查模型切分逻辑8. 最佳实践与工程建议8.1 建立多维度监控不要只看 nvidia-smi生产环境的显存监控至少要包括GPU 总显存使用进程级显存使用vLLM block_manager 的 total / free / usedKV cache 利用率请求级别的前缀缓存命中率。nvidia-smi只是外部视角Kvcachescope 这类工具负责补齐内部视角。推荐在压测和生产中同时采集形成基线数据。8.2 锁住 vLLM 版本vLLM 更新频繁内部接口和显存策略经常变化。建议在requirements.txt中锁住版本号例如vllm0.6.3.post1这种格式升级前先查看 release notes用相同的压测脚本做前后对比。锁版本可以避免“换个环境行为不一样”的坑。8.3 容量规划先行上线前先用公式估算模型权重和 KV cache 的峰值显存不要等线上报警再处理。估算流程加载模型读取层数、KV heads、head_dim、dtype确定业务最大上下文长度根据并发请求数量估算最终 block 使用量设定gpu_memory_utilization安全余量。8.4 日志和采集脚本要脱敏Kvcachescope 定位是显存数据采集不要在日志中打印用户 prompt 或生成内容。如果要从请求维度排查问题只记录请求长度、序列 ID、前缀长度等元数据。8.5 生产环境变更前先验证涉及 vLLM 升级、显存参数调整、驱动更新等操作时先在测试环境用相同模型和相近负载跑一遍观察 KV cache 指标和延迟变化确认稳定后再灰度到生产。9. 总结与后续学习思路Kvcachescope 解决的问题本质是“外部监控工具”和“推理框架内部状态”之间的信息断层。nvidia-smi告诉我们显存被占用了但只有深入 vLLM 的 block_manager才能知道 KV cache 是真正泄漏了还是只是预分配池在等待复用。通过本文你应该掌握了KV cache 的显存占用原理vLLM 的 PagedAttention 和 block 分配机制为什么 nvidia-smi 对 KV cache 泄漏“失明”用 Kvcachescope 采集总 block / free block / used block 的方法常见显存问题的排查方向和配置调优手段。下一步可以继续深入阅读 vLLM 的PagedAttention实现、prefix caching的命中机制以及chunked prefill的显存调度逻辑。如果手头有真实的大模型服务建议先记录一份“空闲 → 压测 → 恢复”的完整显存曲线之后再改动任何配置都能通过曲线对比快速发现问题。
返回列表