Chapter 04 · 推理引擎核心链路:prefill、decode、sampling、KV Cache
如果上一章回答的是“资源和硬件怎样约束推理”,这一章回答的就是“请求在引擎内部怎样以状态机方式活着”。你会从系统角度看 prefill、decode、sampling 和 KV Cache,不再把它们当成互相孤立的术语。
这一章的重点不是再重复“prefill 是什么、decode 是什么”的定义,而是把这些概念放进同一条请求生命周期里,理解它们为什么会共同构成推理引擎的核心执行链路。后面所有调度、分页和吞吐优化都建立在这条链路之上。
请求从进入系统到完成释放,中间到底经历了哪些状态变化?
把 prefill、decode、sampling、KV Cache 放在同一张状态图里解释,而不是分散记忆。
如果这条链路没看懂,后面 batching、continuous batching 和 paged attention 都会像悬空名词。
从模型视角看,推理像是一连串张量计算;从系统视角看,推理更像是一个活跃请求在引擎里不断推进状态。一个请求进入后,要先完成分词、建上下文、执行 prefill、进入 decode 循环、持续采样、直到满足停止条件才释放资源。也就是说,引擎管理的对象不是抽象公式,而是一批正在活着、占用资源的请求。
这层视角非常重要,因为后面所有调度问题都在围绕“多个活跃请求如何共存”展开。你要先看懂单个请求如何活着,才能看懂一群请求如何被安排。
prefill 和 decode 不是两个完全断开的世界,而是同一请求生命周期里的不同阶段。prefill 更像“建立初始状态”,decode 更像“沿着已有状态持续前进”。进入 prefill 时,请求还是“刚到系统的新任务”;进入 decode 后,它变成“带着上下文和缓存的活跃任务”。系统对它的管理方式也会随之变化。
这就是为什么很多推理框架内部会把请求分成不同的运行阶段,而不是把所有工作都塞进一个统一调度队列里。因为不同阶段的资源形态和执行特征不同。
如果 decode 每次都要从头重算全部历史,上下文越长,生成越慢,系统几乎无法承受。KV Cache 的意义就在于把历史注意力计算所需的 key / value 状态保存下来,后续每轮 decode 直接复用,从而避免大量重复计算。
但缓存不是白来的。每个活跃请求都要占据一定缓存空间,上下文越长、并发越高、输出越久,请求就越“重”。所以 KV Cache 同时带来两件事:decode 速度提升,以及显存占用和内存管理复杂度上升。
如果你看到长上下文 + 高并发场景,第一反应就应该想到缓存管理,而不是只看模型权重大小。
每一轮 decode 之后,模型给出下一个 token 的分布;sampling 决定到底拿哪个 token 进入下一轮。被选中的 token 不只是“输出给用户”,它也会回到请求的历史状态里,决定后续上下文如何继续扩展。也就是说,sampling 直接参与状态推进。
这也是为什么服务接口必须把 temperature、top-k、top-p 等参数与请求一起携带。它们不是事后修饰,而是请求执行语义的一部分。
当多个请求同时存在时,系统要决定哪些请求先做 prefill,哪些请求继续 decode,哪些请求因为停止条件已满足而应立即释放缓存,哪些请求还在等待下一轮 token。你会发现:调度并不是“排列任务顺序”这么简单,而是在管理一批随时会变化状态的活跃对象。
这就是为什么下一章谈 batching 和 continuous batching 时,你需要先有“请求状态会变化”的意识。没有这层意识,调度策略只会显得像一些离散技巧,而不是完整系统设计。
一个请求结束并不只是“把答案返回给用户”。系统还要处理停止原因、记录统计信息、释放或复用缓存页、更新调度器中的活跃状态,并决定是否把资源立刻让给其他请求。这个“收尾阶段”做得是否干净,会直接影响高并发下的稳定性。
也正因为如此,工程里经常会区分“请求逻辑已经完成”和“底层资源已经完全回收”。如果你忽略了第二层,很多显存抖动和状态残留问题会很难解释。
当你排查推理流程问题时,优先关注请求状态对象里存了什么、什么时候变化,而不是只盯着一次前向计算函数。
很多缓存异常不只是“空间不够”,而是某些请求活得太久、释放太晚、上下文扩张太快。
用户停止、达到 `max_tokens`、命中终止符,这些事件都会改变请求状态机和资源回收时机。
这是典型的活跃请求历史持续膨胀问题。随着上下文积累,prefill 和缓存占用都会增加,请求会在系统里变得越来越“重”。
如果取消事件只停在接口层,而没有把请求状态完整传到引擎内部,就可能出现“用户看不到了,但资源还占着”的问题。
这一章最容易误解的地方,是把请求当成“一次算完”的任务。实际上它在系统里是一连串状态推进。
先进入系统、建立上下文,再进入生成循环,最后满足停止条件后才真正释放资源。
| 阶段 / 对象 | 系统在维护什么 | 最敏感的资源或问题 | 后续会影响什么 |
|---|---|---|---|
| Prefill | 初始上下文状态、首轮执行结果 | 输入长度、首 token latency | 请求能否顺利进入 decode |
| Decode | 活跃请求的逐轮推进状态 | 输出长度、调度策略、吞吐 | 总 latency 与设备利用率 |
| KV Cache | 历史上下文的可复用状态 | 显存容量、带宽、内存管理 | 并发能力与缓存布局设计 |
| Sampling | 每轮 token 选择逻辑 | 生成控制参数、请求语义 | 输出风格与请求状态推进 |
KV Cache 不是简单“提高一点速度”,而是让 decode 变得可承受的根基,同时也是显存管理的核心负担。
请求生命周期的本质是状态推进。后面学调度,本质上是在学“如何管理一批会动态变化的状态”。
至少包含入队、prefill、中间活跃、decode 循环、完成释放五个状态。最低完成标准是:你能说清楚每个状态主要占用什么资源。
要求同时提到“减少重复计算”和“增加显存压力”。最低完成标准是:两点都不能漏。
例如长上下文、高并发、长输出。最低完成标准是:每种场景都要解释它为什么会增加状态占用。
最低完成标准是:你的答案里要出现“它决定下一轮 token 如何进入历史上下文”。
一张请求生命周期状态图。
一段你自己的 KV Cache 解释。
一份缓存压力来源清单。
如果你能把答案说到状态和资源占用,说明视角已经切换过来。
如果只能说出一面,说明理解还不完整。
如果你能把它和 decode 的循环联系起来,说明理解到位。
因为 batching 不是在操作静态任务,而是在操作一批状态不同的活跃请求。
适合把 KV Cache 放回真实推理实现语境中理解。
适合预习缓存布局和分页管理为什么会成为引擎核心设计。
这是把缓存管理、请求调度和服务吞吐放进一篇论文里统一讨论的经典入口。
适合后面沿着缓存、调度和 serving 路径找关键对象。
适合观察一个更轻量的服务实现如何暴露生成、采样和接口能力。
适合在后续章节里对比另一路高性能 serving 设计时使用。
它不是“查得到就省一次调用”的业务缓存,而是和每个活跃请求绑定的执行状态。
实际上它参与每一轮 token 选择,会改变请求的推进路径。
这样后面再看调度和 batching 时,很容易把引擎行为误解成零散技巧。
现在你已经把单个请求的生命周期看清楚了。下一章开始处理“多个活跃请求怎么共存”的问题:什么时候该组 batch,为什么 continuous batching 会提升吞吐,以及 paged attention 为什么能缓解缓存管理压力。