Chunked Prefill 的收益窗口与代价:TTFT 上升与 TPOT 收敛
在一组 8 卡 **** ****(单卡 64GB HBM,卡间互联 392 GB/s)上,用 **** 的基础软件栈配合 vLLM 起 OpenAI 兼容服务,负载为混合:一批 32k token 量级的长文档问答,与一批短对话交错到达。未开分块时,长 prompt 的 prefill 进入调度后占满单步的 token 预算,同批中正在解码的短请求要等该步结束才拿到下一个 token,表现为 TPOT 抖动,多数 token 间隔正常,少数被拉长到明显高位。把 --enable-chunked-prefill 打开、并把 --max-num-batched-tokens 固定到中等取值后,TPOT 的 p95 收窄,TTFT 反而上升。取舍的判据是:长 prompt 是否占主流、并发是否足够高;两项不成立时,分块只把等待从解码段挪到预填段。
调度一步的内容:prefill 与 decode 同批
vLLM 的调度器按步推进。每一步从待运行序列里取出一批,交给模型做一次前向,取回每个序列的下一个 token。
- decode 序列:KV 已在缓存里,每步只处理一个新 token。
- prefill 序列:prompt 尚未处理,每步要处理 prompt 中尚未计算的 token。
一步能处理的 token 总量受 --max-num-batched-tokens 约束。该参数是每步允许处理的 token 上限,也是分块粒度的直接约束。
不分块时,一条长 prompt 的 prefill 要在一步内提交全部 prompt token。若 prompt 长度超出该步预算,整条请求留在等待队列里等预算;若刚好占满预算,同批 decode 序列在这一步里只能分到很少的 token,其 TPOT 被拉长。这一步的耗时按 prompt 长度增长,抖动随长 prompt 到达而出现。
分块粒度由 max-num-batched-tokens 约束
开分块后,长 prompt 被切成多个 chunk,每个 chunk 的 token 数不超过本步预算扣掉本步 decode token 之后的余额。切块粒度不是独立参数,直接由 --max-num-batched-tokens 决定。
| 参数 | 作用 | 说明 |
|---|---|---|
--enable-chunked-prefill |
打开分块调度 | vLLM v1 中分块是调度器的默认行为;在需要显式开关的版本里用它控制 |
--max-num-batched-tokens |
每步 token 上限,同时是 chunk 粒度上限 | 取值直接决定单块大小与混批比例 |
--max-num-seqs |
同时驻留序列数 | 决定 decode 侧有多少序列需要被保护 |
token 预算与目标取舍相关,公开调优配方给出的参考区间是:延迟敏感、低 TTFT 取较小预算(约 8k–16k);流式解码、低 ITL 取较大预算(约 32k–64k);离线最大吞吐取更大预算。取值为参考区间,非实测,须按本机规格与负载复测。
触发条件与约束
分块是否可用与负载类型无关,但受后端与调度器约束,须按所用分支的发布说明核对:
- 部分厂商分支的发布说明记录,在特定调度器下同时开启 chunked prefill 与 prefix caching,性能与精度结果不正确,两者需分开验证。
- sliding window attention 在部分分支上尚不支持 chunked prefill。
- 若目标后端要求分块关闭,
--max-num-batched-tokens取值不再约束 chunk 粒度,长 prompt 回到单步处理。
在约束不成立的分支上,先把开关打开再压测,会得到与预期无关的结果。
取舍:TTFT 上升,TPOT 收敛
分块把长 prompt 的 prefill 摊到多步,每步同时排入 decode token,两段混批。
| 观察项 | 不分块 | 分块 |
|---|---|---|
| 长 prompt 单步耗时 | 高,近似按 prompt 长度增长 | 低,按 chunk 粒度封顶 |
| 同批 decode 的 TPOT | 被长 prompt 阻塞,抖动大 | 每步都排到,p95 收窄 |
| 长 prompt 的 TTFT | 在单步内完成 prefill | prefill 摊到多步,TTFT 上升 |
| 短请求的 TTFT | 受长 prompt 到达影响 | 略升,每步要匀出 decode token |
| 每步固定开销占比 | 低 | 升高,chunk 越碎越明显 |
代价在于:prefill 摊到多步后,长 prompt 的首 token 要等所有 chunk 算完才产生,TTFT 上升;同时每步混入 prefill 工作,decode 的单步耗时略升,但由抖动转为稳定。
与显存预算的关系
--max-num-batched-tokens 同时决定 prefill 的激活峰值。取值越大,单步 batch 越大,激活与工作区占用越高,留给 KV 池的预算越少。分块把单步 batch 压小,激活峰值下降,为 KV 让出空间;但每步的固定开销被摊到的 token 变少,占比上升。TTFT、TPOT、显存预算三项由同一个参数牵动,调它不能只看一项。这也是分块常与 --max-num-seqs 联动的原因:并发压低后,同批 decode 序列减少,长 prompt 受干扰更小。
收益窗口
| 负载形态 | 分块收益 | 说明 |
|---|---|---|
| 长 prompt 占比高 + 高并发 | 明显 | 有大量 decode 序列需要被保护,抖动被压平 |
| 长短交错混合 | 明显 | 长 prompt 到达不再长时间阻塞短请求 |
| 纯短请求 | 无收益甚至有害 | 没有长 prefill 可切,分块只增加每步调度与固定开销 |
| 单请求或中低并发 | 接近于零 | 没有同批 decode 需要保护 |
判定收益是否成立,要绑定请求长度分布与并发记录。缺少对照数据时,不能把「开关可用」写成「收益成立」。
分块常与 prefix caching 一起评估。prefix caching 减少 prefill 的实际计算量,分块改变 prefill 的调度方式,两者作用不重叠,收益不能互相代入。同时开启前须核对上文列出的后端约束。
现象
- 开启后 TTFT 上升、TPOT 变稳,p95 收窄,端到端吞吐变化不大或略升。
- 设备利用率可能下降:chunk 变小时,每步固定开销占比上升,利用率曲线出现空档。
- 纯短请求负载下 TTFT 与 TPOT 同时略升,收益不成立。
- 参数窗口一变(token 预算或并发变化)收益退化,结论随窗口漂移。
- 并发升高时 TPOT 又出现抖动,说明 chunk 粒度相对当时的 mix 比例已偏粗。
定位步骤
- 核对开关与生效值:读启动日志里 chunked prefill 与
--max-num-batched-tokens的生效取值,确认实际生效的 token 预算,命令行惯用取值不一定等于生效值。 - 分段量指标:分别取 TTFT 与 TPOT / ITL,做开关前后对照,不能只看端到端吞吐。
- 算混批比例:按步统计 prefill token 与 decode token 的占比,确认分块后每步是否真的混入了解码工作。
- 看 prompt 长度分布:长 prompt 占比是否足够,短请求占比是否过高。
- 判定收益落点:收益落在 TPOT(抖动收敛)还是 TTFT(首 token 更快),两者常不同向。
混批比例可以从调度日志读出。vLLM 的按步摘要是 Running: %d reqs, Waiting: %d reqs 形态,配合 prompt 与 generation 两侧的吞吐即可判断该步是否混入了解码工作:
# 字段名示意:一步的调度摘要(vLLM 口径),纯 prefill、纯 decode 与混批三种形态
Running: 1 reqs, Waiting: 0 reqs # 纯 prefill:prompt 吞吐非零、generation 吞吐为零
Running: 64 reqs, Waiting: 0 reqs # 纯 decode:generation 吞吐非零、prompt 吞吐为零
Running: 66 reqs, Waiting: 0 reqs # 混批:prefill 序列与 decode 序列同处运行中集合,两侧吞吐同时非零
Running 是该步运行中集合的序列数,Waiting 是等待队列长度。纯 prefill 步只有 prompt 吞吐非零,纯 decode 步只有 generation 吞吐非零,两侧同时非零即该步为混批,据此判断分块后每步是否真的混入了解码工作。
处置
| 动作 | 作用 | 代价 / 前提 |
|---|---|---|
打开 --enable-chunked-prefill |
长 prefill 切块,与 decode 混批 | 调度更复杂,须确认版本与后端支持 |
--max-num-batched-tokens 调小 |
chunk 变细,TPOT 更稳 | TTFT 更高,每步固定开销占比上升 |
--max-num-batched-tokens 调大 |
chunk 变粗,TTFT 更低 | 单步更长,decode 抖动回升 |
--max-num-seqs 调小 |
减少同批 decode,长 prompt 少受干扰 | 峰值并发下降 |
| 纯短请求负载关闭分块 | 去掉每步调度与固定开销 | 长 prompt 到达时抖动回归 |
记录字段
# 一条 chunked prefill 取舍记录(字段名示意)
chunked_prefill:
enabled: # --enable-chunked-prefill
max_num_batched_tokens: # 生效 token 预算,即 chunk 粒度上限
effective_budget: # 日志中实际生效值
chunk_granularity: # 单块 token 数(派生)
prefill_decode_mix_ratio: # 每步 prefill token / decode token
workload:
model_name:
request_pattern: # 长 prompt 占比、长短交错、纯短
prompt_len_distribution: # 输入长度分布
concurrency: # 并发
max_num_seqs:
perf:
ttft_before:
ttft_after:
tpot_before: # 均值与 p95
tpot_after:
device_util_trace: # 利用率形态
gain:
window: # 收益成立的负载窗口
side_effect: # TTFT 上升 / 利用率下降 / 无副作用
status: # 收益成立 / 收益被抵消 / 无收益
小结
Chunked Prefill 的切块粒度由 --max-num-batched-tokens 约束,作用是把长 prompt 的 prefill 摊到多步,与 decode 混批。它把长 prompt 对同批解码的阻塞消掉,换来 TPOT 的稳定,代价是长 prompt 的 TTFT 上升。收益窗口是长 prompt 占比高且并发高的混合负载;纯短请求下没有可切的长 prefill,分块只增加每步固定开销,收益不成立。判定时先分段量 TTFT 与 TPOT,再算混批比例,最后把结论绑定到请求长度分布与并发窗口。