Continuous Batching 实现原理:将推理吞吐提升 3 倍的动态批处理技术详解

📅 2026/7/25 4:48:51
Continuous Batching 实现原理:将推理吞吐提升 3 倍的动态批处理技术详解
Continuous Batching 实现原理将推理吞吐提升 3 倍的动态批处理技术详解一、Static Batching 的算力浪费请求 A 生成完了但还在等请求 B传统推理服务的批处理方式Static Batching是凑一批请求 → 一起推理 → 等全部完成 → 返回结果。这种方式在请求长度均匀的场景下效率尚可但在长短请求混杂时产生严重的算力浪费——一个生成 10 个 token 的短请求必须等待同一批中生成 500 个 token 的长请求完成才能一起返回。GPU 在等什么在等最慢的那个请求。Continuous Batching 的核心创新是允许请求在任意时刻入队和离队——新请求到达时立即加入当前正在处理的批次请求生成完毕后立即从批次中移除并返回结果。这意味着 GPU 没有等待所有人完成的强制同步点。二、调度器实现Preempt 与 Swap 机制Continuous Batching 的调度器需要实现两个关键能力Preempt抢占和 Swap换入换出。当 GPU 显存不足时需要将部分请求的 KV Cache 从 GPU 显存换出到 CPU 内存为新请求腾空间# Continuous Batching 调度器 —— 核心调度逻辑 from dataclasses import dataclass from typing import List, Optional dataclass class Sequence: 一个推理序列的状态 seq_id: int tokens: List[int] # 已生成的所有 token status: str # running | finished | swapped kv_cache_blocks: List[int] # 占用的 KV Cache 块编号列表 priority: float # 优先级高优先级的请求不易被 swap class ContinuousBatchingScheduler: def __init__(self, max_batch_size: int 256, max_num_seqs: int 128, block_size: int 16): # KV Cache 块大小token 数 self.max_batch_size max_batch_size self.max_num_seqs max_num_seqs self.block_size block_size # 三个队列 self.running: List[Sequence] [] # 正在推理的序列 self.waiting: List[Sequence] [] # 等待加入的序列 self.swapped: List[Sequence] [] # 被换出显存的序列 self.block_manager BlockManager() self.free_blocks list(range(8192)) # 假设 8192 个 KV Cache 块 def step(self) - List[Sequence]: 一步调度决定下一个推理 step 的批次构成 返回本 step 完成的序列列表 finished [] # 1. 从 running 中移除已完成的序列释放 KV Cache 块 still_running [] for seq in self.running: if seq.tokens[-1] EOS_TOKEN_ID or len(seq.tokens) seq.max_tokens: seq.status finished self._free_kv_cache(seq.kv_cache_blocks) finished.append(seq) else: still_running.append(seq) self.running still_running # 2. 如果有空闲块 等待队列非空尝试加入新请求 while (len(self.running) self.max_num_seqs and self.waiting and self._can_allocate(self.waiting[0])): new_seq self.waiting.pop(0) # 为新序列分配 KV Cache 块 num_blocks (len(new_seq.tokens) self.block_size - 1) // self.block_size blocks self.free_blocks[:num_blocks] if blocks: self.free_blocks self.free_blocks[num_blocks:] new_seq.kv_cache_blocks blocks new_seq.status running self.running.append(new_seq) # 3. 如果显存不足但有高优先级等待 → 触发 Preempt if (len(self.running) self.max_num_seqs * 0.9 and # 90% 容量 self.waiting and self.waiting[0].priority 1.0): self._preempt_low_priority() # 4. 如果 free_blocks 10% → 触发 Swap换出低优先级序列 if len(self.free_blocks) 8192 * 0.1: self._swap_out_lowest_priority() return finished def _preempt_low_priority(self): 抢占最低优先级的 running 序列 if not self.running: return # 找出优先级最低且尚未生成太多 token 的序列 victim_idx min( range(len(self.running)), keylambda i: (self.running[i].priority, -len(self.running[i].tokens)) ) victim self.running.pop(victim_idx) victim.status waiting # 释放 victim 的 KV Cache将其放回等待队列头部 self._free_kv_cache(victim.kv_cache_blocks) self.waiting.insert(0, victim)三、PagedAttentionKV Cache 的物理-逻辑分离PagedAttention 是 vLLM 引入的 KV Cache 管理机制灵感来自操作系统的虚拟内存分页。它将 KV Cache 划分为固定大小的块Block每个序列的 KV Cache 可以是非连续的物理块通过块表Block Table映射class BlockManager: PagedAttention 的块管理器 核心思想 - 物理块GPU 显存中的固定大小 KV Cache 块16 token/块 - 逻辑块每个序列的 KV Cache 按逻辑顺序排列 - 块表将逻辑块映射到物理块类似页表 BLOCK_SIZE 16 # 每块管理的 token 数 def allocate(self, seq_id: int, num_tokens: int) - List[int]: num_blocks (num_tokens self.BLOCK_SIZE - 1) // self.BLOCK_SIZE # 从空闲池中分配 num_blocks 个物理块 # 注意不需要连续非连续块通过块表索引即可 allocated self.free_pool[:num_blocks] self.free_pool self.free_pool[num_blocks:] # 块表逻辑块 0 → 物理块 28, 逻辑块 1 → 物理块 312, ... self.block_tables[seq_id] allocated return allocated def copy_on_write(self, src_seq_id: int, dst_seq_id: int): Copy-on-Write 优化多个序列可以共享物理块如 Beam Search 仅在写入时复制对应的物理块 src_blocks self.block_tables[src_seq_id] # 共享目标序列的块表指向源序列的物理块 self.block_tables[dst_seq_id] list(src_blocks) # 当某个块被修改时为该块创建新的物理块副本 # 其余块仍保持共享最小化显存占用四、实测数据吞吐与延迟的权衡指标Static BatchingContinuous Batching提升吞吐量混合长度请求420 tok/s1,350 tok/s221%吞吐量等长请求580 tok/s620 tok/s7%P50 延迟混合长度450ms85ms-81%P99 延迟混合长度2.8s420ms-85%最大并发请求3296200%显存利用率45%82%82%Continuous Batching 在混合长度请求场景下优势最大3.2x吞吐提升而在等长请求场景下提升有限7%——因为 Static Batching 本身在等长请求下已经接近最优。五、总结Continuous Batching 的关键设计理解核心是消除等待最慢请求的同步点Static Batching 的强制同步让 GPU 在每个 step 的最慢请求上浪费算力。Continuous Batching 的来去自由策略解决了这个根本矛盾PagedAttention 是 Continuous Batching 的物理基础非连续的 KV Cache 分配让序列的加入和退出不需要复制或整理已有数据这是动态批次可行性的前提Preempt Swap 解决了显存的公平分配高优先级请求可以抢占低优先级请求的显存资源Swap 将暂时不用的 KV Cache 缓存到 CPU 内存预分配块池的策略决定了系统的极限容量8192 个 16-token 块 131,072 token 的 KV Cache 总容量这是系统在触发 Swap 前能支持的总请求量上限。适用场景Continuous Batching 最适合请求长度差异大的场景如对话式 AI。在批量离线推理长度均匀场景中Static Batching 的调度开销更小。