.

.

文档

本文档系列旨在帮助深度学习初学者深入理解 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

问题的本质

  1. 内部碎片:预分配的空间大部分没有被使用
  2. 外部碎片:剩余的小块空间无法满足新请求的预分配需求
  3. 浪费比例:研究表明,传统方案的显存浪费率高达 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

问题分析

  1. 必须等待最长序列:一个批次中,所有请求必须等待最长的那个完成才能返回结果
  2. 无法动态调整:批次一旦开始,就不能添加新请求或移除已完成的请求
  3. 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:#ffcdd2

Decode 阶段 GPU 利用率低的原因

  1. 计算量小:每次只处理 1 个新 token,计算量很小
  2. 内存访问多:需要读取整个 KV Cache,内存访问量大
  3. 计算/访存比低:GPU 大部分时间在等待数据从显存传输到计算单元

实际测量表明,在 Decode 阶段,GPU 的计算单元利用率可能只有 10-30%!这意味着昂贵的 GPU 大部分时间都在"摸鱼"。


2. vLLM 的解决方案

面对上述三大困境,vLLM 提出了两项核心创新:PagedAttentionContinuous 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:#e8f5e9

PagedAttention 的工作方式

  1. 块(Block):将 KV Cache 划分为固定大小的块,每个块存储固定数量 token 的 KV 数据
  2. 按需分配:请求开始时不预分配空间,生成新 token 时才分配新的块
  3. 非连续存储:一个请求的 KV Cache 可以分散在不连续的物理块中
  4. 块表映射:通过块表(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

连续批处理的工作原理

  1. 迭代级调度:每生成一个 token(一个迭代),就重新进行调度决策
  2. 动态加入:新到达的请求可以立即加入当前批次
  3. 动态退出:已完成的请求立即释放资源,不需要等待其他请求
  4. 资源复用:退出请求释放的资源立即分配给新请求

让我们用时间线对比一下:

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 Transformers1.0x(基准)基准
Text Generation Inference2.2x+120%
vLLM14-24x+1300-2300%

这不是印刷错误——vLLM 的吞吐量可以达到 HuggingFace Transformers 的 14-24 倍

3.2 显存效率对比

指标传统方案vLLM
显存浪费率60-80%< 4%
最大并发请求数高 2-4 倍
内存碎片严重几乎没有

3.3 延迟特性

指标含义vLLM 表现
TTFT首 token 延迟优化(通过 Chunked Prefill)
TPS每秒生成 token 数高且稳定
P99 延迟99% 请求的最大延迟低且稳定

4. vLLM 与其他框架对比

4.1 vs HuggingFace Transformers

特性HuggingFacevLLM
定位通用深度学习框架LLM 推理专用框架
易用性非常简单简单
吞吐量高(14-24x)
显存效率
适用场景开发、实验生产部署

选择建议

  • 如果你在做模型研究或小规模实验,HuggingFace 更方便
  • 如果你需要部署生产服务,vLLM 是更好的选择

4.2 vs Text Generation Inference (TGI)

特性TGIvLLM
开发者HuggingFaceUC Berkeley
核心优化Flash AttentionPagedAttention
连续批处理支持支持
吞吐量中等
生态集成HuggingFace 生态独立

选择建议

  • 如果你深度使用 HuggingFace 生态,TGI 集成更好
  • 如果追求极致性能,vLLM 通常更快

4.3 vs DeepSpeed-Inference

特性DeepSpeedvLLM
开发者MicrosoftUC Berkeley
主要优化分布式推理单机吞吐量
显存管理传统方式PagedAttention
适用规模超大模型中大模型

选择建议

  • 如果模型太大,单机放不下,考虑 DeepSpeed
  • 如果单机可以放下,vLLM 通常性能更好

5. vLLM 的典型应用场景

5.1 在线 API 服务

# 使用 vLLM 启动 OpenAI 兼容的 API 服务
# vllm serve meta-llama/Llama-2-7b-hf --port 8000

适用于:

  • ChatGPT 类对话应用
  • 代码补全服务
  • 文本生成 API

5.2 离线批量处理

from vllm import LLM, SamplingParams

# 创建 LLM 实例
llm = LLM(model="meta-llama/Llama-2-7b-hf")

# 批量生成
prompts = ["Hello, my name is", "The capital of France is"]
sampling_params = SamplingParams(temperature=0.8, max_tokens=100)
outputs = llm.generate(prompts, sampling_params)

适用于:

  • 大规模数据处理
  • 模型评测
  • 数据生成

5.3 多模型服务

vLLM 支持同时加载多个模型,适用于:

  • A/B 测试
  • 模型对比
  • 多任务服务

6. 本章小结

在本章中,我们了解了:

  1. 传统 LLM 推理面临的三大困境

    • 显存碎片化导致资源浪费
    • 静态批处理效率低下
    • GPU 利用率低
  2. vLLM 的两大核心创新

    • PagedAttention:借鉴操作系统虚拟内存,实现高效显存管理
    • Continuous Batching:迭代级调度,动态添加/移除请求
  3. vLLM 的性能优势

    • 吞吐量提升 14-24 倍
    • 显存浪费率从 60-80% 降至 4% 以下
    • 延迟稳定可预测
  4. 框架选择建议

    • 研究实验:HuggingFace Transformers
    • 生产部署:vLLM
    • 超大模型:DeepSpeed-Inference

思考题

  1. 为什么 PagedAttention 选择固定大小的块,而不是可变大小?
  2. 连续批处理相比静态批处理,有什么潜在的缺点?
  3. 如果你有一个 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 模型权重的显存占用

模型权重的显存占用计算相对简单:

模型权重显存 = 参数量 × 每个参数的字节数

不同精度下的字节数:

精度每个参数字节数说明
FP324 字节全精度浮点
FP16/BF162 字节半精度浮点(推理常用)
INT81 字节8 位整数量化
INT40.5 字节4 位整数量化

示例计算:LLaMA-2-7B 模型

精度计算显存占用
FP327B × 4 = 28GB28 GB
FP167B × 2 = 14GB14 GB
INT87B × 1 = 7GB7 GB
INT47B × 0.5 = 3.5GB3.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_layers32
hidden_dim4096
FP16 bytes2

单个请求,不同序列长度的 KV Cache 大小:

序列长度计算KV Cache 大小
5122 × 32 × 4096 × 512 × 2256 MB
10242 × 32 × 4096 × 1024 × 2512 MB
20482 × 32 × 4096 × 2048 × 21 GB
40962 × 32 × 4096 × 4096 × 22 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 连续空间的请求!

碎片化的两种类型

  1. 内部碎片(Internal Fragmentation)

    • 预分配 1GB,实际只用 100MB
    • 浪费了 900MB
  2. 外部碎片(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:#ffcdd2

Prefill 阶段(以 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 需要

  1. 读取模型权重:14GB(FP16)
  2. 读取 KV Cache:约 500MB(1000 tokens × 0.5MB/token)
  3. 计算 Attention 和 FFN
  4. 写入新的 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效率
单请求14GB11x
批处理 3214GB3232x
批处理 6414GB6464x

这就是为什么批处理对 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 完成

动态性表现在

  1. 到达时间不确定:用户随机发送请求
  2. 输入长度不确定:每个请求的 prompt 长度不同
  3. 输出长度不确定:生成长度取决于模型和停止条件
  4. 请求优先级不同:可能有 VIP 用户或紧急请求

3.2 异构性带来的挑战

即使在同一批次中,不同请求的特性也大不相同:

批次中的请求分布示例:
┌─────────┬──────────┬──────────┬─────────────┐
│ 请求 ID │ 输入长度 │ 当前输出 │ 状态        │
├─────────┼──────────┼──────────┼─────────────┤
│ A       │ 10       │ 95       │ Decode 中   │
│ B       │ 500      │ 0        │ Prefill 中  │
│ C       │ 50       │ 200      │ 即将完成    │
│ D       │ 100      │ 30       │ Decode 中   │
│ E       │ 1000     │ 0        │ 等待 Prefill│
└─────────┴──────────┴──────────┴─────────────┘

异构性带来的问题

  1. 序列长度不一致

    • 不同请求的 KV Cache 大小不同
    • Attention 计算的工作量不同
  2. 状态不一致

    • 有的在 Prefill,有的在 Decode
    • 计算密度差异巨大
  3. 完成时间不一致

    • 有的即将完成,有的刚开始
    • 资源释放时机不确定

3.3 静态批处理的问题

传统静态批处理无法处理这种动态异构的场景:

gantt
    title 静态批处理的问题
    dateFormat X
    axisFormat %s

    section 请求 A
    处理 (100 tokens)  :done, a, 0, 100
    等待其他请求完成    :crit, await, 100, 200

    section 请求 B
    处理 (200 tokens)  :done, b, 0, 200

    section 请求 C
    等待加入批次        :crit, cwait, 0, 200

    section GPU 利用
    有效计算          :active, gpu1, 0, 100
    部分空闲          :crit, gpu2, 100, 200

问题总结

问题影响
必须等待最长请求短请求延迟增加
无法动态加入新请求等待时间长
资源不能复用显存利用率低
GPU 利用率波动平均效率低

4. 关键性能指标详解

理解 LLM 推理的性能指标对于评估和优化系统至关重要。

4.1 吞吐量(Throughput)

定义:单位时间内处理的 token 数或请求数

Token 吞吐量 = 总生成 tokens / 总时间 (tokens/second)
请求吞吐量 = 总完成请求 / 总时间 (requests/second)

影响因素

  • GPU 计算能力
  • 内存带宽
  • 批处理大小
  • 显存效率

4.2 延迟(Latency)

延迟有多个维度:

sequenceDiagram
    participant U as 用户
    participant S as 系统

    U->>S: 发送请求
    Note over S: 排队等待时间 (Queue Time)

    S->>S: 开始处理
    Note over S: Prefill 时间

    S-->>U: 第一个 token
    Note over U: TTFT (Time To First Token)

    S-->>U: 后续 tokens...
    Note over S: Decode 时间

    S-->>U: 最后一个 token
    Note over U: 总延迟 (End-to-End Latency)

关键延迟指标

指标英文说明用户感知
TTFTTime To First Token首个 token 生成时间响应速度感
TPOTTime Per Output Token每个输出 token 的时间流畅度感
总延迟End-to-End Latency完整响应时间等待时间

4.3 TPS(Tokens Per Second)

定义:每秒生成的 token 数

有两种计算方式:

  1. 单请求 TPS:单个请求的生成速度
  2. 系统 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:生成速度,影响流畅度
  • 吞吐量:系统容量,影响服务规模

思考题

  1. 对于一个 70B 参数的模型,在 A100 80GB GPU 上最多能支持多长的序列?
  2. 为什么 Decode 阶段不能简单地通过增加 GPU 计算核心来加速?
  3. 如果用户主要发送很短的请求(< 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 --> GPUExecutor

EngineCore.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:#fff9c4

Scheduler(调度器)

文件位置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

调度流程简述

  1. 处理 running 请求

    • 计算每个请求需要的新 token 数
    • 尝试分配 KV Cache
    • 内存不足时执行抢占
  2. 处理 waiting 请求

    • 按优先级从队列取出请求
    • 检查资源是否足够
    • 分配资源并移入 running
  3. 返回 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 --> KVCacheBlock

2.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 IDsSchedulerOutput
positions位置编码索引计算得到
block_tables块表映射KVCacheManager
slot_mapping槽位映射KVCacheManager
kv_cachesKV 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:#c8e6c9

Flash 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_idsRequest 对象EngineCore
调度RequestSchedulerOutputScheduler
缓存分配Requestslot_mapping, block_tablesKVCacheManager
模型执行TensorslogitsGPUModelRunner
采样logitstoken_id=318Sampler
状态更新token_id更新 RequestScheduler
输出处理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 APIvllm/entrypoints/llm.pyLLM, generate()
CLIvllm/entrypoints/cli/main.pymain()
引擎
同步引擎vllm/v1/engine/llm_engine.pyLLMEngine
异步引擎vllm/v1/engine/async_llm.pyAsyncLLM
核心逻辑vllm/v1/engine/core.pyEngineCore, step()
调度
调度器vllm/v1/core/sched/scheduler.pyScheduler, schedule()
请求队列vllm/v1/core/sched/request_queue.pyRequestQueue
内存管理
KV Cachevllm/v1/core/kv_cache_manager.pyKVCacheManager
块池vllm/v1/core/block_pool.pyBlockPool
执行
模型运行vllm/v1/worker/gpu_model_runner.pyGPUModelRunner
Workervllm/v1/worker/gpu_worker.pyGPUWorker
注意力
PagedAttentionvllm/v1/attention/ops/paged_attn.pyPagedAttention
Flash Attentionvllm/v1/attention/backends/flash_attn.pyFlashAttentionBackend
数据结构
请求vllm/v1/request.pyRequest, RequestStatus
采样参数vllm/sampling_params.pySamplingParams

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 --> SchedulerConfig

5.2 常用配置参数

参数说明默认值
--model模型路径或名称必填
--dtype数据精度auto
--max-model-len最大序列长度模型默认
--gpu-memory-utilizationGPU 显存利用率0.9
--max-num-seqs最大并发请求数256
--block-sizeKV Cache 块大小16
--enable-prefix-caching启用前缀缓存False
--tensor-parallel-size张量并行大小1

6. V1 vs 旧版架构

vLLM 当前主要使用 V1 架构,相比旧版有以下改进:

特性旧版V1
调度器BlockSpaceManagerKVCacheManager
执行流程同步为主异步优化
内存管理基础 PagedAttention更细粒度的块管理
前缀缓存有限支持完整支持
代码组织分散模块化

本文档系列主要基于 V1 架构进行讲解。


7. 本章小结

架构层次

  1. 用户接口层:提供 Python API、CLI、OpenAI API 等多种访问方式
  2. 引擎层:LLMEngine/AsyncLLM 协调输入输出处理
  3. 核心层:Scheduler 和 KVCacheManager 负责调度和内存管理
  4. 执行层: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/

思考题

  1. 为什么 vLLM 要将 EngineCore 和 LLMEngine 分开设计?
  2. Scheduler 和 KVCacheManager 之间是如何协作的?
  3. 如果你要添加一个新的用户接口(比如 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))

特点

  • 平滑、非单调
  • 与 GELU 类似的效果

2.3 激活函数对比

graph LR
    subgraph 激活函数特性对比
        R[ReLU] --> R1[简单高效]
        R --> R2[可能死神经元]

        G[GELU] --> G1[平滑非线性]
        G --> G2[Transformer 首选]

        S[SiLU] --> S1[平滑非单调]
        S --> S2[LLaMA 使用]
    end
函数公式范围使用场景
ReLUmax(0, x)[0, +∞)传统 CNN
GELUx·Φ(x)(-∞, +∞)BERT, GPT
SiLUx·σ(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 --> T

3.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:#c8e6c9

GPU 优势

特点CPUGPU
核心数4-641000-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 --> O2

5.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:#c8e6c9
import 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. 本章小结

核心概念

  1. 神经元:接收输入、加权求和、应用激活函数、产生输出
  2. 激活函数:引入非线性,GELU 是 LLM 的常用选择
  3. 张量:多维数组,神经网络中数据的载体
  4. 矩阵乘法:神经网络的核心计算,GPU 加速的关键

关键公式

神经元输出: y = f(w · x + b)
全连接层参数量: in_features × out_features + out_features

LLM 相关

  • Token:文本的基本单位
  • Embedding:将 token ID 转换为向量
  • 语言模型:预测下一个 token 的概率分布
  • 推理:使用训练好的模型进行预测

与 vLLM 的关联

  • 张量形状理解对于理解 vLLM 的内存管理至关重要
  • GPU 并行计算是 vLLM 性能优化的基础
  • 推理优化是 vLLM 的核心目标

思考题

  1. 为什么现代 LLM 普遍使用 GELU 而不是 ReLU?
  2. 如果一个模型有 7B 参数,使用 FP16 精度,需要多少显存存储权重?
  3. 批量矩阵乘法如何帮助提高 GPU 利用率?

下一步

神经网络基础已经介绍完毕,接下来我们将学习 LLM 的核心架构——Transformer:

👉 下一章:Transformer 架构详解

2.2 - Transformer 架构详解

Transformer 架构详解

本章将详细介绍 Transformer 架构,这是现代大语言模型的基础。


引言

2017 年,Google 发表了划时代的论文《Attention Is All You Need》,提出了 Transformer 架构。这个架构彻底改变了自然语言处理领域,成为了 GPT、BERT、LLaMA 等现代 LLM 的基础。

理解 Transformer 架构是理解 vLLM 优化原理的关键。


1. Transformer 的诞生背景

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
    end

RNN 的问题

问题说明
顺序依赖必须按顺序处理,无法并行
长距离依赖难以捕获长序列中的远距离关系
梯度问题长序列训练时梯度消失或爆炸
训练慢无法充分利用 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. Transformer 整体架构

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 成为主流?

优势说明
统一架构预训练和下游任务使用相同架构
自回归生成天然适合文本生成任务
扩展性参数量扩展效果好
简单高效架构简单,训练推理更高效

2.3 单层 Transformer Block 结构

每个 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

关键组件

  1. Layer Normalization:归一化,稳定训练
  2. Multi-Head Self-Attention:捕获序列内的关系
  3. Feed Forward Network (FFN):非线性变换
  4. 残差连接:缓解梯度消失,帮助信息流动

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))

其中:

  • pos:位置索引
  • i:维度索引
  • 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]
    end

RoPE 的优势

  • 相对位置信息自然编码
  • 支持任意长度外推
  • 计算高效
# 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:#c8e6c9

Pre-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

优势

  • 缓解梯度消失
  • 允许训练更深的网络
  • 信息直接传递不会丢失

9. 完整 Transformer Block 代码

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 参数分布

组件公式参数量
Embeddingvocab × hidden32000 × 4096 = 131M
每层 Attention Qhidden × hidden4096² = 16.8M
每层 Attention Khidden × (hidden/n_heads × n_kv_heads)4096 × 4096 = 16.8M
每层 Attention Vhidden × (hidden/n_heads × n_kv_heads)4096 × 4096 = 16.8M
每层 Attention Ohidden × hidden4096² = 16.8M
每层 FFN gatehidden × intermediate4096 × 11008 = 45.1M
每层 FFN uphidden × intermediate4096 × 11008 = 45.1M
每层 FFN downintermediate × hidden11008 × 4096 = 45.1M
每层 Norm2 × hidden2 × 4096 = 8K
LM Headhidden × vocab4096 × 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. 本章小结

架构要点

  1. Decoder-Only 架构:现代 LLM 的主流选择
  2. Transformer Block:Attention + FFN + Norm + 残差
  3. 位置编码:RoPE 是现代标准

关键组件

组件作用现代实现
EmbeddingToken → Vector直接查表
位置编码注入位置信息RoPE
Self-Attention捕获序列关系Multi-Head
FFN非线性变换SwiGLU
Layer Norm稳定训练RMSNorm
残差连接信息直传Pre-Norm

参数分布

  • FFN 占主导(约 65%)
  • Attention 约 32%
  • Embedding 约 2%

与 vLLM 的关联

  • Attention 计算是 KV Cache 优化的核心
  • 参数分布影响显存使用和优化策略
  • 位置编码影响序列长度支持

思考题

  1. 为什么 Decoder-Only 架构在 LLM 中比 Encoder-Decoder 更流行?
  2. RoPE 相比正弦位置编码有什么优势?
  3. 为什么 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
    end

1.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:#c8e6c9

2.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$ 的点积结果会变得很大。这会导致:

  1. Softmax 饱和:大值经过 softmax 后趋近于 1,小值趋近于 0
  2. 梯度消失: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:#c8e6c9

3.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/>关注其他模式]
    end

