【AI工程师精讲】KV Cache 精讲:为什么 AI 越聊越慢,以及长上下文真正的成本在哪

0 阅读30分钟

适合读者:调用过大模型 API、或自己部署过推理服务的人;被"长上下文为什么贵""并发为什么上不去""为什么加了卡还是慢"这类问题困扰的人。

核心收获:① 预填充与解码为什么是两种完全不同的负载;② 能在纸上估出任意模型的 KV Cache 显存占用和并发上限;③ 看懂五种优化手段各自在治什么病、代价是什么、什么时候不该用;④ 一套线上"变慢"的定位流程。


新开一个对话,发一句"你好",字是"跳"出来的,几乎没有延迟。聊到十几轮,同样问一句话,先卡两秒,然后才慢慢往外吐字;再往后,甚至会出现打字机那样一顿一顿的感觉,最后干脆超时报错。

直觉上你会怪网络、怪服务器、怪模型"想得太久"。但做过推理服务的人都知道,这三样通常都不是主因。真正在变慢的是同一件事:你要它看的历史,越来越长了。

更反直觉的是第二层现象:升级到一张更贵的卡,变慢的问题往往没怎么改善。 如果你只把"AI 慢"理解成"算力不够",这个结果无法解释。而理解它,需要先理解一件事——大模型生成文本时,瓶颈根本不在算力上。

这篇文章会从最基本的生成机制讲起,一路推到显存估算、并发上限、五种优化手段的原理与取舍,最后给一套可执行的线上诊断流程。


第一部分:前提 —— 自回归生成到底在做什么

1.1 "预测下一个词"是一个循环

大模型生成文本的方式,本质上是一个循环:

  1. 把当前的 token 序列送进模型;
  2. 模型输出一个概率分布,代表"下一个 token 最可能是谁";
  3. 按某种采样策略挑一个 token,追加到序列末尾;
  4. 回到第 1 步,直到生成结束符或达到长度上限。

这个循环叫自回归生成(autoregressive generation)。关键词是追加:每生成一个 token,序列就长一格,然后整段重新送进模型。

理解这一点非常重要,因为它决定了一个天然的代价:生成第 N 个 token 时,模型必须能看到前 N-1 个 token。 而注意力机制要求"每个位置和前面每个位置都发生一次交互"——这就埋下了后面所有性能问题的种子。

1.2 两个阶段:预填充与解码

虽然从用户视角看,回答是一口气出来的,但在模型内部,一次请求被清晰地切成两个性质完全不同的阶段。

第一阶段:预填充(Prefill)。

你这次发过去的所有内容——系统提示、历史对话、检索到的资料、本轮问题——被一次性、并行地送进模型。所有位置同时计算,产出第一个 token。这一步可以吃满算力,因为它有大量相互独立的计算可以并行。

第二阶段:解码(Decode)。

从第二个 token 开始,一次只能生成一个。每生成一个,就把它接到序列末尾,再算下一个。这一步是严格串行的——不生成第 3 个 token,就没法生成第 4 个。

而串行带来的后果是:每生成一个 token,都要把这一层所有的权重从显存完整读一遍。

这就引出了整篇文章最核心的一组概念:

预填充 Prefill解码 Decode
并行度高(所有输入 token 同时算)无(一个一个来)
瓶颈算力(计算密集)显存带宽(访存密集)
决定指标首字延迟 TTFT出字速度 TPOT
优化方向减少输入长度、复用前缀减少搬运的数据量

1.3 为什么"访存密集"这四个字这么关键

这里需要一个概念:算术强度(arithmetic intensity),定义是"这段计算里,每搬运 1 字节数据,能完成多少次运算"。

  • 预填充阶段:一次读入一批权重,可以同时服务几百上千个 token 的计算。算术强度很高,属于计算密集——瓶颈是"算得够不够快"。
  • 解码阶段:生成一个 token,就要把全部权重读一遍,而这些权重只服务这一次计算。算术强度极低,属于访存密集——瓶颈是"数据搬得够不够快"。

