阶段二 · 补齐系统基础

Chapter 04 · 推理引擎核心链路:prefill、decode、sampling、KV Cache

如果上一章回答的是“资源和硬件怎样约束推理”,这一章回答的就是“请求在引擎内部怎样以状态机方式活着”。你会从系统角度看 prefill、decode、sampling 和 KV Cache,不再把它们当成互相孤立的术语。

时长 3-4 天
难度 L2 基础
请求生命周期 KV Cache 状态机视角

章节导读

这一章的重点不是再重复“prefill 是什么、decode 是什么”的定义,而是把这些概念放进同一条请求生命周期里,理解它们为什么会共同构成推理引擎的核心执行链路。后面所有调度、分页和吞吐优化都建立在这条链路之上。

这一章要解决的问题

请求从进入系统到完成释放,中间到底经历了哪些状态变化?

你要获得的能力

把 prefill、decode、sampling、KV Cache 放在同一张状态图里解释,而不是分散记忆。

为什么这章关键

如果这条链路没看懂,后面 batching、continuous batching 和 paged attention 都会像悬空名词。

本章目标

  • 把请求生命周期拆成可描述的系统状态,而不只是“有输入、有输出”。
  • 理解 KV Cache 为什么既是 decode 加速来源,也是显存压力核心来源。
  • 知道 sampling 不是附属功能,而是请求状态持续推进的一部分。
  • 为下一章的 batching、调度与分页缓存建立状态模型基础。

前置知识

必须知道

  • prefill 先处理已有上下文,decode 持续逐轮生成。
  • GPU、显存容量和带宽会约束执行行为。
  • 推理服务通常不是只处理单个请求,而是持续接收请求流。

建议知道

  • 缓存复用能减少重复计算。
  • 系统状态意味着某个请求会在内存里“占住东西”。
  • 生成参数会随着请求一起进入服务执行逻辑。

本章阅读建议

  • 尽量把每个概念都映射到“请求在什么时候、占用什么、输出什么”。
  • 不要把 KV Cache 当成普通业务缓存。
  • 先看状态,再看优化,不要顺序颠倒。

正文主线

1. 从“请求”而不是“模型”角度看系统

从模型视角看,推理像是一连串张量计算;从系统视角看,推理更像是一个活跃请求在引擎里不断推进状态。一个请求进入后,要先完成分词、建上下文、执行 prefill、进入 decode 循环、持续采样、直到满足停止条件才释放资源。也就是说,引擎管理的对象不是抽象公式,而是一批正在活着、占用资源的请求。

这层视角非常重要,因为后面所有调度问题都在围绕“多个活跃请求如何共存”展开。你要先看懂单个请求如何活着,才能看懂一群请求如何被安排。

2. Prefill、decode 与状态推进

prefill 和 decode 不是两个完全断开的世界,而是同一请求生命周期里的不同阶段。prefill 更像“建立初始状态”,decode 更像“沿着已有状态持续前进”。进入 prefill 时,请求还是“刚到系统的新任务”;进入 decode 后,它变成“带着上下文和缓存的活跃任务”。系统对它的管理方式也会随之变化。

这就是为什么很多推理框架内部会把请求分成不同的运行阶段,而不是把所有工作都塞进一个统一调度队列里。因为不同阶段的资源形态和执行特征不同。

3. KV Cache 为什么会成为引擎核心资产

如果 decode 每次都要从头重算全部历史,上下文越长,生成越慢,系统几乎无法承受。KV Cache 的意义就在于把历史注意力计算所需的 key / value 状态保存下来,后续每轮 decode 直接复用,从而避免大量重复计算。

但缓存不是白来的。每个活跃请求都要占据一定缓存空间,上下文越长、并发越高、输出越久,请求就越“重”。所以 KV Cache 同时带来两件事:decode 速度提升,以及显存占用和内存管理复杂度上升。

工程判断

如果你看到长上下文 + 高并发场景,第一反应就应该想到缓存管理,而不是只看模型权重大小。

4. Sampling 为什么是状态机的一部分

每一轮 decode 之后,模型给出下一个 token 的分布;sampling 决定到底拿哪个 token 进入下一轮。被选中的 token 不只是“输出给用户”,它也会回到请求的历史状态里,决定后续上下文如何继续扩展。也就是说,sampling 直接参与状态推进。

这也是为什么服务接口必须把 temperature、top-k、top-p 等参数与请求一起携带。它们不是事后修饰,而是请求执行语义的一部分。

5. 从状态机视角提前看后面的调度问题

当多个请求同时存在时,系统要决定哪些请求先做 prefill,哪些请求继续 decode,哪些请求因为停止条件已满足而应立即释放缓存,哪些请求还在等待下一轮 token。你会发现:调度并不是“排列任务顺序”这么简单,而是在管理一批随时会变化状态的活跃对象。

这就是为什么下一章谈 batching 和 continuous batching 时,你需要先有“请求状态会变化”的意识。没有这层意识,调度策略只会显得像一些离散技巧,而不是完整系统设计。

6. 请求结束时,系统真正要回收什么

一个请求结束并不只是“把答案返回给用户”。系统还要处理停止原因、记录统计信息、释放或复用缓存页、更新调度器中的活跃状态,并决定是否把资源立刻让给其他请求。这个“收尾阶段”做得是否干净,会直接影响高并发下的稳定性。

也正因为如此,工程里经常会区分“请求逻辑已经完成”和“底层资源已经完全回收”。如果你忽略了第二层,很多显存抖动和状态残留问题会很难解释。

工程判断

看请求对象,而不是只看模型函数

当你排查推理流程问题时,优先关注请求状态对象里存了什么、什么时候变化,而不是只盯着一次前向计算函数。

  • 请求对象决定生命周期怎么推进。
  • 很多引擎问题都藏在状态流转里。
  • 这是后面读源码最稳的入口之一。

