news 2026/10/19 6:06:47

KV Cache显存管理揭秘:vLLM如何将大模型推理吞吐翻倍?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KV Cache显存管理揭秘:vLLM如何将大模型推理吞吐翻倍?

跑过开源大模型推理的都知道,最大的瓶颈往往不是模型权重占多少显存,而是生成过程中不断膨胀的 KV Cache。很多人第一次切到 vLLM,看到同样一张显卡,吞吐翻了几倍,第一反应是它换了什么神级算子,实际上真正拉开差距的,是显存管理思路。这期咱们只围绕一个核心问题:KV Cache 为什么存在、为什么吃显存、怎么围绕它把推理速度提上去。原理讲透之后,我还会带着你把 vLLM 的部署参数调一遍,看到底哪些配置是切中要害的,哪些只是心理安慰。

1. 先搞清楚推理慢在哪:自回归的重复劳动

1.1 一次生成一个 token:自回归的天然代价

现在的对话大模型大多是 Decoder-only 架构,生成流程很简单:输入 prompt,预测下一个 token,把这个新 token 拼到序列尾部,再预测下一个,循环直到遇到结束符。每一步都像走台阶,没有第 n 个 token 就谈不上第 n+1 个。

问题出在“重复计算”上。在第 t 步,模型要计算整个序列 x0 到 xt 的注意力,而 x0 到 x_{t-1} 这些历史 token 的 Key 和 Value,早在之前的步骤里已经算过一遍了。如果没有任何缓存,第 t 步就得老老实实把历史全部重新算一次。把从第 1 步到第 N 步的浪费累加起来,相当于白白多算了 O(N²) 次 KV 计算。序列越长、生成越久,这个浪费越离谱。

我给个不是那么严谨但特别直观的类比:就像你连续回答一百道题,每答一道新题,都要把前九十九道题的草稿重新打一遍。正常人的做法是草稿纸留在手边,后面直接翻——KV Cache 干的就是这件事。

1.2 一句话讲清 KV Cache 到底缓存了什么

KV Cache 缓存的内容,就是序列里每个 token 的 Key 向量和 Value 向量。第 t 个 token 进来,模型经过多头注意力,会为每个头算出一份维度为 head_dim 的 K 和 V。KV Cache 把历史上所有 token 的这些向量按顺序存下来,之后生成新 token 时:

  • 只计算当前这个 token 的 Q、K、V;
  • 用当前 token 的 Q 和缓存里全部 K 做点积,得到注意力分数;
  • softmax 后用分数去加权缓存里的全部 V。

历史 token 的 Q 不需要再算,因为 Q 只服务于自己当前位置;历史 token 的 K、V 更不需要重算,因为已经躺在缓存里了。这就是用显存换算力的典型交易。数学上,每步注意力计算量从与序列长度平方相关的水平,降到了与序列长度线性相关;代价是显存占用随生成 token 数线性增长。

用伪代码可以看得更清楚:

# 无缓存:每一步都在全量重算历史 K / V for step in range(1, max_new_tokens): hidden = embeddings[:, :step] # 序列越来越长 k = hidden @ w_k # 重新计算全部历史的 K v = hidden @ w_v # 重新计算全部历史的 V attn = softmax(q @ k.T) @ v # 每次都是昂贵的大矩阵乘 # 有缓存:新 token 来了只算它自己的 K / V kv_cache = prefill(prompt) for step in range(max_new_tokens): q, k, v = current_token @ (w_q, w_k, w_v) # 当前 token 的 Q/K/V kv_cache.append(k, v) # 新 KV 追加到缓存 attn = softmax(q @ kv_cache.k.T) @ kv_cache.v

自己在推理脚本里加 KV Cache 并不难,难的是管理内存。K 和 V 是形状类似 (num_heads, seq_len, head_dim) 的张量,每生成一个 token 就要往里面追加一列,每个请求长度还各不相同。如果只按最大长度一次性分配,浪费大;如果按需动态扩展,又会出现碎片。vLLM 的价值恰恰在这里。

2. KV Cache 的显存账:算清楚才能优化