这个区别直接解释了一个让很多人困惑的现象:为什么换个算力更强的 GPU,出字速度提升有限?

因为解码阶段 GPU 的计算单元大部分时间在等数据。算力翻倍,但显存带宽没翻倍,速度就上不去。这也是为什么后面所有关于解码的优化,本质上都在做同一件事:减少需要搬运的数据量。

1.4 两个必须分清的性能指标

  • TTFT(Time To First Token,首字延迟):从发出请求到第一个字出现。主要由预填充决定,也就是"你发过去的内容有多长"。
  • TPOT(Time Per Output Token,每字耗时):第一个字之后,平均每个字的耗时。主要由解码决定,也就是"每一步要搬多少数据"。

把这两个混在一起谈"快慢",是线上事故里最常见的沟通障碍。用户说"卡",可能指的是首字等了 3 秒,也可能指的是出字像挤牙膏——这两者的排查方向和优化手段完全不同。


第二部分:KV Cache 的来龙去脉

2.1 没有缓存会怎样:平方级的代价

先做个思想实验:假如不缓存任何东西,生成一个长度为 N 的回答,要付出多少代价?

生成第 1 个 token 时,模型要看 1 个位置;生成第 2 个时,要看 2 个;生成第 N 个时,要看 N 个。总计算量是:

1 + 2 + 3 + ... + N = N(N+1)/2 ≈ O(N²)

长度翻倍,代价翻四倍。 一个 2000 字的回答,如果每步都从头重算,慢到完全没有实用价值——这不只是"慢一点",而是根本无法上线。

但这里有一个关键观察:前面那些 token 的中间结果,其实是可以复用的。

因为当你追加新 token 时,前面那些 token 的表示不会改变。第 5 个 token 在长度为 10 的序列里是什么样,在长度为 1000 的序列里还是什么样。它不受后面内容的影响。

这就是缓存的数学基础:可复用性来自注意力的因果性。

2.2 注意力的三个角色:Q、K、V

要理解缓存什么,得先看清注意力在算什么。

每个 token 进入某一层后,会经过三个不同的线性投影,得到三个向量:

  • Query(Q):我"要找什么"。代表当前这个位置发出的检索请求。
  • Key(K):我"是什么"。代表这个位置能提供的匹配特征。
  • Value(V):我"携带什么信息"。代表这个位置真正要传递的内容。

注意力的计算过程可以概括为三步:

  1. 用当前 token 的 Q,去和所有历史 token 的 K 做点积,得到一组相似度分数;
  2. 把这些分数做 softmax 归一化,变成一组权重;
  3. 用这组权重对所有历史 token 的 V 做加权求和,得到当前 token 的输出。

现在关键问题来了:当你在序列末尾追加一个新 token、重新计算这一层时,哪些东西变了,哪些没变?

  • 新 token 的 Q、K、V:新算的。
  • 历史 token 的 K:完全没变(K 只取决于该位置自身的表示,与后面追加什么无关)。
  • 历史 token 的 V:完全没变(同理)。
  • 历史 token 的 Q:没变,但也不需要了——它在上一步就已经用完了。

结论非常清晰:只需要缓存历史的 K 和 V,Q 不需要缓存。

这也顺便解释了显存公式里那个系数 2 的来历:K 和 V 各存一份。

2.3 缓存到底长什么样

在一层内部,KV Cache 是两个张量:

  • K_cache:形状大致是 [历史长度, KV 头数, 每头维度]
  • V_cache:形状相同

每生成一个新 token,做的事情是:

# 伪代码:一层内的解码步骤
q, k_new, v_new = project(hidden_state)   # 新 token 的 Q、K、V

K_cache.append(k_new)                      # 追加,不重算历史
V_cache.append(v_new)

scores = q @ K_cache.T                     # 用新 Q 匹配全部历史 K
weights = softmax(scores / sqrt(d))
output = weights @ V_cache                 # 对全部历史 V 加权求和