4.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_dimnum_headshead_dim
GPT-2 Small7681264
GPT-2 Large12802064
LLaMA-7B409632128
LLaMA-70B819264128

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
    end

5.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 AttentionIO 优化,减少内存访问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
    end

7.2 GQA 的优势

特性MHAGQA
Q headsNN
K/V headsNN/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/VQuery(查询)、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 优化注意力计算速度

思考题

  1. 如果没有缩放因子 $\sqrt{d_k}$,会发生什么?
  2. 为什么 GQA 可以在减少 KV heads 的同时保持模型质量?
  3. 在因果掩码下,位置 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 需要:

  1. 计算自己的 Q(Query)
  2. 计算自己的 K(Key)和 V(Value)
  3. 用 Q 与所有 K 计算注意力
  4. 用注意力加权所有 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:#ffcdd2

1.3 计算量分析

生成 N 个 token,不使用 KV Cache:

Step需要计算的 K/V累计 K/V 计算次数
111
22(重新计算 1 + 新的 1)1 + 2 = 3
33(重新计算 2 + 新的 1)3 + 3 = 6
NN1 + 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:#c8e6c9

2.2 计算量对比

使用 KV Cache 后:

Step需要计算的 K/V累计 K/V 计算次数
111
21(只计算新的)1 + 1 = 2
31(只计算新的)2 + 1 = 3
N1N

