观文听傑

返回

上一篇把请求调度粒度降到每次模型迭代:短请求完成后,新请求下一轮就能补位。然而一个 32K-token prompt 若作为不可分割的 prefill 塞进某一轮,这一轮的 GPU 时间仍会突然变长;原本每 30 ms 输出一个 token 的聊天请求,可能数百毫秒没有新 token。

Chunked Prefill(分块预填充)把长 prompt 拆成多个 token chunks(token 块),每轮只计算一部分,并把剩余预算留给 decode。它不截断 prompt,也不改变最终 KV Cache;改变的是计算发生的时间顺序。

01 Prefill 为什么会阻塞 Decode?#

假设 GPU 上已有 64 条请求在 decode,每轮各新增 1 token。此时到达一个 4096-token prompt:

不切分:
轮 t      [64 decode tokens]
轮 t+1    [4096 prefill tokens]  <- 超长迭代
轮 t+2    [64 decode tokens]

切分,预算 512:
轮 t+1    [64 decode] [448 prefill]
轮 t+2    [64 decode] [448 prefill]
...       保持每轮工作量有界
text

Decode 请求并没有丢失或抢占;它们只是必须等待同一个 forward 完成。这个现象称为 head-of-line blocking(队首阻塞):排在前面的重工作让后面的轻工作一起等待。

02 “切 prompt”不会破坏因果注意力吗?#

对长度为 PP 的 prompt,完整 prefill 产生每层

K,VRB×Hkv×P×Dh.K,V\in\mathbb R^{B\times H_{kv}\times P\times D_h}.

若先计算前 cc 个 token,就得到前缀 KV:

K0:c,V0:cRB×Hkv×c×Dh.K_{0:c},V_{0:c}\in\mathbb R^{B\times H_{kv}\times c\times D_h}.

下一块位置 [c:c+d) 的 query 读取已缓存的 [0:c) KV,并与本块产生的 KV 做因果注意力,计算完成后把 cache 扩为 [0:c+d)。因为 causal mask 下位置 ii 本来就只依赖 0..i,按顺序分块与一次算完数学上等价。

flowchart LR
  P0[chunk 0<br/>tokens 0:4] --> K0[KV 0:4]
  K0 --> P1[chunk 1<br/>tokens 4:8]
  P1 --> K1[KV 0:8]
  K1 --> P2[chunk 2<br/>tokens 8:10]
  P2 --> K2[完整 prompt KV 0:10]
  K2 --> D[生成第一个输出 token]
mermaid

03 用 8-token 预算手算三轮调度#

本轮 token budget 为 8,已有 3 条 decode 请求 D1/D2/D3,每条占 1 token;等待队列有一个 12-token prompt P。调度器优先保留 decode,剩余 5 个 token 给 prefill:

轮次Decode tokensP 本轮 chunkP 累计完成预算使用
1355/128/8
23510/128/8
33212/125/8
440P 开始 decode4/8

P 的 Time To First Token(首 token 延迟,TTFT)增加了调度轮数,但 D1–D3 的 Time Per Output Token(逐 token 间隔,TPOT)避免了单轮 12-token prefill 的尖峰。这是明确的延迟交换,不是免费优化。

04 Token Budget 是上限,不是精确耗时#

令第 tt 轮 decode 序列集合为 DtD_t,prefill chunks 为 CtC_t

Dt+cCtcBtok,|D_t|+\sum_{c\in C_t}|c|\le B_{tok},

其中 BtokB_{tok} 对应 max_num_batched_tokens。但相同 token 数不保证相同耗时:

  • decode token 读取的历史长度不同;
  • prefill chunk 在更长前缀后计算,attention 工作量更大;
  • batch shape、kernel、量化与 GPU 架构会改变效率;
  • 多模态 token 可能对应额外 encoder 工作。

因此 budget 是稳定迭代大小的第一近似。最终仍要从 per-iteration trace 拟合真实耗时,而不是把 token 数直接当毫秒数。

05 一个最小可检查的分块调度器#

输入是本轮 decode IDs、未完成 prompt 与 token budget;输出明确列出两类工作。测试应断言每轮使用量不超预算、chunk 长度为正、同一 prompt 的已计算区间不重叠且连续。

06 张量怎样从一个 Chunk 进入模型?#

假设 P 的总长度为 12,本轮计算位置 [5:10),chunk 长度 C=5C=5。概念上的输入包括:

