为什么 continuous batching 还不够
LLM 在线推理不是普通批处理任务。一个请求进入系统后,模型不是一次性完成全部输出,而是先处理 prompt,再逐 token 生成结果。请求之间的输入长度、输出长度、到达时间都不同,因此传统固定 batch 很容易造成等待和浪费。
Continuous batching 解决了第一层问题:当某些请求完成后,新请求可以在后续 decode iteration 中加入,而不是等待整个 batch 全部结束。
但 continuous batching 仍然没有完全解决长 prompt 带来的问题。因为一个新请求进入时,系统必须先执行 prefill,为 prompt token 计算 KV cache。如果这个 prompt 很长,它会占用较大的计算窗口,并可能阻塞已经在流式输出的 decode 请求。
这就是 chunked prefill scheduling 要解决的问题。
一句话理解 Chunked Prefill
Chunked prefill 的核心思想是:不要让一个长 prompt 的 prefill 一次性占满调度窗口,而是把它拆成多个较小的 prompt chunk,穿插在 decode 请求之间执行。
可以简化成下面的流程:
Before: Long Prefill -> Decode -> Decode -> Decode
After: Decode -> Prefill Chunk -> Decode -> Prefill Chunk -> Decode
这样做的目标不是单纯提高 tokens/s,而是控制两个关键用户体验指标:
- TTFT:Time To First Token,首 token 延迟。
- ITL / TPOT:Inter-Token Latency / Time Per Output Token,流式输出中 token 与 token 之间的延迟。
Chunked prefill 往往会牺牲一部分 TTFT,换取更稳定的 ITL 和更好的 GPU 利用率。
核心知识结构
可以把 Chunked Prefill Scheduling 拆成 6 个模块。
1. Iteration-Level Scheduling
传统 request-level scheduling 会让一个 batch 从开始跑到结束,中途不能灵活加入新请求。Iteration-level scheduling 把调度粒度降低到每一次生成迭代,让系统可以在每轮 decode 后重新组织 batch。
这是 continuous batching 的基础。
2. Prefill 与 Decode 的资源差异
Prefill 处理完整 prompt,计算量大,但更容易并行,通常偏 compute-bound。
Decode 每次只处理当前 token,但要反复读取模型权重和 KV cache,通常更容易受 memory bandwidth 影响。
两者混在一起调度,如果没有控制,很容易互相干扰。
3. Token Budget
Token budget 是调度器每次形成 batch 时允许处理的 token 上限。
较小的 budget 往往有利于 ITL,因为 prefill 不容易长时间阻塞 decode。较大的 budget 往往有利于 TTFT 和吞吐,因为更多 prompt token 可以被一次性处理。
4. Decode Priority
许多 chunked prefill 策略会优先放入正在生成的 decode 请求,再用剩余 token budget 安排 prefill chunk。
这样可以保护流式输出稳定性,但也可能让新请求的 prefill 等待更久。
5. Fairness 与 Starvation
如果 decode 一直优先,长 prompt 的请求可能被反复延后,形成 starvation。生产系统需要 aging、动态优先级、最大等待时间或目标延迟约束。
6. Tail Latency
平均延迟不够。Chunked prefill 的收益主要体现在 P90/P99 ITL、P99 TTFT、请求饥饿和 SLO 命中率上。
调度流程:decode 先行,prefill 补位
一个典型 chunked prefill 调度循环可以写成:
- 收集所有正在 decode 的请求
- 优先把 decode token 放入本轮 batch
- 计算剩余 token budget
- 从等待队列选择 prefill 请求
- 如果完整 prefill 放不下,就切出一个 prefill chunk
- 执行混合 batch
- 更新 KV cache、请求状态和队列优先级
- 记录 TTFT、ITL、P99、等待时间和 GPU 利用率
它的关键不是”切块”本身,而是切块之后调度器如何决定谁先执行、执行多少、等待多久。
为什么 token budget 是核心旋钮
在 vLLM 这类 serving engine 中,max_num_batched_tokens 这类参数决定每轮 batch 最多处理多少 token。它会直接影响 TTFT、ITL 和吞吐。
较小 token budget 的特征:
| 维度 | 影响 |
|---|---|
| 优点 | decode 更少被 prefill 打断,ITL 更稳定 |
| 缺点 | 长 prompt prefill 需要更多轮,TTFT 可能上升 |
较大 token budget 的特征:
| 维度 | 影响 |
|---|---|
| 优点 | prefill 更快完成,TTFT 可能更好,吞吐可能更高 |
| 缺点 | prefill chunk 更大,decode 可能出现延迟尖刺 |
因此,chunked prefill 没有一个永远正确的默认值。不同业务需要不同取舍:
- 聊天应用:更重视 ITL 和流式稳定性。
- RAG 问答:既要控制 TTFT,也要避免长文档 prefill 阻塞 decode。
- 代码生成:输出长,ITL 和 decode 吞吐很关键。
- 批量离线生成:可能更重视整体吞吐,而不是单请求流式体验。
公平性问题:decode priority 不是万能答案
Decode priority 可以保护已经开始输出的请求,但它会引入另一个风险:新来的长 prompt 请求可能被反复拆分和延后。
例如:
Request A: short prompt, long decode Request B: short prompt, long decode Request C: very long prompt, waiting for prefill
如果系统持续优先 A 和 B 的 decode,C 的 prefill 可能只能获得零碎执行机会,TTFT 会非常差。
因此,生产系统需要额外机制:
- Aging priority:等待越久,优先级越高。
- Max wait bound:超过最大等待时间后强制调度。
- Latency target:用目标 TTFT 或目标完成时间反推 prefill 并发。
- Prefill concurrency control:限制同时进行的 prefill chunk 数量,避免碎片化。
- Work-aware scheduling:根据剩余 prefill token 和已等待时间综合排序。
这些机制的目标是避免系统只优化平均值,却牺牲长 prompt 或低优先级请求。
性能评估不能只看吞吐
Chunked prefill 的评估至少需要拆成 7 类指标。
1. TTFT
首 token 延迟。重点看 P50、P90、P99,而不是只看平均值。
2. ITL / TPOT
流式输出稳定性。对用户体验来说,P99 ITL 尖刺往往比平均 tokens/s 更直观。
3. Queue Time
分别统计 prefill queue time 和 decode queue time,判断瓶颈在哪一侧。
4. Chunk Fragmentation
如果 prefill 被切得太碎,调度开销和状态管理复杂度会上升。
5. Starvation Rate
统计等待时间超过阈值的请求比例,避免长 prompt 被 decode priority 长期压制。
6. GPU Utilization
观察 compute utilization 与 memory bandwidth,判断 mixed batch 是否真的改善硬件利用率。
7. SLO Goodput
只统计满足 TTFT 和 ITL 目标的有效请求吞吐。它比 raw throughput 更接近生产价值。
一个简化监控结构如下:
metrics:
ttft_ms:
p50: 420
p90: 900
p99: 1800
itl_ms:
p50: 32
p90: 70
p99: 130
scheduler:
max_num_batched_tokens: 2048
prefill_chunk_count: 18420
prefill_queue_wait_p99_ms: 2100
starvation_rate: 0.003
goodput:
requests_per_second_under_slo: 56
生产落地 Checklist
上线前建议检查:
- 是否区分 TTFT、ITL/TPOT、吞吐和 goodput。
- 是否用真实 prompt/output 长度分布压测。
- 是否评估不同
max_num_batched_tokens设置。 - 是否统计 prefill queue 与 decode queue。
- 是否监控 P99 ITL,而不是只看平均 ITL。
- 是否有 starvation 检测。
- 是否为长 prompt 请求设计 aging 或最大等待时间。
- 是否记录每轮 batch 的 prefill/decode token 占比。
- 是否评估 chunk 过小带来的调度开销。
- 是否为不同业务设置不同调度策略。
- 是否保留关闭 chunked prefill 的回退方案。
- 是否把 SLO goodput 作为验收指标。
结论
Chunked prefill scheduling 的本质,是把长 prompt 从”不可打断的大任务”变成”可插入、可控制、可观测的小任务”。
它解决的不是单点性能问题,而是在线 LLM 服务中的调度稳定性问题:
- Continuous batching 让请求可以动态加入
- Chunked prefill 让长 prompt 不再长时间阻塞 decode
- Token budget 决定 TTFT 与 ITL 的取舍
- Fairness policy 决定长 prompt 是否会被饿死
- SLO goodput 决定优化是否真正有生产价值
对工程团队来说,最实用的原则是:不要只问 chunked prefill 是否更快,而要问它是否让你的服务在真实请求分布下更稳定、更公平,并且更容易满足 TTFT 与 ITL 的 SLO。