时间复杂度:$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_layersTransformer 层数32
2K 和 V2
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

序列长度计算大小
5122 × 32 × 4096 × 512 × 2256 MB
10242 × 32 × 4096 × 1024 × 2512 MB
20482 × 32 × 4096 × 2048 × 21 GB
40962 × 32 × 4096 × 4096 × 22 GB
81922 × 32 × 4096 × 8192 × 24 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)" : 1
pie 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:#ffcdd2

5.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 要解决的问题!

传统方案的问题:

  1. 预分配浪费:每个请求预留最大空间
  2. 内部碎片:实际使用远小于预分配
  3. 外部碎片:释放后的空间不连续

PagedAttention 的解决方案(下一部分详细介绍):

  1. 按需分配:用多少分配多少
  2. 分块管理:固定大小的块,减少碎片
  3. 非连续存储:块可以不连续

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 操作对比

操作PrefillDecode
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. 本章小结

核心概念

  1. KV Cache 的作用:缓存历史 token 的 K、V,避免重复计算
  2. 加速效果:从 $O(N^2)$ 降到 $O(N)$,约 500 倍加速(N=1000)
  3. 显存占用:随序列长度线性增长,可能成为主要显存消耗

关键公式

KV Cache = 2 × num_layers × num_kv_heads × head_dim × seq_len × bytes

