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 进程

职责:处理 HTTP/gRPC 请求及 I/O 操作

主要任务

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

核心实现vllm/entrypoints/openai/api_server.py

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

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

进程数量:默认为 1 个 API server。启用 data parallelism(--data-parallel-size N)后,自动扩展至 N 个 API server。

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

每个 GPU 都有其专属的 worker 进程。worker 负责:

  1. 加载其对应分片的模型权重
  2. 从对应的 engine core 接收执行请求
  3. 运行模型前向传播
  4. 返回输出结果(logits、KV cache 更新)

进程数量:每个 GPU 一个。对于 8 个 GPU,设置 --tensor-parallel-size 4 --data-parallel-size 2 时:

  • 共 8 个 GPU worker
  • 分为 2 组,每组 4 个(tensor parallel 分组)
  • 2 个 engine core(每个 data parallel rank 各一个)

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

职责:协调 data parallel 引擎

主要任务

  • 在 data parallel rank 之间进行负载均衡
  • 同步 MoE 模型执行
  • 将请求路由至负载最低的引擎

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

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

进程数量示例

下面来看几个具体示例:

示例 1:单 GPU

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

进程:

  • 1 个 API Server
  • 1 个 Engine Core
  • 1 个 GPU Worker
  • 共计:3 个进程

示例 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

进程:

  • 4 个 API Server(随 data parallelism 扩展)
  • 4 个 Engine Core(每个 DP rank 各一个)
  • 4 个 GPU Worker(每个 GPU 各一个)
  • 1 个 DP Coordinator
  • 共计:13 个进程

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

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

进程:

  • 4 个 API Server
  • 4 个 Engine Core(每个 DP rank 各一个)
  • 8 个 GPU Worker(每个 DP rank 2 个,每个 GPU 各一个)
  • 1 个 DP Coordinator
  • 共计:17 个进程

核心组件详解

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 与 ModelRunner

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

Workervllm/v1/worker/gpu_worker.py):

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

ModelRunner(因后端而异):

  • 准备输入 tensor
  • 执行模型前向传播
  • 应用 CUDA graph 优化
  • 返回输出 logits

请求生命周期

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

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:"req_abc123"
  • 初始化请求状态

4. 首次调度迭代

  • Scheduler 发现等待队列中的新请求
  • 检查 KV cache:5 个 token 需要计算
  • 分配 KV cache 块(块大小为 16 token 时,分配 1 个块)
  • 调度 prefill:计算全部 5 个 token

5. Worker 执行

  • 从 engine core 接收调度指令
  • 准备输入 tensor
  • 运行模型前向传播
  • 生成第一个 token(例如,token ID 330 = “Paris”)
  • 将 logits 返回给 engine core

6. 输出处理

  • Engine core 接收 logits
  • 采样下一个 token:330(”Paris”)
  • 更新请求状态:将 token 追加到输出中
  • 通过 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
│ ├── Scheduler
│ │ ├── KVCacheManager
│ │ ├── EncoderCacheManager
│ │ └── StructuredOutputManager
│ └── Executor
│ └── Workers (one per GPU)
│ └── ModelRunner
│ └── Model (nn.Module)
└── OutputProcessor (detokenization, streaming)

每个类在其构造函数中均接受 VllmConfig,从而可以访问所有配置选项。

进程间通信

ZMQ Sockets

API server 与 engine core 通过 ZMQ 进行通信:

1
2
3
4
5
# API Server side
zmq_socket.send(pickle.dumps(request))

# Engine Core side
request = pickle.loads(zmq_socket.recv())

为什么选择 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. 进程数量随并行度扩展:通常为 API server 数 + Engine core 数 + GPU worker 数 +(可选的 DP Coordinator)
  5. ZMQ 实现了高效的进程间通信
  6. 所有组件共享统一的 VllmConfig,确保一致性

参考资料


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