2.1 一个公式估算 KV Cache 占用

先给一个很好用的估算公式:

每 token 的 KV 缓存字节数 = 2(K 和 V 两套)× 层数 × KV 维度 × 每元素字节数

这里 KV 维度可以近似等于模型的 hidden_size,因为在标准多头注意力下,所有注意力头的 K 拼接起来维度刚好是 hidden_size。举个例子,7B 级模型层数 32、hidden_size 4096、fp16 精度:

2 × 32 × 4096 × 2 = 524288 字节 ≈ 0.5MB

注意,这只是一个 token。如果模型支持 2048 长度上下文,单条序列的 KV 就是 0.5MB × 2048 = 1GB;如果同时跑 16 个请求,光 KV Cache 就是 16GB。一张 24GB 显存的卡,加载 7B 模型权重就得吃掉 14GB 左右,剩下空间只能撑起个位数并发。这也是很多人第一次意识到:生成场景里 KV Cache 可能比模型权重还占显存。

2.2 常见模型规模的缓存开销参考

我把几个典型规模的数字整理成表,方便直接查:

模型规模层数hidden_size每 token KV 缓存(fp16)单请求 2048 tokens单请求 4096 tokens
7B 级3240960.5 MB1 GB2 GB
13B 级4051200.8 MB1.6 GB3.2 GB
70B 级8081922.6 MB5.2 GB10.4 GB

如果你的模型用了 GQA 或 MQA(分组查询注意力),KV 头数会明显减少,每 token 缓存占用可以降到上表的一半甚至更少。这也是为什么有些模型参数量看着很大,却能跑很长上下文,除了权重量化之外,GQA 对 KV 的削减功不可没。

实际看模型的时候,打开它的配置文件,找到层数、hidden_size、注意力头数、KV 头数,套公式算一遍,十秒钟就能知道自己这台设备大概能撑多少并发。这个习惯我建议所有人养成,它比任何显存监控工具都靠谱。

2.3 KV Cache 是动态增长的,不能按固定静态算

模型权重是常驻显存的,加载完基本不变。KV Cache 则是边聊边长。用户每发一个字符、模型每生成一个 token,都会往缓存里追加数据。我自己遇到过的典型事故是:一个 7B 对话服务,上午压测一切正常,下午连续长对话后直接 OOM,就是没给 KV Cache 留够余量。

应对思路有两层:

  • 框架层:用 vLLM 这类能按需分配、限制最大长度、及时回收缓存的引擎;
  • 应用层:多轮对话做历史截断,或者对老年头的内容做摘要压缩,别让上下文无节制膨胀。

只做一层都是治标不治本。KV 的计算和存储代价是自回归的天然产物,你能做的不是消灭它,而是把浪费降到最低,把显存留给真正有价值的请求。

3. 显存有了还不快:碎片化与预留浪费

3.1 传统推理框架的显存管理为什么低效

如果你自己写过朴素推理脚本,大概率是给每个并发请求固定分配一块从 0 到 max_seq_len 的 K/V buffer。方案简单,但有三宗罪:

  • 预留浪费:一个请求只生成 30 个 token,却按 2048 长度给它预留空间,90% 的显存是空的;
  • 长度失衡:同一批请求长短能差几百倍,短的用不完,长的又不够,预留空间之间无法互相调拨;
  • 碎片化:一批请求跑完释放的空间长短不齐,新请求进来找不到连续空闲区,显存明明还有不少,却申请不到一块够大的连续内存。

这三件事,操作系统领域很早就遇到过。大模型推理只是把当年的内存管理问题换了个地方重新上演一遍。

3.2 高并发下浪费会被急剧放大

假设有 8 个并发请求,传统方案给每个预留 2048 token,实际平均长度只有 300 token,KV Cache 利用率只有百分之十几。换句话说,这台机器能用起来的有效并发,只有理论容量的零头。

vLLM 官方宣传的“数倍吞吐提升”主要就来自这里:把预留浪费和碎片化腾出来的显存,换成真实请求。算子优化当然有,但不是决定性因素。很多人第一次体会到“同样硬件、同样模型,换个推理框架吞吐差好几倍”时,不要惊讶,你并没有换算子,你只是把显存里跑的有效请求数翻了几倍。

