16. Chunked Prefill 是什么?为什么能同时改善吞吐量和延迟?

整理 Chunked Prefill 的原理、收益与工程取舍。

简单回答

Chunked Prefill(分块预填充)是把长 Prompt 的 Prefill 阶段拆成若干小块(比如每块 512 token),不一次性算完,而是每个 batching 步只算一块。这样做有两个收益:一是新请求的 Prefill 不再阻塞已有请求的 Decode——以前一个长 Prompt 进来要 Prefill 几秒,这期间所有 Decode 中的请求全部停摆,TTFT 和 TBT 都被拉长;二是把"计算密集型的 Prefill"和"访存密集型的 Decode"在同一个 batch 里混合执行,硬件利用率反而更高。是 vLLM、SGLang 等现代推理框架的默认调度策略。

详细解释

没有 Chunked Prefill 之前的痛点

本专题第 01 篇讲过 Prefill 和 Decode 的性质差异——Prefill 是 compute-bound 一次性处理整个 Prompt,Decode 是 memory-bound 逐 token 生成。在第 03 篇讲的 Continuous Batching 框架下两者在同一个推理引擎里调度。

但传统 Continuous Batching 有个调度难题:什么时候让新请求进来做 Prefill。常见的两种朴素策略都有问题:

Prefill-first:新请求一来就先把它的整个 Prompt Prefill 完,这期间已有请求的 Decode 全部停下来。后果是已有请求的 TBT(Time Between Tokens,token 间延迟)会出现明显尖刺——Decode 中的用户突然几秒看不到新 token 蹦出来。如果 Prompt 是 16K,这个停顿可能长达数秒。

Decode-first:让正在 Decode 的请求优先,新请求排队等所有 Decode 请求完成后再 Prefill。后果是新请求的 TTFT 极差——可能要等几十秒。

无论哪种策略,长 Prompt 的进入都会破坏服务质量。这个矛盾在 vLLM 早期版本里非常明显。

Chunked Prefill 的做法

Chunked Prefill 把长 Prompt 拆成固定大小的 chunk(比如 512 或 1024 token),每个调度步只 Prefill 一个 chunk,剩下的 chunk 留到下一步。

具体流程:

  • 一个 batching step 里,调度器看 GPU 还有多少 token 预算(典型 2048-4096 token)
  • 用一部分预算给 Decode 中的请求各算一个新 token(每个请求 1 token)
  • 用剩下的预算给一个或多个 Prefill 中的请求各算一个 chunk(可能 512 token)
  • 把这些 token 拼成一个大 forward 一次性算完

这样 Prefill 和 Decode 在同一个 forward 里并存了——512 个 Prefill token 加上几十个 Decode token 一起进 GPU。

为什么同时改善吞吐和延迟

延迟侧(TTFT 和 TBT)

新请求来了不需要等几秒做 Prefill 了——第一个 chunk 算完就能给用户回第一个 token。TTFT 大幅降低。同时已有请求的 Decode 也不会被长 Prefill 阻塞——chunked 后每步只占用一小段 Prefill 预算,Decode 仍然在每步推进。TBT 不再出现尖刺。

整体效果是延迟分布的尾部(P99)收得很紧。这对生产系统的 SLA 很重要——平均延迟好看不算什么,TBT 偶尔卡几秒会让用户觉得"卡顿"。

吞吐侧

这个是反直觉的——拆成更小的 chunk 直觉上应该让吞吐变差,但实际反而变好。原因是 Prefill 和 Decode 的硬件特性互补。

Prefill 是 compute-bound,把 GPU 的算力打满,但显存带宽用得不充分(一次读完模型参数算 N 个 token)。Decode 是 memory-bound,显存带宽是瓶颈但算力闲置(读完参数只算 1 个 token)。

把两者拼在同一个 forward 里,Prefill 部分把算力用满,同时这次访存也能服务于 Decode token——本来 Decode 自己也要把模型参数读一遍,现在和 Prefill 共用一次访存。硬件上算力和带宽同时被打满。

实测数据上,开 Chunked Prefill 的 vLLM 比关掉的版本,吞吐量能提升 20%-40%,同时 TTFT 和 TBT 的 P99 都改善 50% 以上。

Chunk 大小怎么选

