完整文档索引见 llms.txt。 在任意 URL 后追加 `.md` 即可查看该页面的 Markdown 版本。
静态、动态与连续批处理
GPU 天生适合高度并行的计算工作负载,能够每秒执行数万亿甚至千万亿次浮点运算(FLOPs)。但在 LLM 场景中,这些 GPU 往往无法被充分利用,因为芯片的大量内存带宽都消耗在加载模型参数上。
批处理有助于缓解这个瓶颈。在生产环境中,你的服务可能会同时涌入多个请求。与其逐个处理,不如把它们合并为一个批次,这样多个请求就能共享同一份已加载的模型参数,从而显著提升吞吐量。
使用下面的模拟器可以从高层理解不同批处理策略。
静态批处理
最简单的批处理形式是静态批处理。在这种方式下,服务端会等待直到固定数量的请求到达,然后把它们作为一个批次统一处理。
虽然静态批处理很容易实现,但它也有明显缺点。
- 批次中的第一个请求必须等待最后一个请求到达,造成不必要的延迟。你可以把它想象成一台打印机:只有等你排队到固定数量的文档后,它才开始打印,而不管最后一份文档多久之后才到。
- 批次中的请求并不都是等价的。在 LLM 推理中,有些请求可能只生成非常短的回复,而另一些请求可能涉及冗长、分步的推理。由于批次中所有请求都必须等到最慢的那个结束,这会导致计算资源浪费并增加延迟。
动态批处理
为了解决静态批处理中的问题,许多系统使用动态批处理。这种方法仍然会把到达的请求收集成批次,但不再坚持固定批大小。相反,它设置一个时间窗口,并处理这段时间内到达的所有请求。如果批次更早达到大小上限,就会立即启动。这就像一辆按固定时刻发车、或者一坐满就发车的公交车,以先发生者为准。
动态批处理有助于在吞吐量和延迟之间取得平衡。它确保较早到达的请求不会被后续请求无限期拖延。不过,因为某些批次在启动时可能还没有完全填满,它并不总能实现最高 GPU 效率。另一个缺点是,它和静态批处理一样,仍然由批次中最长的请求决定整个批次何时结束;较短请求仍然会被不必要地等待。
连续批处理
在 LLM 推理中,输出序列的长度差异很大。有些用户可能只问简单问题,而另一些用户会要求详细解释。静态批处理和动态批处理都会让短请求等待最长请求完成,从而使 GPU 资源无法被充分占满。
连续批处理(也称为 in-flight batching)正是为了解决这些低效问题。连续批处理不会强迫整个批次完成后再返回结果。相反,它允许批次中的每条序列独立完成,并在完成后立即用新的序列补上。这就像一条装配线:一旦某个工件完成,不管它花了多久,都会立刻放入新的工件,以保持整条线满负荷运转。

使用连续批处理生成七条序列。第一次迭代(左)中,每条序列都会基于其提示词(黄色)生成一个 token(蓝色)。随着时间推进(右),不同序列会在不同迭代中通过发出序列结束 token(红色)而完成,此时新的序列会被插入进来。图片来源
这项技术采用按迭代调度,也就是说批次的组成会在每次解码迭代时动态变化。只要批次中的某条序列完成了 token 生成,服务端就会立刻插入一个新请求顶替它。这样可以最大化 GPU 占用率,并通过避免等待批次中最慢序列结束所产生的空闲时间,让计算资源始终保持忙碌。
主流 推理框架 如 vLLM、SGLang、TensorRT-LLM(in-flight batching)和 LMDeploy(persistent batching)都支持连续批处理或类似机制。关于长批次或混合长度批次中的内存管理,请参见 PagedAttention。
Chunked prefill
当新请求在其他请求正在解码时到达,连续批处理会引入一个调度冲突。如果在一次预填充迭代中处理完新请求的整个提示词,就能最小化首个 token 时间(TTFT);但较长的预填充也会推迟所有活跃解码请求的下一个 token。在流式应用中,用户可能会看到输出因为处理其他用户的提示词而暂停。
Chunked prefill 会把一个提示词拆分成更小的 token 区间,并分散到多个迭代中调度。每个 chunk 都会扩展该请求的 KV 缓存,后续 chunk 会关注此前已处理的提示词 token。由于注意力计算本身没有变化(每个 token 仍然会通过 KV 缓存关注其之前的所有 token),chunked prefill 在数学上等价于一次性处理整个提示词,而首个 token 仍然只会在最后一个 chunk 完成后才被发出。
调度器可以把一个预填充 chunk 与活跃请求的解码 token 放进同一个批次中。 SARATHI 将这种方式称为 decode-maximal batching:预填充 chunk 提供足够的并行工作去占满 GPU 的计算能力,而解码 token 则以几乎不增加额外成本的方式搭载在同一次模型执行中。这可以防止长提示词独占某一次迭代,也可以减少 pipeline parallelism 下的流水线气泡。
Chunked prefill 的好处在于,即使某个请求有很长的提示词,预填充计算也会被拆成更小的 chunk。活跃解码请求无需等待一次很长的预填充迭代结束,而是可以在各个预填充 chunk 之间继续生成 token。这会降低 Inter-Token Latency(ITL),并让流式响应更平滑。
代价是,chunking 会引入额外的调度和注意力开销,因为提示词不再通过一次大的预填充步骤处理,而是通过多次更小的预填充步骤处理。因此,TTFT 可能会上升,尤其是在 chunk 大小较小时。
大多数推理框架都允许你调节 chunk 大小。需要注意:
- 较小的 chunk 会给调度器更多机会去安排解码,从而降低活跃请求的 ITL 峰值。
- 较大的 chunk 处理新提示词更高效,通常也能改善 TTFT,但活跃解码在 token 之间的等待时间可能更长。
- 过小的 chunk 可能降低 GPU 利用率并增加注意力开销,因为后续 chunk 必须重新读取前面 chunk 创建的 KV 缓存条目。
不存在放之四海而皆准的最佳取值。合适的大小取决于模型、GPU、提示词长度分布、并发度和延迟目标。它应当被视为工作负载特定的调度参数,而不是一个通用常数。
常见问题
LLM 批处理中什么是 padding tokens?
文本序列在 tokenization 后天然具有不同长度。例如:
Sentence A: "Hello world" # [15496, 995] - length 2
Sentence B: "How are you today?" # [2437, 389, 345, 1909] - length 4
你不能直接把这些序列堆叠成一个致密的矩形张量,因为它们的长度不同。padding
的解决方式,是给较短序列补上占位
token,使批次中的每个请求都具有相同的张量长度。常见的 padding token 形式包括
PAD、<pad> 或 token ID 0,但具体 ID 取决于 tokenizer。
attention mask 会告诉模型哪些位置是 padding,但 padding 仍然可能浪费计算和内存。如果一个批次中的序列长度如下:
[1024, 1000, 50, 20]
较短序列可能会被 padding 到长度 1024。这样虽然能让张量形状对 GPU
内核保持规则,但 50 token 和 20 token 的请求现在会携带大量未使用的位置。
LLM 推理中的 ragged tensors 是什么?
Ragged tensors 表示变长序列时,不需要把所有序列都 padding 到一个固定长度。相反,运行时只存储真实 token 以及元数据,例如序列长度、偏移量或 KV 缓存 block 位置。这有助于推理引擎更高效地处理混合长度提示词和连续批处理,尤其是在与 PagedAttention 这类分页 KV 缓存布局结合使用时。