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。

一步能处理的 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);离线最大吞吐取更大预算。取值为参考区间,非实测,须按本机规格与负载复测。

触发条件与约束

分块是否可用与负载类型无关,但受后端与调度器约束,须按所用分支的发布说明核对:

在约束不成立的分支上,先把开关打开再压测,会得到与预期无关的结果。

取舍: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 的调度方式,两者作用不重叠,收益不能互相代入。同时开启前须核对上文列出的后端约束。

现象

定位步骤

  1. 核对开关与生效值:读启动日志里 chunked prefill 与 --max-num-batched-tokens 的生效取值,确认实际生效的 token 预算,命令行惯用取值不一定等于生效值。
  2. 分段量指标:分别取 TTFT 与 TPOT / ITL,做开关前后对照,不能只看端到端吞吐。
  3. 算混批比例:按步统计 prefill token 与 decode token 的占比,确认分块后每步是否真的混入了解码工作。
  4. 看 prompt 长度分布:长 prompt 占比是否足够,短请求占比是否过高。
  5. 判定收益落点:收益落在 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,再算混批比例,最后把结论绑定到请求长度分布与并发窗口。