注意这里的变化:每一步从"重算全部历史",变成了"一次矩阵乘法 + 读一段缓存"。

计算量从"和历史长度成正比"(累计起来是平方级)降到了"和历史长度线性相关"(因为是读缓存,不是重算)。

而代价也很明确:这些缓存必须一直待在显存里,而且随对话长度不断增长。

2.4 一个容易被忽略的事实

KV Cache 不是"可选的性能优化",而是现代推理框架的默认前提。所有主流框架(vLLM、TensorRT-LLM、SGLang 等)都把它作为基础机制,不是在它之上做加法。

它的位置有点像数据库的索引:你不会因为"索引占空间"就把索引删掉,因为删掉之后查询会慢到不可用。


第三部分:算清显存这笔账

3.1 公式与它每一项的含义

这是本文最值得掌握的一个技能——能在纸上估出显存,才不会被"支持 128K 上下文"这种参数忽悠。

单个 token 的 KV Cache 体积:

每 token 字节数 = 2 × 层数 × KV 头数 × 每头维度 × 每个数的字节数
                 ↑
                 K 和 V 各存一份

每一项的来源:

项含义为什么在里面
2K 和 V两个张量各存一份
层数模型的 Transformer 层数每一层都有自己独立的 KV Cache
KV 头数参与缓存的注意力头数量注意这里是 KV 头数,不是查询头数(GQA 下两者不同)
每头维度每个注意力头的向量长度通常是 128
字节数存储精度fp16 是 2 字节,int8 是 1 字节

整个请求的总占用是:

单请求 KV Cache = 每 token 字节数 × 序列长度

3.2 实算:8B 级模型

取一个当前很主流的 8B 量级配置:32 层、KV 头 8 个、每头 128 维、fp16(2 字节)。

2 × 32 × 8 × 128 × 2 = 131,072 字节 ≈ 128 KB / token

128 KB 听起来不多。但乘上长度:

上下文长度单请求 KV Cache
4K约 0.5 GB
8K约 1 GB
32K约 4 GB
128K约 16 GB

再看一眼这个模型的权重占多少:8B × 2 字节 = 约 16 GB。

也就是说,在 128K 上下文下,单个请求的 KV Cache 体积,和整个模型的权重一样大。 而模型权重是全局共享的一份,KV Cache 却是每个并发请求一份。

这就是问题的本质所在。

3.3 实算:70B 级模型,以及一个更值得警惕的结论

换个大模型:80 层、KV 头 8 个、每头 128 维、fp16。

2 × 80 × 8 × 128 × 2 = 327,680 字节 ≈ 320 KB / token
上下文长度单请求 KV Cache
8K约 2.5 GB
32K约 10 GB
128K约 40 GB

这里有个容易被算错的地方值得说清楚:70B 模型在 fp16 下权重约 140 GB,40 GB 的缓存看起来还不到权重的三分之一。

但实际部署 70B 时,几乎都会用量化。如果是 INT4 量化,权重压到约 35 GB——这时候单个 128K 请求的 KV Cache(40 GB)就已经超过了模型权重本身。

这组数字揭示了一个现实的工程困境:你以为把模型量化到 4 bit 就能塞进单卡,结果发现真正吃显存的是缓存,不是权重。

3.4 并发上限怎么估

推理服务能同时服务多少请求,取决于显存预算怎么分。做一个完整的估算:

假设一张 80 GB 的卡,跑一个 8B 模型(fp16 权重 16 GB),框架开销预留 4 GB:

可用于 KV Cache 的显存 = 80 − 16 − 4 = 60 GB
  • 如果每个请求平均长度 4K(每个约 0.5 GB):60 / 0.5 = 120 个并发
  • 如果每个请求平均长度 32K(每个约 4 GB):60 / 4 = 15 个并发

输入长度从 4K 涨到 32K,并发能力掉了 8 倍。

这一行算术,比任何"我们支持超长上下文"的宣传都更能说明问题。长上下文的真实成本不在单价上,而在"同一张卡能同时服务多少人"上。