管理挑战

  • 动态增长:序列长度在生成过程中不断增加
  • 预分配浪费:传统方案浪费 60-80% 显存
  • 碎片化:多请求并发时问题更严重

与 vLLM 的关联

  • PagedAttention:解决 KV Cache 的显存浪费问题
  • 分块管理:将 KV Cache 分成固定大小的块
  • 按需分配:用多少分配多少,不预留

思考题

  1. 如果一个模型使用 GQA,KV heads 是 attention heads 的 1/8,KV Cache 显存会减少多少?
  2. 为什么 Decode 阶段是"内存密集型"而不是"计算密集型"?
  3. 如果 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 个 tokens1 个 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:#c8e6c9

2.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
    end

3.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
    end

4.7 常用参数组合

场景TemperatureTop-pTop-k
代码生成0.1-0.3--
事实问答0.0-0.50.9-
通用对话0.7-0.90.940
创意写作1.0-1.20.9550
脑暴创意1.5-2.00.98100

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
特性PrefillDecode
每次处理 tokensN1
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, 80

6.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. 本章小结

生成流程

  1. Tokenization:文本 → Token IDs
  2. Prefill:并行处理输入,初始化 KV Cache
  3. Decode:逐个生成 token,增量更新 KV Cache
  4. Sampling:从 logits 采样 token
  5. Detokenization:Token IDs → 文本

两阶段特性

阶段PrefillDecode
并行度低(每次 1 token)
计算密度
瓶颈计算内存带宽
优化重点并行计算批处理

采样策略

  • Greedy:确定性,取最大概率
  • Temperature:控制随机程度
  • Top-k:限制候选数量
  • Top-p:动态限制累积概率

