很多刚接触大模型推理的朋友,都会在长文本或多轮对话任务上撞上同一堵墙——显存莫名其妙爆了。模型权重文件看着不大,可实际一跑照样OOM。这里面的隐藏大头,十有八九就是KV Cache。我最早被它坑,是在给一个8B模型接128K长文本场景的时候,当时把权重、激活、批量大小都盘了一遍,最后一算KV Cache,才意识到问题压根不在模型本身。今天这篇就把KV Cache彻底聊透:它是什么、为什么非要存在、怎么把显存吃掉那么多、以及在实际框架里怎么调优和避坑。
这篇文章适合两类人:一类是刚开始玩大模型部署,被长上下文和并发请求打得满头包,想知道显存到底花在哪的;另一类是已经在用vLLM、Ollama这类框架,但对底层参数一问三不知,只会照着默认配置跑,出了问题不知道怎么调。读完之后,你应该能自己估算任意模型的KV Cache大小,看懂框架日志里那些跟cache相关的指标,并且在多轮对话、长文本、企业私有化部署这些场景下,知道该动哪些配置而不是瞎加显存。
1. 先说结论:KV Cache是什么,为什么绕不开
KV Cache,字面拆开就是Key-Value Cache,大模型推理时用来缓存历史token的Key矩阵和Value矩阵的一块显存区域。它不改变模型权重,不改变输出逻辑,纯粹是中间计算结果的复用。但就是这个看似简单的缓存,成了长上下文时代推理性能和显存开销的核心瓶颈,几乎所有主流推理框架都围绕它做了一堆文章。
为什么绕不开?因为大模型生成文本本质上是自回归:一个字一个字往外蹦,每蹦一个新token,都要拿这个新token去和历史所有token做一次注意力计算。如果没有KV Cache,相当于每次生成新字都要把整个历史重新算一遍,计算量随序列长度二次增长,实际应用中根本没法用。有了KV Cache,历史计算结果被保存下来,每次只需要做增量的计算,速度和显存占用达到一个相对可接受的平衡。这个平衡点怎么卡,就是框架和部署人员天天琢磨的事情。
从应用场景来看,KV Cache的份量随着上下文长度增长被无限放大。短对话时代,上下文几K,缓存占用还算温和;到了现在动辄128K甚至百万token上下文的时代,KV Cache吃掉的内存可能比模型权重本身还大好几倍。多模态大模型同样面临这个问题,图像token数量一多,KV Cache规模跟着膨胀。所以不管你是做应用开发、私有化部署还是模型推理优化,KV Cache都是绕不过去的一块硬骨头。
2. 剥开原理:自回归生成时到底在重复算什么
2.1 从一次生成讲起:Prefill和Decode的区别
要理解KV Cache,先得理解大模型的一次完整生成过程。假设你输入“今天天气真”,模型要接着生成“好”字。这个过程分为两个阶段:
- Prefill(预填充)阶段:把整段输入“今天天气真”一次性喂给模型,逐层计算出所有token的Key和Value矩阵,同时得到第一个输出token“好”的概率分布。
- Decode(解码)阶段:把“好”作为新输入,计算它的Key和Value,再和前面所有历史token的Key、Value做注意力计算,得到下一个token“啊”。
看起来很简单对吧?但仔细想想就发现不对劲。在Decode阶段,模型要算“好”和前面每一个历史token的注意力分数。注意力公式是softmax(Q乘K的转置除以根号d)再乘V。要算新token和历史token的注意力,就必须拥有历史所有token的K和V。如果没有缓存,每次生成一个新字,模型都得把“今天天气真好啊你这个...”所有历史token重新输入一遍,重新算一遍它们的K和V,然后才能跟当前新token做注意力。序列长度1K的时候,意味着每生成一个token要重复算1000个token的全套前向计算;序列长度100K的时候,就变成每生成一个字要算10万个历史token,慢得不可想象。
我经常用一个做饭的类比来解释这件事。就好比你每天要做十几道菜,每做一道新菜都要把之前所有菜的全部食材重新洗一遍、切一遍、备一遍,而不是直接用已经处理好的半成品。KV Cache就是那个“已经洗好切好、放冰箱备用”的半成品仓库。第一批菜(Prefill)必须完整准备,后面每加一道菜,只需要把它自己的食材处理好(增量计算),然后直接拿仓库里的半成品组合炒菜就行。这个仓库占的地方,就是显存。
2.2 为什么只缓存K和V,而不缓存Q
这是初学者最容易问的问题:既然K、V要缓存,为什么Q不缓存?Q不是也算过了吗?答案藏在自回归的结构里。
注意力的计算逻辑是:Q用来“问问题”,K用来“匹配”,V用来“取内容”。当模型生成第t个新token时,它需要问的问题是“基于我当前的想法,历史哪些位置跟我相关”。这个“我当前的想法”每一次都在变,因为生成过程是逐字决策的,每一个新token都代表一个新的问题状态。所以Q永远只跟当前位置有关,它就是当前位置的隐状态经过权重矩阵映射出来的,属于一次性用品,这一次用完了,下次生成下一个token时就不再需要它。
但K和V不一样。历史token的K是“我有哪些标签可以被检索”,V是“我具体包含什么内容”。未来的每一个新token都要跟历史全体做注意力,也就是说,历史上的每一个token都要被未来所有新token反复检索、反复读取。这就是为什么K和V必须长期保鲜,一直放到序列结束或者被淘汰为止。可以这样理解:Q是流动的问询者,来了就走;K和V是档案柜,所有新来的问询者都要查阅这堆档案。
另一方面,如果连Q也缓存,意味着你缓存的是“上一次决策时各层的查询向量”,但那个向量对应的决策已经结束了,下一次决策有自己全新的Q,旧Q对后续计算没有任何贡献。缓存它不仅不会省算力,反而白白增加显存占用。所以KV Cache这个名字起得很精确,就是只缓存K和V,多了不要。
2.3 从MHA到GQA:为什么大家都在压缩KV头数
标准Transformer是MHA(Multi-Head Attention),也就是每一层有多个注意力头,每个头都有自己的Q、K、V投影矩阵,KV Cache的大小跟“注意力头数”直接挂钩。头越多,模型表达能力越强,但KV Cache也越大。这就催生了一个专门针对KV Cache的模型架构改动:GQA(Grouped Query Attention)和MQA(Multi-Query Attention)。
GQA的思路很简单:Q头保持多个,K和V头大幅缩减。比如Llama 3.1 8B模型,32个注意力头,但KV头只有8个,相当于每4个Q头共享一组K、V。MQA更激进,所有Q头共享同一组KV,也就是KV头数直接变成1。为什么敢这么干?因为实践发现,对KV做强制压缩对模型效果的影响远小于当初大家的担心,但换来的是KV Cache直接除以4甚至除以8,推理速度和显存压力都大幅改善。
这个变化在我实际用下来感受非常明显。早前用原始MHA架构的模型,同样上下文长度下显存占用几乎是同规模GQA模型的两三倍。现在新出的模型,从Llama 3系到DeepSeek系,几乎都在往压缩KV的方向走。DeepSeek那套MLA(Multi-head Latent Attention)走得更远,把KV压缩成低秩形式,缓存规模还能再小一个量级。理解这个背景很重要,因为很多人比显存时会拿不同架构的模型直接对比,却没意识到KV头数不同导致KV Cache规模天差地别,这种对比是没意义的。
3. 显存开销到底有多大:手把手算一笔账
3.1 KV Cache大小的计算公式和推导
我之前被KV Cache坑过一次之后,就把它的计算公式焊死在脑子里了。它的构成非常规律,每一项都有明确含义:
KV Cache大小 = 2(Key和Value两份) x 层数 x KV头数 x 头维度 x 序列长度 x 批量大小 x 单元素字节数
为什么是2?因为K和V各一份,这是最基本的乘法因子。为什么乘层数?因为每一层Transformer都需要自己的KV Cache,不能跨层共享。为什么乘KV头数和头维度?因为注意力计算是按照“头”来组织的,每个头的K、V矩阵都是一个序列长度乘以头维度的二维结构。最后乘序列长度和批量大小,序列越长、同时处理的请求越多,缓存自然越线性膨胀。
举一个真实例子。假设用Llama 3.1 8B模型,参数规模8B,层数32层,KV头数8,头维度128,上下文长度开到128K(131072个token),使用FP16精度(单元素2字节),批量大小为1。
代入公式:2 × 32 × 8 × 128 × 131072 × 2。算一下这个规模:2乘32等于64,乘8等于512,乘128等于65536,乘131072,最后乘2字节。结果大约17.18GB,约等于16GiB。这还只是KV Cache的占用,还没有算8B模型权重的占用和激活值。也就是说,当你把上下文长度开到128K时,KV Cache吃掉的内存已经超过了模型权重本身(8B FP16权重约16GB)。这就是为什么很多人在框架里把max_model_len调到128K后,显存立刻爆掉的原因——不是模型太大,是KV Cache太肥了。
3.2 长上下文和并发下为什么会爆显存
上面的例子还是单请求、批量大小等于1的情况。实际部署中不可能只跑一个请求。假设并发32路,每路平均上下文32K(32768个token),KV Cache就是上面的公式乘32。量级瞬间来到上百GB,单张80GB的A100/H100根本扛不住。这也是为什么KV Cache从一诞生就和“批量大小”“并发度”捆绑讨论——它不是单请求问题,是并发吞吐问题。
另外有个很容易被忽略的点:长上下文场景下,即便模型权重和KV Cache加起来超了显存,你也未必能立刻发现是KV Cache在“作案”。因为框架通常会先加载权重,再按配置预分配KV Cache空间。如果你把上下文长度设得很大,框架会把后面的KV Cache空间一次性保留出来,导致前几个请求还没跑起来,显存就已经被预占到OOM临界点。我见过不少人把上下文调到128K后,模型加载都成功了,可第一个请求一进来就报Allocation failed,查半天才发现是KV Cache预分配把显存吃干净了。
拿70B模型算一笔更夸张的账。Llama 3.1 70B,80层,KV头数8,头维度128,FP16精度,同样是128K上下文:2 × 80 × 8 × 128 × 131072 × 2,约42.9GB。加上140GB的权重,单卡完全没戏,即使双卡也得很精打细算。如果继续往上走到1M上下文,那个KV Cache规模直接是离谱,单请求就要136GB以上,完全不是普通显存能覆盖的量级。这也是为什么各家搞百万级上下文的模型,都需要在KV Cache压缩上下狠功夫,单纯靠堆显存是堆不出来的。
3.3 量化能省多少:FP8与INT8的取舍
既然KV Cache吃显存,最直接的省钱办法就是降低精度。KV Cache从FP16换成FP8,大小直接减半;换成INT8,同样减半。上面的8B模型128K上下文例子,KV Cache从约16GiB变成8GiB,省出来的一大块显存可以直接变成并发容量或上下文长度。
但代价是什么?KV的精度损失会影响注意力计算的准确性,进而影响生成质量。我自己做过多组长上下文的测评,KV Cache量化后在最开始几轮对话几乎感知不到差异,但在100K以上超长文本、需要精确检索历史信息的高难度任务上,量化后会出现轻微的“记错”“混淆”现象。更稳妥的做法是:如果你的显存还够用,优先保持FP16;如果实在紧张,用FP8而不是直接上INT8;不要为了省显存对全模型无差别量化KV,很多框架支持按层配置,可以把容易出错的层保留高精度。
专业建议是:先看你的核心场景是长文本精确问答还是普通多轮闲聊。前者KV Cache量化带来的质量风险更高,后者基本无感。我个人的经验是,在推理框架里把KV Cache量化开关和权重量化开关分开看,权重量化对显存的影响很大,但KV量化对长序列场景的影响更微妙,不要一键全开。
4. 工程落地:推理框架里的KV Cache调优实操
4.1 主流框架是怎么管理KV Cache的
理论归理论,实际部署中没人手写KV Cache管理逻辑,都是依赖推理框架。但了解框架怎么管理KV Cache,直接决定了你配置参数时脑袋是清醒的还是懵的。
vLLM对KV Cache的贡献最有代表性,它把操作系统里的“分页”思想搬到了显存上,用固定大小的block来切分KV Cache,通过一张block table把逻辑上连续的KV Cache映射到物理上不连续的显存块上。这带来的好处是:碎片化问题大幅缓解,不同请求之间的KV Cache不再互相挤占,而且支持“按需分配”,序列增长到多少就给多少块的缓存,不会一次性预分配导致浪费。所以在vLLM里,你经常会看到--block-size这样的参数,默认通常是16或32,代表一个block能装多少个token的KV。bock size设得小,内存粒度细但寻址开销大;设得大,吞吐好但浪费也可能更多。想要精细化调优的话,多观察日志里的cache usage,慢慢改。
llama.cpp和Ollama是另一套思路。它们更偏这种本地和边缘设备,内存比显存更敏感,所以采用的是连续内存池加滑动窗口式的管理方式。llama.cpp里常见的--ctx-size(或者Ollama的OLLAMA_CONTEXT_LENGTH/num_ctx)控制上下文长度,同时也决定了KV Cache的上限。它的特点是超过上下文长度后采用滚动覆盖,最老的token被挤出去,新的token占进来。好处是内存占用恒定可控,坏处是如果上下文上限设得太小,模型聊着聊着就把之前的设定“忘掉”了,这就是很多人吐槽“多轮对话越聊越蠢”的常见原因之一,不一定是模型不行,是缓存把老内容扔了。
4.2 常用配置参数与调优方向
在实际部署中,我列几个最常用的参数和我的调优思路。
首先是max_model_len(或ctx_size),这个参数直接决定KV Cache的上界。很多框架会按照这个配置去预分配缓存空间,设得太高,显存被缓存预占;设得太低,长文本溢出或滚动覆盖。我的习惯是:先想清楚产品里用户最长的一段输入加输出总共多少token,再乘一个1.2到1.5的余量,设置成这个值。比如你的应用最长输入8K、输出8K,那max_model_len设16K就足够,没必要直接把128K拉满。
其次是显存利用率gpu_memory_utilization,vLLM里通过这个参数控制“最多把多少比例的GPU显存交给框架使用”,默认0.9,通常含义明确。剩下的显存是留给CUDA、算子库和临时激活值的。我见过有人为了多挤一点KV Cache空间把它拉到0.98,结果一进请求就报CUDA out of memory,因为算子执行也需要一点临时显存。这个参数一般来说0.85~0.92都是比较安全的区间,具体要看你的模型本身有没有同时跑权重量化、是否有多卡并行这些变数。
再次是KV Cache的数据类型参数,vLLM里常见的是kv-cache-dtype或quantization里的相关开关,支持FP8/INT8等。如果你确实需要超长上下文,优先考虑FP8。如果跑的是纯本地、硬件带宽有限的小模型,甚至可以尝试INT8,用质量换取长上下文可能性,这个要靠自己的评测权衡。
最后是block级别的参数,比如block_size。默认值一般够用,但如果你观察到某一类场景总是出现缓存碎片率高、利用率低,可以试着调大或调小block大小做一组对比。这个属于进阶调优,新手不要一上来就动,先跑通默认配置,再根据监控指标去改。
4.3 多轮对话、私有化部署场景的落地经验
我最近常帮朋友接企业私有化部署,用的都是Ollama加vLLM这类方案。这类场景最容易踩的坑就是多轮对话“越聊越傻”。很多人部署完本地模型就觉得完事大吉,却不知道默认上下文可能只有4K左右。用户的聊天记录稍微一长,最前面的信息就被滚动覆盖了,模型后面聊的内容完全丢失了上下文,答非所问。解决方案在Ollama里很简单:根据业务需求调整num_ctx参数,或者在模型配置里通过Modelfile设定上下文上限。但注意,调大上下文后KV Cache占用跟着涨,本地Mac或普通PC的内存会瞬间爆掉,所以还得配套把KV Cache量化打开,或者限制对话轮数后主动清理上下文。
vLLM做企业服务化时,我强烈建议多看两个指标:cache hit rate和cache miss rate。这个指标反映的是“新请求有多少KV Cache能直接复用”。如果命中率低,说明你的请求之间历史内容差异大,前缀相同的地方少。如果命中率高,说明很多用户请求的system prompt、工具描述这些长前缀是一样的,这时就可以把prompt template做成固定前缀,让vLLM直接复用已有KV Cache,不用重新计算。我实测过,系统性优化前缀复用之后,服务吞吐能明显提升。
还有一个容易忽略的:长上下文的“填充式等待”。模型生成回答时,越长的历史KV Cache会让每次decode的注意力计算越慢,响应延迟和令牌生成速度会随对话轮数增加而下降。这不是bug,而是KV矩阵变大后的自然结果。应对方式要么限制上下文长度上限,要么在框架层开sliding window、设置早停条件,要么直接把超长对话做摘要压缩——把历史总结成一段摘要文字,替换掉原始长文本。我实际操作中觉得摘要方案最实用,既能保留关键信息,又不会让KV Cache无限膨胀。
5. 常见问题与排查实录
5.1 问题速查表
这一节把我实际跑KV Cache相关场景时遇到过的典型问题整理成速查表,方便照着排查。
| 现象 | 常见原因 | 排查方法 | 处理建议 |
|---|---|---|---|
| 请求一发就OOM | KV Cache预分配过大 | 看框架日志里cache allocation的显存数值 | 调小max_model_len,降低gpu_memory_utilization或开启KV Cache量化 |
| 多轮对话“失忆” | 上下文上限太小,历史被滚动覆盖 | 打印实际输入token数,对比模型上下文上限 | 调大num_ctx/ctx_size,或主动做历史摘要压缩 |
| 上下文开大了但速度极慢 | 每次decode都要遍历全部KV Cache | 观察token生成速度随时间下降 | 使用sliding window、限制上下文,或换用KV压缩更强的模型架构 |
| 并发一高就OOM | 每个请求的KV Cache随序列动态增长 | 估算峰值批量大小乘序列长度的KV规模 | 降低并发上限,开启分页缓存和显存交换,必要时量化KV |
| 长文本回答质量下降 | KV Cache量化过狠 | 对比FP16和FP8/INT8同样长文本下的生成结果 | 只量化部分层或改用FP8,不要无差别上INT8 |
5.2 从“不知道”到“会调参”的几件事
我发现很多人用框架部署模型,从不会主动去看KV Cache相关的日志和指标,导致出了OOM只能重装重启。这里分享三个我踩坑后才形成的习惯。
第一个习惯:看启动日志。vLLM启动时会明确打印模型权重占用、KV Cache预分配大小、剩余可用显存等信息。Ollama虽然日志少,但也可以通过ollama ps看当前加载模型占用的内存量。这些数字比任何估算都准确,每次配置改动后先读一遍日志,心里就有底了。
第二个习惯:用压力测试代替直觉。在正式上线前,我会写一个简单的脚本,用递增的“输入长度+输出长度”组合批量发送请求,观察显存变化曲线。特别是找出“临界上下文长度”——在这个长度下,某个并发数的KV Cache刚好能把显存占满。知道了这个边界值,运营和产品就能心里有数,知道最多允许用户聊多长、并发多大。
第三个习惯:在做批量服务时打开分页缓存与显存交换。vLLM支持将长时间不活跃的KV Cache分页交换到内存,这为内存充裕而显存紧张的服务器提供了一种折中方案。如果你不想让批处理降级,可以限制每个请求最长上下文,但允许很多请求并行,让KV Cache总量可控。这是实际服务化中性价比最高的一套组合拳。
5.3 几个容易让人误判的坑
关于KV Cache还有一个常见的误区,就是拿上下文长度直接等同于模型“记忆力”。模型能够处理的上下文长度只是缓存容量的上限,不等于它真能有效利用那么长历史里的信息。我实测过很多长文本任务,100K上下文中关键信息出现在50K处时,模型检索精度通常不如10K以内。KV Cache只是给模型提供“看过”,模型能不能找准关键内容,还得看注意力机制和模型训练水平。所以在产品设计上,不要因为KV Cache够大就给用户开无限长的上下文,质量不一定跟得上,纯烧显存。
另一个误区是只关注“扩缓存”不关注“省带宽”。在真实的批量服务中,KV Cache的读写带宽消耗甚至比显存容量更先成为瓶颈。当外部请求并发高起来,GPU需要频繁读取每个序列的KV矩阵进行注意力计算,带宽一旦打满,显存容量反而用不满,性能照样上不去。这种情况下光扩显存没有用,要看模型架构是否做了GQA这类KV头压缩,是否能用FP8降低带宽需求。我在调优时遇到过这种情况:把KV量化打开后,吞吐增长了,不是因为显存省了,而是因为同样带宽下能搬运更多的有效请求。
最后说一个Dify接本地大模型时容易被忽略的坑。Dify这类应用平台在编排多轮对话时,默认会带很长的system prompt、工具描述和对话历史。这些内容每次请求都会进入模型,如果模型侧的max_model_len没有同步调大,就会出现“请求被截断”“历史丢失”。但调大之后,KV Cache压力也跟着变大。我的建议是:在Dify里把不参与推理的静态内容尽量做成固定前缀,配合vLLM/推理框架的前缀缓存复用机制,大幅提高缓存命中率;不要把每轮对话都拼成一整段无规律的动态文本,否则KV Cache每轮都得从头算,服务性能会非常难看。
我个人在实际操作中最大的体会是:KV Cache不是一个需要“背下来的概念”,而是一个需要在部署反复碰壁之后才能深刻理解的东西。每次遇到推理性能相关的坑,你只要问自己三个问题——缓存多大?缓存放哪?缓存有没有被复用——基本就能定位问题。先把这个公式记牢:KV大小等于2乘层数乘KV头数乘头维度乘序列长度乘批量大小乘字节数,然后去看你手上框架的日志参数,所有部署层面的谜团都会清晰起来。
最后再分享一个小技巧:当你手头只有一台普通显卡机器,却想跑超长上下文应用时,别把宝全押在单机扩容上。先用KV Cache量化把缓存缩一半,再把上下文限到业务刚需的长度,最后考虑换更大显存的机器——这个顺序能省下最多的钱。把KV Cache这件事整明白了,你对大模型推理性能和工程调优的理解,会直接上一个台阶。