张量/元数据Shape含义
input_ids[5]本轮 5 个 prompt token
positions[5][5,6,7,8,9],不能从 0 重置
slot_mapping[5]每个新 KV 写入哪个物理 slot
block_table[ceil(10/B)]前缀与新块的物理寻址
context_lenscalar本轮结束后为 10

模型内部 hidden states 为 [5,H];每层新 K/V 通常可看作 [H_kv,5,D_h],attention 还要读取此前 [H_kv,5,D_h] 的前缀 cache。不同实现会 flatten token 维或合并层维,但这五类语义不能丢。

07 Position 与 Mask 最容易怎样错?#

第二块从绝对位置 cc 开始。如果错误地把 position IDs 重置为 0..d-1,RoPE(旋转位置编码)相位会重复,最终结果不再等价于完整 prefill。

本块内位置 ii 的 query 可见:

{0,1,,c+i},\{0,1,\dots,c+i\},

而不是只看本块的 c..c+i。调试时用一个 6-token prompt 比较:

full_logits = model_full_prefill(tokens)
chunked_logits = model_chunked_prefill(tokens, chunks=[2, 3, 1])
torch.testing.assert_close(chunked_logits, full_logits, rtol=1e-4, atol=1e-5)
python

生产引擎不一定公开上述两个函数;可分别启动开启/关闭 chunked prefill 的同版本服务,在 greedy、固定模型与固定 dtype 下比较生成 token IDs。

08 为什么通常让 Decode 优先?#

已在流式输出的请求对停顿很敏感。先为每条 decode 分配 1 token,再用剩余 budget 做 prefill,可以限制 ITL 尖峰。伪代码是:

budget = max_num_batched_tokens
schedule running decode requests, each consumes 1
schedule cached/resumed work
fill remaining budget with prefill chunks
execute one model step
commit KV and request progress atomically
text

但严格 decode-first 也可能使 prefill starvation:当活跃 decode 数持续占满预算,新 prompt 永远得不到首 token。解决办法包括保留 prefill 配额、限制 decode admission、按等待时间 aging,或为不同服务等级拆池。

09 Chunk Size 怎样影响 TTFT、TPOT 与吞吐?#

小 chunk:

  • 单轮更短,decode 的 TPOT/ITL 更平滑;
  • prompt 要跨更多轮,调度与 kernel 开销增加;
  • 矩阵更小,GPU 利用率可能下降,TTFT 变长。

大 chunk:

  • prefill GEMM 更高效、prompt 更快完成;
  • 单轮耗时尖峰更大,decode 尾延迟上升;
  • 更容易触碰 KV 容量并导致抢占。

不要只 sweep 固定 chunk_size。在 vLLM 中,实际 chunk 由本轮剩余的 max_num_batched_tokens 决定,因此还会随 decode batch 大小动态变化。

10 当前 vLLM 配置的语义#

当前官方 SchedulerConfig 中:

  • enable_chunked_prefill=True:prefill 可按剩余 token budget 分块;
  • max_num_batched_tokens:一次迭代最多处理多少 tokens;
  • max_num_seqs:一次迭代最多包含多少 sequences;
  • long_prefill_token_threshold:多长才视作 long prefill;
  • max_num_partial_prefills:最多同时部分完成多少条 prefill;
  • scheduler_reserve_full_isl:准入时检查完整 input sequence 是否能放入 KV cache,避免过度准入和反复抢占。
vllm serve your-org/your-model \
  --enable-chunked-prefill \
  --max-num-batched-tokens 2048 \
  --max-num-seqs 128 \
  --scheduler-reserve-full-isl
bash

默认值会随版本、模型与 usage context 变化。部署时保存 vllm --version 和最终解析后的 engine config;不要从一篇旧博客复制默认值后假设它永久成立。

11 KV 容量为什么要按完整 Prompt 预留?#

若只看到第一块 512 tokens 就准入一个 64K prompt,很多长 prompt 可以同时“付得起首付”,却都没有足够 KV blocks 完成。后续每条继续申请 blocks,系统便反复抢占与重算,形成 cache thrashing(缓存颠簸)。

scheduler_reserve_full_isl 的思路是 admission 时按完整 Input Sequence Length(输入序列长度,ISL)检查容量。它更保守,可能降低瞬时并发,却避免把不可能同时完成的请求全部推进系统。是否预留还要结合 prefix caching、滑动窗口和模型的 KV 布局验证。

