vLLM系统拆解-02-Prefill、Decode与KV Cache:理解vLLM之前的推理基础
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 | 时间线 |
- 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 | 当前已有历史: [T1][T2]...[T100] ← 存储在 KV Cache 中 |
关键点: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 | 不使用 KV Cache(prompt 1000 token,生成 100 个新 token): |
这个对比说明 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 时,这个视角会持续有用。
参考资料
- vLLM Architecture Overview(官方文档)
- Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM 论文)
- vLLM Optimization and Tuning(官方文档)

