vLLM系统拆解-02-Prefill、Decode与KV Cache:理解vLLM之前的推理基础

学 vLLM 时有一个常见的模式:先记住 PagedAttention、continuous batching、chunked prefill、prefix caching 这几个名词,然后在被追问"为什么需要这个"时答不出来。

原因通常不是对源码不熟悉,而是跳过了推理的基本物理图景。比如:

  • 为什么 chunked prefill 能改善延迟?
  • 为什么 decode 更怕显存带宽,prefill 更偏算力?
  • 为什么 KV Cache 的管理比模型权重管理复杂得多?

这些问题的答案,都来自对 Prefill、Decode 和 KV Cache 工作机制的理解。这篇文章就是建立这个基础。


从一次生成开始:时间线上发生了什么

假设用户发来一条请求:“请解释什么是 KV Cache,并举一个例子。”

这段 prompt 会被 tokenizer 切成若干 token(假设 100 个),然后模型开始生成回答。表面上看是一段文字不断冒出来,但底层分成两个阶段:

1
2
3
4
5
6
7
8
9
10
11
时间线
──────────────────────────────────────────────────────────────────►

╔══════════════════════════════╗ ╔════╗ ╔════╗ ╔════╗
║ ║ ║ ║ ║ ║ ║ ║ ...
║ Prefill ║ ║ D1 ║ ║ D2 ║ ║ D3 ║
║ (处理全部 100 个输入 token) ║ ║ ║ ║ ║ ║ ║
╚══════════════════════════════╝ ╚════╝ ╚════╝ ╚════╝
一次性处理,算力密集 每步生成一个 token,带宽密集

│← ─ ─ ─ ─ ─ ─ ─ 影响 TTFT ─ ─ ─ ─ ─ ─ ─►│← ─ 影响 ITL ─►
  • Prefill:把所有输入 token 一次性"吃进去",建立上下文,产出 KV Cache
  • Decode:在已有上下文基础上逐步生成新 token,每步只新增一个

这个划分不是随意的——两个阶段的计算特征、资源瓶颈和对用户体验的影响都截然不同。


Prefill:建立上下文,不是"生成"

Prefill 的本质是把用户已经给出的 prompt 转换成模型可持续使用的上下文状态。关键词是"建立上下文",而不是"输出内容"。

如果 prompt 有 100 个 token,Prefill 阶段要对这 100 个 token 做一遍完整的 Transformer 前向:每一层上每个 token 都要计算,完成之后模型才第一次知道"这个请求在说什么",然后才能生成第一个输出 token。

Prefill 的产物是什么

很多人以为 Prefill 只是产出了一个"隐藏状态"。从服务系统的角度看,Prefill 更重要的产物是:每一层、每个历史 token 对应的 KV Cache

这些 KV Cache 是 Decode 阶段得以高效运行的前提——如果没有它们,Decode 每生成一个新 token,都要把整段 prompt 重新算一遍。

为什么 Prefill 更偏 compute-bound

Prefill 一次性处理很多 token,GPU 上形成的矩阵运算规模较大,张量核心更容易充分利用。这个阶段的瓶颈通常是算力(compute-bound),而不是显存访问速度。

prompt 越长,Prefill 的计算量越大,这也是为什么超长 prompt 会明显拉高首 token 延迟(Time To First Token,TTFT)。


Decode:增量推理,每步只看"新增"

Decode 的本质是"增量推理"。有了 Prefill 建立的上下文之后,模型开始逐个生成新 token。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
当前已有历史: [T1][T2]...[T100]  ←  存储在 KV Cache 中

读取历史 K1..K100, V1..V100

新位置 T101: 只计算 Q101, K101, V101
用 Q101 做 attention(看所有历史 K)
按权重聚合历史 V → 输出 T101

把 K101, V101 追加写入 KV Cache

下一步 T102: 读取 K1..K101, V1..V101(多了一个)
只计算 Q102, K102, V102
...

关键点:Decode 不会把历史 token 重新计算,只计算当前新 token 的 Q/K/V,然后读取历史 KV Cache 做 attention。

为什么 Decode 更偏 memory-bound

正是因为每步只新增少量计算,但每步都要读取完整的历史 KV Cache:

  • 序列越长,每次需要读取的历史 K/V 越多
  • 请求越多,同时要维护和读取的 KV Cache 越庞大
  • 这个阶段的瓶颈往往不是算力,而是显存访问速度和带宽(memory-bound)

"每次只生成一个 token"听起来很轻,实际上不然。在线服务里不只有一个请求,多个请求同时在 Decode,GPU 的显存和带宽会被大量历史 KV Cache 持续占用。