4. vLLM 是怎么解决这些问题的:PagedAttention 与连续批处理

4.1 PagedAttention:像操作系统管理内存一样管 KV Cache

PagedAttention 是 vLLM 最核心的机制。思路特别朴素:把 KV Cache 切成固定大小的 block,每个 block 能装固定数量的 token(常见配置是 16 或 32 个)。每个请求维护一张“块表”,记录逻辑块号对应到物理显存的哪个位置,就像操作系统给进程维护页表一样。

  • 逻辑视角:每个请求看到的是连续的 KV 序列;
  • 物理视角:这些 KV 数据散落在显存的不同 block 里,完全允许不连续。

生成新 token 时,如果当前 block 的槽位满了,就从物理空闲池拿一个新块,挂到块表后面;请求结束,把块归还给空闲池,供后续请求复用。这消除了“长块短块无法拼接”的碎片问题,分配单位统一,释放后天然可复用。

这种设计还带来一个巨大的好处:按需分配。不再按最大长度预留,请求实际用了多少 token,就占用多少 block。短请求多的时候,显存利用率可以做到非常高的水平。

4.2 块表机制与需要注意的实现细节

具体到工程层面,vLLM 是这样干的:

  • 启动时根据 gpu_memory_utilization 算出可分配给 KV Cache 的显存,再除以 block 大小,得到总块数;
  • 每个请求持有自己的 block table,一个逻辑块对应物理显存中某个实际位置;
  • 注意力计算时,vLLM 按块读取 K/V 参与运算,每个 GPU 线程块负责一个逻辑 block 的注意力计算,块内并行,减少跨块访问。

这解释了为什么 block_size 不能拍脑袋乱设。太大,一个块只用到 1 个 token 时,其余槽位空着,内部碎片增加;太小,块表管理和调度开销变大。多数场景直接用默认值就行,不用动。

我自己调试时最常看的日志就是启动时打印的 KV cache 总量、最大并发估计,这两个数字直接决定线上容量规划。如果日志显示并发上限远低于你的预期,优先检查 max_model_len 是不是设得太大了。

4.3 连续批处理:不等整批跑完,随时补位

显存利用率上去了,吞吐还有一个隐藏瓶颈:GPU 空转。传统批处理框架是等一批请求全部生成完,再把新请求组织成下一批。如果批里有一个长请求在慢慢生成,其他短请求早就结束了,GPU 只能等这个最长请求结束,白白浪费算力。

连续批处理(continuous batching)的思路是:每轮生成一个 token 之后,立刻检查批内请求状态,谁生成完谁退出,同时把排队中的请求补进当前批。GPU 每个 step 都满载运行,不等待集体完成。

这个特性对对话场景极其友好,因为用户请求长度差异极大,有的回一句就结束,有的要生成几千字。没有连续批处理,短请求会被长请求“绑架”,整体延迟感受特别差;有了之后,长短请求可以交错进行,服务整体吞吐能提升一个量级。

调度层面上,vLLM 一个批里会混着两种任务:正在做 prefill 的新请求,和正在做 decode 的存量请求。调度器会动态平衡两者的比例,对外使用只需要明白它比“整批等待”高效得多。

5. 实操:部署 vLLM 并调出高吞吐

5.1 最小可用的推理配置

先装依赖:

pip install vllm

然后一个最小可用的 Python 示例:

from vllm import LLM, SamplingParams llm = LLM( model="你的模型权重路径", dtype="bfloat16", gpu_memory_utilization=0.90, max_model_len=4096, ) params = SamplingParams( temperature=0.7, max_tokens=512, ) outputs = llm.generate( ["解释一下 KV Cache 的作用", "PagedAttention 是如何管理显存的?"], params, )

model 参数支持本地权重路径,也可以填在线仓库名。我推荐先用本地权重跑通,这样环境变量、依赖版本的影响范围最小。dtype 保持默认 auto 也行,硬件支持 bfloat16 就优先 bfloat16,不支持就退 float16。

