ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

KV Cache 原理与优化实战:从显存估算到 PagedAttention 落地

KV Cache 原理与优化实战:从显存估算到 PagedAttention 落地

1. 从一次线上事故说起:AI 越聊越慢的真相

去年帮一个团队排查他们客服机器人的性能问题,现象很典型:单轮问答响应飞快,200ms 内出首字,但只要用户连续追问超过七八轮,首字延迟就飙到 2 秒以上,到第十五轮左右基本没法用了。他们一开始怀疑是模型推理框架的问题,换了两个框架、加了 GPU、调了 batch size,全都没用。最后把每一轮的耗时打点拉出来一看,问题根本不在计算框架,而在一个很多人听过但没真正搞明白的东西——KV Cache。

这个场景其实非常普遍。你用一个对话类 AI 产品,刚开始聊得挺顺,聊到后面越来越卡,尤其是那种需要它记住前面大段内容的场景,比如让它读一份长文档再连续提问,或者做多轮代码调试。很多人第一反应是"模型太大了"或者"服务器不行",但真正的原因往往藏在注意力机制的计算方式里。KV Cache 就是理解这件事的钥匙,它既是大模型能流畅对话的功臣,也是长上下文成本失控的元凶。

这篇文章我打算把 KV Cache 从原理到实操彻底讲透。你会搞清楚三件事:第一,为什么没有 KV Cache 大模型几乎没法用;第二,KV Cache 到底吃掉了多少显存,这个账怎么算;第三,长上下文真正的成本瓶颈在哪里,以及工程上有哪些能落地的优化手段。内容会涉及注意力计算、显存估算公式、PagedAttention 这类工程方案,也会给可以直接抄的参数计算过程。适合做推理部署、模型服务、AI 应用开发的工程师,也适合想搞明白"为什么我的 AI 越聊越慢"的产品和技术负责人。基础不需要特别深,但最好对 Transformer 的基本结构有个概念,我会尽量用生活化的类比把关键点讲清楚。

2. KV Cache 到底是什么:一个缓存换速度的经典交易

2.1 自回归生成:一个字一个字往外蹦的本质

要理解 KV Cache,得先理解大模型是怎么"说话"的。现在主流的对话模型都是自回归生成,也就是一次只预测下一个 token,然后把预测出来的 token 接到输入后面,再预测下一个,如此循环。这就像你写句子,写一个字看一眼前面所有字,再决定下一个字写什么。

问题就出在"看一眼前面所有字"这个动作上。在 Transformer 的注意力机制里,每生成一个新 token,模型都要拿当前这个 token 的 Query 向量,去和前面所有 token 的 Key 向量做点积,算出注意力权重,再对前面所有 token 的 Value 向量做加权求和。注意,这里"前面所有 token"是随着生成过程不断变长的。

假设你在生成第 100 个 token,那模型需要拿第 100 个 token 的 Q,去和前面 99 个 token 的 K 做计算。生成第 101 个 token 时,又要拿第 101 个 token 的 Q,去和前面 100 个 token 的 K 做计算。你会发现,前面那些 token 的 K 和 V 其实一直没变,但每一轮都在被重新计算一遍。这就是巨大的浪费。

2.2 没有缓存会怎样:计算量的平方级爆炸

我们来算一笔账。假设序列长度是 n,注意力计算的核心是 Q 和 K 的矩阵乘法,复杂度是 O(n²)。在自回归生成里,如果每一轮都把前面所有 token 的 K、V 重新算一遍,那么生成第 n 个 token 时,累计的计算量大致是 1 + 2 + 3 + ... + n,也就是 O(n²) 级别。序列越长,浪费越夸张。

举个具体的例子。生成 1000 个 token,如果不做任何缓存,注意力部分的计算量大致是缓存方案的 500 倍左右(因为平均每轮要重算一半的前缀)。这还只是注意力部分,实际推理里还有前馈网络等其他开销,但光是这一项就足以让长对话变得不可接受。所以 KV Cache 的核心思想特别朴素:既然前面 token 的 K 和 V 算过一次就不会变,那就把它们存下来,下一轮直接拿来用,不再重算。

