持续批处理:LLM 服务中提升 GPU 利用率的最关键技术
生产环境中大多数 LLM 推理基础设施的故障并不是模型故障——而是调度故障。团队部署了一个高性能模型,进行了压力测试,却发现用户在等待的同时,昂贵的 GPU 时间仅以 35% 的利用率在消耗。罪魁祸首几乎总是静态批处理(Static batching):这是从传统深度学习中继承下来的默认设置,但根本不符合语言模型生成文本的方式。
持续批处理(Continuous batching)——也称为迭代级调度(Iteration-level scheduling)或飞行中批处理(In-flight batching)——是解决这一问题的核心机制。它不是一个微调旋钮,而是对推理循环运行方式的架构性改变。在使用相同硬件的情况下,使用该技术的系统与不使用的系统相比,吞吐量可能相差 4–8 倍。
要理解其中的原因,首先需要了解原始方法到底出了什么问题。
为什么静态批处理会浪费你的 GPU
传统的批处理会收集一组请求,将它们作为一个整体由模型处理,直到每个序列都生成完毕,然后再进入下一个批次。这在图像分类中运行良好,因为每个输入都会产生一个固定大小的输出。但对于语言生成来说,这简直是一场灾难。
问题在于输出长度的差异。一个聊天请求可能在 15 个 token 内完成,而同一批次中的代码生成请求可能需要 800 个 token。在短请求完成后的 785 次迭代中,为其分配的 GPU 显存和计算槽位都处于闲置状态(即填充 padding),而整个批次必须等待最长的序列执行完毕。你支付了全部的吞吐成本,但利用率曲线显示 GPU 占用率仅为 30–60%。
动态批处理(Dynamic batching)通过在时间窗口内(例如 50ms)对请求进行分组来减少准入延迟,从而改善了这一状况,但批次级调度的问题依然存在:一旦窗口关闭,该批次将作为一个整体运行,直到最后一个序列完成。
持续批处理通过将调度决策从 请求 粒度转移到 迭代 粒度解决了这个问题。调度器在模型的每次前向传播时运行一次,而不是每个请求运行一次。当一个序列发出终止符(EOS)并结束时,其显存槽位会 立即 被释放。下一个等待中的请求会在 下一次迭代开始前 插入到批次中。没有请求需要等待另一个请求完成——批次构成在每个解码步骤都会发生变化。
吞吐量的提升非常显著。在大规模场景下引入迭代级调度的 ORCA 论文(OSDI 2022)表明,在同等延迟目标下,其吞吐量比 FasterTransformer 提高了 36.9 倍。Anyscale 的真实基准测试显示,与原生的 HuggingFace Transformers 推理相比,性能提升了 8 倍。结合基于 PagedAttention 的 KV 缓存管理,vLLM 的初始版本吞吐量比 HuggingFace Transformers 高出 24 倍,比 HuggingFace TGI 高出 3.5 倍。
调度器是如何运作的
在每次前向传播中,持续批处理调度器会执行一个简短的循环:
- 扫描运行中的批次,查找已完成的序列(已发出 EOS)
- 释放它们的 KV 缓存块
- 从队列中提取等待中的请求——数量取决于显存和批次大小限制
- 将所有活动序列连接成一个复合批次
- 运行一次模型前向传播;每个序列产生其下一个 token
- 重复上述过程
连接(Concatenation)步骤是使其在结构上与众不同的原因。静态批处理要求序列填充到相同长度,因为批处理矩阵运算需要统一的张量形状。而持续批处理则构造一个带有注意力掩码(Attention masks)的“超级序列”,防止任何请求关注到其他请求的 token。没有填充,没有浪费的计算——每一个 GPU FLOP 都在处理真实的 token。
这种连接形式自然地与 FlashAttention 的变长算子(Variable-length kernel)变体相集成,尽管长度不同,它仍能在单个 GPU 算子调用中处理所有序列。结果是,即使批次中混合了处于不同进度的短序列和长序列,GPU 占用率依然很高。
PagedAttention:内存管理层
持续批处理解决了 何时 调度请求。PagedAttention(vLLM, SOSP 2023)则解决了 何处 存储它们的 KV 缓存。
在 PagedAttention 出现之前,LLM 推理框架会为每个请求预分配一块连续的 GPU 显存,大小取决于其 最大可能输出长度。这由于碎片化导致了 60–80% 的显存浪费:过度保留的槽位、对齐间隙,以及大多数序列并不会用完其最大分配空间的事实。
PagedAttention 将操作系统的虚拟内存分页模型应用于 KV 缓存管理。KV 缓存被划分为固定大小的块(vLLM 默认每块 16 个 token),根据序列生成 token 的需求动态分配,而不是预先分配。一个块表将每个序列的逻辑块映射到物理 GPU 显存位置——这些块不需要连续。显存浪费降至 4% 以下(每个序列仅浪费最后一个未填满的块)。
第二个好处是:物理块可以通过写时复制(Copy-on-write)语义在序列之间共享。束搜索(Beam search)的分支、并行采样以及共享相同系统提示词(System prompt)的请求,在发生分歧之前都可以引用相同的物理 KV 块。对于束搜索,这可以减少高达 55% 的开销,并使吞吐量比非共享分配提高 2.2 倍。
SGLang 通过 RadixAttention 进一步扩展了这一能力:这是一种基数树(Radix tree)数据结构,可以在 不同请求 之间维护 KV 缓存,实现自动前缀重用。共享系统提示词、少样本示例或 RAG 上下文的请求可以重用彼此已缓存的 KV 块,而无需重新计算。在具有大量前缀共享的工作负载中,这可以使推理速度提高 5 倍。
