Skip to content

07 · Prefill 与 Decode:两条不同的执行路径

07 · Prefill 与 Decode:两条不同的执行路径

先建立直觉:同一条请求为什么会换节奏?

用户看见的是同一段回复;GPU 看见的却是先“批量读入”、再“逐 token 推进”的两类工作负载。

先观看动画。它把一条请求从 prompt 到流式输出的节奏拆开:Prefill 如何批量建立 KV Cache,Decode 为什么每轮只有一个新 token 却要读取不断增长的历史,以及 is_prefill 如何将两条路径带回代码。

观看时只抓住一个结论:Prefill 和 Decode 不是同一个 batch 的不同命名。它们的输入形状、所需元数据、KV Cache 关系与优化目标都不同;is_prefill 选择的是一条执行路径,而不只是一个注释标签。

Prefill:把整段未缓存 prompt 建成 KV

Prefill 处理尚未缓存的 prompt token。若 A 有 8 个、B 有 12 个未缓存 token,概念上本轮要处理的工作量是 20 个 token。输入准备会把 token 拼接为一批,并用累计序列长度 cu_seqlens 标出请求边界;一次前向计算同时写入这些 token 的 K/V。

观察项Prefill 中你应看到什么它告诉你什么
input_ids多个尚未缓存的 prompt token输入在 token 维度上较宽。
cu_seqlens每条 Sequence 在拼接批次中的边界变长请求仍能在同一批中定位自己的 token。
KV Cache本轮多份 K/V 被建立后续 Decode 不必重新计算 prompt。
用户感受首 token 前的等待排队与 Prefill 是 TTFT 的重要组成。

Decode:每条一个新 token,带着各自历史

Prefill 完成后,每个活跃 Sequence 在一轮 Decode 中通常只提交最后一个 token。两条活跃请求的概念输入形状因而是 [2, 1]:两行,每行一个 last_token。但这个“小输入”并不表示没有历史工作;每条请求还需要自己的 context_lensblock_tables 与缓存位置,读取长度不同的历史 K/V。

小输入,长读取是 Decode 最容易被忽略的反差。它的单步算术强度较低,却不断访问增长中的缓存,因此会更敏感于内存带宽、kernel launch 与调度开销。

从内部节奏到用户指标

TTFT(首 token 时间)衡量用户从发送请求到看见第一个 token 的等待,它受排队和 Prefill 影响很大。TPOT(每输出 token 时间)描述流式输出中相邻 token 的间隔,更接近 Decode 的逐轮节奏。总 tokens/s 仍然重要,但不能替代这两个体验指标。

指标首先问什么更直接对应的阶段
TTFT“第一个字为什么还没出现?”排队 + Prefill
TPOT“后续文字为什么一顿一顿?”Decode 每轮推进
总吞吐“系统一共服务了多少 token?”两阶段与 batching 的组合

代码锚点:从一个布尔分支开始读

下面是概念化阅读路径,不等同于上游源码的逐行转录。先顺着一次 step 的调用链来到 ModelRunner.call("run", seqs, is_prefill),再比较两种输入准备方式。1

text
Scheduler.schedule
  → ModelRunner.call("run", seqs, is_prefill)
    → prepare_prefill 或 prepare_decode
    → model forward
  → Scheduler.postprocess
锚点先问的问题实验里的可观察结果
is_prefill这轮是在建立 prompt KV,还是在推进生成?阶段从 Prefill 切到 Decode。
prepare_prefill为什么输入包含整段未缓存 token?A=8、B=12 时本轮合计 20 token。
prepare_decode为什么每条只带一个 last_token两条请求显示概念形状 [2, 1]
context_lens / block_tablesDecode 如何读到各自的历史?新 token 只加一个,历史 KV 长度继续增长。

动手验证:预测 → 运行 → 解释

不要先点击按钮。先根据 A=8 与 B=12 写下 Prefill 的总 token 数,并判断切入 Decode 后的形状。然后再改变 B 的 prompt 长度,观察“总 token 改变”与“Decode 每条一个 last_token”这两件事为何可以同时成立。

步骤你要做什么你应该观察什么
1. 预测装载默认任务 A=8、B=12。预测首轮 Prefill 总 token 数,以及两条请求进入 Decode 后的 input_ids 概念形状。Prefill 汇总未缓存 token;Decode 汇总活跃 Sequence 的最后一个 token。
2. 运行点击“运行 Prefill”,再点击“运行一个 Decode step”。prompt token 变为已缓存;Decode 时每条新增一个 token,形状变为 [2, 1]
3. 解释点击核对,然后将 B 改成 20 token 重新预测。Prefill 工作量从 20 改为 28;但只要仍有两条活跃请求,Decode 的每轮输入仍是 [2, 1]
HTML INTERACTIVE LAB07 · Prefill 与 Decode:两条不同的执行路径
单独打开 ↗
课后习题等待完成

为什么两条请求在 Decode 中的概念输入形状可以是 `[2, 1]`,即使它们的历史 prompt 长度不同?

动手任务

完成 A=8、B=12 的默认任务后,将 B 改为 20。写下两次 Prefill 总 token 的差异,并说明为什么 Decode 形状仍保持 `[2, 1]`。

下一节:Attention

社区教程,与 nano-vLLM 上游项目无官方隶属关系。