vLLM Deep Dive Part 1: Architecture Overview

第一部分:vLLM 架构概览

简介

在深入研究具体组件之前,我们需要先了解 vLLM 的整体架构。本文梳理了各主要组件及其交互方式,为后续各部分的深入探讨奠定基础。

全局视角

vLLM 的设计围绕以下几个核心原则:

  1. 关注点分离:不同职责(调度、执行、服务)由各自独立的组件负责处理
  2. 进程隔离:V1 架构采用多进程设计,以提升健壮性和 CPU 利用率
  3. 异步处理:请求在流水线中流转,不会产生阻塞
  4. 默认分布式:从底层设计上即支持多 GPU 执行

高层数据流

当你向 vLLM 发送一个请求时,整个流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
User Request (HTTP/gRPC)

API Server Process
↓ (ZMQ Socket)
Engine Core Process

Scheduler

GPU Worker Processes
↓ (NCCL/Collective Ops)
Model Execution

Output Processing
↓ (ZMQ Socket)
API Server Process

Streaming Response to User

V1 多进程架构

V1 架构(于 2024 年底引入)采用多进程设计,以实现更好的 CPU 利用率和进程隔离。下面逐一介绍各进程类型:

1. API Server 进程

职责:处理前端请求、I/O,以及与 engine core 的通信

主要任务

  • 接收并校验 OpenAI-compatible HTTP 请求
  • 对输入文本进行 tokenize
  • 加载多模态数据(图像、音频)
  • 将输出以流式方式返回给客户端
  • 处理 API 鉴权及其他前端请求逻辑

核心实现:OpenAI-compatible HTTP 服务位于 vllm/entrypoints/openai/api_server.py;gRPC 服务位于 vllm/entrypoints/grpc_server.py

API server 对于模型执行而言是无状态的。它不了解 GPU 内存、KV cache 或模型权重,只负责:

  1. 将用户请求转换为 EngineCoreRequest 对象
  2. 通过 ZMQ 将其发送给 engine core
  3. 接收返回的 EngineCoreOutput 对象
  4. 将其转换为 API 响应

进程数量:默认单 API server;在在线 data parallel 部署中,api_server_count 的默认值会随 internal / hybrid / external load-balancing 模式变化,并可由 --api-server-count 覆盖。

CPU 线程:使用 VLLM_MEDIA_LOADING_THREAD_COUNT 个线程(默认 8)并行加载媒体文件。

2. Engine Core 进程

职责:调度请求并协调模型执行

主要任务

  • 维护请求队列
  • 运行 scheduler 以决定计算内容
  • 管理 KV cache 分配
  • 协调 GPU worker
  • 处理请求抢占与换出

核心实现vllm/v1/engine/core.py

engine core 运行一个紧凑的循环(run_busy_loopcore.py:1138):

1
2
3
4
5
6
7
8
9
10
11
12
13
def run_busy_loop(self):
while self._handle_shutdown():
# 1. 从 input_queue 拉取新请求(add_request / abort_requests)
self._process_input_queue()
# 2. 调度 + 执行一次模型 step,将输出推入 output_queue
self._process_engine_step()

def _process_engine_step(self):
# step() 内部:scheduler.schedule() -> model_executor.execute_model()
outputs, model_executed = self.step_fn()
for output in outputs.items() if outputs else ():
self.output_queue.put_nowait(output)
self.post_step(model_executed)

进程数量:每个 data parallel rank 对应一个。设置 --data-parallel-size 4 时,将启动 4 个 engine core。

CPU 用量:运行忙循环以实现低延迟的调度决策。

3. GPU Worker 进程

职责:在 GPU 上执行模型前向传播

主要任务

  • 将模型权重加载到 GPU
  • 执行前向传播
  • 管理 GPU 内存
  • 运行 CUDA kernel(attention、FFN 等)
  • 参与集合通信操作(用于 tensor parallelism / pipeline parallelism)

核心实现vllm/v1/worker/gpu_worker.py

MultiprocExecutor 下,通常会看到“每个 GPU 一个 worker 进程”;但在单机单卡默认的 UniProcExecutor 路径中,worker 会运行在 EngineCore 进程内,而不是独立的 OS 进程。worker 负责:

  1. 加载其对应分片的模型权重
  2. 从对应的 engine core 接收执行请求
  3. 运行模型前向传播
  4. 将执行结果交回 executor / engine core 的后续处理路径

