4. vLLM整体架构:请求处理流程、核心组件
在了解了vLLM的定位与核心优势之后,我们需要深入其内部,理解一个推理请求从进入系统到最终返回文本,究竟经历了怎样的旅程。vLLM并非一个简单的“模型加载+前向计算”的黑盒,而是一个由多个职责明确的组件构成的分布式系统。本章将拆解其整体架构,重点剖析LLMEngine、Scheduler与Worker这三个核心角色,并梳理它们之间的协作关系。
4.1 核心组件与职责划分
vLLM的架构设计遵循了典型的“中心化调度、分布式执行”模式。我们可以将整个系统想象成一家高效运转的餐厅:LLMEngine是餐厅经理,负责接收订单、协调全局;Scheduler是后厨调度员,决定哪一桌的菜先做、如何拼桌;而Worker则是灶台上的厨师,真正动手烹饪(执行张量计算)。
下表清晰地列出了各核心组件的职责与关键接口:
| 组件 | 核心职责 | 关键对象/接口 |
|---|---|---|
| LLMEngine | 顶层抽象,接收请求、维护KV Cache管理器、驱动执行循环、管理输入输出 | add_request(), step(), abort_request() |
| Scheduler | 决定序列的调度顺序,管理序列状态(等待、运行、交换),执行抢占与交换策略 | schedule(), SequenceGroup, SchedulerOutput |
| Worker | 实际执行模型推理的进程,管理GPU显存、加载模型权重、执行注意力计算 | execute_model(), ModelRunner, CacheEngine |
| BlockManager | 管理KV Cache的物理块分配与释放,实现PagedAttention的显存管理 | allocate(), free(), PhysicalTokenBlock |
| ModelRunner | 将调度器输出的批次转换为张量输入,调用模型前向计算 | prepare_input_tensors(), execute_model() |
在单机场景下,LLMEngine直接持有Scheduler和Worker的引用;在多机分布式场景下,LLMEngine通过Ray或自定义通信层与远程Worker交互。但无论何种部署形态,请求处理的主循环逻辑始终由LLMEngine驱动。
4.2 请求处理主循环:从文本到Token再到文本
一个典型的请求处理流程可以拆解为以下五个阶段。我们以一次用户输入“请介绍一下Transformer”为例:
- 请求注册:客户端调用
engine.add_request(request_id, prompt, sampling_params)。LLMEngine将文本提示转换为Token ID序列,并创建Sequence对象,放入等待队列。 - 调度决策:LLMEngine调用
scheduler.schedule()。Scheduler根据当前KV Cache剩余空间、序列优先级、抢占策略,决定本次迭代执行哪些SequenceGroup,并为新序列分配逻辑块。 - 数据准备:Scheduler输出
SchedulerOutput,包含需要执行的序列列表及其块映射。ModelRunner将这些信息转换为GPU张量(如input_tokens,slot_mapping)。 - 模型执行:Worker调用
execute_model(),执行一次前向传播,得到每个序列的logits。随后通过采样(如top-p, temperature)生成新的Token。 - 状态更新:LLMEngine将新Token追加到对应Sequence中,更新KV Cache的块引用计数。若序列未结束(未生成EOS),则继续放入运行队列等待下一轮;若已结束,则释放其占用的物理块,并将完整输出返回给客户端。
上述步骤在engine.step()中被循环调用,直到所有请求处理完毕。值得注意的是,vLLM采用“迭代级调度”(iteration-level scheduling),即每次step()只生成一个Token,而非等待整个序列生成完毕。这使得不同长度的序列可以在同一批次中动态加入或退出,极大提升了GPU利用率。
# 伪代码:LLMEngine主循环
while self.has_unfinished_requests():
scheduler_output = self.scheduler.schedule() # 决策
output = self.worker.execute_model(scheduler_output) # 执行
self._update_sequences(output) # 更新状态
4.3 调度器核心逻辑:抢占与交换
Scheduler是vLLM性能优化的关键。它需要回答两个问题:“下一步执行谁?”和“显存不够时怎么办?”
对于第一个问题,Scheduler维护了三个队列:等待队列(waiting)、运行队列(running)和交换队列(swapped)。默认采用FCFS(先来先服务)策略,但支持优先级调整。每次调度时,Scheduler会尝试将等待队列中的序列加入运行队列,直到KV Cache无法容纳更多序列。
对于第二个问题,vLLM引入了抢占(preemption)机制。当新请求到达且显存不足时,Scheduler会选择运行队列中优先级最低的序列进行抢占。抢占分为两种:
- 交换(swap):将该序列的KV Cache从GPU显存复制到CPU内存(或NVMe),释放GPU空间给新请求。当后续显存充足时再交换回来。
- 重计算(recompute):直接丢弃该序列的KV Cache,后续重新从初始Token开始计算。这节省了显存带宽,但增加了计算开销。
默认策略是先交换,后重计算。交换适合KV Cache较大且CPU内存充足的场景;重计算适合计算资源充裕但显存极小的场景。Scheduler通过SequenceGroup的state字段跟踪每个序列的当前状态,确保状态转换的原子性。
4.4 Worker与模型执行细节
Worker是真正与GPU交互的组件。在单卡场景下,Worker内部包含一个ModelRunner和一个CacheEngine。ModelRunner负责将Scheduler输出的逻辑批次转换为物理张量,而CacheEngine管理GPU上的KV Cache张量。
以PagedAttention为例,ModelRunner需要为每个Token生成slot_mapping张量,该张量指示每个Token的KV Cache在物理块中的具体位置。这一映射关系由BlockManager维护。执行前向计算时,注意力机制通过gather操作从分散的物理块中收集KV对,计算完成后无需移动数据,仅更新块引用计数。
# Worker执行模型的核心调用链
class Worker:
def execute_model(self, scheduler_output):
input_tensors = self.model_runner.prepare_input_tensors(scheduler_output)
hidden_states = self.model_runner.execute_model(input_tensors)
return self.model_runner.sample(hidden_states)
在多GPU场景下,vLLM支持张量并行(tensor parallel)和流水线并行(pipeline parallel)。每个Worker负责模型的一部分(如部分注意力头或部分层),通过AllReduce等集合通信同步梯度或中间结果。此时,Scheduler的调度决策需要广播到所有Worker,确保它们执行相同的批次。
4.5 案例复盘:一次显存不足引发的调度决策
假设GPU总显存为16GB,KV Cache可用空间为8GB。当前运行队列中有3个序列(A、B、C),分别占用2GB、3GB、2GB Cache。此时新请求D到达,预估需要3GB Cache。Scheduler的决策过程如下:
- 检查剩余空间:8 - (2+3+2) = 1GB,不足以容纳D。
- 选择抢占目标:按优先级排序,假设C优先级最低。
- 执行交换:将C的2GB Cache写入CPU内存,释放GPU空间。此时可用空间变为3GB,恰好满足D的需求。
- 将D加入运行队列,C移入交换队列。
- 下一轮调度时,若A或B完成释放空间,则优先将C交换回GPU继续执行。
这一过程体现了vLLM“以空间换时间”的核心思想:通过精细的显存管理,最大化GPU利用率,同时保证请求的最终完成。
--swap-space参数控制CPU交换空间大小,并监控cpu_cache_usage和gpu_cache_usage指标来调优。
通过上述分析可以看出,vLLM的架构设计将复杂的显存管理与调度策略从模型计算中解耦,使得开发者可以专注于模型本身,而系统自动优化资源利用。理解LLMEngine、Scheduler与Worker的协作关系,是深入使用vLLM高级特性(如分布式部署、自定义调度策略)的基础。