文档
本文档系列旨在帮助深度学习初学者深入理解 vLLM —— 一个高性能的大语言模型(LLM)推理和服务框架。我们将从最基础的概念出发,逐步深入到核心算法和代码实现,让你不仅知其然,更知其所以然。
你将学到
- 大语言模型推理面临的核心挑战
- Transformer 架构和注意力机制的工作原理
- vLLM 的核心创新:PagedAttention 和连续批处理
- 从入口到输出的完整代码执行链路
- 如何调试和分析 vLLM 代码
学习路线图
我们提供两条学习路径,你可以根据自己的背景和目标选择合适的路线。
路径一:基础路径(推荐新手)
适合深度学习基础较薄弱的读者,从基础概念学起。
flowchart TD
subgraph 第一阶段:理解问题
A[为什么需要 vLLM] --> B[LLM 推理挑战]
B --> C[vLLM 架构概览]
end
subgraph 第二阶段:学习基础
C --> D[神经网络基础]
D --> E[Transformer 架构]
E --> F[注意力机制]
F --> G[KV Cache 概念]
G --> H[LLM 生成过程]
end
subgraph 第三阶段:掌握核心
H --> I[PagedAttention]
I --> J[连续批处理]
end
subgraph 第四阶段:代码实践
J --> K[代码入口分析]
K --> L[引擎核心流程]
end
style A fill:#e1f5fe
style L fill:#c8e6c9预计阅读量:约 70,000 字,建议分 5-7 天完成
路径二:进阶路径(适合有基础的读者)
如果你已经了解 Transformer 和 KV Cache 的基本概念,可以直接进入核心内容。
flowchart TD
subgraph 快速入门
A[为什么需要 vLLM] --> B[vLLM 架构概览]
end
subgraph 核心模块
B --> C[PagedAttention]
C --> D[KV Cache 管理器]
D --> E[调度器原理]
E --> F[连续批处理]
end
subgraph 代码深入
F --> G[请求生命周期]
G --> H[模型执行流程]
end
subgraph 进阶主题
H --> I[量化技术]
I --> J[投机解码]
J --> K[分布式推理]
end
style A fill:#e1f5fe
style K fill:#c8e6c9预计阅读量:约 50,000 字,建议分 3-5 天完成
文档版本
- vLLM 版本:基于 vLLM v1 架构
- 文档版本:1.0
- 最后更新:2025 年 1 月
1 - 入门篇
理解为什么需要 vLLM,它解决了什么问题
本部分将帮助你理解大语言模型推理面临的挑战,以及 vLLM 如何通过创新的技术解决这些问题。
1.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 与其他框架对比
| 特性 | 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 支持同时加载多个模型,适用于:
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 推理面临的具体挑战:
👉 下一章:LLM 推理面临的挑战
1.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)
影响因素:
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:#c8e6c9
6. 本章小结
本章我们深入分析了 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 的整体架构是如何设计来应对这些挑战的:
👉 下一章:vLLM 整体架构概览
1.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:#bbdefb
2. 核心组件详解
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
2 - 深度学习基础
为理解 vLLM 原理打下必要的基础知识
本部分将介绍理解 vLLM 所需的深度学习基础知识,包括神经网络、Transformer 架构、注意力机制等核心概念。
2.1 - 神经网络基础
神经网络基础
本章将介绍神经网络的基本概念,为理解 Transformer 和 LLM 打下基础。
引言
如果你是深度学习的初学者,本章将帮助你建立必要的基础知识。我们将从最简单的神经元开始,逐步介绍神经网络的核心概念。
如果你已经熟悉这些内容,可以快速浏览或直接跳到下一章。
1. 从生物神经元到人工神经元
1.1 生物神经元
人脑中有约 860 亿个神经元,它们通过突触相互连接。每个神经元:
- 通过树突接收来自其他神经元的信号
- 在细胞体中处理这些信号
- 通过轴突将信号传递给其他神经元
当接收到的信号强度超过某个阈值时,神经元就会"激活"并发出信号。
1.2 人工神经元
人工神经元是对生物神经元的数学抽象:
graph LR
subgraph 输入
X1[x₁]
X2[x₂]
X3[x₃]
end
subgraph 神经元
W1[w₁] --> SUM((Σ))
W2[w₂] --> SUM
W3[w₃] --> SUM
B[b<br/>偏置] --> SUM
SUM --> ACT[激活函数<br/>f]
end
X1 --> W1
X2 --> W2
X3 --> W3
ACT --> Y[y<br/>输出]
style SUM fill:#e3f2fd
style ACT fill:#c8e6c9数学表达:
y = f(w₁x₁ + w₂x₂ + w₃x₃ + b)
或者用向量形式:
y = f(w · x + b)
其中:
- x:输入向量
- w:权重向量(需要学习的参数)
- b:偏置(需要学习的参数)
- f:激活函数
- y:输出
1.3 为什么需要激活函数?
如果没有激活函数,神经网络无论多少层,都只能表达线性函数:
# 两层无激活函数的网络
y = W₂(W₁x + b₁) + b₂
= W₂W₁x + W₂b₁ + b₂
= W'x + b' # 仍然是线性的!
激活函数引入非线性,使神经网络能够学习复杂的模式。
2. 激活函数详解
2.1 经典激活函数
Sigmoid
σ(x) = 1 / (1 + e^(-x))
特点:
- 输出范围 (0, 1)
- 适合二分类的输出层
- 问题:梯度消失(输入很大或很小时,梯度接近 0)
Tanh
tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))
特点:
- 输出范围 (-1, 1)
- 零中心化
- 问题:同样有梯度消失问题
ReLU(Rectified Linear Unit)
ReLU(x) = max(0, x)
特点:
- 计算简单高效
- 缓解梯度消失
- 问题:负值区域梯度为 0(“死神经元”)
2.2 现代激活函数
GELU(Gaussian Error Linear Unit)
GELU(x) = x · Φ(x)
其中 Φ(x) 是标准正态分布的累积分布函数。
近似计算:
GELU(x) ≈ 0.5x(1 + tanh(√(2/π)(x + 0.044715x³)))
特点:
- 平滑的非线性
- 在 Transformer 和 LLM 中广泛使用
- 比 ReLU 表现更好
SiLU / Swish
SiLU(x) = x · σ(x) = x / (1 + e^(-x))
特点:
2.3 激活函数对比
graph LR
subgraph 激活函数特性对比
R[ReLU] --> R1[简单高效]
R --> R2[可能死神经元]
G[GELU] --> G1[平滑非线性]
G --> G2[Transformer 首选]
S[SiLU] --> S1[平滑非单调]
S --> S2[LLaMA 使用]
end| 函数 | 公式 | 范围 | 使用场景 |
|---|
| ReLU | max(0, x) | [0, +∞) | 传统 CNN |
| GELU | x·Φ(x) | (-∞, +∞) | BERT, GPT |
| SiLU | x·σ(x) | (-∞, +∞) | LLaMA, Qwen |
3. 张量(Tensor)基础
3.1 什么是张量
张量是多维数组的通称:
graph TD
subgraph 张量的维度
S[标量 Scalar<br/>0维<br/>例: 3.14]
V[向量 Vector<br/>1维<br/>例: [1, 2, 3]]
M[矩阵 Matrix<br/>2维<br/>例: [[1,2], [3,4]]]
T[张量 Tensor<br/>N维<br/>例: 3D, 4D, ...]
end
S --> V --> M --> T3.2 张量的形状(Shape)
张量的形状描述了每个维度的大小:
import torch
# 标量
scalar = torch.tensor(3.14)
print(scalar.shape) # torch.Size([])
# 向量
vector = torch.tensor([1, 2, 3])
print(vector.shape) # torch.Size([3])
# 矩阵
matrix = torch.tensor([[1, 2], [3, 4], [5, 6]])
print(matrix.shape) # torch.Size([3, 2]) # 3行2列
# 3D 张量
tensor_3d = torch.randn(2, 3, 4)
print(tensor_3d.shape) # torch.Size([2, 3, 4])
3.3 LLM 中的常见张量形状
在 LLM 中,我们经常遇到以下形状的张量:
| 张量 | 形状 | 说明 |
|---|
| 输入 token IDs | [batch_size, seq_len] | 批次中的 token 索引 |
| Embedding 输出 | [batch_size, seq_len, hidden_dim] | 词向量表示 |
| Attention 权重 | [batch_size, num_heads, seq_len, seq_len] | 注意力分数 |
| KV Cache | [num_layers, 2, batch_size, num_heads, seq_len, head_dim] | 键值缓存 |
| Logits | [batch_size, seq_len, vocab_size] | 输出概率分布 |
示例:
# 假设配置
batch_size = 4 # 批次大小
seq_len = 512 # 序列长度
hidden_dim = 4096 # 隐藏维度
num_heads = 32 # 注意力头数
head_dim = 128 # 每个头的维度 (hidden_dim / num_heads)
vocab_size = 32000 # 词表大小
# 输入
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
# Shape: [4, 512]
# Embedding 后
embeddings = torch.randn(batch_size, seq_len, hidden_dim)
# Shape: [4, 512, 4096]
# Attention 输出
attention_output = torch.randn(batch_size, seq_len, hidden_dim)
# Shape: [4, 512, 4096]
# 最终 logits
logits = torch.randn(batch_size, seq_len, vocab_size)
# Shape: [4, 512, 32000]
3.4 常用张量操作
import torch
# 创建张量
x = torch.randn(2, 3, 4) # 随机正态分布
y = torch.zeros(2, 3, 4) # 全零
z = torch.ones(2, 3, 4) # 全一
# 形状操作
x.view(2, 12) # 重塑形状 [2, 3, 4] → [2, 12]
x.reshape(6, 4) # 重塑形状 [2, 3, 4] → [6, 4]
x.transpose(1, 2) # 交换维度 [2, 3, 4] → [2, 4, 3]
x.permute(2, 0, 1) # 重排维度 [2, 3, 4] → [4, 2, 3]
# 数学运算
x + y # 逐元素加法
x * y # 逐元素乘法
x @ y.transpose(-1, -2) # 矩阵乘法
torch.softmax(x, dim=-1) # Softmax
# 索引和切片
x[0] # 第一个样本
x[:, 0, :] # 所有样本的第一个位置
x[..., -1] # 最后一个维度的最后一个元素
4. 矩阵乘法与 GPU 加速
4.1 矩阵乘法基础
矩阵乘法是神经网络的核心操作:
C = A × B
其中 A: [M, K], B: [K, N], C: [M, N]
计算复杂度:O(M × K × N)
# PyTorch 矩阵乘法
A = torch.randn(64, 128) # [M, K]
B = torch.randn(128, 256) # [K, N]
C = A @ B # [M, N] = [64, 256]
# 或者
C = torch.matmul(A, B)
4.2 批量矩阵乘法(BMM)
在处理批次数据时,我们需要批量矩阵乘法:
# 批量矩阵乘法
batch_A = torch.randn(32, 64, 128) # [batch, M, K]
batch_B = torch.randn(32, 128, 256) # [batch, K, N]
batch_C = torch.bmm(batch_A, batch_B) # [batch, M, N] = [32, 64, 256]
4.3 为什么 GPU 适合矩阵运算
graph TB
subgraph CPU
C1[核心 1]
C2[核心 2]
C3[核心 3]
C4[核心 4]
C5[...]
C6[核心 16]
end
subgraph GPU
G1[核心 1]
G2[核心 2]
G3[...]
G4[核心 10000+]
end
subgraph 特点对比
CP[CPU: 少量强核心<br/>适合复杂顺序任务]
GP[GPU: 大量弱核心<br/>适合简单并行任务]
end
style G1 fill:#c8e6c9
style G2 fill:#c8e6c9
style G4 fill:#c8e6c9GPU 优势:
| 特点 | CPU | GPU |
|---|
| 核心数 | 4-64 | 1000-10000+ |
| 单核性能 | 高 | 低 |
| 并行度 | 低 | 极高 |
| 适合任务 | 复杂逻辑、分支 | 大规模并行计算 |
矩阵乘法的每个输出元素可以独立计算,非常适合 GPU 的大规模并行架构。
4.4 实际性能对比
import torch
import time
# 创建大矩阵
A = torch.randn(4096, 4096)
B = torch.randn(4096, 4096)
# CPU 计算
start = time.time()
C_cpu = A @ B
cpu_time = time.time() - start
# GPU 计算
A_gpu = A.cuda()
B_gpu = B.cuda()
torch.cuda.synchronize()
start = time.time()
C_gpu = A_gpu @ B_gpu
torch.cuda.synchronize()
gpu_time = time.time() - start
print(f"CPU: {cpu_time:.3f}s, GPU: {gpu_time:.3f}s")
print(f"加速比: {cpu_time/gpu_time:.1f}x")
# 典型输出: CPU: 2.5s, GPU: 0.01s, 加速比: 250x
5. 多层神经网络
5.1 网络结构
多层神经网络(MLP,Multi-Layer Perceptron)由多个层堆叠而成:
graph LR
subgraph 输入层
I1((x₁))
I2((x₂))
I3((x₃))
end
subgraph 隐藏层1
H11((h₁))
H12((h₂))
H13((h₃))
H14((h₄))
end
subgraph 隐藏层2
H21((h₁))
H22((h₂))
end
subgraph 输出层
O1((y₁))
O2((y₂))
end
I1 --> H11
I1 --> H12
I1 --> H13
I1 --> H14
I2 --> H11
I2 --> H12
I2 --> H13
I2 --> H14
I3 --> H11
I3 --> H12
I3 --> H13
I3 --> H14
H11 --> H21
H11 --> H22
H12 --> H21
H12 --> H22
H13 --> H21
H13 --> H22
H14 --> H21
H14 --> H22
H21 --> O1
H21 --> O2
H22 --> O1
H22 --> O25.2 前向传播
前向传播计算从输入到输出的过程:
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.layer1 = nn.Linear(input_dim, hidden_dim)
self.activation = nn.GELU()
self.layer2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
# x: [batch_size, input_dim]
x = self.layer1(x) # [batch_size, hidden_dim]
x = self.activation(x) # [batch_size, hidden_dim]
x = self.layer2(x) # [batch_size, output_dim]
return x
# 使用
model = SimpleMLP(768, 3072, 768)
input_data = torch.randn(32, 768) # batch_size=32
output = model(input_data) # [32, 768]
5.3 参数量计算
对于一个全连接层 nn.Linear(in_features, out_features):
参数量 = in_features × out_features + out_features(偏置)
示例:
# 层: Linear(768, 3072)
# 权重参数: 768 × 3072 = 2,359,296
# 偏置参数: 3072
# 总计: 2,362,368 ≈ 2.36M
6. 语言模型基础概念
6.1 什么是语言模型
语言模型是一个概率模型,用于预测文本序列的概率:
P(w₁, w₂, ..., wₙ) = P(w₁) × P(w₂|w₁) × P(w₃|w₁,w₂) × ... × P(wₙ|w₁,...,wₙ₋₁)
核心任务:给定前文,预测下一个词的概率分布。
graph LR
I[输入: 'The cat sat on the'] --> LM[语言模型]
LM --> O[输出概率分布:<br/>mat: 0.3<br/>floor: 0.2<br/>roof: 0.15<br/>...]6.2 Token 和词表
Token:文本的基本单位,可以是:
- 单词:“hello”、“world”
- 子词:“play” + “ing” = “playing”
- 字符:“h”、“e”、“l”、“l”、“o”
词表(Vocabulary):所有可能 token 的集合
# 常见词表大小
# GPT-2: 50257
# LLaMA: 32000
# Qwen: 151936
# Tokenization 示例
text = "Hello, how are you?"
tokens = tokenizer.encode(text)
# tokens = [15496, 11, 703, 527, 499, 30]
6.3 Embedding
Embedding 将离散的 token ID 转换为连续的向量:
graph LR
T[Token ID: 15496] --> E[Embedding 层<br/>查表]
E --> V[向量: [0.1, -0.2, 0.5, ...]]
subgraph Embedding 矩阵
EM[矩阵大小: vocab_size × hidden_dim<br/>例: 32000 × 4096]
end
style V fill:#c8e6c9import torch.nn as nn
# Embedding 层
vocab_size = 32000
hidden_dim = 4096
embedding = nn.Embedding(vocab_size, hidden_dim)
# 使用
token_ids = torch.tensor([15496, 11, 703]) # 3 个 token
vectors = embedding(token_ids) # [3, 4096]
7. 推理 vs 训练
7.1 训练过程
graph LR
subgraph 前向传播
I[输入 X] --> M[模型] --> O[输出 Y]
end
subgraph 损失计算
O --> L[Loss 函数]
T[真实标签] --> L
L --> LV[Loss 值]
end
subgraph 反向传播
LV --> G[计算梯度]
G --> U[更新参数]
U --> M
end训练需要:
- 前向传播:计算预测值
- 损失计算:比较预测与真实值
- 反向传播:计算梯度
- 参数更新:使用优化器更新权重
7.2 推理过程
graph LR
I[输入 X] --> M[模型<br/>权重固定] --> O[输出 Y]
style M fill:#c8e6c9推理只需要:
7.3 推理优化的重要性
| 对比项 | 训练 | 推理 |
|---|
| 目标 | 学习参数 | 使用参数 |
| 频率 | 一次(或少数几次) | 大量重复 |
| 延迟要求 | 不敏感 | 敏感(用户等待) |
| 批次大小 | 可以较大 | 通常较小 |
| 内存模式 | 需要存储梯度 | 不需要梯度 |
推理优化的核心目标:
- 降低延迟(用户体验)
- 提高吞吐量(服务更多用户)
- 减少显存占用(支持更大模型或更多并发)
这正是 vLLM 要解决的问题!
8. 本章小结
核心概念
- 神经元:接收输入、加权求和、应用激活函数、产生输出
- 激活函数:引入非线性,GELU 是 LLM 的常用选择
- 张量:多维数组,神经网络中数据的载体
- 矩阵乘法:神经网络的核心计算,GPU 加速的关键
关键公式
神经元输出: y = f(w · x + b)
全连接层参数量: in_features × out_features + out_features
LLM 相关
- Token:文本的基本单位
- Embedding:将 token ID 转换为向量
- 语言模型:预测下一个 token 的概率分布
- 推理:使用训练好的模型进行预测
与 vLLM 的关联
- 张量形状理解对于理解 vLLM 的内存管理至关重要
- GPU 并行计算是 vLLM 性能优化的基础
- 推理优化是 vLLM 的核心目标
思考题
- 为什么现代 LLM 普遍使用 GELU 而不是 ReLU?
- 如果一个模型有 7B 参数,使用 FP16 精度,需要多少显存存储权重?
- 批量矩阵乘法如何帮助提高 GPU 利用率?
下一步
神经网络基础已经介绍完毕,接下来我们将学习 LLM 的核心架构——Transformer:
👉 下一章:Transformer 架构详解
2.2 - Transformer 架构详解
本章将详细介绍 Transformer 架构,这是现代大语言模型的基础。
引言
2017 年,Google 发表了划时代的论文《Attention Is All You Need》,提出了 Transformer 架构。这个架构彻底改变了自然语言处理领域,成为了 GPT、BERT、LLaMA 等现代 LLM 的基础。
理解 Transformer 架构是理解 vLLM 优化原理的关键。
1.1 RNN/LSTM 的局限
在 Transformer 之前,序列建模主要依赖 RNN(循环神经网络)和 LSTM(长短期记忆网络):
graph LR
subgraph RNN 的顺序处理
X1[x₁] --> H1[h₁] --> H2[h₂] --> H3[h₃] --> H4[h₄]
X2[x₂] --> H2
X3[x₃] --> H3
X4[x₄] --> H4
endRNN 的问题:
| 问题 | 说明 |
|---|
| 顺序依赖 | 必须按顺序处理,无法并行 |
| 长距离依赖 | 难以捕获长序列中的远距离关系 |
| 梯度问题 | 长序列训练时梯度消失或爆炸 |
| 训练慢 | 无法充分利用 GPU 并行能力 |
1.2 Attention 的突破
Transformer 的核心创新是自注意力机制(Self-Attention):
- 可以直接建立序列中任意两个位置之间的关系
- 所有位置可以并行计算
- 没有顺序依赖
graph TB
subgraph Self-Attention
X1[x₁] <--> X2[x₂]
X1 <--> X3[x₃]
X1 <--> X4[x₄]
X2 <--> X3
X2 <--> X4
X3 <--> X4
end
2.1 原始 Encoder-Decoder 结构
原始 Transformer 包含 Encoder 和 Decoder 两部分:
graph TB
subgraph 输入
I[源序列<br/>例: 英文句子]
end
subgraph Encoder
E1[Embedding + 位置编码]
E2[Multi-Head Attention]
E3[Feed Forward]
E4[× N 层]
E1 --> E2 --> E3
E3 -.-> E4
end
subgraph Decoder
D1[Embedding + 位置编码]
D2[Masked Multi-Head Attention]
D3[Cross Attention]
D4[Feed Forward]
D5[× N 层]
D1 --> D2 --> D3 --> D4
D4 -.-> D5
end
subgraph 输出
O[目标序列<br/>例: 中文翻译]
end
I --> E1
E4 --> D3
D5 --> O应用场景:
- 机器翻译(英→中)
- 文本摘要
- BERT(仅 Encoder)
- T5(完整 Encoder-Decoder)
2.2 Decoder-Only 架构(现代 LLM)
现代大语言模型(GPT 系列、LLaMA、Qwen 等)都采用 Decoder-Only 架构:
graph TD
subgraph Decoder-Only 架构
I[输入 tokens] --> EMB[Embedding Layer]
EMB --> PE[+ 位置编码]
PE --> B1[Transformer Block 1]
B1 --> B2[Transformer Block 2]
B2 --> B3[...]
B3 --> BN[Transformer Block N]
BN --> LN[Layer Norm]
LN --> LM[LM Head<br/>Linear: hidden → vocab]
LM --> O[输出 logits]
end
style EMB fill:#e3f2fd
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style BN fill:#c8e6c9
style LM fill:#fff9c4为什么 Decoder-Only 成为主流?
| 优势 | 说明 |
|---|
| 统一架构 | 预训练和下游任务使用相同架构 |
| 自回归生成 | 天然适合文本生成任务 |
| 扩展性 | 参数量扩展效果好 |
| 简单高效 | 架构简单,训练推理更高效 |
每个 Transformer Block 包含以下组件:
graph TD
subgraph Transformer Block
I[输入 X] --> LN1[Layer Norm 1]
LN1 --> ATT[Multi-Head<br/>Self-Attention]
ATT --> ADD1[+]
I --> ADD1
ADD1 --> LN2[Layer Norm 2]
LN2 --> FFN[Feed Forward<br/>Network]
FFN --> ADD2[+]
ADD1 --> ADD2
ADD2 --> O[输出]
end
style ATT fill:#bbdefb
style FFN fill:#c8e6c9关键组件:
- Layer Normalization:归一化,稳定训练
- Multi-Head Self-Attention:捕获序列内的关系
- Feed Forward Network (FFN):非线性变换
- 残差连接:缓解梯度消失,帮助信息流动
3. Embedding 层
3.1 Token Embedding
Token Embedding 将离散的 token ID 映射为连续的向量:
import torch.nn as nn
class TokenEmbedding(nn.Module):
def __init__(self, vocab_size, hidden_dim):
super().__init__()
# 创建嵌入矩阵: [vocab_size, hidden_dim]
self.embedding = nn.Embedding(vocab_size, hidden_dim)
def forward(self, token_ids):
# token_ids: [batch_size, seq_len]
# 返回: [batch_size, seq_len, hidden_dim]
return self.embedding(token_ids)
# 示例
vocab_size = 32000
hidden_dim = 4096
embedding = TokenEmbedding(vocab_size, hidden_dim)
# 输入 token IDs
token_ids = torch.tensor([[1, 234, 567], [89, 10, 1112]]) # [2, 3]
# 输出嵌入向量
vectors = embedding(token_ids) # [2, 3, 4096]
3.2 Embedding 矩阵的参数量
参数量 = vocab_size × hidden_dim
示例(LLaMA-2-7B):
参数量 = 32000 × 4096 = 131,072,000 ≈ 131M
占 7B 模型总参数的约 1.9%。
4. 位置编码(Positional Encoding)
4.1 为什么需要位置信息
Self-Attention 本身不包含位置信息——它只看 token 之间的关系,不知道它们的顺序。
# 这两个序列的 Attention 计算结果相同(如果没有位置编码)
"猫 追 狗"
"狗 追 猫"
位置编码为每个位置添加独特的信息,让模型知道 token 的顺序。
4.2 正弦位置编码
原始 Transformer 使用正弦/余弦函数:
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
其中:
import numpy as np
def sinusoidal_position_encoding(max_len, hidden_dim):
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, hidden_dim, 2) * -(np.log(10000.0) / hidden_dim))
pe = np.zeros((max_len, hidden_dim))
pe[:, 0::2] = np.sin(position * div_term)
pe[:, 1::2] = np.cos(position * div_term)
return pe
# 生成位置编码
pe = sinusoidal_position_encoding(512, 4096)
# Shape: [512, 4096]
4.3 RoPE(旋转位置编码)
现代 LLM(如 LLaMA、Qwen)使用 RoPE(Rotary Position Embedding):
graph LR
subgraph RoPE 原理
Q[Query 向量] --> R1[旋转矩阵<br/>R(pos)]
R1 --> RQ[旋转后的 Query]
K[Key 向量] --> R2[旋转矩阵<br/>R(pos)]
R2 --> RK[旋转后的 Key]
endRoPE 的优势:
# RoPE 的核心思想(简化)
def rotate_half(x):
x1, x2 = x[..., :x.shape[-1]//2], x[..., x.shape[-1]//2:]
return torch.cat([-x2, x1], dim=-1)
def apply_rope(q, k, cos, sin):
q_embed = (q * cos) + (rotate_half(q) * sin)
k_embed = (k * cos) + (rotate_half(k) * sin)
return q_embed, k_embed
5. Multi-Head Attention
这是 Transformer 的核心组件,详细原理将在下一章介绍。这里给出结构概览:
graph TD
subgraph Multi-Head Attention
I[输入 X] --> WQ[W_Q]
I --> WK[W_K]
I --> WV[W_V]
WQ --> Q[Query]
WK --> K[Key]
WV --> V[Value]
Q --> SPLIT1[Split Heads]
K --> SPLIT2[Split Heads]
V --> SPLIT3[Split Heads]
SPLIT1 --> H1[Head 1]
SPLIT1 --> H2[Head 2]
SPLIT1 --> HN[Head N]
SPLIT2 --> H1
SPLIT2 --> H2
SPLIT2 --> HN
SPLIT3 --> H1
SPLIT3 --> H2
SPLIT3 --> HN
H1 --> CAT[Concat]
H2 --> CAT
HN --> CAT
CAT --> WO[W_O]
WO --> O[输出]
end参数量:
Q, K, V 投影: 3 × hidden_dim × hidden_dim
输出投影: hidden_dim × hidden_dim
总计: 4 × hidden_dim²
示例(hidden_dim = 4096):
参数量 = 4 × 4096² = 67,108,864 ≈ 67M
6. Feed Forward Network (FFN)
6.1 基本结构
FFN 是一个简单的两层全连接网络:
graph LR
I[输入<br/>hidden_dim] --> L1[Linear 1<br/>hidden → intermediate]
L1 --> ACT[激活函数<br/>GELU/SiLU]
ACT --> L2[Linear 2<br/>intermediate → hidden]
L2 --> O[输出<br/>hidden_dim]class FeedForward(nn.Module):
def __init__(self, hidden_dim, intermediate_dim):
super().__init__()
self.up_proj = nn.Linear(hidden_dim, intermediate_dim)
self.down_proj = nn.Linear(intermediate_dim, hidden_dim)
self.activation = nn.GELU()
def forward(self, x):
# x: [batch, seq_len, hidden_dim]
x = self.up_proj(x) # [batch, seq_len, intermediate_dim]
x = self.activation(x) # [batch, seq_len, intermediate_dim]
x = self.down_proj(x) # [batch, seq_len, hidden_dim]
return x
6.2 SwiGLU 变体
LLaMA 等模型使用 SwiGLU 激活函数:
graph LR
I[输入] --> G[Gate Proj]
I --> U[Up Proj]
G --> SILU[SiLU 激活]
SILU --> MUL[×]
U --> MUL
MUL --> D[Down Proj]
D --> O[输出]class SwiGLUFeedForward(nn.Module):
def __init__(self, hidden_dim, intermediate_dim):
super().__init__()
self.gate_proj = nn.Linear(hidden_dim, intermediate_dim)
self.up_proj = nn.Linear(hidden_dim, intermediate_dim)
self.down_proj = nn.Linear(intermediate_dim, hidden_dim)
def forward(self, x):
gate = torch.nn.functional.silu(self.gate_proj(x))
up = self.up_proj(x)
return self.down_proj(gate * up)
6.3 FFN 参数量
标准 FFN:
参数量 = 2 × hidden_dim × intermediate_dim
SwiGLU FFN(有三个投影矩阵):
参数量 = 3 × hidden_dim × intermediate_dim
示例(LLaMA-7B,hidden=4096,intermediate=11008):
参数量 = 3 × 4096 × 11008 = 135,266,304 ≈ 135M
7. Layer Normalization
7.1 为什么需要归一化
深层网络中,每层输出的分布会发生变化(Internal Covariate Shift),导致:
Layer Normalization 将每层输出归一化到均值 0、方差 1 的分布。
7.2 计算公式
LayerNorm(x) = γ × (x - μ) / √(σ² + ε) + β
其中:
μ:均值σ²:方差ε:防止除零的小常数γ, β:可学习的缩放和偏移参数
class LayerNorm(nn.Module):
def __init__(self, hidden_dim, eps=1e-5):
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_dim))
self.bias = nn.Parameter(torch.zeros(hidden_dim))
self.eps = eps
def forward(self, x):
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)
return self.weight * (x - mean) / torch.sqrt(var + self.eps) + self.bias
7.3 RMSNorm
LLaMA 等模型使用 RMSNorm,去掉了均值中心化:
RMSNorm(x) = γ × x / √(mean(x²) + ε)
优势:计算更简单,效果相当。
class RMSNorm(nn.Module):
def __init__(self, hidden_dim, eps=1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_dim))
self.eps = eps
def forward(self, x):
rms = torch.sqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
return self.weight * x / rms
7.4 Pre-Norm vs Post-Norm
graph TB
subgraph Post-Norm
I1[输入] --> ATT1[Attention]
ATT1 --> ADD1[+]
I1 --> ADD1
ADD1 --> LN1[LayerNorm]
end
subgraph Pre-Norm(现代 LLM 常用)
I2[输入] --> LN2[LayerNorm]
LN2 --> ATT2[Attention]
ATT2 --> ADD2[+]
I2 --> ADD2
end
style LN2 fill:#c8e6c9Pre-Norm 优势:
8. 残差连接
8.1 什么是残差连接
残差连接让信息可以"跳过"某些层直接传递:
output = x + Layer(x)
8.2 为什么残差连接重要
graph LR
subgraph 无残差
X1[x] --> L1[Layer 1] --> L2[Layer 2] --> L3[Layer 3] --> Y1[y]
end
subgraph 有残差
X2[x] --> LA[Layer 1] --> LB[Layer 2] --> LC[Layer 3] --> Y2[y]
X2 --> Y2
LA --> LB
LB --> LC
end优势:
- 缓解梯度消失
- 允许训练更深的网络
- 信息直接传递不会丢失
import torch
import torch.nn as nn
class TransformerBlock(nn.Module):
def __init__(self, hidden_dim, num_heads, intermediate_dim):
super().__init__()
self.norm1 = RMSNorm(hidden_dim)
self.attention = MultiHeadAttention(hidden_dim, num_heads)
self.norm2 = RMSNorm(hidden_dim)
self.ffn = SwiGLUFeedForward(hidden_dim, intermediate_dim)
def forward(self, x, attention_mask=None):
# Pre-Norm + Attention + 残差
residual = x
x = self.norm1(x)
x = self.attention(x, attention_mask)
x = residual + x
# Pre-Norm + FFN + 残差
residual = x
x = self.norm2(x)
x = self.ffn(x)
x = residual + x
return x
10. 参数量计算实战
10.1 LLaMA-2-7B 参数分布
| 组件 | 公式 | 参数量 |
|---|
| Embedding | vocab × hidden | 32000 × 4096 = 131M |
| 每层 Attention Q | hidden × hidden | 4096² = 16.8M |
| 每层 Attention K | hidden × (hidden/n_heads × n_kv_heads) | 4096 × 4096 = 16.8M |
| 每层 Attention V | hidden × (hidden/n_heads × n_kv_heads) | 4096 × 4096 = 16.8M |
| 每层 Attention O | hidden × hidden | 4096² = 16.8M |
| 每层 FFN gate | hidden × intermediate | 4096 × 11008 = 45.1M |
| 每层 FFN up | hidden × intermediate | 4096 × 11008 = 45.1M |
| 每层 FFN down | intermediate × hidden | 11008 × 4096 = 45.1M |
| 每层 Norm | 2 × hidden | 2 × 4096 = 8K |
| LM Head | hidden × vocab | 4096 × 32000 = 131M |
每层总计:约 202M 参数
32 层总计:32 × 202M = 6.46B
加上 Embedding 和 LM Head:约 6.7B
10.2 参数分布饼图
pie title LLaMA-7B 参数分布
"Attention (Q/K/V/O)" : 32
"FFN" : 65
"Embedding + LM Head" : 2
"Norm" : 1关键观察:
- FFN 占比最大(约 65%)
- Attention 其次(约 32%)
- Embedding 占比很小(约 2%)
这解释了为什么 vLLM 主要优化 Attention 和内存管理,而不是 FFN。
11. 本章小结
架构要点
- Decoder-Only 架构:现代 LLM 的主流选择
- Transformer Block:Attention + FFN + Norm + 残差
- 位置编码:RoPE 是现代标准
关键组件
| 组件 | 作用 | 现代实现 |
|---|
| Embedding | Token → Vector | 直接查表 |
| 位置编码 | 注入位置信息 | RoPE |
| Self-Attention | 捕获序列关系 | Multi-Head |
| FFN | 非线性变换 | SwiGLU |
| Layer Norm | 稳定训练 | RMSNorm |
| 残差连接 | 信息直传 | Pre-Norm |
参数分布
- FFN 占主导(约 65%)
- Attention 约 32%
- Embedding 约 2%
与 vLLM 的关联
- Attention 计算是 KV Cache 优化的核心
- 参数分布影响显存使用和优化策略
- 位置编码影响序列长度支持
思考题
- 为什么 Decoder-Only 架构在 LLM 中比 Encoder-Decoder 更流行?
- RoPE 相比正弦位置编码有什么优势?
- 为什么 FFN 的参数量比 Attention 多,但 vLLM 主要优化 Attention?
下一步
Transformer 架构介绍完毕,接下来我们将深入学习其核心——注意力机制:
👉 下一章:注意力机制原理
2.3 - 注意力机制原理
注意力机制原理
本章将深入介绍自注意力机制的数学原理和计算过程,这是理解 vLLM 核心优化的关键。
引言
注意力机制是 Transformer 的核心创新,也是 vLLM 优化的主要目标。理解注意力机制的计算过程,对于理解 KV Cache 和 PagedAttention 至关重要。
1. 注意力的直觉理解
1.1 人类注意力的类比
想象你在阅读一篇文章,当你看到"他"这个代词时,你会自动"关注"前文中提到的人名,以理解"他"指的是谁。
这就是注意力机制的核心思想:让模型学会"关注"序列中最相关的部分。
graph LR
subgraph 阅读理解
T1[张三] --> T2[今天] --> T3[去了] --> T4[公园]
T4 --> T5[他]
T5 -.->|关注| T1
end1.2 从"全局视野"到"重点关注"
没有注意力机制时,模型只能看到固定窗口内的信息。有了注意力机制:
graph TB
subgraph 固定窗口
FW[只能看到附近几个 token]
end
subgraph 注意力机制
ATT[可以关注序列中任意位置<br/>并根据相关性分配权重]
end
style ATT fill:#c8e6c9
2. 自注意力(Self-Attention)计算
2.1 Query、Key、Value 的含义
自注意力使用三个向量:
| 向量 | 类比 | 作用 |
|---|
| Query (Q) | “我要找什么” | 当前位置的查询向量 |
| Key (K) | “我是什么” | 每个位置的索引向量 |
| Value (V) | “我的内容” | 每个位置的值向量 |
直觉理解:
- Q 是"问题"
- K 是"索引/标签"
- V 是"内容"
- 计算 Q 和所有 K 的相似度,用相似度加权所有 V
2.2 计算公式
自注意力的核心公式:
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V
$$
其中:
- $Q$:Query 矩阵,形状 $[seq_len, d_k]$
- $K$:Key 矩阵,形状 $[seq_len, d_k]$
- $V$:Value 矩阵,形状 $[seq_len, d_v]$
- $d_k$:Key 的维度(用于缩放)
2.3 计算步骤详解
flowchart TD
subgraph 步骤1: 生成 Q, K, V
X[输入 X<br/>seq_len × hidden_dim]
X --> WQ[W_Q 投影]
X --> WK[W_K 投影]
X --> WV[W_V 投影]
WQ --> Q[Query<br/>seq_len × d_k]
WK --> K[Key<br/>seq_len × d_k]
WV --> V[Value<br/>seq_len × d_v]
end
subgraph 步骤2: 计算注意力分数
Q --> MM[Q × K^T]
K --> MM
MM --> SC[÷ √d_k<br/>缩放]
SC --> MASK[+ Mask<br/>可选]
MASK --> SM[Softmax]
SM --> ATT[注意力权重<br/>seq_len × seq_len]
end
subgraph 步骤3: 加权求和
ATT --> OUT[× V]
V --> OUT
OUT --> O[输出<br/>seq_len × d_v]
end
style SC fill:#fff9c4
style SM fill:#c8e6c92.4 逐步计算示例
假设我们有一个简单的序列,3 个 token,每个 token 的隐藏维度是 4:
import torch
import torch.nn.functional as F
# 输入
seq_len = 3
d_k = 4
# 假设 Q, K, V 已经通过线性投影得到
Q = torch.tensor([
[1.0, 0.0, 1.0, 0.0], # token 0 的 query
[0.0, 1.0, 0.0, 1.0], # token 1 的 query
[1.0, 1.0, 0.0, 0.0], # token 2 的 query
])
K = torch.tensor([
[1.0, 0.0, 0.0, 1.0], # token 0 的 key
[0.0, 1.0, 1.0, 0.0], # token 1 的 key
[1.0, 1.0, 1.0, 1.0], # token 2 的 key
])
V = torch.tensor([
[1.0, 2.0, 3.0, 4.0], # token 0 的 value
[5.0, 6.0, 7.0, 8.0], # token 1 的 value
[9.0, 10., 11., 12.], # token 2 的 value
])
# 步骤 1: 计算 Q × K^T
scores = Q @ K.T
print("注意力分数 (未缩放):")
print(scores)
# tensor([[1., 1., 2.],
# [1., 1., 2.],
# [1., 1., 3.]])
# 步骤 2: 缩放
d_k = 4
scaled_scores = scores / (d_k ** 0.5)
print("\n缩放后的分数:")
print(scaled_scores)
# 步骤 3: Softmax
attention_weights = F.softmax(scaled_scores, dim=-1)
print("\n注意力权重:")
print(attention_weights)
# 每行和为 1
# 步骤 4: 加权求和
output = attention_weights @ V
print("\n输出:")
print(output)
2.5 注意力权重可视化
注意力权重形成一个 [seq_len, seq_len] 的矩阵:
Token 0 Token 1 Token 2
Token 0 [ 0.30 0.30 0.40 ] # Token 0 关注谁
Token 1 [ 0.30 0.30 0.40 ] # Token 1 关注谁
Token 2 [ 0.20 0.20 0.60 ] # Token 2 关注谁
每一行表示一个 token 对所有 token 的注意力分布(和为 1)。
3. 缩放因子 √d 的作用
3.1 为什么需要缩放
当 $d_k$ 较大时,$QK^T$ 的点积结果会变得很大。这会导致:
- Softmax 饱和:大值经过 softmax 后趋近于 1,小值趋近于 0
- 梯度消失:softmax 在饱和区域的梯度接近 0
graph LR
subgraph 无缩放
S1[大的点积值] --> SM1[Softmax 饱和]
SM1 --> G1[梯度消失]
end
subgraph 有缩放
S2[缩放后的点积] --> SM2[Softmax 正常]
SM2 --> G2[梯度正常]
end
style G1 fill:#ffcdd2
style G2 fill:#c8e6c93.2 数学解释
假设 Q 和 K 的元素服从均值 0、方差 1 的分布,那么:
- $Q \cdot K$ 的均值为 0
- $Q \cdot K$ 的方差为 $d_k$
除以 $\sqrt{d_k}$ 后,方差变为 1,分布更稳定。
4. 多头注意力(Multi-Head Attention)
4.1 为什么需要多头
单头注意力只能学习一种"关注模式"。多头注意力让模型同时学习多种不同的关系:
graph TB
subgraph 多头注意力的优势
H1[Head 1<br/>关注语法关系]
H2[Head 2<br/>关注语义关系]
H3[Head 3<br/>关注位置关系]
H4[Head 4<br/>关注其他模式]
end4.2 多头计算过程
graph TD
X[输入 X<br/>batch × seq × hidden] --> SPLIT[分割成多个头]
subgraph 并行计算
SPLIT --> H1[Head 1<br/>Attention]
SPLIT --> H2[Head 2<br/>Attention]
SPLIT --> H3[Head 3<br/>Attention]
SPLIT --> HN[Head N<br/>Attention]
end
H1 --> CAT[Concat]
H2 --> CAT
H3 --> CAT
HN --> CAT
CAT --> WO[W_O 投影]
WO --> O[输出]4.3 代码实现
class MultiHeadAttention(nn.Module):
def __init__(self, hidden_dim, num_heads):
super().__init__()
self.num_heads = num_heads
self.head_dim = hidden_dim // num_heads
# Q, K, V 投影
self.q_proj = nn.Linear(hidden_dim, hidden_dim)
self.k_proj = nn.Linear(hidden_dim, hidden_dim)
self.v_proj = nn.Linear(hidden_dim, hidden_dim)
# 输出投影
self.o_proj = nn.Linear(hidden_dim, hidden_dim)
def forward(self, x):
batch_size, seq_len, _ = x.shape
# 投影
Q = self.q_proj(x) # [batch, seq, hidden]
K = self.k_proj(x)
V = self.v_proj(x)
# 重塑为多头: [batch, seq, num_heads, head_dim]
Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim)
K = K.view(batch_size, seq_len, self.num_heads, self.head_dim)
V = V.view(batch_size, seq_len, self.num_heads, self.head_dim)
# 转置: [batch, num_heads, seq, head_dim]
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
# 注意力计算
scores = Q @ K.transpose(-2, -1) / (self.head_dim ** 0.5)
attn_weights = F.softmax(scores, dim=-1)
output = attn_weights @ V # [batch, num_heads, seq, head_dim]
# 合并多头
output = output.transpose(1, 2) # [batch, seq, num_heads, head_dim]
output = output.reshape(batch_size, seq_len, -1) # [batch, seq, hidden]
# 输出投影
output = self.o_proj(output)
return output
4.4 头数与维度的关系
hidden_dim = num_heads × head_dim
常见配置:
| 模型 | hidden_dim | num_heads | head_dim |
|---|
| GPT-2 Small | 768 | 12 | 64 |
| GPT-2 Large | 1280 | 20 | 64 |
| LLaMA-7B | 4096 | 32 | 128 |
| LLaMA-70B | 8192 | 64 | 128 |
5. Masked Attention(因果掩码)
5.1 为什么需要掩码
在语言模型中,预测下一个 token 时不能看到未来的 token。因果掩码确保每个位置只能关注它之前的位置。
graph LR
subgraph 无掩码(双向注意力)
A1[token 1] <--> A2[token 2]
A1 <--> A3[token 3]
A2 <--> A3
end
subgraph 有掩码(单向注意力)
B1[token 1]
B2[token 2] --> B1
B3[token 3] --> B1
B3 --> B2
end5.2 掩码矩阵
因果掩码是一个下三角矩阵:
seq_len = 4
mask = torch.tril(torch.ones(seq_len, seq_len))
print(mask)
# tensor([[1., 0., 0., 0.],
# [1., 1., 0., 0.],
# [1., 1., 1., 0.],
# [1., 1., 1., 1.]])
可视化:
位置 0 位置 1 位置 2 位置 3
位置 0 [ 1 0 0 0 ] → 只能看自己
位置 1 [ 1 1 0 0 ] → 可看 0, 1
位置 2 [ 1 1 1 0 ] → 可看 0, 1, 2
位置 3 [ 1 1 1 1 ] → 可看全部
5.3 应用掩码
在 softmax 之前应用掩码,将不允许关注的位置设为负无穷:
def masked_attention(Q, K, V, mask):
d_k = Q.shape[-1]
scores = Q @ K.transpose(-2, -1) / (d_k ** 0.5)
# 应用掩码:将 mask=0 的位置设为 -inf
scores = scores.masked_fill(mask == 0, float('-inf'))
attn_weights = F.softmax(scores, dim=-1)
output = attn_weights @ V
return output
掩码后的注意力分数:
before softmax:
[[ 0.5 -inf -inf -inf]
[ 0.3 0.7 -inf -inf]
[ 0.2 0.4 0.6 -inf]
[ 0.1 0.3 0.5 0.8]]
after softmax:
[[1.00 0.00 0.00 0.00] # 只关注位置 0
[0.40 0.60 0.00 0.00] # 关注位置 0, 1
[0.25 0.33 0.42 0.00] # 关注位置 0, 1, 2
[0.15 0.22 0.28 0.35]] # 关注全部
6. 注意力的计算复杂度
6.1 时间复杂度
核心计算 $QK^T$ 和 $(\text{softmax})V$:
- $QK^T$:$[n, d] \times [d, n] = O(n^2 d)$
- $\text{Attention} \times V$:$[n, n] \times [n, d] = O(n^2 d)$
总时间复杂度:$O(n^2 d)$
其中 $n$ 是序列长度,$d$ 是维度。
6.2 空间复杂度
需要存储注意力权重矩阵:
空间复杂度:$O(n^2)$
6.3 长序列的挑战
graph LR
subgraph 序列长度影响
L1[n=512] --> C1[计算量 262K]
L2[n=2048] --> C2[计算量 4.2M]
L3[n=8192] --> C3[计算量 67M]
L4[n=32768] --> C4[计算量 1B]
end当序列长度增加 4 倍,计算量增加 16 倍!这是长序列 LLM 面临的核心挑战。
6.4 优化方法简介
| 方法 | 原理 | 复杂度 |
|---|
| Flash Attention | IO 优化,减少内存访问 | O(n²) 但更快 |
| Sparse Attention | 稀疏注意力模式 | O(n√n) 或 O(n) |
| Linear Attention | 核方法近似 | O(n) |
| Sliding Window | 只关注局部窗口 | O(nw) |
vLLM 主要使用 Flash Attention 作为注意力后端。
7. Grouped-Query Attention (GQA)
7.1 传统 MHA vs GQA
为了减少 KV Cache 的内存占用,现代模型使用 GQA:
graph TB
subgraph MHA(Multi-Head Attention)
MQ1[Q Head 1] --> MK1[K Head 1]
MQ2[Q Head 2] --> MK2[K Head 2]
MQ3[Q Head 3] --> MK3[K Head 3]
MQ4[Q Head 4] --> MK4[K Head 4]
end
subgraph GQA(Grouped-Query Attention)
GQ1[Q Head 1] --> GK1[K Group 1]
GQ2[Q Head 2] --> GK1
GQ3[Q Head 3] --> GK2[K Group 2]
GQ4[Q Head 4] --> GK2
end7.2 GQA 的优势
| 特性 | MHA | GQA |
|---|
| Q heads | N | N |
| K/V heads | N | N/group_size |
| KV Cache 大小 | 100% | 减少到 1/group_size |
| 模型质量 | 基准 | 接近基准 |
示例(LLaMA-2-70B):
- Q heads: 64
- KV heads: 8
- KV Cache 减少 8 倍!
8. 注意力与 KV Cache 的关系
8.1 为什么需要缓存 K 和 V
在自回归生成中,每生成一个新 token,都需要计算它与所有历史 token 的注意力。
不使用 KV Cache:每次都重新计算所有 token 的 K 和 V
使用 KV Cache:缓存历史 token 的 K 和 V,只计算新 token 的
这正是下一章的主题!
8.2 预览:KV Cache 的作用
sequenceDiagram
participant New as 新 Token
participant Cache as KV Cache
participant ATT as Attention
Note over Cache: 存储历史 token 的 K, V
New->>ATT: 计算新 token 的 Q, K, V
Cache->>ATT: 提供历史 K, V
ATT->>ATT: Q_new × [K_cache, K_new]^T
ATT->>ATT: Attention × [V_cache, V_new]
ATT->>Cache: 将 K_new, V_new 加入缓存
9. 本章小结
核心公式
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V
$$
关键概念
| 概念 | 说明 |
|---|
| Q/K/V | Query(查询)、Key(键)、Value(值) |
| 缩放因子 | $\sqrt{d_k}$,防止 softmax 饱和 |
| 多头注意力 | 并行学习多种注意力模式 |
| 因果掩码 | 防止看到未来 token |
| GQA | 减少 KV heads,降低内存占用 |
计算复杂度
- 时间复杂度:$O(n^2 d)$
- 空间复杂度:$O(n^2)$
- 长序列是主要挑战
与 vLLM 的关联
- KV Cache 是注意力优化的核心
- PagedAttention 优化 K/V 的内存管理
- Flash Attention 优化注意力计算速度
思考题
- 如果没有缩放因子 $\sqrt{d_k}$,会发生什么?
- 为什么 GQA 可以在减少 KV heads 的同时保持模型质量?
- 在因果掩码下,位置 0 的 token 只能关注自己,这会影响模型效果吗?
下一步
理解了注意力机制后,我们将深入学习 KV Cache 的概念和作用:
👉 下一章:KV Cache 概念
2.4 - KV Cache 概念
KV Cache 概念
本章将详细介绍 KV Cache 的概念、作用和实现原理,这是理解 vLLM 核心优化的关键。
引言
KV Cache 是 LLM 推理中最重要的优化技术之一。它通过缓存历史计算结果,避免重复计算,显著提升推理速度。理解 KV Cache 对于理解 vLLM 的 PagedAttention 至关重要。
1. 为什么需要 KV Cache
1.1 自回归生成的特点
LLM 生成文本是自回归的:每次只生成一个 token,然后将其加入输入,继续生成下一个。
sequenceDiagram
participant User as 用户
participant LLM as LLM
User->>LLM: "今天天气"
LLM-->>LLM: 计算所有 token 的 Attention
LLM->>User: "很"
User->>LLM: "今天天气很"
LLM-->>LLM: 重新计算所有 token 的 Attention?
LLM->>User: "好"
User->>LLM: "今天天气很好"
LLM-->>LLM: 又重新计算所有?
LLM->>User: "。"1.2 没有 KV Cache 时的重复计算
在注意力计算中,每个 token 需要:
- 计算自己的 Q(Query)
- 计算自己的 K(Key)和 V(Value)
- 用 Q 与所有 K 计算注意力
- 用注意力加权所有 V
问题:历史 token 的 K 和 V 每次都要重新计算!
flowchart TD
subgraph Step 1: 处理 'Hello'
A1[Hello] --> K1[计算 K₁]
A1 --> V1[计算 V₁]
A1 --> Q1[计算 Q₁]
end
subgraph Step 2: 处理 'Hello World'
B1[Hello] --> K1_2[重新计算 K₁]
B1 --> V1_2[重新计算 V₁]
B2[World] --> K2[计算 K₂]
B2 --> V2[计算 V₂]
B2 --> Q2[计算 Q₂]
end
subgraph Step 3: 处理 'Hello World !'
C1[Hello] --> K1_3[再次计算 K₁]
C1 --> V1_3[再次计算 V₁]
C2[World] --> K2_3[再次计算 K₂]
C2 --> V2_3[再次计算 V₂]
C3[!] --> K3[计算 K₃]
C3 --> V3[计算 V₃]
C3 --> Q3[计算 Q₃]
end
style K1_2 fill:#ffcdd2
style V1_2 fill:#ffcdd2
style K1_3 fill:#ffcdd2
style V1_3 fill:#ffcdd2
style K2_3 fill:#ffcdd2
style V2_3 fill:#ffcdd21.3 计算量分析
生成 N 个 token,不使用 KV Cache:
| Step | 需要计算的 K/V | 累计 K/V 计算次数 |
|---|
| 1 | 1 | 1 |
| 2 | 2(重新计算 1 + 新的 1) | 1 + 2 = 3 |
| 3 | 3(重新计算 2 + 新的 1) | 3 + 3 = 6 |
| … | … | … |
| N | N | 1 + 2 + … + N = N(N+1)/2 |
时间复杂度:$O(N^2)$
2. KV Cache 工作原理
2.1 核心思想
观察:在自回归生成中,历史 token 的 K 和 V 不会改变。
解决方案:计算一次后缓存起来,后续直接使用。
flowchart TD
subgraph 使用 KV Cache
subgraph Step 1
S1A[Hello] --> S1K[计算 K₁]
S1A --> S1V[计算 V₁]
S1K --> Cache1[(缓存 K₁)]
S1V --> Cache1
end
subgraph Step 2
Cache1 --> Use1[使用缓存的 K₁, V₁]
S2A[World] --> S2K[计算 K₂]
S2A --> S2V[计算 V₂]
S2K --> Cache2[(缓存 K₁, K₂)]
S2V --> Cache2
end
subgraph Step 3
Cache2 --> Use2[使用缓存的 K₁, K₂, V₁, V₂]
S3A[!] --> S3K[计算 K₃]
S3A --> S3V[计算 V₃]
end
end
style Use1 fill:#c8e6c9
style Use2 fill:#c8e6c92.2 计算量对比
使用 KV Cache 后:
| Step | 需要计算的 K/V | 累计 K/V 计算次数 |
|---|
| 1 | 1 | 1 |
| 2 | 1(只计算新的) | 1 + 1 = 2 |
| 3 | 1(只计算新的) | 2 + 1 = 3 |
| … | … | … |
| N | 1 | N |
时间复杂度:$O(N)$
加速比:从 $O(N^2)$ 到 $O(N)$,生成 1000 个 token 时加速约 500 倍!
2.3 图解对比
graph TD
subgraph 无 KV Cache
A1[Token 1] --> C1[计算全部 K,V]
A2[Token 1,2] --> C2[计算全部 K,V]
A3[Token 1,2,3] --> C3[计算全部 K,V]
A4[Token 1,2,3,4] --> C4[计算全部 K,V]
style A1 fill:#ffcdd2
style A2 fill:#ffcdd2
style A3 fill:#ffcdd2
style A4 fill:#ffcdd2
end
subgraph 有 KV Cache
B1[Token 1] --> D1[计算 K₁,V₁ + 缓存]
B2[Token 2] --> D2[计算 K₂,V₂ + 读缓存]
B3[Token 3] --> D3[计算 K₃,V₃ + 读缓存]
B4[Token 4] --> D4[计算 K₄,V₄ + 读缓存]
D1 --> Cache[(KV Cache)]
D2 --> Cache
D3 --> Cache
D4 --> Cache
Cache --> D2
Cache --> D3
Cache --> D4
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9
style B4 fill:#c8e6c9
end
3. KV Cache 的数据结构
3.1 基本形状
KV Cache 需要存储每层的 K 和 V:
# KV Cache 形状
# 方式 1: 分开存储
k_cache = torch.zeros(num_layers, batch_size, num_heads, max_seq_len, head_dim)
v_cache = torch.zeros(num_layers, batch_size, num_heads, max_seq_len, head_dim)
# 方式 2: 合并存储
kv_cache = torch.zeros(num_layers, 2, batch_size, num_heads, max_seq_len, head_dim)
# kv_cache[:, 0, ...] 是 K
# kv_cache[:, 1, ...] 是 V
3.2 维度解释
| 维度 | 含义 | 示例值 |
|---|
| num_layers | Transformer 层数 | 32 |
| 2 | K 和 V | 2 |
| batch_size | 批次大小 | 1-64 |
| num_heads | 注意力头数(或 KV heads) | 32 或 8 |
| max_seq_len | 最大序列长度 | 4096 |
| head_dim | 每个头的维度 | 128 |
3.3 代码示例
class KVCache:
def __init__(self, num_layers, num_heads, head_dim, max_seq_len, dtype=torch.float16):
self.num_layers = num_layers
self.max_seq_len = max_seq_len
# 预分配 K 和 V 缓存
# 形状: [num_layers, 2, max_batch, num_heads, max_seq_len, head_dim]
self.cache = None
self.current_len = 0
def allocate(self, batch_size):
self.cache = torch.zeros(
self.num_layers, 2, batch_size, self.num_heads,
self.max_seq_len, self.head_dim,
dtype=self.dtype, device='cuda'
)
self.current_len = 0
def update(self, layer_idx, new_k, new_v):
"""添加新的 K, V 到缓存"""
# new_k, new_v: [batch, num_heads, new_len, head_dim]
new_len = new_k.shape[2]
start_pos = self.current_len
end_pos = start_pos + new_len
self.cache[layer_idx, 0, :, :, start_pos:end_pos, :] = new_k
self.cache[layer_idx, 1, :, :, start_pos:end_pos, :] = new_v
if layer_idx == self.num_layers - 1:
self.current_len = end_pos
def get(self, layer_idx):
"""获取当前层的完整 K, V"""
k = self.cache[layer_idx, 0, :, :, :self.current_len, :]
v = self.cache[layer_idx, 1, :, :, :self.current_len, :]
return k, v
4. 显存占用详细计算
4.1 计算公式
KV Cache 显存 = 2 × num_layers × num_kv_heads × head_dim × seq_len × batch_size × bytes_per_element
简化版(使用 hidden_dim):
KV Cache 显存 = 2 × num_layers × hidden_dim × seq_len × batch_size × bytes_per_element
注意:如果使用 GQA,num_kv_heads 可能小于 num_attention_heads。
4.2 LLaMA-2-7B 示例
模型参数:
- num_layers: 32
- hidden_dim: 4096
- num_kv_heads: 32(MHA)
- head_dim: 128
- 精度: FP16(2 bytes)
单个请求不同序列长度的 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 |
| 8192 | 2 × 32 × 4096 × 8192 × 2 | 4 GB |
4.3 LLaMA-2-70B 示例(使用 GQA)
模型参数:
- num_layers: 80
- hidden_dim: 8192
- num_kv_heads: 8(GQA,原本是 64 个 attention heads)
- head_dim: 128
- 精度: FP16
单个请求 4096 序列长度:
KV Cache = 2 × 80 × 8 × 128 × 4096 × 2 = 1.34 GB
对比 MHA(如果 kv_heads = 64):
KV Cache = 2 × 80 × 64 × 128 × 4096 × 2 = 10.7 GB
GQA 节省了 8 倍显存!
4.4 显存占用可视化
pie title 7B 模型显存分布(单请求 2048 tokens)
"模型权重 (14GB)" : 14
"KV Cache (1GB)" : 1
"激活值等 (1GB)" : 1pie title 7B 模型显存分布(32 并发 × 2048 tokens)
"模型权重 (14GB)" : 14
"KV Cache (32GB)" : 32
"激活值等 (2GB)" : 2
5. KV Cache 管理的挑战
5.1 动态序列长度
KV Cache 的大小随着生成过程动态增长:
graph LR
subgraph 生成过程
S1[Step 1<br/>KV: 10 tokens]
S2[Step 2<br/>KV: 11 tokens]
S3[Step 3<br/>KV: 12 tokens]
SN[Step N<br/>KV: N+10 tokens]
S1 --> S2 --> S3 --> SN
end问题:在请求开始时,我们不知道最终会生成多少 token!
5.2 预分配策略的问题
传统方案:预分配最大可能长度(如 4096 tokens)
预分配: 4096 tokens × 每token 0.5MB = 2GB
实际使用: 100 tokens × 0.5MB = 50MB
浪费: 1.95GB (97.5%)
graph TB
subgraph 预分配的浪费
Alloc[预分配 2GB]
Used[实际使用 50MB]
Waste[浪费 1.95GB]
Alloc --> Used
Alloc --> Waste
end
style Waste fill:#ffcdd25.3 显存碎片化
当多个请求同时运行时,问题更加严重:
显存状态:
+--------+--------+--------+--------+--------+
| Req A | Req B | Req C | Req D | 空闲 |
| 2GB | 2GB | 2GB | 2GB | 碎片 |
| 用50MB | 用100MB| 用30MB | 用200MB| |
+--------+--------+--------+--------+--------+
实际使用: 380MB
预分配: 8GB
浪费: 7.62GB (95%!)
5.4 这就是 PagedAttention 要解决的问题!
传统方案的问题:
- 预分配浪费:每个请求预留最大空间
- 内部碎片:实际使用远小于预分配
- 外部碎片:释放后的空间不连续
PagedAttention 的解决方案(下一部分详细介绍):
- 按需分配:用多少分配多少
- 分块管理:固定大小的块,减少碎片
- 非连续存储:块可以不连续
6. Prefill 和 Decode 中的 KV Cache
6.1 Prefill 阶段
处理输入 prompt,一次性计算所有输入 token 的 K、V:
flowchart LR
subgraph Prefill
I[输入: 'Hello, how are you?'<br/>5 tokens]
C[并行计算 K₁...K₅, V₁...V₅]
S[存入 KV Cache]
I --> C --> S
end特点:
- 批量计算,效率高
- 计算密集型
- KV Cache 从 0 增长到输入长度
6.2 Decode 阶段
逐个生成 token,每次只计算新 token 的 K、V:
flowchart TD
subgraph Decode 循环
R[读取 KV Cache]
N[新 token]
C[计算 K_new, V_new]
A[Attention: Q_new × [K_cache; K_new]]
U[更新 KV Cache]
O[输出 token]
R --> A
N --> C --> A
A --> U --> O
O -.->|下一轮| N
end特点:
- 增量计算,每次只算 1 个
- 内存密集型(需要读取整个 KV Cache)
- KV Cache 每步增长 1
6.3 两阶段的 KV Cache 操作对比
| 操作 | Prefill | Decode |
|---|
| K/V 计算 | 批量(N 个) | 单个(1 个) |
| KV Cache 读取 | 无 | 全部 |
| KV Cache 写入 | N 个 | 1 个 |
| 计算/访存比 | 高 | 低 |
7. vLLM 中的 KV Cache 相关代码
7.1 关键文件位置
| 功能 | 文件 |
|---|
| KV Cache 管理 | vllm/v1/core/kv_cache_manager.py |
| 块池 | vllm/v1/core/block_pool.py |
| 块表 | vllm/v1/worker/block_table.py |
| KV Cache 接口 | vllm/v1/kv_cache_interface.py |
7.2 数据结构预览
# vllm/v1/core/block_pool.py 中的块定义
@dataclass
class KVCacheBlock:
block_id: int # 块 ID
ref_cnt: int # 引用计数
block_hash: Optional[BlockHash] # 用于前缀缓存
# vllm/v1/worker/block_table.py 中的块表
class BlockTable:
"""管理逻辑块到物理块的映射"""
def __init__(self, ...):
self.block_table: torch.Tensor # 形状: [max_blocks]
8. 本章小结
核心概念
- KV Cache 的作用:缓存历史 token 的 K、V,避免重复计算
- 加速效果:从 $O(N^2)$ 降到 $O(N)$,约 500 倍加速(N=1000)
- 显存占用:随序列长度线性增长,可能成为主要显存消耗
关键公式
KV Cache = 2 × num_layers × num_kv_heads × head_dim × seq_len × bytes
管理挑战
- 动态增长:序列长度在生成过程中不断增加
- 预分配浪费:传统方案浪费 60-80% 显存
- 碎片化:多请求并发时问题更严重
与 vLLM 的关联
- PagedAttention:解决 KV Cache 的显存浪费问题
- 分块管理:将 KV Cache 分成固定大小的块
- 按需分配:用多少分配多少,不预留
思考题
- 如果一个模型使用 GQA,KV heads 是 attention heads 的 1/8,KV Cache 显存会减少多少?
- 为什么 Decode 阶段是"内存密集型"而不是"计算密集型"?
- 如果 vLLM 要支持无限长度的上下文,KV Cache 管理会面临什么额外挑战?
下一步
了解了 KV Cache 后,让我们来看看 LLM 完整的生成过程:
👉 下一章:LLM 生成过程
2.5 - LLM 生成过程
LLM 生成过程
本章将详细介绍 LLM 文本生成的完整流程,包括 Prefill、Decode 两个阶段以及各种采样策略。
引言
LLM 生成文本是一个复杂的过程,涉及 tokenization、模型前向传播、采样等多个环节。理解这个过程对于理解 vLLM 的优化策略至关重要。
1. 生成流程概览
1.1 完整流程图
sequenceDiagram
participant User as 用户
participant Tok as Tokenizer
participant Model as LLM
participant Sampler as 采样器
participant DeTok as Detokenizer
User->>Tok: "Hello, world"
Tok->>Model: [15496, 11, 995]
rect rgb(200, 230, 200)
Note over Model: Prefill 阶段
Model->>Model: 处理所有输入 tokens
Model->>Model: 初始化 KV Cache
Model->>Sampler: logits
Sampler->>Model: 第一个输出 token
end
rect rgb(200, 200, 230)
Note over Model: Decode 阶段
loop 直到停止条件
Model->>Model: 处理 1 个新 token
Model->>Model: 更新 KV Cache
Model->>Sampler: logits
Sampler->>Model: 下一个 token
end
end
Model->>DeTok: [318, 716, 257, ...]
DeTok->>User: "I am a language model..."1.2 两阶段模型
LLM 生成分为两个截然不同的阶段:
| 阶段 | Prefill(预填充) | Decode(解码) |
|---|
| 处理内容 | 整个输入 prompt | 新生成的 token |
| 每次处理 | N 个 tokens | 1 个 token |
| KV Cache | 初始化 | 增量更新 |
| 计算特性 | 计算密集型 | 内存密集型 |
| GPU 利用率 | 高 | 低 |
2. Prefill 阶段详解
2.1 输入处理:Tokenization
第一步是将文本转换为 token IDs:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
text = "Hello, how are you?"
tokens = tokenizer.encode(text)
print(tokens) # [1, 15043, 29892, 920, 526, 366, 29973]
print(tokenizer.convert_ids_to_tokens(tokens))
# ['<s>', 'Hello', ',', 'how', 'are', 'you', '?']
2.2 并行计算所有 Token
在 Prefill 阶段,所有输入 token 可以并行处理:
flowchart TD
subgraph Prefill 并行处理
I[输入: token_ids<br/>[1, 15043, 29892, 920, 526, 366]]
E[Embedding Layer<br/>并行查表]
PE[Position Encoding<br/>添加位置信息]
subgraph Transformer Layers
L1[Layer 1]
L2[Layer 2]
LN[Layer N]
end
LH[LM Head]
O[Logits<br/>[seq_len, vocab_size]]
I --> E --> PE --> L1 --> L2 --> LN --> LH --> O
end
style E fill:#e3f2fd
style L1 fill:#c8e6c9
style L2 fill:#c8e6c9
style LN fill:#c8e6c92.3 KV Cache 初始化与填充
Prefill 期间,计算并存储所有输入 token 的 K、V:
def prefill(model, input_ids, kv_cache):
"""
input_ids: [batch_size, seq_len]
"""
batch_size, seq_len = input_ids.shape
# Embedding
hidden_states = model.embed_tokens(input_ids) # [batch, seq, hidden]
# 遍历每一层
for layer_idx, layer in enumerate(model.layers):
# 计算 Q, K, V
q = layer.q_proj(hidden_states)
k = layer.k_proj(hidden_states)
v = layer.v_proj(hidden_states)
# 存入 KV Cache
kv_cache.update(layer_idx, k, v)
# 自注意力计算
# ... (使用完整的 K, V,应用因果掩码)
# FFN
# ...
# LM Head
logits = model.lm_head(hidden_states)
# 只返回最后一个位置的 logits(用于预测下一个 token)
return logits[:, -1, :] # [batch, vocab_size]
2.4 生成第一个 Token
使用最后一个位置的 logits 生成第一个输出 token:
def generate_first_token(logits, sampling_params):
"""
logits: [batch_size, vocab_size]
"""
# 应用采样策略
next_token = sample(logits, sampling_params) # [batch_size, 1]
return next_token
3. Decode 阶段详解
3.1 单 Token 增量计算
Decode 阶段每次只处理一个新 token:
flowchart LR
subgraph Decode 增量计算
NT[新 token]
E[Embedding]
Q[计算 Q_new]
KV[计算 K_new, V_new]
Cache[(读取 KV Cache)]
ATT[Attention<br/>Q_new × [K_cache; K_new]ᵀ]
Update[更新 KV Cache]
FFN[FFN]
LM[LM Head]
O[Logits]
NT --> E --> Q
E --> KV
Cache --> ATT
KV --> ATT
Q --> ATT
ATT --> FFN --> LM --> O
KV --> Update --> Cache
end3.2 如何利用 KV Cache
def decode_step(model, new_token_id, kv_cache, position):
"""
new_token_id: [batch_size, 1]
position: 当前位置索引
"""
# Embedding
hidden_states = model.embed_tokens(new_token_id) # [batch, 1, hidden]
# 遍历每一层
for layer_idx, layer in enumerate(model.layers):
# 只计算新 token 的 Q, K, V
q_new = layer.q_proj(hidden_states) # [batch, 1, hidden]
k_new = layer.k_proj(hidden_states)
v_new = layer.v_proj(hidden_states)
# 从缓存获取历史 K, V
k_cache, v_cache = kv_cache.get(layer_idx)
# 合并:[k_cache, k_new] 和 [v_cache, v_new]
k_full = torch.cat([k_cache, k_new], dim=2)
v_full = torch.cat([v_cache, v_new], dim=2)
# 更新缓存
kv_cache.update(layer_idx, k_new, v_new)
# 注意力计算:Q_new (1个) 与 K_full (N+1个)
# scores: [batch, heads, 1, N+1]
scores = (q_new @ k_full.transpose(-2, -1)) / sqrt(head_dim)
# 无需因果掩码(新 token 可以看到所有历史)
attn_weights = F.softmax(scores, dim=-1)
# 加权求和
attn_output = attn_weights @ v_full # [batch, heads, 1, head_dim]
# ... FFN 等
# LM Head
logits = model.lm_head(hidden_states) # [batch, 1, vocab_size]
return logits.squeeze(1) # [batch, vocab_size]
3.3 Decode 循环
def decode_loop(model, first_token, kv_cache, max_tokens, stop_token_id):
"""完整的 decode 循环"""
generated_tokens = [first_token]
current_token = first_token
position = kv_cache.current_len
for step in range(max_tokens):
# 执行一步 decode
logits = decode_step(model, current_token, kv_cache, position)
# 采样下一个 token
next_token = sample(logits, sampling_params)
# 检查停止条件
if next_token == stop_token_id:
break
generated_tokens.append(next_token)
current_token = next_token
position += 1
return generated_tokens
4. 采样策略详解
4.1 从 Logits 到概率分布
模型输出的是 logits(未归一化的分数),需要转换为概率分布:
# logits: [vocab_size]
# 例如: [-1.2, 0.5, 2.3, -0.1, ...]
# 转换为概率
probs = F.softmax(logits, dim=-1)
# probs: [0.01, 0.05, 0.30, 0.03, ...] 和为 1
4.2 Greedy Decoding(贪婪解码)
最简单的策略:每次选择概率最高的 token。
def greedy_decode(logits):
return torch.argmax(logits, dim=-1)
特点:
- 确定性(相同输入总是相同输出)
- 可能陷入重复
- 不适合创意生成
4.3 Temperature(温度)
Temperature 控制概率分布的"尖锐"程度:
def apply_temperature(logits, temperature):
return logits / temperature
graph LR
subgraph Temperature 效果
T1[T=0.1<br/>非常尖锐<br/>几乎是 Greedy]
T2[T=1.0<br/>原始分布]
T3[T=2.0<br/>更平滑<br/>更随机]
end| Temperature | 效果 | 适用场景 |
|---|
| < 1.0 | 更确定,偏向高概率 | 事实性回答 |
| = 1.0 | 原始分布 | 一般场景 |
| > 1.0 | 更随机,更多样 | 创意写作 |
4.4 Top-k Sampling
只从概率最高的 k 个 token 中采样:
def top_k_sampling(logits, k):
# 找到 top-k 的值和索引
top_k_logits, top_k_indices = torch.topk(logits, k)
# 将其他位置设为 -inf
filtered_logits = torch.full_like(logits, float('-inf'))
filtered_logits.scatter_(-1, top_k_indices, top_k_logits)
# 重新计算概率并采样
probs = F.softmax(filtered_logits, dim=-1)
return torch.multinomial(probs, num_samples=1)
示例(k=3):
原始概率: [0.40, 0.30, 0.15, 0.10, 0.05]
Top-3: [0.40, 0.30, 0.15, 0.00, 0.00]
归一化后: [0.47, 0.35, 0.18, 0.00, 0.00]
4.5 Top-p (Nucleus) Sampling
选择累积概率达到 p 的最小 token 集合:
def top_p_sampling(logits, p):
# 排序
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
probs = F.softmax(sorted_logits, dim=-1)
# 计算累积概率
cumsum_probs = torch.cumsum(probs, dim=-1)
# 找到累积概率 > p 的位置
sorted_indices_to_remove = cumsum_probs > p
# 保留第一个超过阈值的
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
sorted_indices_to_remove[..., 0] = False
# 过滤
sorted_logits[sorted_indices_to_remove] = float('-inf')
# 采样
probs = F.softmax(sorted_logits, dim=-1)
return torch.multinomial(probs, num_samples=1)
示例(p=0.9):
排序后概率: [0.40, 0.30, 0.15, 0.10, 0.05]
累积概率: [0.40, 0.70, 0.85, 0.95, 1.00]
↑ 超过 0.9
保留: [0.40, 0.30, 0.15, 0.10] 累积 = 0.95
4.6 采样策略对比
graph TD
subgraph 采样策略选择
G[Greedy<br/>确定性、可能重复]
TK[Top-k<br/>固定数量的候选]
TP[Top-p<br/>动态数量的候选]
T[Temperature<br/>控制随机程度]
G --> |适合| F[事实问答]
TK --> |适合| C1[通用对话]
TP --> |适合| C2[创意写作]
T --> |配合| TK
T --> |配合| TP
end4.7 常用参数组合
| 场景 | Temperature | Top-p | Top-k |
|---|
| 代码生成 | 0.1-0.3 | - | - |
| 事实问答 | 0.0-0.5 | 0.9 | - |
| 通用对话 | 0.7-0.9 | 0.9 | 40 |
| 创意写作 | 1.0-1.2 | 0.95 | 50 |
| 脑暴创意 | 1.5-2.0 | 0.98 | 100 |
5. 停止条件
5.1 常见停止条件
def check_stop_condition(token_id, generated_tokens, params):
# 1. 生成了 EOS token
if token_id == params.eos_token_id:
return True, "EOS"
# 2. 达到最大长度
if len(generated_tokens) >= params.max_tokens:
return True, "MAX_LENGTH"
# 3. 遇到停止字符串
text = tokenizer.decode(generated_tokens)
for stop_str in params.stop_strings:
if stop_str in text:
return True, "STOP_STRING"
return False, None
5.2 vLLM 中的停止条件
# vllm/sampling_params.py
class SamplingParams:
max_tokens: int = 16 # 最大生成 token 数
stop: List[str] = [] # 停止字符串
stop_token_ids: List[int] = [] # 停止 token ID
include_stop_str_in_output: bool = False
ignore_eos: bool = False # 是否忽略 EOS
6. 计算特性对比
6.1 Prefill vs Decode
graph LR
subgraph Prefill
P1[处理 N 个 tokens]
P2[计算量: O(N² × d)]
P3[内存访问: O(N × d)]
P4[计算密度: 高]
end
subgraph Decode
D1[处理 1 个 token]
D2[计算量: O(N × d)]
D3[内存访问: O(N × d)]
D4[计算密度: 低]
end| 特性 | Prefill | Decode |
|---|
| 每次处理 tokens | N | 1 |
| Attention 计算 | Q[N] × K[N]ᵀ | Q[1] × K[N]ᵀ |
| 计算量 | O(N²d) | O(Nd) |
| 内存读取 | 模型权重 | 模型权重 + KV Cache |
| 计算/访存比 | 高 | 低 |
| GPU 利用率 | 50-80% | 10-30% |
| 瓶颈 | 计算 | 内存带宽 |
6.2 GPU 利用率可视化
gantt
title GPU 利用率时间线
dateFormat X
axisFormat %s
section GPU 计算
Prefill (高利用率) :done, p, 0, 20
Decode Step 1 (低利用率) :crit, d1, 20, 25
Decode Step 2 (低利用率) :crit, d2, 25, 30
Decode Step 3 (低利用率) :crit, d3, 30, 35
...更多 decode steps :crit, dn, 35, 806.3 批处理的重要性
单独处理一个 decode step 时,GPU 大部分时间在等待数据传输。通过批处理多个请求,可以提高 GPU 利用率:
# 单请求
def decode_single(request):
read_weights() # 14GB
process_1_token() # 很小的计算量
# GPU 大部分时间空闲
# 批处理
def decode_batch(requests, batch_size=32):
read_weights() # 14GB(只读一次)
process_32_tokens() # 32 倍的计算量
# GPU 利用率提高 32 倍
7. 完整生成示例
7.1 代码示例
def generate(model, tokenizer, prompt, max_tokens=100, temperature=0.8, top_p=0.9):
# 1. Tokenization
input_ids = tokenizer.encode(prompt, return_tensors='pt').cuda()
# 2. 初始化 KV Cache
kv_cache = KVCache(model.config)
kv_cache.allocate(batch_size=1)
# 3. Prefill 阶段
logits = prefill(model, input_ids, kv_cache)
# 4. 采样第一个 token
sampling_params = SamplingParams(temperature=temperature, top_p=top_p)
first_token = sample(logits, sampling_params)
generated_tokens = [first_token.item()]
# 5. Decode 循环
current_token = first_token
for _ in range(max_tokens - 1):
# Decode 一步
logits = decode_step(model, current_token, kv_cache)
# 采样
next_token = sample(logits, sampling_params)
# 检查停止条件
if next_token.item() == tokenizer.eos_token_id:
break
generated_tokens.append(next_token.item())
current_token = next_token
# 6. Detokenization
output_text = tokenizer.decode(generated_tokens)
return output_text
# 使用
output = generate(model, tokenizer, "Once upon a time", max_tokens=50)
print(output)
7.2 时序图
sequenceDiagram
participant T as Tokenizer
participant P as Prefill
participant D as Decode
participant S as Sampler
participant C as KV Cache
Note over T,C: 输入: "Hello"
T->>P: token_ids = [1, 15043]
P->>C: 初始化缓存
P->>C: 存储 K[0:2], V[0:2]
P->>S: logits
S->>D: token_id = 318 ("I")
loop Decode 循环
D->>C: 读取 K[0:n], V[0:n]
D->>C: 写入 K[n], V[n]
D->>S: logits
S->>D: next_token
end
Note over T,C: 输出: "I am fine"
8. 本章小结
生成流程
- Tokenization:文本 → Token IDs
- Prefill:并行处理输入,初始化 KV Cache
- Decode:逐个生成 token,增量更新 KV Cache
- Sampling:从 logits 采样 token
- Detokenization:Token IDs → 文本
两阶段特性
| 阶段 | Prefill | Decode |
|---|
| 并行度 | 高 | 低(每次 1 token) |
| 计算密度 | 高 | 低 |
| 瓶颈 | 计算 | 内存带宽 |
| 优化重点 | 并行计算 | 批处理 |
采样策略
- Greedy:确定性,取最大概率
- Temperature:控制随机程度
- Top-k:限制候选数量
- Top-p:动态限制累积概率
与 vLLM 的关联
- Continuous Batching:动态组合 Prefill 和 Decode
- Chunked Prefill:分块处理长输入
- 采样优化:批量采样提高效率
思考题
- 为什么 Decode 阶段不能像 Prefill 那样并行处理多个 token?
- 如果使用 temperature=0,结果会和 greedy decoding 一样吗?
- vLLM 的 Continuous Batching 如何同时处理 Prefill 和 Decode 请求?
下一步
深度学习基础部分已经完成!接下来我们将进入核心模块详解,首先介绍 vLLM 的核心创新——PagedAttention:
👉 下一章:PagedAttention 分页注意力
3 - 核心模块详解
深入理解 vLLM 的核心创新和实现
本部分将深入讲解 vLLM 的核心技术创新,包括 PagedAttention、KV Cache 管理、调度器和连续批处理等关键模块。
3.1 - PagedAttention 分页注意力
PagedAttention 分页注意力
本章将详细介绍 vLLM 的核心创新——PagedAttention,包括设计思想、数据结构和实现原理。
引言
PagedAttention 是 vLLM 最重要的创新,它借鉴了操作系统虚拟内存管理的思想,革命性地解决了 KV Cache 的显存浪费问题。本章将深入剖析其设计原理和实现细节。
1. 传统 KV Cache 的问题回顾
1.1 连续内存分配的要求
传统方案要求每个请求的 KV Cache 存储在连续的内存空间中:
传统 KV Cache 布局:
+----------------------------------------------------------+
| Request A 的 KV Cache (预分配 max_seq_len) |
| [K0,V0][K1,V1][K2,V2]...[Kn,Vn][ 空闲预留空间 ] |
+----------------------------------------------------------+
| Request B 的 KV Cache (预分配 max_seq_len) |
| [K0,V0][K1,V1]...[Km,Vm][ 空闲预留空间 ] |
+----------------------------------------------------------+
1.2 显存碎片化图解
当多个请求并发时,显存碎片化问题严重:
graph TB
subgraph 时间 T1: 三个请求开始
M1[Request A<br/>预分配 2GB<br/>实际用 0.1GB]
M2[Request B<br/>预分配 2GB<br/>实际用 0.2GB]
M3[Request C<br/>预分配 2GB<br/>实际用 0.1GB]
M4[空闲 2GB]
end
subgraph 时间 T2: Request B 完成
N1[Request A<br/>预分配 2GB<br/>实际用 0.5GB]
N2[空洞 2GB<br/>外部碎片]
N3[Request C<br/>预分配 2GB<br/>实际用 0.3GB]
N4[空闲 2GB]
end
subgraph 时间 T3: 新请求 D 到来
O1[Request A<br/>2GB]
O2[空洞 2GB]
O3[Request C<br/>2GB]
O4[空闲 2GB]
O5[Request D 需要 3GB<br/>失败!]
end
style N2 fill:#ffcdd2
style O2 fill:#ffcdd2
style O5 fill:#ffcdd21.3 量化浪费
| 问题类型 | 说明 | 浪费比例 |
|---|
| 内部碎片 | 预分配 » 实际使用 | 40-60% |
| 外部碎片 | 空洞无法利用 | 20-30% |
| 总计 | 综合浪费 | 60-80% |
2. PagedAttention 核心思想
2.1 灵感来源:操作系统虚拟内存
操作系统如何管理内存?
graph LR
subgraph 虚拟内存
VA[虚拟地址空间<br/>程序看到的连续空间]
end
subgraph 页表
PT[Page Table<br/>虚拟页 → 物理页]
end
subgraph 物理内存
PA[物理内存<br/>实际不连续的页]
end
VA --> PT --> PA关键特性:
- 程序看到连续的地址空间
- 物理内存可以不连续
- 按需分配(用到才分配)
- 页面可以共享
2.2 PagedAttention 的类比
将操作系统的思想应用到 KV Cache 管理:
| 操作系统概念 | PagedAttention 对应 |
|---|
| 页(Page) | Block(块) |
| 页表(Page Table) | Block Table(块表) |
| 虚拟地址 | 逻辑块索引 |
| 物理地址 | 物理块 ID |
| 页帧 | KV Cache 块 |
2.3 核心改进
graph LR
subgraph 传统方案
T1[预分配连续空间]
T2[大量浪费]
T1 --> T2
end
subgraph PagedAttention
P1[按需分配]
P2[分块存储]
P3[非连续]
P4[高利用率]
P1 --> P2 --> P3 --> P4
end
style T2 fill:#ffcdd2
style P4 fill:#c8e6c9
3. 关键数据结构详解
3.1 Block(块)
Block 是 KV Cache 的基本存储单元:
# 概念定义
class KVCacheBlock:
block_id: int # 物理块 ID
ref_cnt: int # 引用计数(支持共享)
block_hash: Optional[int] # 用于前缀缓存匹配
Block 的特点:
- 固定大小:每个 block 存储固定数量的 token(如 16 个)
- 独立分配:不需要连续
- 可复用:释放后可分配给其他请求
3.2 Block 的存储内容
每个 Block 存储若干 token 的 K 和 V:
Block 结构 (block_size = 16):
┌─────────────────────────────────────────────────┐
│ Token 0: K[layers, heads, head_dim] │
│ V[layers, heads, head_dim] │
├─────────────────────────────────────────────────┤
│ Token 1: K[layers, heads, head_dim] │
│ V[layers, heads, head_dim] │
├─────────────────────────────────────────────────┤
│ ... │
├─────────────────────────────────────────────────┤
│ Token 15: K[layers, heads, head_dim] │
│ V[layers, heads, head_dim] │
└─────────────────────────────────────────────────┘
实际存储形状:
# 单个 Block 的 KV Cache 形状
k_block = torch.zeros(num_layers, num_heads, block_size, head_dim)
v_block = torch.zeros(num_layers, num_heads, block_size, head_dim)
# 整个 KV Cache 池
kv_cache = torch.zeros(num_blocks, 2, num_layers, num_heads, block_size, head_dim)
3.3 Block Table(块表)
Block Table 记录逻辑块到物理块的映射:
classDiagram
class BlockTable {
+block_ids: List[int]
+num_blocks: int
+append(block_id)
+get_physical_block(logical_idx) int
}示例:
Request A 的 Block Table:
逻辑块索引: 0 1 2 3
↓ ↓ ↓ ↓
物理块 ID: [5] [2] [8] [12]
解释:
- 逻辑块 0 → 物理块 5
- 逻辑块 1 → 物理块 2
- 逻辑块 2 → 物理块 8
- 逻辑块 3 → 物理块 12
3.4 Slot Mapping(槽位映射)
Slot Mapping 将 token 位置映射到具体的缓存槽位:
def get_slot_mapping(token_position, block_size, block_table):
"""
token_position: token 在序列中的位置(如 35)
block_size: 每个 block 的 token 数(如 16)
block_table: 块表
"""
logical_block_idx = token_position // block_size # 35 // 16 = 2
block_offset = token_position % block_size # 35 % 16 = 3
physical_block_id = block_table[logical_block_idx] # 假设是 8
slot_id = physical_block_id * block_size + block_offset # 8 * 16 + 3 = 131
return slot_id
图解:
graph LR
subgraph Token 位置 35
T[token_position = 35]
end
subgraph 计算
LB[逻辑块 = 35 // 16 = 2]
OFF[偏移 = 35 % 16 = 3]
PB[物理块 = block_table[2] = 8]
SLOT[slot = 8 × 16 + 3 = 131]
end
T --> LB --> PB
T --> OFF --> SLOT
PB --> SLOT
4. 内存管理优势
4.1 减少显存碎片
graph TB
subgraph 传统方案
A1[Request A: 2GB 预分配<br/>实际 0.1GB]
A2[Request B: 2GB 预分配<br/>实际 0.2GB]
A3[Request C: 2GB 预分配<br/>实际 0.1GB]
A4[空洞和碎片]
end
subgraph PagedAttention
B1[Block Pool<br/>统一管理所有块]
B2[Request A: 2 blocks]
B3[Request B: 4 blocks]
B4[Request C: 2 blocks]
B5[空闲块可立即复用]
end
style A4 fill:#ffcdd2
style B5 fill:#c8e6c94.2 按需分配
sequenceDiagram
participant R as Request
participant S as Scheduler
participant BP as BlockPool
R->>S: 开始生成(0 tokens)
S->>BP: 分配 1 个 block
BP-->>S: Block 5
loop 每 16 个 token
R->>S: 需要新空间
S->>BP: 分配 1 个 block
BP-->>S: Block N
end
R->>S: 生成完成
S->>BP: 释放所有 blocks
Note over BP: 块立即可用于其他请求4.3 支持 Copy-on-Write
当多个请求共享相同前缀时,可以共享 Block:
graph TB
subgraph 共享场景
P[共同前缀<br/>'System prompt...']
end
subgraph 共享的 Blocks
B1[Block 0]
B2[Block 1]
B3[Block 2]
end
subgraph Request A
A[继续生成 A 的内容]
AB[Block 10]
end
subgraph Request B
B[继续生成 B 的内容]
BB[Block 15]
end
P --> B1
P --> B2
P --> B3
B3 --> AB
B3 --> BB
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9引用计数:
- Block 0, 1, 2 的 ref_cnt = 2(被两个请求共享)
- 只有当 ref_cnt = 0 时才真正释放
4.4 支持前缀缓存
相同前缀的请求可以直接复用已计算的 KV Cache:
# 前缀缓存示例
request_1 = "你好,请问" + "天气怎么样?"
request_2 = "你好,请问" + "今天星期几?"
# "你好,请问" 的 KV Cache 可以复用!
5. PagedAttention 计算流程
5.1 写入 KV Cache
sequenceDiagram
participant M as Model
participant PA as PagedAttention
participant BT as Block Table
participant KVC as KV Cache Memory
M->>PA: 新 token 的 K, V
PA->>BT: 查询 slot_mapping
BT-->>PA: slot_id = 131
PA->>KVC: kv_cache[131] = (K, V)5.2 读取并计算 Attention
flowchart TD
subgraph 输入
Q[Query: 新 token 的 Q]
BT[Block Table: 物理块列表]
end
subgraph PagedAttention 计算
FETCH[从各个物理块获取 K, V]
COMPUTE[计算 Attention<br/>Q × K^T / √d]
SOFTMAX[Softmax]
WEIGHTED[加权求和 V]
end
subgraph 输出
O[Attention 输出]
end
Q --> COMPUTE
BT --> FETCH
FETCH --> COMPUTE
COMPUTE --> SOFTMAX
SOFTMAX --> WEIGHTED
WEIGHTED --> O5.3 代码实现概览
# vllm/v1/attention/ops/paged_attn.py (简化版)
class PagedAttention:
@staticmethod
def write_to_paged_cache(
key: torch.Tensor, # [num_tokens, num_heads, head_dim]
value: torch.Tensor, # [num_tokens, num_heads, head_dim]
key_cache: torch.Tensor, # [num_blocks, block_size, num_heads, head_dim]
value_cache: torch.Tensor, # [num_blocks, block_size, num_heads, head_dim]
slot_mapping: torch.Tensor, # [num_tokens]
):
"""将新的 K, V 写入缓存"""
# 使用 slot_mapping 确定写入位置
# slot_mapping[i] 告诉我们 token i 应该写入哪个槽位
pass
@staticmethod
def forward(
query: torch.Tensor, # [num_tokens, num_heads, head_dim]
key_cache: torch.Tensor, # KV Cache
value_cache: torch.Tensor,
block_tables: torch.Tensor, # [batch, max_blocks] 块表
context_lens: torch.Tensor, # [batch] 每个请求的上下文长度
...
) -> torch.Tensor:
"""执行 PagedAttention 计算"""
# 1. 根据 block_tables 定位 K, V
# 2. 计算 Attention
# 3. 返回输出
pass
6. 块的动态管理
6.1 块的生命周期
stateDiagram-v2
[*] --> Free: 初始化
Free --> Allocated: 分配给请求
Allocated --> Free: 请求完成<br/>ref_cnt=0
Allocated --> Cached: 启用前缀缓存
Cached --> Allocated: 缓存命中<br/>ref_cnt++
Cached --> Free: LRU 驱逐<br/>或缓存失效
note right of Allocated : ref_cnt >= 1
note right of Cached : 保留以备复用6.2 块分配流程
def allocate_blocks_for_request(request, kv_cache_manager):
"""为请求分配所需的 blocks"""
num_tokens = len(request.prompt_tokens) + request.num_generated_tokens
num_blocks_needed = (num_tokens + block_size - 1) // block_size
blocks = []
for i in range(num_blocks_needed):
# 尝试获取空闲块
block = kv_cache_manager.get_free_block()
if block is None:
# 没有空闲块,触发驱逐或返回失败
return None
blocks.append(block)
# 更新请求的块表
request.block_table = blocks
return blocks
6.3 块增长过程
gantt
title 请求的块分配时间线
dateFormat X
axisFormat %s
section 块分配
Block 0 (prefill) :done, b0, 0, 16
Block 1 (prefill 续) :done, b1, 16, 32
Block 2 (decode) :active, b2, 32, 48
Block 3 (decode 续) :active, b3, 48, 64
7. CUDA 内核实现
7.1 文件位置
- Python 接口:
vllm/v1/attention/ops/paged_attn.py - CUDA 内核:
csrc/attention/paged_attention_v1.cu、paged_attention_v2.cu
7.2 V1 vs V2 内核
| 特性 | V1 | V2 |
|---|
| 适用场景 | 短序列 | 长序列 |
| 分块策略 | 简单 | 两级分块 |
| 性能 | 中等 | 更优 |
7.3 内核参数
// paged_attention_v2.cu (简化)
template<typename T, int BLOCK_SIZE, int NUM_THREADS>
__global__ void paged_attention_v2_kernel(
T* __restrict__ out, // 输出
const T* __restrict__ q, // Query
const T* __restrict__ k_cache, // Key Cache
const T* __restrict__ v_cache, // Value Cache
const int* __restrict__ block_tables, // 块表
const int* __restrict__ context_lens, // 上下文长度
const float scale, // 缩放因子
...
) {
// 1. 确定当前线程处理的 query
// 2. 遍历 block_table 中的所有块
// 3. 计算 Attention 分数
// 4. Softmax 和加权求和
}
8. 性能对比
8.1 显存效率
| 方案 | 显存利用率 | 最大并发 |
|---|
| 传统预分配 | 20-40% | 低 |
| PagedAttention | 96%+ | 高 2-4 倍 |
8.2 吞吐量提升
graph LR
subgraph 吞吐量对比
T1[传统方案<br/>1x 基准]
T2[PagedAttention<br/>2-4x 提升]
end
style T2 fill:#c8e6c98.3 碎片率
传统方案:
- 内部碎片: 50-70%
- 外部碎片: 10-20%
- 总碎片: 60-80%
PagedAttention:
- 内部碎片: < 4% (最后一个块)
- 外部碎片: 0% (固定大小块)
- 总碎片: < 4%
9. 本章小结
核心创新
- 分块存储:将 KV Cache 分成固定大小的 Block
- 非连续分配:Block 可以分散在显存任意位置
- 按需分配:生成新 token 时才分配新 Block
- 块表映射:通过 Block Table 管理逻辑到物理的映射
关键数据结构
| 结构 | 作用 |
|---|
| Block | KV Cache 的基本存储单元 |
| Block Table | 逻辑块 → 物理块映射 |
| Slot Mapping | Token 位置 → 缓存槽位 |
| BlockPool | 管理所有空闲块 |
优势总结
- 显存效率:从 20-40% 提升到 96%+
- 减少碎片:从 60-80% 降到 4% 以下
- 支持共享:多请求可共享相同前缀的 Block
- 按需增长:不需要预分配最大长度
代码位置
| 功能 | 文件 |
|---|
| Python 接口 | vllm/v1/attention/ops/paged_attn.py |
| CUDA 内核 | csrc/attention/paged_attention_v2.cu |
| 块管理 | vllm/v1/core/block_pool.py |
| 块表 | vllm/v1/worker/block_table.py |
思考题
- 为什么选择固定大小的 Block 而不是可变大小?
- 前缀缓存和 Copy-on-Write 有什么区别和联系?
- 如果 block_size 设置得太大或太小,会有什么影响?
下一步
了解了 PagedAttention 的原理后,让我们来看看 KV Cache Manager 是如何管理这些 Block 的:
👉 下一章:KV Cache 管理器
3.2 - KV Cache 管理器
KV Cache 管理器详解
在上一章中,我们详细了解了 PagedAttention 的原理——它将 KV Cache 分成固定大小的 Block 来管理。但是,谁来负责这些 Block 的分配、释放和缓存查找呢? 答案就是本章的主角:KVCacheManager。
KVCacheManager 是 vLLM 内存管理的核心组件,它连接了调度器(Scheduler)和底层的内存池(BlockPool),为每个请求提供高效的 KV Cache 管理服务。
1. KVCacheManager 在架构中的位置
graph TD
subgraph 调度层
Scheduler[Scheduler<br/>调度器]
end
subgraph 内存管理层
KVM[KVCacheManager<br/>KV Cache 管理器]
Coord[KVCacheCoordinator<br/>协调器]
BP[BlockPool<br/>内存块池]
end
subgraph 执行层
Runner[GPUModelRunner<br/>模型执行器]
end
Scheduler -->|"allocate_slots()"| KVM
Scheduler -->|"get_computed_blocks()"| KVM
Scheduler -->|"free()"| KVM
KVM --> Coord
Coord --> BP
KVM -.->|"block_ids"| Runner
style KVM fill:#e1f5fe
style Scheduler fill:#fff3e0
style BP fill:#e8f5e9KVCacheManager 的核心职责:
- 分配管理:为请求分配 KV Cache 槽位
- 前缀缓存:查找和利用已缓存的前缀块
- 生命周期管理:跟踪和释放请求的内存块
- 使用率监控:提供 KV Cache 使用统计
2. 核心数据结构
2.1 KVCacheBlocks - 分配结果
KVCacheBlocks 是 KVCacheManager 返回给调度器的分配结果,它封装了实际的内存块列表:
# vllm/v1/core/kv_cache_manager.py
@dataclass
class KVCacheBlocks:
"""
KVCacheManager 的分配结果,作为 Scheduler 和 KVCacheManager
之间的接口,隐藏 KVCacheManager 的内部数据结构。
"""
blocks: tuple[Sequence[KVCacheBlock], ...]
"""
blocks[i][j] 表示第 i 个 kv_cache_group 的第 j 个 block。
使用 kv_cache_group 作为外层维度,因为不同 group 可能有
不同数量的 blocks(为未来扩展预留)。
"""
def get_block_ids(self) -> tuple[list[int], ...]:
"""将 KVCacheBlocks 转换为 block_ids 列表"""
return tuple([blk.block_id for blk in group]
for group in self.blocks)
为什么需要这个封装?
- 接口隔离:调度器不需要知道内部的 Block 结构细节
- GC 优化:预分配空的 KVCacheBlocks 避免频繁创建对象
- 多 Group 支持:为混合精度等场景预留扩展能力
2.2 KVCacheManager 类结构
# vllm/v1/core/kv_cache_manager.py
class KVCacheManager:
def __init__(
self,
kv_cache_config: KVCacheConfig,
max_model_len: int,
hash_block_size: int,
enable_caching: bool = True, # 是否启用前缀缓存
use_eagle: bool = False, # 是否使用 EAGLE 投机解码
log_stats: bool = False, # 是否记录统计信息
...
):
self.max_model_len = max_model_len
self.enable_caching = enable_caching
# 核心组件:协调器(封装了 BlockPool)
self.coordinator = get_kv_cache_coordinator(
kv_cache_config=kv_cache_config,
max_model_len=self.max_model_len,
enable_caching=self.enable_caching,
...
)
# BlockPool 引用(实际内存管理)
self.block_pool = self.coordinator.block_pool
# 预分配的空 KVCacheBlocks(避免 GC 开销)
self.empty_kv_cache_blocks = KVCacheBlocks(
tuple(() for _ in range(self.num_kv_cache_groups))
)
组件关系图:
classDiagram
class KVCacheManager {
+max_model_len: int
+enable_caching: bool
+coordinator: KVCacheCoordinator
+block_pool: BlockPool
+empty_kv_cache_blocks: KVCacheBlocks
+get_computed_blocks(request) KVCacheBlocks, int
+allocate_slots(request, num_new_tokens, ...) KVCacheBlocks
+free(request) void
+usage: float
}
class KVCacheCoordinator {
+block_pool: BlockPool
+find_longest_cache_hit() blocks, num_tokens
+allocate_new_blocks() blocks
+cache_blocks() void
+free() void
}
class BlockPool {
+blocks: list~KVCacheBlock~
+free_block_queue: FreeKVCacheBlockQueue
+cached_block_hash_to_block: BlockHashToBlockMap
+get_new_blocks() blocks
+free_blocks() void
+touch() void
}
KVCacheManager --> KVCacheCoordinator : uses
KVCacheCoordinator --> BlockPool : manages
3. 核心方法详解
3.1 get_computed_blocks() - 查找前缀缓存
当新请求到来时,调度器首先调用 get_computed_blocks() 检查是否有可复用的缓存:
def get_computed_blocks(self, request: Request) -> tuple[KVCacheBlocks, int]:
"""
获取请求的已计算(缓存)块。
Returns:
- KVCacheBlocks: 命中的缓存块列表
- int: 已计算的 token 数量
"""
# 情况1:禁用缓存或请求明确跳过缓存
if not self.enable_caching or request.skip_reading_prefix_cache:
return self.empty_kv_cache_blocks, 0
# 情况2:查找最长前缀匹配
# 注意:即使全部命中,也需要保留最后一个 token 用于计算 logits
max_cache_hit_length = request.num_tokens - 1
computed_blocks, num_new_computed_tokens = (
self.coordinator.find_longest_cache_hit(
request.block_hashes, # 请求的 block hash 列表
max_cache_hit_length # 最大命中长度
)
)
# 记录统计信息
if self.log_stats:
self.prefix_cache_stats.record(
num_tokens=request.num_tokens,
num_hits=num_new_computed_tokens,
preempted=request.num_preemptions > 0,
)
return self.create_kv_cache_blocks(computed_blocks), num_new_computed_tokens
缓存查找流程图:
flowchart TD
A[开始 get_computed_blocks] --> B{启用缓存?}
B -->|否| C[返回空块, 0]
B -->|是| D{请求跳过缓存?}
D -->|是| C
D -->|否| E[计算 block_hashes]
E --> F[find_longest_cache_hit]
F --> G[遍历 hash 查找缓存]
G --> H{找到匹配块?}
H -->|是| I[增加引用计数]
I --> J[继续查找下一个]
H -->|否| K[停止查找]
J --> H
K --> L[返回缓存块和命中数]
style F fill:#e8f5e9
style L fill:#e1f5fe关键点:为什么 max_cache_hit_length = num_tokens - 1?
输入: "Hello, how are you?"
Token IDs: [101, 102, 103, 104, 105] (5个token)
假设全部命中缓存:
- 前4个token的KV已缓存 ✓
- 但我们仍需要"重新计算"最后1个token
- 因为我们需要得到最后位置的 logits 来采样下一个 token
所以最多只能利用 num_tokens - 1 = 4 个缓存
3.2 allocate_slots() - 分配缓存槽位
这是 KVCacheManager 最核心的方法,负责为请求分配 KV Cache 存储空间:
def allocate_slots(
self,
request: Request,
num_new_tokens: int, # 新生成的 token 数
num_new_computed_tokens: int = 0, # 前缀缓存命中的 token 数
new_computed_blocks: KVCacheBlocks | None = None, # 缓存命中的块
num_lookahead_tokens: int = 0, # 投机解码的预测 token 数
num_external_computed_tokens: int = 0, # 外部(如 P/D)计算的 token
delay_cache_blocks: bool = False, # 是否延迟缓存
num_encoder_tokens: int = 0, # 编码器 token(如 Whisper)
) -> KVCacheBlocks | None:
"""
为请求分配新的 KV Cache 槽位。
Returns:
- KVCacheBlocks: 新分配的块
- None: 内存不足,分配失败
"""
块布局说明:
----------------------------------------------------------------------
| < comp > | < new_comp > | < ext_comp > | < new > | < lookahead > |
----------------------------------------------------------------------
| < 需要计算的 > |
----------------------------------------------------------------------
| < 需要分配的 > |
----------------------------------------------------------------------
| < 需要缓存的 > |
----------------------------------------------------------------------
术语说明:
comp = 已计算的 token(request.num_computed_tokens)
new_comp = 新命中缓存的 token(前缀缓存)
ext_comp = 外部计算的 token(如 P/D disaggregation)
new = 新生成的 token
lookahead = 投机解码的预测 token
allocate_slots 核心流程:
def allocate_slots(self, request, num_new_tokens, ...):
# 1. 计算需要的总 token 数
num_local_computed_tokens = request.num_computed_tokens + num_new_computed_tokens
total_computed_tokens = num_local_computed_tokens + num_external_computed_tokens
num_tokens_need_slot = total_computed_tokens + num_new_tokens + num_lookahead_tokens
# 2. 释放滑动窗口外的块(如果有)
# 这可以回收一些空间,减少需要新分配的块
self.coordinator.remove_skipped_blocks(
request.request_id, total_computed_tokens
)
# 3. 计算需要分配的新块数
num_blocks_to_allocate = self.coordinator.get_num_blocks_to_allocate(
request_id=request.request_id,
num_tokens=num_tokens_need_slot,
new_computed_blocks=new_computed_block_list,
...
)
# 4. 检查是否有足够的空闲块
if num_blocks_to_allocate > self.block_pool.get_num_free_blocks():
return None # 内存不足!
# 5. 处理前缀缓存命中的块(增加引用计数)
if new_computed_block_list or num_external_computed_tokens > 0:
self.coordinator.allocate_new_computed_blocks(
request_id=request.request_id,
new_computed_blocks=new_computed_block_list,
...
)
# 6. 分配新块
new_blocks = self.coordinator.allocate_new_blocks(
request.request_id,
num_tokens_need_slot,
num_tokens_main_model,
num_encoder_tokens,
)
# 7. 缓存新的满块(用于后续请求复用)
if self.enable_caching and not delay_cache_blocks:
self.coordinator.cache_blocks(request, num_tokens_to_cache)
return self.create_kv_cache_blocks(new_blocks)
完整流程图:
flowchart TD
A[allocate_slots 开始] --> B[计算总 token 数]
B --> C[remove_skipped_blocks<br/>释放滑动窗口外的块]
C --> D[get_num_blocks_to_allocate<br/>计算需要的新块数]
D --> E{空闲块足够?}
E -->|否| F[返回 None<br/>分配失败]
E -->|是| G{有缓存命中?}
G -->|是| H[allocate_new_computed_blocks<br/>处理缓存块]
G -->|否| I[allocate_new_blocks<br/>分配新块]
H --> I
I --> J{启用缓存?}
J -->|是| K[cache_blocks<br/>缓存满块]
J -->|否| L[返回新分配的块]
K --> L
style F fill:#ffcdd2
style L fill:#c8e6c93.3 free() - 释放请求资源
当请求完成或被抢占时,需要释放其占用的 KV Cache:
def free(self, request: Request) -> None:
"""
释放请求占用的块。
按逆序释放,这样当启用缓存时,尾部块会先被驱逐。
"""
self.coordinator.free(request.request_id)
释放流程:
sequenceDiagram
participant Scheduler
participant KVM as KVCacheManager
participant Coord as Coordinator
participant BP as BlockPool
Note over Scheduler: 请求完成或被抢占
Scheduler->>KVM: free(request)
KVM->>Coord: free(request_id)
Coord->>Coord: 获取请求的所有块
Coord->>Coord: 按逆序排列块
Coord->>BP: free_blocks(ordered_blocks)
loop 每个块
BP->>BP: block.ref_cnt -= 1
alt ref_cnt == 0
BP->>BP: 加入 free_block_queue
end
end
Note over BP: 块可被其他请求复用<br/>或从缓存中驱逐为什么要逆序释放?
假设请求占用块: [Block_A, Block_B, Block_C, Block_D]
↑ ↑ ↑ ↑
前缀 .... .... 尾部
如果另一个请求有相同前缀,它更可能复用 Block_A, Block_B
所以我们希望:
- 先释放 Block_D(尾部,最不可能被复用)
- 后释放 Block_A(前缀,最可能被复用)
这样 LRU 驱逐时,尾部块会先被驱逐,前缀块保留更久
4. 前缀缓存机制详解
前缀缓存(Prefix Caching)是 vLLM 的重要优化,它允许多个请求共享相同前缀的 KV Cache。
4.1 Block Hash 计算
每个 Block 的 hash 值基于其包含的 token 序列计算:
# 简化示意
def compute_block_hash(token_ids: list[int], block_size: int) -> BlockHash:
"""
计算一个 block 的 hash 值。
hash 基于:
1. block 内的 token IDs
2. 之前所有 block 的 hash(形成链式依赖)
"""
# 确保是完整的 block
assert len(token_ids) % block_size == 0
# 使用加密 hash 或高效 hash 算法
return hash(tuple(token_ids))
4.2 缓存查找过程
flowchart TD
subgraph 请求1[请求 1: "Hello, how are you?"]
R1B1[Block 1<br/>hash: abc123]
R1B2[Block 2<br/>hash: def456]
end
subgraph 请求2[请求 2: "Hello, how is weather?"]
R2B1[Block 1<br/>hash: abc123]
R2B2[Block 2<br/>hash: ghi789]
end
subgraph 缓存[Block Hash Cache]
C1[abc123 → Block #5]
C2[def456 → Block #8]
end
R1B1 -.-> C1
R1B2 -.-> C2
R2B1 -.->|命中!| C1
R2B2 -.->|未命中| X[分配新块]
style C1 fill:#c8e6c9
style R2B1 fill:#c8e6c94.3 缓存块的生命周期
stateDiagram-v2
[*] --> Free: 初始化
Free --> Allocated: get_new_blocks()
note right of Allocated: ref_cnt = 1
Allocated --> Cached: cache_full_blocks()
note right of Cached: block_hash 被设置<br/>加入 hash_to_block
Cached --> SharedCached: touch() (另一请求命中)
note right of SharedCached: ref_cnt += 1
SharedCached --> Cached: free() (一个请求释放)
note right of Cached: ref_cnt -= 1
Cached --> Evictable: free() (ref_cnt = 0)
note right of Evictable: 加入 free_block_queue<br/>但保留 hash
Evictable --> Evicted: _maybe_evict_cached_block()
note right of Evicted: hash 被清除<br/>从 hash_to_block 移除
Evicted --> Free: 可重新分配
Evictable --> SharedCached: touch() (再次命中)
note left of SharedCached: 从 free_queue 移除<br/>ref_cnt = 1
5. 与调度器的协作
KVCacheManager 和 Scheduler 紧密协作,下面是典型的调度循环中的交互:
sequenceDiagram
participant S as Scheduler
participant KVM as KVCacheManager
participant BP as BlockPool
Note over S: schedule() 开始
rect rgb(230, 245, 230)
Note over S,BP: 处理新请求
S->>KVM: get_computed_blocks(request)
KVM->>BP: 查找缓存
BP-->>KVM: 缓存块, 命中数
KVM-->>S: KVCacheBlocks, num_computed
S->>KVM: allocate_slots(request, num_tokens, cached_blocks)
KVM->>BP: 检查空闲块
alt 空闲块足够
KVM->>BP: 分配新块
BP-->>KVM: 新块列表
KVM-->>S: KVCacheBlocks
Note over S: 将请求加入 running
else 空闲块不足
KVM-->>S: None
Note over S: 请求继续等待
end
end
rect rgb(255, 245, 230)
Note over S,BP: 处理运行中请求
S->>KVM: allocate_slots(request, 1)
Note over KVM: 为新生成的 token 分配槽位
alt 分配成功
KVM-->>S: KVCacheBlocks
else 需要抢占
KVM-->>S: None
S->>S: 选择抢占目标
S->>KVM: free(victim_request)
KVM->>BP: 释放块
S->>KVM: allocate_slots(request, 1)
KVM-->>S: KVCacheBlocks
end
end
rect rgb(245, 230, 230)
Note over S,BP: 处理完成请求
S->>KVM: free(finished_request)
KVM->>BP: 释放块
Note over BP: 块加入 free_queue<br/>可被复用或驱逐
end
6. 配置与调优
6.1 关键配置参数
| 参数 | 说明 | 建议值 |
|---|
enable_prefix_caching | 是否启用前缀缓存 | True(有重复前缀时) |
block_size | 每个 Block 的 token 数 | 16(默认) |
gpu_memory_utilization | GPU 显存利用率 | 0.9(90%) |
max_model_len | 最大序列长度 | 根据需求设置 |
6.2 性能优化建议
1. 前缀缓存优化
# 适合启用前缀缓存的场景:
# - 多轮对话(共享系统提示)
# - RAG 应用(共享检索文档)
# - 批量相似任务
# 启用前缀缓存
llm = LLM(
model="meta-llama/Llama-2-7b",
enable_prefix_caching=True
)
2. 内存使用监控
# 获取 KV Cache 使用率
usage = kv_cache_manager.usage # 0.0 ~ 1.0
# 获取前缀缓存统计
stats = kv_cache_manager.make_prefix_cache_stats()
print(f"缓存命中率: {stats.hit_rate:.2%}")
3. 显存不足时的调整
# 方案1:减小 block_size
# 更细粒度的内存管理,但增加元数据开销
# 方案2:降低 gpu_memory_utilization
llm = LLM(model="...", gpu_memory_utilization=0.8)
# 方案3:减小 max_model_len
llm = LLM(model="...", max_model_len=2048)
7. 代码位置速查
| 功能 | 文件 | 关键函数/类 |
|---|
| KVCacheManager | vllm/v1/core/kv_cache_manager.py | KVCacheManager 类 |
| 分配结果 | vllm/v1/core/kv_cache_manager.py | KVCacheBlocks 数据类 |
| 协调器 | vllm/v1/core/kv_cache_coordinator.py | KVCacheCoordinator |
| BlockPool | vllm/v1/core/block_pool.py | BlockPool 类 |
| Block 数据结构 | vllm/v1/core/kv_cache_utils.py | KVCacheBlock |
8. 小结
本章我们深入了解了 KVCacheManager 的工作原理:
- 核心职责:管理 KV Cache 的分配、释放和前缀缓存
- 分层设计:KVCacheManager → Coordinator → BlockPool
- 关键方法:
get_computed_blocks():查找前缀缓存allocate_slots():分配缓存槽位free():释放请求资源
- 前缀缓存:通过 Block Hash 实现多请求共享
- 与调度器协作:为调度决策提供内存管理支持
在下一章中,我们将深入 BlockPool,了解底层的内存块管理和 LRU 驱逐策略。
导航
3.3 - Block Pool 内存块池
Block Pool 内存块池详解
在前两章中,我们了解了 PagedAttention 的分页思想和 KVCacheManager 的分配接口。本章我们将深入到内存管理的最底层——BlockPool。
BlockPool 是 vLLM 内存管理的基石,它直接管理 GPU 上的物理内存块,负责块的分配、释放、缓存和驱逐。理解 BlockPool 的工作原理,对于深入理解 vLLM 的内存效率至关重要。
1. BlockPool 的作用
graph TD
subgraph 上层接口
KVM[KVCacheManager]
end
subgraph BlockPool 内部
BP[BlockPool]
Blocks[blocks: list]
FreeQ[FreeKVCacheBlockQueue<br/>空闲块队列]
HashCache[BlockHashToBlockMap<br/>缓存查找表]
end
subgraph GPU 显存
GPU[KV Cache 物理内存]
end
KVM -->|get_new_blocks| BP
KVM -->|free_blocks| BP
KVM -->|touch| BP
KVM -->|cache_full_blocks| BP
BP --> Blocks
BP --> FreeQ
BP --> HashCache
Blocks -.->|映射| GPU
style BP fill:#e1f5fe
style GPU fill:#e8f5e9BlockPool 的核心职责:
- 块管理:维护所有物理块的元数据
- 分配/释放:从空闲队列分配块,释放块回队列
- 缓存管理:维护 Block Hash 到 Block 的映射
- LRU 驱逐:当需要分配但无空闲块时,驱逐最久未使用的块
2. 核心数据结构
2.1 KVCacheBlock - 块元数据
每个物理块都有一个对应的 KVCacheBlock 对象来存储元数据:
# vllm/v1/core/kv_cache_utils.py
@dataclass
class KVCacheBlock:
"""KV Cache 块的元数据"""
# 块 ID,范围 [0, num_gpu_blocks - 1]
block_id: int
# 引用计数
ref_cnt: int = 0
# 块的 Hash 值(仅当块满且被缓存时有效)
_block_hash: BlockHashWithGroupId | None = None
# 双向链表指针(用于空闲队列)
prev_free_block: "KVCacheBlock | None" = None
next_free_block: "KVCacheBlock | None" = None
# 是否是占位符块(null block)
is_null: bool = False
关键字段解释:
| 字段 | 说明 |
|---|
block_id | 块的唯一标识,对应 GPU 内存中的物理位置 |
ref_cnt | 引用计数,表示有多少请求正在使用这个块 |
_block_hash | 用于前缀缓存的 hash 值,只有满块才有 |
prev/next_free_block | 空闲队列的链表指针,实现 O(1) 的插入删除 |
is_null | 标记占位符块,用于填充 Block Table 中的空位 |
2.2 FreeKVCacheBlockQueue - 空闲块队列
空闲块队列是一个双向链表,按 LRU 顺序组织空闲块:
# vllm/v1/core/kv_cache_utils.py
class FreeKVCacheBlockQueue:
"""
双向链表实现的空闲块队列。
特点:
- O(1) 的头部/尾部插入和删除
- O(1) 的中间删除(已知节点位置)
- LRU 顺序:最久未使用的块在队列头部
"""
def __init__(self, blocks: list[KVCacheBlock]) -> None:
self.num_free_blocks = len(blocks)
# 初始化双向链表
for i in range(self.num_free_blocks):
if i > 0:
blocks[i].prev_free_block = blocks[i - 1]
if i < self.num_free_blocks - 1:
blocks[i].next_free_block = blocks[i + 1]
# 哨兵节点,简化边界处理
self.fake_free_list_head = KVCacheBlock(block_id=-1)
self.fake_free_list_tail = KVCacheBlock(block_id=-1)
# 连接哨兵与实际链表
self.fake_free_list_head.next_free_block = blocks[0]
blocks[0].prev_free_block = self.fake_free_list_head
self.fake_free_list_tail.prev_free_block = blocks[-1]
blocks[-1].next_free_block = self.fake_free_list_tail
链表结构示意图:
graph LR
subgraph FreeKVCacheBlockQueue
HEAD[fake_head<br/>id=-1]
B0[Block 0]
B1[Block 1]
B2[Block 2]
BN[...]
TAIL[fake_tail<br/>id=-1]
HEAD -->|next| B0
B0 -->|prev| HEAD
B0 -->|next| B1
B1 -->|prev| B0
B1 -->|next| B2
B2 -->|prev| B1
B2 -->|next| BN
BN -->|next| TAIL
TAIL -->|prev| BN
end
style HEAD fill:#ffcccc
style TAIL fill:#ffcccc为什么用双向链表而不是 Python 的 deque?
# 场景:缓存命中,需要从队列中间移除块
# 使用 deque:O(n) 查找 + 移除
def remove_from_deque(deque, block):
deque.remove(block) # O(n)
# 使用双向链表:O(1) 直接移除
def remove_from_linked_list(block):
block.prev_free_block.next_free_block = block.next_free_block
block.next_free_block.prev_free_block = block.prev_free_block
block.prev_free_block = block.next_free_block = None
2.3 BlockHashToBlockMap - 缓存查找表
用于快速查找已缓存的块:
# vllm/v1/core/block_pool.py
class BlockHashToBlockMap:
"""
Block Hash 到 Block 的映射表。
注意:同一个 Hash 可能对应多个 Block(虽然内容相同,
但分配给了不同的请求)。
"""
def __init__(self):
# 单个块时直接存储 KVCacheBlock
# 多个块时存储 dict[block_id, KVCacheBlock]
self._cache: dict[
BlockHashWithGroupId,
KVCacheBlock | dict[int, KVCacheBlock]
] = {}
def get_one_block(self, key: BlockHashWithGroupId) -> KVCacheBlock | None:
"""获取任意一个匹配的块"""
blocks = self._cache.get(key)
if blocks is not None:
if isinstance(blocks, KVCacheBlock):
return blocks
if isinstance(blocks, dict):
return next(iter(blocks.values()))
return None
def insert(self, key: BlockHashWithGroupId, block: KVCacheBlock) -> None:
"""插入块到缓存"""
blocks = self._cache.get(key)
if blocks is None:
self._cache[key] = block
elif isinstance(blocks, KVCacheBlock):
# 第二个相同 hash 的块,升级为 dict
self._cache[key] = {blocks.block_id: blocks, block.block_id: block}
elif isinstance(blocks, dict):
blocks[block.block_id] = block
为什么同一 Hash 可能有多个块?
请求 A: "Hello, how are you?"
请求 B: "Hello, how are you?" (相同内容)
当两个请求同时运行时:
- 请求 A 分配了 Block #5 来存储 "Hello, how are you?"
- 请求 B 也需要这些 token,但由于 Block #5 正在被使用,
所以也分配了 Block #8
两个块内容相同,Hash 相同,但是物理上是两个不同的块。
3. BlockPool 类详解
# vllm/v1/core/block_pool.py
class BlockPool:
"""管理 KV Cache 块的内存池"""
def __init__(
self,
num_gpu_blocks: int, # GPU 块总数
enable_caching: bool, # 是否启用前缀缓存
hash_block_size: int, # Hash 计算的块大小
enable_kv_cache_events: bool = False,
metrics_collector: KVCacheMetricsCollector | None = None,
):
self.num_gpu_blocks = num_gpu_blocks
self.enable_caching = enable_caching
# 所有块的元数据
self.blocks: list[KVCacheBlock] = [
KVCacheBlock(idx) for idx in range(num_gpu_blocks)
]
# 空闲块队列(LRU 顺序)
self.free_block_queue = FreeKVCacheBlockQueue(self.blocks)
# 缓存查找表
self.cached_block_hash_to_block = BlockHashToBlockMap()
# 占位符块(用于填充 Block Table 中的空位)
self.null_block = self.free_block_queue.popleft()
self.null_block.is_null = True
初始化后的状态:
graph TD
subgraph BlockPool
blocks["blocks[0...N-1]"]
FQ[FreeKVCacheBlockQueue]
Cache[BlockHashToBlockMap]
NB[null_block]
end
subgraph "空闲队列 (初始状态)"
B1[Block 1] --> B2[Block 2]
B2 --> B3[Block 3]
B3 --> BN[...]
end
subgraph "GPU 物理内存"
GPU["KV Cache Tensor<br/>[num_blocks, 2, num_heads, block_size, head_dim]"]
end
blocks --> B1
FQ --> B1
NB -.->|"block_id=0"| GPU
style NB fill:#ffcccc注意: null_block 是第一个被"分配"的块(block_id=0),但它实际上是一个占位符,永远不会存储真实数据。它用于填充 Block Table 中那些暂时不需要的位置(例如滑动窗口注意力中窗口外的位置)。
4. 核心操作详解
4.1 get_new_blocks() - 分配新块
def get_new_blocks(self, num_blocks: int) -> list[KVCacheBlock]:
"""
从空闲队列分配指定数量的块。
如果启用了缓存,可能会驱逐一些缓存块来腾出空间。
"""
if num_blocks > self.get_num_free_blocks():
raise ValueError(f"Cannot get {num_blocks} free blocks from the pool")
# 从队列头部弹出 n 个块
ret: list[KVCacheBlock] = self.free_block_queue.popleft_n(num_blocks)
if self.enable_caching:
for block in ret:
# 如果块之前被缓存过,需要驱逐它
self._maybe_evict_cached_block(block)
block.ref_cnt += 1
else:
for block in ret:
block.ref_cnt += 1
return ret
分配流程图:
flowchart TD
A[get_new_blocks] --> B{请求块数 <= 空闲块数?}
B -->|否| C[抛出异常]
B -->|是| D[popleft_n 弹出 n 个块]
D --> E{启用缓存?}
E -->|是| F[遍历每个块]
F --> G{块有 hash?}
G -->|是| H[从缓存表移除]
H --> I[清除 block_hash]
G -->|否| J[跳过]
I --> K[ref_cnt += 1]
J --> K
E -->|否| K
K --> L[返回块列表]
style C fill:#ffcdd2
style L fill:#c8e6c94.2 free_blocks() - 释放块
def free_blocks(self, ordered_blocks: Iterable[KVCacheBlock]) -> None:
"""
释放一组块。块应按驱逐优先级排序(先释放的先被驱逐)。
注意:释放不一定意味着块立即可复用。如果块被多个请求共享
(ref_cnt > 1),释放只会减少引用计数。
"""
blocks_list = list(ordered_blocks)
# 减少所有块的引用计数
for block in blocks_list:
block.ref_cnt -= 1
# 只有 ref_cnt == 0 且非 null 的块才加入空闲队列
self.free_block_queue.append_n(
[block for block in blocks_list
if block.ref_cnt == 0 and not block.is_null]
)
释放流程示意:
sequenceDiagram
participant Caller as 调用者
participant BP as BlockPool
participant Block as KVCacheBlock
participant FQ as FreeBlockQueue
Caller->>BP: free_blocks([B1, B2, B3])
loop 每个块
BP->>Block: ref_cnt -= 1
end
alt B1.ref_cnt == 0
BP->>FQ: append(B1)
Note over FQ: B1 加入队列尾部<br/>可被复用或驱逐
else B1.ref_cnt > 0
Note over Block: B1 仍被其他请求使用<br/>不加入空闲队列
end4.3 touch() - 缓存命中处理
当一个请求命中了缓存块时,需要调用 touch() 来增加引用计数:
def touch(self, blocks: Sequence[KVCacheBlock]) -> None:
"""
触碰块,增加引用计数。
如果块在空闲队列中(ref_cnt 为 0),需要将其从队列中移除,
因为它现在被使用了。
"""
for block in blocks:
# ref_cnt=0 意味着块在空闲队列中
if block.ref_cnt == 0 and not block.is_null:
self.free_block_queue.remove(block) # O(1)
block.ref_cnt += 1
touch 的关键作用:
场景:请求 B 命中了请求 A 的缓存块
初始状态:
- Block #5 被请求 A 使用,ref_cnt = 1
- Block #5 有 hash 值(已缓存)
请求 A 完成后:
- Block #5.ref_cnt = 0
- Block #5 加入空闲队列尾部
- Block #5 的 hash 仍然保留(可被缓存复用)
请求 B 查找缓存,命中 Block #5:
- touch(Block #5)
- 从空闲队列移除 Block #5(因为 ref_cnt 是 0)
- Block #5.ref_cnt = 1
现在 Block #5 又被使用了,不会被驱逐!
4.4 cache_full_blocks() - 缓存满块
当一个块填满后,将其加入缓存表:
def cache_full_blocks(
self,
request: Request,
blocks: list[KVCacheBlock],
num_cached_blocks: int, # 已缓存的块数
num_full_blocks: int, # 满块数
block_size: int,
kv_cache_group_id: int,
) -> None:
"""
缓存新的满块。
为每个新满块计算 hash 并加入缓存表。
"""
if num_cached_blocks >= num_full_blocks:
return
new_full_blocks = blocks[num_cached_blocks:num_full_blocks]
for i, blk in enumerate(new_full_blocks):
if blk.is_null:
continue
# 获取块的 hash
block_hash = request.block_hashes[num_cached_blocks + i]
# 设置块的 hash
block_hash_with_group_id = make_block_hash_with_group_id(
block_hash, kv_cache_group_id
)
blk.block_hash = block_hash_with_group_id
# 加入缓存表
self.cached_block_hash_to_block.insert(block_hash_with_group_id, blk)
5. 块的生命周期
stateDiagram-v2
[*] --> Free: 初始化
Free --> Allocated: get_new_blocks()
note right of Allocated
ref_cnt = 1
block_hash = None
end note
Allocated --> FullAndCached: cache_full_blocks()
note right of FullAndCached
ref_cnt >= 1
block_hash 已设置
加入 hash_to_block
end note
FullAndCached --> Shared: touch() (另一请求命中)
note right of Shared
ref_cnt += 1
end note
Shared --> FullAndCached: free() (一个请求释放)
note right of FullAndCached
ref_cnt -= 1
end note
FullAndCached --> CachedEvictable: free() (ref_cnt = 0)
note right of CachedEvictable
ref_cnt = 0
block_hash 保留
加入空闲队列尾部
end note
CachedEvictable --> Shared: touch() (再次命中)
note left of Shared
从空闲队列移除
ref_cnt = 1
end note
CachedEvictable --> Evicted: _maybe_evict_cached_block()
note right of Evicted
从空闲队列头部被取出
block_hash 清除
从 hash_to_block 移除
end note
Evicted --> Allocated: 重新分配
6. LRU 驱逐策略
BlockPool 使用 LRU(Least Recently Used) 策略来决定驱逐哪些缓存块。
6.1 LRU 顺序的维护
关键规则:
1. 新分配的块从队列头部取出
2. 释放的块(ref_cnt 降为 0)加入队列尾部
3. 缓存命中时,块从队列中间移除
这意味着:
- 队列头部是最久未使用的块(最先被驱逐)
- 队列尾部是最近使用的块(最后被驱逐)
6.2 驱逐过程
sequenceDiagram
participant Caller as 调用者
participant BP as BlockPool
participant FQ as FreeBlockQueue
participant Cache as BlockHashToBlockMap
Note over Caller: 需要分配新块,但内存紧张
Caller->>BP: get_new_blocks(1)
BP->>FQ: popleft()
FQ-->>BP: Block (可能是缓存块)
alt 块有 hash (是缓存块)
BP->>BP: _maybe_evict_cached_block()
BP->>Cache: pop(block_hash, block_id)
Cache-->>BP: 移除成功
BP->>BP: block.reset_hash()
Note over BP: 块的缓存状态被清除
end
BP-->>Caller: 干净的块6.3 为什么使用逆序释放?
# KVCacheManager 中的释放逻辑
def free(self, request: Request) -> None:
"""释放请求占用的块,按逆序释放"""
blocks = self.coordinator.get_blocks(request.request_id)
# 逆序:尾部块先加入空闲队列
self.block_pool.free_blocks(reversed(blocks))
原因:
请求 A 占用块: [Block_1, Block_2, Block_3, Block_4]
↑ ↑ ↑ ↑
前缀 .... .... 尾部
如果请求 B 有相同的前缀 "Hello, how",它更可能复用 Block_1
正序释放的结果:
空闲队列: [..., Block_1, Block_2, Block_3, Block_4]
↑ ↑
先驱逐 后驱逐
// 前缀块先被驱逐,不好!
逆序释放的结果:
空闲队列: [..., Block_4, Block_3, Block_2, Block_1]
↑ ↑
先驱逐 后驱逐
// 尾部块先被驱逐,前缀块保留更久,好!
7. 前缀缓存的缓存命中
7.1 缓存查找流程
def get_cached_block(
self, block_hash: BlockHash, kv_cache_group_ids: list[int]
) -> list[KVCacheBlock] | None:
"""
根据 block hash 查找缓存块。
返回每个 group 的缓存块,如果任何 group 未命中则返回 None。
"""
cached_blocks = []
for group_id in kv_cache_group_ids:
block_hash_with_group_id = make_block_hash_with_group_id(
block_hash, group_id
)
block = self.cached_block_hash_to_block.get_one_block(
block_hash_with_group_id
)
if not block:
return None # 任何 group 未命中,整体失败
cached_blocks.append(block)
return cached_blocks
7.2 完整的缓存命中场景
sequenceDiagram
participant Req as 新请求
participant KVM as KVCacheManager
participant BP as BlockPool
participant Cache as BlockHashToBlockMap
participant FQ as FreeBlockQueue
Req->>KVM: get_computed_blocks()
KVM->>KVM: 计算 block_hashes
loop 每个 block_hash
KVM->>BP: get_cached_block(hash)
BP->>Cache: get_one_block(hash)
alt 命中
Cache-->>BP: Block
BP-->>KVM: Block
Note over KVM: 检查块状态
alt 块在空闲队列中 (ref_cnt = 0)
KVM->>BP: touch(Block)
BP->>FQ: remove(Block)
Note over FQ: 从队列移除<br/>防止被驱逐
BP->>BP: Block.ref_cnt += 1
else 块正在使用中 (ref_cnt > 0)
KVM->>BP: touch(Block)
BP->>BP: Block.ref_cnt += 1
Note over BP: 共享使用
end
else 未命中
Cache-->>BP: None
BP-->>KVM: None
Note over KVM: 停止查找<br/>后续块无法复用
end
end
KVM-->>Req: 缓存块列表, 命中 token 数
8. 配置与调优
8.1 关键配置参数
| 参数 | 说明 | 影响 |
|---|
num_gpu_blocks | 总块数 | 更多块 = 更大的 KV Cache 容量 |
enable_caching | 是否启用前缀缓存 | 开启后有缓存命中收益,但增加管理开销 |
block_size | 每块 token 数 | 更大块 = 更少碎片,但粒度更粗 |
8.2 块数量计算
# vllm/v1/core/kv_cache_utils.py
def get_num_blocks(
vllm_config: VllmConfig,
num_layers: int,
available_memory: int,
page_size: int # 每块占用的显存
) -> int:
"""计算可分配的块数"""
num_blocks = int(available_memory // page_size // num_layers)
num_blocks = max(num_blocks, 0)
return num_blocks
显存消耗公式:
page_size = block_size × num_heads × head_dim × 2 × dtype_size
↑ ↑
(K 和 V) (FP16 = 2 bytes)
total_kv_cache_memory = page_size × num_layers × num_blocks
8.3 监控使用率
def get_usage(self) -> float:
"""获取 KV Cache 使用率"""
total_gpu_blocks = self.num_gpu_blocks - 1 # 减去 null_block
if not total_gpu_blocks:
return 0
return 1.0 - (self.get_num_free_blocks() / total_gpu_blocks)
9. 代码位置速查
| 功能 | 文件 | 关键类/函数 |
|---|
| BlockPool | vllm/v1/core/block_pool.py | BlockPool 类 |
| KVCacheBlock | vllm/v1/core/kv_cache_utils.py | KVCacheBlock 数据类 |
| 空闲队列 | vllm/v1/core/kv_cache_utils.py | FreeKVCacheBlockQueue 类 |
| 缓存查找表 | vllm/v1/core/block_pool.py | BlockHashToBlockMap 类 |
| Hash 计算 | vllm/v1/core/kv_cache_utils.py | hash_block_tokens() |
| 块数计算 | vllm/v1/core/kv_cache_utils.py | get_num_blocks() |
10. 小结
本章我们深入了解了 BlockPool 的工作原理:
核心数据结构:
KVCacheBlock:块元数据,包含引用计数和 hashFreeKVCacheBlockQueue:双向链表实现的 LRU 空闲队列BlockHashToBlockMap:前缀缓存的 hash 查找表
关键操作:
get_new_blocks():从队列头部分配块free_blocks():减少引用计数,ref_cnt=0 时加入队列尾部touch():缓存命中时增加引用计数cache_full_blocks():缓存满块的 hash
LRU 驱逐:
- 队列头部是最久未使用的块
- 逆序释放确保前缀块保留更久
块生命周期:Free → Allocated → Cached → Evictable → Evicted
在下一章中,我们将学习调度器(Scheduler)如何使用这些内存管理组件来做出调度决策。
导航
3.4 - 调度器原理
调度器原理详解
调度器(Scheduler)是 vLLM 的"大脑",它决定了哪些请求可以运行以及每个请求能处理多少 token。一个好的调度策略直接影响系统的吞吐量、延迟和资源利用率。
本章我们将深入了解 vLLM 调度器的工作原理、调度算法和实现细节。
1. Scheduler 在架构中的位置
graph TD
subgraph 用户层
User[用户请求]
end
subgraph 引擎层
Engine[EngineCore]
end
subgraph 调度层
Scheduler[Scheduler<br/>调度器]
Waiting[waiting 队列]
Running[running 队列]
end
subgraph 内存管理层
KVM[KVCacheManager]
end
subgraph 执行层
Executor[ModelExecutor]
end
User --> Engine
Engine --> Scheduler
Scheduler --> Waiting
Scheduler --> Running
Scheduler -->|allocate/free| KVM
Scheduler -->|SchedulerOutput| Executor
style Scheduler fill:#e1f5feScheduler 的核心职责:
- 请求队列管理:维护 waiting 和 running 两个队列
- 资源分配决策:决定哪些请求可以获得 GPU 资源
- 内存协调:与 KVCacheManager 协作管理 KV Cache
- 抢占处理:在内存不足时执行抢占策略
- 输出构建:为 ModelExecutor 构建 SchedulerOutput
2. 核心数据结构
2.1 请求队列
# vllm/v1/core/sched/scheduler.py
class Scheduler:
def __init__(self, ...):
# 等待队列:存放新到达和被抢占的请求
self.waiting = create_request_queue(self.policy)
# 运行队列:存放正在处理的请求
self.running: list[Request] = []
# 请求字典:通过 request_id 快速查找
self.requests: dict[str, Request] = {}
两个队列的关系:
stateDiagram-v2
[*] --> WAITING: add_request()
WAITING --> RUNNING: 调度成功
RUNNING --> WAITING: 被抢占
RUNNING --> FINISHED: 完成
FINISHED --> [*]: 释放资源2.2 调度约束
class Scheduler:
def __init__(self, ...):
# 最大并发请求数
self.max_num_running_reqs = self.scheduler_config.max_num_seqs
# 每步最大 token 数
self.max_num_scheduled_tokens = self.scheduler_config.max_num_batched_tokens
# 最大序列长度
self.max_model_len = vllm_config.model_config.max_model_len
约束说明:
| 约束 | 默认值 | 说明 |
|---|
max_num_seqs | 256 | 最多同时运行的请求数 |
max_num_batched_tokens | 2048 | 每步最多处理的 token 数 |
max_model_len | 模型配置 | 单个序列的最大长度 |
2.3 SchedulerOutput - 调度输出
# vllm/v1/core/sched/output.py
@dataclass
class SchedulerOutput:
"""调度器的输出,传递给 ModelExecutor"""
# 新调度的请求
scheduled_new_reqs: list[NewRequestData]
# 继续运行的请求
scheduled_cached_reqs: CachedRequestData
# 每个请求调度的 token 数
num_scheduled_tokens: dict[str, int]
# 总调度 token 数
total_num_scheduled_tokens: int
# 抢占的请求 ID
preempted_req_ids: set[str]
# 完成的请求 ID
finished_req_ids: set[str]
3. schedule() 方法详解
schedule() 是调度器的核心方法,每个 step 调用一次。
3.1 调度算法概述
vLLM 的调度没有明确的 "prefill 阶段" 或 "decode 阶段"。
每个请求只有两个关键状态:
- num_computed_tokens: 已计算的 token 数
- num_tokens: 总 token 数(prompt + output)
每一步,调度器尝试分配 token,使 num_computed_tokens 追上 num_tokens。
这种设计足够通用,支持分块预填充、前缀缓存、投机解码等各种优化。
3.2 完整调度流程
flowchart TD
Start[schedule 开始] --> Init[初始化预算和输出列表]
Init --> RunLoop{running 队列}
subgraph 处理运行中请求
RunLoop --> |遍历| CalcTokens[计算需要调度的 token 数]
CalcTokens --> Allocate[allocate_slots 分配 KV Cache]
Allocate --> AllocOK{分配成功?}
AllocOK -->|是| AddToScheduled[加入调度列表<br/>减少 token 预算]
AllocOK -->|否| Preempt[抢占低优先级请求]
Preempt --> RetryAlloc{重试分配}
RetryAlloc -->|成功| AddToScheduled
RetryAlloc -->|失败| SkipReq[跳过此请求]
AddToScheduled --> RunLoop
SkipReq --> RunLoop
end
RunLoop -->|完成| WaitCheck{有抢占?}
WaitCheck -->|是| SkipWaiting[跳过 waiting 队列]
WaitCheck -->|否| WaitLoop{waiting 队列}
subgraph 处理等待请求
WaitLoop --> |遍历| CheckStatus[检查请求状态]
CheckStatus --> ReadyCheck{可以调度?}
ReadyCheck -->|否| SkipWait[跳过并继续]
ReadyCheck -->|是| GetCache[查找前缀缓存]
GetCache --> CalcNewTokens[计算需要调度的 token 数]
CalcNewTokens --> AllocWait[allocate_slots 分配]
AllocWait --> AllocWaitOK{分配成功?}
AllocWaitOK -->|是| MoveToRunning[移入 running 队列]
AllocWaitOK -->|否| StopWait[停止处理 waiting]
MoveToRunning --> WaitLoop
SkipWait --> WaitLoop
end
WaitLoop -->|完成| BuildOutput[构建 SchedulerOutput]
SkipWaiting --> BuildOutput
StopWait --> BuildOutput
BuildOutput --> Return[返回输出]3.3 处理 Running 请求
def schedule(self) -> SchedulerOutput:
# ... 初始化 ...
# 第一步:处理 RUNNING 请求
req_index = 0
while req_index < len(self.running) and token_budget > 0:
request = self.running[req_index]
# 计算需要调度的 token 数
num_new_tokens = (
request.num_tokens_with_spec # 总 token 数
+ request.num_output_placeholders # 输出占位符
- request.num_computed_tokens # 已计算的 token 数
)
# 应用长 prefill 分块限制
if 0 < threshold < num_new_tokens:
num_new_tokens = threshold
# 不超过 token 预算
num_new_tokens = min(num_new_tokens, token_budget)
# 不超过模型最大长度
num_new_tokens = min(
num_new_tokens,
self.max_model_len - 1 - request.num_computed_tokens
)
if num_new_tokens == 0:
req_index += 1
continue
# 尝试分配 KV Cache
while True:
new_blocks = self.kv_cache_manager.allocate_slots(
request,
num_new_tokens,
num_lookahead_tokens=self.num_lookahead_tokens,
)
if new_blocks is not None:
# 分配成功
break
# 分配失败,执行抢占
preempted_req = self._select_preempt_target()
self._preempt_request(preempted_req, timestamp)
if preempted_req == request:
# 自己被抢占了,无法继续
break
if new_blocks is None:
break
# 调度成功
scheduled_running_reqs.append(request)
token_budget -= num_new_tokens
req_index += 1
3.4 处理 Waiting 请求
# 第二步:处理 WAITING 请求(只有没有抢占时才处理)
if not preempted_reqs:
while self.waiting and token_budget > 0:
# 检查并发限制
if len(self.running) == self.max_num_running_reqs:
break
request = self.waiting.peek_request()
# 检查各种等待状态
if request.status == RequestStatus.WAITING_FOR_REMOTE_KVS:
# 等待远程 KV Cache 加载
...
if request.status == RequestStatus.WAITING_FOR_FSM:
# 等待语法编译
...
# 查找前缀缓存
new_computed_blocks, num_cached = (
self.kv_cache_manager.get_computed_blocks(request)
)
# 计算需要调度的 token 数
num_new_tokens = request.num_tokens - num_cached
num_new_tokens = min(num_new_tokens, token_budget)
# 分配 KV Cache
new_blocks = self.kv_cache_manager.allocate_slots(
request,
num_new_tokens,
num_new_computed_tokens=num_cached,
new_computed_blocks=new_computed_blocks,
)
if new_blocks is None:
# 无法分配,停止处理 waiting 队列
break
# 调度成功,移入 running 队列
request = self.waiting.pop_request()
self.running.append(request)
request.status = RequestStatus.RUNNING
request.num_computed_tokens = num_cached
token_budget -= num_new_tokens
4. 抢占机制
当 KV Cache 内存不足时,调度器需要抢占一些请求来释放空间。
4.1 抢占策略
def _select_preempt_target(self) -> Request:
"""选择要抢占的请求"""
if self.policy == SchedulingPolicy.PRIORITY:
# 优先级调度:抢占优先级最低、到达最晚的请求
return max(
self.running,
key=lambda r: (r.priority, r.arrival_time),
)
else:
# FCFS:抢占最后加入的请求
return self.running[-1]
4.2 抢占流程
def _preempt_request(self, request: Request, timestamp: float) -> None:
"""抢占请求并放回 waiting 队列"""
assert request.status == RequestStatus.RUNNING
# 释放 KV Cache
self.kv_cache_manager.free(request)
self.encoder_cache_manager.free(request)
# 更新请求状态
request.status = RequestStatus.PREEMPTED
request.num_computed_tokens = 0 # 重置计算进度
request.num_preemptions += 1
# 放回 waiting 队列头部(优先恢复)
self.waiting.prepend_request(request)
4.3 抢占流程图
sequenceDiagram
participant Scheduler
participant KVM as KVCacheManager
participant Running as running 队列
participant Waiting as waiting 队列
participant Request as 请求
Note over Scheduler: allocate_slots 失败
Scheduler->>Running: 选择抢占目标
Running-->>Scheduler: victim 请求
Scheduler->>Request: status = PREEMPTED
Scheduler->>Request: num_computed_tokens = 0
Scheduler->>Request: num_preemptions += 1
Scheduler->>KVM: free(victim)
Note over KVM: 释放 KV Cache 块
Scheduler->>Running: remove(victim)
Scheduler->>Waiting: prepend(victim)
Note over Waiting: victim 加入队列头部<br/>等待恢复
Scheduler->>KVM: retry allocate_slots
Note over Scheduler: 重试分配
5. 调度策略
5.1 FCFS(先来先服务)
# 默认策略
class SchedulingPolicy(Enum):
FCFS = "fcfs"
PRIORITY = "priority"
FCFS 特点:
5.2 Priority(优先级调度)
# 基于优先级的调度
def _select_preempt_target(self):
if self.policy == SchedulingPolicy.PRIORITY:
# 选择优先级最低的请求
return max(
self.running,
key=lambda r: (r.priority, r.arrival_time),
)
Priority 特点:
- 高优先级请求优先处理
- 支持紧急请求插队
- 可能导致低优先级请求"饥饿"
6. 分块预填充(Chunked Prefill)
对于长输入,vLLM 支持分块预填充,将 prefill 分成多个 chunk 处理。
6.1 工作原理
传统 Prefill(一次性处理):
step 1: [token_1 ... token_1000] → 处理 1000 个 token
分块 Prefill:
step 1: [token_1 ... token_256] → 处理 256 个 token
step 2: [token_257 ... token_512] → 处理 256 个 token
step 3: [token_513 ... token_768] → 处理 256 个 token
step 4: [token_769 ... token_1000] → 处理 232 个 token
6.2 配置参数
# 长 prefill 分块阈值
threshold = self.scheduler_config.long_prefill_token_threshold
if 0 < threshold < num_new_tokens:
num_new_tokens = threshold # 限制每次处理的 token 数
6.3 好处
- 降低延迟:长 prefill 不会阻塞其他请求
- 更好的资源利用:允许多个请求交替执行
- 内存平滑:避免一次性分配大量 KV Cache
gantt
title 分块 Prefill vs 传统 Prefill
dateFormat X
axisFormat %s
section 传统 Prefill
请求 A (1000 tokens) :a1, 0, 100
请求 B (等待) :a2, 100, 150
section 分块 Prefill
请求 A chunk 1 :b1, 0, 25
请求 B step 1 :b2, 25, 30
请求 A chunk 2 :b3, 30, 55
请求 B step 2 :b4, 55, 60
请求 A chunk 3 :b5, 60, 85
请求 B step 3 :b6, 85, 90
7. 调度器与其他组件的协作
7.1 完整的调度循环
sequenceDiagram
participant EC as EngineCore
participant Sched as Scheduler
participant KVM as KVCacheManager
participant Exec as ModelExecutor
loop 每个 step
EC->>Sched: schedule()
rect rgb(230, 245, 230)
Note over Sched,KVM: 处理 running 请求
Sched->>KVM: allocate_slots()
alt 内存不足
Sched->>Sched: _preempt_request()
Sched->>KVM: free()
end
end
rect rgb(245, 230, 230)
Note over Sched,KVM: 处理 waiting 请求
Sched->>KVM: get_computed_blocks()
Sched->>KVM: allocate_slots()
end
Sched-->>EC: SchedulerOutput
EC->>Exec: execute_model(SchedulerOutput)
Exec-->>EC: ModelRunnerOutput
EC->>Sched: update_from_output()
Note over Sched: 更新请求状态<br/>检查完成条件
end7.2 请求状态更新
def update_from_output(
self,
model_runner_output: ModelRunnerOutput,
...
) -> EngineCoreOutputs:
"""根据模型输出更新请求状态"""
for req_id, sampler_output in model_output.items():
request = self.requests[req_id]
# 追加输出 token
request.append_output_token_ids(sampler_output.sampled_token_ids)
# 检查停止条件
stopped = check_stop(request, self.max_model_len)
if stopped:
# 请求完成
self._free_request(request)
self.finished_req_ids.add(req_id)
outputs.append(...)
8. 配置与调优
8.1 关键配置参数
| 参数 | 说明 | 建议值 |
|---|
max_num_seqs | 最大并发请求数 | 根据 GPU 内存调整 |
max_num_batched_tokens | 每步最大 token 数 | 2048-8192 |
enable_chunked_prefill | 启用分块预填充 | 建议开启 |
long_prefill_token_threshold | 长 prefill 阈值 | 256-512 |
policy | 调度策略 | fcfs 或 priority |
8.2 性能调优建议
1. 提高吞吐量
# 增加最大并发数
llm = LLM(model="...", max_num_seqs=512)
# 增加每步 token 数
llm = LLM(model="...", max_num_batched_tokens=4096)
2. 降低延迟
# 启用分块预填充
llm = LLM(
model="...",
enable_chunked_prefill=True,
long_prefill_token_threshold=256,
)
3. 处理高优先级请求
# 使用优先级调度
llm = LLM(model="...", policy="priority")
# 发送请求时设置优先级
llm.generate(prompt, priority=0) # 高优先级
llm.generate(prompt, priority=10) # 低优先级
9. 代码位置速查
| 功能 | 文件 | 关键类/函数 |
|---|
| Scheduler 主类 | vllm/v1/core/sched/scheduler.py | Scheduler |
| schedule 方法 | vllm/v1/core/sched/scheduler.py:313 | schedule() |
| 抢占逻辑 | vllm/v1/core/sched/scheduler.py:892 | _preempt_request() |
| 调度输出 | vllm/v1/core/sched/output.py | SchedulerOutput |
| 请求队列 | vllm/v1/core/sched/request_queue.py | create_request_queue() |
| 调度策略 | vllm/v1/core/sched/request_queue.py | SchedulingPolicy |
10. 小结
本章我们深入了解了 vLLM 调度器的工作原理:
- 双队列管理:waiting 和 running 队列
- 调度算法:
- 先处理 running 请求
- 再处理 waiting 请求
- 内存不足时执行抢占
- 抢占机制:释放低优先级请求的资源
- 调度策略:FCFS 和 Priority
- 分块预填充:降低长输入的延迟影响
- 与其他组件协作:KVCacheManager、ModelExecutor
在下一章中,我们将学习连续批处理(Continuous Batching)机制,了解 vLLM 如何实现高效的动态批处理。
导航
3.5 - 连续批处理机制
连续批处理机制详解
连续批处理(Continuous Batching)是 vLLM 实现高吞吐量的关键技术之一。与传统的静态批处理不同,连续批处理允许请求在推理过程中动态加入和退出,极大地提高了 GPU 利用率。
本章我们将深入了解连续批处理的原理、实现和优势。
1. 静态批处理的问题
1.1 什么是静态批处理
传统的 LLM 推理使用静态批处理:将一组请求打包成一个批次,等待所有请求完成后再处理下一批。
静态批处理示意:
Batch 1:
请求 A: "你好" → 生成 50 个 token
请求 B: "Hello, world" → 生成 100 个 token
请求 C: "如何学习编程" → 生成 200 个 token
所有请求必须等待请求 C 完成才能返回结果
1.2 静态批处理的问题
gantt
title 静态批处理的浪费
dateFormat X
axisFormat %s
section Batch 1
请求 A (50 tokens) :active, a1, 0, 50
请求 A 等待 :done, a2, 50, 200
请求 B (100 tokens) :active, b1, 0, 100
请求 B 等待 :done, b2, 100, 200
请求 C (200 tokens) :active, c1, 0, 200
section Batch 2 (等待)
请求 D :crit, d1, 200, 250
请求 E :crit, e1, 200, 280问题分析:
| 问题 | 说明 |
|---|
| GPU 空闲 | 短请求完成后,GPU 资源被浪费 |
| 高延迟 | 所有请求必须等待最长的请求完成 |
| 低吞吐量 | 新请求必须等待当前批次完成 |
| 内存浪费 | 必须为最长序列预分配内存 |
2. 连续批处理的解决方案
2.1 核心思想
连续批处理的核心思想是:在每个推理步骤(iteration)级别进行调度,而不是在批次级别。
连续批处理示意:
Step 1: [请求 A, 请求 B, 请求 C]
Step 2: [请求 A, 请求 B, 请求 C]
...
Step 50: 请求 A 完成 → 请求 D 加入
Step 51: [请求 D, 请求 B, 请求 C] // 请求 D 立即开始!
...
Step 100: 请求 B 完成 → 请求 E 加入
Step 101: [请求 D, 请求 E, 请求 C]
...
2.2 优势对比
gantt
title 连续批处理的高效性
dateFormat X
axisFormat %s
section 请求流
请求 A (50 tokens) :active, a1, 0, 50
请求 B (100 tokens) :active, b1, 0, 100
请求 C (200 tokens) :active, c1, 0, 200
请求 D (立即开始) :active, d1, 50, 100
请求 E (立即开始) :active, e1, 100, 180
请求 F (立即开始) :active, f1, 100, 250优势分析:
| 优势 | 说明 |
|---|
| 高 GPU 利用率 | 完成的请求立即被新请求替换 |
| 低延迟 | 请求完成后立即返回,不等待其他请求 |
| 高吞吐量 | 新请求可以立即加入批次 |
| 按需分配 | 配合 PagedAttention,内存按需分配 |
3. vLLM 中的实现
3.1 迭代级调度(Iteration-Level Scheduling)
vLLM 的调度器在每个 step 都会重新调度:
# vllm/v1/engine/core.py
def step(self) -> EngineCoreOutputs:
"""执行一个推理步骤"""
# 1. 调度:决定这一步处理哪些请求
scheduler_output = self.scheduler.schedule()
# 2. 执行模型
model_output = self.model_executor.execute_model(scheduler_output)
# 3. 采样
sampled_tokens = self.model_executor.sample_tokens(model_output)
# 4. 更新状态:可能有请求完成或新请求加入
outputs = self.scheduler.update_from_output(sampled_tokens)
return outputs
3.2 动态请求管理
sequenceDiagram
participant User as 用户
participant Engine as EngineCore
participant Sched as Scheduler
participant Exec as ModelExecutor
Note over Engine: Step N
User->>Engine: 新请求到达
Engine->>Sched: add_request()
Note over Sched: 加入 waiting 队列
Engine->>Sched: schedule()
Note over Sched: 检查 running 请求<br/>分配新请求资源
Sched-->>Engine: SchedulerOutput<br/>(包含新请求)
Engine->>Exec: execute_model()
Exec-->>Engine: ModelOutput
Engine->>Sched: update_from_output()
alt 某请求完成
Note over Sched: 从 running 移除<br/>释放 KV Cache
Sched-->>Engine: 完成的请求结果
Engine-->>User: 返回结果
end
Note over Engine: Step N+1 (循环)3.3 关键代码路径
# vllm/v1/core/sched/scheduler.py
def schedule(self) -> SchedulerOutput:
"""每个 step 调用一次的调度方法"""
scheduled_new_reqs = []
scheduled_running_reqs = []
token_budget = self.max_num_scheduled_tokens
# 第一步:处理 running 请求
for request in self.running:
if token_budget <= 0:
break
# 计算需要的 token 数(通常是 1,decode 阶段)
num_new_tokens = request.num_tokens - request.num_computed_tokens
num_new_tokens = min(num_new_tokens, token_budget)
# 分配 KV Cache
new_blocks = self.kv_cache_manager.allocate_slots(
request, num_new_tokens
)
if new_blocks is not None:
scheduled_running_reqs.append(request)
token_budget -= num_new_tokens
# 第二步:处理 waiting 请求(动态加入!)
while self.waiting and token_budget > 0:
if len(self.running) >= self.max_num_running_reqs:
break
request = self.waiting.peek_request()
# 查找缓存
cached_blocks, num_cached = self.kv_cache_manager.get_computed_blocks(request)
# 计算需要的 token 数
num_new_tokens = request.num_tokens - num_cached
num_new_tokens = min(num_new_tokens, token_budget)
# 分配 KV Cache
new_blocks = self.kv_cache_manager.allocate_slots(
request, num_new_tokens, new_computed_blocks=cached_blocks
)
if new_blocks is None:
break
# 成功加入 running 队列
self.waiting.pop_request()
self.running.append(request)
scheduled_new_reqs.append(request)
token_budget -= num_new_tokens
return SchedulerOutput(...)
4. Prefill 与 Decode 的混合处理
4.1 两阶段的特性回顾
| 阶段 | 计算类型 | GPU 利用率 | Token 数量 |
|---|
| Prefill | 计算密集型 | 高 | 多(整个 prompt) |
| Decode | 内存密集型 | 低 | 少(每次 1 个) |
4.2 混合处理示意
Step 1:
请求 A: Prefill [token_1...token_100] (100 tokens)
请求 B: Decode [token_51] (1 token)
请求 C: Decode [token_30] (1 token)
Step 2:
请求 A: Decode [token_101] (1 token)
请求 B: Decode [token_52] (1 token)
请求 C: Decode [token_31] (1 token)
请求 D: Prefill [token_1...token_50] (50 tokens,新加入!)
4.3 好处
graph TD
subgraph 传统方式[传统方式]
P1[Prefill A] --> D1[Decode A]
D1 --> P2[Prefill B]
P2 --> D2[Decode B]
end
subgraph 混合处理[混合处理]
M1[Prefill A + Decode B,C]
M2[Decode A,B,C + Prefill D]
M1 --> M2
end
Note1[时间长,GPU 利用不均] -.-> 传统方式
Note2[时间短,GPU 持续高负载] -.-> 混合处理
style 混合处理 fill:#c8e6c9
5. 分块预填充与连续批处理
5.1 分块预填充(Chunked Prefill)
对于长输入,将 prefill 分成多个 chunk,与其他请求的 decode 交替执行:
请求 A: 长 prompt (1000 tokens)
请求 B, C, D: 正在 decode
Step 1:
请求 A chunk 1: [token_1...token_256]
请求 B, C, D: decode
Step 2:
请求 A chunk 2: [token_257...token_512]
请求 B, C, D: decode
请求 E: 新加入
Step 3:
请求 A chunk 3: [token_513...token_768]
请求 B, C, D, E: decode
...
5.2 配置分块预填充
llm = LLM(
model="meta-llama/Llama-2-7b",
enable_chunked_prefill=True,
# 每个 chunk 的最大 token 数
long_prefill_token_threshold=256,
)
5.3 分块预填充的实现
# vllm/v1/core/sched/scheduler.py
def schedule(self):
# 应用长 prefill 分块限制
threshold = self.scheduler_config.long_prefill_token_threshold
if 0 < threshold < num_new_tokens:
# 限制每次处理的 token 数
num_new_tokens = threshold
6. 性能对比
6.1 吞吐量对比
graph LR
subgraph 静态批处理
S1[Batch 1<br/>100 req/s]
S2[等待]
S3[Batch 2<br/>100 req/s]
S1 --> S2 --> S3
end
subgraph 连续批处理
C1[持续处理<br/>300 req/s]
end
style C1 fill:#c8e6c96.2 延迟对比
| 场景 | 静态批处理 | 连续批处理 |
|---|
| 短请求在长请求批次中 | 高延迟(等待长请求) | 低延迟(立即返回) |
| 新请求到达 | 高延迟(等待当前批次) | 低延迟(立即加入) |
| 首 token 延迟 | 高(等待调度) | 低(可立即开始) |
6.3 GPU 利用率对比
静态批处理:
GPU: ████░░░░░░████░░░░░░████
^处理 ^空闲 ^处理
连续批处理:
GPU: ████████████████████████
^持续高效运行
7. 实现细节
7.1 请求完成检测
# vllm/v1/core/sched/utils.py
def check_stop(request: Request, max_model_len: int) -> bool:
"""检查请求是否应该停止"""
# 检查 EOS token
if request.sampling_params.eos_token_id is not None:
if request._all_token_ids[-1] == request.sampling_params.eos_token_id:
return True
# 检查最大长度
if request.num_output_tokens >= request.max_tokens:
return True
# 检查模型最大长度
if len(request._all_token_ids) >= max_model_len:
return True
# 检查停止字符串
if request.sampling_params.stop:
output_text = request.get_output_text()
for stop_str in request.sampling_params.stop:
if stop_str in output_text:
return True
return False
7.2 动态资源释放
# vllm/v1/core/sched/scheduler.py
def update_from_output(self, model_output: ModelRunnerOutput) -> EngineCoreOutputs:
"""更新请求状态,处理完成的请求"""
outputs = []
finished_reqs = []
for req_id, sampler_output in model_output.items():
request = self.requests[req_id]
# 追加输出 token
request.append_output_token_ids(sampler_output.sampled_token_ids)
# 检查停止条件
if check_stop(request, self.max_model_len):
finished_reqs.append(request)
outputs.append(self._create_output(request))
# 释放完成请求的资源
for request in finished_reqs:
self._free_request(request)
self.running.remove(request)
return EngineCoreOutputs(outputs=outputs)
7.3 Token 预算管理
def schedule(self):
token_budget = self.max_num_scheduled_tokens
# 处理 running 请求
for request in self.running:
num_new_tokens = min(
request.num_tokens - request.num_computed_tokens,
token_budget
)
# 分配资源...
token_budget -= num_new_tokens
# 处理 waiting 请求
while self.waiting and token_budget > 0:
# 新请求可以使用剩余的预算
num_new_tokens = min(num_required, token_budget)
# 分配资源...
token_budget -= num_new_tokens
8. 最佳实践
8.1 配置建议
llm = LLM(
model="meta-llama/Llama-2-7b",
# 连续批处理相关
max_num_seqs=256, # 最大并发请求数
max_num_batched_tokens=4096, # 每步最大 token 数
# 分块预填充
enable_chunked_prefill=True,
long_prefill_token_threshold=256,
# 前缀缓存(配合连续批处理)
enable_prefix_caching=True,
)
8.2 监控指标
| 指标 | 说明 | 健康范围 |
|---|
running_requests | 运行中的请求数 | 接近 max_num_seqs |
waiting_requests | 等待中的请求数 | 越小越好 |
gpu_utilization | GPU 利用率 | > 80% |
tokens_per_second | 吞吐量 | 取决于模型和硬件 |
9. 与其他技术的协同
9.1 连续批处理 + PagedAttention
连续批处理: 请求可以动态加入/退出
PagedAttention: 内存可以动态分配/释放
结合效果:
- 请求完成 → 立即释放 KV Cache → 新请求立即使用
- 无内存碎片,无预分配浪费
9.2 连续批处理 + 前缀缓存
场景: 多个请求有相同的系统提示
请求 A: [系统提示] + [用户问题 A]
请求 B: [系统提示] + [用户问题 B]
结合效果:
- 请求 A 处理系统提示,缓存 KV
- 请求 B 直接复用缓存,跳过系统提示的计算
- 连续批处理允许请求 B 立即加入并利用缓存
9.3 连续批处理 + 投机解码
结合效果:
- Draft 模型快速生成多个候选 token
- Target 模型验证
- 验证失败的 token 被拒绝,但其他请求不受影响
- 持续高效的批处理执行
10. 代码位置速查
| 功能 | 文件 | 关键函数 |
|---|
| 主调度循环 | vllm/v1/engine/core.py | step() |
| 调度器 | vllm/v1/core/sched/scheduler.py | schedule() |
| 状态更新 | vllm/v1/core/sched/scheduler.py | update_from_output() |
| 完成检测 | vllm/v1/core/sched/utils.py | check_stop() |
| 请求释放 | vllm/v1/core/sched/scheduler.py | _free_request() |
11. 小结
本章我们深入了解了连续批处理机制:
- 静态批处理的问题:GPU 空闲、高延迟、低吞吐量
- 连续批处理的解决方案:迭代级调度,动态加入/退出
- vLLM 的实现:
- 每个 step 重新调度
- 完成的请求立即释放资源
- 新请求立即加入批次
- Prefill 与 Decode 混合:不同阶段的请求交替执行
- 分块预填充:长输入分成多个 chunk 处理
- 与其他技术协同:PagedAttention、前缀缓存、投机解码
连续批处理是 vLLM 实现高吞吐量的核心技术之一,结合 PagedAttention 和前缀缓存,使得 vLLM 能够高效地服务大量并发请求。
导航
4 - 代码链路分析
跟踪代码执行路径,理解实现细节
本部分将带你深入 vLLM 的源代码,通过分析关键代码路径,理解请求从创建到完成的完整生命周期。
4.1 - 入口点分析
入口点分析
本章开始,我们将从代码层面深入分析 vLLM 的执行流程。首先从入口点开始,了解用户如何与 vLLM 交互,以及请求是如何被处理的。
1. vLLM 的两种使用方式
vLLM 提供两种主要的使用方式:
graph TD
subgraph 离线推理
LLM[LLM 类]
Gen[generate/encode/...]
end
subgraph 在线服务
Server[API Server]
Async[AsyncLLMEngine]
end
User1[用户代码] --> LLM
LLM --> Gen
User2[HTTP 请求] --> Server
Server --> Async
style LLM fill:#e1f5fe
style Server fill:#fff3e0| 方式 | 入口类 | 适用场景 |
|---|
| 离线推理 | LLM | 批量处理、脚本调用、研究实验 |
| 在线服务 | AsyncLLMEngine | Web 服务、API 接口、实时应用 |
2. LLM 类 - 离线推理入口
2.1 类定义与初始化
# vllm/entrypoints/llm.py
class LLM:
"""用于从给定提示和采样参数生成文本的 LLM。
这个类包含一个分词器、一个语言模型(可能分布在多个 GPU 上),
以及为中间状态(即 KV Cache)分配的 GPU 内存空间。
"""
def __init__(
self,
model: str, # 模型路径或名称
*,
tokenizer: str | None = None, # 分词器路径
tokenizer_mode: str = "auto", # 分词器模式
trust_remote_code: bool = False, # 信任远程代码
tensor_parallel_size: int = 1, # 张量并行大小
dtype: str = "auto", # 数据类型
quantization: str | None = None, # 量化方法
gpu_memory_utilization: float = 0.9, # GPU 内存利用率
enable_prefix_caching: bool = False, # 启用前缀缓存
**kwargs, # 更多配置参数
):
# 1. 构建配置
engine_args = EngineArgs(
model=model,
tokenizer=tokenizer,
tensor_parallel_size=tensor_parallel_size,
dtype=dtype,
quantization=quantization,
gpu_memory_utilization=gpu_memory_utilization,
enable_prefix_caching=enable_prefix_caching,
**kwargs,
)
# 2. 创建 LLMEngine
self.llm_engine = LLMEngine.from_engine_args(engine_args)
# 3. 获取分词器
self.tokenizer = self.llm_engine.get_tokenizer()
# 4. 请求计数器
self.request_counter = Counter()
2.2 初始化流程
sequenceDiagram
participant User as 用户
participant LLM as LLM
participant Args as EngineArgs
participant Engine as LLMEngine
participant Core as EngineCore
participant Exec as Executor
User->>LLM: LLM(model, ...)
LLM->>Args: 构建 EngineArgs
LLM->>Engine: from_engine_args()
Engine->>Core: 创建 EngineCore
Core->>Exec: 创建 Executor
Exec->>Exec: 加载模型
Exec->>Exec: 分配 KV Cache
Core->>Core: 创建 Scheduler
Engine-->>LLM: engine 实例
LLM->>LLM: 获取 tokenizer
LLM-->>User: llm 实例2.3 generate() - 文本生成
# vllm/entrypoints/llm.py
def generate(
self,
prompts: PromptType | list[PromptType] | None = None,
sampling_params: SamplingParams | list[SamplingParams] | None = None,
prompt_token_ids: list[list[int]] | None = None,
use_tqdm: bool = True,
lora_request: LoRARequest | list[LoRARequest] | None = None,
) -> list[RequestOutput]:
"""生成文本的主方法。
Args:
prompts: 输入提示(字符串或多模态数据)
sampling_params: 采样参数(温度、top_k、top_p 等)
prompt_token_ids: 直接提供 token IDs
use_tqdm: 是否显示进度条
lora_request: LoRA 适配器请求
Returns:
RequestOutput 列表,包含生成的文本
"""
# 1. 参数验证和标准化
if prompts is None and prompt_token_ids is None:
raise ValueError("prompts or prompt_token_ids must be provided")
# 2. 处理采样参数
if sampling_params is None:
sampling_params = SamplingParams()
# 3. 添加请求到引擎
for i, (prompt, params) in enumerate(zip(prompts, sampling_params_list)):
request_id = str(next(self.request_counter))
self._add_request(
request_id=request_id,
prompt=prompt,
params=params,
lora_request=lora_request,
)
# 4. 运行引擎直到完成
return self._run_engine(use_tqdm=use_tqdm)
2.4 _run_engine() - 运行引擎
def _run_engine(self, use_tqdm: bool = True) -> list[RequestOutput]:
"""运行引擎直到所有请求完成"""
outputs: list[RequestOutput] = []
# 使用 tqdm 显示进度(可选)
pbar = tqdm(total=num_requests, disable=not use_tqdm)
while self.llm_engine.has_unfinished_requests():
# 执行一步
step_outputs = self.llm_engine.step()
for output in step_outputs:
if output.finished:
outputs.append(output)
pbar.update(1)
pbar.close()
# 按请求 ID 排序返回
return sorted(outputs, key=lambda x: int(x.request_id))
3. LLMEngine - 引擎核心
3.1 类结构
# vllm/v1/engine/llm_engine.py
class LLMEngine:
"""vLLM 的同步推理引擎"""
def __init__(self, vllm_config: VllmConfig, ...):
# 核心组件
self.engine_core: EngineCore # 内部循环
self.tokenizer: TokenizerLike # 分词器
self.input_processor # 输入处理器
@classmethod
def from_engine_args(cls, engine_args: EngineArgs) -> "LLMEngine":
"""从引擎参数创建实例(工厂方法)"""
vllm_config = engine_args.create_engine_config()
return cls(vllm_config, ...)
def add_request(
self,
request_id: str,
prompt: PromptType,
params: SamplingParams,
) -> None:
"""添加请求到引擎"""
...
def step(self) -> list[RequestOutput]:
"""执行一步推理"""
...
3.2 关键方法流程
graph TD
subgraph add_request
A1[接收请求] --> A2[处理输入]
A2 --> A3[tokenize]
A3 --> A4[创建 EngineCoreRequest]
A4 --> A5[发送到 EngineCore]
end
subgraph step
S1[调用 EngineCore.step] --> S2[获取输出]
S2 --> S3[detokenize]
S3 --> S4[构建 RequestOutput]
S4 --> S5[返回结果]
end
4. EngineCore - 内部循环
4.1 类定义
# vllm/v1/engine/core.py
class EngineCore:
"""vLLM 引擎的内部循环"""
def __init__(
self,
vllm_config: VllmConfig,
executor_class: type[Executor],
log_stats: bool,
...
):
# 1. 加载插件
from vllm.plugins import load_general_plugins
load_general_plugins()
# 2. 创建模型执行器
self.model_executor = executor_class(vllm_config)
# 3. 初始化 KV Cache
num_gpu_blocks, num_cpu_blocks, kv_cache_config = (
self._initialize_kv_caches(vllm_config)
)
vllm_config.cache_config.num_gpu_blocks = num_gpu_blocks
# 4. 初始化 Worker 端的 KV Cache
self.collective_rpc("initialize_cache", args=(num_gpu_blocks, num_cpu_blocks))
# 5. 创建调度器
Scheduler = vllm_config.scheduler_config.get_scheduler_cls()
self.scheduler = Scheduler(
vllm_config=vllm_config,
kv_cache_config=kv_cache_config,
...
)
4.2 step() - 核心执行循环
def step(self) -> EngineCoreOutputs:
"""执行一步推理"""
# 1. 调度:决定这一步处理哪些请求
scheduler_output = self.scheduler.schedule()
if scheduler_output.is_empty():
return EngineCoreOutputs([])
# 2. 执行模型
model_output = self.model_executor.execute_model(scheduler_output)
# 3. 采样
if not self.is_pooling_model:
sampled_tokens = self.model_executor.sample_tokens(model_output)
else:
sampled_tokens = None
# 4. 更新调度器状态
outputs = self.scheduler.update_from_output(
model_output,
sampled_tokens,
scheduler_output,
)
return outputs
4.3 执行流程图
sequenceDiagram
participant Core as EngineCore
participant Sched as Scheduler
participant KVM as KVCacheManager
participant Exec as Executor
participant GPU as GPU Worker
loop 每个 step
Core->>Sched: schedule()
rect rgb(230, 245, 230)
Note over Sched,KVM: 调度决策
Sched->>KVM: get_computed_blocks()
Sched->>KVM: allocate_slots()
Sched-->>Core: SchedulerOutput
end
rect rgb(245, 230, 230)
Note over Core,GPU: 模型执行
Core->>Exec: execute_model()
Exec->>GPU: 前向传播
GPU-->>Exec: logits
Exec-->>Core: ModelOutput
end
rect rgb(230, 230, 245)
Note over Core: 采样和更新
Core->>Exec: sample_tokens()
Exec-->>Core: sampled_tokens
Core->>Sched: update_from_output()
Sched-->>Core: EngineCoreOutputs
end
end
5. API Server - 在线服务入口
5.1 启动命令
# 启动 OpenAI 兼容的 API 服务
vllm serve meta-llama/Llama-2-7b --port 8000
# 或使用 Python
python -m vllm.entrypoints.openai.api_server --model meta-llama/Llama-2-7b
5.2 服务架构
graph TD
subgraph API Server
FastAPI[FastAPI 应用]
Router[路由器]
Middleware[中间件]
end
subgraph Endpoints
Chat[/v1/chat/completions]
Completions[/v1/completions]
Embeddings[/v1/embeddings]
Models[/v1/models]
end
subgraph Engine
AsyncEngine[AsyncLLMEngine]
end
Client[HTTP 客户端] --> FastAPI
FastAPI --> Router
Router --> Chat
Router --> Completions
Router --> Embeddings
Router --> Models
Chat --> AsyncEngine
Completions --> AsyncEngine
Embeddings --> AsyncEngine5.3 请求处理流程
sequenceDiagram
participant Client as HTTP 客户端
participant API as API Server
participant Async as AsyncLLMEngine
participant Core as EngineCore
Client->>API: POST /v1/chat/completions
API->>API: 验证请求
API->>API: 解析聊天消息
API->>API: 应用聊天模板
API->>Async: add_request()
Async->>Core: 添加到调度队列
loop 异步生成
Core->>Core: step()
Core-->>Async: 部分输出
alt 流式响应
Async-->>API: yield 部分结果
API-->>Client: SSE 事件
end
end
Core-->>Async: 完成输出
Async-->>API: 最终结果
API-->>Client: HTTP 响应
6. 请求数据结构
6.1 EngineCoreRequest
# vllm/v1/engine/__init__.py
@dataclass
class EngineCoreRequest:
"""从 LLMEngine 发送到 EngineCore 的请求"""
request_id: str # 唯一标识
prompt_token_ids: list[int] # prompt 的 token IDs
mm_inputs: list | None # 多模态输入
mm_hashes: list | None # 多模态内容的 hash
mm_positions: list | None # 多模态位置信息
sampling_params: SamplingParams # 采样参数
eos_token_id: int | None # 结束 token ID
arrival_time: float # 到达时间
lora_request: LoRARequest | None # LoRA 请求
6.2 Request(调度器内部)
# vllm/v1/request.py
class Request:
"""调度器内部的请求表示"""
def __init__(self, ...):
self.request_id: str
self.prompt_token_ids: list[int]
self.sampling_params: SamplingParams
# 状态跟踪
self.status: RequestStatus
self.num_computed_tokens: int
self._output_token_ids: list[int]
# 内存管理相关
self.block_hashes: list[BlockHash]
@property
def num_tokens(self) -> int:
"""当前总 token 数"""
return len(self.prompt_token_ids) + len(self._output_token_ids)
@property
def num_output_tokens(self) -> int:
"""输出 token 数"""
return len(self._output_token_ids)
6.3 请求状态机
stateDiagram-v2
[*] --> WAITING: add_request()
WAITING --> RUNNING: 调度成功
WAITING --> WAITING_FOR_FSM: 需要 FSM 编译
WAITING --> WAITING_FOR_REMOTE_KVS: 等待远程 KV
WAITING_FOR_FSM --> WAITING: FSM 就绪
WAITING_FOR_REMOTE_KVS --> WAITING: KV 就绪
RUNNING --> FINISHED_STOPPED: 达到停止条件
RUNNING --> FINISHED_LENGTH: 达到最大长度
RUNNING --> FINISHED_ABORTED: 被中止
RUNNING --> PREEMPTED: 被抢占
PREEMPTED --> WAITING: 重新排队
FINISHED_STOPPED --> [*]
FINISHED_LENGTH --> [*]
FINISHED_ABORTED --> [*]
7. 配置系统
7.1 EngineArgs
# vllm/engine/arg_utils.py
@dataclass
class EngineArgs:
"""引擎配置参数"""
# 模型配置
model: str
tokenizer: str | None = None
revision: str | None = None
dtype: str = "auto"
quantization: str | None = None
# 并行配置
tensor_parallel_size: int = 1
pipeline_parallel_size: int = 1
# 内存配置
gpu_memory_utilization: float = 0.9
max_model_len: int | None = None
block_size: int = 16
# 调度配置
max_num_seqs: int = 256
max_num_batched_tokens: int = 2048
# 功能开关
enable_prefix_caching: bool = False
enable_chunked_prefill: bool = False
7.2 VllmConfig
# vllm/config.py
@dataclass
class VllmConfig:
"""vLLM 的完整配置"""
model_config: ModelConfig # 模型配置
cache_config: CacheConfig # 缓存配置
parallel_config: ParallelConfig # 并行配置
scheduler_config: SchedulerConfig # 调度配置
device_config: DeviceConfig # 设备配置
load_config: LoadConfig # 加载配置
lora_config: LoRAConfig | None # LoRA 配置
speculative_config: SpeculativeConfig | None # 投机解码配置
8. 代码位置速查
| 组件 | 文件 | 关键类/函数 |
|---|
| LLM 入口 | vllm/entrypoints/llm.py | LLM 类 |
| LLMEngine | vllm/v1/engine/llm_engine.py | LLMEngine 类 |
| EngineCore | vllm/v1/engine/core.py | EngineCore 类 |
| API Server | vllm/entrypoints/openai/api_server.py | main() |
| 配置参数 | vllm/engine/arg_utils.py | EngineArgs |
| 请求类 | vllm/v1/request.py | Request 类 |
| 请求状态 | vllm/v1/request.py | RequestStatus 枚举 |
9. 小结
本章我们了解了 vLLM 的入口点和请求处理流程:
两种使用方式:
LLM 类用于离线批量推理- API Server 用于在线服务
核心组件层次:
LLM → LLMEngine → EngineCore → Scheduler + Executor
请求生命周期:
- 用户提交 → tokenize → 调度 → 执行 → 采样 → 返回
配置系统:
EngineArgs → VllmConfig → 各子配置
在下一章中,我们将深入 Executor 和 Worker 的实现,了解模型是如何在 GPU 上执行的。
导航
4.2 - 执行器与 Worker
Executor 与 Worker 详解
在上一章中,我们了解了 vLLM 的入口点和请求处理流程。本章我们将深入 Executor 和 Worker 层,了解模型是如何在 GPU 上执行的。
1. Executor 与 Worker 的关系
graph TD
subgraph EngineCore
Core[EngineCore]
Sched[Scheduler]
end
subgraph Executor 层
Exec[Executor]
end
subgraph Worker 层
W0[Worker 0<br/>GPU 0]
W1[Worker 1<br/>GPU 1]
W2[Worker 2<br/>GPU 2]
WN[Worker N<br/>GPU N]
end
Core --> Exec
Core <--> Sched
Exec --> W0
Exec --> W1
Exec --> W2
Exec --> WN
style Exec fill:#e1f5fe
style W0 fill:#fff3e0
style W1 fill:#fff3e0
style W2 fill:#fff3e0
style WN fill:#fff3e0职责划分:
| 组件 | 职责 |
|---|
| Executor | 管理多个 Worker,协调分布式执行 |
| Worker | 在单个 GPU 上执行模型推理 |
2. Executor 抽象基类
2.1 类定义
# vllm/v1/executor/abstract.py
class Executor(ABC):
"""vLLM Executor 的抽象基类
Executor 负责在一个或多个设备上执行模型。
"""
uses_ray: bool = False # 是否使用 Ray
supports_pp: bool = False # 是否支持流水线并行
def __init__(self, vllm_config: VllmConfig) -> None:
self.vllm_config = vllm_config
self.model_config = vllm_config.model_config
self.cache_config = vllm_config.cache_config
self.parallel_config = vllm_config.parallel_config
...
self._init_executor() # 子类实现
@abstractmethod
def _init_executor(self) -> None:
"""初始化 Executor(由子类实现)"""
raise NotImplementedError
@abstractmethod
def collective_rpc(
self,
method: str | Callable,
args: tuple = (),
kwargs: dict | None = None,
) -> list:
"""在所有 Worker 上执行 RPC 调用"""
raise NotImplementedError
def execute_model(
self,
scheduler_output: SchedulerOutput,
) -> ModelRunnerOutput | None:
"""执行模型推理"""
return self.collective_rpc(
"execute_model",
args=(scheduler_output,),
)[0]
2.2 Executor 工厂方法
@staticmethod
def get_class(vllm_config: VllmConfig) -> type["Executor"]:
"""根据配置获取合适的 Executor 类"""
distributed_executor_backend = vllm_config.parallel_config.distributed_executor_backend
if distributed_executor_backend == "ray":
from vllm.v1.executor.ray_executor import RayDistributedExecutor
return RayDistributedExecutor
elif distributed_executor_backend == "mp":
from vllm.v1.executor.multiproc_executor import MultiprocExecutor
return MultiprocExecutor
elif distributed_executor_backend == "uni":
from vllm.v1.executor.uniproc_executor import UniProcExecutor
return UniProcExecutor
else:
raise ValueError(f"Unknown executor backend: {distributed_executor_backend}")
3. Executor 实现类型
3.1 类型对比
graph TD
Executor[Executor 抽象基类]
Executor --> Uni[UniProcExecutor<br/>单进程单 GPU]
Executor --> MP[MultiprocExecutor<br/>多进程多 GPU]
Executor --> Ray[RayDistributedExecutor<br/>Ray 分布式]
Uni -.-> Single[单 GPU 场景]
MP -.-> Multi[单机多卡场景]
Ray -.-> Distributed[多机分布式场景]
style Uni fill:#c8e6c9
style MP fill:#fff3e0
style Ray fill:#e1f5fe3.2 UniProcExecutor - 单进程
# vllm/v1/executor/uniproc_executor.py
class UniProcExecutor(Executor):
"""单进程 Executor,用于单 GPU 场景"""
def _init_executor(self) -> None:
# 直接在当前进程创建 Worker
self.driver_worker = Worker(
vllm_config=self.vllm_config,
local_rank=0,
rank=0,
distributed_init_method="",
is_driver_worker=True,
)
self.driver_worker.init_device()
self.driver_worker.load_model()
def collective_rpc(self, method, args=(), kwargs=None):
"""直接调用 Worker 方法"""
if isinstance(method, str):
func = getattr(self.driver_worker, method)
else:
func = lambda: method(self.driver_worker)
return [func(*args, **(kwargs or {}))]
3.3 MultiprocExecutor - 多进程
# vllm/v1/executor/multiproc_executor.py
class MultiprocExecutor(Executor):
"""多进程 Executor,用于单机多卡场景"""
supports_pp = True
def _init_executor(self) -> None:
# 创建多个 Worker 进程
self.workers = []
for rank in range(self.parallel_config.world_size):
worker = self._create_worker(rank)
self.workers.append(worker)
def collective_rpc(self, method, args=(), kwargs=None):
"""并行调用所有 Worker"""
futures = []
for worker in self.workers:
future = worker.execute_method(method, args, kwargs)
futures.append(future)
# 等待所有 Worker 完成
results = [f.result() for f in futures]
return results
3.4 RayDistributedExecutor - Ray 分布式
# vllm/v1/executor/ray_executor.py
class RayDistributedExecutor(Executor):
"""Ray 分布式 Executor,用于多机场景"""
uses_ray = True
supports_pp = True
def _init_executor(self) -> None:
import ray
# 创建 Ray Actor(远程 Worker)
self.workers = []
for rank in range(self.parallel_config.world_size):
worker_actor = ray.remote(Worker).remote(
vllm_config=self.vllm_config,
rank=rank,
...
)
self.workers.append(worker_actor)
def collective_rpc(self, method, args=(), kwargs=None):
"""通过 Ray 调用远程 Worker"""
import ray
refs = []
for worker in self.workers:
ref = getattr(worker, method).remote(*args, **(kwargs or {}))
refs.append(ref)
# 异步获取结果
results = ray.get(refs)
return results
4. Worker 详解
4.1 Worker 基类
# vllm/v1/worker/worker_base.py
class WorkerBase(ABC):
"""Worker 抽象基类"""
def __init__(
self,
vllm_config: VllmConfig,
local_rank: int, # 本地 GPU 序号
rank: int, # 全局 Worker 序号
distributed_init_method: str,
is_driver_worker: bool = False,
):
self.vllm_config = vllm_config
self.local_rank = local_rank
self.rank = rank
self.is_driver_worker = is_driver_worker
@abstractmethod
def init_device(self) -> None:
"""初始化设备(GPU)"""
raise NotImplementedError
@abstractmethod
def load_model(self) -> None:
"""加载模型"""
raise NotImplementedError
@abstractmethod
def execute_model(
self,
scheduler_output: SchedulerOutput,
) -> ModelRunnerOutput:
"""执行模型推理"""
raise NotImplementedError
4.2 GPU Worker
# vllm/v1/worker/gpu_worker.py
class Worker(WorkerBase):
"""GPU Worker 实现"""
def __init__(
self,
vllm_config: VllmConfig,
local_rank: int,
rank: int,
distributed_init_method: str,
is_driver_worker: bool = False,
):
super().__init__(...)
# 配置 float32 精度
precision = envs.VLLM_FLOAT32_MATMUL_PRECISION
torch.set_float32_matmul_precision(precision)
# Profiler(可选)
self.profiler = self._setup_profiler()
def init_device(self):
"""初始化 GPU 设备"""
# 设置 CUDA 设备
torch.cuda.set_device(self.local_rank)
self.device = torch.device(f"cuda:{self.local_rank}")
# 初始化分布式环境
init_distributed_environment(
world_size=self.parallel_config.world_size,
rank=self.rank,
distributed_init_method=self.distributed_init_method,
backend="nccl",
)
# 初始化模型并行
ensure_model_parallel_initialized(
tensor_model_parallel_size=self.parallel_config.tensor_parallel_size,
pipeline_model_parallel_size=self.parallel_config.pipeline_parallel_size,
)
def load_model(self):
"""加载模型到 GPU"""
with self._maybe_get_memory_pool_context("weights"):
# 创建 ModelRunner
self.model_runner = GPUModelRunner(
vllm_config=self.vllm_config,
device=self.device,
)
# 加载模型权重
self.model_runner.load_model()
def execute_model(
self,
scheduler_output: SchedulerOutput,
) -> ModelRunnerOutput:
"""执行模型推理"""
return self.model_runner.execute_model(scheduler_output)
4.3 Worker 初始化流程
sequenceDiagram
participant Exec as Executor
participant Worker as Worker
participant Device as CUDA Device
participant Model as ModelRunner
Exec->>Worker: 创建 Worker
Worker->>Worker: __init__()
Exec->>Worker: init_device()
Worker->>Device: torch.cuda.set_device()
Worker->>Device: init_distributed_environment()
Worker->>Device: ensure_model_parallel_initialized()
Note over Device: NCCL 初始化完成
Exec->>Worker: load_model()
Worker->>Model: 创建 GPUModelRunner
Model->>Model: load_model()
Note over Model: 模型权重加载到 GPU
Exec->>Worker: initialize_cache()
Worker->>Worker: 分配 KV Cache 内存
5. ModelRunner 详解
5.1 GPUModelRunner 类
# vllm/v1/worker/gpu_model_runner.py
class GPUModelRunner:
"""GPU 模型执行器"""
def __init__(
self,
vllm_config: VllmConfig,
device: torch.device,
):
self.vllm_config = vllm_config
self.device = device
self.model: nn.Module | None = None
# KV Cache 配置
self.kv_caches: list[torch.Tensor] = []
self.block_size = vllm_config.cache_config.block_size
# 输入处理
self.input_batch = GPUInputBatch(...)
def load_model(self):
"""加载模型"""
from vllm.model_executor.model_loader import get_model
self.model = get_model(
model_config=self.model_config,
load_config=self.load_config,
device_config=self.device_config,
parallel_config=self.parallel_config,
scheduler_config=self.scheduler_config,
)
def execute_model(
self,
scheduler_output: SchedulerOutput,
) -> ModelRunnerOutput:
"""执行模型前向传播"""
# 1. 准备输入
model_input = self._prepare_inputs(scheduler_output)
# 2. 执行前向传播
with torch.inference_mode():
hidden_states = self.model(
input_ids=model_input.input_ids,
positions=model_input.positions,
kv_caches=self.kv_caches,
attn_metadata=model_input.attn_metadata,
)
# 3. 计算 logits
logits = self.model.compute_logits(hidden_states)
# 4. 返回输出
return ModelRunnerOutput(
logits=logits,
...
)
5.2 execute_model 流程
flowchart TD
A[execute_model 开始] --> B[_prepare_inputs]
subgraph 输入准备
B --> B1[处理 token IDs]
B1 --> B2[计算位置编码]
B2 --> B3[构建 attention metadata]
B3 --> B4[更新 block table]
end
B4 --> C[model forward]
subgraph 前向传播
C --> C1[Embedding]
C1 --> C2[Transformer Layers]
C2 --> C3[每层: Attention + FFN]
C3 --> C4[LM Head]
end
C4 --> D[compute_logits]
D --> E[返回 ModelRunnerOutput]
6. KV Cache 管理
6.1 Worker 端的 KV Cache
# vllm/v1/worker/gpu_worker.py
def initialize_cache(self, num_gpu_blocks: int, num_cpu_blocks: int) -> None:
"""初始化 KV Cache"""
self.cache_config.num_gpu_blocks = num_gpu_blocks
self.cache_config.num_cpu_blocks = num_cpu_blocks
def initialize_from_config(self, kv_cache_configs: list[KVCacheConfig]) -> None:
"""根据配置初始化 KV Cache"""
self.model_runner.initialize_kv_cache(kv_cache_configs)
6.2 KV Cache Tensor 布局
# KV Cache 的形状
# [num_blocks, 2, num_heads, block_size, head_dim]
# ↑ ↑ ↑ ↑ ↑
# 块数量 K和V 注意力头数 块大小 头维度
# 示例:16 块,8 头,16 token/块,128 维
kv_cache_shape = (16, 2, 8, 16, 128)
kv_cache = torch.empty(kv_cache_shape, dtype=torch.float16, device="cuda")
6.3 Block Table 使用
# vllm/v1/worker/block_table.py
class BlockTable:
"""管理每个序列的 block 映射"""
def __init__(self, max_num_seqs: int, max_num_blocks_per_seq: int):
# [max_num_seqs, max_num_blocks_per_seq]
self.block_table = torch.zeros(
(max_num_seqs, max_num_blocks_per_seq),
dtype=torch.int32,
device="cuda",
)
def update(self, req_index: int, block_ids: list[int]):
"""更新请求的 block 映射"""
num_blocks = len(block_ids)
self.block_table[req_index, :num_blocks] = torch.tensor(
block_ids, dtype=torch.int32
)
7. 分布式执行
7.1 张量并行(Tensor Parallelism)
graph LR
subgraph GPU 0
A0[输入] --> L0[Linear 分片 0]
L0 --> R0[部分结果]
end
subgraph GPU 1
A1[输入] --> L1[Linear 分片 1]
L1 --> R1[部分结果]
end
R0 --> AllReduce
R1 --> AllReduce
AllReduce --> Output[完整输出]7.2 流水线并行(Pipeline Parallelism)
graph LR
subgraph Stage 0 - GPU 0
L0_6[Layers 0-5]
end
subgraph Stage 1 - GPU 1
L6_12[Layers 6-11]
end
subgraph Stage 2 - GPU 2
L12_18[Layers 12-17]
end
subgraph Stage 3 - GPU 3
L18_24[Layers 18-23]
end
Input --> L0_6
L0_6 -->|激活值| L6_12
L6_12 -->|激活值| L12_18
L12_18 -->|激活值| L18_24
L18_24 --> Output7.3 collective_rpc 通信
sequenceDiagram
participant Exec as Executor
participant W0 as Worker 0
participant W1 as Worker 1
participant W2 as Worker 2
Exec->>W0: execute_model(scheduler_output)
Exec->>W1: execute_model(scheduler_output)
Exec->>W2: execute_model(scheduler_output)
Note over W0,W2: 并行执行前向传播
par TP AllReduce
W0->>W1: 交换中间结果
W1->>W2: 交换中间结果
W2->>W0: 交换中间结果
end
W0-->>Exec: result_0
W1-->>Exec: result_1
W2-->>Exec: result_2
Note over Exec: 只使用 driver worker 的结果
8. 性能优化
8.1 CUDA Graph
# vllm/v1/worker/gpu_model_runner.py
def _capture_cuda_graph(self, ...):
"""捕获 CUDA Graph 以减少启动开销"""
# 预热
for _ in range(3):
self.model(...)
# 捕获
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
output = self.model(...)
return graph, output
def execute_model_with_cuda_graph(self, ...):
"""使用 CUDA Graph 执行"""
# 重放预捕获的计算图
self.cuda_graph.replay()
return self.graph_output
8.2 内存优化
# Worker 的 sleep/wake_up 机制
def sleep(self, level: int = 1) -> None:
"""释放内存进入睡眠模式"""
from vllm.device_allocator.cumem import CuMemAllocator
allocator = CuMemAllocator.get_instance()
if level == 2:
# 保存 buffer 到 CPU
self._sleep_saved_buffers = {
name: buffer.cpu().clone()
for name, buffer in self.model.named_buffers()
}
# 释放 GPU 内存
allocator.sleep(offload_tags=("weights",) if level == 1 else tuple())
def wake_up(self, tags: list[str] | None = None) -> None:
"""从睡眠模式唤醒"""
allocator = CuMemAllocator.get_instance()
allocator.wake_up(tags)
# 恢复 buffer
if self._sleep_saved_buffers:
for name, buffer in self.model.named_buffers():
buffer.data.copy_(self._sleep_saved_buffers[name].data)
9. 代码位置速查
| 组件 | 文件 | 关键类/函数 |
|---|
| Executor 基类 | vllm/v1/executor/abstract.py | Executor |
| 单进程 Executor | vllm/v1/executor/uniproc_executor.py | UniProcExecutor |
| 多进程 Executor | vllm/v1/executor/multiproc_executor.py | MultiprocExecutor |
| Ray Executor | vllm/v1/executor/ray_executor.py | RayDistributedExecutor |
| Worker 基类 | vllm/v1/worker/worker_base.py | WorkerBase |
| GPU Worker | vllm/v1/worker/gpu_worker.py | Worker |
| GPU ModelRunner | vllm/v1/worker/gpu_model_runner.py | GPUModelRunner |
| Block Table | vllm/v1/worker/block_table.py | BlockTable |
10. 小结
本章我们深入了解了 Executor 和 Worker 层:
Executor 类型:
UniProcExecutor:单进程单 GPUMultiprocExecutor:单机多卡RayDistributedExecutor:多机分布式
Worker 职责:
ModelRunner:
分布式执行:
- 张量并行:切分权重矩阵
- 流水线并行:切分模型层
- collective_rpc:跨 Worker 通信
在下一章中,我们将深入模型前向传播的具体实现。
导航
4.3 - 模型前向传播
模型前向传播详解
在前面的章节中,我们了解了 vLLM 的架构和调度机制。本章将深入模型的前向传播过程,以 Llama 模型为例,详细分析从输入到输出的完整计算流程。
1. 模型架构概览
1.1 Llama 模型结构
graph TD
subgraph LlamaForCausalLM
Input[input_ids] --> Embed[Embedding]
Embed --> Layers[Transformer Layers × N]
Layers --> Norm[RMSNorm]
Norm --> LMHead[LM Head]
LMHead --> Logits[logits]
end
subgraph "Transformer Layer"
H[hidden_states] --> LN1[RMSNorm]
LN1 --> Attn[Self-Attention]
Attn --> Add1[+]
H --> Add1
Add1 --> LN2[RMSNorm]
LN2 --> MLP[MLP]
MLP --> Add2[+]
Add1 --> Add2
Add2 --> Out[output]
end1.2 核心组件对应关系
| 组件 | vLLM 类 | 功能 |
|---|
| Embedding | VocabParallelEmbedding | token 到向量的映射 |
| Transformer Layer | LlamaDecoderLayer | 主要计算单元 |
| Self-Attention | LlamaAttention | 注意力计算 |
| MLP | LlamaMLP | 前馈网络 |
| LayerNorm | RMSNorm | 归一化 |
| LM Head | ParallelLMHead | 输出词表概率 |
2. vLLM 中的 Llama 实现
2.1 LlamaForCausalLM 类
# vllm/model_executor/models/llama.py
class LlamaForCausalLM(nn.Module, SupportsLoRA, SupportsPP, SupportsEagle):
"""用于因果语言建模的 Llama 模型"""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
quant_config = vllm_config.quant_config
# 主模型(Transformer 层)
self.model = LlamaModel(vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model"))
# 输出层(LM Head)
if config.tie_word_embeddings:
self.lm_head = self.model.embed_tokens # 权重共享
else:
self.lm_head = ParallelLMHead(
config.vocab_size,
config.hidden_size,
quant_config=quant_config,
)
# Logits 处理器
self.logits_processor = LogitsProcessor(config.vocab_size)
# 采样器
self.sampler = get_sampler()
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
kv_caches: list[torch.Tensor],
attn_metadata: AttentionMetadata,
intermediate_tensors: IntermediateTensors | None = None,
) -> torch.Tensor | IntermediateTensors:
"""模型前向传播"""
# 1. 通过 Transformer 层
hidden_states = self.model(input_ids, positions, intermediate_tensors)
return hidden_states
def compute_logits(
self,
hidden_states: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> torch.Tensor:
"""计算 logits"""
logits = self.logits_processor(
self.lm_head,
hidden_states,
sampling_metadata,
)
return logits
def sample(
self,
logits: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> SamplerOutput:
"""采样下一个 token"""
next_tokens = self.sampler(logits, sampling_metadata)
return next_tokens
2.2 LlamaModel 类
# vllm/model_executor/models/llama.py
@support_torch_compile(shape_invariants=llama_model_invariants)
class LlamaModel(nn.Module):
"""Llama 的 Transformer 模型"""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
# Embedding 层
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
)
# Transformer 层
self.start_layer, self.end_layer, self.layers = make_layers(
config.num_hidden_layers,
lambda prefix: LlamaDecoderLayer(vllm_config=vllm_config, prefix=prefix),
prefix=f"{prefix}.layers",
)
# 最终归一化
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
input_ids: torch.Tensor | None,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor | IntermediateTensors:
# 1. Embedding
if get_pp_group().is_first_rank:
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.embed_tokens(input_ids)
residual = None
else:
# 流水线并行:从前一阶段获取中间结果
hidden_states = intermediate_tensors["hidden_states"]
residual = intermediate_tensors["residual"]
# 2. Transformer 层
for layer in self.layers[self.start_layer:self.end_layer]:
hidden_states, residual = layer(
positions,
hidden_states,
residual,
)
# 3. 最终归一化(仅最后一个 PP 阶段)
if not get_pp_group().is_last_rank:
return IntermediateTensors({
"hidden_states": hidden_states,
"residual": residual
})
hidden_states, _ = self.norm(hidden_states, residual)
return hidden_states
3.1 LlamaDecoderLayer
# vllm/model_executor/models/llama.py
class LlamaDecoderLayer(nn.Module):
"""单个 Transformer 解码器层"""
def __init__(self, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
# Self-Attention
self.self_attn = LlamaAttention(
config=config,
hidden_size=config.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=config.num_key_value_heads,
cache_config=vllm_config.cache_config,
prefix=f"{prefix}.self_attn",
)
# MLP
self.mlp = LlamaMLP(
hidden_size=config.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
prefix=f"{prefix}.mlp",
)
# 归一化层
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
residual: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
层前向传播
使用 Pre-LN 结构(LayerNorm 在子层之前)
"""
# 1. 第一个归一化 + 残差处理
if residual is None:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
else:
hidden_states, residual = self.input_layernorm(hidden_states, residual)
# 2. Self-Attention
hidden_states = self.self_attn(
positions=positions,
hidden_states=hidden_states,
)
# 3. 第二个归一化 + 残差处理
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
# 4. MLP
hidden_states = self.mlp(hidden_states)
return hidden_states, residual
3.2 前向传播数据流
flowchart TD
subgraph "LlamaDecoderLayer Forward"
Input["输入<br/>(hidden_states, residual)"]
Input --> Check{residual is None?}
Check -->|是| Init["residual = hidden_states"]
Check -->|否| Fuse1["hidden_states, residual =<br/>input_layernorm(hidden_states, residual)"]
Init --> LN1["hidden_states = input_layernorm(hidden_states)"]
LN1 --> Attn["hidden_states = self_attn(positions, hidden_states)"]
Fuse1 --> Attn
Attn --> Fuse2["hidden_states, residual =<br/>post_attention_layernorm(hidden_states, residual)"]
Fuse2 --> MLP["hidden_states = mlp(hidden_states)"]
MLP --> Output["输出<br/>(hidden_states, residual)"]
end
style Attn fill:#e1f5fe
style MLP fill:#fff3e0
4. Self-Attention 详解
4.1 LlamaAttention 类
# vllm/model_executor/models/llama.py
class LlamaAttention(nn.Module):
"""Llama 的多头注意力层"""
def __init__(
self,
config: LlamaConfig,
hidden_size: int,
num_heads: int,
num_kv_heads: int,
cache_config: CacheConfig | None = None,
prefix: str = "",
):
super().__init__()
# 张量并行配置
tp_size = get_tensor_model_parallel_world_size()
self.num_heads = num_heads // tp_size
self.num_kv_heads = max(1, num_kv_heads // tp_size)
self.head_dim = hidden_size // num_heads
self.scaling = self.head_dim ** -0.5
# Q、K、V 投影(合并为一个线性层)
self.qkv_proj = QKVParallelLinear(
hidden_size=hidden_size,
head_size=self.head_dim,
total_num_heads=num_heads,
total_num_kv_heads=num_kv_heads,
prefix=f"{prefix}.qkv_proj",
)
# 输出投影
self.o_proj = RowParallelLinear(
input_size=num_heads * self.head_dim,
output_size=hidden_size,
prefix=f"{prefix}.o_proj",
)
# 旋转位置编码
self.rotary_emb = get_rope(
self.head_dim,
max_position=config.max_position_embeddings,
)
# 注意力后端
self.attn = Attention(
self.num_heads,
self.head_dim,
self.scaling,
num_kv_heads=self.num_kv_heads,
cache_config=cache_config,
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
) -> torch.Tensor:
"""
注意力前向传播
Args:
positions: 位置 IDs [num_tokens]
hidden_states: 隐藏状态 [num_tokens, hidden_size]
Returns:
输出 [num_tokens, hidden_size]
"""
# 1. QKV 投影
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
# 2. 旋转位置编码
q, k = self.rotary_emb(positions, q, k)
# 3. 注意力计算(使用 vLLM 的优化后端)
attn_output = self.attn(q, k, v)
# 4. 输出投影
output, _ = self.o_proj(attn_output)
return output
4.2 注意力计算流程
flowchart LR
subgraph "Self-Attention"
H["hidden_states<br/>[tokens, hidden]"]
QKV["QKV Projection"]
Split["Split"]
Q["Q"]
K["K"]
V["V"]
RoPE["RoPE"]
Attn["Attention<br/>(with KV Cache)"]
O["O Projection"]
Out["output"]
H --> QKV
QKV --> Split
Split --> Q
Split --> K
Split --> V
Q --> RoPE
K --> RoPE
RoPE --> Attn
V --> Attn
Attn --> O
O --> Out
end4.3 GQA (Grouped-Query Attention)
标准 MHA (Multi-Head Attention):
Q heads: [h1, h2, h3, h4, h5, h6, h7, h8]
K heads: [h1, h2, h3, h4, h5, h6, h7, h8]
V heads: [h1, h2, h3, h4, h5, h6, h7, h8]
GQA (num_kv_heads=2):
Q heads: [h1, h2, h3, h4, h5, h6, h7, h8]
K heads: [k1, k1, k1, k1, | k2, k2, k2, k2]
V heads: [v1, v1, v1, v1, | v2, v2, v2, v2]
每 4 个 Q head 共享 1 个 KV head,减少 KV Cache 内存
5. MLP 详解
5.1 LlamaMLP 类
# vllm/model_executor/models/llama.py
class LlamaMLP(nn.Module):
"""Llama 的 MLP(SwiGLU 激活)"""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
prefix: str = "",
):
super().__init__()
# gate 和 up 投影(合并)
self.gate_up_proj = MergedColumnParallelLinear(
input_size=hidden_size,
output_sizes=[intermediate_size] * 2, # gate 和 up 各一个
prefix=f"{prefix}.gate_up_proj",
)
# down 投影
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
prefix=f"{prefix}.down_proj",
)
# SiLU 激活 + 门控
self.act_fn = SiluAndMul()
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
MLP 前向传播
SwiGLU: down(SiLU(gate(x)) * up(x))
"""
# 1. gate 和 up 投影(合并执行)
gate_up, _ = self.gate_up_proj(x)
# 2. SiLU 激活 + 门控相乘
x = self.act_fn(gate_up)
# 3. down 投影
x, _ = self.down_proj(x)
return x
5.2 SwiGLU 计算流程
flowchart LR
subgraph "MLP (SwiGLU)"
X["x<br/>[tokens, hidden]"]
GU["gate_up_proj"]
Gate["gate"]
Up["up"]
SiLU["SiLU(gate)"]
Mul["×"]
Down["down_proj"]
Out["output"]
X --> GU
GU --> Gate
GU --> Up
Gate --> SiLU
SiLU --> Mul
Up --> Mul
Mul --> Down
Down --> Out
end
6. KV Cache 集成
6.1 Attention 层的 KV Cache 使用
# vllm/attention/layer.py
class Attention(nn.Module):
"""vLLM 的注意力层,集成 KV Cache"""
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
) -> torch.Tensor:
"""
Args:
query: [num_tokens, num_heads * head_dim]
key: [num_tokens, num_kv_heads * head_dim]
value: [num_tokens, num_kv_heads * head_dim]
内部会:
1. 将新的 K、V 写入 KV Cache
2. 从 KV Cache 读取完整的 K、V
3. 计算注意力
"""
return self.impl.forward(
query,
key,
value,
kv_cache=self.kv_cache,
attn_metadata=self.attn_metadata,
)
6.2 PagedAttention 后端
sequenceDiagram
participant Attn as Attention Layer
participant PA as PagedAttention
participant KVC as KV Cache
Attn->>PA: forward(Q, K, V)
rect rgb(230, 245, 230)
Note over PA,KVC: 写入新的 K、V
PA->>KVC: 根据 slot_mapping 写入
Note over KVC: K、V 存储到对应的 block
end
rect rgb(245, 230, 230)
Note over PA,KVC: 读取完整的 K、V
PA->>KVC: 根据 block_table 读取
Note over KVC: 返回所有历史 K、V
end
rect rgb(230, 230, 245)
Note over PA: 计算注意力
PA->>PA: attention = softmax(Q @ K^T / sqrt(d)) @ V
end
PA-->>Attn: attention output
7. 张量并行实现
7.1 线性层的张量并行
# vllm/model_executor/layers/linear.py
class ColumnParallelLinear(nn.Module):
"""列并行线性层(输出切分)"""
def __init__(self, input_size: int, output_size: int, ...):
tp_size = get_tensor_model_parallel_world_size()
# 每个 GPU 只有 output_size / tp_size 的输出
self.output_size_per_partition = output_size // tp_size
self.weight = Parameter(torch.empty(
self.output_size_per_partition,
input_size,
))
class RowParallelLinear(nn.Module):
"""行并行线性层(输入切分)"""
def __init__(self, input_size: int, output_size: int, ...):
tp_size = get_tensor_model_parallel_world_size()
# 每个 GPU 只处理 input_size / tp_size 的输入
self.input_size_per_partition = input_size // tp_size
self.weight = Parameter(torch.empty(
output_size,
self.input_size_per_partition,
))
def forward(self, x: torch.Tensor) -> torch.Tensor:
output = F.linear(x, self.weight)
if self.reduce_results:
# AllReduce 收集所有 GPU 的结果
output = tensor_model_parallel_all_reduce(output)
return output
7.2 QKV 投影的张量并行
graph TD
subgraph GPU 0
Q0["Q[:, :dim/2]"]
K0["K[:, :dim/4]"]
V0["V[:, :dim/4]"]
end
subgraph GPU 1
Q1["Q[:, dim/2:]"]
K1["K[:, dim/4:]"]
V1["V[:, dim/4:]"]
end
Input["hidden_states"] --> GPU0
Input --> GPU1
Q0 --> Attn0["Attention 0"]
K0 --> Attn0
V0 --> Attn0
Q1 --> Attn1["Attention 1"]
K1 --> Attn1
V1 --> Attn1
Attn0 --> AllReduce
Attn1 --> AllReduce
AllReduce --> Output["output"]
8. 完整前向传播流程
flowchart TD
subgraph "execute_model"
Input["SchedulerOutput"]
Prep["_prepare_inputs()"]
FWD["model.forward()"]
Logits["compute_logits()"]
Sample["sample()"]
Output["ModelRunnerOutput"]
Input --> Prep
Prep --> FWD
FWD --> Logits
Logits --> Sample
Sample --> Output
end
subgraph "model.forward()"
Embed["embed_tokens(input_ids)"]
Layers["for layer in layers:<br/> hidden, residual = layer(...)"]
Norm["norm(hidden, residual)"]
Embed --> Layers
Layers --> Norm
end
subgraph "layer.forward()"
LN1["input_layernorm"]
Attn["self_attn(Q,K,V + KV Cache)"]
LN2["post_attention_layernorm"]
MLP["mlp(SwiGLU)"]
LN1 --> Attn
Attn --> LN2
LN2 --> MLP
end
FWD --> Embed
Norm --> Logits
9. 代码位置速查
| 组件 | 文件 | 关键类/函数 |
|---|
| Llama 模型 | vllm/model_executor/models/llama.py | LlamaForCausalLM |
| Transformer 层 | vllm/model_executor/models/llama.py | LlamaDecoderLayer |
| Self-Attention | vllm/model_executor/models/llama.py | LlamaAttention |
| MLP | vllm/model_executor/models/llama.py | LlamaMLP |
| Attention 后端 | vllm/attention/layer.py | Attention |
| 旋转位置编码 | vllm/model_executor/layers/rotary_embedding.py | get_rope() |
| 并行线性层 | vllm/model_executor/layers/linear.py | *ParallelLinear |
| RMSNorm | vllm/model_executor/layers/layernorm.py | RMSNorm |
10. 小结
本章我们深入了解了 vLLM 中模型前向传播的实现:
模型结构:
LlamaForCausalLM → LlamaModel → LlamaDecoderLayer- Pre-LN 结构,每层包含 Attention 和 MLP
Self-Attention:
- QKV 合并投影
- RoPE 旋转位置编码
- GQA 减少 KV Cache 内存
- PagedAttention 集成
MLP:
张量并行:
- ColumnParallelLinear:输出切分
- RowParallelLinear:输入切分 + AllReduce
KV Cache 集成:
- 自动写入和读取
- 通过 attn_metadata 控制
在下一章中,我们将深入分析采样过程,了解如何从 logits 生成下一个 token。
导航
4.4 - 采样过程
采样过程分析
在模型前向传播得到 logits 后,需要通过采样来生成下一个 token。采样过程不仅仅是简单地选择概率最高的 token,还涉及温度调节、top-k/top-p 过滤、惩罚机制等多种策略。本章将深入分析 vLLM 的采样实现。
1. 采样在推理流程中的位置
graph LR
subgraph 模型推理
Input[输入 tokens] --> Forward[前向传播]
Forward --> Logits[logits]
end
subgraph 采样
Logits --> Processor[Logits 处理]
Processor --> Sample[采样策略]
Sample --> Token[下一个 token]
end
Token --> |追加到序列| Input采样的主要步骤:
- Logits 处理:应用各种处理器修改原始 logits
- 温度调节:控制随机性
- Top-k/Top-p 过滤:限制候选 token 范围
- 实际采样:从处理后的分布中选择 token
2. SamplingParams - 采样参数
# vllm/sampling_params.py
class SamplingParams:
"""控制采样行为的参数"""
def __init__(
self,
# 基本参数
n: int = 1, # 每个 prompt 生成的序列数
best_of: int | None = None, # 候选序列数
# 温度
temperature: float = 1.0, # 采样温度
# Top-k/Top-p
top_k: int = -1, # top-k 采样(-1 表示禁用)
top_p: float = 1.0, # top-p (nucleus) 采样
# 惩罚
repetition_penalty: float = 1.0, # 重复惩罚
frequency_penalty: float = 0.0, # 频率惩罚
presence_penalty: float = 0.0, # 存在惩罚
# 停止条件
max_tokens: int = 16, # 最大生成 token 数
stop: list[str] | None = None, # 停止字符串
stop_token_ids: list[int] | None = None, # 停止 token
# Logprobs
logprobs: int | None = None, # 返回的 logprobs 数量
# 其他
seed: int | None = None, # 随机种子
min_p: float = 0.0, # min-p 采样
...
):
...
3. Sampler 类详解
3.1 类定义
# vllm/v1/sample/sampler.py
class Sampler(nn.Module):
"""
从模型输出中采样下一个 token 的层。
处理步骤:
1. 计算 logprobs(如果请求)
2. 转换为 float32
3. 应用 allowed token ids 白名单
4. 应用 bad words 排除
5. 应用非 argmax-invariant 的 logit 处理器
6. 应用惩罚(重复、频率、存在)
7. 采样(贪婪或随机)
8. 收集 top-k logprobs
9. 返回 SamplerOutput
"""
def __init__(self, logprobs_mode: LogprobsMode = "raw_logprobs"):
super().__init__()
self.topk_topp_sampler = TopKTopPSampler(logprobs_mode)
def forward(
self,
logits: torch.Tensor, # [num_tokens, vocab_size]
sampling_metadata: SamplingMetadata,
) -> SamplerOutput:
"""主采样方法"""
# 1. 计算原始 logprobs(用于返回给用户)
num_logprobs = sampling_metadata.max_num_logprobs
if num_logprobs is not None:
raw_logprobs = self.compute_logprobs(logits)
# 2. 转换为 float32
logits = logits.to(torch.float32)
# 3-6. 应用各种 logits 处理器
logits = self.apply_logits_processors(logits, sampling_metadata)
# 7. 采样
sampled, processed_logprobs = self.sample(logits, sampling_metadata)
# 8. 收集 logprobs
if num_logprobs is not None:
logprobs_tensors = self.gather_logprobs(
raw_logprobs, num_logprobs, token_ids=sampled
)
# 9. 返回结果
return SamplerOutput(
sampled_token_ids=sampled.unsqueeze(-1),
logprobs_tensors=logprobs_tensors,
)
3.2 采样流程图
flowchart TD
A[logits] --> B{需要 logprobs?}
B -->|是| C[compute_logprobs]
B -->|否| D[转换为 float32]
C --> D
D --> E[apply_logits_processors]
subgraph Logits 处理
E --> E1[Allowed Token IDs]
E1 --> E2[Bad Words]
E2 --> E3[Min Tokens]
E3 --> E4[Logit Bias]
E4 --> E5[Penalties]
end
E5 --> F[sample]
subgraph 采样
F --> F1{all_greedy?}
F1 -->|是| F2[argmax]
F1 -->|否| F3[apply_temperature]
F3 --> F4[min_p processor]
F4 --> F5[top_k / top_p]
F5 --> F6[multinomial]
end
F2 --> G[gather_logprobs]
F6 --> G
G --> H[SamplerOutput]
4. 采样策略详解
4.1 贪婪采样(Greedy Sampling)
@staticmethod
def greedy_sample(logits: torch.Tensor) -> torch.Tensor:
"""选择概率最高的 token"""
return logits.argmax(dim=-1).view(-1)
特点:
- 确定性输出(相同输入总是产生相同输出)
- 适合需要一致性的场景
- 可能导致重复和无聊的输出
4.2 温度采样(Temperature Sampling)
@staticmethod
def apply_temperature(
logits: torch.Tensor,
temp: torch.Tensor,
all_random: bool,
) -> torch.Tensor:
"""应用温度缩放"""
if not all_random:
# 避免除以零(贪婪请求的 temp 可能为 0)
temp = torch.where(temp < _SAMPLING_EPS, 1.0, temp)
return logits.div_(temp.unsqueeze(dim=1))
温度的作用:
logits = [2.0, 1.0, 0.5, 0.1]
temp = 0.5(更确定):
logits / 0.5 = [4.0, 2.0, 1.0, 0.2]
softmax → [0.84, 0.11, 0.04, 0.01] # 高概率更集中
temp = 1.0(原始):
softmax → [0.47, 0.17, 0.10, 0.07] # 原始分布
temp = 2.0(更随机):
logits / 2.0 = [1.0, 0.5, 0.25, 0.05]
softmax → [0.36, 0.22, 0.17, 0.14] # 分布更均匀
graph LR
subgraph "温度效果"
Low["temp < 1<br/>更确定"]
Normal["temp = 1<br/>原始分布"]
High["temp > 1<br/>更随机"]
end
Low -.-> |"集中于高概率"| Peak[尖锐分布]
Normal -.-> |"保持原状"| Orig[原始分布]
High -.-> |"均匀化"| Flat[平坦分布]4.3 Top-k 采样
# vllm/v1/sample/ops/topk_topp_sampler.py
def apply_top_k(logits: torch.Tensor, top_k: int) -> torch.Tensor:
"""只保留 top-k 个最高概率的 token"""
if top_k < 0:
return logits
# 找到第 k 大的值
top_k_values, _ = torch.topk(logits, top_k, dim=-1)
min_top_k = top_k_values[:, -1].unsqueeze(-1)
# 将低于阈值的 logits 设为 -inf
return logits.masked_fill(logits < min_top_k, float('-inf'))
示例:
原始 logits: [3.5, 2.1, 1.8, 0.5, 0.1, -0.2, -1.0]
top_k = 3
处理后: [3.5, 2.1, 1.8, -inf, -inf, -inf, -inf]
softmax 后只有前 3 个有概率
4.4 Top-p (Nucleus) 采样
def apply_top_p(logits: torch.Tensor, top_p: float) -> torch.Tensor:
"""保留累积概率达到 top_p 的最小 token 集合"""
if top_p >= 1.0:
return logits
# 按概率排序
probs = torch.softmax(logits, dim=-1)
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
# 计算累积概率
cumsum_probs = torch.cumsum(sorted_probs, dim=-1)
# 找到累积概率首次超过 top_p 的位置
mask = cumsum_probs - sorted_probs > top_p
# 将超出的 logits 设为 -inf
sorted_logits = logits.gather(-1, sorted_indices)
sorted_logits = sorted_logits.masked_fill(mask, float('-inf'))
# 还原顺序
return sorted_logits.scatter(-1, sorted_indices, sorted_logits)
示例:
原始 probs: [0.40, 0.25, 0.15, 0.10, 0.05, 0.03, 0.02]
top_p = 0.9
累积: [0.40, 0.65, 0.80, 0.90, 0.95, 0.98, 1.00]
↑ 首次 >= 0.9
保留前 4 个 token,其余设为 -inf
4.5 Min-p 采样
def apply_min_p(logits: torch.Tensor, min_p: float) -> torch.Tensor:
"""过滤掉概率低于 max_prob * min_p 的 token"""
if min_p <= 0.0:
return logits
probs = torch.softmax(logits, dim=-1)
max_probs = probs.max(dim=-1, keepdim=True).values
# 概率阈值 = 最大概率 * min_p
threshold = max_probs * min_p
# 过滤低概率 token
return logits.masked_fill(probs < threshold, float('-inf'))
5. 惩罚机制
5.1 重复惩罚(Repetition Penalty)
def apply_repetition_penalty(
logits: torch.Tensor,
token_ids: torch.Tensor,
penalty: float,
) -> torch.Tensor:
"""惩罚已出现的 token"""
if penalty == 1.0:
return logits
# 获取已出现 token 的 logits
score = logits.gather(-1, token_ids)
# 应用惩罚
# logits > 0: score = score / penalty
# logits < 0: score = score * penalty
score = torch.where(score > 0, score / penalty, score * penalty)
# 写回
return logits.scatter(-1, token_ids, score)
效果:
penalty = 1.2, token "the" 已出现
原始 logit: 2.5
惩罚后: 2.5 / 1.2 = 2.08 # 概率降低
原始 logit: -0.5
惩罚后: -0.5 * 1.2 = -0.6 # 概率进一步降低
5.2 频率惩罚(Frequency Penalty)
def apply_frequency_penalty(
logits: torch.Tensor,
token_counts: torch.Tensor,
penalty: float,
) -> torch.Tensor:
"""基于出现次数的惩罚"""
if penalty == 0.0:
return logits
# logits = logits - penalty * count
return logits - penalty * token_counts
效果:
penalty = 0.5, token "the" 出现了 3 次
原始 logit: 2.5
惩罚后: 2.5 - 0.5 * 3 = 1.0
5.3 存在惩罚(Presence Penalty)
def apply_presence_penalty(
logits: torch.Tensor,
token_presence: torch.Tensor, # 0 或 1
penalty: float,
) -> torch.Tensor:
"""基于是否出现过的惩罚(不考虑次数)"""
if penalty == 0.0:
return logits
# logits = logits - penalty * presence
return logits - penalty * token_presence
6. Logprobs 计算
6.1 计算 Logprobs
@staticmethod
def compute_logprobs(logits: torch.Tensor) -> torch.Tensor:
"""计算 log 概率"""
return logits.log_softmax(dim=-1, dtype=torch.float32)
6.2 收集 Top-k Logprobs
@staticmethod
def gather_logprobs(
logprobs: torch.Tensor,
num_logprobs: int,
token_ids: torch.Tensor,
) -> LogprobsTensors:
"""收集 top-k logprobs 和采样 token 的 logprob"""
# 1. 找 top-k
topk_logprobs, topk_indices = torch.topk(logprobs, num_logprobs, dim=-1)
# 2. 获取采样 token 的 logprob
token_logprobs = logprobs.gather(-1, token_ids.unsqueeze(-1))
# 3. 计算采样 token 的排名
token_ranks = batched_count_greater_than(logprobs, token_logprobs)
# 4. 合并结果
indices = torch.cat((token_ids.unsqueeze(-1), topk_indices), dim=1)
logprobs = torch.cat((token_logprobs, topk_logprobs), dim=1)
return LogprobsTensors(indices, logprobs, token_ranks)
7. 批量采样优化
# vllm/v1/sample/metadata.py
class SamplingMetadata:
"""批量采样的元数据"""
# 基本信息
all_greedy: bool # 所有请求都是贪婪采样
all_random: bool # 所有请求都是随机采样
# 参数(批量张量)
temperature: torch.Tensor | None # [batch_size]
top_k: torch.Tensor | None # [batch_size]
top_p: torch.Tensor | None # [batch_size]
# Logprobs
max_num_logprobs: int | None
# 惩罚相关
# ...
# 随机数生成器(每个请求一个)
generators: list[torch.Generator] | None
7.2 批量处理流程
sequenceDiagram
participant S as Scheduler
participant R as ModelRunner
participant Sampler as Sampler
S->>R: SchedulerOutput (多个请求)
R->>R: 构建 SamplingMetadata
Note over R: 将各请求的采样参数<br/>批量化为张量
R->>Sampler: forward(logits, metadata)
alt all_greedy
Sampler->>Sampler: argmax (快速路径)
else mixed
Sampler->>Sampler: 批量应用温度
Sampler->>Sampler: 批量 top-k/top-p
Sampler->>Sampler: multinomial 采样
Sampler->>Sampler: 合并贪婪和随机结果
end
Sampler-->>R: SamplerOutput
8. 采样参数组合建议
8.1 常见场景配置
| 场景 | temperature | top_k | top_p | 说明 |
|---|
| 代码生成 | 0.0 | - | - | 贪婪,确保正确性 |
| 技术写作 | 0.3 | - | 0.9 | 低随机性 |
| 创意写作 | 0.8 | 50 | 0.95 | 高随机性 |
| 对话 | 0.7 | 40 | 0.9 | 平衡 |
| 头脑风暴 | 1.2 | - | 0.95 | 非常随机 |
8.2 惩罚参数建议
| 参数 | 推荐范围 | 说明 |
|---|
| repetition_penalty | 1.0-1.2 | 轻微惩罚重复 |
| frequency_penalty | 0.0-0.5 | 减少高频词 |
| presence_penalty | 0.0-0.5 | 鼓励新话题 |
9. 代码位置速查
| 组件 | 文件 | 关键类/函数 |
|---|
| 采样参数 | vllm/sampling_params.py | SamplingParams |
| Sampler | vllm/v1/sample/sampler.py | Sampler |
| 采样元数据 | vllm/v1/sample/metadata.py | SamplingMetadata |
| Top-k/Top-p | vllm/v1/sample/ops/topk_topp_sampler.py | TopKTopPSampler |
| 惩罚 | vllm/v1/sample/ops/penalties.py | apply_*_penalty() |
| Logprobs | vllm/v1/sample/ops/logprobs.py | gather_logprobs() |
10. 小结
本章我们深入了解了 vLLM 的采样过程:
- 采样参数:temperature、top_k、top_p、min_p 等
- 采样策略:
- 贪婪采样:确定性,选择最高概率
- 温度采样:控制随机性
- Top-k:只考虑前 k 个 token
- Top-p:累积概率阈值过滤
- Min-p:相对于最高概率的阈值
- 惩罚机制:
- 重复惩罚:惩罚已出现 token
- 频率惩罚:基于出现次数
- 存在惩罚:基于是否出现
- Logprobs:返回 token 的对数概率
- 批量优化:通过 SamplingMetadata 批量处理
在下一章中,我们将分析输出处理过程,了解采样结果如何转换为用户可见的输出。
导航
4.5 - 输出处理
输出处理流程
采样完成后,生成的 token 需要经过一系列处理才能最终返回给用户。本章将详细分析从采样结果到用户输出的完整处理流程。
1. 输出处理在整体流程中的位置
graph LR
subgraph 模型执行
Sample[采样] --> SamplerOut[SamplerOutput]
end
subgraph 输出处理
SamplerOut --> Update[更新请求状态]
Update --> Check[检查停止条件]
Check --> Detok[Detokenize]
Detok --> Build[构建输出]
end
subgraph 返回用户
Build --> Stream[流式返回]
Build --> Final[最终返回]
end
2. 输出数据结构
2.1 SamplerOutput
# vllm/v1/outputs.py
@dataclass
class SamplerOutput:
"""采样器的输出"""
# 采样的 token IDs [num_requests, 1]
sampled_token_ids: torch.Tensor
# Logprobs 信息(可选)
logprobs_tensors: LogprobsTensors | None = None
@dataclass
class LogprobsTensors:
"""Logprobs 张量"""
# Top-k token indices [num_tokens, num_logprobs + 1]
indices: torch.Tensor
# Top-k logprobs [num_tokens, num_logprobs + 1]
logprobs: torch.Tensor
# 采样 token 的排名 [num_tokens]
ranks: torch.Tensor
2.2 ModelRunnerOutput
# vllm/v1/outputs.py
@dataclass
class ModelRunnerOutput:
"""ModelRunner 的输出"""
# 每个请求的采样结果
# Dict[request_id, SamplerOutput]
sampler_output: dict[str, SamplerOutput]
# 模型特定的输出(如 pooling embeddings)
model_output: Any | None = None
2.3 EngineCoreOutput
# vllm/v1/engine/__init__.py
@dataclass
class EngineCoreOutput:
"""EngineCore 的单个请求输出"""
request_id: str
# 新生成的 token IDs
new_token_ids: list[int]
# 完成原因(如果完成)
finish_reason: FinishReason | None = None
# 停止字符串(如果因停止字符串完成)
stop_str: str | None = None
# Logprobs 信息
new_logprobs: list[dict[int, Logprob]] | None = None
@dataclass
class EngineCoreOutputs:
"""EngineCore 的批量输出"""
outputs: list[EngineCoreOutput]
# 完成的请求 IDs
finished_req_ids: set[str] | None = None
2.4 RequestOutput(最终用户输出)
# vllm/outputs.py
@dataclass
class RequestOutput:
"""用户可见的最终输出"""
request_id: str
# 原始 prompt
prompt: str | None
prompt_token_ids: list[int]
# 生成结果(可能多个,如 beam search)
outputs: list[CompletionOutput]
# 是否完成
finished: bool
# 指标
metrics: RequestMetrics | None = None
@dataclass
class CompletionOutput:
"""单个生成序列的输出"""
index: int # 序列索引
text: str # 生成的文本
token_ids: list[int] # 生成的 token IDs
cumulative_logprob: float | None # 累积对数概率
logprobs: list[dict] | None # 每个 token 的 logprobs
finish_reason: str | None # 完成原因
stop_reason: str | int | None # 停止原因
3. 输出处理流程详解
3.1 update_from_output() - 状态更新
# vllm/v1/core/sched/scheduler.py
def update_from_output(
self,
model_runner_output: ModelRunnerOutput,
sampler_output: SamplerOutput | None,
scheduler_output: SchedulerOutput,
) -> EngineCoreOutputs:
"""根据模型输出更新请求状态"""
outputs: list[EngineCoreOutput] = []
for req_id, req_output in model_runner_output.items():
request = self.requests[req_id]
# 1. 获取新生成的 token IDs
new_token_ids = req_output.sampled_token_ids.tolist()
# 2. 追加到请求
request.append_output_token_ids(new_token_ids)
# 3. 检查停止条件
finish_reason, stop_str = check_stop(request, self.max_model_len)
# 4. 处理完成的请求
if finish_reason is not None:
self._finish_request(request, finish_reason)
# 5. 构建输出
output = EngineCoreOutput(
request_id=req_id,
new_token_ids=new_token_ids,
finish_reason=finish_reason,
stop_str=stop_str,
new_logprobs=self._process_logprobs(req_output),
)
outputs.append(output)
return EngineCoreOutputs(outputs=outputs)
3.2 停止条件检查
# vllm/v1/core/sched/utils.py
def check_stop(
request: Request,
max_model_len: int,
) -> tuple[FinishReason | None, str | None]:
"""检查请求是否应该停止"""
# 1. 检查 EOS token
last_token_id = request.all_token_ids[-1]
if last_token_id == request.eos_token_id:
return FinishReason.STOP, None
# 2. 检查最大输出长度
if request.num_output_tokens >= request.max_tokens:
return FinishReason.LENGTH, None
# 3. 检查模型最大长度
if len(request.all_token_ids) >= max_model_len:
return FinishReason.LENGTH, None
# 4. 检查停止 token IDs
if request.stop_token_ids:
if last_token_id in request.stop_token_ids:
return FinishReason.STOP, None
# 5. 检查停止字符串
if request.stop_strings:
output_text = request.get_output_text()
for stop_str in request.stop_strings:
if stop_str in output_text:
return FinishReason.STOP, stop_str
return None, None # 继续生成
3.3 流程图
flowchart TD
A[SamplerOutput] --> B[遍历每个请求]
B --> C[获取 new_token_ids]
C --> D[append_output_token_ids]
D --> E[check_stop]
E --> F{停止?}
F -->|EOS| G[finish_reason = STOP]
F -->|max_tokens| H[finish_reason = LENGTH]
F -->|stop_string| I[finish_reason = STOP<br/>stop_str = ...]
F -->|否| J[继续]
G --> K[_finish_request]
H --> K
I --> K
K --> L[释放 KV Cache]
L --> M[从 running 移除]
J --> N[构建 EngineCoreOutput]
M --> N
N --> O[EngineCoreOutputs]
4. Detokenization - 反向分词
4.1 增量 Detokenize
# vllm/v1/engine/detokenizer.py
class IncrementalDetokenizer:
"""增量反向分词器"""
def __init__(self, tokenizer: TokenizerLike, request: Request):
self.tokenizer = tokenizer
self.request = request
# 已解码的文本
self.output_text = ""
# 待解码的 token 缓冲区
self.token_buffer: list[int] = []
# 上一次的偏移量(用于流式输出)
self.prev_output_len = 0
def decode(self, new_token_ids: list[int]) -> str:
"""解码新的 token,返回新增的文本"""
# 添加到缓冲区
self.token_buffer.extend(new_token_ids)
# 尝试解码
text = self.tokenizer.decode(
self.token_buffer,
skip_special_tokens=True,
)
# 检查是否有完整的字符
# (某些 token 可能是部分字符,需要等待后续 token)
if self._is_valid_utf8(text):
self.output_text = text
new_text = text[self.prev_output_len:]
self.prev_output_len = len(text)
return new_text
return ""
4.2 增量解码的必要性
Token IDs: [15496, 284, 262, 995, 0]
逐个解码:
15496 → "Hello" ✓ 完整单词
284 → " to" ✓ 完整
262 → " the" ✓ 完整
995 → " wor" ? 可能是 "world" 的一部分
0 → "ld" ✓ 完成 "world"
增量解码会等待 995+0 一起解码为 " world"
而不是输出 " wor" 然后 "ld"
5. 流式输出
5.1 流式输出架构
sequenceDiagram
participant User as 用户
participant API as API Server
participant Engine as LLMEngine
participant Core as EngineCore
User->>API: POST /v1/completions<br/>stream=true
API->>Engine: add_request()
loop 每个 step
Engine->>Core: step()
Core-->>Engine: EngineCoreOutputs
Engine->>Engine: detokenize()
Engine-->>API: yield partial_output
API-->>User: SSE: data: {...}
end
Engine-->>API: final_output
API-->>User: SSE: data: [DONE]5.2 Server-Sent Events (SSE) 格式
# 流式响应示例
async def stream_response():
async for output in engine.generate_stream(prompt, params):
yield f"data: {json.dumps(output)}\n\n"
yield "data: [DONE]\n\n"
实际输出示例:
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"Hello"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":" there"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"!"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]
6. Logprobs 处理
6.1 Logprobs 数据结构
@dataclass
class Logprob:
"""单个 token 的 logprob 信息"""
logprob: float # 对数概率
rank: int | None # 在词表中的排名
decoded_token: str # 解码后的文本
# 每个位置的 logprobs
# Dict[token_id, Logprob]
6.2 Logprobs 处理流程
def _process_logprobs(
self,
sampler_output: SamplerOutput,
) -> list[dict[int, Logprob]] | None:
"""处理 logprobs 输出"""
if sampler_output.logprobs_tensors is None:
return None
tensors = sampler_output.logprobs_tensors
result = []
for i in range(tensors.indices.shape[0]):
token_logprobs = {}
# 获取 top-k token IDs 和 logprobs
for j in range(tensors.indices.shape[1]):
token_id = tensors.indices[i, j].item()
logprob = tensors.logprobs[i, j].item()
# 解码 token
decoded = self.tokenizer.decode([token_id])
token_logprobs[token_id] = Logprob(
logprob=logprob,
rank=j if j > 0 else tensors.ranks[i].item(),
decoded_token=decoded,
)
result.append(token_logprobs)
return result
7. 输出格式化
7.1 OpenAI 兼容格式
# vllm/entrypoints/openai/protocol.py
class ChatCompletionResponse(BaseModel):
"""OpenAI Chat Completion 响应格式"""
id: str
object: str = "chat.completion"
created: int
model: str
choices: list[ChatCompletionResponseChoice]
usage: UsageInfo
class ChatCompletionResponseChoice(BaseModel):
index: int
message: ChatMessage
finish_reason: str | None
logprobs: ChoiceLogprobs | None
class ChatMessage(BaseModel):
role: str
content: str
7.2 格式转换
def create_chat_completion_response(
request_output: RequestOutput,
model_name: str,
) -> ChatCompletionResponse:
"""将 RequestOutput 转换为 OpenAI 格式"""
choices = []
for i, output in enumerate(request_output.outputs):
choice = ChatCompletionResponseChoice(
index=i,
message=ChatMessage(
role="assistant",
content=output.text,
),
finish_reason=output.finish_reason,
logprobs=_convert_logprobs(output.logprobs),
)
choices.append(choice)
return ChatCompletionResponse(
id=f"chatcmpl-{request_output.request_id}",
created=int(time.time()),
model=model_name,
choices=choices,
usage=UsageInfo(
prompt_tokens=len(request_output.prompt_token_ids),
completion_tokens=sum(len(o.token_ids) for o in request_output.outputs),
total_tokens=...,
),
)
8. 输出处理完整流程图
flowchart TD
subgraph EngineCore
SO[SamplerOutput] --> UO[update_from_output]
UO --> CS[check_stop]
CS --> ECO[EngineCoreOutput]
end
subgraph LLMEngine
ECO --> DT[Detokenize]
DT --> PL[Process Logprobs]
PL --> RO[RequestOutput]
end
subgraph API Server
RO --> FMT[格式化]
FMT --> |非流式| JSON[JSON Response]
FMT --> |流式| SSE[SSE Events]
end
subgraph 用户
JSON --> User1[完整响应]
SSE --> User2[增量响应]
end
9. 代码位置速查
| 组件 | 文件 | 关键类/函数 |
|---|
| 输出数据结构 | vllm/v1/outputs.py | SamplerOutput, ModelRunnerOutput |
| EngineCore 输出 | vllm/v1/engine/__init__.py | EngineCoreOutput |
| 用户输出 | vllm/outputs.py | RequestOutput, CompletionOutput |
| 状态更新 | vllm/v1/core/sched/scheduler.py | update_from_output() |
| 停止检查 | vllm/v1/core/sched/utils.py | check_stop() |
| Detokenizer | vllm/v1/engine/detokenizer.py | IncrementalDetokenizer |
| OpenAI 格式 | vllm/entrypoints/openai/protocol.py | ChatCompletionResponse |
10. 小结
本章我们详细分析了输出处理流程:
数据结构链路:
SamplerOutput → ModelRunnerOutput → EngineCoreOutput → RequestOutput
状态更新:
- 追加 token 到请求
- 检查停止条件
- 处理完成的请求
停止条件:
Detokenization:
流式输出:
格式化:
在下一章中,我们将完整跟踪一个请求从提交到返回的完整生命周期。
导航
4.6 - 请求生命周期
请求完整生命周期
本章将完整跟踪一个请求从用户提交到最终返回的全过程,将前面章节的知识串联起来,帮助读者建立完整的认知图景。
1. 生命周期概览
graph TD
subgraph 1. 提交阶段
A1[用户调用 generate]
A2[Tokenize]
A3[创建请求]
A4[加入 waiting 队列]
end
subgraph 2. 调度阶段
B1[查找前缀缓存]
B2[分配 KV Cache]
B3[加入 running 队列]
end
subgraph 3. 执行阶段
C1[准备输入]
C2[模型前向传播]
C3[采样]
end
subgraph 4. 更新阶段
D1[追加 token]
D2[检查停止条件]
D3[更新状态]
end
subgraph 5. 返回阶段
E1[Detokenize]
E2[构建输出]
E3[返回用户]
end
A1 --> A2 --> A3 --> A4
A4 --> B1 --> B2 --> B3
B3 --> C1 --> C2 --> C3
C3 --> D1 --> D2 --> D3
D3 -->|未完成| C1
D3 -->|完成| E1 --> E2 --> E3
2. 阶段 1:请求提交
2.1 用户调用
# 用户代码
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-2-7b-hf")
prompts = ["The capital of France is"]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=50)
outputs = llm.generate(prompts, sampling_params)
2.2 Tokenize
# vllm/entrypoints/llm.py
def generate(self, prompts, sampling_params, ...):
# 1. 处理输入
for prompt in prompts:
# Tokenize prompt
prompt_token_ids = self.tokenizer.encode(prompt)
# 创建请求
request_id = str(next(self.request_counter))
self._add_request(
request_id=request_id,
prompt=prompt,
prompt_token_ids=prompt_token_ids,
params=sampling_params,
)
2.3 创建 EngineCoreRequest
# vllm/v1/engine/llm_engine.py
def add_request(self, request_id, prompt, params, ...):
# 构建 EngineCoreRequest
engine_request = EngineCoreRequest(
request_id=request_id,
prompt_token_ids=prompt_token_ids,
sampling_params=params,
arrival_time=time.time(),
eos_token_id=self.tokenizer.eos_token_id,
)
# 发送到 EngineCore
self.engine_core.add_request(engine_request)
2.4 加入 Waiting 队列
# vllm/v1/core/sched/scheduler.py
def add_request(self, request: EngineCoreRequest) -> None:
# 1. 创建内部 Request 对象
internal_request = Request(
request_id=request.request_id,
prompt_token_ids=request.prompt_token_ids,
sampling_params=request.sampling_params,
)
# 2. 计算 block hashes(用于前缀缓存)
if self.enable_caching:
internal_request.block_hashes = compute_block_hashes(
internal_request.prompt_token_ids,
self.block_size,
)
# 3. 加入 waiting 队列
internal_request.status = RequestStatus.WAITING
self.waiting.append_request(internal_request)
# 4. 记录到请求字典
self.requests[request.request_id] = internal_request
2.5 提交阶段时序图
sequenceDiagram
participant User as 用户
participant LLM as LLM
participant Tokenizer as Tokenizer
participant Engine as LLMEngine
participant Core as EngineCore
participant Sched as Scheduler
User->>LLM: generate(prompts, params)
LLM->>Tokenizer: encode(prompt)
Tokenizer-->>LLM: token_ids
LLM->>Engine: add_request(id, tokens, params)
Engine->>Engine: 创建 EngineCoreRequest
Engine->>Core: add_request(request)
Core->>Sched: add_request(request)
Sched->>Sched: 创建 internal Request
Sched->>Sched: 计算 block_hashes
Sched->>Sched: waiting.append(request)
Note over Sched: 请求进入 WAITING 状态
3. 阶段 2:调度
3.1 查找前缀缓存
# vllm/v1/core/sched/scheduler.py :: schedule()
# 从 waiting 队列取出请求
request = self.waiting.peek_request()
# 查找前缀缓存
new_computed_blocks, num_cached_tokens = (
self.kv_cache_manager.get_computed_blocks(request)
)
# num_cached_tokens 表示可以跳过的 token 数
# 例如:prompt 有 100 tokens,前 64 个已缓存
# 则只需要计算后 36 个
3.2 分配 KV Cache
# 计算需要处理的 token 数
num_new_tokens = request.num_tokens - num_cached_tokens
# 分配 KV Cache slots
new_blocks = self.kv_cache_manager.allocate_slots(
request,
num_new_tokens,
num_new_computed_tokens=num_cached_tokens,
new_computed_blocks=new_computed_blocks,
)
if new_blocks is None:
# 内存不足,请求继续等待
return
# 分配成功
3.3 移入 Running 队列
# 从 waiting 移除
request = self.waiting.pop_request()
# 加入 running
self.running.append(request)
# 更新状态
request.status = RequestStatus.RUNNING
request.num_computed_tokens = num_cached_tokens
3.4 调度阶段示意图
flowchart TD
subgraph Scheduler.schedule
W[waiting 队列] --> Peek[peek_request]
Peek --> Cache[get_computed_blocks]
Cache --> Alloc[allocate_slots]
Alloc --> Check{分配成功?}
Check -->|是| Move[移入 running]
Check -->|否| Wait[继续等待]
Move --> SO[构建 SchedulerOutput]
end
subgraph SchedulerOutput
SO --> Reqs[scheduled_new_reqs]
SO --> Blocks[req_to_new_blocks]
SO --> Tokens[num_scheduled_tokens]
end
4. 阶段 3:模型执行
4.1 准备输入
# vllm/v1/worker/gpu_model_runner.py
def execute_model(self, scheduler_output: SchedulerOutput):
# 1. 准备 input_ids
input_ids = self._prepare_input_ids(scheduler_output)
# 2. 准备 positions
positions = self._prepare_positions(scheduler_output)
# 3. 准备 attention metadata
attn_metadata = self._prepare_attention_metadata(scheduler_output)
# 4. 更新 block table
self._update_block_table(scheduler_output)
4.2 模型前向传播
# 5. 前向传播
with torch.inference_mode():
hidden_states = self.model(
input_ids=input_ids,
positions=positions,
kv_caches=self.kv_caches,
attn_metadata=attn_metadata,
)
# 6. 计算 logits
logits = self.model.compute_logits(hidden_states)
return ModelRunnerOutput(logits=logits, ...)
4.3 采样
# vllm/v1/executor/abstract.py
def sample_tokens(self, model_output: ModelRunnerOutput) -> SamplerOutput:
# 构建采样元数据
sampling_metadata = self._prepare_sampling_metadata()
# 采样
sampler_output = self.sampler(
model_output.logits,
sampling_metadata,
)
return sampler_output
4.4 执行阶段时序图
sequenceDiagram
participant Core as EngineCore
participant Exec as Executor
participant Worker as Worker
participant Runner as ModelRunner
participant Model as Model
participant Sampler as Sampler
Core->>Exec: execute_model(scheduler_output)
Exec->>Worker: execute_model()
Worker->>Runner: execute_model()
Runner->>Runner: _prepare_inputs()
Runner->>Model: forward(input_ids, positions, kv_caches)
Note over Model: Embedding → Transformer Layers → Norm
Model-->>Runner: hidden_states
Runner->>Model: compute_logits(hidden_states)
Model-->>Runner: logits
Runner-->>Worker: ModelRunnerOutput
Worker-->>Exec: output
Core->>Exec: sample_tokens()
Exec->>Sampler: forward(logits, metadata)
Note over Sampler: Temperature → Top-k/p → Sample
Sampler-->>Exec: SamplerOutput
Exec-->>Core: sampled_tokens
5. 阶段 4:状态更新
5.1 追加 Token
# vllm/v1/core/sched/scheduler.py
def update_from_output(self, model_output, sampler_output, scheduler_output):
for req_id, output in sampler_output.items():
request = self.requests[req_id]
# 获取新生成的 token
new_token_ids = output.sampled_token_ids.tolist()
# 追加到请求
request.append_output_token_ids(new_token_ids)
# 更新 computed_tokens
request.num_computed_tokens += 1
5.2 检查停止条件
# 检查是否完成
finish_reason, stop_str = check_stop(request, self.max_model_len)
if finish_reason is not None:
# 请求完成
self._finish_request(request, finish_reason)
finished_outputs.append(...)
else:
# 继续生成
outputs.append(...)
5.3 完成请求处理
def _finish_request(self, request: Request, reason: FinishReason):
# 1. 释放 KV Cache
self.kv_cache_manager.free(request)
# 2. 从 running 移除
self.running.remove(request)
# 3. 更新状态
request.status = RequestStatus.FINISHED
# 4. 记录完成
self.finished_req_ids.add(request.request_id)
6. 阶段 5:返回结果
6.1 Detokenize
# vllm/v1/engine/llm_engine.py
def _process_outputs(self, engine_outputs: EngineCoreOutputs):
results = []
for output in engine_outputs.outputs:
request = self.requests[output.request_id]
# 增量解码
new_text = self.detokenizer.decode(
request,
output.new_token_ids,
)
# 更新请求的输出文本
request.output_text += new_text
results.append(...)
return results
6.2 构建 RequestOutput
def _make_request_output(self, request: Request, finished: bool):
return RequestOutput(
request_id=request.request_id,
prompt=request.prompt,
prompt_token_ids=request.prompt_token_ids,
outputs=[
CompletionOutput(
index=0,
text=request.output_text,
token_ids=request.output_token_ids,
finish_reason=request.finish_reason,
logprobs=request.logprobs,
)
],
finished=finished,
)
6.3 返回用户
# vllm/entrypoints/llm.py
def _run_engine(self, use_tqdm: bool):
outputs = []
while self.llm_engine.has_unfinished_requests():
step_outputs = self.llm_engine.step()
for output in step_outputs:
if output.finished:
outputs.append(output)
return sorted(outputs, key=lambda x: int(x.request_id))
7. 完整生命周期时序图
sequenceDiagram
participant User as 用户
participant LLM as LLM
participant Engine as LLMEngine
participant Core as EngineCore
participant Sched as Scheduler
participant KVM as KVCacheManager
participant Exec as Executor
participant Model as Model
rect rgb(230, 245, 230)
Note over User,Model: 1. 提交阶段
User->>LLM: generate(prompt, params)
LLM->>Engine: add_request()
Engine->>Core: add_request()
Core->>Sched: add_request()
Note over Sched: status = WAITING
end
loop 每个 step
rect rgb(255, 245, 230)
Note over User,Model: 2. 调度阶段
Core->>Sched: schedule()
Sched->>KVM: get_computed_blocks()
KVM-->>Sched: cached_blocks, num_cached
Sched->>KVM: allocate_slots()
KVM-->>Sched: new_blocks
Note over Sched: status = RUNNING
Sched-->>Core: SchedulerOutput
end
rect rgb(245, 230, 230)
Note over User,Model: 3. 执行阶段
Core->>Exec: execute_model()
Exec->>Model: forward()
Model-->>Exec: logits
Exec->>Exec: sample()
Exec-->>Core: SamplerOutput
end
rect rgb(230, 230, 245)
Note over User,Model: 4. 更新阶段
Core->>Sched: update_from_output()
Sched->>Sched: append_token()
Sched->>Sched: check_stop()
alt 完成
Sched->>KVM: free()
Note over Sched: status = FINISHED
end
end
end
rect rgb(245, 245, 230)
Note over User,Model: 5. 返回阶段
Core-->>Engine: EngineCoreOutputs
Engine->>Engine: detokenize()
Engine-->>LLM: RequestOutput
LLM-->>User: outputs
end
8. 状态转换汇总
stateDiagram-v2
[*] --> WAITING: add_request()
WAITING --> RUNNING: schedule() 成功
WAITING --> WAITING_FOR_FSM: 需要 FSM 编译
WAITING --> WAITING_FOR_REMOTE_KVS: 等待远程 KV
WAITING_FOR_FSM --> WAITING: FSM 就绪
WAITING_FOR_REMOTE_KVS --> WAITING: KV 就绪
RUNNING --> RUNNING: step() 继续生成
RUNNING --> PREEMPTED: 内存不足被抢占
RUNNING --> FINISHED_STOPPED: EOS 或停止字符串
RUNNING --> FINISHED_LENGTH: 达到 max_tokens
RUNNING --> FINISHED_ABORTED: 用户取消
PREEMPTED --> WAITING: 重新排队
FINISHED_STOPPED --> [*]: 释放资源
FINISHED_LENGTH --> [*]: 释放资源
FINISHED_ABORTED --> [*]: 释放资源
9. 关键数据结构流转
用户输入
↓
prompt: str
↓ Tokenize
prompt_token_ids: list[int]
↓ 创建请求
EngineCoreRequest
↓ 调度器内部
Request (internal)
↓ 调度
SchedulerOutput
↓ 执行
ModelRunnerOutput (logits)
↓ 采样
SamplerOutput (token_ids)
↓ 更新
EngineCoreOutput
↓ Detokenize
RequestOutput
↓
用户输出
10. 小结
本章我们完整跟踪了一个请求的生命周期:
提交阶段:
- Tokenize → 创建请求 → 加入 waiting 队列
调度阶段:
- 查找缓存 → 分配 KV Cache → 移入 running
执行阶段:
更新阶段:
返回阶段:
通过这个完整的流程分析,我们可以看到 vLLM 的各个组件是如何协同工作的,以及为什么它能够实现高效的 LLM 推理。
导航
5 - 进阶主题
了解 vLLM 的高级功能和优化技术
本部分将介绍 vLLM 的高级特性,包括模型量化、投机解码和分布式推理等进阶优化技术。
5.1 - 投机解码
投机解码(Speculative Decoding)
概述
投机解码(Speculative Decoding)是一种加速大语言模型推理的技术。其核心思想是:使用一个小型的 Draft 模型快速生成多个候选 token,然后让大型的 Target 模型并行验证这些候选 token。由于验证比逐个生成更高效,这种方法可以显著加速推理过程。
本文将深入分析 vLLM 中投机解码的实现原理和代码细节。
为什么需要投机解码
Decode 阶段的瓶颈
在LLM 生成过程中,我们了解到 Decode 阶段是内存带宽密集型的:
传统自回归生成:
Token 1 → Model Forward → Token 2 → Model Forward → Token 3 → ...
| | | | |
GPU 利用率低 等待 GPU 利用率低 等待 GPU 利用率低
每次 Decode 只处理一个 token,无法充分利用 GPU 的并行计算能力。
投机解码的核心思想
投机解码受到 CPU 分支预测的启发:
flowchart LR
subgraph 传统方法
A1[Token 1] --> B1[Model]
B1 --> C1[Token 2]
C1 --> D1[Model]
D1 --> E1[Token 3]
E1 --> F1[Model]
F1 --> G1[Token 4]
end
subgraph 投机解码
A2[Token 1] --> B2[Draft Model]
B2 --> C2[Draft: 2,3,4]
C2 --> D2[Target Model<br/>并行验证]
D2 --> E2[接受: 2,3<br/>拒绝: 4]
E2 --> F2[输出: 2,3,4']
end关键洞察:
- Draft 模型生成 K 个候选 token
- Target 模型一次性验证所有候选
- 接受正确的前缀,从第一个错误位置重新生成
- 验证的计算量 ≈ 生成一个 token,但产出多个 token
投机解码的数学原理
接受率分析
设:
p(x) = Target 模型在位置 i 的概率分布q(x) = Draft 模型在位置 i 的概率分布x_d = Draft 模型采样的 token
接受概率计算:
接受 x_d 的概率 = min(1, p(x_d) / q(x_d))
这种接受准则保证了最终输出的分布与只使用 Target 模型完全一致(无损加速)。
加速比估算
假设:
- Draft 模型生成 K 个候选 token
- 每个 token 的平均接受率为 α
- Draft 模型的前向时间为 t_d
- Target 模型的前向时间为 t_t
预期接受的 token 数:
E[accepted] = α + α² + α³ + ... + α^K ≈ α/(1-α) (当 α < 1)
加速比:
Speedup = E[accepted] × t_t / (K × t_d + t_t)
例如,当 α = 0.8,K = 5,t_d = t_t/10 时:
E[accepted] ≈ 3.2 tokens
Speedup ≈ 3.2 × t_t / (0.5 × t_t + t_t) = 2.1x
vLLM 中的投机解码实现
支持的投机解码方法
vLLM V1 支持多种投机解码方法:
graph TD
A[Speculative Decoding] --> B[Draft Model]
A --> C[EAGLE]
A --> D[EAGLE3]
A --> E[Medusa]
A --> F[MTP]
A --> G[N-gram]
B --> B1[独立小模型<br/>如 TinyLlama]
C --> C1[EAGLE Head<br/>利用隐藏状态]
D --> D1[EAGLE3 Head<br/>多层隐藏状态]
E --> E1[多头预测<br/>并行采样]
F --> F1[Multi-Token Prediction<br/>多 token 预测]
G --> G1[基于历史<br/>N-gram 匹配]核心代码结构
投机解码的核心代码位于 vllm/v1/spec_decode/ 目录:
vllm/v1/spec_decode/
├── __init__.py
├── eagle.py # EAGLE 基类和 Proposer
├── draft_model.py # Draft Model Proposer
├── medusa.py # Medusa Head
├── metadata.py # 投机解码元数据
├── metrics.py # 性能指标
├── ngram_proposer.py # N-gram 方法
├── suffix_decoding.py # 后缀解码
└── utils.py # 工具函数
vllm/v1/worker/gpu/spec_decode/
├── eagle.py # EAGLE CUDA Graph 支持
├── eagle_cudagraph.py # CUDA Graph 实现
└── rejection_sample.py # 拒绝采样内核
SpecDecodeBaseProposer 基类
类定义与初始化
SpecDecodeBaseProposer 是所有投机解码方法的基类:
# vllm/v1/spec_decode/eagle.py
class SpecDecodeBaseProposer:
def __init__(
self,
vllm_config: VllmConfig,
device: torch.device,
pass_hidden_states_to_model: bool, # 是否传递隐藏状态
runner=None,
):
self.vllm_config = vllm_config
self.speculative_config = vllm_config.speculative_config
self.draft_model_config = self.speculative_config.draft_model_config
self.method = self.speculative_config.method
# 配置参数
self.num_speculative_tokens = self.speculative_config.num_speculative_tokens
self.hidden_size = self.draft_model_config.get_hidden_size()
# 持久化缓冲区(用于 CUDA Graph)
self.input_ids = torch.zeros(
self.max_num_tokens, dtype=torch.int32, device=device
)
self.positions = torch.zeros(
self.max_num_tokens, dtype=torch.int64, device=device
)
self.hidden_states = torch.zeros(
(self.max_num_tokens, self.hidden_size),
dtype=self.dtype, device=device
)
Proposer 工作流程
sequenceDiagram
participant Runner as ModelRunner
participant Proposer as SpecDecodeProposer
participant Draft as Draft Model
participant Cache as KV Cache
Runner->>Proposer: propose(target_hidden_states, next_token_ids)
Note over Proposer: 第一次前向传播
Proposer->>Proposer: set_inputs_first_pass()
Proposer->>Draft: model(input_ids, positions, hidden_states)
Draft-->>Proposer: logits, hidden_states
Proposer->>Proposer: argmax(logits) → token_1
loop 剩余 K-1 个 token
Note over Proposer: 增量前向传播
Proposer->>Proposer: 更新 positions, seq_lens
Proposer->>Proposer: 计算 slot_mapping
Proposer->>Draft: model(token_i, position, hidden_state)
Draft-->>Proposer: logits, hidden_states
Proposer->>Proposer: argmax(logits) → token_{i+1}
end
Proposer-->>Runner: draft_token_ids [batch, K]propose() 方法详解
def propose(
self,
target_token_ids: torch.Tensor, # [num_tokens]
target_positions: torch.Tensor, # [num_tokens]
target_hidden_states: torch.Tensor, # [num_tokens, hidden_size]
next_token_ids: torch.Tensor, # [batch_size]
last_token_indices: torch.Tensor | None,
common_attn_metadata: CommonAttentionMetadata,
sampling_metadata: SamplingMetadata,
...
) -> torch.Tensor:
batch_size = common_attn_metadata.batch_size()
# 1. 设置第一次前向传播的输入
num_tokens, last_token_indices, common_attn_metadata = (
self.set_inputs_first_pass(
target_token_ids=target_token_ids,
next_token_ids=next_token_ids,
target_positions=target_positions,
last_token_indices=last_token_indices,
cad=common_attn_metadata,
num_rejected_tokens_gpu=num_rejected_tokens_gpu,
)
)
# 2. 构建 Attention Metadata
attn_metadata = attn_metadata_builder.build_for_drafting(
common_attn_metadata=common_attn_metadata, draft_index=0
)
# 3. 第一次前向传播
with set_forward_context(per_layer_attn_metadata, ...):
ret_hidden_states = self.model(
input_ids=self.input_ids[:num_input_tokens],
positions=self._get_positions(num_input_tokens),
hidden_states=self.hidden_states[:num_input_tokens]
if self.pass_hidden_states_to_model else None,
)
# 4. 采样第一个 draft token
logits = self.model.compute_logits(last_hidden_states[last_token_indices])
draft_token_ids = logits.argmax(dim=-1)
draft_token_ids_list = [draft_token_ids]
# 5. 生成剩余的 draft tokens
for token_index in range(self.num_speculative_tokens - 1):
# 更新输入
input_ids = draft_token_ids_list[-1].int()
positions += 1
# 处理超出最大长度的情况
exceeds_max_model_len = positions >= self.max_model_len
clamped_positions = torch.where(exceeds_max_model_len, 0, positions)
# 更新序列长度
common_attn_metadata.seq_lens += 1
# 计算新的 slot mapping
block_numbers = clamped_positions // block_size
block_ids = common_attn_metadata.block_table_tensor.gather(
dim=1, index=block_numbers.view(-1, 1)
)
common_attn_metadata.slot_mapping = (
block_ids * block_size + clamped_positions % block_size
)
# 屏蔽超长位置
common_attn_metadata.slot_mapping.masked_fill_(
exceeds_max_model_len, PADDING_SLOT_ID
)
# 执行模型
with set_forward_context(...):
ret_hidden_states = self.model(
input_ids=self.input_ids[:input_batch_size],
positions=self._get_positions(input_batch_size),
hidden_states=self.hidden_states[:input_batch_size],
)
# 采样下一个 token
logits = self.model.compute_logits(last_hidden_states[:batch_size])
draft_token_ids = logits.argmax(dim=-1)
draft_token_ids_list.append(draft_token_ids)
# 返回所有 draft tokens
return torch.stack(draft_token_ids_list, dim=1)
DraftModelProposer 详解
独立 Draft 模型
DraftModelProposer 使用独立的小模型(如 TinyLlama)作为 Draft:
# vllm/v1/spec_decode/draft_model.py
class DraftModelProposer(SpecDecodeBaseProposer):
def __init__(
self,
vllm_config: VllmConfig,
device: torch.device,
runner=None,
):
super().__init__(
vllm_config=vllm_config,
device=device,
pass_hidden_states_to_model=False, # 不需要 Target 的隐藏状态
runner=runner,
)
# 验证约束
self._raise_if_multimodal() # 暂不支持多模态
self._raise_if_mrope() # 暂不支持 M-RoPE
self._raise_if_vocab_size_mismatch() # 词表大小必须一致
self._raise_if_draft_tp_mismatch() # TP 大小必须一致
输入处理
Draft Model 的输入处理需要合并 Target 的输出:
def set_inputs_first_pass(
self,
target_token_ids: torch.Tensor,
next_token_ids: torch.Tensor,
target_positions: torch.Tensor,
last_token_indices: torch.Tensor | None,
cad: CommonAttentionMetadata,
num_rejected_tokens_gpu: torch.Tensor | None,
) -> tuple[int, torch.Tensor, CommonAttentionMetadata]:
batch_size = cad.batch_size()
# 使用 Triton kernel 合并 tokens
# target_toks: [a1, b1, b2, c1, c2, c3]
# next_toks: [a2, b3, c4]
# 结果: [a1, a2, b1, b2, b3, c1, c2, c3, c4]
merge_toks_kernel[grid](
target_toks_ptr=target_token_ids,
next_toks_ptr=next_token_ids,
query_start_locs_ptr=start_locs,
query_end_locs_ptr=end_locs,
out_ptr_merged_toks=self.input_ids,
out_ptr_is_rejected_tok=is_rejected_tok,
...
)
# 重新计算 slot mapping
new_slot_mapping = compute_new_slot_mapping(
cad=cad,
new_positions=self.positions[:num_tokens],
is_rejected_token_mask=is_rejected_tok,
block_size=self._block_size(),
max_model_len=self.max_model_len,
)
return num_tokens, new_last_token_indices, new_cad
加载 Draft 模型
def load_model(self, target_model: Any) -> None:
# 创建 Draft 模型专用的 VllmConfig
draft_vllm_config = create_vllm_config_for_draft_model(
target_model_vllm_config=self.vllm_config
)
logger.info(
"Starting to load draft model %s. TP=%d, rank=%d",
draft_vllm_config.model_config.model,
draft_vllm_config.parallel_config.tensor_parallel_size,
draft_vllm_config.parallel_config.rank,
)
# 加载模型并设置编译标签
with set_model_tag("draft_model"):
self.model = get_model(vllm_config=draft_vllm_config, prefix="draft_model")
EAGLE Proposer 详解
EAGLE 方法原理
EAGLE (Extrapolation Algorithm for Greater Language-model Efficiency) 利用 Target 模型的隐藏状态来预测 draft tokens:
graph TD
subgraph Target Model
T1[Input Token] --> T2[Transformer Layers]
T2 --> T3[Hidden States]
T3 --> T4[LM Head]
T4 --> T5[Output Token]
end
subgraph EAGLE Head
T3 --> E1[Feature Projection]
T5 --> E2[Token Embedding]
E1 --> E3[+]
E2 --> E3
E3 --> E4[Lightweight Transformer]
E4 --> E5[LM Head]
E5 --> E6[Draft Tokens]
end
style E1 fill:#f9f,stroke:#333
style E4 fill:#f9f,stroke:#333EAGLE 的关键创新:
- 复用 Target 模型的隐藏状态,避免独立编码
- 只需要很少的额外参数(通常 < 1% 的 Target 模型)
- 共享 Token Embedding 和 LM Head 权重
EagleProposer 实现
# vllm/v1/spec_decode/eagle.py
class EagleProposer(SpecDecodeBaseProposer):
def __init__(
self,
vllm_config: VllmConfig,
device: torch.device,
runner=None,
):
super().__init__(
vllm_config,
device,
pass_hidden_states_to_model=True, # 需要传递隐藏状态
runner=runner,
)
权重共享机制
EAGLE 模型与 Target 模型共享权重:
def load_model(self, target_model: nn.Module) -> None:
# 加载 EAGLE head
with set_model_tag("eagle_head"):
self.model = get_model(
vllm_config=self.vllm_config,
model_config=draft_model_config
)
# 检查是否需要共享 embedding
if hasattr(self.model, "has_own_embed_tokens"):
if not self.model.has_own_embed_tokens:
share_embeddings = True
logger.info("Sharing target model embedding weights with draft model")
if share_embeddings:
# 共享 embed_tokens
del self.model.model.embed_tokens
self.model.model.embed_tokens = target_embed_tokens
# 共享 lm_head
if share_lm_head:
del self.model.lm_head
self.model.lm_head = target_language_model.lm_head
拒绝采样(Rejection Sampling)
核心算法
拒绝采样确保输出分布与 Target 模型一致:
# vllm/v1/worker/gpu/spec_decode/rejection_sample.py
@triton.jit
def _rejection_sample_kernel(
sampled_ptr, # [num_reqs, num_speculative_steps + 1]
sampled_stride,
num_sampled_ptr, # [num_reqs]
target_sampled_ptr, # [num_draft_tokens + num_reqs]
input_ids_ptr, # [num_draft_tokens + num_reqs](draft tokens)
cu_num_logits_ptr, # [num_reqs + 1]
):
req_idx = tl.program_id(0)
start_idx = tl.load(cu_num_logits_ptr + req_idx)
end_idx = tl.load(cu_num_logits_ptr + req_idx + 1)
num_tokens = end_idx - start_idx
num_sampled = 0
rejected = False
# 逐个比较 draft token 和 target token
for i in range(num_tokens - 1):
if not rejected:
target_sampled = tl.load(target_sampled_ptr + start_idx + i)
draft_sampled = tl.load(input_ids_ptr + start_idx + i + 1)
# 存储 target 的采样结果
tl.store(sampled_ptr + req_idx * sampled_stride + i, target_sampled)
num_sampled += 1
# 检查是否匹配
if target_sampled != draft_sampled:
rejected = True # 一旦不匹配,后续全部拒绝
# 处理最后一个 token(bonus token)
if not rejected:
target_sampled = tl.load(target_sampled_ptr + start_idx + num_tokens - 1)
tl.store(
sampled_ptr + req_idx * sampled_stride + num_tokens - 1,
target_sampled
)
num_sampled += 1
tl.store(num_sampled_ptr + req_idx, num_sampled)
工作流程图解
flowchart TD
A[Draft: t1, t2, t3, t4, t5] --> B[Target 验证]
B --> C{t1 匹配?}
C -->|是| D{t2 匹配?}
C -->|否| E[拒绝 t1, t2, t3, t4, t5<br/>输出 target_t1]
D -->|是| F{t3 匹配?}
D -->|否| G[拒绝 t2, t3, t4, t5<br/>输出 t1, target_t2]
F -->|是| H{t4 匹配?}
F -->|否| I[拒绝 t3, t4, t5<br/>输出 t1, t2, target_t3]
H -->|是| J{t5 匹配?}
H -->|否| K[拒绝 t4, t5<br/>输出 t1, t2, t3, target_t4]
J -->|是| L[全部接受<br/>输出 t1-t5 + bonus token]
J -->|否| M[拒绝 t5<br/>输出 t1-t4, target_t5]
Tree-based 投机解码
树结构 Draft
vLLM 支持树结构的投机解码,可以同时探索多个候选路径:
graph TD
R[Root Token] --> A[Candidate A]
R --> B[Candidate B]
R --> C[Candidate C]
A --> A1[A-1]
A --> A2[A-2]
B --> B1[B-1]
B --> B2[B-2]
C --> C1[C-1]
C --> C2[C-2]
A1 --> A11[A-1-1]
A2 --> A21[A-2-1]
B1 --> B11[B-1-1]
B2 --> B21[B-2-1]
style A fill:#9f9
style A1 fill:#9f9
style A11 fill:#9f9Tree Attention
树结构需要特殊的注意力计算:
def propose_tree(
self,
batch_size: int,
logits: torch.Tensor,
positions: torch.Tensor,
hidden_states: torch.Tensor,
common_attn_metadata: CommonAttentionMetadata,
...
) -> list[torch.Tensor]:
# 解析投机 token 树
tree_depth = len(self.cu_drafts_per_level)
# 第一层:从 root 采样多个候选
num_children = self.child_drafts_per_level[0]
if num_children == 1:
draft_token_ids = logits.argmax(dim=-1).view(batch_size, -1)
else:
# Top-K 采样
draft_token_ids = torch.topk(
logits, num_children, dim=-1
).indices.view(batch_size, -1)
draft_token_ids_list = [draft_token_ids]
# 逐层生成
for level in range(tree_depth - 1):
# 构建树注意力元数据
attn_metadata = tree_attn_metadata_builder.build_for_drafting(
common_attn_metadata=common_attn_metadata,
draft_index=level + 1
)
# 执行前向传播
with set_forward_context(...):
last_hidden_states, hidden_states = self.model(
input_ids=self.input_ids[:num_input_tokens],
positions=self.positions[:num_input_tokens],
hidden_states=self.hidden_states[:num_input_tokens],
)
# 为下一层采样
logits = self.model.compute_logits(draft_last_hidden_states)
num_children = self.child_drafts_per_level[level + 1]
if num_children == 1:
draft_token_ids = logits.argmax(dim=-1).view(batch_size, -1)
else:
draft_token_ids = torch.topk(
logits, num_children, dim=-1
).indices.view(batch_size, -1)
draft_token_ids_list.append(draft_token_ids)
return draft_token_ids_list
投机解码配置与使用
配置参数
from vllm import LLM, SamplingParams
# 使用独立 Draft 模型
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
speculative_model="meta-llama/Llama-3.2-1B-Instruct",
num_speculative_tokens=5, # 每次投机生成的 token 数
)
# 使用 EAGLE
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
speculative_model="path/to/eagle-head",
speculative_method="eagle",
num_speculative_tokens=5,
)
命令行使用
# 使用 Draft 模型
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--speculative-model meta-llama/Llama-3.2-1B-Instruct \
--num-speculative-tokens 5
# 使用 EAGLE
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--speculative-model path/to/eagle-head \
--speculative-method eagle \
--num-speculative-tokens 5
性能优化
CUDA Graph 支持
投机解码支持 CUDA Graph 以减少 kernel launch 开销:
class SpecDecodeBaseProposer:
def initialize_cudagraph_keys(self, cudagraph_mode: CUDAGraphMode) -> None:
"""初始化 CUDA Graph dispatcher"""
if (
not self.speculative_config.enforce_eager
and cudagraph_mode.mixed_mode() in [CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL]
):
eagle_cudagraph_mode = CUDAGraphMode.PIECEWISE
else:
eagle_cudagraph_mode = CUDAGraphMode.NONE
self.cudagraph_dispatcher.initialize_cudagraph_keys(eagle_cudagraph_mode)
批量处理优化
Padded Drafter Batch 避免动态形状:
def prepare_inputs_padded(
self,
common_attn_metadata: CommonAttentionMetadata,
spec_decode_metadata: SpecDecodeMetadata,
valid_sampled_tokens_count: torch.Tensor,
) -> tuple[CommonAttentionMetadata, torch.Tensor, torch.Tensor]:
"""
准备投机解码的输入,使用 padding 保持固定形状。
被拒绝的 token 作为 padding,后续通过 token_indices_to_sample 过滤。
"""
num_reqs = common_attn_metadata.num_reqs
device = valid_sampled_tokens_count.device
token_indices_to_sample = torch.empty(
(num_reqs,), dtype=torch.int32, device=device
)
num_rejected_tokens_gpu = torch.empty(
(num_reqs,), dtype=torch.int32, device=device
)
# 使用 Triton kernel 计算
eagle_prepare_inputs_padded_kernel[grid](
spec_decode_metadata.cu_num_draft_tokens,
valid_sampled_tokens_count,
common_attn_metadata.query_start_loc,
token_indices_to_sample,
num_rejected_tokens_gpu,
num_reqs,
)
return spec_common_attn_metadata, token_indices_to_sample, num_rejected_tokens_gpu
投机解码的限制与注意事项
当前限制
- 多模态不完全支持:某些投机解码方法不支持多模态模型
- M-RoPE 限制:Draft Model 方法不支持 M-RoPE 位置编码
- 词表大小:Draft 和 Target 模型必须有相同的词表
- 张量并行:Draft 和 Target 的 TP 大小必须一致
最佳实践
选择合适的 K 值:
- 较大的 K 增加预测深度,但降低平均接受率
- 通常 K=3-5 是较好的平衡点
Draft 模型选择:
- 选择与 Target 模型同系列的小模型
- 确保词表完全一致
- EAGLE 通常比独立 Draft 模型效率更高
监控接受率:
# 检查投机解码统计
# vLLM 会在日志中输出平均接受率等指标
总结
投机解码是加速 LLM 推理的重要技术:
graph TD
A[投机解码核心思想] --> B[Draft Model 预测]
A --> C[Target Model 验证]
A --> D[拒绝采样保证正确性]
E[vLLM 实现特点] --> F[多种方法支持]
E --> G[CUDA Graph 优化]
E --> H[Tree Attention]
E --> I[权重共享]
J[最佳实践] --> K[选择合适的 K 值]
J --> L[监控接受率]
J --> M[选择匹配的 Draft 模型]关键要点:
- 核心原理:用小模型快速预测,大模型并行验证
- 无损加速:拒绝采样保证输出分布不变
- vLLM 优化:CUDA Graph、权重共享、批量处理
- 实际效果:通常可获得 1.5x-3x 的加速
参考资料
- Speculative Decoding 原论文
- EAGLE 论文
- Medusa 论文
- vLLM 投机解码文档
导航
5.2 - 量化技术
量化技术(Quantization)
概述
量化(Quantization)是一种将模型权重和激活值从高精度(如 FP32、FP16)转换为低精度(如 INT8、INT4、FP8)的技术。通过量化,可以显著减少模型的显存占用和计算量,从而提高推理效率。
本文将介绍量化的基本原理以及 vLLM 中支持的各种量化方法。
为什么需要量化
显存压力
以 LLaMA-70B 为例:
| 精度 | 每个参数占用 | 模型权重大小 |
|---|
| FP32 | 4 字节 | 280 GB |
| FP16/BF16 | 2 字节 | 140 GB |
| INT8 | 1 字节 | 70 GB |
| INT4 | 0.5 字节 | 35 GB |
加上 KV Cache 和激活值,FP16 推理需要多张高端 GPU;而 INT4 量化可以在单张 80GB GPU 上运行。
计算加速
graph LR
subgraph FP16 计算
A1[权重 FP16] --> B1[矩阵乘法]
B1 --> C1[输出 FP16]
end
subgraph INT8 计算
A2[权重 INT8] --> B2[整数矩阵乘法]
B2 --> C2[反量化]
C2 --> D2[输出 FP16]
end现代 GPU 的 INT8/INT4 计算单元比 FP16 更快:
- A100: INT8 Tensor Core 是 FP16 的 2 倍吞吐
- H100: FP8 Tensor Core 是 FP16 的 2 倍吞吐
量化的基本原理
线性量化
最常见的量化方法是线性量化(Linear Quantization):
量化: q = round((x - zero_point) / scale)
反量化: x ≈ q × scale + zero_point
其中:
x 是原始浮点值q 是量化后的整数值scale 是缩放因子zero_point 是零点偏移
对称量化 vs 非对称量化
graph TD
subgraph 对称量化
A1["-127 到 127"] --> B1["zero_point = 0"]
B1 --> C1["x = q × scale"]
end
subgraph 非对称量化
A2["-128 到 127"] --> B2["zero_point ≠ 0"]
B2 --> C2["x = q × scale + zero_point"]
end对称量化(Symmetric):
非对称量化(Asymmetric):
量化粒度
graph TD
A[量化粒度] --> B[Per-Tensor<br/>整个张量一个 scale]
A --> C[Per-Channel<br/>每个通道一个 scale]
A --> D[Per-Group<br/>每 G 个元素一个 scale]
A --> E[Per-Block<br/>每个块一个 scale]
B --> B1[内存效率最高<br/>精度最低]
C --> C1[权重常用]
D --> D1[精度与效率平衡]
E --> E1[块大小如 128]
vLLM 支持的量化方法
量化方法概览
vLLM 支持丰富的量化方法:
# vllm/model_executor/layers/quantization/__init__.py
QuantizationMethods = Literal[
"awq", # Activation-aware Weight Quantization
"fp8", # FP8 量化
"gptq", # Post-Training Quantization for GPT
"gptq_marlin", # GPTQ with Marlin kernel
"awq_marlin", # AWQ with Marlin kernel
"bitsandbytes", # BitsAndBytes 量化
"gguf", # GGUF 格式量化
"compressed-tensors", # 压缩张量
"torchao", # PyTorch AO 量化
"modelopt", # NVIDIA ModelOpt FP8
"mxfp4", # MXFP4 格式
...
]
量化配置基类
# vllm/model_executor/layers/quantization/base_config.py
class QuantizationConfig(ABC):
"""量化配置基类"""
@abstractmethod
def get_name(self) -> QuantizationMethods:
"""量化方法名称"""
raise NotImplementedError
@abstractmethod
def get_supported_act_dtypes(self) -> list[torch.dtype]:
"""支持的激活数据类型"""
raise NotImplementedError
@classmethod
@abstractmethod
def get_min_capability(cls) -> int:
"""最低 GPU 计算能力要求
70 = Volta, 75 = Turing, 80 = Ampere
"""
raise NotImplementedError
@abstractmethod
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> QuantizeMethodBase | None:
"""获取量化方法实现"""
raise NotImplementedError
class QuantizeMethodBase(ABC):
"""量化方法基类"""
@abstractmethod
def create_weights(
self, layer: torch.nn.Module, *weight_args, **extra_weight_attrs
):
"""创建量化权重"""
raise NotImplementedError
@abstractmethod
def apply(self, layer: torch.nn.Module, *args, **kwargs) -> torch.Tensor:
"""应用量化计算"""
raise NotImplementedError
FP8 量化详解
FP8 格式介绍
FP8 (8-bit Floating Point) 有两种主要格式:
| 格式 | 符号位 | 指数位 | 尾数位 | 动态范围 | 精度 |
|---|
| E4M3 | 1 | 4 | 3 | 较小 | 较高 |
| E5M2 | 1 | 5 | 2 | 较大 | 较低 |
E4M3 更适合权重,E5M2 更适合梯度。vLLM 主要使用 E4M3。
Fp8Config 实现
# vllm/model_executor/layers/quantization/fp8.py
class Fp8Config(QuantizationConfig):
"""FP8 量化配置"""
def __init__(
self,
is_checkpoint_fp8_serialized: bool = False, # 是否为 FP8 序列化的检查点
activation_scheme: str = "dynamic", # 激活量化方案
ignored_layers: list[str] | None = None, # 忽略的层
weight_block_size: list[int] | None = None, # 块大小
) -> None:
super().__init__()
self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
# 支持的激活方案: static, dynamic
if activation_scheme not in ACTIVATION_SCHEMES:
raise ValueError(f"Unsupported activation scheme {activation_scheme}")
self.activation_scheme = activation_scheme
self.ignored_layers = ignored_layers or []
self.weight_block_size = weight_block_size
@classmethod
def get_min_capability(cls) -> int:
return 75 # 最低支持 Turing 架构
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.bfloat16, torch.half]
动态 vs 静态激活量化
flowchart TD
subgraph 动态量化
A1[输入激活] --> B1[计算 min/max]
B1 --> C1[动态计算 scale]
C1 --> D1[量化为 FP8]
D1 --> E1[计算]
end
subgraph 静态量化
A2[校准数据集] --> B2[收集激活统计]
B2 --> C2[预计算 scale]
C2 --> D2[存储 scale]
A3[输入激活] --> E2[使用预存 scale]
D2 --> E2
E2 --> F2[量化为 FP8]
F2 --> G2[计算]
end动态量化:
静态量化:
AWQ 量化详解
AWQ 原理
AWQ (Activation-aware Weight Quantization) 的核心思想是:保护重要的权重通道。
graph TD
A[原始权重 W] --> B[计算激活幅度]
B --> C[识别重要通道]
C --> D[对重要通道缩放]
D --> E[均匀量化]
E --> F[反缩放恢复]关键洞察:
- 不同权重通道对输出的贡献不同
- 通过激活值的幅度识别重要通道
- 对重要通道进行缩放保护,减少量化误差
AWQ 在 vLLM 中的使用
# 加载 AWQ 量化模型
from vllm import LLM
llm = LLM(
model="TheBloke/Llama-2-7B-AWQ",
quantization="awq",
)
# 或者使用 Marlin 加速
llm = LLM(
model="TheBloke/Llama-2-7B-AWQ",
quantization="awq_marlin", # 使用 Marlin 内核
)
GPTQ 量化详解
GPTQ 原理
GPTQ (Post-Training Quantization for GPT) 使用二阶信息(Hessian)来最小化量化误差:
目标: min ||W - Q(W)||_H
其中 H 是 Hessian 矩阵
GPTQ 逐列量化,并使用 Hessian 信息来补偿量化误差:
flowchart LR
A[权重矩阵 W] --> B[计算 Hessian H]
B --> C[逐列量化]
C --> D[误差补偿]
D --> E[量化后权重 Q]GPTQ 配置
# 加载 GPTQ 量化模型
llm = LLM(
model="TheBloke/Llama-2-7B-GPTQ",
quantization="gptq",
)
# 使用 Marlin 内核加速
llm = LLM(
model="TheBloke/Llama-2-7B-GPTQ",
quantization="gptq_marlin",
)
Marlin 内核
Marlin 是什么
Marlin 是一套高度优化的 CUDA 内核,专门用于 INT4/INT8 矩阵乘法:
graph TD
A[量化权重 INT4/INT8] --> B[Marlin 内核]
C[激活值 FP16] --> B
B --> D[输出 FP16]
E[特点] --> F[高效的内存访问模式]
E --> G[优化的 Tensor Core 使用]
E --> H[支持异步预取]Marlin 相比普通内核可以提供 2-4 倍的加速。
支持的量化格式
awq_marlin: AWQ 格式 + Marlin 内核gptq_marlin: GPTQ 格式 + Marlin 内核gptq_marlin_24: 2:4 稀疏 GPTQ + Marlin 内核
BitsAndBytes 量化
4-bit 量化
BitsAndBytes 提供简单的 4-bit 量化:
llm = LLM(
model="meta-llama/Llama-2-7B",
quantization="bitsandbytes",
load_format="bitsandbytes",
)
NF4 格式
BitsAndBytes 使用 NF4 (Normal Float 4) 格式,专门优化正态分布的权重:
NF4 量化级别:
[-1.0, -0.6962, -0.5251, -0.3949, -0.2844, -0.1848, -0.0911, 0.0,
0.0796, 0.1609, 0.2461, 0.3379, 0.4407, 0.5626, 0.7230, 1.0]
这些级别根据正态分布的分位数选择,使量化误差最小化。
GGUF 格式
GGUF 简介
GGUF (GGML Universal File) 是一种通用的量化模型格式,支持多种量化级别:
| 量化类型 | 位数 | 说明 |
|---|
| Q4_0 | 4 | 基础 4-bit 量化 |
| Q4_K_M | 4 | K-means 聚类量化 |
| Q5_0 | 5 | 5-bit 量化 |
| Q5_K_M | 5 | K-means 5-bit |
| Q8_0 | 8 | 8-bit 量化 |
在 vLLM 中使用
llm = LLM(
model="TheBloke/Llama-2-7B-GGUF",
quantization="gguf",
)
KV Cache 量化
为什么量化 KV Cache
KV Cache 在长序列场景下占用大量显存。量化 KV Cache 可以:
- 减少显存占用 50-75%
- 支持更长的上下文
- 支持更大的批量大小
vLLM 中的 KV Cache 量化
# vllm/model_executor/layers/quantization/kv_cache.py
class BaseKVCacheMethod:
"""KV Cache 量化基类"""
def quant_kv_tensor(
self,
key: torch.Tensor,
value: torch.Tensor,
scale: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""量化 KV 张量"""
raise NotImplementedError
启用 KV Cache 量化:
llm = LLM(
model="meta-llama/Llama-2-7B",
kv_cache_dtype="fp8", # 或 "fp8_e4m3", "fp8_e5m2"
)
在线量化
什么是在线量化
在线量化是指在加载模型时动态进行量化,而不需要预先量化好的检查点:
# vllm/model_executor/model_loader/online_quantization.py
# 动态 FP8 量化
llm = LLM(
model="meta-llama/Llama-2-7B",
quantization="fp8",
# 将 FP16 权重动态转换为 FP8
)
优势与限制
优势:
- 无需预量化的检查点
- 灵活选择量化方法
- 适合实验和快速部署
限制:
- 加载时间稍长
- 某些量化方法不支持在线量化
- 精度可能略低于离线量化
量化方法对比
精度 vs 压缩率
graph LR
subgraph 高精度
A[FP16] --> B[2x 压缩]
end
subgraph 中等精度
C[FP8] --> D[4x 压缩]
E[INT8] --> F[4x 压缩]
end
subgraph 低精度
G[INT4] --> H[8x 压缩]
I[INT3] --> J[10.7x 压缩]
K[INT2] --> L[16x 压缩]
end性能对比表
| 量化方法 | 精度损失 | 速度提升 | 显存节省 | 推荐场景 |
|---|
| FP8 | 极低 | 1.5-2x | 50% | 生产环境 |
| AWQ | 低 | 2-3x | 75% | 长文本推理 |
| GPTQ | 低-中 | 2-3x | 75% | 资源受限 |
| BitsAndBytes | 中 | 1.5x | 75% | 快速实验 |
| GGUF | 可变 | 可变 | 可变 | CPU 推理 |
最佳实践
选择合适的量化方法
flowchart TD
A[选择量化方法] --> B{GPU 类型?}
B -->|H100/A100| C{精度要求?}
B -->|消费级| D{显存大小?}
C -->|高| E[FP8]
C -->|中| F[AWQ/GPTQ + Marlin]
D -->|24GB+| G[AWQ/GPTQ]
D -->|<24GB| H[GPTQ 4-bit]
I[无 GPU] --> J[GGUF]配置建议
# 高精度生产环境
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
quantization="fp8",
tensor_parallel_size=4,
)
# 资源受限环境
llm = LLM(
model="TheBloke/Llama-2-70B-GPTQ",
quantization="gptq_marlin",
tensor_parallel_size=2,
)
# 单 GPU 部署
llm = LLM(
model="TheBloke/Llama-2-7B-AWQ",
quantization="awq_marlin",
max_model_len=8192,
)
注意事项
- 验证精度:量化后务必在实际任务上验证精度
- 选择 Marlin:有 Marlin 版本时优先使用
- KV Cache 量化:长序列场景考虑启用
- 监控性能:关注吞吐量和延迟指标
总结
graph TD
A[量化技术核心] --> B[减少显存]
A --> C[加速计算]
A --> D[保持精度]
E[vLLM 支持] --> F[FP8 - 高精度]
E --> G[AWQ/GPTQ - 高压缩]
E --> H[Marlin - 高性能]
E --> I[KV Cache 量化]
J[选择建议] --> K[生产环境用 FP8]
J --> L[资源受限用 4-bit]
J --> M[使用 Marlin 加速]关键要点:
- 量化原理:用低精度表示权重,平衡精度与效率
- 多种方法:FP8、AWQ、GPTQ、BitsAndBytes 等
- Marlin 加速:INT4/INT8 专用高效内核
- 实际选择:根据硬件和精度需求选择合适方法
参考资料
- AWQ 论文
- GPTQ 论文
- FP8 格式规范
- vLLM 量化文档
- Marlin GitHub
导航
5.3 - 分布式推理
分布式推理(Distributed Inference)
概述
当模型规模超过单个 GPU 的显存容量时,需要使用分布式推理。vLLM 支持多种并行策略,可以将大模型分布到多个 GPU 上运行。
本文将介绍分布式推理的基本概念、并行策略以及 vLLM 中的实现细节。
为什么需要分布式推理
单卡显存限制
以 LLaMA-70B 为例(FP16 精度):
| 组件 | 显存占用 |
|---|
| 模型权重 | ~140 GB |
| KV Cache (4K 上下文, batch=32) | ~20 GB |
| 激活值 | ~5 GB |
| 总计 | ~165 GB |
即使是 H100 80GB,单卡也无法容纳完整模型。
分布式推理的目标
graph TD
A[分布式推理] --> B[支持更大模型]
A --> C[提高吞吐量]
A --> D[降低延迟]
B --> B1[70B/405B 模型]
C --> C1[更大批量]
D --> D1[并行计算]
并行策略概述
主要并行方式
graph TD
A[并行策略] --> B[张量并行 TP]
A --> C[流水线并行 PP]
A --> D[数据并行 DP]
B --> B1[切分权重矩阵]
C --> C1[切分模型层]
D --> D1[复制完整模型]各策略对比
| 策略 | 通信模式 | 显存效率 | 计算效率 | 适用场景 |
|---|
| 张量并行 | AllReduce | 高 | 高 | 单节点多卡 |
| 流水线并行 | P2P | 中 | 中 | 多节点 |
| 数据并行 | AllGather | 低 | 最高 | 高吞吐量 |
张量并行(Tensor Parallelism)
基本原理
张量并行将模型的权重矩阵切分到多个 GPU:
graph LR
subgraph 单卡计算
A1[输入 X] --> B1[完整权重 W]
B1 --> C1[输出 Y = XW]
end
subgraph 2卡张量并行
A2[输入 X] --> B2[权重 W1<br/>GPU 0]
A2 --> B3[权重 W2<br/>GPU 1]
B2 --> C2[Y1 = XW1]
B3 --> C3[Y2 = XW2]
C2 --> D2[AllReduce]
C3 --> D2
D2 --> E2[输出 Y]
end列并行与行并行
graph TD
subgraph 列并行 Column Parallel
W1["W = [W1 | W2]"] --> C1["输出 = [XW1 | XW2]"]
C1 --> R1["需要 AllGather"]
end
subgraph 行并行 Row Parallel
W2["W = [W1]<br/> [W2]"] --> C2["输出 = XW1 + XW2"]
C2 --> R2["需要 AllReduce"]
endLinear 层的张量并行:
- 第一个 Linear:列并行,输出分片
- 第二个 Linear:行并行,输入分片
vLLM 中的实现
# vllm/distributed/parallel_state.py
class GroupCoordinator:
"""分布式组协调器"""
def __init__(
self,
group_ranks: list[list[int]],
local_rank: int,
torch_distributed_backend: str,
use_pynccl: bool,
...
):
self.rank = torch.distributed.get_rank()
self.ranks = group_ranks[local_rank]
self.world_size = len(self.ranks)
self.local_rank = local_rank
# 创建通信组
self.device_group = torch.distributed.new_group(
self.ranks, backend=torch_distributed_backend
)
def all_reduce(self, input_: torch.Tensor) -> torch.Tensor:
"""AllReduce 操作"""
if self.world_size == 1:
return input_
if self.use_custom_op_call:
return torch.ops.vllm.all_reduce(input_, group_name=self.unique_name)
else:
return self._all_reduce_out_place(input_)
def all_gather(self, input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
"""AllGather 操作"""
if self.world_size == 1:
return input_
if self.use_custom_op_call:
return torch.ops.vllm.all_gather(
input_, dim, self.world_size, group_name=self.unique_name
)
else:
return self._all_gather_out_place(input_, dim)
def reduce_scatter(self, input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
"""ReduceScatter 操作"""
if self.world_size == 1:
return input_
return self._reduce_scatter_out_place(input_, dim)
通信原语
sequenceDiagram
participant G0 as GPU 0
participant G1 as GPU 1
participant G2 as GPU 2
participant G3 as GPU 3
Note over G0,G3: AllReduce (求和)
G0->>G0: [1]
G1->>G1: [2]
G2->>G2: [3]
G3->>G3: [4]
G0-->>G3: 通信
G0->>G0: [10]
G1->>G1: [10]
G2->>G2: [10]
G3->>G3: [10]
Note over G0,G3: AllGather (收集)
G0->>G0: [A]
G1->>G1: [B]
G2->>G2: [C]
G3->>G3: [D]
G0-->>G3: 通信
G0->>G0: [A,B,C,D]
G1->>G1: [A,B,C,D]
G2->>G2: [A,B,C,D]
G3->>G3: [A,B,C,D]
流水线并行(Pipeline Parallelism)
基本原理
流水线并行将模型的层分配到不同 GPU:
graph LR
subgraph GPU 0
L1[Layer 0-15]
end
subgraph GPU 1
L2[Layer 16-31]
end
subgraph GPU 2
L3[Layer 32-47]
end
subgraph GPU 3
L4[Layer 48-63]
end
L1 --> L2 --> L3 --> L4流水线调度
为了减少 GPU 空闲时间,使用微批次(micro-batch)流水线:
gantt
title 流水线并行调度
dateFormat X
axisFormat %s
section GPU 0
Micro 1 Forward: 0, 1
Micro 2 Forward: 1, 2
Micro 3 Forward: 2, 3
Micro 4 Forward: 3, 4
section GPU 1
Idle: 0, 1
Micro 1 Forward: 1, 2
Micro 2 Forward: 2, 3
Micro 3 Forward: 3, 4
Micro 4 Forward: 4, 5
section GPU 2
Idle: 0, 2
Micro 1 Forward: 2, 3
Micro 2 Forward: 3, 4
Micro 3 Forward: 4, 5
Micro 4 Forward: 5, 6vLLM 中的配置
from vllm import LLM
# 配置流水线并行
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
tensor_parallel_size=2, # 每个流水线阶段 2 卡张量并行
pipeline_parallel_size=2, # 2 个流水线阶段
# 总共需要 2 × 2 = 4 张 GPU
)
数据并行(Data Parallelism)
基本原理
数据并行复制完整模型到每个 GPU,各 GPU 处理不同的请求:
graph TD
subgraph 请求分发
R[请求队列] --> R1[请求 1,2,3]
R --> R2[请求 4,5,6]
end
subgraph GPU 0
M1[完整模型副本]
R1 --> M1
end
subgraph GPU 1
M2[完整模型副本]
R2 --> M2
endvLLM 中的数据并行
vLLM 支持通过多实例实现数据并行:
# 启动多个 vLLM 实例
# 实例 1:使用 GPU 0-1
CUDA_VISIBLE_DEVICES=0,1 vllm serve model --tensor-parallel-size 2 --port 8000
# 实例 2:使用 GPU 2-3
CUDA_VISIBLE_DEVICES=2,3 vllm serve model --tensor-parallel-size 2 --port 8001
然后使用负载均衡器分发请求。
通信后端
NCCL 通信
vLLM 使用 NCCL (NVIDIA Collective Communications Library) 进行 GPU 间通信:
# vllm/distributed/device_communicators/pynccl.py
class PyNcclCommunicator:
"""PyNccl 通信器"""
def __init__(self, group: ProcessGroup, device: torch.device):
self.group = group
self.device = device
# 初始化 NCCL 通信
self.nccl_comm = self._init_nccl()
def all_reduce(self, tensor: torch.Tensor) -> torch.Tensor:
"""使用 NCCL 执行 AllReduce"""
# 调用 NCCL AllReduce
return self._nccl_all_reduce(tensor)
自定义 AllReduce
vLLM 提供了优化的自定义 AllReduce 实现:
# vllm/distributed/device_communicators/custom_all_reduce.py
class CustomAllReduce:
"""
自定义 AllReduce,针对小张量优化。
使用共享内存和 CUDA 内核实现低延迟通信。
"""
def __init__(self, group: ProcessGroup):
self.group = group
# 分配共享内存用于通信
self._init_shared_memory()
def all_reduce(self, tensor: torch.Tensor) -> torch.Tensor:
# 对于小张量,使用自定义内核
if tensor.numel() < self.threshold:
return self._custom_all_reduce(tensor)
# 对于大张量,使用 NCCL
return self._nccl_all_reduce(tensor)
分布式初始化
初始化流程
sequenceDiagram
participant Main as 主进程
participant W0 as Worker 0
participant W1 as Worker 1
participant W2 as Worker 2
participant W3 as Worker 3
Main->>Main: 解析配置
Main->>W0: 启动 Worker
Main->>W1: 启动 Worker
Main->>W2: 启动 Worker
Main->>W3: 启动 Worker
Note over W0,W3: 初始化分布式环境
W0->>W0: init_distributed_environment()
W1->>W1: init_distributed_environment()
W2->>W2: init_distributed_environment()
W3->>W3: init_distributed_environment()
Note over W0,W3: 初始化模型并行组
W0->>W0: initialize_model_parallel()
W1->>W1: initialize_model_parallel()
W2->>W2: initialize_model_parallel()
W3->>W3: initialize_model_parallel()
Note over W0,W3: 加载模型(分片)
W0->>W0: load_model()
W1->>W1: load_model()
W2->>W2: load_model()
W3->>W3: load_model()并行组配置
# 并行组划分示例
# 假设 4 GPU,TP=2,PP=2
# 张量并行组:
# [GPU 0, GPU 1] # 第一阶段
# [GPU 2, GPU 3] # 第二阶段
# 流水线并行组:
# [GPU 0, GPU 2] # 第一个数据并行副本
# [GPU 1, GPU 3] # 第二个数据并行副本
分布式执行器
Executor 类型
vLLM 提供多种分布式执行器:
graph TD
A[Executor 类型] --> B[UniProcExecutor<br/>单进程]
A --> C[MultiprocExecutor<br/>多进程]
A --> D[RayDistributedExecutor<br/>Ray 分布式]
B --> B1[单 GPU 调试]
C --> C1[单节点多 GPU]
D --> D1[多节点集群]MultiprocExecutor
# vllm/v1/executor/multiproc_executor.py
class MultiprocExecutor:
"""多进程执行器,用于单节点多 GPU"""
def __init__(self, vllm_config: VllmConfig):
self.vllm_config = vllm_config
parallel_config = vllm_config.parallel_config
# 计算总 Worker 数
self.world_size = (
parallel_config.tensor_parallel_size *
parallel_config.pipeline_parallel_size
)
# 启动 Worker 进程
self.workers = self._start_workers()
def _start_workers(self):
"""使用 multiprocessing 启动 Worker"""
workers = []
for rank in range(self.world_size):
worker = multiprocessing.Process(
target=self._worker_main,
args=(rank,)
)
worker.start()
workers.append(worker)
return workers
Ray 分布式执行器
# 使用 Ray 进行多节点分布式推理
from vllm import LLM
# Ray 会自动检测集群中的 GPU
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
tensor_parallel_size=4, # 使用 4 张 GPU
distributed_executor_backend="ray",
)
KV Cache 分布式传输
Prefill-Decode 分离架构
vLLM 支持将 Prefill 和 Decode 阶段分离到不同节点:
graph LR
subgraph Prefill 节点
P1[GPU 0-3<br/>计算密集]
end
subgraph Decode 节点
D1[GPU 0-3<br/>内存密集]
end
subgraph KV Transfer
T[KV Cache 传输]
end
P1 --> T --> D1KV Connector 实现
# vllm/distributed/kv_transfer/kv_connector/v1/base.py
class KVConnectorBase:
"""KV Cache 传输连接器基类"""
def send_kv_cache(
self,
request_id: str,
kv_cache: torch.Tensor,
) -> None:
"""发送 KV Cache 到远程节点"""
raise NotImplementedError
def recv_kv_cache(
self,
request_id: str,
) -> torch.Tensor:
"""从远程节点接收 KV Cache"""
raise NotImplementedError
配置示例
单节点 4 GPU
# 单节点 4 GPU 张量并行
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
tensor_parallel_size=4,
)
# 命令行方式
vllm serve meta-llama/Llama-3.1-70B-Instruct --tensor-parallel-size 4
单节点 8 GPU
# 4 路张量并行 + 2 路流水线并行
llm = LLM(
model="meta-llama/Llama-3.1-405B-Instruct",
tensor_parallel_size=4,
pipeline_parallel_size=2,
)
多节点集群
# 节点 1(主节点)
vllm serve meta-llama/Llama-3.1-405B-Instruct \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--distributed-executor-backend ray
# 确保 Ray 集群已启动并包含所有节点
性能优化
通信优化
- 重叠计算与通信:
# 使用异步通信
with torch.cuda.stream(comm_stream):
all_reduce(tensor)
# 同时在计算流上进行其他操作
with torch.cuda.stream(compute_stream):
other_computation()
- 使用 Custom AllReduce:
# 对于小张量,使用自定义 AllReduce
# vLLM 会自动选择最优策略
负载均衡
对于流水线并行,确保每个阶段的层数均衡:
# 手动指定层分配(如果需要)
# 默认情况下 vLLM 会均匀分配
调试技巧
检查分布式状态
from vllm.distributed import (
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
get_pipeline_model_parallel_rank,
get_pipeline_model_parallel_world_size,
)
# 打印当前进程的并行信息
print(f"TP Rank: {get_tensor_model_parallel_rank()}")
print(f"TP World Size: {get_tensor_model_parallel_world_size()}")
print(f"PP Rank: {get_pipeline_model_parallel_rank()}")
print(f"PP World Size: {get_pipeline_model_parallel_world_size()}")
环境变量
# 设置 NCCL 调试级别
export NCCL_DEBUG=INFO
# 设置 NCCL 超时
export NCCL_TIMEOUT=1800
# 禁用 P2P 通信(调试用)
export NCCL_P2P_DISABLE=1
总结
graph TD
A[分布式推理] --> B[张量并行 TP]
A --> C[流水线并行 PP]
A --> D[数据并行 DP]
B --> B1[切分权重矩阵<br/>AllReduce 通信]
C --> C1[切分模型层<br/>P2P 通信]
D --> D1[复制完整模型<br/>请求分发]
E[vLLM 支持] --> F[NCCL 通信]
E --> G[Custom AllReduce]
E --> H[Ray 分布式]
E --> I[KV Cache 传输]
J[配置建议] --> K[单节点用 TP]
J --> L[多节点用 PP+TP]
J --> M[高吞吐用 DP]关键要点:
- 张量并行:单节点多 GPU 首选,低延迟
- 流水线并行:跨节点扩展,需要权衡
- 数据并行:吞吐量最高,但显存效率低
- 组合使用:大模型通常需要 TP+PP 组合
参考资料
- Megatron-LM 论文
- GPipe 论文
- NCCL 官方文档
- vLLM 分布式推理文档
- Ray 官方文档
导航
6 - 附录
术语表、代码索引和参考资料
本部分提供术语表、代码文件索引和参考资料,方便你在阅读过程中快速查阅。
6.1 - 术语表
术语表(Glossary)
本术语表按字母顺序列出了 vLLM 文档中使用的关键术语及其解释。
A
Activation(激活值)
神经网络中间层的输出张量。在推理过程中,激活值存储在 GPU 显存中,占用一定的显存空间。
AllGather(全收集)
分布式通信原语,将所有进程的数据收集到每个进程。用于张量并行中收集分片的输出。
AllReduce(全归约)
分布式通信原语,将所有进程的数据进行归约(如求和)并将结果分发到每个进程。是张量并行中最常用的通信操作。
Attention(注意力)
Transformer 架构的核心机制,用于计算序列中不同位置之间的关联性。通过 Query、Key、Value 三个矩阵计算注意力权重。
AWQ (Activation-aware Weight Quantization)
一种激活感知的权重量化方法,通过保护对输出影响大的通道来减少量化误差。
Async(异步)
vLLM 中的异步编程模式,允许在等待 I/O 或计算完成时处理其他任务,提高整体效率。
B
Batch Size(批大小)
同时处理的请求数量。更大的批大小通常能提高 GPU 利用率和吞吐量。
Block(块)
PagedAttention 中 KV Cache 的基本分配单位。每个块包含固定数量的 token 的 K 和 V 值。
Block Pool(块池)
管理所有物理块的组件,负责块的分配、释放和 LRU 驱逐。
Block Table(块表)
记录逻辑块到物理块映射关系的数据结构。类似于操作系统的页表。
BF16 (Brain Floating Point 16)
Google 开发的 16 位浮点格式,指数位与 FP32 相同,精度略低于 FP16 但动态范围更大。
C
Causal Mask(因果掩码)
在自回归生成中使用的掩码,防止模型看到未来的 token。也称为 Attention Mask。
Chunked Prefill(分块预填充)
将长输入分成多个小块进行处理的技术,可以与 Decode 阶段交错执行,降低延迟。
Continuous Batching(连续批处理)
vLLM 的核心调度策略,允许在每个迭代动态添加或移除请求,提高 GPU 利用率。
Copy-on-Write(写时复制)
内存管理技术,多个请求可以共享相同的 KV Cache 块,只在需要修改时才创建副本。
CUDA
NVIDIA 的并行计算平台和编程模型,用于 GPU 加速计算。
CUDA Graph
NVIDIA 的优化技术,将一系列 CUDA 操作捕获为图形,减少 kernel launch 开销。
D
Data Parallelism(数据并行)
分布式策略,将数据分配到多个设备,每个设备持有完整的模型副本。
Decode(解码阶段)
LLM 生成过程的第二阶段,逐个生成输出 token。特点是计算量小但依赖 KV Cache 读取。
Draft Model(草稿模型)
投机解码中使用的小型模型,快速生成候选 token 供目标模型验证。
E
EAGLE
一种高效的投机解码方法,利用目标模型的隐藏状态来预测 draft token。
Embedding(嵌入)
将离散的 token 映射到连续的向量空间的过程,或指嵌入向量本身。
EngineCore
vLLM V1 中的核心引擎组件,负责调度、执行和状态管理。
Executor(执行器)
负责管理 Worker 进程并协调模型执行的组件。
F
FFN (Feed-Forward Network)
Transformer 中的前馈网络层,通常由两个线性层和一个激活函数组成。
Flash Attention
一种 IO 优化的注意力计算方法,通过减少 GPU 内存访问显著提高效率。
FP8 (8-bit Floating Point)
8 位浮点数格式,有 E4M3 和 E5M2 两种变体,用于高效量化。
FP16 (16-bit Floating Point)
16 位浮点数格式,是 LLM 推理中常用的精度。
G
GELU (Gaussian Error Linear Unit)
一种激活函数,比 ReLU 更平滑,在 Transformer 中广泛使用。
GPTQ
一种基于二阶信息的后训练量化方法,可以将模型量化到 INT4 精度。
GPU Utilization(GPU 利用率)
GPU 计算资源的使用程度。Continuous Batching 的目标之一就是提高 GPU 利用率。
H
Head(头)
多头注意力中的一个注意力头。每个头独立计算注意力,捕获不同类型的关系。
Hidden Size(隐藏层大小)
Transformer 中间表示的维度,也称为模型维度(d_model)。
Hidden States(隐藏状态)
模型中间层的输出,在 EAGLE 等方法中用于指导 draft token 生成。
I
INT4/INT8
4 位或 8 位整数量化格式,用于减少模型显存占用和加速计算。
Iteration-Level Scheduling(迭代级调度)
每个推理迭代重新进行调度决策的策略,是 Continuous Batching 的基础。
K
Key(键)
注意力机制中的 Key 矩阵,与 Query 矩阵相乘计算注意力分数。
KV Cache
存储已计算的 Key 和 Value 的缓存,避免重复计算,是 LLM 推理优化的关键。
KVCacheManager
vLLM 中管理 KV Cache 分配和释放的组件。
L
Latency(延迟)
从请求发送到收到响应的时间。包括 TTFT(首 token 延迟)和 TPOT(单 token 延迟)。
LayerNorm(层归一化)
一种归一化技术,用于稳定训练和提高模型性能。
Linear Layer(线性层)
执行矩阵乘法和可选偏置加法的神经网络层。
LLM (Large Language Model)
大语言模型,通常指参数量在数十亿以上的语言模型。
LRU (Least Recently Used)
最近最少使用的缓存驱逐策略,用于 Block Pool 管理。
M
Marlin
一套高度优化的 CUDA 内核,用于 INT4/INT8 矩阵乘法加速。
Memory Bandwidth(内存带宽)
GPU 内存的数据传输速率,是 Decode 阶段的主要瓶颈。
MLP (Multi-Layer Perceptron)
多层感知机,在 Transformer 中通常指 FFN 层。
Multi-Head Attention(多头注意力)
将注意力分成多个头并行计算,捕获不同类型的依赖关系。
N
NCCL
NVIDIA Collective Communications Library,用于多 GPU 间高效通信。
num_heads(头数)
多头注意力中的头数量,影响模型的表达能力和计算量。
num_layers(层数)
Transformer 中的解码器层数量。
O
Output Processing(输出处理)
将模型输出转换为用户可读格式的过程,包括采样、去分词等。
P
PagedAttention
vLLM 的核心创新,将 KV Cache 分成固定大小的块进行非连续存储,减少显存碎片。
Pipeline Parallelism(流水线并行)
将模型的层分配到不同设备的并行策略,适用于多节点部署。
Position Encoding(位置编码)
向输入添加位置信息的方法,使模型能够理解序列顺序。
Preemption(抢占)
当内存不足时,暂停低优先级请求,释放资源给高优先级请求的机制。
Prefill(预填充阶段)
LLM 生成过程的第一阶段,并行处理所有输入 token 并初始化 KV Cache。
Prefix Caching(前缀缓存)
缓存相同前缀的 KV Cache,供后续请求复用,提高效率。
Q
Quantization(量化)
将高精度数值转换为低精度的技术,用于减少模型大小和加速计算。
Query(查询)
注意力机制中的 Query 矩阵,用于查询与其他位置的相关性。
R
Ray
分布式计算框架,vLLM 使用它进行多节点分布式推理。
Rejection Sampling(拒绝采样)
投机解码中验证 draft token 的方法,确保输出分布与只用目标模型一致。
Request(请求)
用户发送的推理请求,包含输入 prompt 和采样参数。
RMSNorm (Root Mean Square Normalization)
一种简化的归一化方法,计算效率比 LayerNorm 更高。
RoPE (Rotary Position Embedding)
旋转位置编码,通过旋转操作编码位置信息,支持长度外推。
S
Sampler(采样器)
根据模型输出的 logits 选择下一个 token 的组件。
Sampling Parameters(采样参数)
控制文本生成的参数,如 temperature、top_k、top_p 等。
Scale(缩放因子)
量化中用于映射浮点值和整数值的比例因子。
Scheduler(调度器)
决定哪些请求被执行、分配多少资源的核心组件。
Self-Attention(自注意力)
序列对自身进行注意力计算,捕获序列内部的依赖关系。
Sequence Length(序列长度)
输入或输出的 token 数量。
Slot Mapping(槽位映射)
将 token 位置映射到 KV Cache 存储位置的机制。
Softmax
将任意数值转换为概率分布的函数,在注意力计算中用于归一化。
Speculative Decoding(投机解码)
使用小模型预测、大模型验证的加速技术。
Streaming(流式输出)
边生成边返回结果的输出方式,降低用户感知延迟。
T
Temperature(温度)
采样参数,控制输出分布的平滑度。较高温度使输出更随机。
Tensor Parallelism(张量并行)
将模型的权重矩阵切分到多个设备的并行策略。
Throughput(吞吐量)
单位时间内处理的 token 数量,通常以 tokens/s 表示。
Token(词元)
文本的基本单位,由分词器生成。
Tokenization(分词)
将文本转换为 token 序列的过程。
Top-K Sampling
只从概率最高的 K 个 token 中采样的策略。
Top-P Sampling(Nucleus Sampling)
从累积概率达到 P 的 token 集合中采样的策略。
基于注意力机制的神经网络架构,是现代 LLM 的基础。
TTFT (Time To First Token)
首 token 延迟,从请求发送到收到第一个输出 token 的时间。
V
Value(值)
注意力机制中的 Value 矩阵,根据注意力权重聚合信息。
vLLM
高效的大语言模型推理引擎,核心创新是 PagedAttention。
Vocab Size(词表大小)
模型支持的不同 token 数量。
W
Weight(权重)
模型的可学习参数,存储在模型文件中。
Worker
执行模型计算的工作进程,在分布式设置中运行在各个 GPU 上。
Z
Zero-Point(零点)
量化中的偏移值,用于非对称量化。
导航
6.2 - 代码文件索引
代码文件索引(Code Map)
本文档提供 vLLM 代码库的关键文件索引,帮助读者快速定位感兴趣的代码。
代码目录结构概览
vllm/
├── entrypoints/ # 入口点
│ ├── llm.py # Python API 入口
│ ├── cli/ # 命令行入口
│ └── openai/ # OpenAI 兼容 API
│
├── v1/ # V1 版本核心实现
│ ├── engine/ # 引擎相关
│ ├── core/ # 核心调度和内存管理
│ ├── worker/ # Worker 执行
│ ├── attention/ # 注意力实现
│ ├── sample/ # 采样器
│ └── spec_decode/ # 投机解码
│
├── model_executor/ # 模型执行
│ ├── models/ # 模型实现
│ └── layers/ # 层实现和量化
│
├── distributed/ # 分布式通信
│
├── config/ # 配置管理
│
└── csrc/ # CUDA 内核
└── attention/ # Attention CUDA 内核
入口点(Entry Points)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/entrypoints/llm.py | Python API 入口 | LLM, LLM.generate(), LLM.chat() |
vllm/entrypoints/cli/main.py | CLI 入口 | serve, bench 命令 |
vllm/entrypoints/openai/api_server.py | OpenAI API 服务 | API 端点定义 |
vllm/engine/arg_utils.py | 参数解析 | EngineArgs, create_engine_config() |
V1 引擎(Engine)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/engine/llm_engine.py | LLM 引擎入口 | LLMEngine |
vllm/v1/engine/core.py | 引擎核心 | EngineCore, EngineCore.step() |
vllm/v1/engine/processor.py | 输入/输出处理 | InputProcessor, OutputProcessor |
vllm/v1/engine/async_llm.py | 异步引擎 | AsyncLLM |
核心调度(Core Scheduling)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/core/sched/scheduler.py | 调度器 | Scheduler, Scheduler.schedule() |
vllm/v1/core/sched/request_queue.py | 请求队列 | FCFSRequestQueue, PriorityRequestQueue |
vllm/v1/core/kv_cache_manager.py | KV Cache 管理 | KVCacheManager, allocate_slots(), free() |
vllm/v1/core/block_pool.py | 块池管理 | BlockPool, FreeKVCacheBlockQueue |
vllm/v1/core/kv_cache_utils.py | KV Cache 工具 | KVCacheBlock, BlockHashToBlockMap |
请求处理(Request)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/request.py | 请求数据结构 | Request, RequestStatus |
vllm/sampling_params.py | 采样参数 | SamplingParams |
vllm/outputs.py | 输出数据结构 | RequestOutput, CompletionOutput |
Worker 执行(Worker)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/worker/gpu_worker.py | GPU Worker | GPUWorker |
vllm/v1/worker/gpu_model_runner.py | 模型执行 | GPUModelRunner, execute_model() |
vllm/v1/worker/gpu_input_batch.py | 输入批处理 | InputBatch, CachedRequestState |
Executor 执行器
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/executor/abstract.py | 执行器基类 | Executor |
vllm/v1/executor/uniproc_executor.py | 单进程执行器 | UniProcExecutor |
vllm/v1/executor/multiproc_executor.py | 多进程执行器 | MultiprocExecutor |
vllm/v1/executor/ray_distributed.py | Ray 分布式执行器 | RayDistributedExecutor |
注意力机制(Attention)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/attention/ops/paged_attn.py | PagedAttention 接口 | PagedAttention |
vllm/v1/attention/backends/flash_attn.py | Flash Attention 后端 | FlashAttentionBackend |
vllm/v1/attention/backends/triton_attn.py | Triton Attention 后端 | TritonAttentionBackend |
vllm/attention/layer.py | Attention 层 | Attention |
采样(Sampling)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/sample/sampler.py | 采样器 | Sampler, Sampler.forward() |
vllm/v1/sample/metadata.py | 采样元数据 | SamplingMetadata |
vllm/v1/sample/ops/penalties.py | 惩罚项计算 | apply_penalties() |
vllm/v1/sample/ops/topk_topp.py | Top-K/Top-P 采样 | apply_top_k_top_p() |
投机解码(Speculative Decoding)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/v1/spec_decode/eagle.py | EAGLE 基类 | SpecDecodeBaseProposer, EagleProposer |
vllm/v1/spec_decode/draft_model.py | Draft Model | DraftModelProposer |
vllm/v1/spec_decode/medusa.py | Medusa | MedusaProposer |
vllm/v1/worker/gpu/spec_decode/rejection_sample.py | 拒绝采样 | rejection_sample() |
模型实现(Models)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/model_executor/models/llama.py | LLaMA 模型 | LlamaForCausalLM |
vllm/model_executor/models/qwen2.py | Qwen2 模型 | Qwen2ForCausalLM |
vllm/model_executor/models/mixtral.py | Mixtral MoE | MixtralForCausalLM |
vllm/model_executor/models/deepseek_v2.py | DeepSeek V2 | DeepseekV2ForCausalLM |
vllm/model_executor/model_loader/loader.py | 模型加载 | get_model() |
量化(Quantization)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/model_executor/layers/quantization/__init__.py | 量化入口 | get_quantization_config() |
vllm/model_executor/layers/quantization/base_config.py | 量化基类 | QuantizationConfig |
vllm/model_executor/layers/quantization/fp8.py | FP8 量化 | Fp8Config |
vllm/model_executor/layers/quantization/awq.py | AWQ 量化 | AWQConfig |
vllm/model_executor/layers/quantization/gptq.py | GPTQ 量化 | GPTQConfig |
分布式通信(Distributed)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/distributed/parallel_state.py | 并行状态管理 | GroupCoordinator |
vllm/distributed/communication_op.py | 通信操作 | tensor_model_parallel_all_reduce() |
vllm/distributed/device_communicators/pynccl.py | NCCL 通信 | PyNcclCommunicator |
vllm/distributed/device_communicators/custom_all_reduce.py | 自定义 AllReduce | CustomAllReduce |
配置(Config)
| 文件路径 | 说明 | 关键类/函数 |
|---|
vllm/config/vllm.py | 总配置 | VllmConfig |
vllm/config/model.py | 模型配置 | ModelConfig |
vllm/config/parallel.py | 并行配置 | ParallelConfig |
vllm/config/scheduler.py | 调度器配置 | SchedulerConfig |
vllm/config/cache.py | 缓存配置 | CacheConfig |
CUDA 内核(CUDA Kernels)
| 文件路径 | 说明 |
|---|
csrc/attention/paged_attention_v1.cu | PagedAttention V1 内核 |
csrc/attention/paged_attention_v2.cu | PagedAttention V2 内核 |
csrc/quantization/ | 量化相关内核 |
csrc/moe/ | MoE 相关内核 |
关键函数速查
请求处理流程
# 1. 用户调用
LLM.generate() # vllm/entrypoints/llm.py
# 2. 引擎处理
LLMEngine.add_request() # vllm/v1/engine/llm_engine.py
EngineCore.step() # vllm/v1/engine/core.py
# 3. 调度
Scheduler.schedule() # vllm/v1/core/sched/scheduler.py
KVCacheManager.allocate_slots() # vllm/v1/core/kv_cache_manager.py
# 4. 执行
GPUModelRunner.execute_model() # vllm/v1/worker/gpu_model_runner.py
model.forward() # vllm/model_executor/models/*.py
# 5. 采样
Sampler.forward() # vllm/v1/sample/sampler.py
# 6. 输出
OutputProcessor.process() # vllm/v1/engine/processor.py
KV Cache 管理流程
# 分配
KVCacheManager.allocate_slots() # vllm/v1/core/kv_cache_manager.py
BlockPool.get_free_blocks() # vllm/v1/core/block_pool.py
# 释放
KVCacheManager.free() # vllm/v1/core/kv_cache_manager.py
BlockPool.free_blocks() # vllm/v1/core/block_pool.py
# 前缀缓存
KVCacheManager.get_computed_blocks() # vllm/v1/core/kv_cache_manager.py
调试建议
关键断点位置
| 功能 | 文件:行号 | 说明 |
|---|
| 请求添加 | v1/engine/llm_engine.py:add_request | 追踪请求入口 |
| 调度决策 | v1/core/sched/scheduler.py:schedule | 理解调度逻辑 |
| KV 分配 | v1/core/kv_cache_manager.py:allocate_slots | 内存分配 |
| 模型执行 | v1/worker/gpu_model_runner.py:execute_model | 前向传播 |
| 采样 | v1/sample/sampler.py:forward | Token 采样 |
日志配置
# 详细日志
export VLLM_LOGGING_LEVEL=DEBUG
# 函数追踪
export VLLM_TRACE_FUNCTION=1
# 调度器日志
export VLLM_LOG_SCHEDULER=1
导航
6.3 - 参考资料
参考资料(References)
本文档汇总了学习 vLLM 和 LLM 推理优化所需的关键参考资料。
官方资源
vLLM 官方
vLLM GitHub 仓库
vLLM 官方文档
vLLM 博客
核心论文
PagedAttention
- Efficient Memory Management for Large Language Model Serving with PagedAttention
- Attention Is All You Need
Flash Attention
投机解码
Fast Inference from Transformers via Speculative Decoding
EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty
Medusa: Simple LLM Inference Acceleration Framework with Multiple Decoding Heads
量化技术
AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration
GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers
FP8 Formats for Deep Learning
分布式并行
深度学习基础
书籍
在线课程
GPU 和 CUDA
NVIDIA 官方
性能优化
- GPU Performance Background User’s Guide
相关项目
推理引擎
量化工具
模型库
技术博客
LLM 推理
vLLM 相关
- vLLM: PagedAttention for 24x Faster LLM Inference
社区资源
讨论论坛
vLLM Discord
Hugging Face Forums
GitHub Issues
学习路径建议
入门阶段
- 阅读《动手学深度学习》Transformer 章节
- 阅读 “The Illustrated Transformer”
- 了解 vLLM 基本使用
进阶阶段
- 阅读 PagedAttention 论文
- 阅读 Flash Attention 论文
- 学习 vLLM 源码中的核心模块
深入阶段
- 阅读量化相关论文(AWQ、GPTQ)
- 阅读投机解码论文(Speculative Decoding、EAGLE)
- 了解分布式并行(Megatron-LM)
导航