文章

Chunked Prefill 实战:长上下文推理的延迟治理

本文从连续批处理与预填充分块的协同机制出发,深入解析长上下文推理中如何通过 Chunked Prefill 治理延迟,涵盖核心调度原理、chunk size 调参策略、工程落地指标与生产上线检查清单。

背景问题:长上下文请求为什么会拖慢在线推理

大模型在线推理通常分成两个阶段:prefilldecode。prefill 阶段一次性处理输入 tokens,生成后续解码需要的 KV Cache;decode 阶段则按 token 逐步生成输出。两者使用 GPU 的方式并不相同——prefill 更偏 compute-bound,容易吃满矩阵计算单元;decode 更偏 memory-bound,每一步只处理新 token,但要读取不断增长的 KV Cache。

在短 prompt 场景里,这个差异不明显。一旦进入长上下文、Agent 工具链、RAG 拼接、多轮对话压缩不充分的场景,prefill 会突然变成在线服务的主要扰动源。一个几万 token 的请求进入队列后,如果调度器把它作为完整 prefill 一次性执行,正在 decode 的请求可能被迫等待——用户看到的不是平均吞吐下降,而是 inter-token latency(ITL) 抖动、流式输出卡顿、尾延迟升高。

传统静态批处理的问题更明显:一个 batch 中部分请求已经结束,仍可能被长请求拖住;新请求也要等当前 batch 完成后才能进入。Orca 在 OSDI 2022 中提出 iteration-level scheduling,把调度粒度从请求级降到迭代级,使完成的请求可以及时返回,新请求可以在下一轮迭代进入 batch。这是今天 continuous batching 的基础思想之一。

但 continuous batching 本身还不够。它解决的是「请求如何动态进出 batch」,没有完全解决「一个超长 prefill 是否会在某次迭代中占用过多预算」。这就是 Chunked Prefill 要处理的问题。

核心原理:把长 prefill 切小,让 decode 不被饿死

Continuous Batching 解决批次空洞

Continuous batching 的核心是:不要等整个 batch 全部结束后再换下一批,而是在每个生成迭代后重新检查队列。某个请求完成,就释放它的位置;队列中有新请求,就尽快加入。Hugging Face TGI 文档也把 continuous batching 与 streaming 作为生产推理引擎的核心能力,强调动态组合 in-flight requests 并通过 SSE 流式返回 tokens。

它主要解决两个问题:

  1. 短输出请求不再被长输出请求拖住
  2. GPU batch 尽量保持填充状态,减少静态批处理中的空洞。

不过,continuous batching 面对长上下文输入时仍会遇到一个尖锐问题:如果某个请求的 prefill 很大,它在某一轮中占用过多 token budget,decode 请求虽然理论上可以插入,但实际仍可能被 prefill 拉慢。

Chunked Prefill 解决长输入阻塞

Chunked Prefill 的做法是把一个大 prefill 拆成多个小块执行。vLLM 文档明确说明,chunked prefill 会把 large prefills 切成 smaller chunks,并与 decode requests 一起 batch,从而在 compute-bound 的 prefill 和 memory-bound 的 decode 之间取得更好的平衡。

在 vLLM V1 中,chunked prefill 在可能时默认启用;调度策略会优先处理 pending decode requests,然后再把剩余的 max_num_batched_tokens 预算分配给 prefill。如果某个 prefill 放不进预算,系统会自动把它切块。

可以把它理解成一个简单调度策略:

while serving:
    batch = []
    # 1. 先保护正在流式输出的 decode 请求
    batch.extend(pending_decode_requests)
    # 2. 再用剩余 token budget 填充 prefill 工作
    remaining = max_num_batched_tokens - tokens(batch)
    while remaining > 0 and has_pending_prefill():
        chunk = next_prefill_chunk(max_tokens=remaining)
        batch.append(chunk)
        remaining -= tokens(chunk)
    run_one_iteration(batch)

这个策略的关键不是「让 prefill 更快」,而是让 prefill 不再以完整长请求的形式堵塞调度循环。decode 请求可以更频繁地拿到执行机会,流式输出也更稳定。