第四部分:为什么"越聊越慢"—— 三个叠加的效应

现在把机制串起来,就能完整解释开篇那个现象。它不是错觉,而是三个效应叠加的结果。

效应一:预填充越来越长 → 首字变慢

多轮对话里,你这一轮发过去的内容包含了全部历史对话。历史越长,预填充需要并行处理的 token 就越多。

更关键的是,这部分开销是累积的:第 1 轮预填充 500 token,第 10 轮可能预填充 5000 token。同样是问一句话,首字延迟可以差十倍。

这也解释了一个常见抱怨:"我问的问题很短,为什么要等这么久?"——因为你发的问题短,但你发过去的上下文很长。

效应二:显存被占满 → 并发能力下降

这是最容易被忽略的一环。

推理服务的吞吐,取决于"同一时刻能装下多少个请求的 KV Cache"。缓存把显存吃满之后,新请求只能排队——而排队在用户看来,就是"卡"。

注意这里的因果关系:不是服务器算不动,而是服务器装不下。

效应三:批处理被打散 → 出字速度下降

为了不让任何一个超长请求独占显存导致其他人饿死,调度器往往要做取舍:把它单独处理,或者切成小块逐步推进。

而解码阶段的效率高度依赖批大小——因为它是访存密集的,一批处理 32 个请求和处理 1 个请求,读权重的开销几乎一样。批越小,每次搬运的"性价比"越低。

所以长请求不仅自己慢,还会拖累同批次其他请求的出字速度。

小结这条因果链

历史变长
  ├─→ 预填充 token 变多 ──→ 首字延迟 TTFT 上升
  └─→ KV Cache 变大
        ├─→ 并发数下降 ──→ 排队 ──→ "卡"
        └─→ 调度被迫拆批 ──→ 批变小 ──→ 出字速度 TPOT 下降

这条链条比"服务器忙"这个解释有用得多,因为它每一环都对应一个可测量的指标,也对应一个可操作的优化点。


第五部分:五种优化手段,从原理到取舍

理解了病根,就能看懂每项技术到底在治什么病、代价是什么。

手段一:GQA / MQA —— 直接缩小每 token 的体积

治的病:缓存体积本身太大。

原理:传统多头注意力(MHA)里,每个 Query 头都配一个独立的 KV 头。GQA(Grouped-Query Attention)让若干个 Query 头共享一组 KV 头。

把 KV 头数从 32 降到 8,代入公式:

MHA:2 × 32 × 32 × 128 × 2 = 524 KB / token
GQA:2 × 32 ×  8 × 128 × 2 = 131 KB / token

缓存直接缩小到四分之一。

MQA 更激进:所有 Query 头共享唯一一组 KV,缓存再缩 8 倍(约 16 KB / token),但质量损失明显,没有成为主流。

取舍:GQA 的质量损失很小,是目前几乎所有主流开源模型的默认选择。它是唯一既能显著砍掉缓存、又几乎不损失质量的手段——这也解释了为什么长上下文模型都爱用它。

手段二:KV 量化 —— 把每个数从 2 字节压到 1 字节

治的病:同样的体积问题,换个角度解决。

原理:缓存里存的每个数值,从 fp16 换成 int8,体积直接减半;更激进的方案能压到 4 bit 甚至更低。

取舍:实现简单,收益确定。代价是精度损失——而长链推理任务对缓存精度最敏感(因为误差会沿着推理步骤累积)。所以量化缓存通常配合一个"保留部分 fp16 层"的混合策略。

什么时候用:显存紧张,且你的任务对精度损失不敏感(比如摘要、改写类任务)。

手段三:PagedAttention —— 治的是"碎片",不是"体积"

治的病:显存浪费,而不是显存占用大。这两个是完全不同的问题。

原理:传统实现要为每个请求预留一整块连续显存,并且按"最大可能长度"来分配。问题是绝大多数请求的实际长度远小于上限,于是大量空间被浪费——实测利用率常常不到 50%。

