文章

Chunked Prefill Scheduling:LLM 推理调度为什么不能只靠 Continuous Batching

本文系统拆解 Chunked Prefill Scheduling 的工程原理、continuous batching 的局限、TTFT 与 ITL 的指标取舍、token budget 调优、公平性控制和生产上线 Checklist,帮助判断 LLM 推理系统如何降低长 prompt 对流式输出的干扰。

为什么 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 调度循环可以写成:

  1. 收集所有正在 decode 的请求
  2. 优先把 decode token 放入本轮 batch
  3. 计算剩余 token budget
  4. 从等待队列选择 prefill 请求
  5. 如果完整 prefill 放不下,就切出一个 prefill chunk
  6. 执行混合 batch
  7. 更新 KV cache、请求状态和队列优先级
  8. 记录 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。

常见问题

Chunked prefill 和 continuous batching 是一回事吗?
不是。Continuous batching 解决请求能否在生成过程中动态加入 batch;chunked prefill 进一步把长 prompt 的 prefill 拆成小块,减少它对 decode 请求的阻塞。
为什么启用 chunked prefill 后 TTFT 可能变差?
因为 decode 请求通常会被优先调度,长 prompt 的 prefill 会被拆分并延后执行。这样可以改善 ITL 和流式输出稳定性,但可能增加首 token 等待时间。
生产环境如何调 chunked prefill 的 token budget?
应按真实 prompt/output 分布压测,联合观察 TTFT、ITL、P99、GPU 利用率、队列等待和请求饥饿情况,而不是只看平均吞吐。