为什么 Chunk Size 是核心旋钮

Chunked prefill 的核心调参项通常是 token budgetchunk size。在 vLLM 中,一个关键参数是 max_num_batched_tokens。文档给出的方向很清楚:

策略优势代价
较小 chunk改善 ITL,prefill 对 decode 的干扰更少prefill 迭代次数增加,TTFT 可能变差
较大 chunk降低 TTFT,单次 batch 处理更多 prefill可能阻塞 decode,影响尾延迟

NVIDIA TensorRT-LLM 的 chunked prefill 技术博客也指出,较大的 chunk size 可以减少处理 prefill 序列所需迭代次数,从而降低 TTFT;但它也会增加正在进行的 decode 阶段完成时间,影响 query completion time 和输出 TPS。因此,chunk size 不是越大越好,也不是越小越好,而是一个由业务目标决定的折中。

工程落地:从指标、参数到部署策略

先拆分服务目标

上线前不要只看 tokens/s。至少要拆成四类指标:

  • TTFT(Time to First Token):用户等待第一个 token 的时间,主要受 prefill、排队、调度策略影响。
  • ITL(Inter-Token Latency):流式输出期间相邻 token 的间隔,直接影响「是否卡顿」。
  • End-to-end Latency:完整响应完成时间,受输出长度、decode 速度和排队影响。
  • Goodput:在满足 SLO 的前提下真正完成的请求吞吐,而不是无约束压测中的最大吞吐。

很多推理优化在低并发压测中表现很好,但进入混合 workload 后会暴露问题。比如 RAG 请求、普通聊天请求、Agent 长轨迹请求混在同一实例中时,平均 tokens/s 可能仍然不错,但 P95/P99 ITL 已经不可接受。

再确定调参顺序

推荐按以下顺序推进:

1. 固定模型、量化方式、并发压测脚本
2. 先开启 continuous batching / 默认调度能力
3. 再调 max_num_batched_tokens 或等价 token budget
4. 分别观察 TTFT、ITL、P95/P99 latency、OOM/preemption
5. 最后再叠加 prefix caching、KV offloading、P/D disaggregation

在 vLLM 中,可以通过类似方式指定 token budget:

from vllm import LLM

llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_batched_tokens=8192,
)

这个值不是通用答案。小模型、大显存、短输出场景可以尝试更大的 token budget;长上下文、多租户、强流式交互场景应更关注 ITL 与尾延迟,通常需要更保守的 chunk 配置。

观察 Preemption 和队列积压

Chunked prefill 不是显存无限扩容。长上下文请求仍会消耗 KV Cache,输出长度不确定仍可能导致峰值显存超出预估。vLLM 文档在优化章节中也提醒可以通过 Prometheus metrics 监控 preemption requests。上线时应同时观察:

  • prefill queue 长度是否持续增长
  • decode queue 是否被长 prefill 间歇性阻塞
  • preemption 是否增加
  • KV Cache 使用率是否长期接近上限
  • P99 ITL 是否比 P50 ITL 高出过多

如果 preemption 频繁出现,说明问题可能已经不是单纯 chunk size 调参,而是并发上限、上下文长度、KV Cache 策略、实例隔离需要重新设计。

适用场景

适合使用 Chunked Prefill 的场景

  • 长上下文但仍需要流式体验:例如 RAG、代码仓库问答、文档分析、Agent 任务回放。用户不只关心最终完成时间,也关心输出是否连续。
  • 混合请求长度明显:短聊天和长 prompt 共用一组 GPU。如果没有 chunked prefill,长请求容易制造尾延迟。
  • 单实例内还没有必要做 P/D 解耦:当业务规模还没有复杂到需要独立 prefill 集群和 decode 集群时,chunked prefill 是较轻量的治理手段。

