2、批处理架构设计:请求队列与调度器设计、动态批处理 vs 静态批处理、批处理窗口与超时策略

好,咱们进入批处理架构设计的核心环节。说白了,这一章讲的就是「怎么把一堆请求组织起来,让 GPU 吃得饱、跑得快」。我见过不少团队,模型调得挺准,但一到线上就卡成狗,问题往往就出在批处理架构上。

2.1 请求队列与调度器设计

先聊聊请求队列。这东西看着简单,但坑不少。我早期做过一个项目,直接把所有请求塞进一个全局队列,结果呢?长尾请求把整个 batch 都拖慢了。

我个人习惯把队列分成三级:

  • 优先级队列:处理紧急请求,比如用户交互类的推理
  • 普通队列:处理常规请求,比如批量数据分析
  • 延迟队列:处理非实时任务,比如离线评估

调度器的设计就更讲究了。你想想看,GPU 就像个大力士,你一次给它搬一块砖,它觉得不过瘾;你一次给它搬一车砖,它又可能累趴下。调度器要做的就是「刚刚好」。

核心原则:调度器要平衡「吞吐量」和「延迟」。别为了追求吞吐,把单个请求的延迟搞到用户无法接受。

我建议的调度策略是这样的:

  1. 轮询 + 权重:给不同优先级的队列分配不同的权重,高优先级队列的请求被优先调度
  2. 饥饿避免:低优先级队列不能永远等不到服务,设置一个最大等待时间
  3. 动态调整:根据当前 GPU 负载,实时调整 batch size

嗯,这里要注意:调度器本身不能成为瓶颈。我见过有人用 Python 写调度器,结果调度逻辑比推理还慢。建议用 C++ 或 Rust 实现核心调度逻辑。

2.2 动态批处理 vs 静态批处理

这是个大话题。静态批处理,说白了就是「攒够了再跑」。你设定一个 batch size,比如 32,等队列里攒够 32 个请求,一次性送进 GPU。

动态批处理呢?它更灵活。不等攒够,时间到了就发车。或者,来了新请求,如果当前 batch 还没开始处理,就把它加进去。

特性 静态批处理 动态批处理
延迟 高(需要等待攒够) 低(可随时发车)
吞吐量 高(batch 利用率满) 中等(可能发半车)
实现复杂度
适用场景 离线批量推理 在线实时推理

我在项目中遇到过一个问题:用静态批处理做在线服务,结果用户请求稀疏时,等了 500ms 才攒够 4 个请求,延迟直接爆炸。后来改成动态批处理,设置最大等待时间 100ms,问题就解决了。

我的建议:线上服务用动态批处理,离线任务用静态批处理。别混用,否则调度逻辑会变得极其复杂。

动态批处理还有一个变种叫「连续批处理」(Continuous Batching)。它允许在推理过程中动态插入新请求。比如,当前 batch 正在跑第 3 层,来了个新请求,可以把它加到第 4 层的计算中。这个技术现在很火,vLLM 和 TensorRT-LLM 都支持。

2.3 批处理窗口与超时策略

批处理窗口,就是「等多久发车」。这个参数很关键。窗口太短,batch 太小,GPU 利用率低;窗口太长,延迟高,用户骂娘。

我一般这样设置:

  • 最小窗口:50ms,保证低延迟场景
  • 最大窗口:500ms,防止请求饿死
  • 动态窗口:根据当前队列长度自动调整。队列长,窗口缩短;队列短,窗口延长

超时策略呢?说白了就是「等不及了怎么办」。我见过三种常见策略:

  1. 直接丢弃:超时的请求直接扔掉,返回超时错误。简单粗暴,但用户体验差
  2. 降级处理:用更小的模型或更低的精度处理超时请求。比如,本来用 70B 模型,超时了改用 7B 模型
  3. 排队重试:把超时请求重新放回队列,但优先级降低。防止同一个请求反复超时

注意:超时策略要和业务方对齐。我曾经有个项目,超时后直接丢弃请求,结果业务方说「丢一个请求损失 100 万」。从那以后,我再也不敢随便丢请求了。

最后,我画了一张图,帮你理解整个批处理架构的流程:

请求入口 优先级队列 普通队列 延迟队列 调度器 动态/静态策略 批处理窗口 超时策略 GPU 推理 输出结果 批处理架构流程图

这张图展示了从请求进入,到队列排队,再到调度器决策,最后经过批处理窗口送入 GPU 的完整流程。你可以看到,调度器是核心,它决定了「从哪个队列取请求」和「用哪种批处理策略」。

避坑指南:我曾经把批处理窗口设成固定值 200ms,结果高峰期队列堆积到 1000 个请求,每个请求都要等 200ms 才被处理。后来改成动态窗口,根据队列长度实时调整,问题就解决了。

总结一下:批处理架构设计没有银弹。你需要根据业务场景,在延迟、吞吐、复杂度之间做权衡。我个人建议,先从动态批处理 + 动态窗口开始,跑一段时间后,根据实际数据再调优。

专注资料整理