12 怎样设计一组有教学价值的压测?#

构造三种可控流量:

  1. 纯 decode 基线:prompt 16、output 512;
  2. 短交互:prompt 256、output 64;
  3. 长文档突发:prompt 16K/32K、output 32。

在相同 offered load 下比较 chunked prefill 开/关,并 sweep token budget。至少记录:

TTFT / TPOT / ITL / E2E: p50, p95, p99
prompt tokens/s, output tokens/s
per-step prefill tokens, decode tokens, step latency
waiting/running requests, KV utilization, preemptions
text

关键图不是单一平均柱状图,而是“长 prompt 到达时刻”附近的 step latency 与各流式请求 ITL 时间线。它能直接显示阻塞尖峰是否被摊平。

13 常见错误与最短调试路径#

症状常见原因最短检查
开启后输出与完整 prefill 不同position、mask 或 KV 写入偏移错误[2,3,1] 不规则分块逐 token 对账
TPOT 仍周期性尖峰token budget 过大或多长 prompt 同轮记录每轮 prefill tokens 与 kernel 时间
TTFT 急剧变差chunk 太小或 decode 永久占满预算查看每条 prefill 的 progress 与等待轮数
KV 明明够却频繁抢占只按首 chunk 准入,未考虑完整 ISL打开完整 ISL 容量检查并对账 blocks
GPU 利用率下降chunks 过碎、shape 变化导致图复用差sweep budget 并看 kernel/CUDA graph 命中
多模态输入在边界报错图像 embedding 不允许任意切半核对当前版本的 multimodal chunk 约束

14 与相近方法的边界#

Continuous batching 在每轮替换请求;chunked prefill 在多轮之间拆分同一个 prompt。Prefix Caching(前缀缓存)让相同前缀直接复用已有 KV,减少必须 prefill 的 token;它不能帮助从未见过的长 prompt。

Disaggregated Prefill/Decode(预填充—解码分离)把两类工作放到不同 GPU 池,进一步隔离干扰,但增加 KV 传输、路由和容量规划复杂度。Speculative Decoding(投机解码)则用 draft model 一轮提出多个候选 token,目标是减少 decode 串行轮数;它解决的是另一条轴。

15 今天真正需要记住什么?#

  1. 不可分割的长 prefill 会拉长整次 forward,使同批 decode 请求出现 ITL/TPOT 尖峰。
  2. 因果注意力允许 prompt 按顺序分块;每块必须继承绝对位置、前缀 KV 与正确可见范围。
  3. token budget 把单轮工作量控制在近似上限,通常以 TTFT 换取更平滑的 TPOT。
  4. 过度准入部分 prefill 会造成 KV 颠簸;配置必须与完整输入长度、KV 容量和抢占指标联合验证。

16 思考题与小练习#

  1. token budget 为 16,已有 6 条 decode;两个 prompt 分别剩 18 和 5 tokens。按 FCFS 写出前三轮 chunks,并说明第二个 prompt 的 TTFT 风险。
  2. 对总长 10 的 prompt,按 [4,3,3] 分块。写出每块的 position IDs、结束后的 context length,以及最后一块每个 query 能看到的 key 范围。
  3. 修改教学调度器,为 prefill 保留每轮至少 25% budget;构造 decode 永不清空的流量,验证长 prompt 最终仍能完成。

相关工作#

  1. Agrawal et al., SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills,提出用 chunked prefill 消除 prefill-decode 干扰气泡。
  2. Agrawal et al., Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve,系统评估吞吐与调度延迟权衡。
  3. Zhong et al., DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving,从分离部署角度隔离两阶段干扰。
  4. Patel et al., Splitwise: Efficient Generative LLM Inference Using Phase Splitting,研究 prefill/decode 分阶段资源配置。
  5. vLLM, SchedulerConfig 官方文档,定义当前 chunked prefill、token budget 与容量保护参数。

17 下一篇预告#

Chunked prefill 控制了每轮输入工作量,却没有减少生成 kk 个输出通常需要 kk 次串行大模型前向。下一篇将进入 speculative decoding:小 draft model 一次提议多个 token,大模型如何并行验证并保持目标分布不变。

一个长 Prompt 为何让所有流式输出停顿?Chunked Prefill 的 Token 预算
https://zwjcode.cn/blog/chunked-prefill-token-budget-decode-latency
作者
发布于 2026年9月19日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。