登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

连续批处理怎样平衡首 token 延迟

来源:17golang原创

时间:2026-10-17 23:42:27 262浏览 收藏

连续批处理的目标不是把批次盲目做大,而是在 GPU 有足够工作可做的同时,让新请求尽快拿到第一个 token。真正需要平衡的是三段时间:请求在队列里等待的时间、输入 prompt 的 prefill 时间,以及首个输出 token 被生成前的调度时间。吞吐通常随 token 预算和并发上升,但 TTFT 的高分位也可能随之变差。

本文只讨论“吞吐与首 token 延迟权衡”这一条边界。Hugging Face 的相关文档把连续批处理描述为每个生成步骤重新调度批次;vLLM 也提供了 token 预算、序列数和排队请求数等调度边界。官方资料入口如下,便于按实现继续查参数:

Transformers 官方地址:https://huggingface.co/docs/transformers/continuous_batching

vLLM 官方地址:https://docs.vllm.ai/en/latest/usage/metrics/

TGI 官方地址:https://huggingface.co/docs/text-generation-inference/basic_tutorials/launcher

先给结论:先观察 TTFT 的 p95 与队列时间,再小步调整 prefill token 预算;只有确认 KV cache 仍有余量、decode 没有被长 prompt 持续打断时,才增加并发或批次上限。

先把 TTFT 拆成三个可观测部分

TTFT(time to first token)不是单一的 GPU 计算耗时。在线服务里,一条请求至少要经过排队、prefill 和首 token 生成这三个阶段。排队时间过长,说明调度器没有及时接纳请求;prefill 时间过长,常见原因是输入很长或单次 token 预算过大;首 token 生成时间异常,则要继续看 KV cache、显存压力和调度切换。

连续批处理把请求、等待队列、prefill、KV cache、首 token 和 decode 组织成三个观测分组的静态说明图
图1:TTFT 的静态分解说明图,展示请求、队列、prefill、KV cache、首 token 与 decode 的关系,不是运行截图或性能证据。

建议至少同时记录以下维度:

指标回答的问题偏高时先看什么
队列时间 p95请求是否卡在进入运行态之前活动请求上限、排队上限和调度策略
prefill 时间输入 token 是否占满一次前向预算prompt 长度、token 预算、chunked prefill
TTFT p95用户多久看到第一个 token前两项叠加后的尾部表现
TPOT/ITL首 token 后的输出是否稳定decode 批次、KV cache 和抢占

不要只用平均值做调参结论。短请求可能掩盖长 prompt 的排队尾部,而平均吞吐上升也不能证明交互式请求变快。

用 token 预算控制 prefill 对 decode 的影响

连续批处理会在生成过程中不断接纳和移出请求,但 prefill 仍然需要一次性处理输入 token。token 预算越大,一次前向可以塞进更多 prompt,通常有利于 prefill 吞吐;代价是该轮占用更多显存和计算时间,正在 decode 的请求可能更久才能获得下一次调度。

以 Transformers 的配置为例,max_batch_tokens 控制单次前向可处理的 query token 预算。下面是一个用于说明参数关系的最小片段:

from transformers.generation import ContinuousBatchingConfig

# 用显存比例约束 KV cache,避免只追求更大的批次。
cb_config = ContinuousBatchingConfig(
    max_memory_percent=0.80,
    block_size=256,
    max_batch_tokens=8192,
    max_requests_per_batch=128,
    scheduler_type="fifo",
    safety_margin=0.15,
)

# 这里的配置只描述调度和显存边界,生成长度仍由 GenerationConfig 控制。
outputs = model.generate_batch(
    inputs=inputs,
    generation_config=generation_config,
    continuous_batching_config=cb_config,
)

参数名会随框架版本和实现变化,关键不是照抄某个数,而是理解三个方向:

  • prefill-bound 负载中,适当增加 token 预算可以减少分批次数,但要观察 TTFT p95 是否变长。
  • 交互式短请求较多时,较小的 token 预算通常更容易保护正在 decode 的请求;代价是长 prompt 的吞吐可能下降。
  • 超长 prompt 不应简单地把全局预算继续放大,可以用分块 prefill 或请求级长度上限把影响摊开。

vLLM 中常见的对应边界是 max_num_batched_tokens 和 max_num_scheduled_tokens。前者约束一次迭代最多处理的 token,后者约束调度器一次发出的 token 数。不要把它们理解成“越大越快”的开关:如果 decode 请求已经拥挤,增加预算可能先改善吞吐,随后推高 TTFT 尾部。

用 KV cache 和请求上限守住并发边界

连续批处理的可接纳请求并不只由请求数量决定。每条请求的 prompt token、预计生成 token 和 KV cache block 都会占用显存。长输入加上较大的 max_new_tokens,可能让少量请求就吃掉大部分缓存,随后新请求只能等待、被拒绝或触发卸载。

连续批处理中 token 预算、请求上限、KV cache 与等待请求之间的静态关系说明图
图2:输入预算、调度边界和显存资源的关系说明图,用于理解容量取舍,不表示某次真实压测结果。