这就像你做菜,切好的葱姜蒜放在案板上备用,而不是每炒一道菜都重新切一遍。案板就是缓存,切好的配料就是 K 和 V。这个类比虽然粗糙,但抓住了本质:用空间(显存)换时间(计算)。

2.3 缓存带来的收益:从平方到线性

加上 KV Cache 之后,每生成一个新 token,只需要计算当前 token 的 Q、K、V,然后拿新的 Q 去和缓存里所有的 K 做注意力,再把新的 K、V 追加到缓存末尾。计算量从 O(n²) 降到了 O(n),每一轮的计算量基本恒定(除了注意力那部分随长度线性增长)。

这个改变有多关键?可以说,没有 KV Cache,就没有今天能流畅对话的大模型产品。你每次和 AI 聊天时感受到的"秒回",背后都是 KV Cache 在撑着。但天下没有免费的午餐,省下来的计算时间,是用显存换来的。而且随着对话变长,这份显存开销会线性增长,最终成为新的瓶颈。这就是为什么 AI 会"越聊越慢"——不是计算变慢了,而是缓存越来越大,显存压力越来越大,能并行处理的请求越来越少,排队时间越来越长。

3. 显存账本:KV Cache 到底吃掉多少资源

3.1 一个可以直接套用的显存估算公式

很多人对 KV Cache 的显存占用没有概念,觉得"不就是存点中间结果吗"。实际上,在大 batch、长序列的场景下,KV Cache 的显存占用可以轻松超过模型权重本身。我们来看公式。

单个 token 的 KV Cache 占用(字节)可以这样算:

每 token 占用 = 2 × 层数 × 注意力头数 × 头维度 × 数据类型字节数

其中:

  • 2 是因为要同时存 K 和 V
  • 层数、注意力头数、头维度由模型结构决定
  • 数据类型字节数:FP16 是 2 字节,FP8 是 1 字节,INT8 也是 1 字节

注意,这里用的是注意力头数 × 头维度,它等于隐藏维度(在标准多头注意力里)。有些模型用分组查询注意力(GQA),K 和 V 的头数比 Q 少,这时候要按 K、V 的实际头数来算,能省不少。

整个序列的 KV Cache 占用就是:

总占用 = 每 token 占用 × 序列长度 × batch size

3.2 拿一个真实规模的模型算一遍

我们拿一个常见的 70 亿参数级别模型举例,假设它有 32 层,隐藏维度 4096,用 32 个注意力头,头维度 128,数据类型 FP16。那么每 token 的 KV Cache 占用是:

2 × 32 × 32 × 128 × 2 字节 = 524288 字节 ≈ 0.5 MB

也就是说,每存一个 token 的 KV,要花 0.5 MB 显存。这个数字单看不大,但乘上序列长度和 batch 就吓人了。

  • 序列长度 2048,batch size 1:0.5 MB × 2048 ≈ 1 GB
  • 序列长度 8192,batch size 1:0.5 MB × 8192 ≈ 4 GB
  • 序列长度 8192,batch size 16:4 GB × 16 = 64 GB

一张 80GB 的卡,光 KV Cache 就吃掉 64GB,模型权重(FP16 下 70 亿参数约 14GB)加上激活值、框架开销,基本就爆了。这就是为什么长上下文 + 大并发是显存杀手。你可能会说,那用 GQA 呢?如果 K、V 的头数从 32 降到 8,每 token 占用直接降到 0.125 MB,同样的场景显存占用变成 16GB,压力小很多。这也是为什么现在新模型普遍用 GQA 而不是标准多头注意力——省 KV Cache 显存是核心动机之一。

3.3 长上下文为什么贵:注意力计算和缓存的双重压力

很多人以为长上下文的成本就是"能塞进去多少 token",其实真正的成本分两块。

第一块是注意力计算成本。虽然有了 KV Cache,每轮计算量是线性的,但注意力的核心矩阵乘法仍然是 O(n) 每轮,累计到整个生成过程还是 O(n²)。序列翻倍,注意力计算量翻四倍。这就是为什么 128K 上下文的模型,处理长文档时首字延迟明显比短对话高。

第二块是KV Cache 显存成本,也就是上面算的那笔账。序列越长,缓存越大,能同时服务的请求越少,吞吐量下降,单位成本上升。而且长上下文场景下,用户往往不会只发一轮,多轮对话会让缓存持续累积,直到触及显存上限。