PagedAttention 借用了操作系统内存分页的思路:把 KV Cache 切成固定大小的块(block),按需分配、不要求连续。

于是:

  • 显存利用率从不足 50% 提升到 90% 以上;
  • 更重要的是,它让"按需增长"成为可能,不需要预先猜测长度。

取舍:这是 vLLM 出圈的核心技术。它不减少单个请求的缓存体积,但让同一张卡能装下更多请求。

一个常见误解:很多人以为 PagedAttention 是"压缩缓存"的技术。不是。它解决的是分配效率,和前面的 GQA、量化是两个维度的问题,可以叠加使用。

手段四:前缀缓存 —— 让相同的开头只算一次

治的病:预填充的重复计算,也就是首字延迟。

原理:如果多个请求共享同一段开头(比如同一套系统提示、同一个长文档),那么这段前缀的 KV Cache 是可以直接复用的——因为前缀的 K/V 只取决于前缀自身,与后面追加什么无关(和 2.2 节是同一个道理)。

效果:多轮对话场景下收益极大。因为第 N 轮的前缀,就是前 N-1 轮的完整内容——理论上最多可以省掉几乎全部的预填充计算。

取舍:需要额外的机制来识别和匹配前缀,实现复杂度不低。而且如果请求之间前缀差异大,命中率就低。

实操建议:把内容完全相同的部分(系统提示、工具定义、固定文档)严格放在最前面且保持字节级一致,能显著提高命中率。改造提示词顺序来提升缓存命中率,是性价比很高的优化。

手段五:滑动窗口与稀疏注意力 —— 主动丢弃远处的缓存

治的病:缓存无限增长。

原理:只保留最近一段窗口内的 K/V,更早的直接丢弃;或者只对部分历史位置做注意力计算。

取舍:这是主动的取舍,不是免费的午餐。省显存、能做超长文本,但代价是"远处的信息真的会看不到"。

适合的场景:对话、流式处理这类"近期信息更重要"的任务。 不适合的场景:需要在长文档中精确定位某一段的任务(比如"找出合同第 3 页的那条条款")。

五种手段的对照

手段治什么病影响哪个阶段主要代价
GQA / MQA缓存体积大解码(并发)轻微质量损失
KV 量化缓存体积大解码(并发)精度损失,长链推理敏感
PagedAttention显存碎片、利用率低解码(并发)实现复杂度
前缀缓存预填充重复计算预填充(首字)需要前缀匹配机制
滑动窗口 / 稀疏注意缓存无限增长两者远处信息丢失

注意前两个和第三个的区别:GQA 和量化是减小分子(每个请求要多少显存),PagedAttention 是减少浪费(同样显存装更多请求)。它们解决的是不同问题,可以而且应该同时用。


第六部分:什么时候 KV Cache 不划算

这一节容易被跳过,但它能帮你避免"技术正确、业务上白做"。

场景一:单次、无多轮的短任务。

比如批量分类、批量信息抽取。每条输入独立、长度都很短、请求之间没有共享前缀。这时缓存没有复用机会,反而增加了分配、回收、管理的内存开销。

场景二:极短输出。

如果每次只需要生成几个 token(比如一个分类标签、一个 yes/no 判断),那么缓存省下的重复计算非常有限,而管理成本照付。这种情况下,直接算可能更简单。

场景三:显存极度受限、且要高并发。

这时候"给缓存设上限、超了就丢弃或截断"可能比"什么都要完整缓存"换来更高的总吞吐。

一个反直觉的判断准则:

缓存是拿显存换时间。显存不缺时它是白赚的;显存紧张时,它本身就是瓶颈。

所以"关掉缓存省显存"是所有做法里最糟的一个——它把时间开销从线性推回平方级。要省显存,正确做法是减小缓存体积(GQA、量化)、限制长度,或改进调度,而不是砍掉缓存本身。


第七部分:线上变慢了,怎么定位

讲了这么多原理,最终要落回到"遇到问题怎么办"。下面是一套可以直接执行的流程。