Chunk 大小是核心参数。太大失去拆分意义,太小调度开销盖过收益。

太大(比如 4096)—— 一个 step 算 4096 个 Prefill token 加几十 Decode token,相当于又退回到 Prefill-first 的体验,已有 Decode 的 TBT 又会有尖刺。

太小(比如 64)—— 每步只 Prefill 64 token,调度开销比例上升,且 Prefill 总耗时变长(总 token 数不变但每次开销增加)。

实际经验上 512 或 1024 是甜区。具体值还要看 GPU 算力和模型大小——A100 跑 70B 模型可能 1024 合适,H100 跑 7B 模型 512 已经够。一些框架用 token budget 而不是固定 chunk 大小——每步总共能处理 N token(比如 2048),按需分配给 Prefill 和 Decode,更灵活。

工程实现细节

Attention kernel 要支持混合长度。一个 forward 里有的请求是 Prefill chunk(多个 token),有的是 Decode(1 token),attention 计算对每个请求的实际长度不一样。Flash Attention 的 varlen 接口(处理变长序列)就是为这种场景设计的。

KV Cache 的写入要分阶段。Prefill chunk 写入 KV Cache 时只算这一块的 KV,下一个 chunk 来时要拿到上一块已经写入的部分做 attention。这要求 KV Cache 管理(本专题第 07 篇讲的 vLLM PagedAttention)支持"部分写入"。

调度器要做 token budget 平衡。每步先满足正在 Decode 的请求(每个 1 token),剩下的预算分配给 Prefill。如果有多个 Prefill 请求要排序——通常按 FIFO 或优先级。

何时不开 Chunked Prefill

Chunked Prefill 不是免费的——固定开销略高(每步多一次调度逻辑,attention kernel 复杂度上升)。在以下场景反而可能拖慢:

  • 短 Prompt 场景:Prompt 都在几百 token 以内,Prefill 本身就快,拆分意义不大。
  • 批吞吐优先:如果系统的目标是离线批处理,单次大批量 Prefill 不需要考虑 TBT,传统 Prefill-first 反而效率更高。
  • 超大 batch decode 时:Decode 请求已经把 token budget 占满了,Prefill 进不来,Chunked 的优势体现不出来。

但在线服务场景下 Chunked Prefill 几乎是默认选择,主流推理框架(vLLM、SGLang、TensorRT-LLM)都内置了。

面试时可以这样答

Chunked Prefill 解决的核心问题是 Prefill 和 Decode 在同一个推理引擎里抢资源的矛盾。新请求来了,Prefill 一整个长 Prompt 要几秒,这期间已有 Decode 全部停摆 TBT 出现尖刺;如果反过来让 Decode 优先,新请求 TTFT 又会很差。

做法是把长 Prompt 的 Prefill 拆成固定大小的 chunk,比如每块 512 token。每个调度步先满足正在 Decode 的请求各 1 个 token,剩下的 token 预算分给 Prefill 一个 chunk,混合在一个 forward 里算完。

收益有两块。延迟上 TTFT 和 TBT 同时改善——新请求第一个 chunk 算完就能蹦字,已有 Decode 不再被长 Prefill 阻塞。吞吐上反而变好——Prefill 是 compute-bound、Decode 是 memory-bound,混在一起算同一份模型参数,算力和带宽同时被打满,硬件利用率比单做 Prefill 或单做 Decode 都高。实测吞吐量能涨 20-40%,TTFT/TBT 的 P99 改善 50%+。

Chunk 大小是关键参数。太大就退回 Prefill-first 的尖刺问题,太小调度开销盖过收益。经验上 512 或 1024 是甜区,更先进的做法是用 token budget 动态分配。

实现上 attention kernel 要支持变长(Flash Attention varlen)、KV Cache 要支持分阶段写入。这些都是 vLLM、SGLang 等现代推理框架的默认能力。在线服务场景几乎是必开的优化,离线批处理的话传统 Prefill-first 可能更高效。

常见追问

  1. Chunked Prefill 和 Speculative Decoding 能不能同时开?两者是互补还是冲突?
  2. Token budget 怎么动态调整?根据什么信号变?
  3. 多机多卡推理下 Chunked Prefill 还有同样的收益吗?通信开销会不会抵消?