批处理推理优化:Dynamic Batching 在真实流量中的表现

发布时间:2026/7/19 0:27:14
批处理推理优化:Dynamic Batching 在真实流量中的表现 批处理推理优化Dynamic Batching 在真实流量中的表现一、个性化深度引言周五下午16:00业务方反馈OCR识别服务偶尔超时。我查日志发现一个规律超时集中在每分钟的第05秒和第3035秒——也就是定时任务批量提交的时间窗口。在这个窗口内单次推理请求变成了30~50个并发小请求每个请求独立走一遍CUDA kernel launchGPU的利用率曲线像一个过山车。这不是模型慢而是调度策略没利用好GPU的并行特性。GPU单次处理一个batch1的请求和batch32的请求延迟差异远小于吞吐量差异。把单独的小请求攒成一批处理是降本增效的基本功。二、个性化原理剖析Dynamic Batching 的核心思路是在推理服务中设置一个微小的等待窗口通常100-500微秒将在这个窗口内到达的请求打包成一个batch提交给GPU。这样GPU的SM流式多处理器可以同时处理多个请求大幅提升吞吐量。sequenceDiagram participant Client1 participant Client2 participant Client3 participant Batcher participant GPU Client1-Batcher: 请求A (t0ms) Client2-Batcher: 请求B (t0.05ms) Note over Batcher: 等待窗口 0.2ms Client3-Batcher: 请求C (t0.15ms) Note over Batcher: 窗口到期 Batcher-GPU: batch [A,B,C] GPU--Batcher: 推理结果 [A,B,C] Batcher--Client1: 响应A (t2ms) Batcher--Client2: 响应B (t2ms) Batcher--Client3: 响应C (t2ms)等待窗口的设计是关键参数。窗口太小batch里请求太少吞吐量提升有限窗口太大单个请求的排队延迟增加P99延迟上升。我们需要在吞吐量和延迟之间找到平衡点。见证奇迹的时刻在NVIDIA A10 GPU上对于ResNet-50模型batch1时吞吐量为320 img/sbatch32时吞吐量跃升到2100 img/s提升了6.5倍。而单个请求的延迟仅从3.1ms增加到3.5ms增幅不到13%。这就是GPU并行计算的魅力——边际延迟成本极低边际吞吐收益极高。我们还需要考虑padding问题。不同输入序列长度不同时batch内部的短序列需要padding到最长序列长度这部分计算是浪费的。实现时应当将长度相近的请求放在同一个batch中bucketing策略减少padding开销。三、个性化代码实践import time import threading from collections import deque class DynamicBatcher: 动态批处理器 def __init__(self, model, max_batch_size32, max_wait_us200): self.model model self.max_batch_size max_batch_size # 设计原因200us 是在 A10 GPU 上实测的甜点值 # 太小则 batch 利用率低太大则引入不可接受的排队延迟 self.max_wait_us max_wait_us self.queue deque() self.lock threading.Lock() self.batch_event threading.Event() # 设计原因用独立线程收集请求避免阻塞调用方 self.collector threading.Thread(targetself._collect_loop, daemonTrue) self.collector.start() def infer(self, input_tensor): 提交推理请求返回 Future 对象 future threading.Event() result_holder [None] with self.lock: self.queue.append((input_tensor, future, result_holder)) self.batch_event.set() # 通知收集线程 # 设计原因用 Event.wait() 而非忙等不消耗 CPU future.wait() return result_holder[0] def _collect_loop(self): 批量收集与调度循环 while True: self.batch_event.wait() self.batch_event.clear() # 设计原因微睡眠等待更多请求到达 # time.sleep 精度不够用 busy-wait 保证微秒级精度 deadline time.perf_counter_ns() self.max_wait_us * 1000 while time.perf_counter_ns() deadline: if len(self.queue) self.max_batch_size: break # 设计原因短忙等(200us)对 CPU 影响可忽略 # 但能显著减少零散的小 batch 提交 pass with self.lock: if not self.queue: continue batch_size min(len(self.queue), self.max_batch_size) batch [self.queue.popleft() for _ in range(batch_size)] # 设计原因将不等长的输入做 padding 和后处理 tensors self._pad_and_stack([item[0] for item in batch]) results self.model.forward(tensors) for i, (_, future, holder) in enumerate(batch): holder[0] results[i] future.set() def _pad_and_stack(self, tensors): Padding 到相同长度后堆叠 max_len max(t.shape[0] for t in tensors) padded [] for t in tensors: if t.shape[0] max_len: # 设计原因零填充不引入额外信息对卷积和注意力计算友好 pad torch.zeros(max_len - t.shape[0], *t.shape[1:]) t torch.cat([t, pad]) padded.append(t) return torch.stack(padded)四、个性化边界权衡延迟敏感与吞吐敏感的取舍Dynamic Batching 天然增加单请求的排队延迟。对于P99延迟要求5ms的实时服务等待窗口必须设为50us以下这会导致batch大小通常只有2~4吞吐提升有限。解决方案是区分请求优先级实时请求走小batch快速通道离线请求走大batch通道。显存占用陡增batch32时中间激活值activation的显存占用是batch1时的32倍。对于大模型如7B参数以上的LLM显存很容易超出GPU容量。需要配合Gradient Checkpointing或Activation Offloading技术来缓解但这又带来计算开销。负载不均衡时的浪费深夜低峰期QPS很低等待窗口内可能只有1-2个请求此时动态批处理退化到接近逐条处理但多了一层调度开销实测约增加3~8%延迟。框架兼容性差异TensorRT的Dynamic Batching是原生支持的PyTorch需要手动实现。ONNX Runtime也有内置的Dynamic Batching但不同后端的实现成熟度不同。五、总结Dynamic Batching 在保持单请求延迟小幅增加15%的前提下可将GPU推理吞吐量提升3~8倍。核心参数是等待窗口和最大batch大小需要根据具体硬件和延迟SLA调优。适用场景是中高并发的在线推理服务不适合极低延迟或极低负载场景。配合bucketing策略可进一步减少padding浪费。