不适合只靠 Chunked Prefill 的场景

  • 极长上下文占比很高:如果大部分请求都是几十万 token 级别,单实例调度很难兼顾 TTFT 与 ITL,应评估上下文压缩、检索裁剪、KV Cache 分层、P/D 解耦。
  • 严格低延迟对话服务:如果业务要求每个 token 都要非常稳定,且 P99 ITL 是硬指标,单纯调 chunk size 可能不够,需要请求分级、实例隔离或 decode 优先队列。
  • 离线批处理:如果不需要流式输出,也不关心 TTFT,过度追求 chunked prefill 反而可能增加调度复杂度。离线任务更关注整体吞吐和资源占用。

常见误区

误区一:Continuous Batching 开了就万事大吉

Continuous batching 只解决动态进出 batch 的问题。长 prefill 如果不切块,仍可能在某次迭代中压住 decode。对长上下文服务而言,continuous batching 与 chunked prefill 应该一起看

误区二:只看吞吐,不看 ITL

用户感知最强的不是 tokens/s,而是流式输出是否停顿。一个系统可能在 tokens/s 上表现很好,但输出过程中频繁停顿,用户仍会认为服务很慢。

误区三:把 Chunk Size 当成静态最优值

Chunk size 与模型大小、GPU、并发、输入长度分布、输出长度分布都相关。NVIDIA 的资料也提到 动态 chunk sizing 的价值:开发者手动寻找平衡点并不轻松,动态策略可以根据 GPU utilization 等信息给出更合适的配置方向。

误区四:忽略业务入口的上下文治理

推理层只能缓解问题,不能替代上游治理。RAG 拼接过多、工具轨迹无限增长、系统提示词重复注入,都会把压力转嫁给 prefill。Chunked prefill 不是纵容无限上下文的理由。

上线检查清单

压测数据准备

上线前至少构造三类 workload:

  1. 短输入短输出:模拟普通聊天。
  2. 长输入短输出:模拟 RAG、文档问答、代码检索。
  3. 长输入长输出:模拟报告生成、Agent 总结、复杂推理。

不要只压单一输入长度。真实线上最危险的是混合分布,尤其是少量长 prompt 对大量短请求的干扰。

指标看板

建议把这些指标放到同一个看板:

  • QPS / active requests / waiting requests
  • TTFT P50/P95/P99
  • ITL P50/P95/P99
  • output tokens/s 与 request/s
  • GPU SM utilization、HBM bandwidth、KV Cache 使用率
  • preemption 次数、OOM 次数、请求取消次数
  • 不同输入长度 bucket 的延迟分布

回滚条件

建议提前定义回滚条件,而不是线上再讨论:

  • P99 ITL 超过目标阈值并持续 10 分钟
  • preemption 明显增加并伴随 TTFT 上升
  • 长上下文请求导致短请求 P95 latency 翻倍
  • GPU 利用率上升但 goodput 下降
  • 错误率、取消率、超时率高于基线

与其他技术的关系

Chunked Prefill 与 PagedAttention:两者解决的问题不同。PagedAttention 更偏 KV Cache 的显存管理与块级调度,减少碎片和提升并发能力;Chunked Prefill 更偏调度层,把长 prefill 拆成更小执行单位,避免阻塞 decode。生产中通常会叠加使用。

何时考虑 P/D 解耦:当少量长上下文请求持续影响短请求体验时,应考虑按输入长度、用户等级或任务类型做路由隔离。Chunked prefill 可以缓解干扰,但不是所有混合 workload 都适合同池运行。

参考资料

常见问题

Chunked Prefill 和普通 continuous batching 是一回事吗?
不是。continuous batching 解决请求级别的动态进出批问题,chunked prefill 进一步把长输入的 prefill 切成小块,让 decode 请求有机会穿插执行。
chunk size 越小越好吗?
不一定。较小 chunk 通常改善 ITL,但可能增加 prefill 迭代次数并拉长 TTFT;较大 chunk 则更有利于 prefill 吞吐,但可能阻塞 decode。
什么时候应该考虑 P/D 解耦而不是继续调 chunked prefill?
当长上下文请求持续冲击 decode 尾延迟,且单实例内 chunk size 很难兼顾 TTFT 与 ITL 时,应评估 prefill/decode 解耦部署。