1. 从GTC现场的一个反差说起:算力之外,大家都在聊缓存
这几年只要参加GTC这类技术大会,我有一个很直观的感受:台上讲的是下一代计算卡、更大规模的集群,台下工程师们三三两两聊的,却是另一个更“朴素”的问题——显存不够用了。在AI推理场景里,这个问题聊到最后,几乎都会绕回同一个名字:KV Cache。如果你正在做大模型推理服务的性能优化,或者还没彻底搞明白为什么模型跑起来后显存占用高得离谱,那这篇文章正好可以帮你把KV Cache从原理到实践完整梳理一遍。
过去大家觉得推理慢,第一反应是“GPU算力不够”,于是加卡、换卡。但当你真正把一个几十B参数的模型部署上线,开始压测高并发时,会发现大部分情况下算力并没有被打满,卡住的往往是显存容量和带宽。KV Cache就是那个在显存里悄悄膨胀的“大头”。一次对话、一批Prompt、一条长文档,全都是靠缓存中间结果才能高效生成,而这些中间结果不释放,新的请求就进不来。所以我一直觉得,不懂KV Cache的人做不好推理服务优化。
这篇文章我会先讲清楚KV Cache是什么、为什么必须有它,再给出一套可以直接套用的显存估算方法和调优经验,最后聊一聊在当前推理框架里,KV Cache如何影响吞吐、时延和成本。做算法、做后端、做MLOps的都可以来看,哪怕你现在只是在跑本地模型,搞清楚这个概念也能帮你少踩很多OOM的坑。
1.1 为什么推理瓶颈从“算力”转移到了“内存墙”
先说一个反直觉的现象:大模型做推理时,计算强度其实在持续下降。自回归生成是一步一步往外蹦token的,每生成一个token,真正做完的矩阵乘法规模并不大,难的是把上一轮算出来的K、V矩阵重新读出来用。GPU的算力可以很快,但显存带宽有限,数据从显存搬进计算单元的速度跟不上计算速度,于是整个生成过程就被“等数据”拖住了。
这就是常说的“内存墙”。你在NVIDIA的官方材料或者GTC的分论坛里会反复听到一个词叫memory-bound,意思是当前推理过程不是compute-bound,而是访存受限的。KV Cache越大,每生成一个token要读取的数据就越多,单token延迟自然就高。这也是为什么很多模型虽然参数量没变,但只要上下文拉长、并发拉高,体验就会急剧下降。你可以把KV Cache理解成一个人在做长对话时手里的笔记本,本子越厚,翻找旧内容的时间就越长。
另一个现实压力是显存总容量。GPU的计算卡虽然显存越出越大,但模型权重本身也在膨胀,还要给KV Cache腾地方。多个并发请求就意味着多份互不相同的KV Cache同时存在。我在GTC现场听到最多的不是“你们用什么架构训的模型”,而是“你们现在单卡能塞多少路并发”。这个问题背后其实就是KV Cache的体量在作怪。
1.2 KV Cache是什么:Transformer自回归生成时的那张便签纸
直接说结论:KV Cache是在Transformer模型做自回归生成时,把已经计算过的历史token对应的Key和Value矩阵缓存下来,避免每一步都重新计算前面所有token的一种工程手段。
自回归生成的特点是“自己生成的东西会变成下一次的输入”。生成第100个token时,模型需要把第1到第99个token重新纳入注意力范围。注意力机制的公式里有个关键运算:当前第100个token的Query要跟前面所有token的Key做点积,再拿结果去加权对应的Value。理论上你不缓存也没错,可以每次把整段历史都重新过一遍网络,但那样的话,生成第100个token就要重复计算前99个token的Key和Value,复杂度直接翻着倍往上涨,推理速度会慢到完全没法用。
所以实际工程中,模型在第一次看到某个token时,就会把这个token经过各层计算产生的K和V存下来。后续生成只需要拿最新的Query去跟缓存里的K、V做注意力运算。从效果上讲,KV Cache就是一个不断增长的动态内存块,序列越长、并发越多、层数越深,它占的显存就越大。这也是许多框架里即使模型权重没有变化,显存却会随着对话轮数增长而持续走高的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆开KV Cache:从注意力公式到一张显存账单
要真正理解KV Cache的优化思路,还是得回到公式层面。注意力机制的核心表达式是Attention(Q,K,V)=softmax(QK^T/√d)V。Q代表当前正在“提问”的向量,K是所有候选内容的“索引标签”,V是候选内容本身。每生成一个新token,它的Q都要去和KV Cache里所有历史token的K、V做运算,从而决定现在应该关注过去的哪些内容。
2.1 注意力机制里为什么不缓存会“亏到底裤”
想象一下不缓存的情况:生成到第1000个token时,你的输入序列实际上已经有1000个token。如果每次都要用这1000个token重新计算一遍完整的Q、K、V,再做注意力,那么这个模型每生成一步,FLOPs都在随序列长度一起增长。等生成长度到了几千上万,几乎每一步都在重复做大量无效运算。
缓存K和V之后,生成第1001个token时,理论上只需要完成新token的前向传播,然后拿这个新token的Query去和已经存在显存里的1000个Key做点积、再和1000个Value做加权。也就是说,每一层的计算量主要围绕新token展开,而不是把整段序列重新算一遍。少了重复的前向传播,推理速度会有数量级提升。
但缓存也有代价。首先它要占显存,而且随序列长度线性增长;其次每次读缓存都消耗带宽。每一步生成都要把当前需要的历史K、V从显存读进计算单元,序列越长,单步读取量越大,token生成速度越慢。所以KV Cache从来不是“有了就完事”,它是在计算量、显存容量和访问带宽三者之间做平衡。理解这一点,才能明白为什么FlashAttention、PagedAttention、KV Cache量化这些技术会轮番登场。
2.2 算一笔账:一个请求到底吃掉多少显存
很多新手用户搞不清KV Cache到底占多大地方,我直接给一个可复用的估算公式。每个token产生的KV Cache大小由模型结构决定,和输入的语义内容关系不大。给定层数L、KV头数H、每个头的维度D、精度字节数B,那么单个token对应的KV Cache字节数大概是:
python复制def kv_cache_bytes_per_token(layers, kv_heads, head_dim, bytes_per_param=2):
# 每个token在每层都有K和V两份,所以乘2
return layers * kv_heads * head_dim * 2 * bytes_per_param
def kv_cache_gib(layers, kv_heads, head_dim, seq_len, batch_size=1, bytes_per_param=2):
per_token = kv_cache_bytes_per_token(layers, kv_heads, head_dim, bytes_per_param)
total_bytes = batch_size * seq_len * per_token
return total_bytes / (1024 ** 3)
# 一个常见的7B规模MHA模型:32层、32个KV头、每个头128维、FP16
print(kv_cache_bytes_per_token(32, 32, 128)) # 约524288字节
print(kv_cache_gib(32, 32, 128, 4096)) # 2GiB
print(kv_cache_gib(32, 32, 128, 4096, batch_size=16)) # 32GiB
计算逻辑很简单:每一层都要为当前token存一份K和一份V,所以先乘2;每个KV头的维度是head_dim,KV头数量是kv_heads,层数把所有层加起来;最后乘精度字节数。FP16就是2字节,INT8是1字节,FP8也是1字节。
以刚才的7B规模模型为例,FP16下单个token大约要占512KB,4096个token的上下文就是2GiB。如果同时有16路请求都跑到4096长度,KV Cache总量就接近32GiB,这还没算模型权重和激活值。许多模型的层数更深,或者头维度更大,KV Cache会更夸张。这也是为什么长上下文场景下,看起来“只是把max_length调大了”,显存需求却成倍增长。
2.3 MHA到GQA、MLA:架构设计也在影响这张账单
传统Transformer用MHA,也就是每个注意力头都有独立的K、V投影,KV Cache自然最大。后来大家发现并非所有头都需要完整保留独立K、V,于是出现了GQA和MLA这类设计,目的之一就是大幅压缩KV Cache。
GQA的思路是让多个Query头共享一组Key和Value头。比如原本32个Query头对应32组KV Cache,如果改成8组KV头,KV Cache直接降到原来的四分之一。模型质量通常不会有明显下跌,因为很多注意力头关注的信息高度重叠。当前不少开源大模型都采用了GQA,能够明显降低推理时的显存压力。
MLA的思路更进一步,它把K、V先压到一个低维空间再参与注意力计算,缓存时存的是压缩后的向量而不是完整K、V。有MLA这类结构的模型,KV Cache可以做得非常小,长上下文场景下优势尤其明显。这类架构层级的改动说明,KV Cache不只是“工程上的缓存策略”,它已经反过来影响模型怎么设计。你在做技术选型时,如果业务里长上下文和高并发是硬需求,模型是否采用GQA或MLA应该纳入评估维度,而不是只盯模型在榜单上的分数。
3. GTC上关于KV Cache的讨论,主要有哪几条优化路线
在GTC这类技术会议上,KV Cache经常出现在推理优化相关的分享里。综合来看,业界目前的优化思路应该分成三层:算子层、存储层、调度层。每一层的目标不一样,对应的技术也不一样,千万不要混着用。
3.1 算子层:FlashAttention这类东西到底改了些什么
算子层优化的核心是减少显存读写,而不是直接减少KV Cache的体量。以FlashAttention为代表的一系列优化,针对的是标准Attention实现里中间矩阵S、P反复写入显存再读出的问题。传统实现会把QK^T这个巨大的分数矩阵完整算出来,先落到显存,再softmax,再和V相乘,这个过程中显存峰值很高,读写量也很大。
FlashAttention通过分块计算和在线softmax,把注意力计算尽量在GPU的高速缓存里完成,避免把大矩阵来回倒腾。这个技巧对长序列推理特别有价值。虽然FlashAttention不能帮你把KV Cache变小,但它能让每一步生成时读取KV Cache这件事变得更高效,从而提升解码速度。在MHA结构下,FlashAttention的收益非常明显;在GQA、MLA结构下,由于KV Cache已经变小,attention的访存量也相应降低,算子优化的收益需要重新评估。
我在实际部署中常看到有人把FlashAttention和“KV Cache压缩”混为一谈。前者解决的是数据搬运效率,后者解决的是数据占用空间。两者都重要,但优化方向和瓶颈判断完全不同。先搞清楚你的服务卡在显存容量还是卡在带宽,再决定往哪个方向使劲。
3.2 存储层:量化、压缩、以及给KV Cache做“分页”
存储层优化比较直接:把KV Cache用更低精度存储,比如从FP16压到INT8或FP8,甚至压到INT4,理想情况下显存占用分别能减少一半或四分之三。代价是精度损失带来的生成质量波动。很多文章会告诉你“量化KV Cache几乎不掉点”,但严格来说这个结论依赖任务、模型和量化粒度。有些任务对细节极其敏感,比如数学推导、代码生成、条款抽取,KV Cache量化之后可能表现得不太稳定。比较稳妥的做法是先跑一批贴近业务的评测样本,观察量化前后输出质量再决定。
另一个容易被忽略的技术是PagedAttention。它把KV Cache切分成固定大小的“块”来管理,像操作系统的分页机制一样,允许这些块在显存中不连续存放。这样做能减少因为KV Cache碎片化造成的浪费,也让多个请求在显存紧张时依然可以被调度执行。vLLM等推理框架就是靠这个思路支撑高并发推理的。从GTC的一些分享来看,KV Cache的内存管理已经从“一次性分配连续大块”走向了“按块动态分配、支持共享”的模式。
存储层优化还有更激进的方案,比如对部分层或部分注意力头的KV Cache做剪枝,或者按重要性把一部分缓存降精度、一部分保留高精度。这类方法在论文里效果不错,但工程落地时要考虑迭代成本和框架兼容性。如果你不是专门做大模型底层优化的团队,优先用框架已经集成的量化缓存和分页管理,最省力也最稳。
3.3 调度层:缓存复用和投机推理带来的思路转变
调度层优化是我认为KV Cache未来最值得关注的方向。先说前缀缓存,也就是Prefix Caching。很多请求其实共享同一段开头的Prompt,比如系统提示词、工具定义、角色设定。如果能把这段前缀算好的KV Cache放在显存或内存里,后续相同前缀的请求就能直接复用,省掉整段前缀的重复prefill计算。
我在做多轮对话服务时有个明显体感:并发用户虽然很多,但他们发送的第一轮Prompt往往都从同一段系统提示词开始。如果没有Prefix Caching,每个人都要重新计算一遍这段长期不变的内容,GPU时间被白白浪费。开启之后,这些请求的前缀部分命中缓存,GPU立刻从“从头算起”变成“增量计算”,吞吐提升非常可观。
投机推理算是调度层的一种另类思路。它先用一个小模型快速草拟多个候选token,再用大模型一次性验证,目的是减少大模型串行解码的步数。KV Cache在这个场景里依然存在,但每一步生成的实际等待时间变短了,相当于从时间维度缓解了KV Cache带来的带宽压力。
3.4 三层优化之间如何取舍
三层优化不是互相替代的关系。理想状态下,一个推理框架应该同时具备算子级高效Attention、存储级分页与压缩、调度级上下文复用。但实际业务里先做什么后做什么,取决于你的瓶颈在哪。如果单看显存占用过高,先引入KV Cache量化和PagedAttention类机制;如果是整体吞吐上不去,再考虑Prefix Caching和投机推理;如果已经觉得单Token延迟偏高,尤其长上下文下生成变慢,则有必要检查Attention算子是否用了显存高效的实现。
做取舍时不要凭感觉。我建议直接用性能分析工具,或者查看框架的日志指标来说话。比如vLLM在运行时会输出KV Cache使用量、Token吞吐等数据,先看cache usage是不是频繁接近上限,再看平均decode延迟有没有随着序列长度明显恶化。用数据定位瓶颈,比反复调参靠谱得多。
4. 真实服务里调KV Cache最容易踩的坑
理论部分讲完,说点实际操作经验。KV Cache最让人头疼的地方在于它不像模型权重那样是个固定大小的文件,它是动态增长的。动态的东西一旦失控,问题就特别隐蔽。下面这几个坑我在上线推理服务时都踩过,写出来帮你避一避。
4.1 把max_length调大,有时候不是开通能力,而是给自己上锁
很多人部署模型时,习惯把max_length或者max_model_len设得很高,理由很简单:“万一用户输入很长呢?”从产品角度这个想法没毛病,但从资源调度角度,这等于提前告诉推理框架:请给这个长度的KV Cache预留空间。
我在一次压测中发现,把max_model_len从4096调到8192后,同样的并发量下显存直接吃紧,系统为了给长上下文留余地,反而降低了同时处理的请求数量。用户实际使用时的上下文根本没到那么长,但预留的KV Cache池已经按上限占住了显存。后来我把max_model_len调到业务真实所需的长度,而不是理论上限,整体QPS立刻回升。
正确做法是统计线上请求的真实长度分布,设置一个能覆盖95%以上请求的max_model_len,再配合超长请求单独转发到另一条通道处理的策略。不要为了让“上限更高”就把所有请求都拖下水。
4.2 只盯着并发数,没算清每路请求的KV Cache占用
另一个常见误区是压测时只看显存总量。比如你感觉80GB显存很大,模型权重占40GB,还剩40GB,应该能放不少请求吧?实际上一旦请求输出变长,每路请求的KV Cache都会持续累积。假设每路请求平均总长度2000 token,一个7B级MHA模型下就超过1GiB,四十路请求瞬间就能吃掉几十GB。
如果你用的是GQA或MLA模型,情况会好很多,但依然要养成估算习惯。我习惯把“每路请求的KV Cache峰值大小”作为一个基本参数写进README,后面估算并发上限、规划显存、给业务方解释为什么并发不能无脑调大时都非常好用。推理框架虽然能动态调度,但显存物理上限在那里摆着,提前算清楚总比OOM之后再看日志强。
4.3 KV Cache量化不是“白捡”,需要分场景评估
KV Cache量化是最能快速释放显存的手段,但也不是无脑开。我试过用一个对代码能力要求很高的模型跑生成任务,开启INT8量化缓存后,输出偶发出现细微错误,尤其在某些稀有token组合的上下文里。后来换成只对部分层做量化,或者用FP8保留更多信息,问题才基本消失。
所以我的建议是:如果显存紧张到必须量化KV Cache,先不要直接压到INT4。可以从FP8或INT8试起,重点对比量化前后在长上下文、指令跟随、结构化输出这三类任务上的表现。不要让量化造成的质量损失变成线上事故的隐患。另外一个经验是,量化KV Cache的收益在长序列+高并发场景下最明显,短对话场景其实看不出多大差别,盲目量化反而可能增加框架开销。
4.4 一张KV Cache排查表
很多问题本质上就是KV Cache配置不合理,我整理成排查表,方便你直接对照。
| 现象 | 最可能的原因 | 检查和调整方向 |
|---|---|---|
| 服务启动后很快OOM | max_model_len设太大 | 调低max_model_len或显存利用率上限 |
| 并发一高就报显存不足 | KV Cache总量超过显存预算 | 引入量化缓存、分页管理,降低单路长度 |
| 长上下文下生成越来越慢 | 单步需要读取的KV Cache数据过多 | 检查Attention算子是否高效,考虑减少缓存精度 |
| 多路请求开头相同却还是很慢 | 没开Prefix Caching或前缀不完全一致 | 开启前缀缓存,统一公共Prompt格式 |
| 显存没满但吞吐上不去 | KV Cache频繁换入换出 | 查看框架日志,检查是不是连续批调度或碎片问题 |
| 量化后输出质量下降 | KV Cache/权重低精度叠加 | 改成更大量化位宽或对敏感层保留高精度 |
这张表不是万能药,但按照“先确认占用、再检查命中、最后看算子”的顺序排查,大多数问题都能定位到具体原因。
5. KV Cache之外:AI推理系统的设计正在被重塑
KV Cache不只是“模型推理时的一种缓存”,它正在改变整个推理系统和上层应用的架构方式。以前做推理服务只需要想清楚“放多大的模型、什么并发”,现在还要想清楚“KV Cache放哪里、怎么移动、什么时候复用”。
5.1 从“显存里放不放得下”到“KV Cache要往哪里搬”
单卡推理时代,KV Cache和模型权重待在同一个GPU上,模型和缓存共存。可一旦模型规模变大,或者上下文变得很长,显存放不下怎么办?通用做法是把KV Cache换到CPU内存甚至NVMe上,按需把当前计算需要的数据搬回GPU。这有点像操作系统里的内存换页,命中率高就快,命中率低就慢。
再进一步,业界已经开始讨论“Prefill和Decode分离”的架构。Prefill阶段要处理整段Prompt,计算密集;Decode阶段要逐步生成,访存密集。两者混跑时,Prefill会挤占Decode所需的KV Cache空间,互相拖累。把两个阶段拆到不同机器上,各自按需调度,就能让两边的资源都能被匹配到位。但代价是KV Cache需要跨节点传输。你说到底什么最重要?不只是缓存容量,而是缓存能不能在正确的时间出现在正确的地方。
GTC这样的场合之所以反复讨论这类架构,是因为当上下文长度从几千涨到几万甚至百万,KV Cache的体量已经大到不可能全部放在计算单元旁边。未来推理服务的核心指标会从“单卡能跑多大模型”转向“整套系统里KV Cache的命中率、移动成本和释放速度”。听起来很系统级,但它决定了你最终能不能让长上下文模型在高负载下稳定服务。
5.2 长上下文和大量Agent场景下,缓存命中率正在成为新指标
当应用开始依赖长上下文和多Agent协作时,同一个请求可能会产生多次模型调用。Agent反复读取工具文档、在不同轮次之间带着同样的“记忆”回到主对话,这些场景天然有大量重复或部分重复的上下文。如果推理框架能复用这些KV Cache,成本会大幅下降。
反过来,如果你写的应用代码随手把随机时间戳、随机用户ID拼进系统提示词,那每次请求虽然内容大体相同,但前缀完全不一样。KV Cache没法按语义“大概复用”,它只认逐token完全一致的前缀,所以这种写法会把命中率打到接近零。我建议所有做LLM应用的人把“固定公共上下文”当成一种重要工程约束,尤其是System Prompt的结构和顺序不要随意变化。
从这个角度看,KV Cache命中率和应用层代码结构密切相关。以前我们说缓存命中率是数据库、CDN的事,现在大模型应用同样要考虑。前端稍微调整一下Prompt组装方式,后端显存压力和延迟表现可能就会有明显差别。
5.3 如果你只做上层应用,KV Cache为什么也值得关心
很多人觉得KV Cache是搞推理框架的人才需要研究的领域,自己只是调API、写Prompt,听不懂也没关系。但实际上KV Cache的存储需求会直接反映成API的计费逻辑、长度限制和限流策略。你会发现商用量化后的“上下文长度”并不直接等于你可用内存,而是等于系统在KV Cache资源上的预算。
如果哪天线上服务开始频繁出现超时或限流,不一定是厂商在“卡你”,很可能就是并发请求的KV Cache总量触到了物理上限。理解这个原理之后,你在设计多轮对话应用时就会更谨慎。比如及时清理不再需要的历史轮次,不要把所有历史消息无脑拼接进Prompt,能用摘要代替完整历史时尽量用摘要。这些习惯看起来是在省token,本质上是帮推理服务省KV Cache。
6. 最后分享几点我的体感
这篇文章写到这里,核心内容基本都覆盖了。最后说点我在实操里的真实体会。
如果你是个算法工程师,可能觉得KV Cache是工程问题;如果你是个后端工程师,可能觉得它是框架黑盒。但现实是,KV Cache是连接模型结构、推理框架和应用层设计的那根线。对模型结构熟悉的人,应该明白为什么GQA和MLA能省显存;对应用熟的人,应该理解为什么Prompt前缀要稳定。只会埋头调框架参数,不往回多看一层原理,很多问题都没法从根本上解决。
我自己在跑推理服务时,现在多了一个习惯:不做线上改动之前,先用代码按公式估算一趟KV Cache占用,再去看框架自带的Metrics数据。两者对不上,说明配置或理解有问题;对上了,后面压测和调优就心中有数。另外一个实用技巧是,如果你经常做同一批测试集,可以把这批数据的固定部分抽出来作为公共前缀,并开启Prefix Caching,这样做批量评测的耗时能下降不少。这些小操作不需要惊动整个技术架构,只是把KV Cache的特性用起来,收益却实打实。
KV Cache这个看似“细枝末节”的缓存,背后反映的是整个大模型推理从“堆算力”走向“精打细算”的过程。以后再去听技术大会,如果你发现台下工程师聊的不再是谁的显卡更多,而是谁的缓存命中率更高、谁的显存利用率更实在,别奇怪,行业真的已经走到了这一步。
