3、静态批处理与动态批处理
在深度学习推理服务中,批处理(Batching)是提升GPU利用率、增加系统吞吐量的核心手段。然而,批处理策略的选择直接决定了延迟、吞吐与资源效率之间的平衡。本章将深入剖析静态批处理与动态批处理两种范式的原理、局限与适用场景,帮助你在实际部署中做出合理的技术决策。
3.1 静态批处理的原理与局限
3.1.1 核心原理
静态批处理(Static Batching)是最传统的批处理方式,其核心逻辑是:将请求收集到固定大小的批次中,当批次填满或达到预设条件时,统一执行一次前向推理。在LLM推理场景中,静态批处理通常与固定序列长度配合使用——所有请求被padding到相同长度,形成一个规整的矩阵输入。
其工作流程可以概括为以下步骤:
- 请求收集:将到达的推理请求放入队列缓冲区。
- 批次构建:当队列中的请求数量达到预设的batch_size(如8、16、32)时,触发批处理。
- 序列对齐:将批次内所有请求的token序列padding到相同长度(通常取批次内最长序列)。
- 统一推理:将整个批次作为一个整体送入GPU执行前向计算。
- 结果返回:推理完成后,将结果按请求ID拆分并返回给各调用方。
3.1.2 静态批处理的局限
静态批处理虽然实现简单、计算模式规整,但在真实生产环境中存在明显短板:
| 局限维度 | 具体表现 | 产生原因 |
|---|---|---|
| 队头阻塞 | 短请求必须等待长请求完成才能返回 | 批次内所有请求共享同一次前向传播 |
| 计算浪费 | padding token参与计算但不产生有效输出 | 固定长度对齐导致无效计算占比高 |
| 延迟波动 | P99延迟可能达到平均延迟的5-10倍 | 批次填充等待时间不确定 |
| GPU利用率不均 | 批次间存在空闲间隙,利用率呈锯齿状 | 请求到达速率不均匀 |
3.1.3 案例拆解:静态批处理的延迟问题
假设某在线客服系统使用静态批处理,batch_size=8,平均请求长度约200 token。某时刻队列中到达了8个请求,其中1个请求需要生成512 token的长回复,其余7个仅需生成30 token的短回复。
初始现象:系统平均延迟为800ms,但P99延迟高达4.2秒,用户体验极差。
分析过程:
- 长请求的生成时间为3.5秒,短请求实际只需0.3秒。
- 由于静态批处理必须等待整个batch完成,7个短请求被迫等待3.5秒。
- P99延迟被长请求完全主导,且这种长尾效应在静态批处理中无法避免。
优化方向:将静态批处理改为动态批处理,让短请求可以立即执行,长请求单独或与相近长度的请求组批。
3.2 动态批处理的核心思想
3.2.1 核心原理
动态批处理(Dynamic Batching)的核心思想是:不等待批次填满,而是基于时间窗口、请求数量或资源水位等条件,动态决定何时执行推理。在LLM场景中,动态批处理通常与连续批处理(Continuous Batching)结合,允许请求在生成过程中动态加入或离开批次。
动态批处理的关键机制包括:
- 调度策略:基于最大延迟目标(如200ms)、最小吞吐要求或GPU利用率阈值触发推理。
- 请求分组:将长度相近的请求分到同一批次,减少padding浪费。
- 流式处理:请求完成生成后立即从批次中移除,新请求可加入,实现“边生成边换人”。
- 优先级队列:支持高优先级请求插队,满足SLA要求。
3.2.2 动态批处理的实现框架
当前主流推理框架(如vLLM、TensorRT-LLM、TGI)均实现了动态批处理机制。以vLLM为例,其核心调度逻辑如下:
# 伪代码:动态批处理调度逻辑
class Scheduler:
def __init__(self, max_batch_size, max_wait_ms):
self.running_batch = []
self.waiting_queue = []
self.max_batch_size = max_batch_size
self.max_wait_ms = max_wait_ms
def schedule(self):
# 条件1:当前批次未满且等待队列非空
if len(self.running_batch) < self.max_batch_size and self.waiting_queue:
# 从等待队列中取出请求加入批次
new_requests = self.waiting_queue.pop(0)
self.running_batch.append(new_requests)
# 条件2:达到最大等待时间,强制推理
if self._elapsed_time() >= self.max_wait_ms:
self._execute_batch()
# 条件3:批次已满,立即推理
if len(self.running_batch) >= self.max_batch_size:
self._execute_batch()
3.2.3 案例拆解:动态批处理优化效果
继续使用3.1.3中的客服系统案例。将静态批处理改为动态批处理(最大等待时间50ms,最大batch_size=8)后:
优化后表现:
- 短请求在50ms内被调度执行,生成时间0.3秒,总延迟约350ms。
- 长请求单独或与后续到达的长请求组批,生成时间3.5秒,但不再阻塞其他请求。
- P99延迟从4.2秒降至1.2秒,平均延迟从800ms降至450ms。
- GPU利用率从45%提升至72%,吞吐量提升约60%。
3.3 对比分析
3.3.1 多维度对比
| 对比维度 | 静态批处理 | 动态批处理 |
|---|---|---|
| 延迟表现 | 高且波动大,受最长请求主导 | 低且稳定,可配置延迟上限 |
| 吞吐量 | 中等,padding浪费严重 | 高,计算资源利用率更充分 |
| GPU利用率 | 锯齿状波动,存在空闲间隙 | 平滑连续,利用率更高 |
| 实现复杂度 | 简单,逻辑清晰 | 较复杂,需要调度器和状态管理 |
| 内存开销 | 较低,KV Cache固定分配 | 较高,需要动态管理KV Cache |
| 适用负载 | 请求长度均匀、到达速率稳定 | 请求长度差异大、到达速率波动 |
| SLA保障 | 难以保障P99延迟 | 可通过调度策略保障SLA |
3.3.2 选择判断路径
在实际部署中,选择静态还是动态批处理,可以遵循以下判断逻辑:
- 评估请求长度分布:若P90/P10长度比小于3,静态批处理可接受;若大于5,应使用动态批处理。
- 评估到达速率稳定性:若请求到达间隔的变异系数(CV)小于0.5,静态批处理效率尚可;若CV大于1,动态批处理优势明显。
- 评估延迟SLA:若P99延迟要求小于平均延迟的2倍,必须使用动态批处理。
- 评估GPU资源:若GPU利用率已超过80%,动态批处理可进一步提升;若低于40%,静态批处理可能已足够。
3.4 适用场景与选型建议
3.4.1 静态批处理的适用场景
- 离线批量推理:如离线文档分析、批量文本分类,对延迟不敏感。
- 请求长度均匀:如固定长度的向量化任务、图像分类(输入尺寸固定)。
- 吞吐优先且可等待:如数据管道中的批处理环节,允许秒级延迟。
- 资源受限环境:内存较小,无法承担动态KV Cache管理的额外开销。
3.4.2 动态批处理的适用场景
- 在线交互式应用:如聊天机器人、代码助手、实时翻译,要求低延迟。
- 请求长度差异大:如文档问答(长上下文)与简单问答(短上下文)混合。
- 流量波动明显:如电商大促期间的客服机器人,请求量在分钟级内剧烈变化。
- 多租户服务:不同租户的请求量、优先级不同,需要差异化调度。
3.4.3 混合策略建议
在实际生产环境中,静态与动态批处理并非互斥。推荐采用分层混合策略:
- 第一层:使用动态批处理作为主调度策略,保证在线请求的延迟SLA。
- 第二层:对低优先级、可延迟的请求(如日志分析、异步摘要),使用静态批处理模式,在GPU空闲时批量执行。
- 第三层:结合请求长度预测,将长度相近的请求优先组批,进一步减少padding浪费。
3.5 常见误区与总结复盘
3.5.1 常见误区
- 误区一:动态批处理一定比静态批处理好。实际上,在请求长度均匀、到达速率稳定的场景下,静态批处理更简单高效,动态批处理的调度开销反而成为负担。
- 误区二:动态批处理就是“来一个处理一个”。真正的动态批处理仍然会组batch,只是组batch的条件更灵活,而非完全放弃批处理。
- 误区三:忽略KV Cache管理。动态批处理中请求动态加入/离开,KV Cache需要动态分配和释放,若管理不当会导致内存碎片和显存溢出。
- 误区四:只关注平均延迟。动态批处理的核心优势在于降低P99延迟,评估时应重点关注延迟分布而非平均值。
3.5.2 总结复盘
静态批处理与动态批处理的选择本质上是延迟与吞吐的权衡。静态批处理通过牺牲延迟换取实现简单性和计算规整性;动态批处理则通过更复杂的调度逻辑换取更优的延迟表现和资源利用率。
在LLM部署场景中,由于生成式任务的序列长度天然存在较大差异,且在线服务对延迟敏感,动态批处理已成为主流选择。但静态批处理在离线场景中仍有其价值。建议团队根据自身业务特征,结合请求长度分布、到达速率、SLA要求三个核心维度进行量化评估,选择最合适的批处理策略。
最后需要强调的是,批处理策略并非“一劳永逸”的配置。随着业务发展、模型迭代和流量变化,应定期回顾批处理参数(如最大等待时间、最大batch_size),通过A/B测试验证当前配置是否仍然最优。