3. 并发请求调度优化:请求队列管理、动态批处理策略、优先级调度、请求超时处理
好,咱们直接进入正题。并发请求调度,说白了就是vLLM怎么把一堆涌进来的请求安排得明明白白。你想想看,大模型推理不像普通Web服务,一个请求可能占着GPU几十秒甚至几分钟。如果调度做得不好,GPU利用率低不说,用户等得也急。
我个人习惯把这块拆成四个核心点:队列怎么管、批怎么组、优先级怎么排、超时怎么处理。咱们一个一个聊。
3.1 请求队列管理:别让请求乱成一锅粥
vLLM内部维护了一个请求队列。每个请求进来,先排队。但这里有个坑——队列不是简单的FIFO。为什么?因为不同请求的输入长度、输出长度都不一样,直接先进先出会导致短请求被长请求堵死。
我在项目中遇到过这种情况:一个用户发了个超长文档要总结,后面几十个用户只是问“今天天气怎么样”。结果长请求处理了30秒,后面的短请求全卡着。用户体验直接崩了。
所以vLLM的队列管理做了几件事:
- 请求入队时记录元数据:包括输入token数、预估输出长度、请求到达时间
- 支持多级队列:比如一个高优队列、一个普通队列、一个后台队列
- 队列长度限制:防止内存被撑爆。我建议设置一个硬上限,比如队列最多积压1000个请求
核心原则:队列不是无限大的。当队列满了,新请求直接返回503或者走降级策略。别让系统被拖垮。
3.2 动态批处理策略:把请求“打包”送进GPU
vLLM最牛的地方之一就是动态批处理。传统做法是等一批请求凑齐了再一起推理,但这样延迟高。vLLM的做法是:边推理边接新请求。
具体来说,vLLM维护了一个“正在处理的批次”。当一个请求的某个token生成完后,vLLM会检查队列里有没有新请求可以加进来。如果有,就把新请求的prefill阶段和当前请求的decode阶段合并成一个批次。
嗯,这里要注意:不是所有请求都能随便合并。合并的条件包括:
- 新请求的prefill计算量不能太大,否则会拖慢当前批次
- 当前批次的剩余解码步数不能太少,否则合并收益不大
- GPU显存要够用
我曾经踩过一个坑:把一个大prompt的请求强行合并进一个快结束的批次,结果那个批次的延迟从50ms飙到了500ms。后来我加了个判断逻辑——新请求的prefill时间不能超过当前批次剩余时间的20%。
调优技巧:你可以通过环境变量VLLM_MAX_NUM_BATCHED_TOKENS来控制一个批次最多包含多少token。我一般设成4096或8192,具体看你的GPU显存。
3.3 优先级调度:给重要请求“插队”
不是所有请求都同等重要。比如线上API的付费用户请求,肯定比后台批量处理的任务优先级高。vLLM支持基于优先级的调度。
实现方式其实不复杂:
# 伪代码示意
class PriorityScheduler:
def __init__(self):
self.queues = {
'high': deque(),
'normal': deque(),
'low': deque()
}
def schedule(self):
# 高优队列每次取2个
# 普通队列每次取1个
# 低优队列每5次才取1个
for _ in range(2):
if self.queues['high']:
yield self.queues['high'].popleft()
if self.queues['normal']:
yield self.queues['normal'].popleft()
if self.counter % 5 == 0 and self.queues['low']:
yield self.queues['low'].popleft()
但这里有个问题:优先级不能滥用。如果所有请求都标成高优,那优先级就没意义了。我建议只对两类请求设高优:
- 实时交互类请求(比如聊天机器人)
- 付费用户的请求
其他请求,比如离线批量推理,统统丢到低优队列。
注意:优先级调度可能会导致低优请求长时间得不到处理,出现“饥饿”现象。我一般会加一个老化机制——低优请求在队列里等超过30秒,自动提升一级优先级。
3.4 请求超时处理:别让用户无限等
这个点容易被忽略,但生产环境里特别重要。一个请求如果因为各种原因卡住了,不能让它一直占用资源。
vLLM支持两种超时:
| 超时类型 | 默认值 | 说明 |
|---|---|---|
| 队列等待超时 | 60秒 | 请求在队列里等太久,直接返回超时错误 |
| 推理执行超时 | 300秒 | 请求开始推理后,超过这个时间还没完成,强制终止 |
我建议你在客户端也设一个超时,双重保险。比如:
# 客户端设置超时
import openai
client = openai.OpenAI(
base_url="http://your-vllm-server:8000/v1",
timeout=60 # 客户端超时60秒
)
曾经有一次,我忘了设服务端超时,结果一个请求因为模型输出长度设得太大,跑了整整10分钟。那个GPU卡被占着,其他请求全堵死了。从那以后,我每个部署都强制加上--max-model-len和--request-timeout参数。
最佳实践:超时后不要直接丢弃请求。可以返回一个“请求超时,请稍后重试”的提示,或者把请求信息记录下来,后续人工处理。
3.5 整体调度流程
来,我画个图帮你理清整个流程:
整个流程其实就是一个流水线:请求进来先排队,然后根据优先级决定谁先被处理,接着动态批处理把多个请求打包,最后GPU执行推理。整个过程都有超时机制兜底。
好了,关于并发请求调度优化,核心就是这四点。你想想看,如果把这四点都做到位,你的vLLM服务在高并发下基本不会出大问题。我自己的生产环境就是按这个思路搭的,单机扛住了每秒50+的请求量,P99延迟控制在2秒以内。
最后一个小建议:上线前一定要做压力测试。用wrk或者locust模拟高并发场景,看看队列会不会积压、批处理会不会失效、超时处理是否正常。别等到线上出问题了再排查。