Prefill 和 Decode 的完整对比

维度 Prefill Decode
任务目标 建立上下文,产出 KV Cache 利用上下文逐步生成新 token
输入形态 一次处理大量 token 每步只新增极少量 token(通常 1 个)
计算特征 大规模矩阵运算,compute-bound 小量新计算 + 大量历史读取,memory-bound
用户体验 决定 TTFT(首 token 何时出现) 决定 ITL(流式输出是否流畅)
系统设计压力 超长 prompt 会独占调度机会,拖慢其他请求 KV Cache 随请求数和长度放大,显存管理复杂

这五条一旦清楚,vLLM 里很多设计的动机就能直接推导出来(后文详细说明)。


KV Cache 是什么

理解 KV Cache 需要先回到 Transformer 的 attention 机制。

Attention 的基本结构

在每一层 attention 中,当前 token 会产生 Query(Q)、Key(K)、Value(V)三个向量:

  • Q:表达"我(当前 token)想关注什么"
  • K:历史每个位置的"标签",用于被 Q 检索
  • V:历史每个位置实际携带的信息

计算过程是:当前 token 的 Q 与历史所有位置的 K 做相似度计算,得到权重,再按权重聚合所有位置的 V。这样,当前 token 就"看到了"历史上下文。

为什么要缓存 K 和 V

Decode 阶段每生成一个新 token,都要做一次 attention——用新 token 的 Q 去和历史所有位置的 K 匹配。

如果不缓存,历史 token 的 K 和 V 就要每次重算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
不使用 KV Cache(prompt 1000 token,生成 100 个新 token):

生成 T1001: 重算历史 1000 个位置的 K/V → 计算量 ×1000
生成 T1002: 重算历史 1001 个位置的 K/V → 计算量 ×1001
生成 T1003: 重算历史 1002 个位置的 K/V → 计算量 ×1002
...
生成 T1100: 重算历史 1099 个位置的 K/V → 计算量 ×1099

累计重复计算:1000 + 1001 + ... + 1099 ≈ 105,000 份 K/V 重算
(其中绝大多数是完全相同的历史,被反复计算)

使用 KV Cache:

Prefill 后,1000 个历史 token 的 K/V 已存入 Cache。
生成 T1001: 读取 Cache(K/V×1000),只额外计算 K1001, V1001
生成 T1002: 读取 Cache(K/V×1001),只额外计算 K1002, V1002
...
每步只多算 1 份 K/V,其余全部复用。

这个对比说明 KV Cache 的意义:不是"快了一点点"的优化,而是把自回归生成从二次方重复计算降到近似线性的必要机制。

KV Cache 的工程定义

从系统角度,KV Cache 是:模型在 Decode 过程中为避免重复计算而持久保存的历史上下文表示,按层、按头、按 token 位置不断增长,并在每次 Decode 时被读取

五个关键属性:

属性 说明
按层存储 模型每一层都有自己的 K/V,不是只有最后一层
按 token 位置 每新增一个 token,就多一份 K/V
持续增长 生成越长,Cache 越大
高频读取 每次 Decode 都要读一遍全部历史
需要主动管理 不是算完存着就行,而是要分配、复用、回收,可能还要跨请求共享

最后一点是 KV Cache 成为系统设计核心的根本原因。


为什么 KV Cache 是在线推理真正难管的资源

权重是静态资产,KV Cache 是动态工作集

模型权重虽然体积大,但有一个关键特点:加载完之后相对稳定,所有请求共享同一份,不会因为某个请求多生成几百 token 就突然变化。

KV Cache 完全不同:

  • 每个请求有自己独立的一份
  • 长度随生成进度实时变化
  • 生命周期不一——有的请求已经跑完几百 token,有的刚开始
  • 请求结束后要释放,下一个请求还要重新分配
  • 两个有相同 system prompt 的请求,理论上可以共享前缀对应的 K/V

用一句话概括:权重像静态资产,KV Cache 像动态工作集。在线服务真正难管的是后者。

多请求并发如何放大 KV Cache 的复杂度

单请求时,管理一条序列的 Cache 还算直接。多请求并发时,同时面对的是:

  • 有的请求刚进入 Prefill,Cache 几乎是空的
  • 有的请求正在 Decode 第 30 个 token,Cache 已经有一定规模
  • 有的请求序列很长,Cache 占用大
  • 有的请求马上结束,准备释放
  • 有的请求与其他请求共享前缀,理论上可以复用 Cache
  • 有的请求因为显存不够需要暂停,其 Cache 可能要被换出

