我先把结论放在前面:Chunked Prefill 不是某个框架独有的黑魔法,也不是一篇论文里突然冒出来的理论名词。在 vLLM、SGLang、TensorRT-LLM 这些主流推理引擎里,它已经是处理长输入、混合负载、高吞吐场景下的默认方案之一。如果你正在做 LLM 服务化部署,或者准备优化自家推理系统的首 Token 延迟和吞吐,这篇源码级别的拆解值得你花二十分钟读完。
我这次不会只停留在“Chunked Prefill 就是把 Prefill 切块”这个层面。我会从它到底解决了什么问题开始,把 vLLM 里的调度器实现、Continuous Batching 的交互逻辑、显存管理策略、Prefix Caching 的碰撞关系、以及和 PD 分离这种架构方案的对比,全部串起来讲清楚。看完之后你不仅知道它怎么用,还能直接打开源码找到对应的实现位置。
1. Chunked Prefill 到底解决了什么:一个被排队拖垮的 GPU 场景
先说一个所有做过 LLM 推理服务的人都会遇到的场景:你的 GPU 显存 80GB,模型 70B,线上并发请求一多,为什么 GPU 利用率上不去,但首 Token 延迟却高得离谱?
如果你打开 NVIDIA SMI 看,会发现 GPU 计算单元大量空闲,显存带宽也没有跑满。问题的根源往往不是模型算不动,而是调度策略把 GPU 饿死了。这时候 Chunked Prefill 就是用来解决“GPU 饿死”问题的。
1.1 Prefill 和 Decode 的计算特性差异:一个吃算力,一个吃带宽
LLM 推理过程分两个阶段。Prefill 阶段处理整段输入 prompt,一次性并行计算所有 token 的注意力,这个阶段计算密集,GPU 的 Tensor Core 能跑得很满。Decode 阶段逐 token 生成输出,每一步只计算一个 token,注意力部分还需要读取之前所有 token 的 KV Cache,这个阶段是典型的访存密集,GPU 计算单元大多数时候在等显存数据。
问题来了:如果你把 Prefill 和 Decode 用同一个 batch 混跑,Prefill 一个长请求可能占用几十毫秒甚至几百毫秒,Decode 请求在 batch 里只能干等。等 Prefill 算完了,Decode 才能继续。这就像一个单人流水线上突然插入一个大件,后面所有小件都被堵住了。
对于在线服务来说,这个等待直接转化为首 Token 延迟和每个 Decode 步的间隔拉长。体验上就是“问了问题之后半天才开始蹦字”,而且生成过程中还一顿一顿的。
1.2 经典的“等长批处理”和它的问题
早期推理框架都是等一个 batch 里所有请求都完成 Prefill,才开始 Decode。这个方案实现简单,但问题很明显:
- 长 prompt 请求会拖垮整个 batch 的 Decode 节奏
- 短 prompt 请求无法及时插队
- GPU 利用率呈现出剧烈的锯齿状波动:Prefill 阶段算力拉满,Decode 阶段算力闲置
如果把 Prefill 和 Decode 直接混在一个 batch 里,虽然能缓解一部分等待问题,但 GPU 的实际执行效率会下降。因为 Prefill 的矩阵乘形状和 Decode 完全不同,两者混在一个 kernel 里往往需要 padding,或者用低效的 shape 去跑。
Chunked Prefill 的思路就是:不把 Prefill 当一个整体,而是把它按 chunk 粒度拆开。每个 chunk 大小合适,既能填充 Decode batch 的剩余算力,又不至于让 Decode 等太久。这就像往流水线里塞小件,而不是一个大件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. vLLM 里 Chunked Prefill 的调度核心:从 LLMEngine 到 Scheduler 的链路
vLLM 是目前实现 Chunked Prefill 最典型的开源参考。很多人以为 Chunked Prefill 是模型层或者 kernel 层的改进,其实它首先是调度层的设计。
2.1 入口:LLMEngine 的调度循环
vLLM 的 LLMEngine.step() 是整个推理循环的心脏。每次 step 调 Scheduler.schedule(),拿到一个 SchedulingOutput。里面有 scheduled_seq_groups、blocks_to_swap_in、blocks_to_swap_out、blocks_to_copy 这些关键数据。Chunked Prefill 的核心决策就发生在 Scheduler 内部。
在 vLLM 配置里,有一个参数叫 max_num_batched_tokens,还有一个叫 max_num_seqs。这两个参数直接控制 Chunked Prefill 的行为。max_num_batched_tokens 代表一个 step 里最多处理多少 token,如果模型支持,这个值可以设成大于 1 的值;max_num_seqs 代表一个 step 里最多有多少个序列。
默认情况下,如果你不显式开启 Chunked Prefill,vLLM 会把 Prefill 当作一个不可分割的整体。只有当 max_num_batched_tokens 小于当前 batch 里的 prompt token 总数时,调度器才会把一个 prompt 切分成多个 chunk。
code复制# 伪代码逻辑
def _schedule_prefills(self, budget):
for seq_group in waiting_queue:
if not budget.can_schedul