这两块成本叠加,就是"长上下文真正的成本在哪"的答案:不在模型权重,而在随序列长度线性增长的 KV Cache 和随长度平方增长的注意力计算。理解了这一点,后面的优化手段就都有方向了。

4. 工程优化实战:把 KV Cache 的成本压下来

4.1 PagedAttention:像操作系统管理内存一样管理缓存

KV Cache 有一个很讨厌的特性:它的长度是动态的,每个请求的序列长度不一样,而且生成过程中不断增长。如果按最大长度预分配显存,浪费极其严重;如果动态分配,又容易产生内存碎片。这个问题和操作系统里的内存管理几乎一模一样。

vLLM 提出的PagedAttention就是借鉴了操作系统的分页思想。它把 KV Cache 切成固定大小的块(block),每个块存若干个 token 的 KV。一个请求的缓存由多个不连续的块组成,通过一张块表来索引。这样带来几个好处:第一,显存按需分配,不用预留最大长度,浪费大幅减少;第二,块可以在请求之间共享,比如多个请求有相同的系统提示词前缀,那部分 KV 可以共用一份;第三,内存碎片问题基本消除。

实测下来,PagedAttention 能把 KV Cache 的显存利用率从传统方案的 20% 到 40% 提升到 90% 以上,吞吐量提升 2 到 4 倍。这个提升幅度在推理部署里是相当夸张的,也是 vLLM 能火起来的关键原因之一。

4.2 量化 KV Cache:FP16 到 FP8 甚至 INT4

既然 KV Cache 占显存,那最直接的想法就是把它压小。KV Cache 里的数值本来就是激活值,对精度没那么敏感,量化空间很大。

从 FP16 降到 FP8,显存直接减半,精度损失在大多数对话场景下几乎感知不到。再激进一点降到 INT4,显存变成原来的四分之一,但精度损失开始明显,尤其是长上下文里误差会累积,可能导致输出质量下降。我的经验是:FP8 基本可以无脑上,INT8 需要评测,INT4 要谨慎,最好只在对质量要求不高的场景用。

量化 KV Cache 有个坑要注意:K 和 V 对量化的敏感度不一样。有些实现只量化 V 不量化 K,或者对 K 用更细的粒度。另外,量化后的反量化操作会带来额外计算开销,需要权衡。实测在长序列场景下,量化省下的显存带来的并发提升,通常能覆盖反量化的计算开销,整体是划算的。

4.3 前缀共享与缓存复用:别重复算一样的东西

很多 AI 应用有一个共同特征:大量请求共享相同的前缀。比如客服机器人,所有对话都以同一段系统提示词开头;比如文档问答,同一份文档被多个用户提问。这些相同前缀的 KV Cache 完全可以只算一次,多个请求复用。

这个优化在工程上叫前缀缓存(prefix caching)。实现方式是把前缀的 KV Cache 存起来,新请求进来时先做前缀匹配,命中就直接复用,只计算新增部分。对于系统提示词很长的场景,这个优化能省下大量首字延迟和计算量。

注意:前缀缓存对匹配的要求是"完全一致的前缀",差一个 token 都不能复用。所以系统提示词要尽量固定,不要在里面塞动态内容(比如当前时间、用户 ID),否则缓存永远命中不了。

4.4 滑动窗口与稀疏注意力:不是所有历史都值得记住

还有一个思路是从源头减少需要缓存的 token 数量。滑动窗口注意力只保留最近 N 个 token 的 KV,更早的直接丢掉。这样缓存大小恒定,不随对话变长而增长。代价是模型看不到太久远的信息,适合那些"只需要记住近期上下文"的场景。

稀疏注意力则更精细,它不丢 token,而是让每个 token 只关注一部分历史 token,减少计算和缓存。这类方法在长上下文模型里越来越常见,但实现复杂度高,需要模型结构配合训练,不是推理阶段能随便加的。

选择哪种方案,取决于你的场景。如果对话轮次多但每轮信息量小,滑动窗口够用;如果需要精确回忆长文档细节,那就得老老实实保留完整缓存,靠 PagedAttention 和量化来压成本。

5. 常见问题与排查技巧实录

5.1 首字延迟随对话变长而飙升,怎么定位