进程数量:取决于 executor 后端。对于 8 个 GPU、设置 --tensor-parallel-size 4 --data-parallel-size 2 这类多 GPU 部署,通常会看到 8 个 worker,由 2 个 engine core 分别协调 2 组 tensor parallel worker。

4. DP Coordinator 进程(按需启动)

职责:在 data parallel 部署中汇总引擎状态,并为前端负载均衡与 wave coordination 提供协调信息

主要任务

  • 汇总各 DP engine 的 waiting / running 队列统计
  • 将这些统计发布给 front-end client,供其做负载均衡决策
  • 维护 request wave / running state,并在需要时广播 START_DP_WAVE

核心实现vllm/v1/engine/coordinator.py

进程数量:当 --data-parallel-size > 1 时启动 1 个,否则不启动。

进程数量示例

下面来看几个具体示例。需要注意的是,实际 OS 进程数量会随 executor 后端与 load-balancing 模式变化;以下示例以当前 CLI 默认路径为参考:

示例 1:单 GPU

1
vllm serve meta-llama/Llama-3-8B

进程:

  • 1 个 API Server(当前进程)
  • 1 个 Engine Core
  • GPU worker 以内嵌方式运行在 EngineCore 进程内(UniProcExecutor
  • 共计:通常约 2 个 OS 进程

示例 2:Tensor Parallelism(4 个 GPU)

1
vllm serve meta-llama/Llama-3-70B --tensor-parallel-size 4

进程:

  • 1 个 API Server
  • 1 个 Engine Core
  • 4 个 GPU Worker(每个 GPU 各一个)
  • 共计:6 个进程

示例 3:Data Parallelism(4 个 GPU)

1
vllm serve meta-llama/Llama-3-8B --data-parallel-size 4

进程:

  • 1 个 launcher / manager
  • 4 个 API Server
  • 4 个 Engine Core(每个 DP rank 各一个)
  • 4 个 GPU Worker(每个 GPU 各一个)
  • 1 个 DP Coordinator
  • 共计:单机 internal-LB 部署下通常约 14 个进程

示例 4:混合并行(8 个 GPU)

1
vllm serve meta-llama/Llama-3-70B --tensor-parallel-size 2 --data-parallel-size 4

进程:

  • 1 个 launcher / manager
  • 4 个 API Server
  • 4 个 Engine Core(每个 DP rank 各一个)
  • 8 个 GPU Worker(每个 DP rank 2 个,每个 GPU 各一个)
  • 1 个 DP Coordinator
  • 共计:单机 internal-LB 部署下通常约 18 个进程

核心组件详解

LLMEngine

LLMEngine 类是离线推理(直接使用 Python API)的主要入口点。

位置vllm/v1/engine/llm_engine.py

主要职责:

  • 创建并管理 engine core
  • 通过 InputProcessor 处理输入
  • 通过 OutputProcessor 转换输出
  • 管理请求生命周期

使用示例

1
2
3
4
5
6
7
8
9
10
from vllm import LLM, SamplingParams

# Initialize engine
llm = LLM(model="meta-llama/Llama-3-8B")

# Generate
outputs = llm.generate(
["Hello, my name is"],
SamplingParams(temperature=0.8, top_p=0.95)
)

在底层,LLM 创建 LLMEngineLLMEngine 再创建 EngineCore,由其协调各 worker。

Scheduler

Scheduler 是 vLLM 的核心大脑,负责决策:

  • 每次迭代处理哪些请求
  • 每个请求计算多少 token
  • 何时以及对哪些请求进行抢占
  • 如何分配 KV cache 块

位置vllm/v1/core/sched/scheduler.py

Scheduler 维护三个队列:

  1. 等待队列(Waiting):等待开始处理的新请求
  2. 运行队列(Running):正在被处理的请求
  3. 暂跳队列(Skipped Waiting):因依赖关系而被临时跳过的请求

调度算法(简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def schedule(self) -> SchedulerOutput:
scheduled_requests = []
token_budget = max_num_scheduled_tokens

# First, schedule running requests (priority)
for req in running:
if token_budget > 0:
num_tokens = min(req.remaining_tokens, token_budget)
scheduled_requests.append((req, num_tokens))
token_budget -= num_tokens

# Then, schedule waiting requests
for req in waiting:
if token_budget >= req.num_prompt_tokens:
scheduled_requests.append((req, req.num_prompt_tokens))
token_budget -= req.num_prompt_tokens
else:
break # Not enough budget

return SchedulerOutput(scheduled_requests)

我们将在第三部分深入探讨 scheduler。

KV Cache Manager

使用 PagedAttention 管理 attention key-value cache 的内存。

位置vllm/v1/core/kv_cache_manager.py

核心概念:

  • Block(块):固定大小的 KV cache 单元(例如 16 个 token)
  • Block Pool(块池):预分配的块集合
  • Block Table(块表):逻辑位置到物理块的映射
  • Prefix Cache(前缀缓存):用于共享公共 prompt 前缀的块

示例:若块大小为 16,共有 1000 个块,则可以服务于:

  • 1 个拥有 16,000 个 token 的请求,或
  • 16 个各有 1,000 个 token 的请求,或
  • 任何能放入 1000 个块的组合

我们将在第二部分深入剖析 PagedAttention。

Worker 与后端 Runner

worker 进程负责加载模型并执行前向传播。

Workervllm/v1/worker/gpu_worker.py):

  • 管理 GPU 设备
  • 初始化模型权重
  • 与其他 worker 协调(用于 tensor parallelism / pipeline parallelism)

具体后端的 model runner(例如 GPUModelRunner / CPUModelRunner / XPUModelRunner,以及较新的 GPU runner 路径):

  • 准备输入 tensor
  • 执行模型前向传播
  • 应用 CUDA graph 优化
  • 产出供 executor / engine core 后续处理的执行结果

请求生命周期

让我们跟踪一个完整请求在系统中的流转过程:

1. 请求到达

1
2
3
4
5
6
POST /v1/completions
{
"model": "meta-llama/Llama-3-8B",
"prompt": "The capital of France is",
"max_tokens": 50
}

2. API Server 处理

  • 校验请求
  • 对 prompt 进行 tokenize:[791, 3139, 315, 9822, 374]
  • 创建 EngineCoreRequest 对象
  • 通过 ZMQ 发送至 engine core

3. Engine Core 接收

  • 将请求加入 scheduler 的等待队列
  • 请求 ID 通常已在前端 InputProcessor 阶段分配/随机化,并随 EngineCoreRequest 一起传入
  • 初始化请求状态

4. 首次调度迭代

  • Scheduler 发现等待队列中的新请求
  • 检查 prefix cache / KV cache 状态
  • 为本轮需要计算的 token 分配 KV cache 块(块大小为 16 token 时,这个例子中通常只需要 1 个块)
  • 生成本轮 SchedulerOutput

5. Worker 执行

  • executor / worker 接收调度结果
  • 准备输入 tensor
  • 运行模型前向传播
  • 若需要采样,则通过 model_executor.sample_tokens(...) 生成本轮 token 输出

6. 输出处理

  • engine core 使用 scheduler.update_from_output(...) 更新请求状态
  • EngineCoreOutput 通过 ZMQ 发送给 API server

7. API Server 流式输出

  • 从 engine core 接收 token
  • Detokenize:”Paris”
  • 流式返回给客户端:data: {"text": "Paris", "finish_reason": null}

8. 后续迭代

  • 请求移入运行队列
  • Scheduler 持续分配 token
  • 每次迭代:计算 1 个新 token(decode 阶段)
  • 将每个 token 流式发送给客户端

9. 请求完成

  • 达到 max_tokens 上限或生成 EOS
  • 释放 KV cache 块(或保留在 prefix cache 中)
  • 向客户端发送最终完成结果
  • 从 scheduler 中移除请求

配置与初始化

vLLM 的配置统一集中在 VllmConfig 中:

1
2
3
4
5
6
7
8
9
from vllm.config import VllmConfig

vllm_config = VllmConfig(
model_config=ModelConfig(...),
cache_config=CacheConfig(...),
scheduler_config=SchedulerConfig(...),
parallel_config=ParallelConfig(...),
# ... more configs
)

许多核心组件会共享或持有同一个 VllmConfig,以确保配置一致性。

主要配置项

  • ModelConfig:模型名称、dtype、tokenizer
  • CacheConfig:KV cache 大小、块大小、prefix caching
  • SchedulerConfig:最大批量大小、调度策略
  • ParallelConfig:TP/PP/DP 规模、分布式后端

类层次结构

类层次结构遵循一致的模式:

1
2
3
4
5
6
7
8
9
10
11
12
LLMEngine
├── InputProcessor (tokenization, preprocessing)
├── EngineCore
│ ├── StructuredOutputManager
│ ├── Scheduler
│ │ ├── KVCacheManager
│ │ └── EncoderCacheManager
│ └── Executor
│ └── Workers / backend runners
│ └── GPUModelRunner / CPUModelRunner / XPUModelRunner
│ └── Model (nn.Module)
└── OutputProcessor (detokenization, streaming)

许多核心类会接收或持有 VllmConfig,但并非所有类都直接以它作为构造参数;例如 OutputProcessor 并不直接接收 VllmConfig

进程间通信

ZMQ Sockets

API server 与 engine core 通过 ZMQ 进行通信,消息编码采用基于 MsgpackEncoder / MsgpackDecoder 的 multipart frame:

  • front-end / API server 一侧会把 EngineCoreRequest 编码成 multipart 消息并通过 send_multipart(...) 发送
  • engine core 一侧通过 recv_multipart(...) 接收,再按消息类型用 MsgpackDecoder 解码
  • 在需要传输 tensor 的路径上,还会配合 tensor IPC,而不是把所有内容都塞进单个 pickle payload

为什么选择 ZMQ?

  • 高性能(微秒级延迟)
  • 灵活的通信模式(req-rep、pub-sub、push-pull)
  • 内置队列管理
  • 语言无关

NCCL 用于 GPU 通信

GPU worker 使用 NCCL 进行集合通信操作:

1
2
3
4
5
6
# Tensor parallelism: all-reduce across GPUs
torch.distributed.all_reduce(
tensor,
op=torch.distributed.ReduceOp.SUM,
group=tensor_parallel_group
)

通信模式

  • Tensor Parallel:在 attention/FFN 之后执行 All-reduce
  • Pipeline Parallel:各阶段之间执行发送/接收操作
  • Data Parallel:前向传播期间无需通信

内存布局

理解内存布局对于 vLLM 至关重要:

GPU 内存分布

1
2
3
4
5
Total GPU Memory: 80GB (H100)
├── Model Weights: 16GB (Llama-3-8B in FP16)
├── KV Cache: 60GB (dynamically allocated blocks)
├── Activation Memory: 2GB (for batch processing)
└── Framework Overhead: 2GB (PyTorch, CUDA)

KV Cache 内存

对于块大小为 16 token 的 8B 模型:

  • 每个块存储 32 个 attention 层的 K 和 V
  • 每块大小:16 token × 32 层 × 2(K,V)× 4096 维 × 2 字节 = 8.4 MB
  • 若 KV cache 分配 60GB:约 7,100 个块
  • 总容量:跨所有请求约 113,600 个 token

性能特征

H100 GPU 上的典型性能表现:

指标 数值
吞吐量(Throughput) ~8,000 tokens/sec(Llama-3-8B)
延迟(TTFT) ~20-50ms
延迟(TPOT) ~10-15ms
最大批量大小(Max Batch Size) ~256 并发请求
内存效率(Memory Efficiency) ~95%(相比不使用 PagedAttention 时的 ~60%)

下一步

现在我们已经了解了整体架构,接下来可以深入探讨各具体组件:

  • 第二部分:PagedAttention 与 KV cache 管理
  • 第三部分:Scheduler 的决策过程
  • 第四部分:请求处理与状态管理
  • 第五部分:分布式执行与并行策略

关键要点

  1. V1 采用多进程架构,以提升 CPU 利用率和故障隔离能力
  2. Scheduler 是核心协调者,负责决策每次迭代的计算内容
  3. KV cache 管理(PagedAttention)是内存效率的关键所在
  4. 进程数量随并行度扩展:但会受到 executor 后端、load-balancing 模式以及是否存在 launcher / manager 进程的影响
  5. ZMQ 实现了高效的进程间通信
  6. 许多核心组件共享或持有统一的 VllmConfig,以保持配置一致性

参考资料


在下一篇文章中,我们将详细探讨 PagedAttention,了解 vLLM 如何通过精巧的内存管理实现近乎零浪费的内存利用。