KV Cache详解:从原理到显存优化,大模型推理加速的基石

KV Cache详解:从原理到显存优化,大模型推理加速的基石 本地跑过大模型的人应该都有体验输入问题后第一个 token 总要等上一会儿可一旦第一个字出来后面就像开了闸。对话拉长之后速度反而变慢显存占用却一路走高。这些现象都能用 KV Cache 来解释。KV Cache 不是什么神秘黑科技也不是某一个推理框架独有的设计。它是 Transformer 架构在自回归生成时几乎绕不开的一块工作缓存也是服务端推理引擎、本地部署工具和面试题里的常客。只要聊到大模型为什么这么吃显存、长对话为什么越跑越慢、vLLM 这类引擎为什么能提升吞吐最后都会落到 KV Cache 上。想把它讲清楚只需要回答四个问题它缓存了什么为什么必须要缓存它把显存吃到了哪里它到底改变了什么1. 先搞清楚 KV Cache 解决的是哪一类“重复劳动”1.1 一次生成请求里最花钱的动作是什么大模型生成文本本质上是一个 token 一个 token 地预测。所谓自回归就是每次生成时新的 token 依赖于之前已经生成的所有 token。具体到 Transformer 的注意力层当前 token 会生成一个 Query 向量然后和上下文里所有 token 的 Key 向量做匹配得到注意力权重再用权重对 Value 向量做加权求和。也就是说要算出当前 token 的输出不能只盯着当前 token 看而是要和整段上下文里每一个 token 做一次注意力交互。这里的 Key 和 Value正是从历史 token 的隐层状态变换而来的。它们承担了“历史信息摘要”的角色。问题就出在这里在生成第 t 个 token 时第 1 到第 t-1 个 token 的 K/V其实在它们各自被生成的时候已经算过一遍了。如果没有缓存机制那第 t 步就要把整段历史重新 forward 一遍重新算出所有历史 token 的 K/V再做 attention。这就像一个文档越写越长但每加一句话你都要把整份文档从头重打一遍。长度短时还好上下文一旦拉到几千 token这种重复劳动的代价会成倍放大。1.2 不用缓存会怎样每走一步都把历史重算一遍如果不使用 KV Cache每一步解码都要对当前完整上下文重新执行自注意力计算。随着生成不断推进每一步要处理的序列越来越长而历史 token 的 K/V 反复被重新计算。很多资料会用复杂度来解释这件事KV Cache 能让生成阶段的总计算量从 O(n³) 量级降到 O(n²) 量级。这个说法在面试里很常见但我觉得比硬记公式更重要的是理解“为什么”。核心原因是它把前缀序列的 K/V 计算固定为一次后续每一步只需要追加一个新的 K/V注意力读取的是缓存而不是把历史重新 forward 一遍。换句话说KV Cache 不是在“增强模型能力”而是在“去除重复劳动”。这里有一个很容易被忽略的细节启用 KV Cache 之后每一步的注意力计算仍然要读取完整的历史缓存。所以序列越长单个 token 的生成延迟还是会上升。它并没有把注意力变成 O(1)只是把 K/V 的生成从反复执行变成了一次性固化。真正减少的是重复计算不是注意力本身。1.3 主判断KV Cache 本质是“用显存换算力”我理解 KV Cache 最重要的切入点是这是一个“用显存换算力”的工程取舍。它的价值不是让模型变聪明而是把重复计算变成存储和读取让生成更快、吞吐更高、长上下文在工程上成为可能。但副作用也很直接显存开始被缓存占用而且随并发数和序列长度线性增长注意力读取带来的带宽压力也会随上下文长度变大。理解这一点之后再看各种优化手段就非常清晰了GQA/MQA 是为了减少要缓存的数据量PagedAttention 是为了减少显存碎片Prefix Caching 是为了复用多个请求中的相同前缀KV Cache 量化是为了压缩缓存体积。它们本质上都是在为同一个“成本转移”买单。2. Prefill 和 Decode一条请求里的两个阶段KV Cache 的完整链路2.1 Prefill 阶段先把 prompt 变成“初始记忆”大模型拿到输入后并不是立刻就开始逐字输出。你输入一段 prompt 之后实际发生的第一个阶段是 Prefill也叫预填充。在这个阶段模型会把 prompt 中所有 token 一次性并行地过一遍 Transformer 层计算出每个位置的 Key 和 Value并把它们写入 KV Cache。这个阶段结束之后模型才算是“读完了”你的输入。Prefill 阶段的关键特征是它是并行的不是逐 token 的。但也正因如此prompt 越长prefill 需要的计算量越大首 token 延迟也会越高。很多推理引擎会把 prefill 和 decode 分开调度原因就在这里。连续批处理、chunked prefill 这类优化也都是为了让 prefill 阶段的算力和显存分配更平滑。2.2 Decode 阶段每步追加一个 token 的 K/V当 prefill 完成模型进入 Decode 阶段也就是真正的生成阶段。每生成一个新 token模型会把当前 token 送入网络注意力层用当前 token 的 Q 和已缓存的所有历史 K/V 做注意力计算然后得到输出。同时当前 token 自己也有一份 K/V 会被写进缓存供后续 token 使用。这里要特别注意一个容易误解的地方在高效实现里decode 阶段输入给模型的可能只是最新 token但模型并没有丢失历史信息。因为历史 token 的 K/V 都已经在 KV Cache 里了注意力计算仍然访问了全部历史位置。KV Cache 承载的正是“历史信息”本身。这就是很多人常说的 token 复用逻辑每个 token 的 K/V 只计算一次生成后续 token 时反复读取。2.3 伪代码与基础调用开关用伪代码来表示整个过程大致是这样的# 伪代码表示核心流程不依赖具体框架 # Prefill 阶段 prompt_tokens tokenizer.encode(prompt) k_cache, v_cache model.compute_kv(prompt_tokens) # Decode 阶段 while not stop: logits model.decode(last_token, k_cache, v_cache) next_token sample(logits) new_k, new_v model.compute_kv(next_token) k_cache append(k_cache, new_k) v_cache append(v_cache, new_v)如果你用过 HuggingFace Transformers 做生成可能会注意到一个参数叫use_cache。它就是 KV Cache 最直观的开关之一。默认打开时模型会把上一轮生成的 K/V 传给下一轮手动关掉同一个模型也能跑但每一步都会把所有历史 token 重新算一遍速度会肉眼可见地变慢。不同框架对 KV Cache 的封装差别很大比如 Transformers 里的past_key_values、vLLM 里的 KV block、llama.cpp 里的 kv cache 逻辑底层结构都不一样。但核心思想完全一致保存历史 token 的 K/V避免重复计算。2.4 不改变模型能力只改变成本结构如果缓存里的数值和重新计算完全一致KV Cache 不会改变模型的概率分布。它不能让模型凭空多出记忆也不能提升回答质量。真正的模型能力由模型结构和训练过程决定KV Cache 只负责把历史信息在推理时高效地传下去。所以当有人说“模型的上下文记忆能力来自 KV Cache”时这个说法是不够准确的。KV Cache 让长上下文推理在工程上可用但模型能记住多少、会不会混淆取决于模型自身的结构、训练数据和注意力机制。3. 显存账本为什么 KV Cache 特别吃显存3.1 KV Cache 显存估算公式KV Cache 占用多少显存通常可以用下面的公式估算KV Cache 字节数 2 × 层数 × batch_size × 序列长度 × 单头维度 × KV 头数 × 数据类型字节数拆开来看2一份 Key、一份 Value层数每个 Transformer 层都有自己独立的 K/V层数越多缓存越多batch_size同时处理的请求数/序列数序列长度当前上下文长度单头维度 × KV 头数KV 投影的总维度数据类型字节数fp16 占 2 字节int8 占 1 字节。在一些 MHA多头注意力模型里KV 头数 × 单头维度 hidden_size所以公式可以简化成KV Cache 字节数 ≈ 2 × 层数 × batch_size × 序列长度 × hidden_size × 数据类型字节数注意这个简化版本适用于 MHA。如果模型用了 GQA 或 MQA就必须按实际的 KV 头数来算不能直接套 hidden_size。3.2 7B 模型、4K 上下文的一个具体估算以常见的 7B 规模模型为例假设它有 32 层、hidden_size 为 4096、采用 MHA、精度是 fp16。代入公式大约是2 × 32 × 1 × 4096 × 4096 × 2 字节 2 GiB也就是说在 4K 上下文下单条请求的 KV Cache 就可能占 2GB 显存。如果换成 8K 上下文这个数字直接翻倍到 4GB。如果 batch_size 提升到 8再翻 8 倍。模型权重本身在 fp16 下大概是 13GB 左右。一张 24GB 显卡加载模型权重后剩下 11GB 左右再扣掉激活值和临时计算空间真正能留给 KV Cache 的就很有限了。这也是为什么本地部署 7B 模型时稍微把上下文拉长或者并发一高很容易就爆显存。需要说明的是这个数字是“在一个 MHA 结构的 7B 模型下”的估算。如果模型采用了 GQAKV Cache 会小很多。所以实际部署前先查清楚模型的层数、hidden_size、KV 头数和精度再按公式算一遍比直接背一个数字可靠得多。3.3 并发越高、长度越长显存压力越大KV Cache 的显存压力不是线性变化的“小问题”而是和 batch_size、序列长度同时相乘的大问题。很多人在本地试用大模型时觉得“4K、8K 也没多大”但服务端要同时给几十个用户提供长上下文时KV Cache 总量会非常可观。这也是为什么长上下文 API 通常更贵、更慢。服务端确实要为长对话的缓存付出真金白银的显存成本。从这个角度看KV Cache 直接决定了一个推理系统的并发上限和单请求上下文上限。调优时它往往是比模型权重更关键的资源瓶颈。3.4 从 MHA 到 GQA减少 KV 数量的关键演进近几年很多新模型开始用 GQA分组查询注意力而不是传统的 MHA一个重要原因就是为了减少 KV Cache。三种机制的区别可以这样理解注意力形式KV 头数KV Cache 相对大小典型场景MHA每个 Q 头对应一组独立 K/V最大早期 Transformer、不少 7B 开源模型MQA所有 Q 头共享一组 K/V最小部分追求推理效率、低显存的模型GQAQ 头分成若干组组内共享一组 K/V中等偏小现代大规模模型中的常见选择GQA 不是 KV Cache 的唯一解法但它是在模型结构层面直接减少 KV Cache 最有效的方案之一。这也是为什么讨论 KV Cache 优化时总要顺带提到 GQA你可以在推理框架层做各种缓存管理但如果模型结构本身就能少产生 K/V那是更底层、更长效的优化。4. 单次跑通不是结束KV Cache 的四个工程优化方向4.1 显存碎片化与 PagedAttention传统实现里KV Cache 通常需要一整段连续显存。不同请求的上下文长度不同请求到达和结束的时间不同显存很容易出现碎片一些小块显存无法被新请求有效利用导致实际可用容量小于总量。vLLM 这类推理框架之所以能提升吞吐一个关键原因是它采用了类似操作系统分页的思路。PagedAttention 把 KV Cache 切分成固定大小的 block按需分配不要求物理连续并维护一张块表。这样显存碎片可以被抑制不同请求之间也能更灵活地共享显存。实际部署时如果你的应用并发较高、上下文长度差异很大选择支持分页管理的推理引擎往往比在朴素实现里手动调参有效得多。4.2 共享前缀与 Prefix Caching另一个很容易被忽略的优化是 Prefix Caching。很多应用里所有请求都会携带完全相同的 system prompt、工具说明或者聊天模板前缀。如果不做任何缓存每个新请求都要重新 prefill 一遍这些共享 token重复计算量很大。Prefix Caching 的思路是把共享前缀对应的 KV block 缓存下来如果下一个请求的前缀相同直接命中缓存不再重新计算。这能同时降低首 token 延迟和 prefill 阶段的算力消耗。这个思路特别适合 Agent 类应用、多轮对话和固定模板场景。但它的效果高度依赖前缀的命中率。如果每个请求的 prompt 前缀都被拼接了随机内容命中率就会大幅下降。4.3 KV Cache 量化用精度换空间既然 KV Cache 是显存瓶颈一个直接思路就是压缩它的体积。如果把 KV Cache 从 fp16 降到 int8显存占用大约可以减少一半降到更低精度还能进一步缩小。不同推理框架对 KV Cache 量化的支持程度不同有的可以在启动参数里配置有的需要额外校准。但要明确一点KV Cache 量化是有损的。它可能会让输出结果产生细微变化对某些任务影响不大对某些需要严谨逻辑或精确引用的任务可能更明显。落地前最好在自己的数据集上做小样本对比确认质量波动是否可接受。4.4 稀疏注意力与滑动窗口换一种“记忆”方式还有一类优化思路不是压缩 KV而是改变 KV 的“记忆范围”。滑动窗口注意力只保留最近一段 token 的 KV显存占用会随窗口大小固定而不是随整段上下文无限增长。稀疏注意力则只对部分 token 计算注意力比如关注局部窗口外加少量全局 token。这类方法对长文本推理更友好但代价是可能牺牲长距离依赖能力。对很多任务来说最近的上下文比早期信息更重要所以这种取舍是合理的。但如果是文档级理解、全局推理类任务只保留局部窗口可能不够。4.5 落地时的参数边界和常见设置如果只是本地学习或者小规模验证默认配置通常够用。但如果要长期部署就需要额外考虑这些点max_model_len或n_ctx这类参数控制最大上下文长度不是越大越好越大意味着预留的 KV Cache 可能越多显存预留比例需要平衡模型权重、激活值和 KV Cache 三者的关系并发策略上先跑通单请求再逐步增加并发观察显存曲线如果显存吃紧可以尝试 KV Cache 量化但要验证精度损失部署服务后要监控显存占用和 token 级延迟才能判断瓶颈到底在哪一层。注意不要一上来就把上下文长度和并发数都拉满。先用一条请求跑通观察正常情况下的显存占用再逐步增加 batch找到当前机器能稳定支撑的边界。5. 推理变慢、显存 OOM从现象到根因的排查链路5.1 现象一首 token 特别慢首 token 延迟主要来自 Prefill 阶段。如果你的输入 prompt 很长比如消息记录拼接了几千 token那么首 token 慢是很正常的因为模型要一次性处理完整输入。需要重点排查的是是不是每次都在重复 prefill 同样的内容。比如用 API 或本地推理时每次请求都把完整对话历史重新拼接一遍而不是复用上一轮的 KV Cache那首 token 延迟会被明显放大。这时可以考虑开启 prefix caching或者改用支持增量推理的调用方式。如果首 token 延迟和输入长度没什么关系而是整体都很慢那更可能是服务端排队、显存不足或者硬件能力不够。5.2 现象二生成阶段越来越慢生成阶段越跑越慢有一个很物理的原因随着生成的 token 越来越多KV Cache 越来越长每一步的注意力计算都要读取完整缓存。序列越长单步需要读取的数据量越大单个 token 的生成延迟自然会上升。如果只是“长对话之后变慢”且速度曲线平滑这不一定是故障。真正需要警惕的是“每一步都慢得离谱”那可能是 KV Cache 没有正确生效或者每次 decode 都在重复执行 prefill。检查方法很简单看日志里的 decode 阶段是否只计算了新增 token还是又把完整历史序列重新 forward 了一遍。5.3 现象三显存 OOM 或服务被挤爆显存 OOM 的常见原因一般集中在这几个方向并发请求太多每个请求都有自己的 KV Cache单请求上下文过长KV Cache 按线性增长请求结束后KV Cache 没有及时释放或复用显存碎片导致剩余显存很多但无法分配连续块Prefill 阶段大输入时激活值峰值过高。碰到 OOM先看监控里的显存占用是“长期缓步爬升”还是“某个时间点突然冲高”。前者大概率是 KV Cache 累积或并发过高后者更可能是 prefill 阶段激活值突刺。5.4 一套可复用的排查顺序我在处理这类问题时一般会按下面这个顺序排查排查层关注点常见手段现象层变慢、OOM、截断、无输出记录复现步骤、日志、显存曲线输入层prompt 长度、共享前缀、cache 是否命中检查 token 数确认是否重复 prefill环境层驱动、框架版本、硬件规格核对依赖版本、GPU 显存、服务端时间参数层并发、max_len、量化、缓存预留调低 batch/max_len尝试开启量化结构层MHA/GQA、注意力类型、引擎实现换模型结构或换支持分页管理的引擎这个顺序的核心原则是先确定是哪一层出了问题再决定要不要改代码或换引擎而不是一上来就调一堆参数。6. 面试笔试里怎么回答才算真的懂 KV Cache6.1 高频问题清单以下这些问题几乎每次聊到大模型推理都会出现什么是 KV Cache它缓存的是什么为什么需要 KV Cache不用会怎样KV Cache 会改变模型的输出吗KV Cache 的显存占用怎么估算Prefill 和 Decode 阶段分别做了什么如何优化 KV CacheGQA、PagedAttention、量化、Prefix Caching 各解决什么问题为什么长上下文的成本主要来自 KV Cache这些问题看起来分散但本质上都围绕同一个核心KV Cache 解决的重复计算以及它引入的显存和带宽开销。6.2 三个最容易被误判的说法第一个误判是“KV Cache 让模型记住了上下文”。更准确的说法是KV Cache 让模型在解码时能高效地拿到历史 K/V。但模型是否有效利用了这些信息取决于模型自身容量和训练水平。缓存本身不产生记忆。第二个误判是“KV Cache 一定会提升生成质量”。这句话不准确。它的主要收益在减少重复计算、降低延迟、提升吞吐。在数值精度完全一致的前提下它不会改变模型原本的输出概率。第三个误判是“KV Cache 越大越好”。KV Cache 越大意味着能容纳的上下文越长但代价是显存被占用、单步注意力读取带宽增加、并发能力下降。合理的方式是在预算内最大化吞吐而不是盲目追求上下文长度。6.3 五层回答框架从“知道”到“能设计”如果面试中被问到 KV Cache建议按下面这五层来组织回答第一层是定义。KV Cache 缓存的是历史 token 在注意力层中的 Key 和 Value。第二层是动机。Transformer 自回归生成时每一步都需要历史 token 的 K/V 做注意力计算。如果不缓存每生成一个新 token 都要重新计算一遍前缀的 K/V重复劳动非常严重。第三层是过程。Preflill 阶段构建初始 KVDecode 阶段每步只计算当前 token 的 K/V 并追加到缓存。第四层是代价。KV Cache 随层数、batch_size、序列长度线性增长所以长上下文和并发会把显存压力放大。第五层是优化。模型层可以用 GQA/MQA 减少 KV 数量系统层可以用 PagedAttention 降低碎片、Prefix Caching 复用共享前缀、量化压缩体积部署层则要控制 max_len 和并发并做好监控。这套框架既能回答“是什么”也能展示工程思维。面试官继续追问任何一层你都有内容可以往下展开。KV Cache 不是一个单纯的缓存小技巧。它牵扯到的是整个大模型推理系统的成本结构算力、显存、带宽、并发、上下文长度全都被这一个机制连在一起。如果你要去解决一个真实的大模型服务性能问题可以先记住这句话KV Cache 解决的是重复计算消耗的是显存和带宽。它不值得追求无限大而是要在可用预算内最大化吞吐。动手时从单条请求、短上下文、默认参数开始跑通后再逐步加并发、加长度每一步都观察显存曲线和 token 速度。这样你才不是“听过一个名词”而是真的理解它。