与 vLLM 的关联

  • Continuous Batching:动态组合 Prefill 和 Decode
  • Chunked Prefill:分块处理长输入
  • 采样优化:批量采样提高效率

思考题

  1. 为什么 Decode 阶段不能像 Prefill 那样并行处理多个 token?
  2. 如果使用 temperature=0,结果会和 greedy decoding 一样吗?
  3. 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:#ffcdd2

1.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

关键特性

  1. 程序看到连续的地址空间
  2. 物理内存可以不连续
  3. 按需分配(用到才分配)
  4. 页面可以共享

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:#c8e6c9

4.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 --> O

5.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.cupaged_attention_v2.cu

7.2 V1 vs V2 内核

特性V1V2
适用场景短序列长序列
分块策略简单两级分块
性能中等更优

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%
PagedAttention96%+高 2-4 倍

8.2 吞吐量提升

graph LR
    subgraph 吞吐量对比
        T1[传统方案<br/>1x 基准]
        T2[PagedAttention<br/>2-4x 提升]
    end

    style T2 fill:#c8e6c9

8.3 碎片率

传统方案:
- 内部碎片: 50-70%
- 外部碎片: 10-20%
- 总碎片: 60-80%

PagedAttention:
- 内部碎片: < 4% (最后一个块)
- 外部碎片: 0% (固定大小块)
- 总碎片: < 4%

9. 本章小结

核心创新

  1. 分块存储:将 KV Cache 分成固定大小的 Block
  2. 非连续分配:Block 可以分散在显存任意位置
  3. 按需分配:生成新 token 时才分配新 Block
  4. 块表映射:通过 Block Table 管理逻辑到物理的映射

关键数据结构

结构作用
BlockKV Cache 的基本存储单元
Block Table逻辑块 → 物理块映射
Slot MappingToken 位置 → 缓存槽位
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

思考题

  1. 为什么选择固定大小的 Block 而不是可变大小?
  2. 前缀缓存和 Copy-on-Write 有什么区别和联系?
  3. 如果 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:#e8f5e9

KVCacheManager 的核心职责:

  1. 分配管理:为请求分配 KV Cache 槽位
  2. 前缀缓存:查找和利用已缓存的前缀块
  3. 生命周期管理:跟踪和释放请求的内存块
  4. 使用率监控:提供 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:#c8e6c9

3.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:#c8e6c9

4.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_utilizationGPU 显存利用率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. 代码位置速查

功能文件关键函数/类
KVCacheManagervllm/v1/core/kv_cache_manager.pyKVCacheManager
分配结果vllm/v1/core/kv_cache_manager.pyKVCacheBlocks 数据类
协调器vllm/v1/core/kv_cache_coordinator.pyKVCacheCoordinator
BlockPoolvllm/v1/core/block_pool.pyBlockPool
Block 数据结构vllm/v1/core/kv_cache_utils.pyKVCacheBlock

8. 小结

本章我们深入了解了 KVCacheManager 的工作原理:

  1. 核心职责:管理 KV Cache 的分配、释放和前缀缓存
  2. 分层设计:KVCacheManager → Coordinator → BlockPool
  3. 关键方法
    • get_computed_blocks():查找前缀缓存
    • allocate_slots():分配缓存槽位
    • free():释放请求资源
  4. 前缀缓存:通过 Block Hash 实现多请求共享
  5. 与调度器协作:为调度决策提供内存管理支持

在下一章中,我们将深入 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:#e8f5e9

BlockPool 的核心职责:

  1. 块管理:维护所有物理块的元数据
  2. 分配/释放:从空闲队列分配块,释放块回队列
  3. 缓存管理:维护 Block Hash 到 Block 的映射
  4. 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:#c8e6c9

4.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/>不加入空闲队列
    end

4.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. 代码位置速查

功能文件关键类/函数
BlockPoolvllm/v1/core/block_pool.pyBlockPool
KVCacheBlockvllm/v1/core/kv_cache_utils.pyKVCacheBlock 数据类
空闲队列vllm/v1/core/kv_cache_utils.pyFreeKVCacheBlockQueue
缓存查找表vllm/v1/core/block_pool.pyBlockHashToBlockMap
Hash 计算vllm/v1/core/kv_cache_utils.pyhash_block_tokens()
块数计算vllm/v1/core/kv_cache_utils.pyget_num_blocks()

10. 小结

本章我们深入了解了 BlockPool 的工作原理:

  1. 核心数据结构

    • KVCacheBlock:块元数据,包含引用计数和 hash
    • FreeKVCacheBlockQueue:双向链表实现的 LRU 空闲队列
    • BlockHashToBlockMap:前缀缓存的 hash 查找表
  2. 关键操作

    • get_new_blocks():从队列头部分配块
    • free_blocks():减少引用计数,ref_cnt=0 时加入队列尾部
    • touch():缓存命中时增加引用计数
    • cache_full_blocks():缓存满块的 hash
  3. LRU 驱逐

    • 队列头部是最久未使用的块
    • 逆序释放确保前缀块保留更久
  4. 块生命周期: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:#e1f5fe

Scheduler 的核心职责:

  1. 请求队列管理:维护 waiting 和 running 两个队列
  2. 资源分配决策:决定哪些请求可以获得 GPU 资源
  3. 内存协调:与 KVCacheManager 协作管理 KV Cache
  4. 抢占处理:在内存不足时执行抢占策略
  5. 输出构建:为 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_seqs256最多同时运行的请求数
max_num_batched_tokens2048每步最多处理的 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 好处

  1. 降低延迟:长 prefill 不会阻塞其他请求
  2. 更好的资源利用:允许多个请求交替执行
  3. 内存平滑:避免一次性分配大量 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/>检查完成条件
    end

7.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.pyScheduler
schedule 方法vllm/v1/core/sched/scheduler.py:313schedule()
抢占逻辑vllm/v1/core/sched/scheduler.py:892_preempt_request()
调度输出vllm/v1/core/sched/output.pySchedulerOutput
请求队列vllm/v1/core/sched/request_queue.pycreate_request_queue()
调度策略vllm/v1/core/sched/request_queue.pySchedulingPolicy