第一步:先分清是哪种"慢"

问清楚(或者看监控确认):是首字慢,还是出字慢?

  • 首字慢(TTFT 高)→ 往预填充方向查。
  • 出字慢(TPOT 高)→ 往解码与批处理方向查。
  • 两个都慢 → 大概率是并发问题,往下走。

第二步:看三个数

指标看什么异常意味着
输入 token 数平均与 P95持续上涨 → 预填充问题、成本问题
单请求显存占用 / 显存利用率是否接近打满接近打满 → 并发受限于缓存
运行中的并发请求数实际值 vs 期望值远低于期望 → 排队严重

一个快速判据:如果输入 token 数明显在涨、显存接近打满,基本可以确定是缓存与调度问题;如果这两个都稳定、只有出字慢,才去怀疑模型或硬件。

第三步:按症状对应手段

症状大概率原因优先动作
首字越来越慢预填充变长前缀缓存、历史摘要、精简系统提示
出字速度变慢批变小 / 显存紧张上 PagedAttention、限并发、KV 量化
并发上不去KV Cache 占满显存换 GQA 模型、降上下文、扩卡
长文本问答不准有效上下文不足检索、分段处理、关键信息放两端
成本突然升高输入 token 累积历史摘要、语义缓存

一个真实的排查误区

很多团队的排查顺序是"先加卡"。但对上面这张表你会发现:加卡只对应"并发上不去"中的一部分情况,而且通常是成本最高、见效最慢的那个动作。

正确顺序应该是:

  1. 先看输入是不是可以变短(摘要、裁剪、前缀缓存)—— 零硬件成本;
  2. 再看调度是不是有优化空间(PagedAttention、批处理参数)—— 软件层面;
  3. 再看缓存本身能不能变小(量化);
  4. 最后才考虑加卡或换模型。

这个顺序能省下大量预算,而且不牺牲效果。


常见误区

误区一:"越聊越慢是因为模型记住了上下文,在变聪明。"

与聪明无关。长度增加导致的注意力计算量上升和缓存体积膨胀,是纯粹的工程代价,跟推理能力没有关系。事实上,上下文过长反而会让模型更容易"看漏"关键信息。

误区二:"标称 128K 上下文,就能塞 128K 内容进去正常用。"

这混了两件事:能不能塞进去是显存问题,能不能用好是模型能力问题。 显存允许你塞,不代表模型在那么长的位置还能准确取到信息。业界的实测普遍表明,"有效上下文"远小于"标称上下文",而且任务难度越高、有效长度越短。

误区三:"换个更快的显卡就能解决。"

解码阶段是访存密集的,瓶颈在显存带宽和缓存体积。算力翻倍,带宽没翻倍,出字速度就上不去。 对这个瓶颈,把缓存体积降下来的收益,远大于换卡。

误区四:"KV Cache 是可选的优化,关掉能省显存。"

关掉之后每步都要重算全部历史,等于把时间开销从线性推回平方级。要省显存,正确做法是减缓存体积、限长度或改调度,而不是砍掉缓存。

误区五:"并发上不去是模型太重。"

更常见的原因是 KV Cache 把显存吃满了。同样是这个模型,把平均上下文从 32K 降到 4K,并发能力可能提升 8 倍——模型没变,是缓存变了。

误区六:"首字延迟高是网络问题。"

网络通常只占几十到几百毫秒。如果首字要等好几秒,几乎一定是预填充在处理的输入太长。先去看输入 token 数,别急着抓包。

误区七:"批处理越大吞吐越高,所以应该把批开满。"

批太大有代价:一是显存被吃满后新请求排不上队,二是首字延迟会变差(新请求要等当前批次跑完才能插进去)。所以批大小的选择是"吞吐"和"延迟"之间的平衡,不是单向往大调。

误区八:"PagedAttention 能压缩 KV Cache。"

不压缩。它解决的是显存碎片与分配效率,让同样的显存装下更多请求。压缩体积要靠 GQA 和量化。 这两个概念经常被混淆,但在做容量规划时区别很大。


