本部分将帮助你理解大语言模型推理面临的挑战,以及 vLLM 如何通过创新的技术解决这些问题。
1 - 为什么需要 vLLM
为什么需要 vLLM
本章将帮助你理解:为什么传统方案无法满足 LLM 推理需求,以及 vLLM 是如何解决这些问题的。
引言:LLM 时代的推理挑战
2022 年底,ChatGPT 横空出世,大语言模型(Large Language Model,LLM)迅速成为人工智能领域最热门的话题。随后,GPT-4、Claude、LLaMA、Qwen 等一系列强大的语言模型相继发布,LLM 的能力不断突破我们的想象。
然而,当我们尝试将这些强大的模型部署到生产环境中时,一个严峻的问题摆在了面前:
如何让 LLM 高效地服务大量用户?
这个问题看似简单,实则涉及到计算机系统的方方面面——显存管理、并行计算、任务调度、网络通信等。而 vLLM 正是为了解决这个问题而诞生的。
在深入了解 vLLM 之前,让我们先看看传统方案面临的困境。
1. 传统方案面临的三大困境
1.1 困境一:显存被严重浪费
让我们用一个具体的例子来说明这个问题。
假设你有一块 NVIDIA A100 80GB 显卡,想要部署 LLaMA-2-7B 模型来服务用户。首先,模型权重本身就需要约 14GB 显存(FP16 精度)。剩下的 66GB 显存可以用来处理用户请求。
听起来还不错?但问题来了。
在 LLM 推理过程中,有一个叫做 KV Cache(键值缓存)的东西,它会随着生成的文本长度不断增长。对于每个用户请求,KV Cache 的大小计算公式是:
KV Cache 大小 = 2 × 层数 × 隐藏维度 × 序列长度 × 精度字节数
对于 LLaMA-2-7B(32 层,隐藏维度 4096,FP16 精度):
- 最大序列长度 4096 tokens 时,单个请求的 KV Cache 约需 2GB 显存
这意味着理论上你最多只能同时服务 33 个用户(66GB ÷ 2GB)。
但实际情况更糟糕!
传统方案采用预分配策略:在请求开始时,就为其分配最大可能长度的 KV Cache 空间。即使用户只问了一句"你好"(可能只生成 10 个 token),系统也会预留 4096 个 token 的空间。
这导致了严重的显存碎片化问题:
graph TB
subgraph 传统方案的显存分配
direction TB
M1[请求 A<br/>预分配 2GB<br/>实际使用 100MB]
M2[请求 B<br/>预分配 2GB<br/>实际使用 500MB]
M3[请求 C<br/>预分配 2GB<br/>实际使用 200MB]
M4[碎片空间<br/>无法利用]
M5[空闲空间<br/>不足 2GB<br/>无法接受新请求]
end
style M1 fill:#ffcdd2
style M2 fill:#ffcdd2
style M3 fill:#ffcdd2
style M4 fill:#fff9c4
style M5 fill:#e0e0e0问题的本质:
- 内部碎片:预分配的空间大部分没有被使用
- 外部碎片:剩余的小块空间无法满足新请求的预分配需求
- 浪费比例:研究表明,传统方案的显存浪费率高达 60-80%!
1.2 困境二:静态批处理效率低下
在机器学习中,批处理(Batching) 是提高 GPU 利用率的关键技术。简单来说,就是把多个请求打包在一起,让 GPU 同时处理。
然而,传统的静态批处理在 LLM 推理中面临严重问题。
静态批处理的工作方式:
flowchart LR
subgraph 静态批处理
direction TB
R1[请求 1<br/>输出 10 tokens]
R2[请求 2<br/>输出 50 tokens]
R3[请求 3<br/>输出 100 tokens]
end
subgraph 处理过程
direction TB
S1[Step 1] --> S2[Step 2] --> S3[...] --> S100[Step 100]
end
subgraph 问题
P1[请求 1 在 Step 10 完成<br/>但必须等到 Step 100]
P2[请求 2 在 Step 50 完成<br/>但必须等到 Step 100]
P3[GPU 在等待时空转]
end
R1 --> S1
R2 --> S1
R3 --> S1
S100 --> P1
S100 --> P2
S100 --> P3
style P1 fill:#ffcdd2
style P2 fill:#ffcdd2
style P3 fill:#ffcdd2问题分析:
- 必须等待最长序列:一个批次中,所有请求必须等待最长的那个完成才能返回结果
- 无法动态调整:批次一旦开始,就不能添加新请求或移除已完成的请求
- GPU 利用率波动:随着请求陆续完成,实际在处理的请求越来越少,GPU 利用率下降
让我们用一个时间线来直观感受这个问题:
gantt
title 静态批处理时间线
dateFormat X
axisFormat %s
section 请求 1
实际工作 (10 tokens) :done, r1, 0, 10
空等待 :crit, r1wait, 10, 100
section 请求 2
实际工作 (50 tokens) :done, r2, 0, 50
空等待 :crit, r2wait, 50, 100
section 请求 3
实际工作 (100 tokens) :done, r3, 0, 100从图中可以看到:
- 请求 1 完成后白白等待了 90% 的时间
- 请求 2 完成后白白等待了 50% 的时间
- 整个批次的平均响应时间被拉长到最长请求的水平
1.3 困境三:GPU 利用率低
LLM 推理分为两个阶段:Prefill(预填充) 和 Decode(解码)。这两个阶段有着截然不同的计算特性:
| 特性 | Prefill 阶段 | Decode 阶段 |
|---|---|---|
| 处理内容 | 处理整个输入提示 | 逐个生成新 token |
| 计算密度 | 高(计算密集型) | 低(内存密集型) |
| GPU 利用率 | 高 | 低 |
| 瓶颈 | 计算能力 | 内存带宽 |
graph LR
subgraph Prefill 阶段
P1[输入: 'Hello, how are you?']
P2[并行处理所有 tokens]
P3[初始化 KV Cache]
P4[生成第一个 token]
P1 --> P2 --> P3 --> P4
end
subgraph Decode 阶段
D1[读取 KV Cache]
D2[处理 1 个 token]
D3[更新 KV Cache]
D4[生成下一个 token]
D1 --> D2 --> D3 --> D4
D4 -.->|循环| D1
end
P4 --> D1
style P2 fill:#c8e6c9
style D2 fill:#ffcdd2Decode 阶段 GPU 利用率低的原因:
- 计算量小:每次只处理 1 个新 token,计算量很小
- 内存访问多:需要读取整个 KV Cache,内存访问量大
- 计算/访存比低:GPU 大部分时间在等待数据从显存传输到计算单元
实际测量表明,在 Decode 阶段,GPU 的计算单元利用率可能只有 10-30%!这意味着昂贵的 GPU 大部分时间都在"摸鱼"。
2. vLLM 的解决方案
面对上述三大困境,vLLM 提出了两项核心创新:PagedAttention 和 Continuous Batching。
2.1 PagedAttention:像操作系统一样管理显存
vLLM 的核心创新是 PagedAttention(分页注意力),其灵感来自操作系统的虚拟内存管理。
操作系统的虚拟内存是如何工作的?
在操作系统中,物理内存被划分为固定大小的"页"(Page),程序使用的是"虚拟地址",通过"页表"映射到物理内存。这样做的好处是:
- 程序不需要连续的物理内存
- 内存可以按需分配,不用一次性预留
- 多个程序可以共享相同的物理页
vLLM 将这个思想应用到 KV Cache 管理:
graph TB
subgraph 逻辑视图(每个请求看到的)
L1[请求 A 的 KV Cache]
L2[请求 B 的 KV Cache]
L3[请求 C 的 KV Cache]
end
subgraph Block Table(页表)
BT[逻辑块 → 物理块<br/>映射关系]
end
subgraph 物理显存(实际存储)
P1[Block 0]
P2[Block 1]
P3[Block 2]
P4[Block 3]
P5[Block 4]
P6[Block 5]
P7[空闲]
P8[空闲]
end
L1 --> BT
L2 --> BT
L3 --> BT
BT --> P1
BT --> P2
BT --> P3
BT --> P4
BT --> P5
BT --> P6
style P7 fill:#e8f5e9
style P8 fill:#e8f5e9PagedAttention 的工作方式:
- 块(Block):将 KV Cache 划分为固定大小的块,每个块存储固定数量 token 的 KV 数据
- 按需分配:请求开始时不预分配空间,生成新 token 时才分配新的块
- 非连续存储:一个请求的 KV Cache 可以分散在不连续的物理块中
- 块表映射:通过块表(Block Table)记录逻辑块到物理块的映射关系
好处:
graph LR
subgraph 传统方案
T1[预分配 4096 tokens]
T2[实际使用 100 tokens]
T3[浪费 97.5%]
T1 --> T2 --> T3
end
subgraph PagedAttention
P1[按需分配]
P2[用多少分配多少]
P3[浪费 < 4%]
P1 --> P2 --> P3
end
style T3 fill:#ffcdd2
style P3 fill:#c8e6c9- 消除内部碎片:只分配实际需要的块,不预留空间
- 减少外部碎片:固定大小的块可以灵活复用
- 支持内存共享:相同前缀的请求可以共享相同的物理块(后续章节详细介绍)
2.2 Continuous Batching:连续批处理
vLLM 的第二项创新是 Continuous Batching(连续批处理),也叫做 Iteration-Level Scheduling(迭代级调度)。
与静态批处理的对比:
flowchart TB
subgraph 静态批处理
direction LR
S1[批次开始] --> S2[所有请求一起处理] --> S3[等待最长完成] --> S4[批次结束]
S5[新请求等待下一批]
end
subgraph 连续批处理
direction LR
C1[每个 step 重新调度]
C2[完成的请求立即退出]
C3[新请求立即加入]
C4[始终保持高并发]
C1 --> C2
C2 --> C3
C3 --> C4
C4 --> C1
end
style S3 fill:#ffcdd2
style C4 fill:#c8e6c9连续批处理的工作原理:
- 迭代级调度:每生成一个 token(一个迭代),就重新进行调度决策
- 动态加入:新到达的请求可以立即加入当前批次
- 动态退出:已完成的请求立即释放资源,不需要等待其他请求
- 资源复用:退出请求释放的资源立即分配给新请求
让我们用时间线对比一下:
gantt
title 连续批处理时间线
dateFormat X
axisFormat %s
section 请求 1
处理完成 (10 tokens) :done, r1, 0, 10
section 请求 2
处理完成 (50 tokens) :done, r2, 0, 50
section 请求 3
处理完成 (100 tokens) :done, r3, 0, 100
section 请求 4(新加入)
等待 :r4wait, 0, 10
处理完成 (40 tokens) :done, r4, 10, 50
section 请求 5(新加入)
等待 :r5wait, 0, 50
处理完成 (30 tokens) :done, r5, 50, 80对比静态批处理,连续批处理的优势:
- 请求 1 在 step 10 完成后立即返回,不需要等待
- 请求 4 在 step 10 时立即加入,利用请求 1 释放的资源
- 系统始终保持高并发,GPU 利用率更高
3. vLLM 的性能优势
3.1 吞吐量对比
根据 vLLM 官方的 benchmark 测试,在 A100 GPU 上使用 LLaMA-13B 模型:
| 框架 | 吞吐量 (requests/s) | 相对性能 |
|---|---|---|
| HuggingFace Transformers | 1.0x(基准) | 基准 |
| Text Generation Inference | 2.2x | +120% |
| vLLM | 14-24x | +1300-2300% |
这不是印刷错误——vLLM 的吞吐量可以达到 HuggingFace Transformers 的 14-24 倍!
3.2 显存效率对比
| 指标 | 传统方案 | vLLM |
|---|---|---|
| 显存浪费率 | 60-80% | < 4% |
| 最大并发请求数 | 低 | 高 2-4 倍 |
| 内存碎片 | 严重 | 几乎没有 |
3.3 延迟特性
| 指标 | 含义 | vLLM 表现 |
|---|---|---|
| TTFT | 首 token 延迟 | 优化(通过 Chunked Prefill) |
| TPS | 每秒生成 token 数 | 高且稳定 |
| P99 延迟 | 99% 请求的最大延迟 | 低且稳定 |
4. vLLM 与其他框架对比
4.1 vs HuggingFace Transformers
| 特性 | HuggingFace | vLLM |
|---|---|---|
| 定位 | 通用深度学习框架 | LLM 推理专用框架 |
| 易用性 | 非常简单 | 简单 |
| 吞吐量 | 低 | 高(14-24x) |
| 显存效率 | 低 | 高 |
| 适用场景 | 开发、实验 | 生产部署 |
选择建议:
- 如果你在做模型研究或小规模实验,HuggingFace 更方便
- 如果你需要部署生产服务,vLLM 是更好的选择
4.2 vs Text Generation Inference (TGI)
| 特性 | TGI | vLLM |
|---|---|---|
| 开发者 | HuggingFace | UC Berkeley |
| 核心优化 | Flash Attention | PagedAttention |
| 连续批处理 | 支持 | 支持 |
| 吞吐量 | 中等 | 高 |
| 生态集成 | HuggingFace 生态 | 独立 |
选择建议:
- 如果你深度使用 HuggingFace 生态,TGI 集成更好
- 如果追求极致性能,vLLM 通常更快
4.3 vs DeepSpeed-Inference
| 特性 | DeepSpeed | vLLM |
|---|---|---|
| 开发者 | Microsoft | UC Berkeley |
| 主要优化 | 分布式推理 | 单机吞吐量 |
| 显存管理 | 传统方式 | PagedAttention |
| 适用规模 | 超大模型 | 中大模型 |
选择建议:
- 如果模型太大,单机放不下,考虑 DeepSpeed
- 如果单机可以放下,vLLM 通常性能更好
5. vLLM 的典型应用场景
5.1 在线 API 服务
# 使用 vLLM 启动 OpenAI 兼容的 API 服务
# vllm serve meta-llama/Llama-2-7b-hf --port 8000
适用于:
- ChatGPT 类对话应用
- 代码补全服务
- 文本生成 API
5.2 离线批量处理
from vllm import LLM, SamplingParams
# 创建 LLM 实例
llm = LLM(model="meta-llama/Llama-2-7b-hf")
# 批量生成
prompts = ["Hello, my name is", "The capital of France is"]
sampling_params = SamplingParams(temperature=0.8, max_tokens=100)
outputs = llm.generate(prompts, sampling_params)
适用于:
- 大规模数据处理
- 模型评测
- 数据生成
5.3 多模型服务
vLLM 支持同时加载多个模型,适用于:
- A/B 测试
- 模型对比
- 多任务服务
6. 本章小结
在本章中,我们了解了:
传统 LLM 推理面临的三大困境:
- 显存碎片化导致资源浪费
- 静态批处理效率低下
- GPU 利用率低
vLLM 的两大核心创新:
- PagedAttention:借鉴操作系统虚拟内存,实现高效显存管理
- Continuous Batching:迭代级调度,动态添加/移除请求
vLLM 的性能优势:
- 吞吐量提升 14-24 倍
- 显存浪费率从 60-80% 降至 4% 以下
- 延迟稳定可预测
框架选择建议:
- 研究实验:HuggingFace Transformers
- 生产部署:vLLM
- 超大模型:DeepSpeed-Inference
思考题
- 为什么 PagedAttention 选择固定大小的块,而不是可变大小?
- 连续批处理相比静态批处理,有什么潜在的缺点?
- 如果你有一个 24GB 显存的 GPU,想要部署一个 7B 参数的模型服务 100 个并发用户,你会怎么做?
下一步
现在你已经了解了为什么需要 vLLM,接下来让我们深入了解 LLM 推理面临的具体挑战:
2 - LLM 推理面临的挑战
LLM 推理面临的挑战
本章将深入分析 LLM 推理在显存、计算、批处理三个维度面临的具体挑战,为后续理解 vLLM 的解决方案打下基础。
引言
上一章我们从宏观视角了解了 LLM 推理的困境。本章将进行更深入的技术分析,用具体的数字和公式来量化这些挑战。理解这些挑战对于理解 vLLM 的设计决策至关重要。
1. 显存挑战:一道简单的数学题
1.1 显存占用的组成
LLM 推理时的显存占用主要由三部分组成:
pie title LLM 推理显存占用分布(以 7B 模型为例)
"模型权重" : 45
"KV Cache" : 50
"激活值和临时变量" : 5| 组成部分 | 说明 | 占比 |
|---|---|---|
| 模型权重 | 神经网络的参数 | 约 45% |
| KV Cache | 注意力机制的缓存 | 约 50%(动态增长) |
| 激活值和临时变量 | 计算过程中的中间结果 | 约 5% |
可以看到,KV Cache 是显存占用的主要部分,而且它还会随着生成长度动态增长!
1.2 模型权重的显存占用
模型权重的显存占用计算相对简单:
模型权重显存 = 参数量 × 每个参数的字节数
不同精度下的字节数:
| 精度 | 每个参数字节数 | 说明 |
|---|---|---|
| FP32 | 4 字节 | 全精度浮点 |
| FP16/BF16 | 2 字节 | 半精度浮点(推理常用) |
| INT8 | 1 字节 | 8 位整数量化 |
| INT4 | 0.5 字节 | 4 位整数量化 |
示例计算:LLaMA-2-7B 模型
| 精度 | 计算 | 显存占用 |
|---|---|---|
| FP32 | 7B × 4 = 28GB | 28 GB |
| FP16 | 7B × 2 = 14GB | 14 GB |
| INT8 | 7B × 1 = 7GB | 7 GB |
| INT4 | 7B × 0.5 = 3.5GB | 3.5 GB |
1.3 KV Cache 的显存占用
KV Cache 是 LLM 推理中的关键数据结构,用于存储注意力计算的中间结果。它的显存占用计算公式:
KV Cache 显存 = 2 × num_layers × num_heads × head_dim × seq_len × batch_size × bytes_per_element
或者简化为:
KV Cache 显存 = 2 × num_layers × hidden_dim × seq_len × batch_size × bytes_per_element
其中:
2表示 Key 和 Value 两部分num_layers是 Transformer 层数hidden_dim是隐藏维度(= num_heads × head_dim)seq_len是序列长度(会动态增长)batch_size是批次大小(并发请求数)bytes_per_element是每个元素的字节数(FP16 = 2)
示例计算:LLaMA-2-7B 模型
| 参数 | 值 |
|---|---|
| num_layers | 32 |
| hidden_dim | 4096 |
| FP16 bytes | 2 |
单个请求,不同序列长度的 KV Cache 大小:
| 序列长度 | 计算 | KV Cache 大小 |
|---|---|---|
| 512 | 2 × 32 × 4096 × 512 × 2 | 256 MB |
| 1024 | 2 × 32 × 4096 × 1024 × 2 | 512 MB |
| 2048 | 2 × 32 × 4096 × 2048 × 2 | 1 GB |
| 4096 | 2 × 32 × 4096 × 4096 × 2 | 2 GB |
关键观察:KV Cache 与序列长度线性增长!
1.4 KV Cache 的动态增长问题
让我们用一个图来展示 KV Cache 的动态增长过程:
flowchart TD
subgraph 初始状态
A1[用户输入: 'Hello, how are you?'<br/>5 tokens]
A2[KV Cache: 5 × 每token大小]
end
subgraph 生成第1个token
B1[模型输出: 'I'<br/>新增 1 token]
B2[KV Cache: 6 × 每token大小]
end
subgraph 生成第2个token
C1[模型输出: 'am'<br/>新增 1 token]
C2[KV Cache: 7 × 每token大小]
end
subgraph 生成第N个token
D1[模型输出: '...'<br/>新增 1 token]
D2[KV Cache: (5+N) × 每token大小]
end
A1 --> A2
A2 --> B1
B1 --> B2
B2 --> C1
C1 --> C2
C2 -.->|持续增长| D1
D1 --> D2
style A2 fill:#e3f2fd
style B2 fill:#bbdefb
style C2 fill:#90caf9
style D2 fill:#64b5f6这带来了一个严重问题:在请求开始时,我们不知道最终会生成多少 token!
传统方案的处理方式:
- 预分配最大长度:浪费大量显存
- 动态扩展:可能导致显存不足或碎片化
1.5 显存碎片化详解
让我们用一个具体的场景来说明显存碎片化:
假设我们有 10GB 可用显存,最大序列长度 2048 tokens(每个请求预分配 1GB)。
场景演示:
初始状态:
+--------------------------------------------------+
| 空闲 (10GB) |
+--------------------------------------------------+
接受请求 A(预分配 1GB):
+----------+---------------------------------------+
| 请求 A | 空闲 (9GB) |
+----------+---------------------------------------+
接受请求 B(预分配 1GB):
+----------+----------+----------------------------+
| 请求 A | 请求 B | 空闲 (8GB) |
+----------+----------+----------------------------+
接受更多请求 C, D, E, F, G, H, I, J:
+----+----+----+----+----+----+----+----+----+----+
| A | B | C | D | E | F | G | H | I | J |
+----+----+----+----+----+----+----+----+----+----+
请求 A, C, E, G, I 完成并释放:
+----+----+----+----+----+----+----+----+----+----+
|空闲| B |空闲| D |空闲| F |空闲| H |空闲| J |
+----+----+----+----+----+----+----+----+----+----+
↑ ↑ ↑ ↑ ↑
1GB 1GB 1GB 1GB 1GB
问题:虽然有 5GB 空闲,但都是分散的 1GB 块!
无法接受需要 2GB 连续空间的请求!
碎片化的两种类型:
内部碎片(Internal Fragmentation):
- 预分配 1GB,实际只用 100MB
- 浪费了 900MB
外部碎片(External Fragmentation):
- 总空闲 5GB,但最大连续块只有 1GB
- 无法满足大于 1GB 的请求
1.6 实际案例:A100 80GB 能服务多少用户?
让我们计算一下在 A100 80GB GPU 上部署 LLaMA-2-7B 模型时,最多能服务多少并发用户。
条件:
- GPU 显存:80GB
- 模型:LLaMA-2-7B (FP16)
- 最大序列长度:4096 tokens
计算:
1. 模型权重:7B × 2 = 14GB
2. 系统开销(CUDA、激活值等):约 2GB
3. 可用于 KV Cache 的显存:80 - 14 - 2 = 64GB
4. 每个请求的 KV Cache(4096 tokens):约 2GB
5. 理论最大并发:64 ÷ 2 = 32 个请求
但是,如果考虑实际使用场景:
- 平均输入长度:100 tokens
- 平均输出长度:200 tokens
- 平均 KV Cache:300 tokens × (2GB/4096) ≈ 150MB
如果使用 PagedAttention:
实际并发:64GB ÷ 150MB ≈ 426 个请求
这就是 vLLM 显存效率提升的来源!
2. 计算挑战:Prefill vs Decode
2.1 两阶段的计算特性
LLM 推理分为两个截然不同的阶段:
graph LR
subgraph Prefill 阶段
P1[输入 Prompt<br/>N 个 tokens]
P2[并行计算<br/>所有 tokens 的 Attention]
P3[初始化 KV Cache]
P4[输出第一个 token]
P1 --> P2 --> P3 --> P4
end
subgraph Decode 阶段
D1[单个新 token]
D2[增量计算<br/>Attention]
D3[更新 KV Cache]
D4[输出下一个 token]
D1 --> D2 --> D3 --> D4
D4 -.->|循环直到结束| D1
end
P4 --> D1详细对比:
| 特性 | Prefill 阶段 | Decode 阶段 |
|---|---|---|
| 处理 tokens 数 | N(整个输入) | 1(单个新 token) |
| 计算量 | O(N²) 或 O(N) | O(1) 或 O(N) |
| 内存访问 | 读取模型权重 + 写入 KV Cache | 读取模型权重 + 读写 KV Cache |
| 计算密度 | 高 | 低 |
| 瓶颈 | 计算能力 | 内存带宽 |
| GPU 利用率 | 高(50-80%) | 低(10-30%) |
| 持续时间 | 短(一次性) | 长(循环多次) |
2.2 计算密度分析
计算密度(Arithmetic Intensity) 是衡量一个操作是计算密集型还是内存密集型的关键指标:
计算密度 = 浮点运算次数 / 内存访问字节数
对于 GPU 来说:
- 计算密度高:GPU 计算单元充分利用,性能好
- 计算密度低:GPU 等待数据传输,计算单元空闲
graph TB
subgraph Prefill 阶段
P1[处理 512 个 tokens]
P2[一次性计算 512×512 的 Attention 矩阵]
P3[计算密度高<br/>GPU 利用率高]
end
subgraph Decode 阶段
D1[处理 1 个 token]
D2[计算 1×513 的 Attention 向量]
D3[计算密度低<br/>GPU 利用率低]
end
style P3 fill:#c8e6c9
style D3 fill:#ffcdd2Prefill 阶段(以 512 token 输入为例):
计算量:512 × 512 × hidden_dim × num_layers = 很大
内存访问:读取模型权重 + 写入 KV Cache
计算密度:高
→ 计算密集型,GPU 利用率高
Decode 阶段:
计算量:1 × seq_len × hidden_dim × num_layers = 较小
内存访问:读取模型权重 + 读取整个 KV Cache + 写入新 KV
计算密度:低
→ 内存密集型,GPU 利用率低
2.3 Decode 阶段的瓶颈分析
让我们用具体数字来分析 Decode 阶段为什么慢:
假设条件:
- 模型:LLaMA-2-7B
- GPU:A100 80GB
- 当前序列长度:1000 tokens
每个 Decode step 需要:
- 读取模型权重:14GB(FP16)
- 读取 KV Cache:约 500MB(1000 tokens × 0.5MB/token)
- 计算 Attention 和 FFN
- 写入新的 KV 数据:约 0.5MB
A100 GPU 的能力:
- 内存带宽:2TB/s
- FP16 计算能力:312 TFLOPS
时间分析:
数据读取时间 = (14GB + 0.5GB) / 2TB/s ≈ 7.25ms
计算时间 = 实际计算量 / 312 TFLOPS ≈ 0.5ms(相对很少)
结论:大部分时间在读取数据,而不是计算!
这就是为什么 Decode 阶段是内存带宽瓶颈。
2.4 批处理如何提高效率
批处理可以提高 Decode 阶段的效率,原理是分摊模型权重的读取开销:
graph TB
subgraph 单请求处理
S1[读取模型权重 14GB]
S2[处理 1 个 token]
S3[计算效率低]
S1 --> S2 --> S3
end
subgraph 批处理 32 个请求
B1[读取模型权重 14GB]
B2[同时处理 32 个 tokens]
B3[计算效率提高 32 倍]
B1 --> B2 --> B3
end
style S3 fill:#ffcdd2
style B3 fill:#c8e6c9数学分析:
| 模式 | 模型权重读取 | 处理 tokens | 效率 |
|---|---|---|---|
| 单请求 | 14GB | 1 | 1x |
| 批处理 32 | 14GB | 32 | 32x |
| 批处理 64 | 14GB | 64 | 64x |
这就是为什么批处理对 LLM 推理如此重要!
3. 批处理挑战:动态与异构
3.1 请求的动态特性
LLM 服务面临的请求具有高度动态性:
sequenceDiagram
participant User1 as 用户 1
participant User2 as 用户 2
participant User3 as 用户 3
participant Server as 服务器
Note over Server: 时间线开始
User1->>Server: 发送请求 A(100 tokens)
Note over Server: 开始处理请求 A
User2->>Server: 发送请求 B(50 tokens)
Note over Server: 请求 B 需要等待?
Note over Server: 请求 A 完成
User3->>Server: 发送请求 C(200 tokens)
Note over Server: 请求 B, C 如何调度?
Note over Server: 请求 B 完成
Note over Server: 请求 C 完成动态性表现在:
- 到达时间不确定:用户随机发送请求
- 输入长度不确定:每个请求的 prompt 长度不同
- 输出长度不确定:生成长度取决于模型和停止条件
- 请求优先级不同:可能有 VIP 用户或紧急请求
3.2 异构性带来的挑战
即使在同一批次中,不同请求的特性也大不相同:
批次中的请求分布示例:
┌─────────┬──────────┬──────────┬─────────────┐
│ 请求 ID │ 输入长度 │ 当前输出 │ 状态 │
├─────────┼──────────┼──────────┼─────────────┤
│ A │ 10 │ 95 │ Decode 中 │
│ B │ 500 │ 0 │ Prefill 中 │
│ C │ 50 │ 200 │ 即将完成 │
│ D │ 100 │ 30 │ Decode 中 │
│ E │ 1000 │ 0 │ 等待 Prefill│
└─────────┴──────────┴──────────┴─────────────┘
异构性带来的问题:
序列长度不一致:
- 不同请求的 KV Cache 大小不同
- Attention 计算的工作量不同
状态不一致:
- 有的在 Prefill,有的在 Decode
- 计算密度差异巨大
完成时间不一致:
- 有的即将完成,有的刚开始
- 资源释放时机不确定
3.3 静态批处理的问题
传统静态批处理无法处理这种动态异构的场景:
gantt
title 静态批处理的问题
dateFormat X
axisFormat %s
section 请求 A
处理 (100 tokens) :done, a, 0, 100
等待其他请求完成 :crit, await, 100, 200
section 请求 B
处理 (200 tokens) :done, b, 0, 200
section 请求 C
等待加入批次 :crit, cwait, 0, 200
section GPU 利用
有效计算 :active, gpu1, 0, 100
部分空闲 :crit, gpu2, 100, 200问题总结:
| 问题 | 影响 |
|---|---|
| 必须等待最长请求 | 短请求延迟增加 |
| 无法动态加入 | 新请求等待时间长 |
| 资源不能复用 | 显存利用率低 |
| GPU 利用率波动 | 平均效率低 |
4. 关键性能指标详解
理解 LLM 推理的性能指标对于评估和优化系统至关重要。
4.1 吞吐量(Throughput)
定义:单位时间内处理的 token 数或请求数
Token 吞吐量 = 总生成 tokens / 总时间 (tokens/second)
请求吞吐量 = 总完成请求 / 总时间 (requests/second)
影响因素:
- GPU 计算能力
- 内存带宽
- 批处理大小
- 显存效率
4.2 延迟(Latency)
延迟有多个维度:
sequenceDiagram
participant U as 用户
participant S as 系统
U->>S: 发送请求
Note over S: 排队等待时间 (Queue Time)
S->>S: 开始处理
Note over S: Prefill 时间
S-->>U: 第一个 token
Note over U: TTFT (Time To First Token)
S-->>U: 后续 tokens...
Note over S: Decode 时间
S-->>U: 最后一个 token
Note over U: 总延迟 (End-to-End Latency)关键延迟指标:
| 指标 | 英文 | 说明 | 用户感知 |
|---|---|---|---|
| TTFT | Time To First Token | 首个 token 生成时间 | 响应速度感 |
| TPOT | Time Per Output Token | 每个输出 token 的时间 | 流畅度感 |
| 总延迟 | End-to-End Latency | 完整响应时间 | 等待时间 |
4.3 TPS(Tokens Per Second)
定义:每秒生成的 token 数
有两种计算方式:
- 单请求 TPS:单个请求的生成速度
- 系统 TPS:整个系统的总吞吐量
单请求 TPS = 输出 tokens / Decode 时间
系统 TPS = 所有请求的输出 tokens / 总时间
4.4 指标之间的权衡
这些指标之间存在权衡关系:
graph LR
subgraph 权衡关系
A[增加批处理大小]
B[提高吞吐量]
C[增加单请求延迟]
D[增加 TTFT]
A --> B
A --> C
A --> D
end
subgraph 优化目标
E[高吞吐量<br/>服务更多用户]
F[低延迟<br/>更好体验]
end
B --> E
C --> F
D --> F
style E fill:#c8e6c9
style F fill:#c8e6c9权衡策略:
| 场景 | 优先指标 | 策略 |
|---|---|---|
| 实时对话 | 低 TTFT,低 TPOT | 小批次,快速响应 |
| 批量处理 | 高吞吐量 | 大批次,最大化效率 |
| 混合场景 | 平衡 | 动态调整批次大小 |
5. 挑战总结
让我们用一张图总结 LLM 推理面临的所有挑战:
graph TB
subgraph 显存挑战
M1[KV Cache 动态增长]
M2[显存碎片化]
M3[预分配浪费]
M1 --> M2
M2 --> M3
end
subgraph 计算挑战
C1[Prefill 计算密集]
C2[Decode 内存密集]
C3[GPU 利用率波动]
C1 --> C3
C2 --> C3
end
subgraph 批处理挑战
B1[请求动态到达]
B2[长度异构]
B3[静态批处理低效]
B1 --> B3
B2 --> B3
end
subgraph vLLM 解决方案
V1[PagedAttention]
V2[Continuous Batching]
V3[高效调度器]
end
M3 --> V1
C3 --> V2
B3 --> V2
V1 --> V3
V2 --> V3
style V1 fill:#c8e6c9
style V2 fill:#c8e6c9
style V3 fill:#c8e6c96. 本章小结
本章我们深入分析了 LLM 推理面临的三大挑战:
显存挑战
- KV Cache 显存占用:与序列长度线性相关,可占总显存 50% 以上
- 显存碎片化:预分配导致内部碎片,动态释放导致外部碎片
- 量化影响:模型参数 7B → 14GB (FP16) → 7GB (INT8) → 3.5GB (INT4)
计算挑战
- Prefill vs Decode:计算密集型 vs 内存密集型
- GPU 利用率:Prefill 高 (50-80%),Decode 低 (10-30%)
- 瓶颈转换:Prefill 瓶颈在计算,Decode 瓶颈在内存带宽
批处理挑战
- 动态性:请求随机到达,长度不确定
- 异构性:同一批次内请求状态不同
- 静态批处理的问题:必须等待最长请求,无法动态调整
关键性能指标
- TTFT:首 token 延迟,影响用户体验
- TPS:生成速度,影响流畅度
- 吞吐量:系统容量,影响服务规模
思考题
- 对于一个 70B 参数的模型,在 A100 80GB GPU 上最多能支持多长的序列?
- 为什么 Decode 阶段不能简单地通过增加 GPU 计算核心来加速?
- 如果用户主要发送很短的请求(< 50 tokens),显存碎片化问题会更严重还是更轻?
下一步
了解了 LLM 推理的挑战后,让我们来看看 vLLM 的整体架构是如何设计来应对这些挑战的:
3 - vLLM 整体架构概览
vLLM 整体架构概览
本章将带你了解 vLLM 的整体架构设计,包括核心组件、数据流程和代码目录结构。
引言
经过前两章的学习,我们已经了解了 LLM 推理面临的挑战以及 vLLM 的核心创新理念。本章将从系统架构的角度,全面介绍 vLLM 的设计。
理解架构是深入学习的基础。当你后续阅读代码或调试问题时,这张"地图"将帮助你快速定位。
1. 系统架构全景图
1.1 高层架构
vLLM 采用分层架构设计,从上到下分为四层:
graph TD
subgraph 用户接口层
A1[Python API<br/>LLM 类]
A2[CLI<br/>vllm serve]
A3[OpenAI API<br/>HTTP Server]
A4[gRPC Server]
end
subgraph 引擎层
B1[LLMEngine<br/>同步引擎]
B2[AsyncLLM<br/>异步引擎]
B3[InputProcessor<br/>输入处理]
B4[OutputProcessor<br/>输出处理]
end
subgraph 核心层
C1[EngineCore<br/>核心逻辑]
C2[Scheduler<br/>调度器]
C3[KVCacheManager<br/>缓存管理]
C4[BlockPool<br/>内存块池]
end
subgraph 执行层
D1[ModelExecutor<br/>执行器]
D2[GPUModelRunner<br/>模型运行器]
D3[Worker<br/>工作进程]
D4[Attention Backend<br/>注意力后端]
end
A1 --> B1
A2 --> B2
A3 --> B2
A4 --> B2
B1 --> B3
B1 --> B4
B2 --> B3
B2 --> B4
B3 --> C1
B4 --> C1
C1 --> C2
C1 --> D1
C2 --> C3
C3 --> C4
D1 --> D2
D2 --> D3
D3 --> D4
style A1 fill:#e3f2fd
style A2 fill:#e3f2fd
style A3 fill:#e3f2fd
style B1 fill:#fff3e0
style B2 fill:#fff3e0
style C1 fill:#e8f5e9
style C2 fill:#e8f5e9
style D1 fill:#fce4ec
style D2 fill:#fce4ec各层职责:
| 层级 | 职责 | 关键组件 |
|---|---|---|
| 用户接口层 | 提供多种访问方式 | LLM、CLI、OpenAI API |
| 引擎层 | 协调输入输出处理 | LLMEngine、AsyncLLM |
| 核心层 | 调度与内存管理 | Scheduler、KVCacheManager |
| 执行层 | 模型计算与采样 | ModelExecutor、ModelRunner |
1.2 组件交互关系
让我们用一个更详细的流程图展示组件之间的交互:
flowchart TB
subgraph 用户请求
U[用户] -->|generate/chat| API[API 入口]
end
subgraph 引擎处理
API --> IP[InputProcessor<br/>Tokenization<br/>Prompt 处理]
IP --> EC[EngineCore<br/>核心逻辑]
EC --> OP[OutputProcessor<br/>Detokenization<br/>结果封装]
OP --> U
end
subgraph 核心调度
EC <--> SCH[Scheduler<br/>请求调度<br/>资源分配]
SCH <--> KVM[KVCacheManager<br/>缓存分配<br/>前缀缓存]
KVM <--> BP[BlockPool<br/>块管理<br/>LRU 驱逐]
end
subgraph 模型执行
EC <--> EX[ModelExecutor<br/>执行协调]
EX --> MR[GPUModelRunner<br/>输入准备<br/>模型前向]
MR --> W[Worker<br/>GPU 计算]
W --> ATT[Attention<br/>PagedAttention]
W --> SAM[Sampler<br/>Token 采样]
end
style EC fill:#c8e6c9
style SCH fill:#bbdefb
style KVM fill:#bbdefb2. 核心组件详解
2.1 用户接口层
vLLM 提供多种使用方式,满足不同场景需求。
LLM 类(Python API)
文件位置:vllm/entrypoints/llm.py
这是最直接的使用方式,适合批量处理场景:
from vllm import LLM, SamplingParams
# 创建 LLM 实例
llm = LLM(model="meta-llama/Llama-2-7b-hf")
# 定义采样参数
sampling_params = SamplingParams(
temperature=0.8,
top_p=0.95,
max_tokens=100
)
# 批量生成
prompts = ["Hello, my name is", "The capital of France is"]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
print(output.outputs[0].text)
CLI 命令
文件位置:vllm/entrypoints/cli/main.py
适合快速启动服务:
# 启动 OpenAI 兼容的 API 服务
vllm serve meta-llama/Llama-2-7b-hf --port 8000
# 运行 benchmark
vllm bench --model meta-llama/Llama-2-7b-hf
OpenAI 兼容 API
文件位置:vllm/entrypoints/openai/
提供与 OpenAI API 兼容的 HTTP 接口:
import openai
client = openai.OpenAI(
base_url="http://localhost:8000/v1",
api_key="token-abc123" # vLLM 不验证 API key
)
response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-hf",
messages=[{"role": "user", "content": "Hello!"}]
)
2.2 引擎层
LLMEngine
文件位置:vllm/v1/engine/llm_engine.py
LLMEngine 是同步模式的核心协调器:
classDiagram
class LLMEngine {
+vllm_config: VllmConfig
+input_processor: InputProcessor
+output_processor: OutputProcessor
+engine_core: EngineCoreClient
+add_request(request_id, prompt, params)
+step() EngineCoreOutputs
+get_output() List~RequestOutput~
}
class InputProcessor {
+tokenizer: Tokenizer
+process_inputs(prompt) ProcessedInputs
}
class OutputProcessor {
+detokenizer: Detokenizer
+process_outputs(outputs) List~RequestOutput~
}
LLMEngine --> InputProcessor
LLMEngine --> OutputProcessor
LLMEngine --> EngineCoreClient核心职责:
- 接收用户请求,通过 InputProcessor 处理
- 将请求发送给 EngineCore 执行
- 通过 OutputProcessor 处理输出结果
AsyncLLM
文件位置:vllm/v1/engine/async_llm.py
AsyncLLM 是异步模式的引擎,支持流式输出和高并发:
# AsyncLLM 的典型使用场景
async for output in engine.generate(prompt, params):
# 流式输出每个 token
print(output.outputs[0].text, end="", flush=True)
2.3 核心层
EngineCore
文件位置:vllm/v1/engine/core.py
EngineCore 是整个系统的"大脑",包含核心的调度和执行逻辑:
classDiagram
class EngineCore {
+scheduler: Scheduler
+model_executor: GPUExecutor
+kv_cache_config: KVCacheConfig
+step() EngineCoreOutputs
+add_request(request: Request)
+abort_requests(request_ids)
}
class Scheduler {
+waiting: RequestQueue
+running: List~Request~
+kv_cache_manager: KVCacheManager
+schedule() SchedulerOutput
+update_from_output(output)
}
class GPUExecutor {
+model_runner: GPUModelRunner
+execute_model(scheduler_output)
+sample_tokens(logits)
}
EngineCore --> Scheduler
EngineCore --> GPUExecutorEngineCore.step() 方法是核心循环:
flowchart TD
A[开始 step] --> B[Scheduler.schedule<br/>决定哪些请求执行]
B --> C{有请求需要执行?}
C -->|否| D[返回空输出]
C -->|是| E[ModelExecutor.execute_model<br/>执行前向传播]
E --> F[获取 logits]
F --> G[Scheduler.get_grammar_bitmask<br/>获取语法约束]
G --> H[ModelExecutor.sample_tokens<br/>采样生成 token]
H --> I[Scheduler.update_from_output<br/>更新请求状态]
I --> J[检查完成条件]
J --> K[构建 EngineCoreOutputs]
K --> L[返回输出]
style B fill:#bbdefb
style E fill:#c8e6c9
style H fill:#fff9c4Scheduler(调度器)
文件位置:vllm/v1/core/sched/scheduler.py
Scheduler 负责决定每个 step 执行哪些请求:
classDiagram
class Scheduler {
+waiting: RequestQueue
+running: List~Request~
+kv_cache_manager: KVCacheManager
+max_num_running_reqs: int
+max_num_scheduled_tokens: int
+schedule() SchedulerOutput
+update_from_output(output, sampled_tokens)
+add_request(request)
+finish_requests(request_ids)
}
class RequestQueue {
+queue: Deque~Request~
+policy: SchedulingPolicy
+append(request)
+popleft() Request
+peek() Request
}
class KVCacheManager {
+allocate_slots(request, num_tokens)
+free(request)
+get_computed_blocks(request)
}
Scheduler --> RequestQueue
Scheduler --> KVCacheManager调度流程简述:
处理 running 请求:
- 计算每个请求需要的新 token 数
- 尝试分配 KV Cache
- 内存不足时执行抢占
处理 waiting 请求:
- 按优先级从队列取出请求
- 检查资源是否足够
- 分配资源并移入 running
返回 SchedulerOutput:
- 包含需要执行的请求信息
- 传递给 ModelExecutor 执行
KVCacheManager(KV Cache 管理器)
文件位置:vllm/v1/core/kv_cache_manager.py
KVCacheManager 管理 KV Cache 的分配和释放:
classDiagram
class KVCacheManager {
+coordinator: KVCacheCoordinator
+block_pool: BlockPool
+enable_caching: bool
+get_computed_blocks(request) Tuple
+allocate_slots(request, num_tokens) List~int~
+free(request)
}
class BlockPool {
+blocks: List~KVCacheBlock~
+free_block_queue: FreeKVCacheBlockQueue
+cached_block_hash_to_block: Dict
+get_free_block() KVCacheBlock
+free_block(block)
}
class KVCacheBlock {
+block_id: int
+ref_cnt: int
+block_hash: Optional~BlockHash~
}
KVCacheManager --> BlockPool
BlockPool --> KVCacheBlock2.4 执行层
GPUModelRunner
文件位置:vllm/v1/worker/gpu_model_runner.py
GPUModelRunner 负责准备输入数据并执行模型前向传播:
flowchart TD
subgraph GPUModelRunner.execute_model
A[接收 SchedulerOutput] --> B[准备输入 Tensors<br/>input_ids, positions]
B --> C[构建 AttentionMetadata<br/>block_tables, slot_mapping]
C --> D[模型前向传播<br/>model.forward]
D --> E[获取 hidden_states]
E --> F[LM Head 计算<br/>获取 logits]
F --> G[返回 logits]
end
subgraph GPUModelRunner.sample_tokens
H[接收 logits] --> I[应用采样参数<br/>temperature, top_p]
I --> J[Sampler.forward<br/>采样逻辑]
J --> K[返回 sampled_token_ids]
end
G --> H关键数据结构:
| 数据 | 说明 | 来源 |
|---|---|---|
| input_ids | 输入 token IDs | SchedulerOutput |
| positions | 位置编码索引 | 计算得到 |
| block_tables | 块表映射 | KVCacheManager |
| slot_mapping | 槽位映射 | KVCacheManager |
| kv_caches | KV Cache 张量 | GPU 显存 |
Attention Backend
文件位置:vllm/v1/attention/backends/
vLLM 支持多种注意力实现后端:
graph TD
A[Attention Backend 接口] --> B[Flash Attention V2]
A --> C[Flash Attention V3]
A --> D[Flash Infer]
A --> E[XFormers]
style B fill:#c8e6c9
style C fill:#c8e6c9Flash Attention 是默认后端,提供高效的注意力计算和 PagedAttention 支持。
3. 数据流完整追踪
让我们用一个具体的例子追踪数据在系统中的完整流程:
3.1 完整请求处理时序图
sequenceDiagram
participant User as 用户
participant LLM as LLM 类
participant IP as InputProcessor
participant EC as EngineCore
participant SCH as Scheduler
participant KVM as KVCacheManager
participant EX as ModelExecutor
participant MR as GPUModelRunner
participant OP as OutputProcessor
User->>LLM: generate("Hello, world", params)
LLM->>IP: process_inputs("Hello, world")
IP-->>LLM: ProcessedInputs(token_ids=[...])
LLM->>EC: add_request(request)
EC->>SCH: add_request(request)
Note over SCH: 请求加入 waiting 队列
loop 直到完成
LLM->>EC: step()
EC->>SCH: schedule()
SCH->>KVM: allocate_slots(request, num_tokens)
KVM-->>SCH: [slot_ids]
SCH-->>EC: SchedulerOutput
EC->>EX: execute_model(scheduler_output)
EX->>MR: execute_model(...)
MR-->>EX: logits
EX-->>EC: logits
EC->>EX: sample_tokens(logits)
EX->>MR: sample(logits)
MR-->>EX: sampled_token_ids
EX-->>EC: sampled_token_ids
EC->>SCH: update_from_output(output, tokens)
Note over SCH: 更新请求状态<br/>检查完成条件
EC-->>LLM: EngineCoreOutputs
end
LLM->>OP: process_outputs(outputs)
OP-->>LLM: RequestOutput
LLM-->>User: RequestOutput(text="...")3.2 数据结构变化追踪
| 阶段 | 输入数据 | 输出数据 | 处理组件 |
|---|---|---|---|
| 用户输入 | "Hello, world" | - | - |
| Tokenization | 字符串 | token_ids=[15496, 11, 995] | InputProcessor |
| 请求创建 | token_ids | Request 对象 | EngineCore |
| 调度 | Request | SchedulerOutput | Scheduler |
| 缓存分配 | Request | slot_mapping, block_tables | KVCacheManager |
| 模型执行 | Tensors | logits | GPUModelRunner |
| 采样 | logits | token_id=318 | Sampler |
| 状态更新 | token_id | 更新 Request | Scheduler |
| 输出处理 | token_ids | "I am..." | OutputProcessor |
4. 代码目录结构详解
4.1 目录树概览
vllm/
├── entrypoints/ # 用户接口层
│ ├── llm.py # LLM 类(Python API)
│ ├── cli/ # CLI 命令
│ │ └── main.py # CLI 入口
│ ├── openai/ # OpenAI 兼容 API
│ │ ├── api_server.py # HTTP 服务器
│ │ └── serving_*.py # 各种 serving 实现
│ └── serve/ # serve 相关
│
├── v1/ # V1 架构(新版本)
│ ├── engine/ # 引擎层
│ │ ├── llm_engine.py # LLMEngine
│ │ ├── async_llm.py # AsyncLLM
│ │ ├── core.py # EngineCore
│ │ ├── core_client.py # 核心客户端
│ │ ├── input_processor.py # 输入处理
│ │ ├── output_processor.py # 输出处理
│ │ └── detokenizer.py # 解码器
│ │
│ ├── core/ # 核心层
│ │ ├── sched/ # 调度相关
│ │ │ ├── scheduler.py # Scheduler
│ │ │ ├── request_queue.py # 请求队列
│ │ │ └── output.py # 调度输出
│ │ ├── kv_cache_manager.py # KV Cache 管理
│ │ ├── block_pool.py # 内存块池
│ │ └── kv_cache_utils.py # 缓存工具
│ │
│ ├── worker/ # 执行层
│ │ ├── gpu_model_runner.py # GPU 模型运行器
│ │ ├── gpu_worker.py # GPU 工作进程
│ │ └── block_table.py # 块表管理
│ │
│ ├── attention/ # 注意力实现
│ │ ├── backends/ # 后端实现
│ │ │ └── flash_attn.py # Flash Attention
│ │ └── ops/ # 底层操作
│ │ └── paged_attn.py # PagedAttention
│ │
│ ├── sample/ # 采样
│ │ └── sampler.py # Sampler
│ │
│ ├── request.py # Request 数据结构
│ └── outputs.py # 输出数据结构
│
├── config/ # 配置
│ └── vllm.py # VllmConfig
│
├── model_executor/ # 模型执行器
│ ├── models/ # 模型实现
│ └── layers/ # 层实现
│
├── sampling_params.py # SamplingParams
│
└── csrc/ # C++/CUDA 代码
└── attention/ # 注意力 CUDA 内核
├── paged_attention_v1.cu
└── paged_attention_v2.cu
4.2 关键文件索引
| 功能类别 | 文件路径 | 关键类/函数 |
|---|---|---|
| 入口 | ||
| Python API | vllm/entrypoints/llm.py | LLM, generate() |
| CLI | vllm/entrypoints/cli/main.py | main() |
| 引擎 | ||
| 同步引擎 | vllm/v1/engine/llm_engine.py | LLMEngine |
| 异步引擎 | vllm/v1/engine/async_llm.py | AsyncLLM |
| 核心逻辑 | vllm/v1/engine/core.py | EngineCore, step() |
| 调度 | ||
| 调度器 | vllm/v1/core/sched/scheduler.py | Scheduler, schedule() |
| 请求队列 | vllm/v1/core/sched/request_queue.py | RequestQueue |
| 内存管理 | ||
| KV Cache | vllm/v1/core/kv_cache_manager.py | KVCacheManager |
| 块池 | vllm/v1/core/block_pool.py | BlockPool |
| 执行 | ||
| 模型运行 | vllm/v1/worker/gpu_model_runner.py | GPUModelRunner |
| Worker | vllm/v1/worker/gpu_worker.py | GPUWorker |
| 注意力 | ||
| PagedAttention | vllm/v1/attention/ops/paged_attn.py | PagedAttention |
| Flash Attention | vllm/v1/attention/backends/flash_attn.py | FlashAttentionBackend |
| 数据结构 | ||
| 请求 | vllm/v1/request.py | Request, RequestStatus |
| 采样参数 | vllm/sampling_params.py | SamplingParams |
5. 配置系统
5.1 VllmConfig
vLLM 使用统一的配置系统,主要配置包括:
classDiagram
class VllmConfig {
+model_config: ModelConfig
+cache_config: CacheConfig
+parallel_config: ParallelConfig
+scheduler_config: SchedulerConfig
+speculative_config: SpeculativeConfig
}
class ModelConfig {
+model: str
+dtype: str
+max_model_len: int
}
class CacheConfig {
+block_size: int
+num_gpu_blocks: int
+enable_prefix_caching: bool
}
class SchedulerConfig {
+max_num_seqs: int
+max_num_batched_tokens: int
}
VllmConfig --> ModelConfig
VllmConfig --> CacheConfig
VllmConfig --> SchedulerConfig5.2 常用配置参数
| 参数 | 说明 | 默认值 |
|---|---|---|
--model | 模型路径或名称 | 必填 |
--dtype | 数据精度 | auto |
--max-model-len | 最大序列长度 | 模型默认 |
--gpu-memory-utilization | GPU 显存利用率 | 0.9 |
--max-num-seqs | 最大并发请求数 | 256 |
--block-size | KV Cache 块大小 | 16 |
--enable-prefix-caching | 启用前缀缓存 | False |
--tensor-parallel-size | 张量并行大小 | 1 |
6. V1 vs 旧版架构
vLLM 当前主要使用 V1 架构,相比旧版有以下改进:
| 特性 | 旧版 | V1 |
|---|---|---|
| 调度器 | BlockSpaceManager | KVCacheManager |
| 执行流程 | 同步为主 | 异步优化 |
| 内存管理 | 基础 PagedAttention | 更细粒度的块管理 |
| 前缀缓存 | 有限支持 | 完整支持 |
| 代码组织 | 分散 | 模块化 |
本文档系列主要基于 V1 架构进行讲解。
7. 本章小结
架构层次
- 用户接口层:提供 Python API、CLI、OpenAI API 等多种访问方式
- 引擎层:LLMEngine/AsyncLLM 协调输入输出处理
- 核心层:Scheduler 和 KVCacheManager 负责调度和内存管理
- 执行层:GPUModelRunner 执行模型计算
关键组件
- EngineCore:系统"大脑",包含 step() 核心循环
- Scheduler:决定哪些请求在每个 step 执行
- KVCacheManager:管理 KV Cache 的分配和释放
- GPUModelRunner:准备输入并执行模型前向传播
数据流程
用户输入 → Tokenization → 请求调度 → 缓存分配
→ 模型执行 → 采样 → 状态更新 → Detokenization → 用户输出
代码定位
- 入口:
vllm/entrypoints/ - 引擎:
vllm/v1/engine/ - 调度:
vllm/v1/core/sched/ - 执行:
vllm/v1/worker/ - 注意力:
vllm/v1/attention/
思考题
- 为什么 vLLM 要将 EngineCore 和 LLMEngine 分开设计?
- Scheduler 和 KVCacheManager 之间是如何协作的?
- 如果你要添加一个新的用户接口(比如 WebSocket),需要修改哪些组件?
下一步
架构概览已经完成,接下来我们将进入深度学习基础部分,为理解核心算法打下理论基础:
附:快速参考卡片
请求处理流程
User → LLM.generate() → InputProcessor → EngineCore
→ Scheduler.schedule() → KVCacheManager.allocate_slots()
→ GPUModelRunner.execute_model() → Sampler
→ Scheduler.update_from_output() → OutputProcessor → User
核心文件速查
调度逻辑 → vllm/v1/core/sched/scheduler.py
缓存管理 → vllm/v1/core/kv_cache_manager.py
模型执行 → vllm/v1/worker/gpu_model_runner.py
核心循环 → vllm/v1/engine/core.py