vLLM 全局架构概览
vLLM 是一个面向高吞吐量 LLM 推理的引擎,其核心设计哲学是关注点分离:用户接口、请求调度、内存管理、模型执行各层解耦,通过清晰的接口协议连接。整个系统可以分为以下六个主要层级:
- LLM(用户入口):
vllm/entrypoints/llm.py— 面向离线批推理的高层封装,屏蔽引擎细节。 - LLMEngine(推理引擎):
vllm/engine/llm_engine.py— 核心调度枢纽,管理 Tokenizer、Scheduler、Executor。 - Scheduler(调度器):
vllm/core/scheduler.py— 维护三队列(waiting/running/swapped),决定每一步执行哪些序列、分配多少 KV-Block。 - Executor(执行器):
vllm/executor/— 抽象执行后端,支持单 GPU(GPUExecutor)、多 GPU Ray(RayGPUExecutor)、CPU、Neuron 等。 - Worker / ModelRunner(工作单元):
vllm/worker/— 每个 GPU 对应一个 Worker,负责管理 CacheEngine(物理 KV-cache 内存)并通过 ModelRunner 运行 forward pass。 - BlockManager(显存管理):
vllm/core/block_manager_v1.py / v2.py— PagedAttention 的物理块分配器,以 block 为粒度管理 GPU/CPU KV-cache。
下图展示了各层级之间的层次关系与调用方向:
entrypoints/llm.py
离线推理入口"] ASYNC["AsyncLLMEngine
在线服务入口"] end subgraph EngineLayer["引擎层"] ENGINE["LLMEngine
engine/llm_engine.py
请求管理 · step() 主循环"] end subgraph ScheduleLayer["调度层"] SCHED["Scheduler
core/scheduler.py
waiting / running / swapped 三队列"] BM["BlockManager
core/block_manager_v1.py
PagedAttention KV-Block 分配"] end subgraph ExecutionLayer["执行层"] EXEC["Executor
executor/gpu_executor.py
单/多 GPU 调度"] WORKER["Worker
worker/worker.py
GPU 管理 · 分布式通信"] RUNNER["ModelRunner
worker/model_runner.py
Prefill / Decode forward"] end subgraph TokenLayer["Tokenize 层"] TOK["Tokenizer / Detokenizer
transformers_utils/"] end LLM -->|"from_engine_args()"| ENGINE ASYNC -->|"从 from_engine_args() 继承"| ENGINE ENGINE -->|"schedule()"| SCHED SCHED -->|"分配/回收 blocks"| BM ENGINE -->|"execute_model()"| EXEC EXEC -->|"execute_model()"| WORKER WORKER -->|"execute_model()"| RUNNER ENGINE -->|"encode/decode"| TOK
从调用链看,LLM 是对 LLMEngine 的薄封装,真正的工作都在 LLMEngine.step() 里完成:先调度(Scheduler.schedule()),再执行(Executor.execute_model()),最后处理输出(_process_model_outputs())。
LLM(entrypoints/llm.py)用于离线批处理:同步调用 step() 直到所有请求完成,简单直接。
AsyncLLMEngine(engine/async_llm_engine.py)用于在线服务:通过 asyncio 事件循环异步地向客户端流式返回 token,支持并发请求。
两者共用同一个 LLMEngine 核心,只是驱动方式不同。
LLM 类详解
LLM 是 vLLM 暴露给用户的最高层接口,位于 vllm/entrypoints/llm.py,全文 267 行。它的职责非常单纯:收集参数、创建引擎、提供 generate() 方法。
2.1 __init__ 初始化流程
@LLM_decorator
def __init__(
self,
model: str,
tokenizer: Optional[str] = None,
tokenizer_mode: str = "auto",
skip_tokenizer_init: bool = False,
trust_remote_code: bool = False,
tensor_parallel_size: int = 1,
dtype: str = "auto",
quantization: Optional[str] = None,
revision: Optional[str] = None,
tokenizer_revision: Optional[str] = None,
seed: int = 0,
gpu_memory_utilization: float = 0.9,
swap_space: int = 4,
enforce_eager: bool = False,
max_context_len_to_capture: Optional[int] = None,
max_seq_len_to_capture: int = 8192,
disable_custom_all_reduce: bool = False,
**kwargs,
) -> None:
if "disable_log_stats" not in kwargs:
kwargs["disable_log_stats"] = True # 离线批处理默认不输出统计日志
engine_args = EngineArgs(
model=model,
... # 所有参数透传给 EngineArgs
)
self.llm_engine = LLMEngine.from_engine_args(
engine_args, usage_context=UsageContext.LLM_CLASS)
self.request_counter = Counter() # 单调递增的请求 ID 计数器
初始化只做了三件事:
- 强制
disable_log_stats=True:离线批处理场景不需要 Prometheus 统计日志,默认关闭。 - 构建
EngineArgs数据类:将用户传入的参数打包成 dataclass,为create_engine_config()做准备。 - 调用
LLMEngine.from_engine_args():解析配置、选择 Executor 类型、完成引擎初始化(加载模型权重、初始化 KV-cache)。
LLM_decorator 来自 vllm/entrypoints/llm_decorator.py,是一个厂商扩展点,允许在不修改 vLLM 核心代码的情况下对方法进行 monkey-patch(例如用于自定义推理前后处理)。在标准 vLLM 中它是一个透明的 identity decorator。
2.2 generate() 方法执行路径
generate() 是用户调用的核心方法,接受 prompts 列表,返回 List[RequestOutput]。它的执行路径可以分为两个阶段:请求提交和引擎循环。
@LLM_decorator
def generate(
self,
prompts: Optional[Union[str, List[str]]] = None,
sampling_params: Optional[Union[SamplingParams, List[SamplingParams]]] = None,
prompt_token_ids: Optional[List[List[int]]] = None,
use_tqdm: bool = True,
lora_request: Optional[LoRARequest] = None,
multi_modal_data: Optional[MultiModalData] = None,
) -> List[RequestOutput]:
# 1. 输入校验与归一化
if isinstance(prompts, str):
prompts = [prompts] # 单条 prompt 转为列表
if sampling_params is None:
sampling_params = SamplingParams() # 使用默认采样参数
# 2. 逐条请求提交到引擎
for i in range(num_requests):
self._add_request(
prompts[i], sampling_params_i, token_ids_i, ...)
# 3. 驱动引擎循环直到所有请求完成
return self._run_engine(use_tqdm)
@LLM_decorator
def _run_engine(self, use_tqdm: bool) -> List[RequestOutput]:
outputs: List[RequestOutput] = []
while self.llm_engine.has_unfinished_requests():
step_outputs = self.llm_engine.step() # 每次调用完成一个解码步
for output in step_outputs:
if output.finished:
outputs.append(output)
# 按 request_id 排序(因为请求可能不按顺序完成)
outputs = sorted(outputs, key=lambda x: int(x.request_id))
return outputs
这个 while 循环是离线推理的驱动引擎:每次 step() 调用对应一次 GPU forward pass(可能是 prefill 或 decode),直到所有请求的 finished 标志为 True。最后按 request_id 排序是为了保证返回结果的顺序与输入一致,因为短请求可能先于长请求完成。
2.3 _add_request 的工作
@LLM_decorator
def _add_request(
self,
prompt: Optional[str],
sampling_params: SamplingParams,
prompt_token_ids: Optional[List[int]],
lora_request: Optional[LoRARequest] = None,
multi_modal_data: Optional[MultiModalData] = None,
) -> None:
request_id = str(next(self.request_counter)) # "0", "1", "2", ...
self.llm_engine.add_request(
request_id, prompt, sampling_params,
prompt_token_ids, lora_request=lora_request,
multi_modal_data=multi_modal_data)
_add_request 的核心是生成单调递增的 request_id(字符串形式的整数),然后把请求委托给 LLMEngine.add_request()。这个 ID 贯穿请求的整个生命周期,用于排序、跟踪和取消。
EngineArgs 参数解析
EngineArgs 是 vLLM 的参数收集器,位于 vllm/engine/arg_utils.py(654 行)。它是一个 @dataclass,将所有引擎配置参数汇聚在一处,并提供 create_engine_config() 方法将参数分拆为若干专职 Config 对象。
3.1 EngineArgs 字段分类
@dataclass
class EngineArgs:
"""Arguments for vLLM engine."""
# 模型加载
model: str
tokenizer: Optional[str] = None
tokenizer_mode: str = 'auto'
trust_remote_code: bool = False
download_dir: Optional[str] = None
load_format: str = 'auto' # 'auto'|'pt'|'safetensors'|'npcache'|'dummy'|'tensorizer'
revision: Optional[str] = None
# 数值精度
dtype: str = 'auto' # 'auto'|'float16'|'bfloat16'|'float32'
kv_cache_dtype: str = 'auto' # KV-cache 独立精度,支持 fp8
# 显存与块
max_model_len: Optional[int] = None
block_size: int = 16 # PagedAttention 块大小(token 数),可选 8/16/32
gpu_memory_utilization: float = 0.90 # GPU 显存使用比例
swap_space: int = 4 # CPU swap 空间 (GiB)
enable_prefix_caching: bool = False
use_v2_block_manager: bool = False
# 并行
tensor_parallel_size: int = 1
pipeline_parallel_size: int = 1
worker_use_ray: bool = False
# 调度
max_num_batched_tokens: Optional[int] = None # 每步最多处理的 token 数
max_num_seqs: int = 256 # 每步最多并发序列数
scheduler_delay_factor: float = 0.0
enable_chunked_prefill: bool = False
# 量化
quantization: Optional[str] = None # 'awq'|'gptq'|'squeezellm'|'fp8'|None
# 运行模式
enforce_eager: bool = False # 禁用 CUDA Graph,纯 eager 模式
max_seq_len_to_capture: int = 8192 # CUDA Graph 覆盖的最大序列长度
# LoRA
enable_lora: bool = False
max_loras: int = 1
max_lora_rank: int = 16
# 推测解码(Speculative Decoding)
speculative_model: Optional[str] = None
num_speculative_tokens: Optional[int] = None
# 视觉语言模型
image_input_type: Optional[str] = None
image_token_id: Optional[int] = None
3.2 create_engine_config():参数分拆为专职 Config 对象
create_engine_config() 是 EngineArgs 最关键的方法(L521-L616),它将扁平的参数字典拆分为 7 个专职配置对象,每个对象只关心自己的领域:
@preprocess_engine_config
def create_engine_config(self) -> EngineConfig:
device_config = DeviceConfig(self.device) # CPU/GPU/Neuron 设备类型
model_config = ModelConfig( # 模型架构、精度、上下文长度
self.model, self.tokenizer, self.tokenizer_mode,
self.trust_remote_code, self.dtype, self.seed,
self.revision, ..., self.max_model_len,
self.quantization, ...)
cache_config = CacheConfig( # KV-cache 块大小、显存占比、swap
self.block_size, self.gpu_memory_utilization,
self.swap_space, self.kv_cache_dtype,
self.num_gpu_blocks_override,
model_config.get_sliding_window(),
self.enable_prefix_caching)
parallel_config = ParallelConfig( # TP/PP 并行度、Ray 配置
self.pipeline_parallel_size, self.tensor_parallel_size,
self.worker_use_ray, ...)
speculative_config = SpeculativeConfig.maybe_create_spec_config(...) # 推测解码
scheduler_config = SchedulerConfig( # 调度策略参数
self.max_num_batched_tokens, self.max_num_seqs,
model_config.max_model_len, ...,
enable_chunked_prefill=self.enable_chunked_prefill)
lora_config = LoRAConfig(...) if self.enable_lora else None
load_config = LoadConfig( # 权重加载格式、路径
load_format=self.load_format,
download_dir=self.download_dir, ...)
decoding_config = DecodingConfig( # 引导解码后端
guided_decoding_backend=self.guided_decoding_backend)
return EngineConfig(
model_config=model_config, cache_config=cache_config,
parallel_config=parallel_config, scheduler_config=scheduler_config,
device_config=device_config, lora_config=lora_config,
speculative_config=speculative_config, load_config=load_config,
decoding_config=decoding_config)
3.3 关键参数深析
PagedAttention 的核心参数。KV-cache 以 block 为粒度分配,每个 block 存储 block_size 个 token 的 K、V 向量。block_size=16 意味着每次至少分配 16 个 token 的 KV 空间,内碎片最大为 15 个 token。增大 block_size 提高 GPU 内存利用率但增加内碎片;减小则相反。
vLLM 启动时进行一次 profile run:先加载模型权重,然后用 gpu_memory_utilization * 总显存 - 已用显存 计算出可以分配多少个 KV-cache block,这个数字直接决定了系统能并发处理的最大序列长度之和。设置过高可能 OOM,过低浪费显存。
这两个参数共同约束调度器每一步的工作量:
• max_num_batched_tokens:一次 forward pass 最多处理的 token 数(prefill+decode 之和)。
• max_num_seqs:同时运行的序列数上限(默认 256)。
调度器在每个 step 选取序列时,必须同时满足这两个约束。enable_chunked_prefill=True 时,一个长 prefill 可以跨多个 step 分块处理,从而与 decode 请求混合批处理。
默认情况下(enforce_eager=False),vLLM 对长度较短的序列使用 CUDA Graph 捕获固定的计算图以消除 GPU kernel launch overhead,对超出 max_seq_len_to_capture(默认 8192)的序列回退到 eager 模式。enforce_eager=True 完全禁用 CUDA Graph,适合调试或显存紧张场景。
从 LLM.generate() 到 LLMEngine 的数据流全景
本节追踪一条请求从用户调用 LLM.generate("Hello, world!") 到最终拿到 RequestOutput 的完整路径,重点关注数据如何被封装、变换和传递。
4.1 LLMEngine 的初始化:from_engine_args()
在 generate() 触发之前,LLMEngine.from_engine_args() 已经完成了引擎初始化。这个过程是 vLLM 启动最耗时的部分(加载权重、profile GPU 显存):
@classmethod
def from_engine_args(cls, engine_args: EngineArgs, ...) -> "LLMEngine":
engine_config = engine_args.create_engine_config() # 分拆为各种 Config 对象
# 根据配置选择 Executor 类型
if device_config.device_type == "neuron":
executor_class = NeuronExecutor
elif device_config.device_type == "cpu":
executor_class = CPUExecutor
elif parallel_config.worker_use_ray:
initialize_ray_cluster(...)
executor_class = RayGPUExecutor
else:
assert parallel_config.world_size == 1
executor_class = GPUExecutor # 单 GPU 场景默认路径
engine = cls(**engine_config.to_dict(),
executor_class=executor_class, ...)
return engine
LLMEngine.__init__ 的执行顺序(L86-L242):
- 初始化 Tokenizer(L150-L156):创建
BaseTokenizerGroup和Detokenizer。 - 创建 model_executor(L162-L172):根据选定的 executor_class 实例化执行器,执行器内部立即调用
_init_executor(),进而创建 Worker,加载模型权重到 GPU。 - 初始化 KV-cache(L174):调用
_initialize_kv_caches(),让 executor 做一次 profile run,确定 GPU/CPU 可用的 block 数,然后分配物理 KV-cache 内存。 - 创建 Scheduler(L219):此时
cache_config.num_gpu_blocks已被更新,Scheduler 拿到准确的资源数量初始化 BlockManager。 - 创建 output_processor(L231-L242):根据调度配置决定是否使用 beam search / speculative decoding 的输出处理器。
def _initialize_kv_caches(self) -> None:
# Executor 在 worker 上运行 profile_run,测量实际显存占用
num_gpu_blocks, num_cpu_blocks = (
self.model_executor.determine_num_available_blocks())
# 允许通过 num_gpu_blocks_override 强制指定块数(用于测试抢占)
if self.cache_config.num_gpu_blocks_override is not None:
num_gpu_blocks = self.cache_config.num_gpu_blocks_override
self.cache_config.num_gpu_blocks = num_gpu_blocks
self.cache_config.num_cpu_blocks = num_cpu_blocks
# 分配物理 KV-cache 内存
self.model_executor.initialize_cache(num_gpu_blocks, num_cpu_blocks)
4.2 add_request():请求封装为 SequenceGroup
用户的 prompt 字符串在 LLMEngine.add_request() 中完成从文本到数据结构的转变:
@nvtx_tag
def add_request(self, request_id: str, prompt: Optional[str],
sampling_params: SamplingParams, ...) -> None:
arrival_time = time.time() # 记录到达时间(用于调度优先级)
# Step 1: 分词(tokenize)
prompt_token_ids = self.encode_request(
request_id=request_id, prompt=prompt, ...)
# Step 2: 创建 Sequence 对象
seq_id = next(self.seq_counter)
eos_token_id = self.tokenizer.get_lora_tokenizer(...).eos_token_id
seq = Sequence(seq_id, prompt, prompt_token_ids,
block_size, eos_token_id, lora_request)
# Step 3: clone SamplingParams(防止多个请求共享同一对象)
sampling_params = sampling_params.clone()
sampling_params.update_from_generation_config(self.generation_config_fields)
# Step 4: 创建 SequenceGroup
seq_group = SequenceGroup(request_id, [seq], sampling_params,
arrival_time, lora_request, multi_modal_data)
# Step 5: 提交到 Scheduler
self.scheduler.add_seq_group(seq_group) # 进入 waiting 队列
Sequence:单条序列,包含 token ID 列表、已分配的 KV-block 列表、当前状态(WAITING/RUNNING/SWAPPED/FINISHED)。
SequenceGroup:同一 prompt 派生出的所有序列(best_of=N 时产生 N 条)。调度器以 SequenceGroup 为粒度决策,但以 Sequence 为粒度分配 KV-block。对于普通请求(best_of=1),一个 SequenceGroup 只含一条 Sequence。
4.3 step():一次解码迭代的三步骤
LLMEngine.step()(L556-L630)是整个系统的心跳,每次调用完成一次 prefill 或 decode 迭代:
@nvtx_tag
def step(self) -> List[RequestOutput]:
# ===== Step 1: 调度 =====
seq_group_metadata_list, scheduler_outputs = self.scheduler.schedule()
# scheduler_outputs 包含:
# scheduled_seq_groups: 本步骤要运行的序列组
# blocks_to_swap_in: CPU -> GPU 的 block 迁移
# blocks_to_swap_out: GPU -> CPU 的 block 迁移(抢占/swap)
# blocks_to_copy: copy-on-write 的 block 复制(beam search)
# ===== Step 2: 执行模型 =====
if not scheduler_outputs.is_empty():
execute_model_req = ExecuteModelRequest(
seq_group_metadata_list=seq_group_metadata_list,
blocks_to_swap_in=scheduler_outputs.blocks_to_swap_in,
blocks_to_swap_out=scheduler_outputs.blocks_to_swap_out,
blocks_to_copy=scheduler_outputs.blocks_to_copy,
num_lookahead_slots=scheduler_outputs.num_lookahead_slots,
running_queue_size=scheduler_outputs.running_queue_size,
)
output = self.model_executor.execute_model(
execute_model_req=execute_model_req)
# output: List[SamplerOutput],每个 SamplerOutput 包含采样到的 token
else:
output = []
# ===== Step 3: 处理输出 =====
request_outputs = self._process_model_outputs(
output,
scheduler_outputs.scheduled_seq_groups,
scheduler_outputs.ignored_seq_groups,
seq_group_metadata_list)
self.do_log_stats(scheduler_outputs, output)
return request_outputs
4.4 数据流全景图
4.5 SequenceGroupMetadata:引擎层与执行层的数据契约
SequenceGroupMetadata 是调度器向执行层传递信息的核心数据结构,包含:
request_id:请求 IDis_prompt:是否是 prefill 阶段seq_data:每条 Sequence 的 token ID 和已生成 token 数sampling_params:采样参数block_tables:每条 Sequence 已分配的 KV-block 映射表(逻辑块 → 物理块)token_chunk_size:本次 forward 处理的 token 数(chunked prefill 场景下可小于 prompt 长度)
BlockManager 在调度阶段完成逻辑块到物理块的映射,ModelRunner 拿到 block_tables 后直接用物理块地址做 PagedAttention,无需了解调度逻辑。
设计哲学
理解 vLLM 的设计需要回答一个问题:为什么要这样分层?每一层的存在都有其深刻的工程动机。
5.1 为什么 LLM 和 LLMEngine 要分开?
LLMEngine 本身是无状态驱动的——它不会自己跑循环,需要外部调用 step()。这个设计使得同一个引擎可以被两种截然不同的方式驱动:
- 离线批处理(
LLM):用一个同步while循环驱动,简单粗暴,适合脚本场景。 - 在线服务(
AsyncLLMEngine):在 asyncio 事件循环中异步驱动,支持请求动态到达、流式输出、并发取消,适合生产服务。
如果引擎自带循环,就无法支持这种双模式。
5.2 为什么 EngineArgs 要转换为多个 Config 对象?
EngineArgs 是面向用户的接口(CLI 参数、Python API),强调易用性,所以参数扁平化放在一起。而 ModelConfig、CacheConfig、SchedulerConfig 等是面向内部组件的接口,强调关注点分离:
- Scheduler 只需关心
SchedulerConfig,不需要看到模型 dtype。 - ModelRunner 只需关心
ModelConfig,不需要看到 swap_space 大小。 - 测试时可以只构造相关的 Config 对象,不需要完整的 EngineArgs。
这种"用户友好的扁平接口 → 内部专职对象"的模式在大型系统中极为常见。
5.3 为什么 Executor 要抽象出来?
ExecutorBase 的抽象层(vllm/executor/executor_base.py)实现了执行后端无关性。LLMEngine 只调用 executor.execute_model(),完全不知道下面是单 GPU、多 GPU Ray 集群、CPU 还是 Neuron 芯片。这使得:
- 新增硬件支持只需实现一个新的 Executor,不需要修改引擎逻辑。
- 分布式推理(
RayGPUExecutor)的复杂性被完全封装在 Executor 层。 - 测试时可以用 Mock Executor 验证调度逻辑,无需真实 GPU。
class ExecutorBase(ABC):
@abstractmethod
def determine_num_available_blocks(self) -> Tuple[int, int]:
"""Profile GPU to determine available KV-cache blocks."""
...
@abstractmethod
def initialize_cache(self, num_gpu_blocks: int, num_cpu_blocks: int) -> None:
"""Allocate KV-cache memory."""
...
@abstractmethod
def execute_model(self, execute_model_req: ExecuteModelRequest
) -> List[SamplerOutput]:
"""Run forward pass, return sampled tokens."""
...
5.4 为什么 Scheduler 要独立于 Engine?
调度逻辑(选哪些序列、分配多少 block、何时抢占)是 vLLM 性能的核心,也是最容易变化的部分。将其独立为 Scheduler 有几个好处:
- 可测试性:Scheduler 的逻辑可以在不启动 GPU 的情况下单独测试。
- 可替换性:可以实现不同的调度策略(Default、Chunked Prefill、Speculative Decoding 感知调度)而不影响 Engine 代码。
- 关注点清晰:调度决策(要执行什么)和执行(如何执行)完全解耦。
5.5 整体设计原则总结
-
迭代级调度(Iteration-level Scheduling):每个
step()都重新调度,而不是为每个请求预留固定时间片。这使得短请求不会被长请求阻塞,GPU 始终保持高利用率。 - PagedAttention(分页注意力):KV-cache 不按序列长度预分配,而是按需分配物理 block,彻底消除外部碎片,显著提高 GPU 显存利用率,这是 vLLM 高吞吐量的根本来源。
- 层次解耦:用户接口 → 引擎 → 调度器 → 执行器 → Worker,每层只向下层发出请求,不感知实现细节。
- 配置与代码分离:所有可调参数集中在 EngineArgs / Config 对象中,核心代码路径不包含硬编码配置。
理解了这五点设计哲学,再阅读 vLLM 任何其他模块的代码,都能快速找到该模块在整体架构中的位置和职责边界。