推理服务在处理大量请求时,如果逐个请求串行计算,GPU利用率会非常低。批处理把多个请求合成一个批次进行前向计算,能显著提高吞吐量。但不同的批处理策略对延迟、显存和实现复杂度的影响差异很大,静态批处理与连续批处理就是两种典型方案。理解它们的内部机制,有助于在线推理服务架构选型。

静态批处理的执行流程与限制
静态批处理(Static Batching)也叫固定批次处理,核心思想是在开始计算前确定一个固定大小的批次,所有请求一起送入模型,等批次内所有序列都生成结束后才统一返回结果。这种方式在离线批量推理和短文本分类任务中比较常见。它的最大优点是实现简单,批次形状固定,可以预先分配显存,计算过程稳定,延迟可预测。
但静态批处理有一个明显缺点:批次内的所有请求必须同时开始、同时结束。如果某个请求生成了很长的输出,而其他请求早已结束,那么整个批次都会等待这个最慢的请求,导致GPU出现空闲。这种木桶效应在文本生成、机器翻译等变长输出的任务中尤其严重。假设批次内有10个请求,其中9个只生成20个token,第10个生成了200个token,那第10个请求的后180步计算中,批次的有效利用率只有10%。这直接拉低了吞吐量,也增加了其他请求的排队延迟。
下面是一段简化的静态批处理循环代码,所有输入已经填充到相同长度,模型一次前向返回所有序列的logits,然后贪婪解码直到遇到结束符或达到最大长度。实际实现中,批次会被固定,无法中途加入新请求。
def static_batching_inference(model, requests, max_len):
# 假设所有请求的输入已经填充到相同长度
batch_inputs = [req.input_ids for req in requests]
batch_size = len(batch_inputs)
generated = [[] for _ in range(batch_size)]
finished = [False] * batch_size
for step in range(max_len):
if all(finished):
break
logits = model.forward(batch_inputs)
next_tokens = logits.argmax(dim=-1)
for i in range(batch_size):
if finished[i]:
continue
token = next_tokens[i].item()
generated[i].append(token)
if token == eos_token_id:
finished[i] = True
else:
batch_inputs[i] = batch_inputs[i] + [token]
return generated
连续批处理的核心原理与调度机制
连续批处理(Continuous Batching)也叫动态批处理或in-flight batching,它改变了静态批处理的固定范围限制。在连续批处理中,推理引擎维护一个活跃请求池,每个解码步骤都会重新构建批次。当某个序列生成结束符EOS时,该序列会立即从批次中移除,腾出的位置可以立即分配给等待队列中的新请求。这样GPU在前向计算中始终保持较高的利用率。
连续批处理的调度循环通常包含以下步骤:从等待队列中取出若干请求加入活跃池,直到达到最大批次大小或最大序列长度限制;对活跃池中的所有序列执行一次前向计算,得到下一个token的概率分布;根据采样策略为每个序列生成一个token;检查新生成的token是否为结束符,是则将该序列标记为完成并释放对应资源;未完成的序列继续保留在活跃池中,进入下一轮迭代。这个过程逐token推进,每个请求的生命周期动态变化。
以下代码展示了连续批处理循环的基本框架,其中使用了<进行批次上限判断,实际框架还会加入采样、KV cache管理等细节。
def continuous_batching_loop(model, request_queue, max_batch_size, max_seq_len):
active = []
while request_queue or active:
while len(active) < max_batch_size and request_queue:
req = request_queue.pop(0)
active.append({
'input_ids': req.input_ids,
'generated': [],
'finished': False
})
if not active:
break
batch_inputs = [seq['input_ids'] for seq in active]
logits = model.forward(batch_inputs)
next_tokens = logits.argmax(dim=-1)
new_active = []
for i, seq in enumerate(active):
if seq['finished']:
continue
token = next_tokens[i].item()
seq['generated'].append(token)
if token == eos_token_id or len(seq['generated']) >= max_seq_len:
seq['finished'] = True
else:
seq['input_ids'] = seq['input_ids'] + [token]
new_active.append(seq)
active = new_active
return
性能对比与选型建议
从吞吐量来看,连续批处理在输出长度分布差异大的场景下具有明显优势。因为它避免了长序列拖累短序列的问题,可以让GPU在每次迭代时尽可能满载。静态批处理则更适合输入输出长度固定或差异很小的任务,比如批量文本分类、图像特征提取等。在这些任务中,静态批处理的调度开销更低,批次形状固定也利于使用TensorRT等推理优化器进行算子融合。
从延迟角度来看,静态批处理的单个批次耗时由最慢的请求决定,因此P99延迟可能很差,对在线服务不友好。连续批处理虽然也受到最长序列影响,但因为可以同时服务不同进度的请求,短请求的等待时间大幅缩短。对于聊天机器人、代码补全、翻译等实时性要求高的服务,连续批处理几乎是默认选择。
选型时还需要考虑实现复杂度。静态批处理代码简单,容易调试,适合离线流水线;连续批处理需要维护活跃序列状态、处理显存碎片、设计调度队列,工程成本更高。不过目前主流的推理框架,比如vLLM、TensorRT-LLM、Hugging Face TGI等都已经内置了连续批处理能力,开发者可以直接使用。如果条件允许,在线服务优先选择连续批处理,同时设置合理的最大批次大小和最大序列长度,避免显存溢出。
连续批处理的工程实现要点
显存管理是连续批处理需要重点解决的问题。由于序列长度动态变化,如果为每个请求预留最大长度的KV cache,显存浪费会很严重。现代推理引擎引入了PagedAttention等分页管理机制,把KV cache切分成固定大小的块,按需分配,减少碎片。例如vLLM通过块表把逻辑位置映射到物理显存,使不同长度的序列可以共享显存池。这种设计与连续批处理天然匹配,能进一步提高吞吐量。
调度策略也直接影响性能。常见的策略包括先进先出FIFO、优先级队列、抢占式调度等。当等待队列较长时,可以优先调度短序列,让它们快速完成,降低平均延迟。还可以设置超时机制,避免某些请求长时间占用活跃池位置。最大批次大小通常需要根据GPU显存和模型大小动态调整,过大会导致显存不足,过小会降低吞吐量。工程中一般通过压力测试找到最优值。
静态批处理和连续批处理不是非此即彼,离线场景仍然可以用静态批处理获得简单稳定的性能,而在线推理服务则更适合连续批处理。理解它们的调度差异和性能特征,能够帮助开发者在设计推理系统时做出合理的选择。