背景:为什么传统固定批处理不适合 LLM 在线服务
在传统深度学习推理中,batching 的直觉很简单:把多个请求凑成一个批次,一次送进 GPU,提高并行度。但大模型文本生成服务并不是一次前向计算就结束的事。一个请求通常包含两个阶段:
- Prefill 阶段:处理完整 prompt,计算第一批 KV Cache。
- Decode 阶段:每轮生成一个或少量 token,并持续读取、追加 KV Cache。
问题在于,不同用户请求的 prompt 长度、输出长度、到达时间差异很大。如果使用传统固定批处理,系统通常会遇到三个核心问题:
- 短请求被长请求拖住:同一批请求需要近似同步执行,短请求即使已经完成,也可能等到整批结束才能释放位置。
- 新请求等待批次结束:批次运行期间,新到请求不能马上进入 GPU,只能在队列中等待。
- padding 和 KV Cache 浪费严重:为了对齐不同长度的序列,系统会产生大量无效计算或显存占用。
ORCA 论文把这个问题描述得很直接:生成式 Transformer 推理具有多轮迭代特征,传统调度不能在当前批次运行时改变请求集合,导致已完成请求不能及时返回、新请求必须等待当前批次完全结束。ORCA 的核心改进是 iteration-level scheduling,也就是以每一轮生成迭代为单位重新组织批次。
这正是 Continuous Batching 的出发点。
Continuous Batching 的核心思想
Continuous Batching,也常被称为 in-flight batching 或 iteration-level batching。它不是简单把 batch size 调大,而是从根本上改变了调度粒度。
传统 batching 的粒度是**「请求级」**:
batch = [request_a, request_b, request_c]
run until all requests finish
return results
start next batch
Continuous Batching 的粒度是**「生成迭代级」**:
while server_running:
remove_finished_requests()
add_new_requests_if_capacity_available()
schedule_prefill_and_decode_tokens()
run_one_or_more_model_steps()
stream_completed_tokens()
这意味着系统可以在 token generation loop 的每一步动态调整活跃请求集合:完成的请求马上退出,新来的请求只要有容量就能进入。TensorRT-LLM 的 Batch Manager 文档也把 in-flight batching 描述为一种减少队列等待、消除 padding 请求、提高 GPU 利用率的机制,并且允许在每次 token generation loop 迭代中纳入新请求、返回已完成请求。
从工程角度看,Continuous Batching 不是一个单点优化,而是一组能力的组合:
| 能力 | 说明 |
|---|---|
| 请求池管理 | 维护 waiting、running、finished、aborted 等状态 |
| 迭代级调度 | 每轮决定哪些请求执行 prefill、哪些请求执行 decode |
| token budget 控制 | 限制每轮可处理的 token 总量,避免长 prompt 抢占所有资源 |
| KV Cache 分配与回收 | 请求进入、增长、完成、取消时都要动态管理 KV Cache |
| 流式返回 | decode 阶段生成 token 后尽快推给客户端,而不是等待整批结束 |
为什么 PagedAttention 经常和 Continuous Batching 一起出现
Continuous Batching 解决的是「请求如何进入和离开批次」的问题,但它会进一步放大另一个问题:KV Cache 是动态增长的,且生命周期高度不规则。
每个请求都会维护自己的 KV Cache。请求越长,KV Cache 越大;请求完成、取消、超时后,KV Cache 又要释放。如果直接使用连续大块显存管理,系统很容易产生碎片。碎片一多,可用显存看似还有,但无法容纳新的请求,最终限制 batch size 和吞吐。
PagedAttention 的思路是借鉴操作系统里的虚拟内存分页,把 KV Cache 切成块来管理。PagedAttention 论文指出,LLM serving 的 KV Cache 很大,并且会动态增长和收缩;低效管理会因为碎片和重复复制浪费大量内存,从而限制 batch size。PagedAttention 通过块级管理让 KV Cache 接近零浪费,并支持请求内和请求间共享 KV Cache。
两者的关系可以这样理解:
- Continuous Batching 让请求集合在运行中动态变化。
- PagedAttention 让动态变化的 KV Cache 更容易被分配、复用和释放。
- 两者结合,才能在高并发、长短请求混合的真实服务中稳定提升吞吐。
这也是为什么 vLLM 会同时强调 PagedAttention、continuous batching、chunked prefill、prefix caching 等能力。单独看某一项优化容易误判,真正上线时要把它们放在同一个调度系统里评估。
一条请求在 Continuous Batching 系统中的生命周期
1. Admission:进入等待队列
请求到达后,系统不会立即执行,而是先进入等待队列。调度器需要检查:
- 当前 GPU 是否还有可用 KV Cache block。
- 当前 active request 数是否达到上限。
- 当前 token budget 是否允许新增 prompt token。
- 请求是否带有优先级、超时、租户或 SLA 约束。
如果只用 max_batch_size 控制容量,很容易出问题。LLM 服务更应该同时控制请求数和 token 数。一个 32K prompt 请求对系统的压力,远大于几十个短 prompt 请求。
2. Prefill:计算上下文
Prefill 阶段通常是 compute-heavy。长 prompt 会消耗大量算力,并推高 TTFT(Time To First Token)。如果连续批处理把大量长 prompt 一次性塞进 GPU,decode 请求可能被挤压,导致正在流式输出的用户感觉「卡顿」。
因此,生产环境常把 Continuous Batching 和 chunked prefill 结合:长 prompt 不一次性处理完,而是拆成多个 chunk,与 decode 请求穿插执行。这样可以减少 prefill 对 decode 的阻塞。
3. Decode:逐 token 生成
Decode 阶段通常更接近 memory-bound,因为每步都要读取已有 KV Cache。Continuous Batching 的主要收益之一,就是让 decode 阶段保持较高并发:当某些请求结束后,新请求可以补进来,GPU 不会因为批次尾部几个长输出请求而空转。
但 decode 并不是越塞越多越好。活跃序列太多时,KV Cache 读取压力会上升,ITL(Inter-Token Latency)和尾延迟可能恶化。
4. Completion / Abort:释放资源
请求完成、客户端断开、超时或被取消后,系统必须及时释放:
- KV Cache block。
- 调度器中的 running slot。
- 流式连接资源。
- trace、metrics、日志上下文。
这一步经常被低估。取消请求如果不能及时释放 KV Cache,会让线上显存使用率异常升高,进而影响后续请求 admission。
生产落地时最容易犯的误区
误区一:只看吞吐,不看 TTFT 和 ITL
Continuous Batching 很容易提升总吞吐,但吞吐不是唯一指标。在线服务至少要拆开看:
| 指标 | 含义 |
|---|---|
| TTFT | 用户多久看到第一个 token |
| ITL | 后续 token 间隔是否稳定 |
| TPOT | 每个输出 token 的平均耗时 |
| P95/P99 latency | 尾部请求是否被拖慢 |
| Queue waiting time | 延迟来自排队还是模型执行 |
如果只看 tokens/s,系统可能在压测中很好看,但真实用户会感觉首字慢或流式输出抖动。
误区二:把 max batch size 当成唯一旋钮
传统推理常调 batch size,但 LLM serving 要调的是一组约束:
scheduler:
max_num_seqs: 128
max_num_batched_tokens: 8192
max_model_len: 32768
prefill_chunk_size: 2048
kv_cache_utilization_limit: 0.85
waiting_timeout_ms: 2000
这些参数之间互相制约:
max_num_seqs太大 → decode 并发高,但 KV Cache 压力更大。max_num_batched_tokens太大 → prefill 吞吐高,但可能压制 decode。max_model_len太大 → 单请求上限高,但 admission 更保守。kv_cache_utilization_limit太激进 → 容易在尾部负载下触发 OOM 或频繁拒绝。
误区三:把 benchmark 结果直接搬到生产
压测脚本常用固定输入长度、固定输出长度、固定到达率。但真实业务里,请求分布通常是长尾的:
- 有些用户只问一句话。
- 有些 Agent 会塞入几十 KB 工具上下文。
- 有些任务输出十几个 token。
- 有些任务输出上千 token。
- 有些客户端会中途断开。
因此,生产压测不能只跑「平均长度」。至少要构造短 prompt、长 prompt、短输出、长输出、混合到达、突刺流量、取消请求几类场景。
误区四:忽略多租户公平性
Continuous Batching 会让系统更积极地填满 GPU,但如果没有公平调度,大租户或长 prompt 任务可能挤压小租户。常见做法包括:
- 按租户设置并发上限。
- 按请求类型设置 token budget。
- 对交互式请求和离线任务分队列。
- 对长上下文请求设置更严格的 admission 策略。
- 使用 deadline-aware 或 SLA-aware 调度策略。
上线前的检查清单
调度参数
- 是否同时设置 active request 上限和 batched token 上限。
- 是否限制单请求最大输入长度和最大输出长度。
- 是否区分 prefill token budget 与 decode token budget。
- 是否启用或评估 chunked prefill。
- 是否对请求取消、超时、客户端断连做了资源释放测试。
显存与 KV Cache
- 是否监控 KV Cache block 使用率、空闲数和回收速度。
- 是否监控 admission failure 的原因:显存不足、token budget 不足、并发满、超时。
- 是否压测长上下文混合流量。
- 是否验证 prefix caching、LoRA、speculative decoding 等功能与调度器的组合影响。
延迟指标
- 是否拆分 TTFT、ITL、TPOT、queue time、model execution time。
- 是否看 P50、P95、P99,而不是只看平均值。
- 是否单独观察长 prompt 对短请求的影响。
- 是否观察高负载下 streaming token 的抖动。
容错与降级
- GPU 显存接近阈值时,是否拒绝新请求而不是直接 OOM。
- 是否支持按租户、模型、请求类型限流。
- 是否支持快速下调 max_num_seqs 或 token budget。
- 是否在异常时保留 trace id,便于定位具体请求。
一个更稳妥的调参顺序
生产调优不要一开始就追求最大吞吐。建议按下面顺序推进:
第一步:建立基线
先用保守配置跑通服务:
max_num_seqs: 32
max_num_batched_tokens: 4096
max_model_len: 8192
kv_cache_utilization_limit: 0.75
记录不同场景下的 TTFT、ITL、吞吐、显存使用率。
第二步:提高 decode 并发
逐步增加 max_num_seqs,观察 ITL 是否恶化。如果吞吐提升明显、ITL 稳定,可以继续提高;如果 ITL 波动变大,说明 decode 阶段已经接近瓶颈。
第三步:控制 prefill 冲击
逐步调整 max_num_batched_tokens 和 chunked prefill 参数。目标不是让 prefill 一次跑得最大,而是让 prefill 不阻塞 decode。
第四步:加入真实分布
使用真实日志抽样构造压测数据,至少包含:
- 输入长度分布。
- 输出长度分布。
- 到达率分布。
- 请求取消比例。
- 租户分布。
- 工具调用或 Agent 上下文分布。
第五步:设置保护线
为线上服务设置明确保护线:
slo:
ttft_p95_ms: 1500
itl_p95_ms: 120
queue_wait_p95_ms: 500
kv_cache_utilization_max: 0.88
admission_reject_rate_max: 0.01
当指标触发保护线时,优先降级 admission 或长上下文请求,而不是继续扩大 batch。
适用场景与边界
适合的场景
- 在线聊天、代码补全、Agent 服务等请求持续到达的场景。
- 输入输出长度差异较大的混合流量。
- 流式输出服务。
- GPU 利用率不足、队列等待明显的服务。
- 需要在吞吐和延迟之间动态平衡的服务。
需要谨慎的场景
- 极低延迟、低并发的小模型服务 — 调度开销可能抵消收益。
- 超长上下文占比很高,且没有 chunked prefill 或 prefix caching。
- 多租户强隔离场景,但缺乏公平调度。
- 显存已经非常紧张,KV Cache 管理不完善。
- 使用复杂组合优化(Multi-LoRA、speculative decoding、guided decoding),同时缺乏独立压测。
结论
Continuous Batching 的价值不在于「把 batch size 调大」,而在于把 LLM 生成服务从请求级固定批处理改造成迭代级动态调度。它让完成的请求及时退出,让新请求尽快进入,并配合 KV Cache 管理减少显存浪费。
但它也会把调度、显存、SLO、公平性问题暴露得更明显。真正可上线的 Continuous Batching,需要同时管理 prefill、decode、KV Cache、token budget、请求取消和多租户策略。对于长上下文和 Agent 服务,建议把它与 PagedAttention、chunked prefill、prefix caching 和 SLA-aware admission 一起设计,而不是作为孤立优化项。
主要参考资料
- vLLM Documentation: https://docs.vllm.ai/en/latest/
- TensorRT-LLM Overview: https://nvidia.github.io/TensorRT-LLM/overview.html
- TensorRT-LLM GPT Attention / In-flight Batching: https://nvidia.github.io/TensorRT-LLM/advanced/gpt-attention.html
- TensorRT-LLM Batch Manager: https://github.com/NVIDIA/TensorRT-LLM/blob/main/docs/source/batch_manager.md
- Hugging Face TGI: https://huggingface.co/docs/text-generation-inference/en/index
- ORCA (OSDI ‘22): https://www.usenix.org/conference/osdi22/presentation/yu
- PagedAttention (arXiv): https://arxiv.org/abs/2309.06180
- Memory-aware Dynamic Batching (arXiv): https://arxiv.org/abs/2503.05248
- Ragged Paged Attention (arXiv): https://arxiv.org/abs/2604.15464