4、vLLM初探:架构解析、核心组件
聊完TensorRT-LLM,咱们来看看vLLM。说实话,我第一次接触vLLM时,最直观的感受就是——这玩意儿把「显存管理」玩出了花。你想想看,大模型推理最头疼的是什么?不就是显存不够用嘛。vLLM的PagedAttention,说白了就是给显存做了一次「内存分页」,跟操作系统那套虚拟内存的思路如出一辙。
4.1 整体架构:一张图看懂vLLM
先别急着看代码,咱们先搭个框架。vLLM的核心架构,我习惯把它拆成三层:
- 调度层(Scheduler):决定谁先跑、谁后跑、跑多少
- 显存管理层(KV Cache Manager):管好每一块显存,不浪费一丁点
- 执行层(Executor):真正干活的,调用GPU跑模型
这三层配合起来,就像一家餐厅:调度层是前台排号,显存管理层是后厨备菜,执行层就是大厨炒菜。嗯,这个比喻我经常在培训时用,大家一下就懂了。
核心逻辑流程图
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 三个组件如何协同工作
光说单个组件没意思,咱们看看它们怎么配合的。一个请求从进来到输出,大致走这么几步:
- 请求到达:Scheduler收到请求,先问KV Cache Manager:「还有多少空闲块?」
- 资源检查:Manager查一下空闲块列表,返回可用数量。
- 调度决策:Scheduler根据可用块数和当前负载,决定是否接受请求。
- 显存分配:如果接受,Manager分配初始块,并记录到Block Table。
- 模型执行:Executor拿到请求,开始推理。每生成一个token,Manager动态分配新块。
- 请求结束:Manager回收所有块,Scheduler更新状态。
你看,整个过程环环相扣。任何一个组件出问题,整个推理链路都会卡住。我调试vLLM时,最常看的就是Scheduler的日志和Manager的块分配情况。
一句话总结:vLLM通过PagedAttention把显存利用率拉满,Scheduler负责「看人下菜碟」,KV Cache Manager则像个精打细算的管家。三个组件配合好了,大模型推理的吞吐量能翻倍。
好了,vLLM的架构和核心组件就聊到这儿。下一节咱们会深入代码,看看这些组件到底是怎么实现的。到时候我会带着大家手撕源码,把每个关键函数都过一遍。
公众号:蓝海资料掘金营,微信deep3321