批处理策略:动态批处理大小、请求排队与优先级调度、最大批处理延迟控制
批处理,说白了就是“攒一波再干”。
在vLLM里,这是控制成本最直接的手段。GPU算力很贵,你让它一次只处理一个请求,那跟开法拉利去菜市场买菜没啥区别——浪费。我刚开始做推理优化时,就犯过这个错。当时一个客户抱怨延迟高,我一看监控,GPU利用率不到15%。嗯,问题找到了。
动态批处理大小:别死脑筋
固定批处理大小,是最容易想到的方案。但实际跑起来,你会发现它很蠢。
为什么?因为请求大小不一样。有的请求就几个字,有的请求能写一篇论文。你固定batch size=8,遇到小请求时,GPU算力闲置;遇到大请求时,显存直接爆掉。
我个人习惯的做法是:动态调整。
核心思路:根据当前请求的token长度总和,动态决定这次批处理能塞多少个请求。
具体来说,我会设置两个阈值:
- 最大token数上限:比如4096个token。所有请求的token加起来不能超过这个数。
- 最大请求数上限:比如64个请求。防止请求太小导致调度开销过大。
代码实现其实不复杂:
def dynamic_batch_size(requests, max_tokens=4096, max_requests=64):
batch = []
total_tokens = 0
for req in requests:
req_tokens = len(req['input_ids'])
if total_tokens + req_tokens <= max_tokens and len(batch) < max_requests:
batch.append(req)
total_tokens += req_tokens
else:
# 当前批次满了,先处理
process_batch(batch)
batch = [req]
total_tokens = req_tokens
if batch:
process_batch(batch)
我在项目中遇到过一个问题:有些请求特别长,比如几千个token。如果它一来就把整个batch占满了,其他小请求就得等着。后来我加了个最大请求长度限制,超过512token的请求单独处理,或者拆分成多个小请求。效果好了很多。
请求排队与优先级调度:谁先谁后,得有个规矩
请求多了,就得排队。但排队不是简单的先来后到。
你想想看,一个用户在线聊天,他等不了3秒。但一个后台批量生成报告的任务,等个30秒也无所谓。如果一视同仁,那在线用户全跑了。
我建议的做法是:多级优先级队列。
| 优先级 | 适用场景 | 最大等待时间 |
|---|---|---|
| P0(最高) | 在线推理、实时对话 | 500ms |
| P1 | 半实时任务、数据分析 | 5s |
| P2 | 批量处理、离线生成 | 30s |
调度逻辑也很直接:
- 每次取batch时,优先从P0队列取。P0队列空了,再从P1取。
- 但如果P1队列里的请求等了太久(超过最大等待时间),就把它提升到P0。
- 这叫优先级老化,防止低优先级请求永远得不到处理。
小技巧:我曾经遇到一个坑——P0请求太多,导致P2请求一直饿死。后来我加了个“最低保障”机制:每处理10个P0请求,必须处理1个P2请求。这样既保证了实时性,又不至于让后台任务完全卡死。
最大批处理延迟控制:别让请求等到天荒地老
动态批处理有个副作用:你为了攒够一个batch,可能会让请求等很久。
比如现在只有1个请求,你非要等到凑够8个才处理。那这个请求就得等。如果后续请求一直不来,它可能等上好几秒。这在在线场景里是致命的。
解决方案很简单:设置一个最大等待时间。
import time
class BatchScheduler:
def __init__(self, max_batch_size=8, max_wait_ms=200):
self.max_batch_size = max_batch_size
self.max_wait_ms = max_wait_ms
self.queue = []
self.last_process_time = time.time()
def add_request(self, request):
self.queue.append(request)
now = time.time()
wait_time = (now - self.last_process_time) * 1000
if len(self.queue) >= self.max_batch_size or wait_time >= self.max_wait_ms:
self.process_batch()
这里有个权衡:max_wait_ms设得太小,batch size上不去,吞吐量低;设得太大,延迟高。
我个人习惯的做法是:动态调整max_wait_ms。如果当前系统负载低,我就把等待时间设长一点,多攒几个请求;如果负载高,我就缩短等待时间,优先保证延迟。
注意:我曾经在生产环境里踩过一个坑——把max_wait_ms设成了500ms,但某个时段请求量突然暴增,导致batch size一直达不到上限,每个请求都等了500ms才被处理。后来我改成“如果队列长度超过某个阈值,立即处理”,才解决了这个问题。
三者如何配合?一张图说清楚
动态批处理大小、请求排队与优先级调度、最大批处理延迟控制,这三者不是孤立的。它们需要协同工作。
流程其实很清晰:
- 请求先进入优先级队列,按P0/P1/P2分类排队。
- 调度器从队列中取请求,按照动态批处理大小的逻辑组装batch。
- 如果等待时间超过最大批处理延迟,不管batch满没满,直接处理。
这三者配合好了,你就能在延迟和吞吐量之间找到一个平衡点。我个人经验是:先保证延迟满足SLA,再尽量提高吞吐量。毕竟用户等不起,但GPU可以多跑一会儿。
总结一下:
- 动态批处理大小——别死板,根据请求大小灵活调整。
- 请求排队与优先级调度——分清楚轻重缓急,别让重要请求等着。
- 最大批处理延迟控制——设置一个底线,别让任何请求无限等待。
这三板斧砍下去,你的vLLM推理成本至少能降30%。我亲测有效。
公众号:蓝海资料掘金营,微信deep3321