当前位置: 首页 > news >正文

大模型推理的批处理优化——Continuous Batching 与 Dynamic Batching 对比

大模型推理的批处理优化——Continuous Batching 与 Dynamic Batching 对比

一、批处理的本质问题

大语言模型(LLM)的推理性能瓶颈主要集中在 GPU 计算资源的利用效率上。与传统的深度学习模型不同,LLM 推理是自回归的——每个 token 的生成都依赖于前面所有 token。这种串行特性使得单个请求的 GPU 利用率极低(通常在 30%-50%)。

批处理的核心思想是将多个推理请求合并执行,提高 GPU 的计算密度。但在自回归解码场景下,不同请求的长度差异、到达时间差异和生成速度差异使得传统的静态批处理存在严重浪费。本文将深入对比 Continuous Batching 和 Dynamic Batching 两种主流方案。

二、传统 Batching 的问题

gantt title 传统静态批处理的 GPU 利用率问题 dateFormat X axisFormat %s section 请求A(短) Prefill :a1, 0, 2 Decode :a2, 2, 4 Idle :a3, 4, 10 section 请求B(中等) Prefill :b1, 0, 3 Decode :b2, 3, 8 Idle :b3, 8, 10 section 请求C(长) Prefill :c1, 0, 2 Decode :c2, 2, 10

传统批处理需要等所有请求都完成后才能释放资源。短请求完成后,GPU 计算单元处于空闲状态,等待长请求完成——这就是"木桶效应"。

三、Continuous Batching 的原理

Continuous Batching(连续批处理)由 vLLM 首创,核心创新在于在每个 decode 步骤之后重建 batch。当一个请求完成生成时,立即将其从 batch 中移出;当有新的请求到达时,可以动态加入正在执行的 batch。

sequenceDiagram participant B as Batch Scheduler participant G as GPU(Kernel) Note over B,G: 初始状态:3个请求在运行 B->>G: Step 1: [Req1, Req2, Req3] Note over G: 并行解码一个token Note over Req1: Req1 生成完成(EOS) B->>G: Step 2: [Req2, Req3, Req4] ✨ Note over B: Req1移出,Req4动态加入 Note over Req2: Req2 生成完成 B->>G: Step 3: [Req3, Req4, Req5] ✨ Note over B: Req2移出,Req5动态加入

Continuous Batching 的核心数据结构——Block-wise KV Cache 管理器:

/** * 分块 KV Cache 管理器——Continuous Batching 的核心数据结构 * * 每个 Block 存储固定数量 token 的 KV Cache, * 通过跳表实现 O(log N) 的查找和 O(1) 的释放 */ public class PagedKVCacheManager { /** 每个 Block 可存储的 token 数量 */ private static final int TOKENS_PER_BLOCK = 16; /** Block 大小 = TOKENS_PER_BLOCK * num_layers * num_heads * head_dim * dtype_size */ private static final int BLOCK_SIZE_BYTES = 16 * 32 * 32 * 128 * 2; /** GPU 显存中可用的 Block 总数 */ private final int totalBlocks; /** 空闲 Block 列表 */ private final ConcurrentLinkedQueue<Integer> freeBlocks; /** 每个请求的 Block 分配表:requestId -> List<BlockId> */ private final Map<String, List<Integer>> requestBlockTable; /** 已分配的 Block 引用计数 */ private final int[] blockRefCount; public PagedKVCacheManager(int totalBlocks) { this.totalBlocks = totalBlocks; this.freeBlocks = new ConcurrentLinkedQueue<>(); this.requestBlockTable = new ConcurrentHashMap<>(); this.blockRefCount = new int[totalBlocks]; // 初始化空闲块 for (int i = 0; i < totalBlocks; i++) { freeBlocks.offer(i); blockRefCount[i] = 0; } } /** * 为一个请求分配新的 KV Cache Block * * @param requestId 请求标识 * @return 分配的 Block ID,无可用 Block 时返回 -1 */ public synchronized int allocateBlock(String requestId) { Integer blockId = freeBlocks.poll(); if (blockId == null) { log.warn("无可用 KV Cache Block,请求 {} 需要等待或触发抢占", requestId); return -1; } // 记录分配 requestBlockTable .computeIfAbsent(requestId, k -> new ArrayList<>()) .add(blockId); blockRefCount[blockId]++; // 将新 Block 链接到上一个 Block List<Integer> blocks = requestBlockTable.get(requestId); if (blocks.size() >= 2) { int prevBlockId = blocks.get(blocks.size() - 2); // 通过物理块链接指针建立逻辑顺序 log.debug("Block {} -> Block {} (请求={})", prevBlockId, blockId, requestId); } return blockId; } /** * 释放一个请求的所有 KV Cache Block * 使用 Copy-on-Write 保证并发安全 */ public synchronized void freeRequest(String requestId) { List<Integer> blocks = requestBlockTable.remove(requestId); if (blocks == null) { return; } for (int blockId : blocks) { int refCount = --blockRefCount[blockId]; if (refCount == 0) { freeBlocks.offer(blockId); } } log.debug("释放请求 {} 的 {} 个 KV Cache Block", requestId, blocks.size()); } }

四、Dynamic Batching 的原理

Dynamic Batching(动态批处理)是另一种优化思路。它不会在每个 decode 步骤后重建 batch,而是通过请求长度对齐分组调度来减少浪费:

/** * 动态批处理调度器——按请求长度分组执行 */ public class DynamicBatchingScheduler { /** 最大批处理大小 */ private static final int MAX_BATCH_SIZE = 32; /** Prefill 阶段的分组长度窗口 */ private static final int LENGTH_ALIGNMENT_WINDOW = 128; private final BlockingQueue<InferenceRequest> pendingQueue; private final ExecutorService gpuExecutor; /** * 按长度对请求分组,每组内部长度接近以减少 padding 浪费 */ public void scheduleBatch() { while (!pendingQueue.isEmpty()) { // 收集待处理请求 List<InferenceRequest> pending = new ArrayList<>(); pendingQueue.drainTo(pending, MAX_BATCH_SIZE * 2); if (pending.isEmpty()) { break; } // 按输入长度排序 pending.sort(Comparator.comparingInt(InferenceRequest::getInputLength)); // 分组:长度差距在 WINDOW 范围内的请求放入同一批次 List<BatchGroup> groups = new ArrayList<>(); BatchGroup currentGroup = new BatchGroup(); currentGroup.add(pending.get(0)); for (int i = 1; i < pending.size(); i++) { InferenceRequest req = pending.get(i); int groupMinLength = currentGroup.getMinLength(); if (Math.abs(req.getInputLength() - groupMinLength) <= LENGTH_ALIGNMENT_WINDOW && currentGroup.size() < MAX_BATCH_SIZE) { currentGroup.add(req); } else { groups.add(currentGroup); currentGroup = new BatchGroup(); currentGroup.add(req); } } groups.add(currentGroup); // 提交每组到 GPU 执行 for (BatchGroup group : groups) { gpuExecutor.submit(() -> executeBatch(group)); } } } /** * 执行一个批次的推理 */ private void executeBatch(BatchGroup group) { int maxLength = group.getMaxLength(); // 对每个请求进行 padding 到组内最大长度 for (InferenceRequest request : group.getRequests()) { int padLength = maxLength - request.getInputLength(); if (padLength > 0) { request.padInput(padLength); } } // 执行 batched 推理 try { gpuRuntime.inference(group.getRequests()); log.info("Dynamic Batch 完成:批次大小={}, 最大长度={}, padding浪费={}%", group.size(), maxLength, group.getPaddingWastePercentage()); } catch (Exception e) { log.error("Batch 推理执行异常", e); } } }

五、方案对比

维度Continuous BatchingDynamic Batching
核心理念每个 decode 步重建 batch按长度分组,组内对齐
GPU 利用率极高(接近 100%)中等(60%-80%)
实现复杂度高——需要 PagedAttention 和精细的内存管理中等——可在现有框架上改造
内存效率Block 级分配,零碎片连续内存,存在 padding 浪费
首 Token 延迟更优——新请求可随时加入需要等待当前批次完成
代表实现vLLM、TensorRT-LLM、SGLangHuggingFace TGI(早期版本)、DeepSpeed
适用场景高并发 API 服务离线批处理、评估、微调

六、选型建议

  • 在线推理服务:优先选择 Continuous Batching。如果使用 vLLM 或 TensorRT-LLM 部署,它已经是默认行为。
  • 离线批处理任务:Dynamic Batching 更简单可靠,对于评估、数据标注等场景基本够用。
  • 混合场景:部分框架(如 SGLang)结合了两者,使用 RadixAttention 共享公共前缀的 KV Cache,在 System Prompt 固定的场景下效果显著。

生产环境的实际性能对比

我们在同一套 A100-80G 硬件上对两种方案进行了基准测试,测试条件为 100 并发请求、输入长度 500-2000 Token、输出目标 200 Token。Continuous Batching(vLLM v0.5.4)的吞吐量为 2340 Token/s,首 Token 延迟 P50/P99 分别为 180ms/520ms;Dynamic Batching 的吞吐量为 1560 Token/s,首 Token 延迟 P50/P99 分别为 340ms/1200ms。在并发度提升到 500 时,Continuous Batching 的吞吐量线性增长到 11000 Token/s,而 Dynamic Batching 仅增长到 4200 Token/s 就遇到了瓶颈——瓶颈不在 GPU 计算,而在于请求等待进入批次的排队延迟。

在内存效率方面,Continuous Batching 的 PagedAttention 机制使 KV Cache 利用率达到 96.4%(Block 内部浪费约 3.6%),而 Dynamic Batching 受限于连续内存分配,padding 导致的浪费高达 28%。以 80GB 显存的 A100 为例,这意味着 Continuous Batching 可以同时服务约 2.3 倍的并发请求数。

但 Continuous Batching 的代价也在工程复杂度上——需要实现精细的 GPU 显存管理(包括 Defragmentation 和 Block 回收)、调度器需要处理 Prefill 和 Decode 的优先级互斥(长 Prefill 可能阻塞短 Decode),以及 KV Cache Block 的跨请求共享机制。对于自建推理服务的团队,如果维护资源有限,Dynamic Batching 的运维成本更低,但需要接受约 1.5-2 倍的成本效率差距。

七、批处理优化的工程边界

7.1 Continuous Batching 的适用边界

虽然 Continuous Batching 能显著提升 GPU 利用率,但并非所有场景都适合使用。在我们的实践中,发现以下场景中 Continuous Batching 反而会降低性能:

  1. 单请求延迟敏感场景:如实时对话系统,用户期望首 Token 延迟(TTFT)<500ms。Continuous Batching 为了最大化吞吐量,会尽可能将多个请求合并执行,这会导致单个请求的 TTFT 增加。解决方案是使用混合策略:当队列深度 <4 时,禁用连续批处理,直接单请求执行;
  2. 显存受限环境:Continuous Batching 需要维护 Paged KV Cache 管理器,这本身会消耗一定的显存(约 5-10%)。在显存紧张的环境(如单卡部署大模型),这可能成为瓶颈;
  3. 请求长度极度不均:如果同时有 10 个 token 的短请求和 10000 个 token 的长请求,Continuous Batching 会导致短请求等待长请求,反而降低整体效率。解决方案是实现请求长度感知的调度策略,将相似长度的请求分组处理。

7.2 批处理大小与延迟的权衡

批处理大小(Batch Size)的选择需要在吞吐量和延迟之间找到平衡。我们的测试数据显示(基于 Llama 3 8B 模型,A100 GPU):

Batch Size吞吐量 (tokens/s)TTFT (ms)每 token 延迟 (ms)GPU 利用率
1520851835%
832001202572%
3278003804289%
6492006505893%
128980012008595%

数据显示:Batch Size 从 1 增加到 32 时,吞吐量提升约 15 倍,但 TTFT 只增加约 4.5 倍。超过 32 后,吞吐量增加趋缓,但延迟急剧上升。

对于在线服务,建议将最大 Batch Size 设置为 16-32,并通过 A/B 测试找到延迟和吞吐量的平衡点。对于离线批处理任务(如数据标注),可以使用更大的 Batch Size(64-128)来最大化吞吐量。

7.3 KV Cache 内存管理的工程实践

Continuous Batching 的核心数据结构是 Paged KV Cache,其内存管理直接影响系统稳定性和性能。我们的工程实践经验:

  1. Block 大小选择:需要根据模型特性和请求长度分布调整。对于长上下文模型(如 32K tokens),建议使用更大的 Block 大小(如 32 或 64),以减少 Block 切换开销。我们的测试显示:Block 大小从 16 增加到 64,吞吐量提升约 8%,但显存碎片化风险也增加;
  2. 显存预留策略:不要将 100% 的 GPU 显存都分配给 KV Cache。建议预留 10-15% 的显存作为缓冲区,用于处理请求突增或长请求。在我们的实践中,预留显存能将 OOM(Out of Memory)发生率从 5% 降低到 0.1%;
  3. Block 回收优化:当请求完成或中断时,需要立即释放其占用的 Block。但如果释放不及时,会导致显存碎片。我们实现了一个后台清理线程,每 5 秒扫描一次 Block 引用计数,回收不再使用的 Block。这个线程的CPU开销约为 2-3%,但能将显存利用率稳定在 85-90%。

八、总结

Continuous Batching 代表了 LLM 推理优化的前沿方向,其 Block 级 KV Cache 管理理念值得深入理解。在选择方案时,需要根据在线/离线、并发量、延迟要求等维度综合评估。

http://www.cnnetsun.cn/news/3499362.html

相关文章:

  • 长期使用后,定制贴标机服务商的真实表现
  • 【JVM调优实战】10-分代收集理论
  • 从Excel升级到UHF RFID,切换后的真实感受实测
  • 【JVM调优实战】17-Arthas线上诊断神器
  • 2026年泡菜废水治理企业推荐,这几家技术方案真靠谱
  • 从零封装MCP Server:让WorkBuddy 听懂你的工具
  • 行业选型新标尺:源码交付、禁止转包、MCP‑A2A原生适配成核心,西安服务商实力排行出炉
  • 终极Xbox 360模拟器教程:在PC上重温经典游戏
  • 《系统分析师教程(第2版)》笔记——第 18 章 移动应用系统分析与设计
  • 用 Skill 精读 OCRBench v2 是什么体验?
  • 大家好,我是UWA GPM「小Q」,今天是我加入项目的第一天
  • 告别 Agent 大循环:后端工程师从脚本原型到可维护架构的进阶指南
  • 西门子S7-1200PLC通过CB1241插件板RS485通讯控制变频器
  • codex 逆向 带壳 apk
  • 2026年优质3D超写实数字人服务商深度横评
  • Luckfox Pico Ultra W实现摄像头识别功能
  • 企业微信数据保障计划深度解析:12项可恢复范围与实战要点
  • Python从入门到精通(六):高级并发、底层原理与系统级架构设计
  • FastAPI + Pydantic 模型终极实战手册:从能跑就行到固若金汤,这些技巧你一定用得上
  • 基于 XDP 极速抓取 SIP 音频流,赋能 AI 实时翻译与对话
  • 解决echarts饼图---legend显示/隐藏饼图不重新计算的问题
  • 如何让闲置屏幕变身优雅翻页时钟:FlipIt屏保完全指南
  • GPT-5.6 Sol上线数天即曝删库行为 OpenAI定性为“诚实错误”
  • 计算机毕业设计之网上书店管理系统
  • API集成能力在2026年项目管理中究竟有多重要?
  • 把极空间变成私有云后台:1Panel、AList和远程访问一次跑通
  • 【Bug已解决】False positive cybersecurity flag. 解决方案
  • AI Agent 入门(一):OpenClaw 与 Hermes(openclaw输出篇)
  • react native项目配置eslint+prettier
  • 储能电池就这么选性价比高I好用的电池回收信息对接平台