在这种状态下,KV Cache 不再只是一个"张量",而是一个必须被调度系统持续管理的资源池。

KV Cache 是吞吐的瓶颈

服务型推理想要高吞吐,核心是:同一时刻能让多少请求一起在 GPU 上高效前进。这个上限往往不取决于算力,而取决于:

  • 显存里能放下多少并发请求的 KV Cache
  • Cache 管理是否存在大量碎片
  • 有公共前缀的请求是否能共享 Cache
  • Prefill 和 Decode 的调度是否能高效协同

这就是 PagedAttention 作为 vLLM 核心创新被重点介绍的原因——它直接解决的是 KV Cache 的显存管理问题。


四个关键指标:TTFT、ITL、TPOT、Throughput

推理系统的性能不能用单一指标衡量,不同角色关注的维度不同。

指标 全称 含义 主要受什么影响
TTFT Time To First Token 从请求发出到第一个输出 token 出现的时间 输入处理时间 + 调度等待 + Prefill 执行时间
ITL Inter-Token Latency 相邻两个输出 token 之间的平均间隔 Decode 阶段的执行效率和调度稳定性
TPOT Time Per Output Token 平均每个输出 token 的生成耗时 与 ITL 接近,更常用于系统级统计表述
Throughput 单位时间内系统处理的 token 数或请求数 batch 大小、GPU 利用率、KV Cache 管理效率

从用户角度:TTFT 影响"点发送后多久开始出文字",ITL 影响"输出是否流畅"。从服务提供方角度:Throughput 直接关系到资源利用率和运营成本。

为什么这些指标会互相冲突

提高 Throughput 的常见做法是增大 batch——把更多请求塞进同一轮 GPU 执行。但 batch 变大意味着:

  • 某些请求等待进入 batch 的时间变长,TTFT 上升
  • 如果一个大 Prefill 任务独占了调度机会,正在 Decode 的请求要等更长时间才能被调度,ITL 变差

这三个维度之间存在真实的权衡(trade-off),不存在让三者同时最优的魔法设置。vLLM 的很多设计本质上都在做这种动态平衡。


从这套基础推导出 vLLM 的四个关键设计

理解了 Prefill、Decode 和 KV Cache,vLLM 的几个核心机制就不再是"神奇的优化 trick",而是直接从问题推导出来的解法:

vLLM 机制 解决的核心问题 与基础概念的关系
continuous batching 请求动态到达和结束,固定 batch 浪费 GPU Decode 是步进式的,不同请求处于不同阶段,需要持续调度让请求随时加入和退出
PagedAttention KV Cache 连续分配导致显存碎片严重 KV Cache 按 token 动态增长,连续大块分配会产生大量内部和外部碎片;按 block 分页管理可以大幅降低碎片
prefix caching 相同前缀的请求反复做 Prefill 浪费算力 Prefill 的核心产物是 KV Cache,相同前缀对应相同 K/V,可以跨请求复用
chunked prefill 超长 Prefill 独占调度,拖慢 Decode 的 ITL Prefill 是 compute-bound,Decode 是 memory-bound,两者瓶颈不同;把 Prefill 切成小块,与 Decode 交织执行,可以让调度更均衡

这四个设计的逻辑来自同一个底层认知:Prefill 和 Decode 是两类不同的计算任务,KV Cache 是连接它们的动态资源,而这个资源的管理质量直接决定系统的并发能力和延迟表现


推理系统本质上是资源调度问题

从算法层面,自回归生成逻辑很简单:输入 prompt,预测下一个 token,把新 token 接回去,循环。

从系统层面,真正复杂的是:

  • 什么时候让哪个请求先做 Prefill
  • 谁先进入 Decode,谁等待
  • token budget 怎么在不同请求之间分配
  • 哪些请求可以合进同一个 batch
  • 哪些 KV Cache 可以共享,哪些该回收
  • CPU 侧的调度逻辑能否跟得上 GPU 的执行节奏
  • 在 TTFT 和 Throughput 之间如何实时权衡

这些问题不属于模型算法范畴,而属于基础设施工程——更接近操作系统的资源调度,而不是深度学习的模型优化。

理解这一点,是从"知道 vLLM 有哪些功能"升级到"理解 vLLM 为什么这样设计"的关键转变。后续文章讲 scheduler、KV Cache Manager、PagedAttention 时,这个视角会持续有用。


参考资料

  1. vLLM Architecture Overview(官方文档)
  2. Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM 论文)
  3. vLLM Optimization and Tuning(官方文档)

系列导航