想象一家川菜馆的后厨:大师傅每炒一道新菜,都要把从开店到现在所有客人点过的菜全部重新切一遍、重新备一遍料,才能决定这一勺该放多少盐。你会不会觉得这家店迟早要关门?可惜,很多大模型在“思考”时就是这么干的——每生成一个新的词,都要重新计算前面所有词的位置和含义(Key/Value 向量),重复劳动量大得吓人。 KV Cache 就是给后厨装上的“备菜架”:把每道菜已经切好的配菜(Key 和 Value 向量)留在案板上,新菜只切自己的部分,旧配菜直接拿来用。可问题来了:菜越点越多,案板(显存)不够用了。于是工程师们出了三招——把案板换成“分格置物架”(PagedAttention),让同样锅底的客人“共享一锅高汤”(Prefix Caching),以及让聊了很久的客人“续桌加菜不重做”(多轮对话共享)。这三招,就是大模型推理优化里最值钱的 KV Cache 工程。
KV Cache 优化是大模型推理部署的核心功课,掌握它,意味着你能用更少的显卡扛住更高的并发,这也是推理框架与 ML Infra 岗位面试的必考点。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
1. KV Cache 原理:备菜架与长记性的厨师
川菜馆后厨里,一位真正的大师傅炒新菜前,总要回味最近几道菜的味道——这一勺盐放多重、辣椒下几克,全都取决于前面的菜。Transformer 生成每一个新词,也是同样的“尝菜”过程:新词必须跟前面所有词逐一比对,才能决定自己是什么。如果每生成一个词,都把前面所有词从零开始重新“切一遍”(重新计算它们的键和值),那后厨的刀工再快也跟不上客人点菜的速度。
自回归生成模型逐字输出,生成第 N+1 个字时,注意力层需要用到前 N 个字各自的 Key(键)和 Value(值)向量。最朴素的做法是每步都把整套历史 K、V 重算一遍,计算量随序列长度平方级爆炸。于是工程师发明了 KV Cache:把已经算好的 K、V 原样放在显存里,后续步骤直接取用,只计算新字自己的 K、V。
缓存并没有改变注意力本身的数学定义,它省掉的只是“重复计算”。注意力公式如下:
有了备菜架,大师傅的刀工从“每天重切所有旧菜”变成“只切新菜”。账本越来越厚,但每一页都写得明明白白——这就是 KV Cache 的全部智慧:把重复劳动变成查表。
- Q 是当前查询Q 来自最新生成的 token,代表“现在这道菜想要什么味道”,每个新 token 都有自己的 Q,无法复用。
- K 是历史键K 是历史 token 的“菜牌标签”,缓存后无需重算,直接取出与 Q 做点积。
- V 是历史值V 是历史 token 的“备好的食材”,按注意力权重聚合出最终结果,同样被缓存。
- softmax 加权融合Q 与 K 点积后除以根号 d_k 缩放,再取 softmax,得到对每个历史 token 的注意力权重。
2. 显存瓶颈:案板不够用了
备菜虽好,案板却有限。后厨每切一份菜都占一块板面,客人越点越多,还没炒完,案板先堆满了,新菜没处落刀。KV Cache 也一样——它不占“模型体重”,却占“工作台面积”:对话越长,显存占用越高,而且是一条直线往上走。
要精确知道占多少,可以套一个简单公式。它由四个因素决定:每份 KV 有两套向量(K 和 V)、Transformer 每一层都要一份、每个 token 的向量维度、以及总共缓存了多少个 token。
举一个具体数字:模型 32 层、隐藏维度 4096,处理 2048 个 token,用 FP16,一条序列的 KV 就要 2×32×4096×2048×2 ≈ 1.07 GB。这还只是一个人!如果同时服务 8 个用户,光 KV 就是 8GB,一张 24GB 的消费级显卡瞬间告急。更麻烦的是,传统内存分配要求整块连续案板,序列一长就要换更大的板并整体搬菜(拷贝),剩下的边角料还放不下一整份菜(碎片)。案板满了,后厨只能“赶客”——降低并发。
- 因子 2 与层数 LK、V 各一份(×2),Transformer 每一层都各有一份 KV 缓存(×L)。
- 隐藏维度 H每个 token 的 K、V 向量长度为 H,多头只是把 H 拆分,总量不变。
- token 总数 NN = batch_size × 序列长度,上下文越长、并发越高,占用越大。
- 精度字节数 bFP16 是 2 字节,FP32 是 4 字节,换更低精度的量化能直接减半显存。
3. PagedAttention:分格置物架
为什么非要一整块大案板?聪明的后厨改成统一尺寸的“分格置物架”:每份菜按固定分量装进一格,格子可以放在后厨任何一个空闲角落,不必连成一片。哪个格子放着哪道菜,记在索引牌上就行。
这就是 vLLM 提出的 PagedAttention 的核心思路,灵感来自操作系统虚拟内存的分页:KV 不再按“连续大块”分配,而是切成固定大小的 block(如 16 个 token 一块),物理块可以散落各处。
置物架让边角碎料全都用上了,索引牌一查就知道菜在哪。后厨的“案板面积”利用率接近满格,能同时接待的客人瞬间翻倍。
- 块数计算ceil 向上取整,序列切成固定大小的逻辑块,最后不足一块也要占一格。
- 分配总量实际显存按块分配,可能略多于实际 token 数,用少量冗余换取消灭碎片。
- 块表映射逻辑块号经 block table 找到物理块号,物理块之间不必连续。
- 引用计数共享多个序列可指向同一物理块(如并行采样),写时才复制,靠引用计数维护安全。
4. Prefix Caching:共享一锅高汤
火锅店最大的聪明,不是每桌现熬锅底,而是提前煨一大锅高汤。谁来点鸳鸯锅,直接舀一勺汤底,再涮各自的菜。请求大模型也是这样:大量请求前面都挂着同一段“服务须知”。
在真实服务里,几乎每个请求都带相同的系统提示词,比如“你是客服助手,请用礼貌语气回答……”。如果每个请求都从头把这 500 个 token 的 KV 算一遍,就是在每桌重新熬汤。Prefix Caching 专门缓存公共前缀的 KV。
实现上,对前缀 token 序列做哈希,命中则把 KV 直接接进新请求的缓存,只计算前缀之后的差异 token。汤底越浓,翻桌越快。把企业客户的合规前缀缓存下来,成千上万个请求都在同一锅高汤里舀,GPU 的火力全用在涮新菜上。
- 公共前缀长度L_common 是两条请求从头开始完全相同的 token 数,通常来自系统提示词或固定指令。
- 总长度L_total 是请求完整 token 数,包含前缀与各自差异部分。
- 命中率命中率反映有多少 KV 可以直接复用,越高省得越多。
- 节省量节省的显存与计算量等于总缓存乘以命中率。
5. 多轮共享策略:续桌加菜不重做
客人边吃边聊,突然招手:“再加一份毛肚。”后厨绝不会把桌上已上的菜收回重做,只做新点的毛肚。多轮对话也一样:第 5 轮提问时,前 4 轮的问答已经端端正正“摆在桌上”。
朴素实现每轮都要把完整对话历史重新编码,等于每加一道菜就把整桌菜重做一遍。合理做法是:把多轮历史 token 的 KV 留在显存里,新的一轮只对新增 token 计算 K、V 再追加进缓存。
这意味着第 t 轮推理的 KV 成本只与“这一轮新增了多少 token”成正比,与总历史长度解耦。续桌经济学,越聊越省。长对话 Agent 之所以能跑起来,靠的就是这个增量策略。
- 本轮总缓存M_t 是第 t 轮结束时总 KV 显存占用。
- 上一轮缓存M_{t-1} 是上一轮已经算好并留在显存里的 KV,数学上完全精确,可直接复用。
- 增量计算只算本轮新增 token 的 K/V,ΔN 是两轮 token 数之差。
- 零重复代价历史部分全部命中,每轮复杂度从 O(N_t) 降为 O(ΔN)。
6. 工程实践权衡:后厨的管理智慧
置物架和高汤不是万能的。高汤一直煨着要占灶眼(缓存占用显存),每桌菜也不能无限堆(显存预算),共享汤底还得防串味——万一两个客人的过敏原不同呢(数据隔离)。
工程上需要在命中率与显存开销之间取舍:缓存按 LRU/FIFO 淘汰,太短的公共前缀不值得缓存;多租户场景按前缀隔离缓存空间,防止 A 租户的数据被 B 租户的请求读到。
如果公共前缀太短导致命中率只有 10%,缓存反而白占显存、拖累并发。好的后厨,不是菜越堆越多,而是用最少的案板做出最高的翻桌率。KV Cache 优化最终拼的,就是对“空间、时间、隔离”这三本账的平衡。
- 总 token 数N_total 是请求中全部参与注意力计算的 token 数量。
- 新增 token 数N_new 是实际需要新计算 KV 的 token,等于总数量减去命中部分。
- 加速比定义无缓存工作量除以有缓存工作量,比值越大越划算。
- 命中率形式用命中率直接表达,命中 80% 时加速 5 倍。
AI 应用场景
在线大模型推理服务
阶段:模型推理与部署
解决的问题:传统推理框架每条请求从头算 KV,全局并发低、显存碎片多,遇长上下文频繁 OOM。PagedAttention 让 KV 显存利用率接近满格,配合 Prefix Caching 让系统提示词共享,吞吐可提升数倍。
具体案例:某客服机器人接入微调后的 7B 模型,每用户携带 2000 token 系统提示词。优化前 8 卡 A100 仅撑 60 并发,改用 vLLM 后并发提升到 200+,单 token 延迟基本不变。
多轮对话与 Agent 应用
阶段:模型推理与上下文管理
解决的问题:Agent 每轮工具调用后都要重新发送完整历史,朴素实现重复编码导致延迟随轮数线性恶化。多轮 KV 共享把历史 KV 留在显存,每轮只算增量,长对话成本骤降。
具体案例:某代码助手的多轮会话平均 20 轮,使用会话级 KV 缓存后,平均每轮推理时间从 1.8s 降到 0.5s,GPU 成本下降约 60%。
企业私有化 LLM 网关
阶段:模型推理与资源调度
解决的问题:多租户共享固定 system prompt 和安全审核前缀,重复计算浪费严重。网关按前缀哈希做 Prefix Caching,并隔离各租户的缓存空间,既提升命中率又保证数据隔离。
具体案例:某金融企业网关统一注入“合规风控指令”500 token,日均请求 100 万次,前缀命中率约 70%,KV 计算量减少七成,首 token 延迟降低 40%。
Python 演示
"""KV Cache 优化极简模拟:对比无缓存与分块缓存的工作量"""class PagedCache: """分块管理的 KV 缓存,模拟 PagedAttention 的块表""" def __init__(self, block_size=4, total_blocks=10, occupied=(1, 5, 8)): self.block_size = block_size self.free_blocks = [b for b in range(total_blocks) if b not in occupied] self.block_table = [] self.kv_store = {} def append_kv(self, token_id, kv): """为新 token 分配物理块(按需扩容)并保存 KV""" block_id = token_id // self.block_size if block_id == len(self.block_table): if not self.free_blocks: raise RuntimeError("OOM: 物理块用完,需要淘汰或扩容") self.block_table.append(self.free_blocks.pop(0)) self.kv_store[token_id] = kv def has_kv(self, token_id): return token_id in self.kv_store def simulate(): cache = PagedCache() naive_work = 0 cached_work = 0 rounds = (3, 5, 7, 9) pos = 0 for r, cnt in enumerate(rounds, 1): added = 0 for _ in range(cnt): if not cache.has_kv(pos): cache.append_kv(pos, (pos, pos)) added += 1 naive_work += pos + 1 cached_work += 1 pos += 1 print(f"第{r}轮: 新增{added} KV, 序列{pos} token, " f"无缓存累计{naive_work}, 有缓存累计{cached_work}") print(f"\n块表(逻辑->物理): {cache.block_table}") print(f"无缓存总工作量 {naive_work} ≈ O(N^2)") print(f"有缓存总工作量 {cached_work} = O(N)") simulate()
self.free_blocks = [b for b in range(total_blocks) if b not in occupied]初始化时把被其他请求占用的物理块剔除,剩下的才是可用的空闲格,对应显存的空闲块表。if block_id == len(self.block_table):只有逻辑块号超出当前块表长度时才分配新物理块,旧块直接命中,这就是“按需分页”。self.block_table.append(self.free_blocks.pop(0))从空闲池取一个物理块,它很可能不连续,块表负责记住逻辑到物理的映射。naive_work += pos + 1无缓存时每生成一个 token 要把前面所有历史 PV 重新算一遍,工作量持续累加。cached_work += 1有缓存时每步只算新增 1 个 token 的 KV,历史全部复用。print(f"块表(逻辑->物理): {cache.block_table}")输出逻辑块到物理块的映射表,直观呈现 PagedAttention 的“索引牌”。
常见误区
误区我一开始以为 KV Cache 是模型训练好的参数,像 Transformer 的权重一样存在模型文件里,加载模型时就有了,不用自己管理。
正解KV Cache 是推理时动态计算出来的“中间产物”。每输入一段文本,都要在显存里重新算一遍 K/V 并缓存;换一段输入,缓存就完全换掉。它不写进模型文件,也不属于权重,而是像后厨“今天切好的配菜”一样,属于一次性备菜。
为何常见:K/V 名字听起来像“模型学到的知识”,容易和注意力层的可学习权重混淆。
误区8B 模型 FP16 权重 16GB,我觉得显存大头就是模型权重,上下文长短影响很小,KV 缓存那点内存不值一提。
正解推理时权重不管处理多少 token 都占固定一份,但 KV 随上下文线性增长。32 层、4096 维下,2048 token 的 KV 就约 1GB;若 batch 和并发大,KV 会远超权重。这就是为什么长上下文服务常常先 OOM 在 KV 上。
为何常见:很少人实际算过 KV 显存公式,被“模型大小”先入为主,忽略了推理时的显存大头其实在动态缓存。
误区PagedAttention 只是更“省内存”,对单请求的生成速度没帮助,所以重要性一般。
正解省内存本身就是加速:能塞下更多并发请求,GPU 利用率上升,端到端吞吐显著提升;同时避免了连续分配时整体搬移的拷贝开销,还支持多序列共享前缀块。vLLM 发布时吞吐可提升数倍,靠的就是这套“分格置物架”。
为何常见:把“省内存”和“省时间”对立起来,忽略了部署场景下吞吐才是核心指标。
误区凡是公共前缀,不管多长都缓存,反正能复用,缓存越多越好。
正解前缀缓存要占显存;如果公共前缀短、命中率低,缓存本身会挤占可用显存,反而降低并发。工程上要设前缀长度阈值、按 LRU 淘汰,在收益与成本之间做权衡。
为何常见:只看到复用好处,没看到缓存也有“机会成本”——占着灶眼就不能炒别的菜。
误区缓存了历史 KV,模型会不会“记不清”或回答质量下降?感觉缓存就像图片压缩,会丢信息。
正解KV Cache 是精确缓存,不是近似压缩。复用的 K/V 与重新计算完全一致,注意力输出逐位相同,生成质量毫无损失。唯一的风险是显存放不下时被淘汰,那属于容量问题,不是精度问题。
为何常见:把“缓存”类比成图像压缩或人脑遗忘,忽略了它本质上是无损的中间结果暂存。
面试题
Q1. KV Cache 中的 K 和 V 分别指什么?
- A.损失值与梯度更新
- B.注意力中键与值向量← 正确答案
- C.词表编号与词频统计
- D.学习率与动量参数
答案:B。注意力计算用 Q 去匹配 K,再按权重取 V,K、V 是 token 经矩阵变换得到的向量(B)。A/D 是训练动态概念,C 是词频统计,都与注意力无关。
Q2. KV Cache 主要解决什么问题?
- A.减少参数并压缩显存
- B.复用历史键值防重算← 正确答案
- C.提升训练集准确精度
- D.加速梯度反向传播
答案:B。KV Cache 的核心是缓存历史 token 的 K/V,避免自回归生成时每步重复计算前面所有 token 的 K/V(B)。A 混淆了参数压缩,C/D 属于训练阶段的事,与推理缓存无关。
Q3. 关于 KV Cache 显存占用,正确的说法是?
- A.与序列长度完全无关
- B.随序列长度线性增长← 正确答案
- C.随序列长度指数暴涨
- D.只随模型参数变化
答案:B。每个新增 token 都要额外存 2×L×H 个数的 K/V,显存总量与 token 数 N 成正比(B)。指数暴涨是 C 的夸张,A/D 忽略了上下文长度对缓存的影响。
Q4. PagedAttention 借鉴了什么思想?
- A.数据库 B 树索引优化
- B.操作系统虚拟内存分页← 正确答案
- C.编译器寄存器分配
- D.网络拥塞窗口控制
答案:B。PagedAttention 把 KV 切成固定块、用块表做逻辑物理映射、引用计数共享与写时复制,整个框架都来自 OS 虚拟内存分页(B)。A/C/D 是其他领域技术,与分页机制无关。
Q5. PagedAttention 中 block table 的作用是?
- A.保存模型全部权重值
- B.记录逻辑块物理块映射← 正确答案
- C.缓存每条训练的样本
- D.存放注意力分数矩阵
答案:B。块表维护“逻辑块号 → 物理块号”的映射,物理块可分散在显存任意空闲位置(B)。A 是权重存储,C 是数据缓存,D 是注意力中间结果,都不是块表的职责。
Q6. Prefix Caching 最适合哪种场景?
- A.请求间完全无公共前缀
- B.共享长系统提示词场景← 正确答案
- C.单 token 短查询占多数
- D.纯离线批量推理任务
答案:B。前缀复用的收益来自“公共前缀占比高”,长系统提示词正是典型场景(B)。A 没有可复用内容,C 前缀太短收益低,D 离线批处理通常无共享前缀需求。
Q7. 关于无缓存与有缓存的工作量,说法正确的有?
- A.无缓存时生成新词要重算全部历史 KV← 正确答案
- B.无缓存累计工作量约为序列长度平方← 正确答案
- C.有缓存后每步仍需重算所有历史
- D.有缓存每步新增工作量变为常数← 正确答案
答案:ABD。无缓存时每步重扫全部历史,累计工作量 ≈ N(N+1)/2,是 O(N²)(A、B 对);有缓存后每步只算 1 个新 token 的 KV,历史直接命中(D 对),C 恰恰相反,故排除。
Q8. PagedAttention 减少内存碎片的手段包括?
- A.固定大小的块式分配← 正确答案
- B.逻辑物理块映射机制← 正确答案
- C.模型参数也进行分页
- D.块可放入任意空闲位← 正确答案
答案:ABD。固定块分配(A)、块表映射(B)、散落空闲位(D)共同消除碎片。C 错误:PagedAttention 只对 KV 缓存分页,模型权重常驻显存,不走块分配。
Q9. 关于 Prefix Caching 与多轮共享,正确的有?
- A.命中前缀仍要重算历史 KV
- B.前缀缓存可在请求间复用← 正确答案
- C.多轮对话只算新增的 token← 正确答案
- D.复用缓存会降低生成质量
答案:BC。前缀缓存按前缀哈希命中后直接复用(B 对);多轮共享下每轮只算新增 token 的 KV(C 对)。A 与 B 相反,D 错误——KV 缓存是精确缓存,数学上与重算完全一致,不损失质量。
Q10. 某服务通过 Prefix Caching 使公共前缀命中率达到 80%,理想情况下 KV 计算加速比约为?
- A.约 1.25 倍
- B.约 2 倍
- C.约 5 倍← 正确答案
- D.约 10 倍
答案:C。加速比 = 1 / (1 − 命中率) = 1 / 0.2 = 5(C 对)。A 是把 0.8 直接取倒数(1.25),B/D 数值与公式不符。命中率越接近 1,加速比越恐怖。