这是最典型的症状。排查思路分三步。第一步,确认是不是 KV Cache 导致的:把对话历史截断到固定长度,看延迟是否稳定。如果截断后延迟正常,基本就是缓存问题。第二步,看显存占用曲线,如果随对话轮次线性上升直到接近上限,说明缓存没被有效管理。第三步,检查推理框架是否开启了 PagedAttention 和前缀缓存,很多默认配置是关的。

如果确认是缓存问题,优化顺序建议是:先开 PagedAttention(如果框架支持),再开前缀缓存,然后考虑 KV Cache 量化,最后才是滑动窗口。这个顺序是按"收益/风险比"排的,前面的改动风险低收益高。

5.2 显存明明够,为什么还是 OOM

这种情况通常是显存碎片或者峰值估算错误。KV Cache 动态增长,如果框架按最大长度预分配,实际用量可能远小于预留量,但预留的部分别人也用不了。PagedAttention 能解决大部分碎片问题。另一个原因是并发请求的缓存峰值叠加,比如 16 个请求同时到达最长序列,峰值显存是单请求的 16 倍,如果按平均情况估算就会 OOM。解决办法是限制最大并发数,或者用请求调度让长请求错峰。

5.3 量化 KV Cache 后输出质量下降怎么办

先确认下降是不是量化导致的:关掉量化跑同样的输入,对比输出。如果确实是量化问题,几个调整方向:把 K 的量化精度调高(比如 K 用 FP8,V 用 INT4);缩小量化分组粒度,让每组内的数值范围更集中;对异常值做特殊处理,因为注意力里偶尔会有数值特别大的激活值,量化时容易被压扁。实测分组量化比全局量化效果好很多,代价是元数据占用略增。

5.4 常见问题速查表

症状可能原因排查方向解决手段
首字延迟随轮次上升KV Cache 累积截断历史对比延迟开 PagedAttention、前缀缓存
显存够但 OOM碎片或峰值叠加看显存分配曲线限制并发、用分页管理
长上下文吞吐骤降缓存占满显存算 KV Cache 占用量化、GQA、滑动窗口
量化后质量下降精度损失累积关量化对比输出调高 K 精度、分组量化
前缀缓存不命中前缀含动态内容检查提示词固定系统提示词

5.5 几个踩过的坑

第一个坑:以为 batch size 越大吞吐越高。在长上下文场景下,大 batch 会让 KV Cache 显存爆炸,反而触发 OOM 或者频繁换页,吞吐不升反降。正确做法是根据序列长度动态调 batch,长序列用小 batch。

第二个坑:忽略 prefill 和 decode 的区别。prefill 阶段是处理输入,计算密集;decode 阶段是逐 token 生成,显存带宽密集。KV Cache 主要影响 decode 阶段。优化时要分开看两个阶段的指标,别混在一起调。

第三个坑:前缀缓存和动态提示词冲突。有些应用喜欢在系统提示词里插入当前时间戳,结果每次前缀都不一样,缓存永远不命中。把动态内容挪到用户消息里,前缀就能稳定复用。

6. 我个人的一些实操体会

KV Cache 这个东西,刚接触时觉得就是个"存中间结果"的小技巧,真正做过推理部署之后才发现,它是整个大模型服务成本结构的核心变量。你去看任何一个推理框架的优化重点,绕来绕去都绕不开它。PagedAttention、量化、前缀缓存、GQA,这些看起来独立的技术,本质上都在回答同一个问题:怎么用更少的显存,撑住更长的上下文和更大的并发。

我的建议是,如果你在做 AI 应用,哪怕不直接碰推理框架,也一定要把 KV Cache 的显存账算清楚。因为你的产品设计——比如允许多长的对话历史、支持多大的文档、能扛多少并发——最终都受这个账本约束。很多产品经理拍脑袋定的"支持 100 轮对话",在工程上可能意味着显存成本翻十倍,这个账不算清楚,上线就是事故。

最后分享一个我常用的估算习惯:拿到一个新模型,先算它每 token 的 KV Cache 占用,然后乘以你预期的最大序列长度和并发数,看看总显存够不够。这个数字如果超过显存的 60%,就要提前规划优化方案了。这个习惯帮我避开了好几次上线前的性能翻车。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进