可以把容量判断写成一个简单的工程检查表:

  1. 先统计 prompt token 和生成上限的分布,不要只看请求数。
  2. 确认 KV cache 的可用 block 是否会因长请求持续减少。
  3. 用 max_requests_per_batch 或 max_num_seqs 限制单轮请求数,防止 logits 和每请求缓冲区突然膨胀。
  4. 为 decode 留出安全余量;当缓存接近边界时,宁可延迟新 prefill,也不要让正在输出的请求整体抖动。

这也是为什么调大 batch token 预算后,必须同时复查显存和并发。Transformers 的连续批处理文档把 token budget、KV cache budget 和 request cap 分开描述;它们是相互消耗同一块硬件资源的不同边界。

按负载选择 FIFO 或 prefill-first

调度策略决定了“谁先占用下一次 forward”。FIFO 更适合希望按到达顺序推进、并优先完成已有 decode 的场景;prefill-first 则更适合长 prompt 持续进入、需要尽快把分块 prefill 做完的场景。两者没有脱离负载的绝对优劣。

可以按下面的信号选择方向:

负载特征优先尝试主要风险
短 prompt、交互式请求多FIFO、小一些的 prefill 预算长 prompt 吞吐偏低
长 prompt、批量生成多prefill-first、分块 prefill短请求 TTFT 尾部上升
输出长度差异很大请求上限加 KV 安全余量缓存不足导致等待或卸载

以 Transformers 的配置表达时,调度器是硬件和队列策略的一部分,而不是生成参数的一部分。把 GenerationConfig 的采样和停止设置,与 ContinuousBatchingConfig 的缓存和调度设置分开管理,排查时会清楚很多。

from transformers.generation import ContinuousBatchingConfig

# 长 prompt 较多时才尝试 prefill-first;不要与 FIFO 的结论混用。
cb_config = ContinuousBatchingConfig(
    scheduler_type="prefill_first",
    max_batch_tokens=8192,
    max_requests_per_batch=64,
    safety_margin=0.0,
)

# 先用同一组请求分布比较 p50、p95 和显存,再决定是否保留该策略。
outputs = model.generate_batch(
    inputs=inputs,
    generation_config=generation_config,
    continuous_batching_config=cb_config,
)

这里的数值只是配置示意,不是通用推荐值。尤其是 safety_margin、请求上限和 token 预算,应通过固定的请求样本做对照实验。

用分位数指标验证平衡是否成立

一次改动至少要同时看吞吐、TTFT 和 decode 阶段。建议固定模型、硬件、采样参数、输入长度分布和输出上限,只改变一个调度变量,再比较:

  • TTFT p50/p95:分别反映典型请求和尾部请求看到首 token 的时间。
  • 队列时间 p95:判断问题发生在接纳之前,还是发生在 GPU 计算阶段。
  • TPOT 或 ITL:判断首 token 之后的持续输出是否因为 prefill 抢占而抖动。
  • 生成吞吐:按 output tokens/s 观察批次是否真的提高了有效产出。
  • KV cache 使用与抢占:确认吞吐收益不是用频繁卸载、重算或更高失败率换来的。

vLLM 官方指标中有 time_to_first_token_seconds、请求队列时间、prefill 时间、decode 时间和生成 token 数等维度;TGI 也提供批次大小、forward duration、inference duration 等批次指标。实际接入时,优先把这些指标按模型、请求长度和调度策略分组,而不是只画一条全局平均曲线。

一个稳妥的调参顺序是:

  1. 先把活动请求、最大排队请求和最大输入长度设成明确边界。
  2. 用较保守的 token 预算取得基线,记录 TTFT p50/p95、队列时间和吞吐。
  3. 每次只增加一档 prefill token 预算,观察 TTFT p95 是否超过业务目标。
  4. 若吞吐上升但 TTFT 尾部恶化,先回退预算,再考虑请求分类或独立服务池。
  5. 只有在缓存余量稳定时,才提高并发请求数。

常见问题

批次越大,TTFT 一定越低吗?

不一定。更大的 prefill 预算可能提升吞吐,但会拉长当前 forward 或让 decode 请求等待下一轮,TTFT 的 p95 可能上升。

为什么平均 TTFT 没变,p95 却变差?

通常是长 prompt、缓存紧张或少量排队请求形成了尾部。平均值被大量短请求稀释,分位数才能暴露这类不均衡。

应该先调并发还是先调 token 预算?

先固定并发,调 token 预算并观察 TTFT、TPOT 和缓存;确认缓存有余量后再逐步提高并发,否则很难判断恶化来自哪一个变量。

连续批处理适合所有请求混在一个池里吗?

不一定。交互式短请求和长上下文离线任务的目标不同。若一个调度策略无法同时满足两者的 TTFT 和吞吐目标,可以按请求类型拆分服务池。

总结

连续批处理的平衡点不是某个固定 batch size,而是“在缓存边界内,用足够的 token 预算保持 GPU 忙,同时给等待和 decode 请求保留可预测的服务机会”。先拆 TTFT,再看 token 预算、KV cache 和请求上限,最后用 p95 与吞吐一起做决定,通常比单独追求 GPU 利用率更可靠。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>