vLLM 启动时会自动检测当前环境能分给 KV Cache 多少显存,并打印出来。你不需要先手动算一次,但一定要看懂那段日志,后面调优全靠它对照。

5.2 直接影响 KV Cache 容量的关键参数

最影响并发和吞吐的参数不是 temperature,而是这三个:

  • max_model_len:决定单条请求允许的最长 token 数。设得越大,每个请求最多可占用的 block 越多,并发上限自然下降。别盲目跟模型“最大训练长度”走,要看你的业务实际。客服场景的对话大多数几百 token,你给它预留 4096 就是浪费。
  • gpu_memory_utilization:默认 0.9,表示 vLLM 可使用整卡显存的比例。0.9 意味着预留 10% 给权重加载、运行时上下文、临时计算张量。如果这张卡还要跑别的进程,必须相应调低。
  • max_num_seqs:限制同一时刻批内并行的请求数量,防止个别大请求把缓存池挤爆。先不改,OOM 再调。

我举个实际调参的例子。24GB 显存跑 7B 模型:

  • 权重 fp16 约 14GB;
  • gpu_memory_utilization=0.9 时,vLLM 认为可用约 21.6GB;
  • 减去权重后留给 KV Cache 的约 7.6GB(实际还要扣运行时开销,略少);
  • 7B 模型每 token 约 0.5MB,7.6GB 大约能缓存 12000 个 token;
  • 如果 max_model_len=2048,单请求最坏占 1GB,并发上限约 6~7 条;
  • 如果拉高到 4096,并发上限降到 3~4 条。

所以你会发现,把 max_model_len 从 4096 砍到 2048,并发直接多一倍。做短对话服务,这个参数是非常值得压榨的配额。

5.3 验证提速效果的两个指标

部署完别只看“能跑”,建议固定两个指标来量化效果:

  • 吞吐(tokens/s):拿一组长度差异大的真实请求跑压测,统计累计 completion tokens 除以总耗时。这是衡量服务整体处理能力的核心指标。
  • 首 token 延迟(Time To First Token,TTFT):从发起请求到看到第一个输出 token 的时间。长 prompt 场景主要受 prefill 阶段影响,KV Cache 在 decode 阶段才发力,TTFT 偏慢不一定是缓存配置有问题。

调参的时候两个指标要一起看。吞吐上去了,TTFT 却飙升,交互体验还是不行。我自己一般先压 max_model_len 提吞吐,再看 TTFT 是否还能接受,两者之间找一个业务可接受的点。

6. 常见问题与排查实录

6.1 总是 OOM:先看三处配置

遇到过最典型的场景:同一个模型,之前用原生脚本跑得好好的,换 vLLM 反而容易 OOM。多数情况下不是 vLLM 更吃显存,而是 gpu_memory_utilization 设置过高,同时 max_model_len 开得太大。

排查顺序我建立一个固定清单:

  1. 看启动日志里 KV Cache 容量分配了多少,如果可用显存几乎为 0,说明比例设太满;
  2. 把 gpu_memory_utilization 降到 0.85 试一遍;
  3. 如果业务允许,把 max_model_len 从 4096 砍到 2048 或 1024;
  4. 最后考虑 KV Cache 量化(FP8 或 INT8),显存能省接近一半,多数任务精度损失很小。

自己写推理脚本也遵循同一个原则:别把显存算到 100%。除了权重和 KV Cache,临时激活值、运行时上下文、碎片都会占空间,全塞满必然翻车。

6.2 长 prompt 首字慢:这是 prefill 的事,别怪缓存

有人反馈:“prompt 有 3000 token,加了 vLLM 之后第一个 token 照样好几秒,是不是没生效?”

这里要厘清一个阶段差异。处理 3000 token 的 prompt 时,模型需要一次性为这 3000 个位置计算 Q/K/V,并产出第一个新 token。这个阶段叫 prefill,计算量大,KV Cache 这时候帮不上忙——本来这些 KV 就是第一次算完再存进缓存的。首 token 延迟在这个阶段主要受算力制约。