实战问答

Q1:面试被问"KV Cache 为什么能加速",怎么答得有层次?

A:按三层递进说。

第一层(根因):自回归生成里,历史 token 的 K/V 一旦算出就不再变化——因为注意力是因果的,追加新 token 不会改变前面位置的表示,所以天然可复用。

第二层(代价):不缓存的话,生成第 N 个 token 要重算 N 个位置,总计算量是 O(N²);缓存之后降到 O(N),代价是显存占用随长度线性增长。

第三层(推论):所以长上下文真正的瓶颈是显存而不是算力,这解释了为什么解码阶段是访存密集的、为什么 GQA 和量化是主流、为什么并发能力高度依赖上下文长度。

能说到第三层,就和背定义的人拉开了距离。


Q2:为什么同样的模型,别人家部署的并发是我的好几倍?

A:优先查这四项,通常答案就在其中。

  1. 平均上下文长度:这是最容易被忽略的。对方可能通过摘要、裁剪把平均输入压缩了一半,并发就直接翻倍。
  2. 是否用了 PagedAttention:显存利用率可能差出一倍。
  3. KV 头数是否相同:同样的模型家族,不同版本可能一个用 GQA、一个用 MHA,缓存体积差 4 倍。
  4. 批处理与调度策略:是否设置了合理的最大长度上限、是否有抢占机制。

这四项里没有一项是"硬件更好"。


Q3:多轮对话里,历史到底该保留多少?

A:没有统一答案,但有个可操作的判据:保留"后续对话还会用到的事实",丢掉"过程性内容"。

具体做法:把历史组织成三段结构——

  • 摘要:把前面对话压缩成要点(保留事实、结论、用户偏好);
  • 关键事实:结构化的键值对(比如"用户所在城市=上海""已确认的订单号=XXX");
  • 最近 N 轮原文:保持对话连贯性。

这通常比全量保留更省、也更准——因为过长的历史本身就会稀释注意力,让关键信息更难被取到。


Q4:怎么判断该上 GQA 还是该上量化?

A:看你的约束在哪。

  • 如果还能换模型:优先换 GQA 模型。质量损失更小,而且是"设计层面"的优化,不增加运行时开销。
  • 如果模型不能换:用量化。它是"运行时"的优化,可以直接作用在现有部署上。
  • 如果两者都能做:先 GQA 再量化,收益可以叠加(4 倍 × 2 倍)。

判断依据是"改动成本"而不是"哪个效果更好"——很多时候换模型的成本(重新评测、重新调提示词、验证下游兼容性)远高于它的收益。


Q5:前缀缓存命中率低怎么办?

A:先确认三件事。

  1. 前缀是否严格一致:哪怕多一个空格、换一个标点,都会导致缓存不命中。要做字节级比对,不要凭感觉。
  2. 内容顺序是否稳定:如果前缀里包含时间戳、随机 ID、动态排序的内容,缓存永远不会命中。把动态内容挪到前缀之后。
  3. 是否有长度门槛:多数实现会设置最小长度(比如只对超过一定 token 数的前缀启用缓存),太短的前缀不缓存。

最容易出问题的是第 2 条:一个放在系统提示开头的时间戳,能让整个前缀缓存机制彻底失效。


Q6:为什么长上下文模型普遍用 GQA,而不是更省的 MQA?

A:因为 MQA 压得太狠了。

MQA 让所有 Query 头共享唯一一组 KV(缓存约为 GQA 的 1/8),但多个 Query 头被迫在同一个"信息通道"里读取内容,表达能力的损失比较明显,实测质量下降肉眼可见,所以没有被主流采用。

GQA 的关键在于它是可调的折中:KV 头数可以从 1(等于 MQA)到与 Query 头数相同(等于 MHA)之间自由选择。实践中通常取 8 或更少——在缓存缩小 4 倍的同时,质量损失小到几乎测不出来。

这就是工程上的"最优解"长什么样:不是极值,而是曲线拐点。


