vLLM架构总览:调度器、块管理器、推理引擎的协作
说实话,我第一次接触vLLM源码时,也被它的三层架构绕晕过。调度器、块管理器、推理引擎——这三个家伙到底怎么分工的?
后来我在一个生产项目中调试显存泄漏,才真正搞明白它们的协作关系。今天我就用最直白的方式,把这张"铁三角"的底牌翻给你看。
一、整体架构:三个角色一台戏
vLLM的核心架构,说白了就是三个模块的精密配合:
- 调度器(Scheduler):负责"谁先上"的问题。它决定哪些请求该被处理,哪些该排队。
- 块管理器(Block Manager):负责"内存怎么分"的问题。它管理KV Cache的分配和回收。
- 推理引擎(LLM Engine):负责"怎么算"的问题。它执行真正的模型前向计算。
我习惯把这三者比作一个餐厅的运作:调度器是前台领位员,块管理器是仓库管理员,推理引擎就是后厨的大厨。缺一个,这顿饭都做不成。
核心要点:vLLM的创新在于,它把显存管理从"静态预分配"变成了"动态按需分配"。这个转变,让PagedAttention成为可能。
二、调度器:请求的"交通警察"
调度器的工作,比你想象的要复杂得多。它不只是简单地把请求排个队。
2.1 调度策略
vLLM默认使用先来先服务(FCFS)策略。但我在实际部署中发现,光靠这个远远不够。
举个例子:你同时收到一个短查询和一个长文档生成请求。如果先处理长的,短的就得等半天。用户体验会很差。
我的经验:在生产环境中,我通常会配合请求超时机制。如果一个请求等待超过5秒,就给它插队。这个策略在延迟敏感场景下特别管用。
2.2 批次组装
调度器另一个关键任务,是把多个请求拼成一个批次(batch)。
你想想看,GPU最擅长的是并行计算。如果一次只处理一个请求,那利用率就太低了。调度器会尽量把长度相近的请求凑在一起,减少padding浪费。
# 调度器核心逻辑(简化版)
class Scheduler:
def schedule(self, waiting_queue):
batch = []
current_len = 0
max_batch_size = 64
for req in waiting_queue:
if current_len + req.len <= max_seq_len:
batch.append(req)
current_len += req.len
else:
break
return batch
注意:调度器不是万能的。如果请求长度差异太大,再好的调度策略也救不了。我曾经遇到一个极端情况:一个请求是10个token,另一个是4096个token。强行拼在一起,短的那个被padding浪费了99%的计算量。
三、块管理器:显存的"精算师"
块管理器是vLLM最核心的创新点。它实现了PagedAttention,说白了就是把KV Cache切成固定大小的块(block),按需分配。
3.1 逻辑块 vs 物理块
这里有个关键概念:逻辑块和物理块。
- 逻辑块:每个请求看到的连续地址空间。就像你写程序时看到的虚拟内存。
- 物理块:实际在GPU显存上的存储位置。可能是不连续的。
块管理器维护着一张映射表,把逻辑块映射到物理块。这个设计,让显存碎片化问题得到了极大缓解。
| 特性 | 传统方案 | vLLM 块管理 |
|---|---|---|
| 分配方式 | 预分配最大长度 | 按需分配 |
| 显存利用率 | 低(平均40-60%) | 高(可达95%+) |
| 碎片处理 | 容易产生碎片 | 块级管理,碎片少 |
| 支持动态长度 | 困难 | 天然支持 |
3.2 分配与回收策略
块管理器采用引用计数来跟踪每个物理块的使用情况。当一个请求完成,它占用的块引用计数减1。减到0时,块被回收。
我曾经踩过一个坑:有个请求因为异常中断,块管理器没有收到释放信号。结果那些块一直"死"在那里,显存越占越多。后来我加了一个超时回收机制,才解决这个问题。
关键参数:块大小(block_size)默认是16个token。太小了映射表太大,太大了浪费显存。我个人建议根据模型层数和head数来调,一般16-32之间比较合适。
四、推理引擎:真正的"算力核心"
推理引擎是执行层。它接收调度器送来的批次,以及块管理器分配好的显存地址,然后调用CUDA kernel进行计算。
4.1 执行流程
推理引擎的执行流程大致如下:
- 预处理:把输入token转换成embedding
- Transformer层计算:逐层执行self-attention和FFN
- PagedAttention kernel:使用块管理器提供的物理地址,执行attention计算
- 输出采样:从logits中采样下一个token
这里最特别的是第3步。传统的attention需要连续的KV Cache,但vLLM的PagedAttention kernel可以直接处理非连续的物理块。这个kernel是vLLM团队手写的CUDA代码,性能非常关键。
4.2 与调度器的交互
推理引擎和调度器之间有一个异步队列。调度器把批次放入队列,推理引擎从队列中取出执行。执行完后,通过回调通知调度器。
这种设计让两个模块可以并行工作。调度器在准备下一批时,推理引擎还在算当前批。吞吐量就这么提上来了。
调优建议:队列深度不要太大。我试过设成10,结果调度器塞得太快,推理引擎跟不上,反而增加了延迟。一般设成2-3就够了。
五、三者协作:一个完整的请求生命周期
现在我们把三个模块串起来,看看一个请求从进来到出去,到底经历了什么。
- 请求到达:调度器收到请求,放入等待队列
- 调度决策:调度器从队列中选取请求,组装成批次
- 显存分配:调度器通知块管理器,为批次中的每个请求分配物理块
- 执行计算:调度器把批次和块映射表一起交给推理引擎
- 迭代生成:推理引擎逐token生成,每次生成后更新KV Cache,块管理器按需分配新块
- 请求完成:生成结束,推理引擎通知调度器,调度器通知块管理器释放显存
这个流程看起来简单,但每个环节都有很多细节。我在项目中遇到过最头疼的问题,就是显存分配和释放的时序。如果调度器释放得太早,推理引擎还在用那个块,就会crash。如果释放得太晚,显存又不够用。
避坑指南:我曾经因为块管理器的引用计数bug,导致显存泄漏。排查了整整两天才发现,是某个异常路径没有正确减引用。建议你在自己的实现中,给块管理器加上健康检查接口,定期打印所有块的引用状态。
六、总结
vLLM的三层架构,说白了就是:
- 调度器管"节奏"——什么时候该处理谁
- 块管理器管"地盘"——显存怎么分怎么收
- 推理引擎管"干活"——真正把计算跑起来
这三者通过精心设计的接口和异步机制协作,才实现了vLLM的高吞吐和低延迟。理解了这个架构,你再去读vLLM源码,就会清晰很多。
嗯,下一节我们会深入PagedAttention的细节,看看那个神奇的kernel到底是怎么工作的。
公众号:蓝海资料掘金营,微信deep3321