4、vLLM初探:架构解析、核心组件

聊完TensorRT-LLM,咱们来看看vLLM。说实话,我第一次接触vLLM时,最直观的感受就是——这玩意儿把「显存管理」玩出了花。你想想看,大模型推理最头疼的是什么?不就是显存不够用嘛。vLLM的PagedAttention,说白了就是给显存做了一次「内存分页」,跟操作系统那套虚拟内存的思路如出一辙。

4.1 整体架构:一张图看懂vLLM

先别急着看代码,咱们先搭个框架。vLLM的核心架构,我习惯把它拆成三层:

  • 调度层(Scheduler):决定谁先跑、谁后跑、跑多少
  • 显存管理层(KV Cache Manager):管好每一块显存,不浪费一丁点
  • 执行层(Executor):真正干活的,调用GPU跑模型

这三层配合起来,就像一家餐厅:调度层是前台排号,显存管理层是后厨备菜,执行层就是大厨炒菜。嗯,这个比喻我经常在培训时用,大家一下就懂了。

核心逻辑流程图

vLLM 核心架构与数据流 用户请求(Request) Scheduler(调度层) 决定批次大小、序列调度策略 KV Cache Manager(显存管理层) PagedAttention 分页管理、显存分配与回收 Executor(执行层) GPU 模型前向推理 核心组件 • PagedAttention • Block Manager • Scheduler Policy • Worker • Model Runner • Cache Engine • Tokenizer • Sampling Params

4.2 PagedAttention:vLLM的杀手锏

PagedAttention,这名字起得真妙。它把KV Cache切成固定大小的「块」(Block),每个块可以独立分配、释放、甚至共享。我刚开始看论文时,脑子里蹦出的第一个念头就是:「这不就是操作系统的分页嘛!」

传统做法是什么样的?每个请求的KV Cache是一整块连续显存。假设一个请求生成了1024个token,你就得预留1024个token的显存。但问题是——你事先不知道它会生成多少个token啊!于是只能按最大长度预留,结果就是大量显存被浪费。

vLLM的做法呢?把KV Cache切成16个token一页。生成到哪,就分配哪一页。用不完的页,还能给别的请求用。你想想看,这显存利用率能不高吗?

个人经验:我在部署一个70B模型时,传统方案需要4张A100 80G,换成vLLM后3张就够了。省下来的那张卡,我拿去跑了个小模型做辅助任务。这就是PagedAttention带来的实际收益。

4.3 Scheduler:调度器的艺术

调度器,说白了就是决定「谁先跑」和「跑多少」。vLLM的Scheduler跟传统方案最大的区别在于——它知道每个请求的显存占用情况。

传统调度器只看请求数量,不管显存。结果就是:明明显存还剩50%,但新请求就是进不来,因为预留策略把显存「锁死」了。vLLM的Scheduler会实时查询KV Cache Manager,看看还剩多少空闲块,再决定能不能接新请求。

具体调度策略我总结了一下:

策略 说明 适用场景
FCFS(先来先服务) 按请求到达顺序处理 低并发、延迟敏感
Max Utilization(最大利用率) 尽量填满GPU,提高吞吐 高并发、离线批处理
Preemption(抢占) 高优先级请求打断低优先级 混合负载、在线服务

嗯,这里要注意:抢占策略虽然好用,但代价不小。被抢占的请求需要重新计算KV Cache,相当于白干了一次。我建议只在延迟要求特别严格的场景下才开启。

4.4 KV Cache Manager:显存的管家

KV Cache Manager,我管它叫「显存管家」。它的职责很明确:管好每一块显存,做到物尽其用。

它的核心数据结构是Block Table,记录着每个逻辑块映射到哪个物理块。这跟操作系统的页表简直一模一样。每个请求进来时,Manager会分配一个逻辑地址空间,然后按需映射到物理显存块。

举个例子:

# 伪代码示意
class BlockManager:
    def __init__(self, total_blocks):
        self.free_blocks = list(range(total_blocks))
        self.block_table = {}  # 逻辑块 -> 物理块
    
    def allocate(self, num_blocks):
        # 从空闲池中取出 num_blocks 个物理块
        allocated = self.free_blocks[:num_blocks]
        self.free_blocks = self.free_blocks[num_blocks:]
        return allocated
    
    def free(self, physical_blocks):
        # 回收物理块
        self.free_blocks.extend(physical_blocks)

这段代码看着简单,但实际实现要考虑的事情多着呢。比如:块碎片化问题、共享块的生命周期管理、显存不足时的淘汰策略……

避坑指南:我曾经在生产环境遇到过一个问题——显存明明还有剩余,但新请求就是分配不到块。查了半天,发现是块碎片化太严重了。每个请求都申请了不同大小的块,导致空闲块虽然多,但都是零散的,凑不出一个完整的连续块。后来我调整了块大小,从固定16改成可配置的,问题就解决了。

4.5 三个组件如何协同工作

光说单个组件没意思,咱们看看它们怎么配合的。一个请求从进来到输出,大致走这么几步:

  1. 请求到达:Scheduler收到请求,先问KV Cache Manager:「还有多少空闲块?」
  2. 资源检查:Manager查一下空闲块列表,返回可用数量。
  3. 调度决策:Scheduler根据可用块数和当前负载,决定是否接受请求。
  4. 显存分配:如果接受,Manager分配初始块,并记录到Block Table。
  5. 模型执行:Executor拿到请求,开始推理。每生成一个token,Manager动态分配新块。
  6. 请求结束:Manager回收所有块,Scheduler更新状态。

你看,整个过程环环相扣。任何一个组件出问题,整个推理链路都会卡住。我调试vLLM时,最常看的就是Scheduler的日志和Manager的块分配情况。

一句话总结:vLLM通过PagedAttention把显存利用率拉满,Scheduler负责「看人下菜碟」,KV Cache Manager则像个精打细算的管家。三个组件配合好了,大模型推理的吞吐量能翻倍。

好了,vLLM的架构和核心组件就聊到这儿。下一节咱们会深入代码,看看这些组件到底是怎么实现的。到时候我会带着大家手撕源码,把每个关键函数都过一遍。


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