Q7:单机跑不动了,是不是该上多卡?

A:先做完这四件事再考虑。

  1. 缩短平均输入:摘要、裁剪、前缀缓存。收益最直接,零硬件成本。
  2. 上 PagedAttention 类调度:把显存利用率从不到 50% 提到 90%。
  3. 限制最大长度与并发:设置合理的上限,防止单个长请求独占资源。
  4. KV 量化:缓存体积减半。

这四步做完,等效容量可能已经提升 3~5 倍。如果还不够,再上多卡。

而且要注意:多卡并行本身也有代价——跨卡通信开销、负载均衡、故障域扩大。这些是实打实的运维复杂度,不该被"加卡就能解决"这句话掩盖。


Q8:能不能预估一下我的服务需要多少显存?

A:可以,按这个顺序算。

第 1 步:权重显存 = 参数量 × 每参数字节数
        (fp16 是 2 字节,int8 是 1,int4 是 0.5)

第 2 步:单请求 KV Cache = 每 token 字节数 × 平均上下文长度
        (每 token 字节数 = 2 × 层数 × KV 头数 × 每头维度 × 字节数)

第 3 步:并发所需缓存 = 单请求缓存 × 目标并发数

第 4 步:总需求 = 权重 + 并发缓存 + 框架开销(通常预留 10%~20%)

用一个具体例子走一遍:8B 模型(fp16 权重 16 GB),平均上下文 8K(单请求约 1 GB),目标并发 50:

16 + 1 × 50 + 预留 4 ≈ 70 GB

这个算式最大的价值不是"算得准",而是让你看清哪个变量最敏感。 在上面这个例子里,把平均上下文从 8K 降到 4K,总需求立刻从 70 GB 降到 45 GB——比换一张更好的卡有用得多。


术语表

术语含义
预填充(Prefill)把整段输入一次性并行处理、生成第一个 token 的阶段;计算密集,决定首字延迟
解码(Decode)逐 token 串行生成的阶段;访存密集,决定出字速度
KV Cache缓存历史 token 的 Key 与 Value,避免每步重算;推理提速的基础机制
算术强度每搬运 1 字节数据能完成多少次运算;判断计算密集还是访存密集的核心概念
GQA / MQA让多个 Query 头共享少量 KV 头,直接缩小缓存体积
TTFT / TPOT首 token 延迟 / 每 token 耗时,分别反映预填充与解码的性能
PagedAttention把 KV Cache 按块分配,解决显存碎片;vLLM 的核心技术
前缀缓存复用相同开头部分的缓存,显著降低多轮与共享系统提示的延迟
滑动窗口注意力只保留最近一段的 K/V,主动丢弃远处信息以控制缓存增长
有效上下文模型在实际任务中真正能用好的长度,通常远小于标称上下文

结语

如果把这篇文章压缩成三句话:

第一,AI 生成文本的过程,天然会随对话变长而变慢——因为每生成一个字,都要和前面所有的字做一次交互。这是自回归机制的固有代价,不是实现缺陷。

第二,KV Cache 是把这个代价从平方级压到线性级的关键手段,它的原理很简单:历史 token 的 K/V 算完就不会再变,所以可以存下来复用。而它的代价同样简单:这些缓存必须一直待在显存里,并且随长度增长。

第三,理解了代价的形态,就能理解所有相关的工程问题——为什么长上下文贵在显存不在算力、为什么并发上不去、为什么换更好的卡效果有限、为什么 GQA 和量化是主流、为什么前缀缓存对多轮对话收益巨大。

这条线索的实用价值在于:它把"AI 慢"这个模糊的抱怨,拆成了一次可测量、可归因、可操作的排查。

而长上下文还有另一半问题没讲完:就算显存装得下、模型也算得动,"支持 100 万上下文"是否意味着它真的能在那么长的位置准确取到信息? 标称上下文与有效上下文之间的那条鸿沟,比大多数人想的要宽——这是下一个值得展开的话题。