← Back to homepage

1.1 Continuous Batching(连续批处理)

在大语言模型(LLM)推理服务中,如何高效地将多个请求组织在一起进行批量推理,是决定系统吞吐量与延迟的核心问题。传统的静态批处理方式存在严重的资源浪费,而连续批处理(Continuous Batching)通过在每一次迭代(iteration)粒度上动态调度请求,从根本上解决了这一瓶颈。本节将从静态批处理的缺陷出发,逐步推导出连续批处理的设计思路,并深入分析其关键参数与代码实现。

1.1.1 静态批处理的问题

什么是静态批处理

静态批处理(Static Batching)是最朴素的批量推理策略:推理引擎收集若干请求,将它们组成一个固定的批次(batch),然后让这个批次中的所有请求从头到尾一起执行,直到批次内所有请求都生成完毕后,才将结果统一返回,并开始接收下一个批次。

时间轴 →

Batch 1: [Req A, Req B, Req C]  全部完成 → 返回结果
                                              ↓
Batch 2: [Req D, Req E]         全部完成 → 返回结果

核心问题:长短不齐导致的 GPU 空转

LLM 的自回归生成有一个关键特点——不同请求的输出长度差异极大。一个简单的问答可能只需要生成 10 个 token,而一篇摘要可能需要 500 个 token。在静态批处理中,短请求生成完毕后,必须等待同批次中最长的请求结束,GPU 在这段等待时间内要么完全空转,要么在对已完成请求做无意义的 padding 计算。

假设 Batch 中有 4 个请求,生成长度分别为:

  Req A:  20 tokens   ████░░░░░░░░░░░░░░░░░░░░░░  (step 1-20)
  Req B:  50 tokens   ████████████░░░░░░░░░░░░░░  (step 1-50)
  Req C: 100 tokens   ██████████████████████████  (step 1-100)
  Req D:  10 tokens   ██░░░░░░░░░░░░░░░░░░░░░░░░  (step 1-10)

  ░ = GPU 空转 / padding 计算

整个 Batch 必须运行 100 步才能结束。
GPU 有效利用率 = (20+50+100+10) / (4×100) = 180/400 = 45%
55% 的算力被完全浪费了。 在生产环境中,输出长度的方差往往更大,这一浪费比例可能达到 70% 以上。

延迟的放大效应

考虑一个实际场景:

请求的等待时间可能远大于其实际计算时间,这对用户体验是不可接受的。

小结

静态批处理的两个根本缺陷:

  1. 资源浪费:短请求被迫等待长请求,GPU 空转或做 padding 计算,有效利用率低。

  2. 延迟膨胀:新请求无法插入正在执行的批次,必须等当前批次整体完成,排队延迟不可控。

这两个问题的根源在于同一个设计决策:批次的生命周期与批次内最长请求绑定。要打破这一限制,就需要将调度粒度从"批次级"下沉到"迭代级"。

1.1.2 迭代级批处理与队头阻塞的消除

从批次级到迭代级

连续批处理的核心思想来自 2022 年由 Orca 论文(Yu et al.)提出的迭代级调度(Iteration-Level Scheduling):不再以整个批次的生命周期为调度单位,而是在每一步解码迭代(即每生成一个 token)结束时重新审视批次的组成。

其核心规则极其简洁:

时间轴 →   step1  step2  step3  step4  step5  step6  step7  step8 ...

  Req A:    ██     ██     ██     Done
  Req B:    ██     ██     ██     ██     ██     Done
  Req C:    ██     ██     ██     ██     ██     ██     ██     ██ ...
  Req D:                         ██     ██     Done           ← step4 加入
  Req E:                                       ██     ██ ... ← step6 加入

批次大小动态变化:3 → 3 → 3 → 3 → 3 → 3 → 2 → 2 ...

与静态批处理相比,关键变化在于:

队头阻塞(Head-of-Line Blocking)的消除

"队头阻塞"是网络领域的经典概念——队列最前面的元素处理缓慢时,会阻塞后面所有元素的处理。静态批处理天然具有这种队头阻塞特性:一个超长请求会拖慢同批次内所有其他请求的返回。连续批处理通过迭代级调度,彻底消除了队头阻塞:

维度静态批处理连续批处理
短请求延迟受同批次最长请求影响生成完毕立刻返回
新请求等待必须等当前批次结束下一步迭代即可加入
GPU 利用率随长短差异增大而下降始终接近满载
队头阻塞严重不存在

Prefill 与 Decode 的混合调度

连续批处理还引入了一个重要的设计考量:当新请求加入批次时,它需要先经历 Prefill 阶段(处理整个输入 prompt),而批次中已有的请求正处于 Decode 阶段(每步只生成一个 token)。这两种操作的计算特性截然不同:

在连续批处理框架下,Prefill 和 Decode 可以在同一个 batch step 内混合执行。调度器需要在两者之间做权衡:如果让过多新请求同时进行 Prefill,单步延迟会被拉高,影响正在 Decode 的请求的 TPOT(Time Per Output Token);如果过度限制 Prefill,新请求的 TTFT(Time To First Token)会增大。

现代推理引擎(如 vLLM)通过 max_num_batched_tokens 等参数来控制单步内 Prefill 的 token 总量,从而在 TTFT 和 TPOT 之间寻找平衡。

1.1.3 max_num_seqs 与 max_num_batched_tokens 的相反影响

连续批处理的调度器有两个核心控制旋钮,它们从不同维度约束了批次的规模,且对系统性能的影响方向恰好相反。理解这两个参数的交互关系,是调优推理服务性能的关键。

max_num_seqs:序列数上限

max_num_seqs(在某些框架中也叫 max_batch_size)定义了同时存在于批次中的请求(序列)数量上限

max_num_seqs = 4 时:

  Batch 最多同时包含 4 个序列
  ┌──────────────────────┐
  │ Seq 0 (decode)       │
  │ Seq 1 (decode)       │
  │ Seq 2 (prefill)      │
  │ Seq 3 (decode)       │
  └──────────────────────┘
  第 5 个请求必须在队列中等待

增大 max_num_seqs 的影响

max_num_batched_tokens:单步 Token 总量上限

max_num_batched_tokens 定义了单步迭代中参与计算的 token 总数上限。这个数字是所有序列在当前步的 token 数之和:处于 Decode 阶段的序列贡献 1 个 token,处于 Prefill 阶段的序列贡献其 prompt 长度个 token。

max_num_batched_tokens = 2048 时:

  假设当前批次有 3 个 decode 序列 (各贡献 1 token)
  token 预算剩余 = 2048 - 3 = 2045
  → 可以再接入一个 prompt 长度 ≤ 2045 的新请求做 Prefill
  → 或接入多个短 prompt 的新请求

相反影响的本质

参数增大时对吞吐的影响增大时对延迟的影响瓶颈维度
max_num_seqs↑ 提高TPOT 略增显存容量(KV Cache)
max_num_batched_tokens↑ 提高(Prefill)TPOT 显著恶化计算量 / 单步耗时

它们的"相反影响"体现在实际调优中的权衡:

场景一:长对话、短输出(如客服机器人) — Prompt 长、输出短,Prefill 是瓶颈。应适当增大 max_num_batched_tokens 以提高 Prefill 吞吐,max_num_seqs 可以适中,因为序列存活时间短。

场景二:短输入、长输出(如代码生成) — Prefill 很快,Decode 阶段长。应增大 max_num_seqs 以并行处理更多序列,max_num_batched_tokens 保持适中,避免拖慢 Decode 的 TPOT。

场景三:严格延迟要求(如实时交互) — 两个参数都不宜过大,需要通过压力测试找到 TPOT ≤ 目标值时的最大 batch 规模。

参数间的隐式联动

需要特别注意的是,这两个参数并非独立生效,而是存在隐式的联动关系:

实际单步 token 数 = min(
    max_num_batched_tokens,
    sum(各序列当前步的 token 数)  ← 受 max_num_seqs 间接约束
)

max_num_seqs 很小时,即使 max_num_batched_tokens 设得很大,实际的 token 总量也不会太高(除非某个 prompt 极长)。反之,当 max_num_seqs 很大且都在 Decode 阶段时,每步 token 总量 ≈ max_num_seqs(每个序列贡献 1 token),此时 max_num_batched_tokens 几乎不起约束作用。

真正的调优需要将两个参数结合目标硬件的显存容量和计算能力一起考虑,而不是孤立地调整某一个。

© Xiaoyi | Homepage