基础概念:LLM推理中的批处理、吞吐量、延迟与动态批处理
2.1 为什么LLM推理需要“批处理”
在理解动态批处理之前,必须先回到一个最朴素的资源视角:GPU 是一种极其昂贵的并行计算设备,它拥有数千个计算核心和数十GB的高带宽显存。如果一次只处理一个用户的请求,那么 GPU 的绝大部分算力在等待下一个 Token 生成时都处于空闲状态。LLM 推理是典型的 自回归(Autoregressive) 过程——模型逐个 Token 地生成输出,每个 Token 的生成都依赖前一个 Token 的结果。这种串行依赖使得单条请求的推理无法利用 GPU 的并行优势。
批处理(Batching)的核心思想是:在同一时刻,让 GPU 同时处理多个独立的请求。每个请求拥有独立的 KV Cache 和注意力掩码,但共享同一套模型权重。通过将多个请求的输入拼接成更大的矩阵,GPU 的矩阵乘法单元(Tensor Core)可以一次性完成多个序列的前向计算,从而显著提高硬件利用率。
下图展示了单请求处理与批处理在 GPU 时间片利用上的本质差异:
从图中可以清晰看到:单请求模式下 GPU 在两次 Token 生成之间频繁空闲;静态批处理虽然能并行处理多条请求,但一旦某条请求提前完成(如 Req C 只生成了 4 个 Token),其占用的槽位就会空转,造成资源浪费;而动态批处理允许新请求在任意时刻插入空出的槽位,从而让 GPU 始终处于“满载”状态。
2.2 吞吐量与延迟:一对需要权衡的指标
在评估 LLM 推理服务时,有两个最核心的指标:吞吐量(Throughput) 和 延迟(Latency)。它们之间存在天然的张力,理解这种张力是掌握动态批处理的前提。
| 指标 | 定义 | 典型单位 | 优化方向 |
|---|---|---|---|
| 吞吐量 | 单位时间内系统完成的请求数或生成的 Token 数 | requests/s 或 tokens/s | 越高越好,代表系统整体处理能力 |
| 首 Token 延迟(TTFT) | 从请求发出到收到第一个输出 Token 的时间 | ms | 越低越好,影响用户“第一感知” |
| 单 Token 延迟(TPOT) | 生成后续每个 Token 的平均间隔时间 | ms/token | 越低越好,影响输出流畅度 |
| 端到端延迟 | 从请求发出到完整回复生成完毕的总时间 | s | 受 TTFT 和 TPOT 共同影响 |
这里有一个关键认知:盲目增大 Batch Size 会提升吞吐量,但会线性增加每个请求的排队时间和计算等待时间。当 Batch Size 过大时,单个请求需要等待同批次其他请求完成 Prefill 或 Decode 阶段才能继续推进,导致 TTFT 和 TPOT 恶化。因此,吞吐量和延迟之间存在一个“甜蜜点”——在满足延迟约束(SLA)的前提下,尽可能提高 Batch Size 以最大化吞吐量。
2.3 动态批处理:定义与核心机制
动态批处理(Dynamic Batching) 是指推理引擎在运行过程中,根据实时到达的请求和 GPU 资源占用情况,动态地决定哪些请求进入当前批次、何时进入、何时退出的调度策略。与静态批处理(预先固定 Batch 组成)不同,动态批处理将“批”视为一个不断变化的集合。
其核心机制可以拆解为以下四个步骤:
- 请求入队:新请求到达后,先进入一个等待队列(Scheduler Queue),并完成 Prefill 阶段的计算(生成初始 KV Cache)。
- 槽位检测:调度器持续监控当前 Decode 批次中每个请求的状态。当某个请求生成完毕(遇到 EOS Token 或达到最大长度限制)时,其占用的 GPU 槽位被释放。
- 动态插入:调度器从等待队列中选取一个或多个新请求,将其 KV Cache 加载到刚释放的槽位中,使新请求立即参与下一轮的 Decode 计算。
- 连续批处理(Continuous Batching):每一轮迭代(Step)结束后,都重新计算当前批次的有效请求集合。这意味着不同请求可以处于不同的生成阶段——有的刚加入,有的已生成 20 个 Token,有的即将结束——但它们在同一轮前向计算中共享 GPU 资源。
vLLM 中的 PagedAttention 技术是动态批处理得以高效实现的关键支撑。它将每个请求的 KV Cache 划分为固定大小的“页”(Page),不再要求整条请求的 KV Cache 在显存中连续存放。这使得:
- 新请求可以按页为单位灵活申请显存,无需预留整块连续空间;
- 已完成的请求可以立即释放其所有页,供其他请求复用;
- 调度器可以像操作系统管理内存一样,精细地管理显存中的 KV Cache 页,从而支持更大规模的动态批处理。
2.4 一个具体的动态批处理案例
假设某推理服务器当前批次中有 3 个请求(Req A、Req B、Req C),它们的生成进度如下:
- Req A:已生成 15 个 Token,还需 5 个 Token 结束;
- Req B:已生成 8 个 Token,还需 20 个 Token;
- Req C:已生成 30 个 Token,即将结束。
在下一轮迭代中,Req C 生成了它的第 31 个 Token,恰好是 EOS Token,于是 Req C 完成。此时调度器立即从等待队列中取出 Req D(一个刚到达的新请求),为其分配 KV Cache 页,并执行 Prefill。在接下来的 Decode 迭代中,批次变为 {Req A, Req B, Req D}。Req D 虽然刚加入,但它与 Req A、Req B 一起参与矩阵计算,GPU 的并行度没有因为 Req C 的退出而下降。
如果使用静态批处理,Req C 退出后,其槽位会一直空转到整个批次结束,造成约 1/3 的算力浪费。而动态批处理通过“随走随补”的方式,将这部分浪费降为零。
2.5 常见误区与澄清
在实际学习和使用动态批处理时,有几个容易混淆的概念需要澄清:
动态批处理的核心是“动态调整”,而非“一味增大”。如果 Batch Size 过大导致显存溢出或延迟超标,调度器反而会减少批次中的请求数量。动态批处理的目标是在延迟约束内找到最优 Batch Size,而不是追求最大值。
实际上,动态批处理同样适用于 Prefill 阶段。vLLM 支持将多个新请求的 Prefill 计算合并成一个较大的矩阵运算(Chunked Prefill),从而减少 Prefill 阶段的 GPU 空闲。此外,动态批处理还涉及 Prefill 与 Decode 的混合调度——某些请求在做 Prefill 的同时,其他请求在做 Decode,两者可以并行执行。
虽然动态插入新请求会引入微小的调度开销(通常为微秒级),但相比其带来的吞吐量提升,这部分开销可以忽略不计。真正影响延迟的是 Batch Size 过大导致的排队效应,而动态批处理恰恰通过精细调度避免了这种效应。
最后需要强调的是,动态批处理并非一个孤立的算法,它与 KV Cache 管理、显存分配策略、调度器设计 紧密耦合。vLLM 之所以能实现高效的动态批处理,正是因为它在系统层面将 PagedAttention、块分配器(Block Allocator)和迭代级调度器(Iteration-level Scheduler)三者有机整合。理解这一点,是后续掌握 vLLM 性能调优的关键基础。