4、动态Batch队列:固定Batch vs 动态Batch、动态Batch的实现原理、动态Batch的适用场景

说到Batch队列,很多同学第一反应就是「设个32或者64,跑就完了」。嗯,我刚开始做推理优化时也是这么想的。直到有一次线上服务,输入请求的文本长度从几十到几千不等,固定Batch直接让显存炸了。那次事故之后,我才认真研究起动态Batch来。

说白了,固定Batch就像你开一辆固定座位的公交车——不管乘客多少,座位数不变。人少了浪费,人多了装不下。而动态Batch呢,更像一辆能根据乘客数量自动伸缩的魔法巴士。

固定Batch vs 动态Batch

先看看固定Batch的问题在哪。我习惯用一个简单的对比来说明:

对比维度 固定Batch 动态Batch
显存占用 预先分配固定大小,容易浪费或溢出 按需分配,利用率高
吞吐量 短文本时浪费算力,长文本时可能OOM 长短文本混合时吞吐更稳定
实现复杂度 简单,直接设置batch_size即可 需要队列管理和动态调度逻辑
延迟抖动 相对稳定,但长文本请求会拖慢整个batch 短文本请求响应更快,长文本不会阻塞

我在项目中遇到过这样一个场景:一个文本分类服务,输入长度从10个token到2000个token不等。固定Batch设为32时,短文本的GPU利用率只有不到30%,而长文本一来就直接OOM。后来改成动态Batch,同样的硬件,吞吐量提升了将近3倍。

核心差异:固定Batch是「请求等Batch」,动态Batch是「Batch等请求」。前者以固定大小凑满一批再处理,后者以时间窗口或请求数量为触发条件,灵活组合。

动态Batch的实现原理

动态Batch的实现,说白了就是一个「请求排队 + 智能打包」的过程。我画了一张流程图,帮你理清核心逻辑:

请求队列 动态Batch调度器 (时间窗口+最大token数) 短文本Batch (batch_size=64) 长文本Batch (batch_size=8) GPU执行 (padding对齐) 动态Batch队列处理流程 核心:根据请求长度动态调整batch_size,在时间窗口内最大化GPU利用率

具体实现上,我一般用两个核心参数来控制:

  • 最大等待时间(max_wait_ms):比如50ms,超过这个时间不管凑没凑满都开始推理
  • 最大token数(max_tokens):比如4096,防止单个batch显存溢出

代码实现其实不复杂,我贴一个简化版的调度逻辑:

class DynamicBatchQueue:
    def __init__(self, max_wait_ms=50, max_tokens=4096):
        self.queue = []
        self.max_wait_ms = max_wait_ms
        self.max_tokens = max_tokens
        self.last_process_time = time.time()
    
    def add_request(self, request):
        self.queue.append(request)
        # 检查是否满足触发条件
        if self._should_process():
            self._process_batch()
    
    def _should_process(self):
        # 条件1:队列中的总token数超过阈值
        total_tokens = sum(r.token_len for r in self.queue)
        if total_tokens >= self.max_tokens:
            return True
        # 条件2:等待时间超过最大等待时间
        if time.time() - self.last_process_time >= self.max_wait_ms / 1000:
            return True
        return False
    
    def _process_batch(self):
        # 按token长度排序,短请求优先处理
        self.queue.sort(key=lambda r: r.token_len)
        # 打包成batch,注意padding对齐
        batch = self._pack_batch(self.queue)
        # 执行推理
        results = model.infer(batch)
        # 分发结果
        self._dispatch_results(results)
        self.queue = []
        self.last_process_time = time.time()

我的经验:排序这一步很关键。把短请求放在前面处理,能显著降低平均响应时间。我曾经在一个翻译服务中测试过,排序后P99延迟降低了40%。

动态Batch的适用场景

不是所有场景都适合动态Batch。我总结了几类典型场景:

  • 文本长度差异大的场景:比如客服对话、文档摘要、代码补全。输入长度从几个字到几千字不等,动态Batch能最大化GPU利用率。
  • 延迟敏感型服务:比如在线翻译、实时语音识别。短请求不需要等长请求处理完,响应更快。
  • 混合负载场景:同时有批处理和流式请求。动态Batch能自动适配不同的负载模式。

但有些场景就不太适合:

  • 所有请求长度都差不多:比如固定尺寸的图像分类,固定Batch反而更高效。
  • 对延迟极度敏感:比如自动驾驶中的实时推理,动态Batch的排队逻辑可能引入额外延迟。
  • 硬件资源极度受限:比如嵌入式设备,动态调度的开销可能得不偿失。

避坑指南:我曾经在一个OCR服务中强行上动态Batch,结果因为图像预处理时间远大于推理时间,动态调度的开销反而拖慢了整体性能。后来改成固定Batch + 异步预处理,效果才好起来。所以,先分析瓶颈在哪,再决定要不要用动态Batch。

嗯,动态Batch的核心思想其实就一句话:让Batch大小适应数据,而不是让数据适应Batch。你想想看,这跟我们在生活中处理任务是一样的——灵活应对,才能事半功倍。

想要更多资料,联系可可