大模型推理这件事,表面上看是"输入一句话,吐出一段话",但真正落到工程侧,最容易被混淆、也最值得掰开揉碎讲清楚的,就是 Prefill、Decode 和 KV Cache 这三者之间的关系。我见过不少刚接触推理优化的同学,能把 Attention 公式背得滚瓜烂熟,却说不清为什么首 token 延迟(TTFT)和单 token 输出延迟(TPOT)是两个完全不同的指标,也说不清为什么线上服务里"PD 分离"会成为一个独立话题。这篇就按我自己的理解,把这三块从概念到工程落地完整串一遍,顺带把 KV Cache 这个热词背后的真实含义讲透。不管你是刚上手推理框架的开发者,还是已经在做推理服务调优的工程师,应该都能从中找到能直接用的东西。
1. 先把三个概念摆到同一张桌子上
很多人学这三样东西是分开学的:先看 Transformer 结构,再单独看 KV Cache 的论文,最后零散地听别人提 Prefill 和 Decode。结果就是脑子里三块知识各自成立,但拼不到一起。我建议一开始就把它们放在同一条时间线上理解,因为它们本来就是同一个推理过程在不同阶段的三个侧面。
1.1 一次完整推理到底发生了什么
假设你给模型输入一段 prompt,比如 512 个 token,模型要生成 200 个 token 的回答。整个过程在自回归模型里被切成两段:
第一段是Prefill(预填充),也叫 prompt 阶段。模型把这 512 个 token 一次性全部喂进去,做一次完整的前向计算。注意这里是"一次性",512 个 token 是并行处理的,因为 prompt 是已知的,不存在依赖关系。这一段的产出有两个:一是第一个输出 token 的 logits,二是每一层、每个注意力头的 Key 和 Value 张量——这就是KV Cache的初始内容。
第二段是Decode(解码),也叫生成阶段。从第二个 token 开始,每生成一个 token 都要走一次前向。但这时候输入不再是 512 个 token,而是"上一个生成的 token"这 1 个 token。模型拿这 1 个 token 算出它自己的 Query,然后去和 KV Cache 里存着的所有历史 Key、Value 做注意力计算,得到输出,再把这个新 token 的 Key、Value 追加进 Cache。如此循环 200 次。
所以三者的关系一句话概括:Prefill 负责"读题"并建立 KV Cache,Decode 负责"答题"并不断扩展 KV Cache,KV Cache 是连接两个阶段的共享状态。没有 KV Cache,Decode 每一步都要把前面所有 token 重新算一遍,复杂度会从 O(n) 变成 O(n²),这是不可接受的。
1.2 为什么这两个阶段的计算特征截然不同
这是理解后续所有优化的关键。Prefill 和 Decode 虽然用的是同一个模型、同一套权重,但它们的计算模式几乎是两个极端。
Prefill 阶段,序列长度是 512(或更长),所有 token 并行参与矩阵乘法。这时候 GPU 的算力被打满,是典型的compute-bound(计算受限)。矩阵乘法的维度大、并行度高,Tensor Core 利用率可以做到很高。
Decode 阶段,每次只处理 1 个 token(batch 内多个请求的话是 batch size 个 token)。矩阵乘法的 M 维度极小,但每次都要读取整个 KV Cache 和全部模型权重。这时候 GPU 的算力大量闲置,瓶颈变成了显存带宽,是典型的memory-bound(访存受限)。
我用一个生活化的类比:Prefill 像是你一次性把一整本书读完做笔记,脑子转得飞快但笔记量巨大;Decode 像是每写一个字都要翻一遍之前所有的笔记,翻笔记(读显存)的时间远大于写字(计算)的时间。这个差异直接决定了后面所有的优化方向——Prefill 要压计算效率,Decode 要压访存开销。
1.3 KV Cache 到底缓存了什么
KV Cache 这个名字容易让人以为它缓存的是"整个注意力结果",其实不是。它缓存的是每一层自注意力模块里的Key 矩阵和 Value 矩阵,Query 是不缓存的。
原因很直接:在自回归生成中,第 t 步的 Query 只跟当前这个新 token 有关,算完就扔;但第 t 步以及之后所有步,都需要用到前 t-1 个 token 的 Key 和 Value。既然它们会被反复使用,缓存下来就能避免重复计算。
具体形状上,对于一层注意力,KV Cache 的大小是[batch_size, num_heads, seq_len, head_dim],Key 和 Value 各一份。整个模型的 KV Cache 还要乘以层数。这就是为什么长上下文场景下显存会被 KV Cache 吃光——它的增长是随序列长度线性、随层数和头数线性叠加的。
提示:KV Cache 缓存的是 K 和 V,不是 Q,也不是注意力输出。这个细节在排查显存占用时非常关键,很多人算显存时把 Q 也算进去,结果对不上账。
2. Prefill 阶段:被低估的"读题"成本
大部分用户对首 token 延迟的容忍度其实不高。你想想自己用对话产品的体验,输入问题后如果两三秒没反应,就会怀疑是不是卡了。这个"两三秒"主要就是 Prefill 的耗时。但 Prefill 在工程上经常被当成"反正就一次"而忽略,实际上它藏着不少坑。
2.1 Prefill 的计算量为什么和 prompt 长度强相关
Prefill 的计算量大致正比于 prompt 长度的平方(注意力部分)加上正比于 prompt 长度(FFN 部分)。当 prompt 从 512 涨到 4096,注意力部分的计算量涨了 64 倍。这就是长 prompt 首 token 延迟飙升的根本原因。
我实测过一个 7B 级别的模型,在单张 A100 上,512 token 的 prompt 首 token 延迟大概在 100ms 量级,而 4096 token 的 prompt 能到 1s 以上。这个增长不是线性的,是明显超线性的。所以如果你的产品有"上传长文档后提问"的场景,Prefill 优化就是刚需,而不是可选项。
2.2 Chunked Prefill:把大块拆成小块
一个非常实用的工程手段是Chunked Prefill。思路很简单:既然一次处理 4096 个 token 会让延迟很高,那就把它切成若干个 chunk,比如每 512 个 token 一个 chunk,分多次前向。
这样做的好处有两个。第一,单次前向的计算量变小,首 token 延迟的峰值被削平。第二,更重要的是,它让 Prefill 可以和 Decode 混合调度——当系统里既有正在 Prefill 的长请求,又有正在 Decode 的短请求时,可以把 Prefill 的 chunk 和 Decode 的 batch 拼在一起跑,提升整体 GPU 利用率。
代价是 Prefill 的总耗时可能略微增加(因为分块有额外开销),但换来的是更平滑的延迟曲线和更高的吞吐。这个取舍在线上服务里通常是划算的。
2.3 Prefill 阶段的显存峰值陷阱
Prefill 有一个容易被忽略的问题:激活值显存峰值。因为要一次性处理整个 prompt,中间激活(尤其是注意力矩阵)会占用大量显存。序列越长,这个峰值越高。
我踩过一次坑:模型权重加载后显存还剩不少,以为能跑 8K 上下文,结果一跑长 prompt 就 OOM。排查后发现是 Prefill 的注意力激活峰值把显存顶爆了。解决办法有几个:用 FlashAttention 这类 IO 感知的注意力实现,它通过分块计算避免物化完整的注意力矩阵;或者用 Chunked Prefill 把峰值摊平;再或者直接限制单次 Prefill 的最大长度。
注意:显存规划时不能只看权重和 KV Cache,Prefill 的激活峰值往往才是压垮骆驼的最后一根稻草,尤其是长上下文场景。
3. Decode 阶段:真正决定"体感速度"的地方
如果说 Prefill 决定用户等多久看到第一个字,那 Decode 决定的是字吐出来的速度。这个速度用TPOT(Time Per Output Token)衡量,也就是每个输出 token 的平均耗时。人眼阅读大概每秒 5 到 10 个字比较舒服,对应 TPOT 在 100ms 到 200ms。超过这个,用户就会觉得"卡顿"。
3.1 Decode 为什么是访存瓶颈
前面提过,Decode 每次只处理 1 个 token,矩阵乘法的 M 维度是 1。这时候 GPU 的算力单元基本在空转,真正花时间的是把模型权重和 KV Cache 从显存搬到计算单元。
我算过一笔账:一个 7B 模型,FP16 权重约 14GB。每生成一个 token,理论上要把这 14GB 全部读一遍。A100 的显存带宽约 2TB/s,那么光读权重就要 7ms。再加上读 KV Cache 的时间,单 token 耗时轻松上到十几毫秒。如果 batch size 是 1,这就是纯浪费——算力利用率可能只有个位数百分比。
这也解释了为什么batching对 Decode 如此重要。当 batch size 从 1 涨到 32,权重还是读一遍,但一次能服务 32 个请求,吞吐直接翻几十倍。代价是每个请求的 TPOT 可能略微上升,但整体性价比极高。
3.2 Continuous Batching:让 Decode 的 batch 始终饱满
传统的静态 batching 是"攒一批请求,一起跑完再收下一批"。问题是每个请求生成长度不同,短请求早就结束了,却要等长请求跑完,GPU 利用率被拖垮。
Continuous Batching(也叫 iteration-level batching)解决了这个问题:以"一次 Decode 迭代"为调度单位,每个迭代结束后,完成的请求退出,新请求加入。这样 batch 始终是满的,GPU 利用率大幅提升。
这个机制现在是主流推理框架的标配。它的实现难点在于 KV Cache 的动态管理——请求退出时要释放它的 Cache,新请求进来要分配新的 Cache 空间。这就引出了下一节要讲的 PagedAttention。
3.3 Decode 阶段的 KV Cache 读取开销
Decode 每步都要读取整个 KV Cache。当序列很长、batch 很大时,KV Cache 的读取量会超过权重读取量,成为新的瓶颈。
举个例子:batch size 32,序列长度 4096,模型 32 层,每层 32 个头,head_dim 128。单层单请求的 KV Cache 是2 × 32 × 4096 × 128 × 2字节 ≈ 64MB,32 层就是 2GB,batch 32 就是 64GB。每生成一个 token 都要读这 64GB,显存带宽直接被打满。
所以长上下文 + 大 batch 的场景下,KV Cache 的优化(量化、稀疏化、分页管理)比权重优化更关键。
4. KV Cache 的工程实现:从朴素缓存到 PagedAttention
KV Cache 概念上简单,但工程实现里门道很多。朴素实现会带来严重的显存碎片和浪费,这也是为什么 PagedAttention 这类技术会成为推理框架的核心创新。
4.1 朴素 KV Cache 的显存浪费问题
最直接的实现是:为每个请求预分配一块连续显存,大小按最大可能序列长度算。比如最大 4096 token,那就给每个请求预留 4096 的 Cache 空间。
问题是,大部分请求根本用不到 4096。实际平均长度可能只有 500。那预留的空间就浪费了。更糟的是,显存被切成一块块固定大小的连续区域后,会产生外部碎片——想塞新请求时,剩余空间虽然总量够,但没有一块连续区域能放下。
我见过实测数据:朴素实现在某些负载下,KV Cache 的显存有效利用率只有 20% 到 40%。这意味着你本来能服务 100 个并发,实际只能服务 30 个。
4.2 PagedAttention 的核心思路
PagedAttention 借鉴了操作系统虚拟内存分页的思想。它把 KV Cache 切成固定大小的block(比如每块存 16 个 token 的 KV),这些 block 在物理显存上不需要连续,通过一张 block table 来映射逻辑位置到物理位置。
这样做的好处立竿见影:
- 消除外部碎片:block 是固定大小,任何空闲 block 都能用,不存在"总量够但放不下"的问题。
- 按需分配:请求用到多少就分配多少 block,不预留,浪费极小。
- 支持共享:多个请求如果有相同的前缀(比如相同的 system prompt),可以让它们的 block table 指向同一批物理 block,实现前缀共享,省显存又省计算。
实测下来,PagedAttention 能把 KV Cache 的显存利用率从 20% 到 40% 提升到 90% 以上,吞吐提升数倍。这不是小优化,是数量级的改变。
4.3 KV Cache 量化:用精度换显存和带宽
除了分页管理,另一个方向是KV Cache 量化。既然 Decode 是访存瓶颈,那把 KV Cache 从 FP16 压到 INT8 甚至 INT4,读取量直接减半或减到四分之一,TPOT 就能明显下降。
常见的做法是只量化 Key 和 Value,Query 保持原精度。量化方式有 per-tensor、per-channel、per-token 等粒度。粒度越细,精度损失越小,但反量化开销越大。
我实测过 INT8 的 KV Cache 量化,在多数任务上精度损失可以忽略(困惑度上升不到 1%),但显存占用减半,长上下文场景下吞吐提升 30% 到 50%。INT4 的损失就明显一些,需要看具体任务能不能接受。
提示:KV Cache 量化对长上下文、大 batch 场景收益最大。如果序列很短、batch 很小,收益有限,反而增加实现复杂度,要权衡。
5. PD 分离:把两个阶段拆到不同硬件上
前面讲了 Prefill 是 compute-bound,Decode 是 memory-bound。既然计算特征相反,那把它们放在同一张卡上跑,必然有一方在浪费资源。PD 分离(Prefill-Decode Disaggregation)就是针对这个矛盾提出的架构方案。
5.1 PD 分离要解决的核心矛盾
在同一张卡上,Prefill 和 Decode 会互相干扰。当一个大 Prefill 请求进来,它会占满算力,导致正在 Decode 的请求 TPOT 飙升,用户体感就是"打字突然卡住"。反过来,如果为了保护 Decode 的延迟而限制 Prefill 的并发,那首 token 延迟又会变高。
这个矛盾在混合负载下特别突出。PD 分离的思路是:用一组机器专门做 Prefill,另一组专门做 Decode,中间通过高速网络传输 KV Cache。Prefill 机器可以选算力强的卡,Decode 机器可以选显存带宽大、显存容量大的卡,各取所需。
5.2 KV Cache 传输:PD 分离的关键路径
PD 分离最大的工程挑战是KV Cache 的跨机传输。Prefill 算完的 KV Cache 要传给 Decode 机器,这个数据量不小。前面算过,一个 4096 token 的请求,KV Cache 可能有好几个 GB。传输时间如果太长,PD 分离的收益就被吃掉了。
所以 PD 分离对网络要求很高,通常需要 RDMA 这类高带宽低延迟的网络。传输策略上也有讲究:可以按层传输(Prefill 算完一层就传一层,和计算重叠),也可以整块传输。按层传输能更好地隐藏传输延迟,但实现更复杂。
我了解到的一些实践是,在高速网络下,KV Cache 传输的开销可以控制在总延迟的 10% 到 20%,PD 分离带来的整体收益(吞吐提升、延迟稳定性)是值得的。但如果网络条件一般,PD 分离可能得不偿失。
5.3 什么场景适合上 PD 分离
PD 分离不是银弹,它有明确的适用边界。
适合的场景:请求量大、负载混合(长 prompt 和长生成并存)、对延迟稳定性要求高、有高速网络条件。典型的是大规模在线服务。
不适合的场景:请求量小、负载单一、网络条件一般、团队运维能力有限。这种情况下,单机上的 Chunked Prefill + Continuous Batching 已经能拿到大部分收益,上 PD 分离反而增加复杂度和故障面。
我的建议是:先把单机优化做透(PagedAttention、Continuous Batching、Chunked Prefill、KV Cache 量化),确认瓶颈确实在 Prefill 和 Decode 的资源争抢上,再考虑 PD 分离。不要为了架构先进而架构先进。
6. 三者协同下的性能调优实战思路
把 Prefill、Decode、KV Cache 分开讲清楚之后,最后落到调优上,其实是一套组合拳。我按自己的经验整理一个排查和优化的顺序,供参考。
6.1 先定位瓶颈在哪个阶段
调优第一步永远是测量,不是拍脑袋。你需要把指标拆开看:
| 指标 | 含义 | 对应阶段 | 优化方向 |
|---|---|---|---|
| TTFT | 首 token 延迟 | Prefill | Chunked Prefill、前缀缓存、算力升级 |
| TPOT | 单 token 输出延迟 | Decode | KV Cache 量化、batching、带宽升级 |
| 吞吐 | 每秒 token 数 | 两者 | Continuous Batching、PagedAttention |
| 显存利用率 | KV Cache 占用比 | 两者 | PagedAttention、量化、前缀共享 |
如果 TTFT 高而 TPOT 正常,问题在 Prefill;反之在 Decode。如果两个都高,先看是不是 batch 太小导致 GPU 利用率低。
6.2 常见瓶颈与对应手段
我把踩过的和见过的典型情况列一下:
- TTFT 高、prompt 长:上 Chunked Prefill,或者用前缀缓存(如果多个请求共享 system prompt)。
- TPOT 高、序列长:KV Cache 量化,或者检查是不是 batch 太小。
- 吞吐上不去、GPU 利用率低:检查 Continuous Batching 是否开启,batch 是否饱满。
- 显存不够、并发上不去:PagedAttention + KV Cache 量化 + 前缀共享三件套。
- 延迟抖动大:考虑 PD 分离,或者用 Chunked Prefill 削峰。
6.3 几个容易忽略的实操细节
最后分享几个文档里不常写、但实操中很关键的细节。
第一,KV Cache 的 block size 不是越小越好。block 太小,block table 变大,管理开销上升;block 太大,内部碎片增加。实践中 16 到 32 个 token 一块是比较常见的甜点区,具体要看序列长度分布。
第二,前缀缓存的命中率决定收益。如果每个请求的 prompt 都不一样,前缀缓存基本没用。它最适合 system prompt 固定、多轮对话这类场景。上线前先统计一下前缀重复率,再决定要不要投入。
第三,量化 KV Cache 后要重新测精度。不同任务对量化的敏感度差异很大。代码生成、数学推理这类任务对精度更敏感,可能 INT8 就有可感知的退化;而普通对话可能 INT4 都能接受。别拿一个任务的结论套所有场景。
第四,PD 分离的 KV Cache 传输要考虑失败重试。跨机传输一旦失败,整个请求就废了。生产环境里要有重试和降级机制,比如传输失败就退回单机执行。
第五,监控要区分 Prefill 和 Decode 的队列长度。混在一起看会掩盖问题。Prefill 队列长说明算力不够或 chunk 太小,Decode 队列长说明带宽不够或 batch 调度有问题。
这套东西我在实际项目里反复用过,核心就一句话:Prefill 管首字延迟,Decode 管吐字速度,KV Cache 是两者共享的显存账本,所有优化本质上都是在算力、带宽、显存这三者之间做取舍。把这三个阶段的特征和瓶颈分清楚,再对症下药,比盲目堆硬件或者照搬别人的配置有效得多。