拆解 KV Cache:从 Prefill 到 Decode,看懂大模型推理加速的完整逻辑 不少人第一次听说 KV Cache都简单理解成推理过程中做了缓存所以运行速度变快。这个说法不算完全错但讲得太表面了。实际上 KV Cache 牵扯到整套大模型推理引擎的设计逻辑包括 Prefill 和 Decode 两个阶段如何拆分、显存资源怎么分配管理、多个用户请求怎么调度、固定文本前缀如何重复利用还有结构化输出场景里怎么控制模型停止生成、怎么做采样处理。接下来就顺着一次完整的请求流程把这些内容从头到尾梳理清楚。先弄明白一个核心问题大模型推理到底慢在哪里平时我们给大模型发消息等待回复的这段时间里后台一直在做大量运算。模型会先把你输入的所有内容转成token逐个送入Transformer网络层逐层计算注意力权重和前馈网络结果这个过程就是 Prefill。Prefill 阶段有个明显特点所有输入token可以同步并行计算。除去注意力机制里的因果掩码限制token之间基本不存在先后依赖所以这个阶段GPU算力能充分跑满整体处理吞吐量也很高。等输入内容全部处理完毕模型就开始生成回复内容这一步就是 Decode。模型每产出一个新token都要回头读取过往所有内容包括最初的输入和已经生成的回复内容。它会用当前token对应的Query和历史所有Key、Value完成注意力计算。而且这个阶段没法像 Prefill 那样一次性批量生成多个token因为后一个token的生成必须依赖前一个token的计算结果。Decode 阶段的痛点很突出每一轮只生成单个token却要不断读取越来越长的历史KV数据反复做注意力运算。随着文本序列不断变长每一轮的显存读写压力都会持续增加这也是大模型推理速度上不去的主要原因之一。如果没有 KV CacheDecode 环节会一遍又一遍重复计算历史token的K、V向量有了 KV Cache 之后历史部分的K、V就不用重新计算了。但即便如此每一轮依旧要读取缓存里的历史KV数据做注意力运算所以生成长文本时显存带宽依旧会面临很大压力。KV Cache 到底缓存的是什么Transformer 架构的注意力机制核心就是 Query、Key、Value 三组向量之间的运算。简单理解Q代表当前token要去上下文里查找相关信息的查询指令K代表每一个token对外提供信息的索引标识V则是每个token真正承载的内容数据。注意力的完整计算流程就是用当前的Q和所有K做乘积运算算出一组权重数值再依靠这组权重对全部V做加权求和最终得到融合了上下文信息的当前token表征。这里有个关键点进入 Decode 阶段后每生成一个新token过往所有token对应的K、V向量都不会再发生变化。因为K和V只由当前token本身以及它之前的上下文决定和后续新增的token没有任何关联。基于这个特性我们可以在 Prefill 阶段把所有输入token经过每一层网络后产出的K、V向量全部保存下来。后续 Decode 过程中只需要计算新token的Q、K、V再把新生成的K、V追加到已有缓存中最后用新token的Q和整套缓存数据完成注意力计算就行这就是 KV Cache 的工作逻辑。它缓存的并不是模型参数也不是原始输入文本而是每一个token经过每一层Transformer网络后生成的Key和Value向量。举一组实际数据方便大家理解假设模型一共96层隐藏层维度为12288采用bf16格式存储每个浮点数据占用2字节。单个token在单层网络中对应的KV缓存占用空间为 2K和V两组数据× 12288 × 2 字节大约是48 KB。叠加96层之后一个token对应的KV缓存整体大小约4.5 MB。如果模型上下文窗口支持128K token单单一条请求产生的KV缓存占用显存就会达到约576 GB。以上计算基于标准多头注意力机制现在主流模型大多采用GQA结构能把显存占用降低4到8倍。这个数据足以看出显存压力有多大。目前常见的A100、H100单卡显存基本在80 GB左右H200单卡显存能达到141 GB。而模型自身参数就会占据大部分显存留给KV Cache的空间本就有限。所以说KV Cache的管理本质就是显存资源的调度与优化问题。Prefill 和 Decode 的资源冲突Prefill 和 Decode 两个阶段对硬件资源的需求完全相反。Prefill 属于计算密集型任务大批量token同步送入模型运算全程吃满GPU算力这个阶段的瓶颈主要是计算速度。而且每个token的K、V数据只需要计算一次即可。Decode 则是显存带宽密集型任务每一轮仅计算单个token核心开销集中在读取整份KV缓存、执行注意力运算上。这个阶段算力完全够用真正拖慢速度的是数据从显存传输到计算单元的速度。两种负载放在同一块GPU上运行就会出现两难的情况。如果同时承接 Prefill 和 Decode 请求要么正在等待处理的Decode请求不断排队要么处理Decode时GPU大量算力处于空闲状态。针对这个问题现在主流的推理引擎主要有两大优化方向。第一种是 Continuous Batching也就是连续批处理。不再等待整批请求全部处理完毕再开启下一批而是按照单次迭代的节奏动态调度任务。某一条请求处理完成就立刻接入新请求最大限度让GPU保持满载运行。第二种是 Prefill-Decode Disaggregation即分离式推理。思路更进一步把 Prefill 和 Decode 两类任务拆分到不同的硬件资源上让各自发挥硬件优势。简单总结就是同一批请求里一部分执行Prefill一部分执行Decode推理引擎实时调度避免资源闲置。目前连续批处理已经成为各类主流推理框架的标配功能Prefill与Decode分离的部署模式也在vLLM、TensorRT-LLM等工具中逐步落地不过这项能力并不会在所有部署场景中默认开启。PagedAttention用内存分页的思路管理 KV Cache前面提到KV Cache落地最大的难题就是显存不足。传统的处理方式是提前为每一条请求划分一块连续显存空间空间大小按照模型支持的最大上下文长度来设定。举个例子如果模型最大支持4096个token就直接预先分配对应大小的连续显存不管这条请求实际会用到多少内容。这种方式和早期操作系统的内存管理逻辑一模一样给每个程序划定固定大小的连续内存区域哪怕用不上这块空间也会被一直占用。但实际场景里绝大多数请求都用不满最大长度。比如一次简单问答可能只用到500个token可系统依旧会分配4096大小的显存剩下的空间全部闲置其他请求也无法调用显存资源浪费十分严重。vLLM推出的 PagedAttention借鉴了操作系统虚拟内存分页的思路完美解决了这个问题。核心做法是把存放KV Cache的整块显存切分成一个个固定大小的Page比如单个Page用来存储16个token对应的KV数据。每一条请求都会配套一张页表记录自身的KV数据分别存放在哪些Page当中。这些Page不需要物理地址连续系统根据实际使用量按需分配空间。这套方案带来了不少优势首先彻底解决了显存碎片问题零散的空闲Page都能被充分利用其次显存利用率从原本的20%-30%提升至90%以上设备可以同时承载更多并发请求最后也为后续灵活调度打下基础KV数据不再绑定整块连续显存推理引擎可以更轻松地对数据块进行分配、释放、共享和数据换入换出操作。如果需要实现跨显卡数据迁移、显存数据转存至内存等功能还需要搭配额外的系统机制。根据vLLM公开的测试数据在硬件条件完全相同的前提下对比传统方案使用PagedAttention后整体吞吐量可以提升2到4倍。性能提升的核心原因就是减少了显存浪费支撑了更多并发请求。计算机领域很多优秀的优化思路都是跨领域借鉴而来虚拟内存分页、数据库缓冲池、垃圾回收分代机制底层逻辑都相通把稀缺资源拆分成小块按需分配从根源减少资源浪费。Prompt Cache重复利用固定前缀内容搞懂KV Cache的存储和管理逻辑后再来看一个落地中很实用的优化点。很多智能代理系统的请求都有一个共性系统提示词是固定不变的。这类提示词通常包含角色设定、工具调用规则、行为约束等内容整体长度大概在8000到10000个token。如果每一次请求都重新计算这部分固定内容对应的KV数据会产生大量无效运算。Prompt Cache 也叫前缀缓存思路很直白既然固定的系统提示词不会改变它运算得出的KV缓存也保持不变直接把这份缓存保存下来后续同类请求直接复用即可。不同平台的具体实现方式略有区别。Anthropic 的前缀缓存需要手动标记在API请求中通过cache_control参数标注出固定内容服务端就会单独保存这部分的KV缓存。后续请求如果前缀内容匹配成功就能直接命中缓存跳过重复的Prefill计算流程。DeepSeek和Gemini也搭载了类似的前缀缓存能力。OpenAI则采用自动缓存机制当请求中超过1024个token的固定文本前缀系统会自动完成缓存不需要手动标记。部分模型还支持通过prompt_cache_retention参数延长缓存的保留时长。这里要分清一个容易混淆的概念Prompt Cache 和 KV Cache 并不是同一种东西。KV Cache是推理过程自动生成的每一条请求都会产生它存储的是单条请求全量历史token对应的KV向量请求结束后这份缓存生命周期也就随之终止。而Prompt Cache支持跨多条请求重复使用它会把不同请求里重复出现的固定前缀对应的计算结果保存下来让后续请求不用再重复做这部分Prefill运算。缓存具体存在哪里、能保留多久由各服务平台自行设计也不能简单理解为永久保存。打个形象的比方KV Cache就像是做题时临时打的草稿一道题做完草稿就丢掉Prompt Cache相当于整理好的公式手册常用内容提前整理完毕每次做题直接查阅不用重新推导。对于智能代理系统来说Prompt Cache带来的提速效果十分明显。拿实际项目举例系统提示词大概8000到9000个token再加上工具描述、示例内容整套固定前缀接近10000个token。如果每次都完整执行Prefill流程单这一步就要耗费300至500毫秒。启用前缀缓存并命中后这段耗时可以直接省去。放到多轮对话场景中累积节省的时间会非常可观。EOS Token模型如何判断停止生成聊完各类缓存技术再说说一个和推理紧密相关、却常常被忽略的细节。大模型生成文本时怎么判断内容已经写完可以停止输出答案就是EOS Token也就是序列结束标记。每一个模型都会预设专属的结束标记常见形式比如 |endoftext| 或者 |eot_id|。当模型生成出这个特殊token就代表本次内容生成到此结束。普通对话场景下模型正常输出EOS标记流程就可以顺利收尾。但放到智能代理系统中情况会变得复杂。比如模型需要输出工具调用指令输出格式一般为工具名称加标准JSON参数。如果JSON内容还没有完整生成模型就提前产出了EOS标记最终得到的就是残缺的JSON数据程序解析时会直接报错。为了解决这个问题在执行工具调用、输出结构化内容的过程中业内普遍会用到EOS Suppression也就是抑制结束标记生成。具体实现主要作用在logits数据层面。模型每一步都会输出一组词表分数向量向量里每一个数值对应词库中单个token的原始得分。在执行采样筛选token之前推理引擎或者解码器会把EOS token对应的分数设置为负无穷或是一个极小的负数。经过softmax运算后EOS被选中的概率就会无限趋近于零以此强制模型继续生成内容。等到模型完整输出符合要求的工具调用JSON结构后再解除对EOS标记的限制也可以依靠外部设定的终止条件直接结束生成流程。这套处理逻辑和接下来要讲的约束解码本质思路一致只是应用场景不同。约束解码从分数层面严格把控输出格式智能代理系统运行过程中最让人头疼的问题之一就是模型输出的工具调用JSON格式出错。可能是缺少括号、参数名称拼写错误也可能是多出多余标点。传统的解决办法无非是在提示词里反复强调格式要求、增加示例内容再搭配后置的JSON解析做异常兜底。这些手段能起到一定作用但没法彻底解决问题。究其根本大模型本身是基于概率做采样选择的工具只要某类错误格式的token概率不为零就总有出现异常的可能。约束解码换了一套解决思路不再依靠提示词引导模型而是直接在logits分数层面做干预。核心逻辑是既然我们明确知道模型接下来需要输出的格式比如严格匹配指定JSON结构那就可以在每一轮采样时只保留符合规则的token作为候选。具体落地会借助有限状态机FSM或者上下文无关文法CFG把合法的输出格式描述出来。模型每输出一个token有限状态机就同步切换到对应状态。系统再根据当前状态筛选出下一步允许出现的所有token。最后在logits数据中把规则以外的token分数全部置为负无穷彻底屏蔽这类选项。这样一来模型只能从合法token里做选择最终输出内容必然符合预设格式。举个简单例子假设当前需要输出JSON里的字符串键名name按照规则当下仅允许出现引号和英文字母。如果模型尝试输出大括号或者数字都会被直接屏蔽没有选中的可能。目前业内有不少成熟工具实现了这套能力Outlines是其中知名度最高的开源库它可以把JSON格式规则转换成正则表达式对应的有限状态机在解码过程中动态调整token分数。llama.cpp也内置了基于语法规则的约束解码功能。vLLM则支持通过guided decoding参数传入JSON格式规则自动启用约束解码能力。这项技术对智能代理系统价值巨大。工具调用是代理系统的高频操作必须依赖标准JSON格式。哪怕出错概率只有千分之一在日均数千次调用的规模下每天都会出现多次执行失败。启用约束解码后格式错误的问题基本可以杜绝。完整梳理一次大模型推理的全流程把上面所有技术点串联起来就能清晰看到单次大模型推理的完整流程分词处理将用户输入的文本转换成对应的token编号序列。Prefill计算全部token送入Transformer网络完成前向运算逐层生成KV缓存。如果匹配到已保存的前缀缓存固定内容部分直接复用已有KV数据仅对新增内容执行Prefill运算。循环解码进入Decode循环流程把上一轮生成的token和对应的KV缓存送入模型模型输出logits分数向量。如果开启约束解码就根据有限状态机的当前状态屏蔽所有非法token。之后通过采样选出本轮要输出的token编号再还原成可读文本。接着判断当前token是否为EOS标记如果是则终止生成反之继续循环解码。后置处理对完整的输出文本做解析提取工具调用指令或者常规回复内容最终返回给调用方。在整个推理流程里KV Cache的管理贯穿全程。PagedAttention负责提升显存使用效率Prompt Cache负责复用固定前缀减少重复运算Continuous Batching负责多请求的合理调度。采样环节依靠约束解码保证输出格式合规EOS Suppression则避免结构化内容生成时提前终止。这些技术并不是相互独立的而是一套环环相扣的完整链路每一项技术都针对性解决了运行过程中的具体问题。吃透整条推理链路再去搭建智能代理架构、做推理性能优化或是排查线上运行故障就不会只停留在简单调用接口的层面。写在最后单纯调用接口和弄懂接口背后的运行原理完全是两回事。就像日常开车出行不代表我们精通汽车发动机的工作原理但两种认知都不可或缺。理解底层逻辑之后你才能真正明白Prompt Cache为什么能提速、约束解码为什么比单纯加示例更靠谱也能判断出在智能代理场景中非流式请求往往是更合适的选择。这些技术概念不是空洞的知识点当你把推理链路的每一个环节都理解透彻在设计智能代理架构、制定技术方案时才能做出更合理、更落地的决策。