vLLM架构总览:调度器、块管理器、推理引擎的协作

说实话,我第一次接触vLLM源码时,也被它的三层架构绕晕过。调度器、块管理器、推理引擎——这三个家伙到底怎么分工的?

后来我在一个生产项目中调试显存泄漏,才真正搞明白它们的协作关系。今天我就用最直白的方式,把这张"铁三角"的底牌翻给你看。

一、整体架构:三个角色一台戏

vLLM的核心架构,说白了就是三个模块的精密配合:

  • 调度器(Scheduler):负责"谁先上"的问题。它决定哪些请求该被处理,哪些该排队。
  • 块管理器(Block Manager):负责"内存怎么分"的问题。它管理KV Cache的分配和回收。
  • 推理引擎(LLM Engine):负责"怎么算"的问题。它执行真正的模型前向计算。

我习惯把这三者比作一个餐厅的运作:调度器是前台领位员,块管理器是仓库管理员,推理引擎就是后厨的大厨。缺一个,这顿饭都做不成。

核心要点:vLLM的创新在于,它把显存管理从"静态预分配"变成了"动态按需分配"。这个转变,让PagedAttention成为可能。

vLLM 核心架构:调度器 · 块管理器 · 推理引擎 调度器 请求排队与优先级 批次组装 块管理器 KV Cache 分配/回收 物理块 ↔ 逻辑块映射 推理引擎 模型前向计算 Attention 执行 请求批次 计算指令 协作流程 ① 调度器接收请求 → ② 块管理器分配显存 → ③ 推理引擎执行计算 ④ 结果返回调度器 → ⑤ 块管理器回收空闲块 → ⑥ 循环 vLLM 架构协作图 | 公众号:蓝海资料掘金营

二、调度器:请求的"交通警察"

调度器的工作,比你想象的要复杂得多。它不只是简单地把请求排个队。

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 执行流程

推理引擎的执行流程大致如下:

  1. 预处理:把输入token转换成embedding
  2. Transformer层计算:逐层执行self-attention和FFN
  3. PagedAttention kernel:使用块管理器提供的物理地址,执行attention计算
  4. 输出采样:从logits中采样下一个token

这里最特别的是第3步。传统的attention需要连续的KV Cache,但vLLM的PagedAttention kernel可以直接处理非连续的物理块。这个kernel是vLLM团队手写的CUDA代码,性能非常关键。

4.2 与调度器的交互

推理引擎和调度器之间有一个异步队列。调度器把批次放入队列,推理引擎从队列中取出执行。执行完后,通过回调通知调度器。

这种设计让两个模块可以并行工作。调度器在准备下一批时,推理引擎还在算当前批。吞吐量就这么提上来了。

调优建议:队列深度不要太大。我试过设成10,结果调度器塞得太快,推理引擎跟不上,反而增加了延迟。一般设成2-3就够了。

五、三者协作:一个完整的请求生命周期

现在我们把三个模块串起来,看看一个请求从进来到出去,到底经历了什么。

  1. 请求到达:调度器收到请求,放入等待队列
  2. 调度决策:调度器从队列中选取请求,组装成批次
  3. 显存分配:调度器通知块管理器,为批次中的每个请求分配物理块
  4. 执行计算:调度器把批次和块映射表一起交给推理引擎
  5. 迭代生成:推理引擎逐token生成,每次生成后更新KV Cache,块管理器按需分配新块
  6. 请求完成:生成结束,推理引擎通知调度器,调度器通知块管理器释放显存

这个流程看起来简单,但每个环节都有很多细节。我在项目中遇到过最头疼的问题,就是显存分配和释放的时序。如果调度器释放得太早,推理引擎还在用那个块,就会crash。如果释放得太晚,显存又不够用。

避坑指南:我曾经因为块管理器的引用计数bug,导致显存泄漏。排查了整整两天才发现,是某个异常路径没有正确减引用。建议你在自己的实现中,给块管理器加上健康检查接口,定期打印所有块的引用状态。

六、总结

vLLM的三层架构,说白了就是:

  • 调度器管"节奏"——什么时候该处理谁
  • 块管理器管"地盘"——显存怎么分怎么收
  • 推理引擎管"干活"——真正把计算跑起来

这三者通过精心设计的接口和异步机制协作,才实现了vLLM的高吞吐和低延迟。理解了这个架构,你再去读vLLM源码,就会清晰很多。

嗯,下一节我们会深入PagedAttention的细节,看看那个神奇的kernel到底是怎么工作的。


公众号:蓝海资料掘金营,微信deep3321