想优化 prefill 有几条路:

  • 多轮对话里把 system prompt 和历史记录压得短一些;
  • 开启 vLLM 的前缀缓存(prefix caching),重复前缀直接命中缓存,跳过重复计算;
  • 请求规律强、固定 system prompt 加几千 token 文档的场景,前缀缓存收益非常可观。

6.3 KV Cache 量化与可能的方向

显存不够时,最直接的省法是降低 KV Cache 每元素字节数,从 fp16 的 2 字节压到 INT8 的 1 字节,或者用 FP8 等更低精度格式。vLLM 对这块有原生支持。实测下来,大部分对话和生成任务精度损失可接受,但数学推理、代码生成这类对 token 分布敏感的任务,上线前建议先跑一轮评测。

最后再提一句,加速不只是 KV Cache 和 vLLM 两件事。投机采样优化的是 decode 步数,FlashAttention 优化的是注意力算子的访存效率,这些都能与 KV Cache 叠加使用。搞明白 KV Cache 这笔账之后,看其他优化手段会轻松很多。

我个人实操中的体会是:KV Cache 不是模型参数,也不是可以随手关掉的“锦上添花”。它是 Transformer 自回归结构里必然存在的账。真正提高吞吐的关键,是算清楚每条请求占多少缓存空间,然后让推理框架把显存的余光都用干净。我踩过最深的坑就是一开始把 max_model_len 拉满,不求甚解,结果并发上不去;后来拿着公式一算,砍到业务真实长度,吞吐直接翻倍。先算账、再调参,这个习惯比记住任何单个参数都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 6:06:46

毕业论文AI辅助工具怎么选?6款实用工具对比与全流程实操指南

每年三四月份,总有一批人同时陷入同样的焦虑:文献没读完、大纲被推翻、初稿写不出来、查重红了一片。我这些年帮不少人看过毕业论文,发现一个越来越明显的趋势——大家手里都装了AI工具,但用得好的没几个。大多数人要么把AI当百度…

作者头像 李华
网站建设 2026/10/19 6:06:30

手推RNN与LSTM反向传播:从BPTT到梯度消失的数学原理

1. 从"能跑就行"到"心里有底":为什么还要手推RNN/LSTM的反向传播刚入门深度学习那会儿,我调包调得飞起,model.fit()一按,梯度自己就更新了,损失曲线往下掉就开心,不降就换学习率、加层…

作者头像 李华
网站建设 2026/10/19 6:05:26

artcraft 创意落地指南:从灵感到成品的全流程拆解

1. 从“artcraft”这个名字说起:它到底想解决什么问题第一次看到“artcraft”这个词,我脑子里蹦出来的第一反应是“艺术”加“手艺”的组合。这不是一个随便拼出来的名字,它背后指向的是一类非常具体的东西:把创意表达和动手实现捏…

作者头像 李华
网站建设 2026/10/19 6:05:26

MT5画图工具统一界面设计:集成8种常用绘图对象的高效实践

1. 为什么要给MT5画图工具做一套统一界面做MT5量化或者手工交易的朋友应该都有体会:平台自带的画图功能其实不弱,趋势线、斐波那契、通道、矩形这些基础对象全都有,但真正用起来,总觉得差点意思。最典型的问题是——画一个工具要切…

作者头像 李华
网站建设 2026/10/19 6:05:19

分类问题入门:K近邻与逻辑回归实战解析

1. 从回归到分类:为什么"预测数值"和"预测类别"是两个世界上一篇笔记里,我把机器学习的整体框架捋了一遍,重点是线性回归和最小二乘法——用一条直线去拟合数据,预测房价、温度这种连续数值。今天这篇笔记2&a…

作者头像 李华
网站建设 2026/10/19 6:05:15

Windows上跑iPadOS模拟器:三条路线与避坑实操指南

简介:这是一份面向希望在Windows环境(含Windows 7)体验苹果iOS系统的PC/iPad模拟器资源,适合开发调试、应用兼容性测试及普通用户尝鲜。资源以RAR压缩包形式提供,整体约48.79MB,文件总数与内部类型明细显示…

作者头像 李华