2、批处理架构设计:请求队列与调度器设计、动态批处理 vs 静态批处理、批处理窗口与超时策略
好,咱们进入批处理架构设计的核心环节。说白了,这一章讲的就是「怎么把一堆请求组织起来,让 GPU 吃得饱、跑得快」。我见过不少团队,模型调得挺准,但一到线上就卡成狗,问题往往就出在批处理架构上。
2.1 请求队列与调度器设计
先聊聊请求队列。这东西看着简单,但坑不少。我早期做过一个项目,直接把所有请求塞进一个全局队列,结果呢?长尾请求把整个 batch 都拖慢了。
我个人习惯把队列分成三级:
- 优先级队列:处理紧急请求,比如用户交互类的推理
- 普通队列:处理常规请求,比如批量数据分析
- 延迟队列:处理非实时任务,比如离线评估
调度器的设计就更讲究了。你想想看,GPU 就像个大力士,你一次给它搬一块砖,它觉得不过瘾;你一次给它搬一车砖,它又可能累趴下。调度器要做的就是「刚刚好」。
核心原则:调度器要平衡「吞吐量」和「延迟」。别为了追求吞吐,把单个请求的延迟搞到用户无法接受。
我建议的调度策略是这样的:
- 轮询 + 权重:给不同优先级的队列分配不同的权重,高优先级队列的请求被优先调度
- 饥饿避免:低优先级队列不能永远等不到服务,设置一个最大等待时间
- 动态调整:根据当前 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,防止请求饿死
- 动态窗口:根据当前队列长度自动调整。队列长,窗口缩短;队列短,窗口延长
超时策略呢?说白了就是「等不及了怎么办」。我见过三种常见策略:
- 直接丢弃:超时的请求直接扔掉,返回超时错误。简单粗暴,但用户体验差
- 降级处理:用更小的模型或更低的精度处理超时请求。比如,本来用 70B 模型,超时了改用 7B 模型
- 排队重试:把超时请求重新放回队列,但优先级降低。防止同一个请求反复超时
注意:超时策略要和业务方对齐。我曾经有个项目,超时后直接丢弃请求,结果业务方说「丢一个请求损失 100 万」。从那以后,我再也不敢随便丢请求了。
最后,我画了一张图,帮你理解整个批处理架构的流程:
这张图展示了从请求进入,到队列排队,再到调度器决策,最后经过批处理窗口送入 GPU 的完整流程。你可以看到,调度器是核心,它决定了「从哪个队列取请求」和「用哪种批处理策略」。
避坑指南:我曾经把批处理窗口设成固定值 200ms,结果高峰期队列堆积到 1000 个请求,每个请求都要等 200ms 才被处理。后来改成动态窗口,根据队列长度实时调整,问题就解决了。
总结一下:批处理架构设计没有银弹。你需要根据业务场景,在延迟、吞吐、复杂度之间做权衡。我个人建议,先从动态批处理 + 动态窗口开始,跑一段时间后,根据实际数据再调优。