缓存问题先看生命周期,再看容量

很多缓存异常不只是“空间不够”,而是某些请求活得太久、释放太晚、上下文扩张太快。

  • 先看请求何时进入、退出和回收。
  • 再看长上下文与长输出是否叠加。
  • 最后再决定是否需要量化或分页管理。

停止条件是系统语义,不是 UI 细节

用户停止、达到 `max_tokens`、命中终止符,这些事件都会改变请求状态机和资源回收时机。

  • 它们必须进入引擎逻辑。
  • 日志与指标也应记录停止原因。
  • 否则后面定位请求异常结束会很吃力。

场景拆解

现象 缓存 动作

长对话会话越来越重,后面的响应越来越慢

这是典型的活跃请求历史持续膨胀问题。随着上下文积累,prefill 和缓存占用都会增加,请求会在系统里变得越来越“重”。

  • 先看会话历史有没有被裁剪或总结。
  • 再看 KV Cache 占用是否持续增长。
  • 这类问题本质上是状态对象越来越大。
现象 停止条件 动作

用户中途取消请求后,显存占用没有立刻回落

如果取消事件只停在接口层,而没有把请求状态完整传到引擎内部,就可能出现“用户看不到了,但资源还占着”的问题。

  • 检查取消是否进入调度器和缓存管理层。
  • 确认请求是否真的从活跃集合中移除。
  • 这类问题经常暴露系统收尾逻辑的缺口。

图解实验室

这一章最容易误解的地方,是把请求当成“一次算完”的任务。实际上它在系统里是一连串状态推进。

请求生命周期状态图

先进入系统、建立上下文,再进入生成循环,最后满足停止条件后才真正释放资源。

进入请求 服务入口接收 prompt 与生成参数。
Tokenize 文本变成 token 序列,形成可执行输入。
Prefill 处理已有上下文,建立初始状态和缓存。
活跃请求 进入可持续推进的 decode 阶段。
生成循环 只要还没命中停止条件,请求就会在 decode 与 sampling 之间持续往前推进。
decode sampling 更新历史上下文 下一轮 decode
停止条件 命中终止符、达到 `max_tokens` 或用户取消。
回收资源 释放 KV Cache、更新状态、记录请求结果与停止原因。

关键表格与结论

阶段 / 对象 系统在维护什么 最敏感的资源或问题 后续会影响什么
Prefill 初始上下文状态、首轮执行结果 输入长度、首 token latency 请求能否顺利进入 decode
Decode 活跃请求的逐轮推进状态 输出长度、调度策略、吞吐 总 latency 与设备利用率
KV Cache 历史上下文的可复用状态 显存容量、带宽、内存管理 并发能力与缓存布局设计
Sampling 每轮 token 选择逻辑 生成控制参数、请求语义 输出风格与请求状态推进

结论 1

KV Cache 不是简单“提高一点速度”,而是让 decode 变得可承受的根基,同时也是显存管理的核心负担。

结论 2

请求生命周期的本质是状态推进。后面学调度,本质上是在学“如何管理一批会动态变化的状态”。

动手任务

任务 1:画一张“请求生命周期状态图”

至少包含入队、prefill、中间活跃、decode 循环、完成释放五个状态。最低完成标准是:你能说清楚每个状态主要占用什么资源。

任务 2:写一段 100 字以内的 KV Cache 解释

要求同时提到“减少重复计算”和“增加显存压力”。最低完成标准是:两点都不能漏。

任务 3:列出三种会显著拉高缓存压力的场景

例如长上下文、高并发、长输出。最低完成标准是:每种场景都要解释它为什么会增加状态占用。

任务 4:解释 sampling 为什么属于请求执行语义的一部分

最低完成标准是:你的答案里要出现“它决定下一轮 token 如何进入历史上下文”。

阶段产出

产出 1

一张请求生命周期状态图。

产出 2

一段你自己的 KV Cache 解释。

产出 3

一份缓存压力来源清单。

自测问题

  1. 1. 为什么说推理引擎管理的是“活跃请求”,而不只是“模型计算”?

    如果你能把答案说到状态和资源占用,说明视角已经切换过来。

  2. 2. KV Cache 为什么既是收益来源,也是压力来源?

    如果只能说出一面,说明理解还不完整。

  3. 3. sampling 为什么会进入服务接口和请求状态,而不是只留给产品层?

    如果你能把它和 decode 的循环联系起来,说明理解到位。

  4. 4. 后面谈 batching 时,为什么必须先有请求状态机意识?

    因为 batching 不是在操作静态任务,而是在操作一批状态不同的活跃请求。

推荐资料

源码入口

  • Source vllm-project/vllm

    适合后面沿着缓存、调度和 serving 路径找关键对象。

  • llama.cpp server README

    适合观察一个更轻量的服务实现如何暴露生成、采样和接口能力。

实践建议

  • Practice sgl-project/sglang

    适合在后续章节里对比另一路高性能 serving 设计时使用。

常见误区

误区 1:把 KV Cache 当成普通命中缓存

它不是“查得到就省一次调用”的业务缓存,而是和每个活跃请求绑定的执行状态。

误区 2:觉得 sampling 只是“输出更随机一点”的 UI 参数

实际上它参与每一轮 token 选择,会改变请求的推进路径。

误区 3:没把 prefill 和 decode 放在一条生命周期里理解

这样后面再看调度和 batching 时,很容易把引擎行为误解成零散技巧。

下一章衔接

现在你已经把单个请求的生命周期看清楚了。下一章开始处理“多个活跃请求怎么共存”的问题:什么时候该组 batch,为什么 continuous batching 会提升吞吐,以及 paged attention 为什么能缓解缓存管理压力。