10. 小结

本章我们深入了解了 vLLM 调度器的工作原理:

  1. 双队列管理:waiting 和 running 队列
  2. 调度算法
    • 先处理 running 请求
    • 再处理 waiting 请求
    • 内存不足时执行抢占
  3. 抢占机制:释放低优先级请求的资源
  4. 调度策略:FCFS 和 Priority
  5. 分块预填充:降低长输入的延迟影响
  6. 与其他组件协作: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:#c8e6c9

6.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_utilizationGPU 利用率> 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.pystep()
调度器vllm/v1/core/sched/scheduler.pyschedule()
状态更新vllm/v1/core/sched/scheduler.pyupdate_from_output()
完成检测vllm/v1/core/sched/utils.pycheck_stop()
请求释放vllm/v1/core/sched/scheduler.py_free_request()

11. 小结

本章我们深入了解了连续批处理机制:

  1. 静态批处理的问题:GPU 空闲、高延迟、低吞吐量
  2. 连续批处理的解决方案:迭代级调度,动态加入/退出
  3. vLLM 的实现
    • 每个 step 重新调度
    • 完成的请求立即释放资源
    • 新请求立即加入批次
  4. Prefill 与 Decode 混合:不同阶段的请求交替执行
  5. 分块预填充:长输入分成多个 chunk 处理
  6. 与其他技术协同: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批量处理、脚本调用、研究实验
在线服务AsyncLLMEngineWeb 服务、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 --> AsyncEngine

5.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.pyLLM
LLMEnginevllm/v1/engine/llm_engine.pyLLMEngine
EngineCorevllm/v1/engine/core.pyEngineCore
API Servervllm/entrypoints/openai/api_server.pymain()
配置参数vllm/engine/arg_utils.pyEngineArgs
请求类vllm/v1/request.pyRequest
请求状态vllm/v1/request.pyRequestStatus 枚举

9. 小结

本章我们了解了 vLLM 的入口点和请求处理流程:

  1. 两种使用方式

    • LLM 类用于离线批量推理
    • API Server 用于在线服务
  2. 核心组件层次

    • LLMLLMEngineEngineCoreScheduler + Executor
  3. 请求生命周期

    • 用户提交 → tokenize → 调度 → 执行 → 采样 → 返回
  4. 配置系统

    • EngineArgsVllmConfig → 各子配置

在下一章中,我们将深入 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:#e1f5fe

3.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 --> Output

7.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.pyExecutor
单进程 Executorvllm/v1/executor/uniproc_executor.pyUniProcExecutor
多进程 Executorvllm/v1/executor/multiproc_executor.pyMultiprocExecutor
Ray Executorvllm/v1/executor/ray_executor.pyRayDistributedExecutor
Worker 基类vllm/v1/worker/worker_base.pyWorkerBase
GPU Workervllm/v1/worker/gpu_worker.pyWorker
GPU ModelRunnervllm/v1/worker/gpu_model_runner.pyGPUModelRunner
Block Tablevllm/v1/worker/block_table.pyBlockTable

10. 小结

本章我们深入了解了 Executor 和 Worker 层:

  1. Executor 类型

    • UniProcExecutor:单进程单 GPU
    • MultiprocExecutor:单机多卡
    • RayDistributedExecutor:多机分布式
  2. Worker 职责

    • 设备初始化
    • 模型加载
    • 模型执行
    • KV Cache 管理
  3. ModelRunner

    • 输入准备
    • 前向传播
    • logits 计算
  4. 分布式执行

    • 张量并行:切分权重矩阵
    • 流水线并行:切分模型层
    • 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]
    end

1.2 核心组件对应关系

组件vLLM 类功能
EmbeddingVocabParallelEmbeddingtoken 到向量的映射
Transformer LayerLlamaDecoderLayer主要计算单元
Self-AttentionLlamaAttention注意力计算
MLPLlamaMLP前馈网络
LayerNormRMSNorm归一化
LM HeadParallelLMHead输出词表概率

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. Transformer 层详解

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
    end

4.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.pyLlamaForCausalLM
Transformer 层vllm/model_executor/models/llama.pyLlamaDecoderLayer
Self-Attentionvllm/model_executor/models/llama.pyLlamaAttention
MLPvllm/model_executor/models/llama.pyLlamaMLP
Attention 后端vllm/attention/layer.pyAttention
旋转位置编码vllm/model_executor/layers/rotary_embedding.pyget_rope()
并行线性层vllm/model_executor/layers/linear.py*ParallelLinear
RMSNormvllm/model_executor/layers/layernorm.pyRMSNorm

10. 小结

本章我们深入了解了 vLLM 中模型前向传播的实现:

  1. 模型结构

    • LlamaForCausalLMLlamaModelLlamaDecoderLayer
    • Pre-LN 结构,每层包含 Attention 和 MLP
  2. Self-Attention

    • QKV 合并投影
    • RoPE 旋转位置编码
    • GQA 减少 KV Cache 内存
    • PagedAttention 集成
  3. MLP

    • SwiGLU 激活函数
    • gate_up 合并投影
  4. 张量并行

    • ColumnParallelLinear:输出切分
    • RowParallelLinear:输入切分 + AllReduce
  5. 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

采样的主要步骤:

  1. Logits 处理:应用各种处理器修改原始 logits
  2. 温度调节:控制随机性
  3. Top-k/Top-p 过滤:限制候选 token 范围
  4. 实际采样:从处理后的分布中选择 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. 批量采样优化

7.1 SamplingMetadata

# 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 常见场景配置

场景temperaturetop_ktop_p说明
代码生成0.0--贪婪,确保正确性
技术写作0.3-0.9低随机性
创意写作0.8500.95高随机性
对话0.7400.9平衡
头脑风暴1.2-0.95非常随机

8.2 惩罚参数建议

参数推荐范围说明
repetition_penalty1.0-1.2轻微惩罚重复
frequency_penalty0.0-0.5减少高频词
presence_penalty0.0-0.5鼓励新话题

9. 代码位置速查

