首Token为何慢、后续快?KV缓存决定大模型推理性能
很少有一个体验能像聊天AI那样制造出如此固定的节奏输入一长段文本点击发送光标转圈等待一两秒后第一个字蹦出来然后后面的内容像泄洪一样哗哗往外冒。很多人的第一反应是第一个token慢是因为模型算力太重它在“思考”怎么回答一旦想明白了输出自然就顺了。这个解释听着合理但方向基本是错的。那个让首token变慢的东西和让后续输出变得丝滑的东西其实是同一件事——KV缓存。今天我想把这件事彻底讲清楚为什么首token要等为什么后面又这么快以及KV缓存到底在服务的哪个环节扮演了什么样的角色。这篇文章适合两类人。一类是刚开始接触大模型推理的开发者想搞懂服务端部署时显存为什么总不够用另一类是纯用户视角、只是好奇聊天AI底层机制的朋友。我会尽量把公式和术语都翻译成人话同时保留足够的工程细节保证读完你能自己估算一个模型的KV缓存开销也能在部署时准确判断服务到底是卡在算力还是卡在缓存。1. 从点击发送到第一个字那几秒钟服务端到底在忙什么我们先还原一下服务端那一刻的状态。模型权重已经加载在显存里用户把一段可能长达几千字的prompt发过来推理引擎需要做的事情并不是“从第一个字开始慢慢想”而是一次性把整段prompt跑完一整轮前向传播。这个过程在工程上叫prefill也叫预填充阶段。prefill阶段要做的事情很重模型要把prompt里的每一个token都过一遍所有层算出一组对后续生成至关重要的中间结果。这组中间结果就是所谓的Key和Value也就是KV。每一层Transformer都会给当前序列里的每个位置计算一份K向量和V向量然后把这些向量缓存起来。首token的延迟几乎全部消耗在这个阶段上。用户感知到的“第一个字”其实是prefill跑完之后从最后一个位置的logits里采样出来的结果。也就是说在你看到第一个字之前服务端已经默默把整段prompt“读”完了并且把它压缩成了一张可检索的索引表。这张索引表就是KV缓存。后面每个字的生成都是在这张表的基础上进行的。1.1 所有对话AI都逃不开的两段式生成很多人以为AI写答案就像人写文章是一边想一边写写到哪算哪。实际上大模型生成是严格的自回归过程先有第一个token把它拼回输入序列再预测第二个token再拼回去如此循环。每一步只能生成一个token整个句子是一个token一个token“挤”出来的。这带来一个工程上的必然结论生成阶段天然分成两大段。第一段是你把整段prompt一次性给模型计算所有位置的状态这叫prefill第二段是从已经算好的状态出发逐token往外蹦这叫decode。两段的工作量完全不同表现出来的速度也完全不同。prefill阶段的特点是“一次算一大片”可以充分压榨GPU的矩阵计算能力decode阶段的特点是“一次只算一个点”但每一步都要查阅之前所有的KV缓存。所以如果你把首token耗时和后续每token耗时分别测出来会发现一个很有意思的现象prompt越长首token越慢而后续token的速度在上下文不太长的时候基本稳定。这就是两段式生成最直观的印记。1.2 真正可复用的“索引”KV缓存的雏形prefill阶段为什么要专门去算K和V因为它们是后续所有生成的“素材”。在注意力机制里K可以理解为“这个token能被什么特征检索到”V是“这个token真正要提供给下游的信息”。当一个新token产生时它需要拿着自己的查询Q去和历史所有token的K做匹配找到哪些历史信息值得参考再把这些信息的V加权求和。关键在于历史token的K和V只取决于它们自己不取决于后面谁会来查它们。所以一旦算出来就可以长期保存。服务端每生成一个新token都会为这个新token也算一份K和V追加到缓存里。对话越长缓存就越厚缓存越厚后面每一步要翻阅的资料就越多。聊天AI前期像在读一本新书后期则像在一摞越来越厚的笔记里快速翻找答案。理解了这一点就能明白为什么标题说“KV缓存才是丝滑输出的秘密”。如果没有缓存每生成一个新token模型都得把历史所有token重新算一遍K和V那计算量会随着对话长度线性飙升别说丝滑了多轮对话能做到不卡死都是奇迹。2. 自回归的笨办法没有中间缓存每个字都得把历史重算一遍先做一个思想实验假设我们完全不做KV缓存模型每一步生成时把截至目前的所有历史token重新喂进模型从头跑一遍完整前向。这样能不能工作能但代价极其离谱。每一步解码都要重新计算之前所有token在各层的K和V而这些K和V在上一步刚算过内容一模一样。等于你每写一个字都要把已经写过的每一个字重新读一遍、重新理解一遍。第一轮对话可能还好到了第十轮模型每次吐一个字都要过一遍上万token的历史GPU再强也扛不住。复杂度上说无缓存解码的总计算量大致是O(T²)乘以模型参数量级的常数T是生成的总token数。而带了KV缓存的方案prefill仍然要算一遍O(T_prompt²)级别的前向计算但decode阶段每步只新增一个token单步计算量基本恒定。这就是缓存带来数量级提升的原因。2.1 注意力公式里那三个字母决定了什么能被复用要理解KV缓存最好把注意力公式展开看一眼。标准点积注意力的计算是Attention softmax(Q × K^T / sqrt(d)) × V。这里Q是当前查询向量K是所有候选位置的键向量V是这些候选位置的值向量d是向量维度。Q、K、V这三个字母的语义差异直接决定了缓存策略。Q代表“我现在想找什么”它只跟当前正在生成的那个token有关。K代表“我这里有某某信息可供查阅”是历史token自身属性的编码。V代表“我实际提供的内容是什么”同样是历史token的固有信息。当一个新token产生时它需要拿自己的Q去和历史全部K做点积。注意历史token的K不需要随新token变化V也一样。所以它们可以被算一次、存起来、反复用。Q则不一样每个新token的Q都是新的旧token的Q不会再有其他token拿它来当查询因此完全没有缓存的必要。还有一个很多人问过的问题为什么不缓存attention矩阵也就是softmax之后的权重因为权重是Q和K交互的结果新token一来Q变了所有位置的权重都要重新算旧的权重对任何后续token都无效。缓存权重既占空间又没用而缓存K和V才是既能复用、又体积可控的做法。2.2 无缓存推理的复杂度灾难一次O(n²)的表演把无缓存的代价说得更具体一点。假设对话累积到了1万token模型有32层每层都要为这1万个token重新计算K和V每个token的K/V维度是几百到上千。这一步涉及的总计算量大约等于prefill阶段为同样长文本付出的计算量而且每生成一个token都要重复一次。换句话说如果没有KV缓存一个1000 token的回复等于要把同样长度的前向计算做1000遍。哪怕GPU算得再快这种重复劳动也会把单token延迟推高到不可容忍的程度。实际部署时所需的显存带宽和算力都会膨胀到硬件完全无法支持。所以KV缓存不是一种可有可无的优化而是自回归生成能在今天落地的先决条件之一。工程上所有推理框架的第一优先级基本都围绕着同一件事把KV缓存管好。显存分配、批处理策略、长上下文支持本质上都是在给KV缓存腾地方、省空间、提高命中率。理解了这一点你再看各种推理框架的文档会发现每条配置都变得清晰了。3. KV缓存的一生从一次前向传播到长对话里的隐形成本接下来我们跟着KV缓存走完它的一生。从它诞生的那一刻到它被反复读取再到它挤爆显存每个阶段都对应着用户可以感知到的性能特征。假设现在有一条用户消息进来长度是n个token。prefill阶段会做这样几件事把n个token的嵌入向量喂进第一层算出这一层每个位置的Q、K、V把K和V存入该层的KV缓存Q用完即弃接着进入下一层重复同样操作直到所有层都完成。最后从最后一层输出里算下一个token的概率分布。注意prefill阶段就已经把整段prompt的KV全部写进了缓存。所以等到decode第一个token时模型并不需要重新处理用户输入的每个字只需要处理当前这个新token然后去缓存里查历史K/V。这就是为什么首token虽然等了半天后续每个字却出得很快。3.1 Prefill阶段把整个prompt写成一张“检查表”prefill阶段可以被理解成“做检查表”的过程。模型读完整段输入为每一个位置提炼出它的话题索引K和具体内容V整整齐齐排好。这些索引和内容接下来会被后续所有新生成的token查阅。这个过程本身很贵因为它涉及两层复杂度。一层是模型宽度带来的每一层都要对n个token做矩阵乘法计算量和模型参数量成正比。另一层是注意力机制带来的每个token都要和其他所有token做注意力交互计算量和n²成正比。prompt越长prefill阶段的耗时增长越接近超线性。所以“第一个token慢是因为算力太重”这个说法从现象上不算全错。prefill确实算力需求巨大但它不是因为模型在“想答案”而是因为模型必须先为整段输入建立完整的KV缓存。从产品体验上讲这段等待时间本质上是“建索引”的时间。还有一点容易被忽略prefill阶段虽然计算量大但它是高度并行的矩阵运算GPU利用率通常很高。真正决定首token延迟的往往不是模型token数多少而是prompt里有多少token。短问题秒回长文档慢这个规律基本就是prefill复杂度的直接体现。3.2 Decode阶段每吐一个字缓存就厚一分进入decode阶段后模型每一步只生成一个新token。这个token会被拼接进序列计算它自己的Q、K、V然后通过注意力机制读取历史所有位置的K和V得到输出向量最后预测下一个token。每完成一步新token的K/V也会被追加进缓存。所以KV缓存是动态增长的。对话从第10个token到第1000个token缓存体积涨了100倍。也正是因为这种增长长对话后期即使输出速度没变慢显存压力也会越来越大。如果你的服务端没有做好缓存管理聊着聊着就可能OOM表现为突然断掉或报错。decode阶段的单步计算量不大真正的瓶颈通常在于读取缓存的显存带宽。想象一下每生成一个新token模型都要把当前已累积的所有历史KV从头到尾读一遍。上下文长度越长这一步需要搬运的数据就越多。很多人以为对话变长后模型回答变慢是因为“模型累了”或者“找不到思路”其实更常见的原因是KV缓存膨胀导致带宽吃紧。3.3 为什么“第一个token慢”的锅不能全甩给算力回到标题里的那个误解。大多数人以为第一个token慢是因为模型算力太重尤其当模型宣称自己有几千亿参数时这句解释听上去特别合理。但实际做推理服务的人会告诉你算力“重”和“慢”之间需要重新算一笔账。首token慢的直接原因是prefill必须处理完整段输入而这段输入对应的计算量和显存访问量都在同一时刻爆发。模型参数只是被读取和参与计算真正让首token延迟随输入线性增长的是那n个token之间两两交互的注意力矩阵以及为每个token生成并缓存K/V的代价。模型参数大小影响的是单次前向的基座成本而prompt长度影响的是KV缓存建立的边际成本。换句话说算力太重解释的是“为什么哪怕输入很短也会有一个基础延迟”KV缓存解释的则是“为什么输入越长首token越慢而后续输出却能相对丝滑”。两者一个管底线一个管斜率缺了KV缓存的概念你对首token延迟的理解就是不完整的。4. 一笔显存账单从7B到70BKV缓存是上下文长度的隐形价格知道KV缓存存在是一回事知道它有多占显存是另一回事。很多部署大模型的人第一次跑服务时都会遇到一个困惑模型权重明明只占十几GB怎么一跑推理显存就见底了答案往往是你忘了给KV缓存留预算。KV缓存的体积可以精确估算公式是KV缓存大小 2 × 层数 × KV头数 × 每头维度 × 序列长度 × 批大小 × 精度字节数。公式里的2来源于K和V各一份。这个公式拿任何模型的配置都能套误差很小。下面我给出一个简单的Python脚本输入模型配置就能估算出单条序列的KV缓存占用def kv_cache_bytes(layers, kv_heads, head_dim, seq_len, batch_size, dtype_bytes2): # dtype_bytes: fp16为2bf16为2int8为1 return 2 * layers * kv_heads * head_dim * seq_len * batch_size * dtype_bytes # 示例1某7B级模型32层8个KV头head_dim128fp16精度 print(kv_cache_bytes(32, 8, 128, 4096, 1, 2) / 1024**3, GB per sequence) print(kv_cache_bytes(32, 8, 128, 4096, 16, 2) / 1024**3, GB for batch16) # 示例2某70B级模型80层8个KV头head_dim128fp16精度 print(kv_cache_bytes(80, 8, 128, 4096, 1, 2) / 1024**3, GB per sequence) print(kv_cache_bytes(80, 8, 128, 128000, 1, 2) / 1024**3, GB at 128K context)我们按常见的GQA配置估算会发现一个惊人的事实一篇4000 token的短文档7B级模型单条序列的KV缓存大约是128MB这还不算多。但如果同时服务16个用户这批KV缓存就涨到2GB开始有点疼了。到了70B级模型同样4000 token的上下文单条序列的KV缓存轻松超过1GB而如果上下文的长度拉到128K单条序列的KV缓存可以逼近几十GB。也就是说长上下文能力本质上是用显存换来的。4.1 KV缓存体积的计算公式上一节的公式其实已经说明了一切。为了让大家有更直观的感受我把几个典型配置塞进一张表里按fp16精度估算单位是GB。模型规模层数KV头数head_dim上下文长度单序列KV缓存16批KV缓存7B级3281284K0.12527B级32812832K11613B级4081284K0.1562.570B级8081284K0.5870B级808128128K16256注意这张表用的是GQA配置下常见的8个KV头。如果是MHA每个注意力头都有独立的K/VKV头数可能冲到32甚至更多表里的数字还要翻几倍。这也就是为什么如今新发布的大模型几乎都在往GQA方向走KV缓存实在太贵了。4.2 不同模型规模与序列长度下的账单明细部署时只看权重显存必然翻车。一张消费级显卡通常也就16GB到24GB显存一个7B模型权重在fp16下就要占用14GB左右看起来勉强够跑。可真要跑起来你还要预留激活值、计算缓冲区以及随对话长度不断膨胀的KV缓存。上下文稍微开长一点或者并发用户一多立刻爆显存。对比一个更极端的场景70B模型配合128K上下文单条序列的KV缓存就是16GB起步。也就是说光是一个用户的长文档对话KV缓存就能吃满一张专业显卡的全部显存模型权重反而要放在另一张卡上。这时候你会发现所谓“能处理百万token上下文”的模型显存成本高得离谱硬件不堆上去长上下文只能看看参数文档。这也是为什么量化模型、量化KV缓存在部署中越来越常见。模型权重量化是把“模型参数”这个大头变小而KV量化则是把“中间缓存”这个动态大头变小。两者解决的问题不同但最终都是为了让同样的显存能塞进更多并发和更长上下文。4.3 部署中最容易翻车的三个显存场景我见过不少项目在部署推理服务时翻车基本都逃不出三个场景。第一个是只按模型权重规划显存没给KV缓存留预算上线一测并发就OOM。第二个是盲目拉长上下文窗口把max_length从4K调到32K却忘了KV缓存会线性膨胀结果显卡直接爆掉。第三个是没注意批处理大小和KV缓存之间的联动批大小一增加KV缓存成倍增长在长上下文的场景下尤其致命。这类问题的最佳解法是在上线前就按公式把KV缓存预算算清楚。把“模型权重 激活值 KV缓存峰值 推理框架自身开销”列成一个显存规划表再根据峰值并发反推单卡能承受的上下文长度。宁可把max_length设得保守一点也不要让服务在用户聊到一半时突然断掉那种体验基本等于功能事故。5. 工业界的“KV瘦身运动”GQA、量化、分页与窗口KV缓存既然这么贵工业界自然想尽办法给它瘦身。这些年围绕KV缓存诞生了一系列优化方案从模型结构上的改动到推理框架的显存管理再到量化压缩和稀疏化各有适用场景。把它们放在一起看其实就是一场针对显存和带宽的持久战。最根本的思路是从模型结构下手直接减少KV的体积。MQA让所有注意力头共享同一份K/V体积直接缩到接近1/头数但会牺牲一定的表达力。GQA是MQA的折中把注意力头分成若干组组内共享K/V。它比MQA保留更多信息又比MHA省下大量显存所以成了近年大模型的主流配置。你看到某个模型配置里写“GQA”基本可以确认它是为了控制KV缓存成本而设计的。5.1 GQA/MQA让多个注意力头共享同一份K/V为什么共享K/V可行因为注意力头虽然各有所长但它们处理的都是同一个序列里的信息。不同头的主要差异体现在“用什么样的Q去查询”而K/V作为被查询的对象存在大量冗余。让几个Q头共享同一份K/V会损失一些“查询视角”的丰富度但换来的是KV缓存大幅缩小工程收益远大于精度损失。从部署角度看GQA带来的好处非常直接相同显存下可以开更大的批处理或者支持更长的上下文。尤其是长上下文场景KV头和层数是决定缓存增长率的核心变量KV头数减半缓存直接减半。所以新模型宁可加深层数也要压KV头数本质就是在做这个权衡。还有一种更新颖的压缩思路是把K/V做低秩映射先投影到一个很小的潜在空间再缓存。这样KV缓存体积可以压得更狠但实现复杂度更高也属于“KV瘦身运动”里比较前沿的方向。理解GQA之后看这类方案会容易很多它们都在想方设法让“每一个历史token在缓存里占的空间”更小。5.2 KV量化把缓存从fp16压到int8甚至更低除了动结构还可以动精度。KV缓存默认用fp16或bf16存储如果把它量化成int8显存占用直接减半极端情况下量化到4bit缓存体积能压到原来的四分之一。代价是量化误差会影响注意力计算的精度极端情况下会出现回答质量轻微下降。我个人的实践感受是KV量化在中短上下文场景下影响不明显但在很长上下文的场景里要谨慎。上下文越长需要累积的量化误差越多注意力分数的细微偏差可能会在长距离依赖上被放大。如果你要处理128K上下文这种极限场景建议先做一轮基准测试对比量化前后模型在长文档问答上的准确率再决定是否开启KV量化。另外要注意KV量化的收益在并发场景会被放大。单条序列省下几十MB不算什么但几百条并发时省下的就是几GB甚至几十GB显存。量化后的KV缓存还能降低带宽压力让decode阶段的每token延迟更稳定这在长上下文服务中往往比显存收益更明显。5.3 分页式缓存与连续批处理把显存利用率抠出来模型结构和精度之外的另一个方向是缓存管理本身。传统推理框架会给每条请求预分配一块连续的KV缓存空间但请求长度是不可预测的预分配多了浪费显存预分配少了又要迁移。这个问题在并发场景下尤其明显显存碎片化能白白浪费掉两到三成空间。近年来流行起来的思路是给KV缓存做分页管理把缓存切成固定大小的块按需分配逻辑上连续、物理上可以不连续。这就像操作系统里的虚拟内存分页按页管理碎片率大幅下降显存利用率显著提升。配合连续批处理新请求可以在旧请求即将结束时立刻插入空档吞吐量又能上一个台阶。连续批处理本身不减少单个请求的KV缓存但它会动态调整不同请求占用的缓存空间避免因为一个慢请求卡住整批请求。把这两项技术一起用等于在有限的显存里“抠”出了更多并发槽位。选推理框架的时候可以重点看它有没有类似的KV管理机制这往往决定了高并发场景下的实际吞吐差距。5.4 最激进的一类对历史KV做稀疏化或丢弃最激进的KV瘦身思路是有些历史信息也许根本不值得缓存。滑动窗口注意力只保留最近若干token的KV更早的上下文直接忽略非常适合流式对话。有些方案则通过启发式规则先给历史KV打分认为当前最相关的部分保留不相关的丢弃。这种稀疏化能大幅压缩缓存但存在信息丢失风险属于拿质量换性能的典型做法。在实际项目里我建议把优化手段分层使用。如果只是想让服务跑起来先上GQA模型和分页式缓存如果显存仍然紧张再考虑KV量化和连续批处理只有当你真的需要超长上下文且能接受效果波动时才去碰稀疏化和丢弃策略。从工程稳妥性的角度讲越晚尝试越安全先把最容易预判收益的手段用足。6. 部署实战用三招判断你的服务是卡在算力还是卡在KV缓存原理讲完最后分享一点实战经验。很多人在部署推理服务时遇到“生成慢”就想升级显卡但慢的原因未必是算力不够很可能是KV缓存把显存带宽吃满了或者显存容量不够导致批处理上不去。判断清楚瓶颈在哪才能把钱花在刀刃上。我自己常用的方法有三招不需要太复杂的工具靠着日志和系统监控就能给出比较可信的判断。核心思路是把“算力瓶颈”和“缓存瓶颈”分开来看因为它们表现出的特征完全不同。6.1 第一招观察首token耗时对输入长度的敏感度先说最简单的观察方法固定batch大小为1把prompt长度从几百token拉到几千token记录首token延迟的变化。如果首token延迟随prompt长度明显上升说明你正处在prefill计算密集阶段模型算力是当前的主要约束如果首token延迟基本平稳后续每token延迟却很高那更像显存带宽或缓存管理出了问题。这个方法不需要做复杂profiling跑一组短、中、长的请求就能看出趋势。有一次我排查一个“首token要等三秒”的问题测下来发现prompt只有几十token时首token也要两秒多明显不是prefill算力的问题。查了一圈发现是推理框架在每次请求时都会重新分配KV缓存块分配逻辑有性能缺陷。换掉分配策略之后首token延迟立刻降了下来。所以别急着归因到GPU算力。先做长度扫描确认延迟是否随输入长度线性增长。如果不增长问题大概率出在框架调度、缓存分配或者其他固定开销上换显卡是解决不了的。6.2 第二招盯显存增量看它跟并发与上下文的关系第二招是打开显存监控观察KV缓存增长模式。在推理过程中用监控工具连续采样显存占用会发现当并发数上升或上下文变长时显存占用会跟着涨这部分增量基本就是KV缓存。对比模型权重占用的固定值KV缓存是动态项它的增速直接反映了缓存管理效率。如果显存占用已经接近上限同时显存带宽利用率很高但GPU计算单元利用率却不高那基本可以判定瓶颈在KV缓存的读取和存储上。这种情况下单纯堆算力没用你更应该做的是开KV量化、缩小上下文长度、减少并发或者换一个缓存管理更好的推理框架。我自己习惯把这些数据画成曲线横轴是时间纵轴是显存占用和GPU利用率。当GPU利用率掉下来而显存曲线还在涨时大概率就是缓存已经把显存吃满计算单元在等数据搬运典型的带宽受限特征。6.3 第三招把一个批次塞满再看两个阶段的耗时分布第三招适用于判断batch大小和KV缓存的联动关系。固定同样的输入长度逐步加大batch记录prefill阶段和decode阶段的平均耗时。如果batch加大后prefill耗时的增速明显高于decode耗时说明算力在预填充阶段是瓶颈如果decode耗时的增速更猛说明decode阶段读取KV的带宽不够了。这个测试能帮你定出一条“最大安全并发线”。我在实际项目里通常把batch逐步翻倍每一档都观察两个阶段的耗时比。当decode阶段单token耗时开始明显爬升时就停止增加batch把它作为容量上限。再往后硬加并发用户体感会断崖式变差表现为每个字都要卡顿半天。这种测试也能顺便验证你做的KV优化是否有效。比如开启KV量化后如果decode耗时曲线的斜率明显缓了说明优化确实降低了带宽压力如果没什么变化那就要重新考虑问题是不是出在别处。6.4 一次真实排障的复盘从“换显卡”到“换缓存策略”最后复盘一个我印象很深的项目。当时我们跑一个对话服务模型不大但用户聊到中后段时输出速度肉眼可见地下降。最初大家都怀疑显卡算力不够计划升级到更大的GPU。我先把首token耗时做了长度扫描发现prompt很短时首token很快根本没到算力极限。然后看了显存曲线发现随着对话轮数增加显存占用稳步爬升同时GPU利用率在下滑。真正的问题在KV缓存预留策略上每条会话都提前分配了能支持最大上下文长度的连续显存块但绝大多数会话根本聊不到那么长于是显存被大量闲置预留占据实际能并发的会话数远低于预期。并发一高每条会话能分到的带宽和算力都被稀释用户感知到的就是越聊越慢。后来我们把连续预留改成按需分块分配同时给KV缓存开了量化单条会话的显存占用降了一半多并发能力直接翻倍。整个过程没有动模型、没有换显卡只改了KV缓存的管理方式问题就解决了。这件事之后我在看任何推理性能问题时都会先问一句KV缓存是怎么管理的十次里有七次答案都藏在里面。