详细解释
分块预填充(Chunked Prefill)是推理引擎调度层的一项关键优化,其核心思路常被称为 Split-Fuse:把一条超长请求的 预填充 计算拆成多个固定 Token 预算的小块(chunk),每一块与当前正在解码的其他请求融合(fuse)进同一个批次一起执行,而不是让这条长 prompt 一次性独占整个前向。这个思路最早由 Sarathi-Serve 系统化提出(见 Sarathi-Serve 论文),并在 vLLM 等主流引擎中落地为可调参数。要理解它为什么重要,得先看没有分块时会出什么问题:一条 32K Token 的 prompt 进来,引擎会把它当成一个巨大的 Prefill 批次一次算完,这期间整张卡的 SM 与算力都被占住,算完可能要几百毫秒,与此同时其他正在流式输出的短请求只能干等,这就是经典的队头阻塞(head-of-line blocking)。
它解决的核心痛点正是长 Prefill 独占一次前向造成的连锁反应。同批的 Decode 请求本该每步吐一个 Token,但被那条长 prompt 卡住后,相邻 Token 间隔 TPOT 出现尖峰,用户侧看到的就是「打字机突然停住」;更糟的是首 Token 延迟的 P99 会被偶尔出现的长请求反复拉高,形成难以预测的尖刺。开启分块预填充后,长 prompt 不再霸占整批,而是被摊到多个连续批次里逐步消化:每个批次固定放 512 至 2048 Token 的 Prefill 计算量,算完这一块就把配额让给解码请求,批处理调度器趁机把若干待解码请求塞进同批,一步步吐 Token。这样解码请求每一步都有机会插进批次,TPOT 尖峰被削平,P99 首 Token 延迟 也变得可预测得多。代价是长 prompt 自己的 TTFT 会略微变长,因为它不再享受一口气算完的特权,本质上是用一点 TTFT 换延迟分布的稳定。
分块预填充与 连续批处理 是互补关系:连续批处理决定「谁能在每一步自由进出批次」,是并发调度的骨架;分块预填充决定「一个批次里放多少 Prefill、多少 Decode 的计算量」,是批次内部的比例分配。两者配合时,引擎按 Token 预算做统一编排,把每一步的计算配额优先留给已开始的解码请求,剩余额度再用长 prompt 的分块去填,从而兼顾并发与公平。典型 chunk 预算区间是 512 至 2048 Token,2026 年公开档的引擎默认值常见 512 或 1024,需结合显卡算力与平均输出长度调参。配合 分页注意力 的 KV 分页管理,分块之间的 KV 可以跨批次无缝复用,不会因拆分而重算,这也是分块几乎不增加额外算力的原因。要判断分块效果,看 延迟分位数 的 P99 远比看平均值可靠。唯元智创 的推理网关默认为所有后端开启分块预填充,并按业务的历史输出长度分布自动推荐 chunk 预算,帮助企业在不增卡的前提下把 P99 抖动压到原来的一半以下。
Prefill 调度策略对比表
| 策略 | 单次前向处理量 | 队头阻塞 | P99 TTFT 表现 | Decode 抖动 | 显存峰值 | 典型支持框架 |
|---|---|---|---|---|---|---|
| 整段 Prefill(Full Prefill) | 一条 prompt 一次全部算完 | 严重,长请求独占整批 | 差,尖峰可达 5 至 8 秒 | 高,TPOT P99 尖峰 300 毫秒以上 | 高,需一次性容纳整段 KV | 所有引擎的基础模式 |
| 分块预填充(Chunked Prefill) | 按 chunk 预算分批,常见 512 至 2048 | 基本消除,长请求不再独占 | 好,P99 压到 2.0 至 3.0 秒 | 低,TPOT P99 约 50 至 90 毫秒 | 较低,KV 按需增长 | vLLM、SGLang 原生支持 |
| 分块 加 抢占式调度 | 分块执行,必要时抢占并重排 | 无,优先级可动态调整 | 很好,P99 约 1.8 至 2.5 秒 | 极低,TPOT P99 约 40 至 70 毫秒 | 低,支持 KV 换出 | vLLM 抢占与重算机制 |
| PD 分离(跨节点) | Prefill 与 Decode 分池执行 | 无,跨池彻底解耦 | 好,但需高速互联支撑 | 低,取决于 KV 传输链路 | 分池管理,两池各自预留 | vLLM、SGLang 的 PD 模式 |
| 唯元智创 默认分块组合 | 自适应 chunk 加 抢占式调度 | 基本消除 | 好,P99 约 1.9 至 2.6 秒 | 低,TPOT P99 约 45 至 75 毫秒 | 低,按需分配 | 网关统一纳管后端引擎 |
chunk size 取值对比表
| chunk size | 显存峰值 | TTFT | TPOT | 适用场景 |
|---|---|---|---|---|
| 256 | 最低 | 最长,长 prompt 排队块数最多 | 最平稳 | 高并发、平均输出极短、极重视 TPOT 稳定 |
| 512 | 低 | 较长 | 平稳 | 多数在线对话的稳妥起点 |
| 1024 | 中 | 中 | 较平稳 | 通用推荐,兼顾 TTFT 与 TPOT |
| 2048 | 较高 | 较短 | 略回升 | 平均输出长、并发不高的场景 |
| 4096 | 高 | 最短 | 抖动明显回升 | 仅适合以超长输入为主、几乎无并发的离线任务 |
多数线上业务的推荐区间是 512 至 2048,其中 512 与 1024 是最常见的甜点值。
实战经验与避坑
-
chunk size 不要设得比模型的 max_num_batched_tokens 上限还大:分块的语义是「单个批次最多算多少 Token」,如果 chunk size 超过引擎一次前向能容纳的 Token 上限,多出来的部分只能被截断或退化成整段 Prefill,分块就失去了意义。正确做法是先确认引擎配置里的最大批处理 Token 数,把 chunk size 设成它的一个合理分数(常见是二分之一到四分之一),既能保证单批不超限,又留出余量给同批的 Decode 请求。上线前务必检查这两个参数的相对关系,很多「明明开了分块却没效果」的案例,根因就是 chunk size 被配置得比批处理上限还大,分块压根没生效。
-
必须与连续批处理同时开启,否则收益大幅缩水:分块预填充解决的是「批次里放多少 Prefill 计算量」,但如果引擎本身没有连续批处理、请求不能随时进出批次,那么分块算完的配额也没有新请求能接上,解码请求依旧要等前一批全部结束。所以正确姿势是两个开关一起打开:连续批处理负责让请求随时进出,分块预填充负责让每批的计算配额分配合理。实测对比中,只开连续批处理时遇到 32K 长 prompt 的 TPOT P99 抖动约 200 至 350 毫秒,两者同开后可压到 60 至 100 毫秒,同时集群吞吐还能再涨 20% 至 40%。
-
长上下文任务要把分块与 KV 分页、滑动窗口注意力组合使用:对于动辄几十万 Token 的超长上下文,单纯分块只解决了调度公平性,没有解决 KV 显存膨胀问题。更稳妥的组合是分块预填充加 分页注意力 的 KV 分页管理(避免显存碎片、支持增量复用),必要时再叠加滑动窗口注意力把超长序列的 KV 占用压下来。三者配合时,分块负责把长 prompt 摊平到多批、KV 分页负责让跨批的 KV 无缝拼接不重算、滑动窗口负责限制显存上限,这样才能在超长上下文下同时保住 TPOT 稳定与显存可控。
-
不要为了压 TTFT 把 chunk 设到极小,会拉低整体吞吐并抬高调度开销:直觉上 chunk 越小、解码请求插空机会越多、TPOT 越平稳,但代价是批次数量变多,kernel 启动与调度开销上升,整体每秒处理 Token 数反而下降。当 chunk 小到 256 以下时,调度开销常常吃掉收益,出现「TPOT 更稳了但总吞吐掉了两三成」的情况。经验做法是从 512 或 1024 起步,按 2 倍步长向上逼近,观察 TPOT 抖动从哪个点开始明显回升,在回升点前一档定稿;只有在平均输出极短且并发极高时才考虑降到 256。
-
要区分 TTFT 与 TPOT 两个指标分开评估,避免只优化一头:分块预填充对这两个指标的影响方向相反——它让长 prompt 的 TTFT 略微变长(典型增加 15% 至 30%),却让 TPOT 的 P99 大幅改善(典型下降 60% 至 80%)。如果只盯 TTFT 会误判为退步,只盯 TPOT 又可能忽略长请求排队过久。正确做法是同时看两组的 P50、P90、P99,并结合业务权重判断:重视流式打字体验的产品应优先保 TPOT,重视首屏出字速度的产品则要控制 TTFT 增幅,必要时把 chunk 调大一点换回 TTFT。
-
上线前必须用混合长短请求的压测流量验证,纯短请求测不出收益:分块预填充的收益全部来自「长请求打断短请求」这一现象,如果压测流量清一色是短 prompt,那么开不开分块几乎没有差别,很容易得出「没用」的错误结论。因此压测集必须刻意混入长尾:常见做法是让 5% 到 10% 的请求携带 8K 以上的长输入,其余为短请求,并观察这批长请求出现时短请求的 TPOT 是否被拖出尖峰。只有在这种混合流量下,分块带来的「TPOT P99 从数百毫秒降到几十毫秒」的改善才会显现,评估结论也才可信。