组件文件关键类/函数
采样参数vllm/sampling_params.pySamplingParams
Samplervllm/v1/sample/sampler.pySampler
采样元数据vllm/v1/sample/metadata.pySamplingMetadata
Top-k/Top-pvllm/v1/sample/ops/topk_topp_sampler.pyTopKTopPSampler
惩罚vllm/v1/sample/ops/penalties.pyapply_*_penalty()
Logprobsvllm/v1/sample/ops/logprobs.pygather_logprobs()

10. 小结

本章我们深入了解了 vLLM 的采样过程:

  1. 采样参数:temperature、top_k、top_p、min_p 等
  2. 采样策略
    • 贪婪采样:确定性,选择最高概率
    • 温度采样:控制随机性
    • Top-k:只考虑前 k 个 token
    • Top-p:累积概率阈值过滤
    • Min-p:相对于最高概率的阈值
  3. 惩罚机制
    • 重复惩罚:惩罚已出现 token
    • 频率惩罚:基于出现次数
    • 存在惩罚:基于是否出现
  4. Logprobs:返回 token 的对数概率
  5. 批量优化:通过 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.pySamplerOutput, ModelRunnerOutput
EngineCore 输出vllm/v1/engine/__init__.pyEngineCoreOutput
用户输出vllm/outputs.pyRequestOutput, CompletionOutput
状态更新vllm/v1/core/sched/scheduler.pyupdate_from_output()
停止检查vllm/v1/core/sched/utils.pycheck_stop()
Detokenizervllm/v1/engine/detokenizer.pyIncrementalDetokenizer
OpenAI 格式vllm/entrypoints/openai/protocol.pyChatCompletionResponse

10. 小结

本章我们详细分析了输出处理流程:

  1. 数据结构链路

    • SamplerOutputModelRunnerOutputEngineCoreOutputRequestOutput
  2. 状态更新

    • 追加 token 到请求
    • 检查停止条件
    • 处理完成的请求
  3. 停止条件

    • EOS token
    • 最大长度
    • 停止字符串/token
  4. Detokenization

    • 增量解码
    • 处理部分字符
  5. 流式输出

    • Server-Sent Events
    • 增量返回
  6. 格式化

    • OpenAI 兼容格式
    • Logprobs 处理

在下一章中,我们将完整跟踪一个请求从提交到返回的完整生命周期。


导航

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. 小结

本章我们完整跟踪了一个请求的生命周期:

  1. 提交阶段

    • Tokenize → 创建请求 → 加入 waiting 队列
  2. 调度阶段

    • 查找缓存 → 分配 KV Cache → 移入 running
  3. 执行阶段

    • 准备输入 → 前向传播 → 采样
  4. 更新阶段

    • 追加 token → 检查停止 → 更新状态
  5. 返回阶段

    • Detokenize → 构建输出 → 返回用户

通过这个完整的流程分析,我们可以看到 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

关键洞察

  1. Draft 模型生成 K 个候选 token
  2. Target 模型一次性验证所有候选
  3. 接受正确的前缀,从第一个错误位置重新生成
  4. 验证的计算量 ≈ 生成一个 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:#333

EAGLE 的关键创新

  1. 复用 Target 模型的隐藏状态,避免独立编码
  2. 只需要很少的额外参数(通常 < 1% 的 Target 模型)
  3. 共享 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:#9f9

Tree 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

投机解码的限制与注意事项

当前限制

  1. 多模态不完全支持:某些投机解码方法不支持多模态模型
  2. M-RoPE 限制:Draft Model 方法不支持 M-RoPE 位置编码
  3. 词表大小:Draft 和 Target 模型必须有相同的词表
  4. 张量并行:Draft 和 Target 的 TP 大小必须一致

最佳实践

  1. 选择合适的 K 值

    • 较大的 K 增加预测深度,但降低平均接受率
    • 通常 K=3-5 是较好的平衡点
  2. Draft 模型选择

    • 选择与 Target 模型同系列的小模型
    • 确保词表完全一致
    • EAGLE 通常比独立 Draft 模型效率更高
  3. 监控接受率

    # 检查投机解码统计
    # 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 模型]

关键要点:

  1. 核心原理:用小模型快速预测,大模型并行验证
  2. 无损加速:拒绝采样保证输出分布不变
  3. vLLM 优化:CUDA Graph、权重共享、批量处理
  4. 实际效果:通常可获得 1.5x-3x 的加速

参考资料

  1. Speculative Decoding 原论文
  2. EAGLE 论文
  3. Medusa 论文
  4. vLLM 投机解码文档

导航

5.2 - 量化技术

量化技术(Quantization)

概述

量化(Quantization)是一种将模型权重和激活值从高精度(如 FP32、FP16)转换为低精度(如 INT8、INT4、FP8)的技术。通过量化,可以显著减少模型的显存占用和计算量,从而提高推理效率。

本文将介绍量化的基本原理以及 vLLM 中支持的各种量化方法。


为什么需要量化

显存压力

以 LLaMA-70B 为例:

精度每个参数占用模型权重大小
FP324 字节280 GB
FP16/BF162 字节140 GB
INT81 字节70 GB
INT40.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):

  • 零点固定为 0
  • 计算更简单
  • 适合权重分布对称的情况

非对称量化(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) 有两种主要格式:

格式符号位指数位尾数位动态范围精度
E4M3143较小较高
E5M2152较大较低

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

动态量化

  • 运行时计算 scale
  • 精度更高
  • 开销稍大

静态量化

  • 使用预计算的 scale
  • 计算更快
  • 需要校准数据

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_04基础 4-bit 量化
Q4_K_M4K-means 聚类量化
Q5_055-bit 量化
Q5_K_M5K-means 5-bit
Q8_088-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-2x50%生产环境
AWQ2-3x75%长文本推理
GPTQ低-中2-3x75%资源受限
BitsAndBytes1.5x75%快速实验
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,
)

注意事项

  1. 验证精度:量化后务必在实际任务上验证精度
  2. 选择 Marlin:有 Marlin 版本时优先使用
  3. KV Cache 量化:长序列场景考虑启用
  4. 监控性能:关注吞吐量和延迟指标

总结

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 加速]

关键要点:

  1. 量化原理:用低精度表示权重,平衡精度与效率
  2. 多种方法:FP8、AWQ、GPTQ、BitsAndBytes 等
  3. Marlin 加速:INT4/INT8 专用高效内核
  4. 实际选择:根据硬件和精度需求选择合适方法

参考资料

  1. AWQ 论文
  2. GPTQ 论文
  3. FP8 格式规范
  4. vLLM 量化文档
  5. 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"]
    end

Linear 层的张量并行

  • 第一个 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, 6

vLLM 中的配置

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
    end

vLLM 中的数据并行

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 --> D1

KV 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 集群已启动并包含所有节点

性能优化

通信优化

  1. 重叠计算与通信
# 使用异步通信
with torch.cuda.stream(comm_stream):
    all_reduce(tensor)

# 同时在计算流上进行其他操作
with torch.cuda.stream(compute_stream):
    other_computation()
  1. 使用 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]

关键要点:

  1. 张量并行:单节点多 GPU 首选,低延迟
  2. 流水线并行:跨节点扩展,需要权衡
  3. 数据并行:吞吐量最高,但显存效率低
  4. 组合使用:大模型通常需要 TP+PP 组合

参考资料

  1. Megatron-LM 论文
  2. GPipe 论文
  3. NCCL 官方文档
  4. vLLM 分布式推理文档
  5. 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 集合中采样的策略。

Transformer

基于注意力机制的神经网络架构,是现代 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.pyPython API 入口LLM, LLM.generate(), LLM.chat()
vllm/entrypoints/cli/main.pyCLI 入口serve, bench 命令
vllm/entrypoints/openai/api_server.pyOpenAI API 服务API 端点定义
vllm/engine/arg_utils.py参数解析EngineArgs, create_engine_config()

V1 引擎(Engine)

文件路径说明关键类/函数
vllm/v1/engine/llm_engine.pyLLM 引擎入口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.pyKV Cache 管理KVCacheManager, allocate_slots(), free()
vllm/v1/core/block_pool.py块池管理BlockPool, FreeKVCacheBlockQueue
vllm/v1/core/kv_cache_utils.pyKV 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.pyGPU WorkerGPUWorker
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.pyRay 分布式执行器RayDistributedExecutor

注意力机制(Attention)

文件路径说明关键类/函数
vllm/v1/attention/ops/paged_attn.pyPagedAttention 接口PagedAttention
vllm/v1/attention/backends/flash_attn.pyFlash Attention 后端FlashAttentionBackend
vllm/v1/attention/backends/triton_attn.pyTriton Attention 后端TritonAttentionBackend
vllm/attention/layer.pyAttention 层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.pyTop-K/Top-P 采样apply_top_k_top_p()

投机解码(Speculative Decoding)

文件路径说明关键类/函数
vllm/v1/spec_decode/eagle.pyEAGLE 基类SpecDecodeBaseProposer, EagleProposer
vllm/v1/spec_decode/draft_model.pyDraft ModelDraftModelProposer
vllm/v1/spec_decode/medusa.pyMedusaMedusaProposer
vllm/v1/worker/gpu/spec_decode/rejection_sample.py拒绝采样rejection_sample()

模型实现(Models)

文件路径说明关键类/函数
vllm/model_executor/models/llama.pyLLaMA 模型LlamaForCausalLM
vllm/model_executor/models/qwen2.pyQwen2 模型Qwen2ForCausalLM
vllm/model_executor/models/mixtral.pyMixtral MoEMixtralForCausalLM
vllm/model_executor/models/deepseek_v2.pyDeepSeek V2DeepseekV2ForCausalLM
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.pyFP8 量化Fp8Config
vllm/model_executor/layers/quantization/awq.pyAWQ 量化AWQConfig
vllm/model_executor/layers/quantization/gptq.pyGPTQ 量化GPTQConfig

分布式通信(Distributed)

文件路径说明关键类/函数
vllm/distributed/parallel_state.py并行状态管理GroupCoordinator
vllm/distributed/communication_op.py通信操作tensor_model_parallel_all_reduce()
vllm/distributed/device_communicators/pynccl.pyNCCL 通信PyNcclCommunicator
vllm/distributed/device_communicators/custom_all_reduce.py自定义 AllReduceCustomAllReduce

配置(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.cuPagedAttention V1 内核
csrc/attention/paged_attention_v2.cuPagedAttention 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:forwardToken 采样

日志配置

# 详细日志
export VLLM_LOGGING_LEVEL=DEBUG

# 函数追踪
export VLLM_TRACE_FUNCTION=1

# 调度器日志
export VLLM_LOG_SCHEDULER=1

导航

6.3 - 参考资料

参考资料(References)

本文档汇总了学习 vLLM 和 LLM 推理优化所需的关键参考资料。


官方资源

vLLM 官方


核心论文

PagedAttention

  • Efficient Memory Management for Large Language Model Serving with PagedAttention

Transformer 架构

Flash Attention

  • FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness

  • FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning

投机解码

  • 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

分布式并行

  • Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism

  • GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism


深度学习基础

书籍

  • Deep Learning (花书)

  • Dive into Deep Learning (动手学深度学习)

    • 作者: Aston Zhang, Zachary C. Lipton, et al.
    • 链接: https://d2l.ai/
    • 要点: 实践导向的深度学习教程

在线课程


GPU 和 CUDA

NVIDIA 官方

性能优化


相关项目

推理引擎

量化工具

模型库


技术博客

LLM 推理

vLLM 相关


社区资源

讨论论坛

GitHub Issues


学习路径建议

入门阶段

  1. 阅读《动手学深度学习》Transformer 章节
  2. 阅读 “The Illustrated Transformer”
  3. 了解 vLLM 基本使用

进阶阶段

  1. 阅读 PagedAttention 论文
  2. 阅读 Flash Attention 论文
  3. 学习 vLLM 源码中的核心模块

深入阶段

  1. 阅读量化相关论文(AWQ、GPTQ)
  2. 阅读投机解码论文(Speculative Decoding、EAGLE)
  3. 了解分布式并行(Megatron-LM)

导航