s00

KV Cache 全景

核心概念

从原理到实践,以 vLLM 为主线 | 13 章

KV Cache 是 LLM 推理中最核心的优化技术——没有它,自回归生成每秒只能生成 1 个 token。

理解 KV Cache = 理解 LLM 推理性能问题的关键。vLLM 的 PagedAttention 是这个领域近两年最重要的创新之一。

KV Cache 是什么

KV Cache(Key-Value Cache)是 LLM 推理过程中对历史 token 的 Key 和 Value 矩阵的缓存

在自回归生成中,每生成一个新 token,模型需要关注所有之前的 token。如果不缓存,每次都要重新计算所有历史 token 的 K 和 V,计算量会从 O(n) 膨胀到 O(n²)

vLLM 为什么重要

vLLM 由 UC Berkeley 开发,它提出的 PagedAttention 机制借鉴了操作系统虚拟内存的分页思想, 将 KV Cache 从连续内存中解放出来,解决了三个核心问题:

  • 内存碎片 — 传统实现为最大序列长度预分配,浪费大量内存
  • 利用率低 — 实际使用的 KV Cache 远小于预分配量
  • 无法共享 — 相同前缀的请求无法复用 KV Cache

你能从这个教程学到什么

章节主题核心知识点
s01–s03核心概念自注意力机制 → KV Cache 定义 → 传统实现的痛点
s04–s07vLLM 深度整体架构 → PagedAttention → Block Manager → 调度策略
s08–s10优化技术Prefix Caching → KV Cache 量化 → 分布式场景
s11–s13实践与对比配置调优 → 系统对比 → 监控工具

先修知识

  • 了解 Transformer 架构的基本概念(Q/K/V/Attention)
  • 了解自回归生成的流程(逐 token 生成)
  • 了解 GPU 显存的基本概念

一句话记住

KV Cache 是"用空间换时间"——用显存存历史计算的 K/V,避免每次生成都重新算一遍所有历史 token。

学习路径建议

  1. 先过 s01-s03 — 理解"为什么需要 KV Cache"比"怎么实现"更重要
  2. 重点读 s05(PagedAttention) — 这是 vLLM 最核心的贡献
  3. 再读 s06(Block Manager) — 理解内存如何被实际管理
  4. 最后 s08(Prefix Caching) — 最实用的优化技术

推荐阅读

s01

自注意力回顾

核心概念

理解 KV Cache 之前,先理解 Transformer 的自注意力计算|Attention Is All You Need

自注意力是 Transformer 的核心,KV Cache 是加速自注意力推理的关键。

自注意力计算公式

Transformer 的自注意力机制的核心公式:

Attention(Q, K, V) = softmax(Q × K^T / √d_k) × V

其中:
Q = 输入 × W_Q  (Query 矩阵)
K = 输入 × W_K  (Key 矩阵)
V = 输入 × W_V  (Value 矩阵)
d_k = Key 的维度

自回归生成过程

LLM 生成文本时是逐 token 生成的(自回归):

Step 1: [I] → 生成 "love"
Step 2: [I, love] → 生成 "you"
Step 3: [I, love, you] → 生成 "."

每一步,模型需要计算当前所有 token 之间的注意力。

关键观察

在 Step 3 中,模型需要计算 3 个 token 的注意力:

对 token "." 的注意力计算:
  q_4 = "." 的 Query
  K = [k_1, k_2, k_3]  ← 所有历史 token 的 Key
  V = [v_1, v_2, v_3]  ← 所有历史 token 的 Value

问题:k_1, k_2, v_1, v_2 在 step 2 已经算过了!
每次重新计算 → O(n²) 的计算量

朴素实现的问题

如果不使用 KV Cache,生成第 n 个 token 时需要:

  • 重新计算前 n-1 个 token 的 K 和 V
  • 计算量随序列长度平方增长:O(n²)
  • 对于一个 2048 token 的序列,约 50% 的计算是重复的
  • 对于一个 8192 token 的序列,约 87% 的计算是重复的

推理的阶段性差异

┌─────────────────────────────────────────────┐
│              LLM 推理两阶段                    │
├──────────────┬──────────────────────────────┤
│  Prefill 阶段 │        Decode 阶段            │
│ (并行计算)     │       (逐 token 生成)          │
│              │                              │
│ 输入完整 prompt│  每步只生成 1 个新 token       │
│ 计算所有 K/V  │  需要关注所有历史 token        │
│ O(n²) 但可并行│  O(n) 但不可并行(串行)        │
│ 计算密集      │  访存密集(Memory-bound)       │
└──────────────┴──────────────────────────────┘
s02

什么是 KV Cache

核心概念

用显存换时间——缓存历史计算,避免重复|空间换时间

KV Cache = 把之前算好的 Key 和 Value 矩阵存下来,下次直接用。

KV Cache 的定义

KV Cache 是在 Decode 阶段,对每一层 Transformer 中已经计算过的 Key 和 Value 矩阵的缓存

传统方式(无 KV Cache):
  Step 1: 计算 token 1 的 K₁, V₁ → 输出 token 2
  Step 2: 计算 token 1,2 的 K₁,V₁,K₂,V₂ → 输出 token 3  ← 重复计算 K₁,V₁
  Step 3: 计算 token 1,2,3 的 K₁,V₁,K₂,V₂,K₃,V₃ → 输出 token 4  ← 再次重复!

使用 KV Cache:
  Step 1: 计算 K₁, V₁ → 缓存到 KV Cache → 输出 token 2
  Step 2: 只计算 K₂, V₂ → 追加到 KV Cache → 输出 token 3
  Step 3: 只计算 K₃, V₃ → 追加到 KV Cache → 输出 token 4

KV Cache 有多大

KV Cache 的显存占用公式:

每 token 的 KV Cache = 2 × num_layers × num_heads × head_dim × 2 bytes
                                                    ↑ FP16 每个元素 2 字节

以 LLaMA-70B 为例:
  num_layers = 80
  num_heads = 64
  head_dim = 128
  每 token ≈ 2 × 80 × 64 × 128 × 2 = 2.6 MB

  batch_size=16, seq_len=4096:
  KV Cache ≈ 2.6 MB × 16 × 4096 ≈ 170 GB!
  → 远超单张 GPU 显存(A100 80GB)

KV Cache 的重要性

指标有 KV Cache无 KV Cache
Decode 每步计算量O(1) — 只计算新 tokenO(n) — 重新计算所有历史
推理延迟随序列长度缓慢增长随序列长度线性增长
瓶颈访存带宽(Memory-bound)计算量(Compute-bound)

Decode 阶段的瓶颈转换

使用 KV Cache 后,Decode 阶段的主要瓶颈从计算变成了访存

Decode 每步的计算量很小(只算 1 个 token 的 QKV)
但需要把整个 KV Cache 从 HBM 搬到 SRAM
→ 访存带宽成为瓶颈

这就是为什么:
- FlashAttention 通过分块减少 HBM 访问
- vLLM 的 PagedAttention 通过分页管理减少内存碎片
s03

传统实现与问题

核心概念

HuggingFace Transformers 等传统实现为什么效率低|预分配 vs 按需分配

传统实现的 KV Cache 就像一个为每本书预留一整面书架——即使只读几页,也要占一整面墙。

传统实现方式

大多数框架(HuggingFace Transformers、DeepSpeed 等)的 KV Cache 实现:

class KVCache:
    def __init__(self, max_batch, max_seq_len, ...):
        # 预分配最大可能的内存
        self.key_cache = torch.zeros(
            max_batch, num_layers, num_heads,
            max_seq_len, head_dim,  # ← 预分配到最大长度
            dtype=torch.float16, device='cuda'
        )
        self.value_cache = torch.zeros(...)  # 同理

    def append(self, token_idx, key, value):
        # 把新的 K/V 写入预分配的位置
        self.key_cache[:, :, :, token_idx, :] = key
        self.value_cache[:, :, :, token_idx, :] = value

三大问题

1. 预分配浪费

# 假设 max_seq_len=8192,实际只用了 512
# 浪费率 = (8192 - 512) / 8192 = 93.75%!

问题在于:你不知道请求会生成多长
→ 只能按最大值预分配
→ 大多数请求比最大值短得多
→ 巨大浪费

2. 内存碎片

不同的请求长度不同,释放后留下碎片:

[请求 A: 2048 tokens] [请求 B: 128 tokens] [请求 C: 4096 tokens]
                                                            ↓ 请求 C 结束后
[请求 A: 2048 tokens] [空闲: 128] [空闲: 4096]
                       ↑ 128 的碎片,无法被任何新请求复用

3. 无法共享

两个请求有相同的前缀 "What is the capital of France?":

请求 A: "What is the capital of France? Answer in English."
请求 B: "What is the capital of France? Answer in French."

传统实现:每个请求独立计算和存储 K/V
          前 7 个 token 的 K/V 被计算了 2 次
          如果 1000 个请求共享前缀 → 重复计算 1000 次!

问题汇总

问题后果严重程度
预分配浪费大量显存被空占,batch size 受限非常高
内存碎片即使总空闲空间足够,也无法分配给新请求
无法共享相同前缀重复计算 K/V,浪费算力和显存中高

这些问题的根源

所有问题的根源在于:KV Cache 被当作连续内存块管理

就像操作系统早期用连续分配管理物理内存一样——会产生严重的碎片和浪费。 vLLM 的核心创新就是:用虚拟内存的分页思想来管理 KV Cache

s04

vLLM 架构概览

vLLM 深度

从整体看 vLLM 如何解决 KV Cache 管理问题|vllm-project/vllm

vLLM 的架构可以概括为:PagedAttention + Block Manager + Scheduler,三者协同工作。

vLLM 整体架构

┌─────────────────────────────────────────────────────┐
│                    LLM API (OpenAI 兼容)              │
├─────────────────────────────────────────────────────┤
│                   Scheduler (调度器)                  │
│    请求排队 → 内存检查 → 批处理 → 执行 → 输出        │
├─────────────────────────────────────────────────────┤
│                Block Manager (内存管理器)             │
│    分页分配 → 写时复制 → 碎片回收 → 共享管理          │
├─────────────────┬───────────────────────────────────┤
│  PagedAttention │     Attention 计算后端             │
│  (分页注意力)    │  (FlashAttention / FlashInfer)    │
├─────────────────┴───────────────────────────────────┤
│                Worker (GPU 执行器)                    │
│               模型加载 → 前向传播 → 输出              │
└─────────────────────────────────────────────────────┘

核心组件

Scheduler(调度器)

决定哪些请求可以进入当前批次,核心逻辑:

1. 检查剩余显存能否容纳新请求的 KV Cache
2. 如果可以 → 加入当前批次
3. 如果不行 → 请求排队等待
4. 已完成的请求释放 KV Cache 块

Block Manager(内存管理器)

管理 KV Cache 的物理块分配,这是 PagedAttention 的地基。

PagedAttention(分页注意力)

修改了注意力计算方式,使得 K/V 可以从非连续的物理块中读取。

工作流程

1. 客户端发送请求(prompt + 参数)
2. Scheduler 检查内存,分配 logical blocks
3. Block Manager 映射 logical → physical blocks
4. Prefill 阶段:计算 prompt 所有 token 的 K/V
5. Decode 阶段循环:
   a. 计算新 token 的 Q,读取对应物理块的 K/V
   b. 执行 PagedAttention
   c. 生成下一个 token
   d. 分配新的 block(如果需要)
6. 请求完成 → 释放 blocks

与传统系统的对比

维度vLLM传统实现 (HF)
内存分配按需分配,非连续预分配,连续
内存碎片几乎无碎片(分页管理)严重碎片
前缀共享支持(写时复制)不支持
批处理效率高(内存利用率高)低(预分配浪费)
s05

PagedAttention 原理

vLLM 深度

vLLM 最核心的贡献——用虚拟内存的思路管理 KV Cache|SOSP 2023

PagedAttention 借鉴了操作系统虚拟内存的分页思想——让 KV Cache 可以存在不连续的物理块中。

核心思想

把 KV Cache 分成固定大小的 Block(页),用 Block Table(页表) 映射逻辑位置到物理位置。

传统方式:连续内存
┌──────────────────────────────────────────────┐
│ K₁V₁│K₂V₂│K₃V₃│K₄V₄│K₅V₅│K₆V₆│K₇V₇│K₈V₈│...  │  ← 连续分配
└──────────────────────────────────────────────┘

PagedAttention:分页内存
Logical Blocks:           Physical Blocks:
┌─────────┐              ┌─────────┐
│ Block 0 │────┐         │ Block 2 │  (别的请求的)
├─────────┤    │         ├─────────┤
│ Block 1 │    ├────────→│ Block 5 │
├─────────┤    │         ├─────────┤
│ Block 2 │    │         │ Block 0 │←──── 其他请求也共享
├─────────┤    └────────→│ Block 7 │
│ Block 3 │              └─────────┘
└─────────┘

Block Table

每个请求的 Block Table:

Request "Hello, world! How are you?"
  logical_block_0 → physical_block_7  [Hello, wo]
  logical_block_1 → physical_block_5  [rld! How]
  logical_block_2 → physical_block_3  [ are you]

请求只关心 logical blocks,Block Manager 处理物理映射。

写时复制(Copy-on-Write)

当多个请求共享相同前缀时,vLLM 使用写时复制:

请求 A: "What is the capital of France? Answer in English."
请求 B: "What is the capital of France? Answer in French."
        ↑ 前 7 个 token 共享相同的物理块

初始状态:A 和 B 的 Block Table 指向同一物理块
当请求 A 需要写入新 token 时:
  → 先复制物理块
  → 在副本上写入
  → A 的 Block Table 指向新副本
  → B 仍然指向原块

传统 vs PagedAttention

特性传统 AttentionPagedAttention
K/V 存储连续内存非连续的分页内存
内存浪费~60-80%(预分配)<4%(页内碎片)
前缀共享不支持写时复制
计算方式torch.matmul 直接计算分块读取,非连续计算
最大 batch受预分配限制只受物理显存限制

PagedAttention 计算细节

传统 Attention 计算:
  attn = softmax(Q @ K^T / sqrt(d)) @ V
  # K 和 V 是连续的 [seq_len, head_dim] 矩阵

PagedAttention 计算:
  # K 和 V 分布在多个非连续的物理块中
  for each physical_block in block_table:
      block_k = load_block(physical_block)  # [block_size, head_dim]
      block_v = load_block(physical_block)
      attn += softmax(Q @ block_k^T / sqrt(d)) @ block_v

  # 使用特殊 kernel(flash_attn_varlens_func 或 flashinfer)
  # 直接接受 block_table + 物理地址作为输入

Block 大小的影响

block_size 是关键超参数:

- block_size=16: 更细粒度,碎片更少,但页表更大
- block_size=64: 计算效率更高,但页内碎片更多

vLLM 默认值:block_size=16(对大多数场景最优)

vLLM 源码关键位置

# vllm/attention/backends/flash_attn.py
class FlashAttentionBackend:
    def forward(
        self,
        query,           # [num_tokens, num_heads, head_size]
        key_cache,       # 物理块中的 K
        value_cache,     # 物理块中的 V
        block_table,     # logical → physical 映射表
        ...

# vllm/worker/model_runner.py
# 调用 attention 后端的地方
attn_output = self.attn_backend.forward(
    query=query,
    key_cache=self.cache_engine.key_cache[layer_idx],
    value_cache=self.cache_engine.value_cache[layer_idx],
    block_table=block_tables,
)
s06

Block Manager

vLLM 深度

KV Cache 的「内存分配器」——管理物理块的分配和回收|vllm/block_manager.py

Block Manager 是 vLLM 的内存分配器——决定了谁能用、用多少、用完后怎么回收。

Block Manager 的职责

1. 初始化:计算可用物理块数量
   total_blocks = available_gpu_memory / (block_size * KV_cache_per_token)

2. 分配:为新请求(或增长中的请求)分配物理块
   alloc(seq_group) → [physical_block_ids]

3. 写时复制:共享前缀时管理 COW
   cow(seq_group, block_id) → new_physical_block

4. 回收:请求完成后释放物理块
   free(seq_group) → [freed_physical_block_ids]

5. 交换:当显存不足时,把块交换到 CPU 内存
   swap_out(blocks, cpu_blocks)
   swap_in(blocks, gpu_blocks)

物理块初始化

# vllm/block_manager.py (简化)
class BlockManager:
    def __init__(self, cache_config, device_config):
        # 计算 KV Cache 每个 token 的字节数
        cache_dtype = cache_config.cache_dtype  # FP16/BF16/FP8
        num_layers = cache_config.num_layers
        num_heads = cache_config.num_kv_heads
        head_dim = cache_config.head_dim

        self.block_size = cache_config.block_size  # 默认 16
        # 每个物理块的字节数
        bytes_per_block = (
            2  # K 和 V
            * num_layers
            * num_heads
            * self.block_size
            * head_dim
            * torch.finfo(cache_dtype).bits // 8
        )
        # 可用块数
        gpu_memory = get_gpu_memory()
        kv_cache_memory = gpu_memory * cache_config.gpu_memory_utilization
        self.num_gpu_blocks = kv_cache_memory // bytes_per_block
        self.num_cpu_blocks = ...  # CPU 交换空间

分配策略

# Prefill 阶段:为 prompt 中的每个块预分配物理块
def allocate_prefill_blocks(seq_group):
    num_prompt_blocks = ceil(seq_group.prompt_len / self.block_size)
    # 先检查是否有空闲块
    if self.free_blocks.count() >= num_prompt_blocks:
        return self.free_blocks.pop(num_prompt_blocks)
    else:
        # 尝试交换
        # 如果还不行 → OutOfMemory

# Decode 阶段:每次生成 block_size 个 token 后分配一个新块
def allocate_token_block(seq_group):
    if seq_group.needs_new_block():
        # 分配一个物理块
        block = self.free_blocks.pop()
        seq_group.block_table.append(block)
        return block
    return None

Watermark 机制

为了应对突发内存需求,vLLM 使用 watermark 预留:

总物理块:1000
Watermark: 50(5%)

当空闲块 ≤ 50 时:
  → 停止接受新请求
  → 先完成正在运行的请求
  → 回收内存后再接受新请求

避免:接受了新请求后发现内存不足

GPU 内存分布

GPU 显存布局(以 A100 80GB / LLaMA-70B 为例):
┌────────────────────────────────────────┐
│ 模型权重: ~140GB(需要多 GPU 分片)      │
├────────────────────────────────────────┤
│ KV Cache: 可用显存 × gpu_memory_util    │
│   (默认 0.90, 即 72GB × 0.9 ≈ 65GB)   │
├────────────────────────────────────────┤
│ 激活值 + 临时 buffer                    │
├────────────────────────────────────────┤
│ 预留 (watermark + 余量)                 │
└────────────────────────────────────────┘
s07

调度与批处理

vLLM 深度

Scheduler 如何决定哪些请求一起跑|vllm/scheduler.py

有了分页管理 KV Cache,就可以更大胆地组 batch——只要物理块够,什么请求都能塞一起。

Scheduler 的核心决策

每个 step,Scheduler 需要决定:

1. 哪些等待队列中的请求可以加入当前批次?
   → 检查可用块数是否足够容纳新请求的 KV Cache

2. 哪些运行中的请求应该暂停(交换出去)?
   → 当显存不足时,选择优先级最低的请求

3. 哪些请求已经完成?
   → 回收其 KV Cache 块

调度策略

默认策略:先来先服务 + 贪心批处理

def schedule(self):
    running = []  # 当前正在运行的请求
    waiting = []  # 等待的请求

    # 1. 尝试把等待中的请求加入批处理
    for seq in waiting:
        needed_blocks = compute_needed_blocks(seq)
        if free_blocks >= needed_blocks:
            running.append(seq)
            allocate_blocks(seq, needed_blocks)

    # 2. 运行一步 Decode
    outputs = self.model.execute(running)

    # 3. 处理完成和新生成的块
    for seq in running:
        if seq.finished:
            free_blocks_for(seq)
        elif seq.needs_new_block():
            if free_blocks > 0:
                allocate_one_block(seq)
            else:
                # 内存不足 → 暂停
                pause(seq)

    return outputs

vLLM 的批处理优势

因为 PagedAttention 让 KV Cache 不再连续,vLLM 可以:

传统系统:
  batch_size 受限于"预分配 x max_seq_len"
  如果一个请求需要 4K 长度 → 所有请求都按 4K 预分配
  → 短请求浪费大量空间
  → batch_size 被迫减小

vLLM:
  batch_size 受限于"物理块总数"
  短请求只占几块,长请求占多块
  不会因一个长请求而浪费所有请求的空间
  → 同样的显存,可以处理更大的 batch

调度参数

max_num_batched_tokens  # 每批最大 token 数(默认 2048)
max_num_seqs            # 每批最大请求数(默认 256)
gpu_memory_utilization  # GPU 显存用于 KV Cache 的比例(默认 0.90)

# 当这两个条件任一满足时,停止加入新请求
# - 总 token 数 > max_num_batched_tokens
# - 请求数 > max_num_seqs

性能对比

场景传统批处理vLLM 批处理
短请求(128 token)batch=64,浪费 92% 显存batch=1024+
混合长度按最长请求预分配,浪费严重按需分配,无浪费
长文本(32K token)batch 被拉低到 1-2batch 可到 8-16
s08

Prefix Caching

优化技术

自动前缀缓存(APC)——让相同开头的请求共享 KV Cache|vllm --enable-prefix-caching

如果你的用户都在问同一个系统提示/prompt 前缀——APC 让第一个请求算完,后面的直接复用。

什么是 Prefix Caching

Automatic Prefix Caching(APC)是 vLLM 的一项关键优化: 自动检测请求中相同的前缀 token 序列,共享它们的 KV Cache 块

请求 A: "[System Prompt] Tell me a story about a cat."
请求 B: "[System Prompt] Tell me a story about a dog."

传统方式:
  A 和 B 各自独立计算 "[System Prompt] Tell me a story about a" 的 8 个 token
  → 重复计算,浪费!

APC 方式:
  A 先算 → 这些块的 hash 存入全局缓存
  B 来了 → 检测到相同前缀 → 直接共享 A 的物理块
  B 只需要计算 " dog" 的 2 个新 token

实现原理

# 核心:每个物理块通过其内容的 hash 来索引
class PrefixCache:
    def __init__(self):
        self.hash_to_block = {}  # hash → physical_block

    def get_or_compute(self, tokens):
        # 逐块尝试匹配
        blocks = []
        for block in chunk_to_blocks(tokens):
            block_hash = hash(block.tokens)
            if block_hash in self.hash_to_block:
                # 命中缓存,复用物理块
                blocks.append(self.hash_to_block[block_hash])
            else:
                # 未命中,创建新块
                new_block = alloc_block()
                compute_kv(new_block, block.tokens)
                self.hash_to_block[block_hash] = new_block
                blocks.append(new_block)
        return blocks

APC 的适用场景

最佳场景(TTFT 降低 50-90%):
  ✓ 所有请求共享长系统提示(如 ChatML 模板)
  ✓ 少量 prompt 模板 + 大量请求(如 LLM API 服务)
  ✓ 多轮对话(历史消息反复出现)

效果有限:
  ○ 每个请求 prompt 完全不同
  ○ prompt 很短(< block_size=16)

不适合:
  ✗ 前缀长度 < block_size(无法共享)
  ✗ 请求都是独立的长文本

启用方式

# 启动 vLLM 时开启
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Meta-Llama-3-8B \
    --enable-prefix-caching  # ← 开启 APC

# 开启后不需要任何配置,自动生效

性能影响

指标关闭 APC开启 APC
TTFT(首 token 延迟)100%(基准)30-60%(共享前缀越⻓,降低越多)
KV Cache 显存100%(基准)50-70%(共享前缀不需额外存储)
吞吐量100%(基准)120-180%(节省的显存可服务更多请求)
s09

量化 KV Cache

优化技术

FP8 / INT8 / INT4——用更少的字节存更多 KV Cache|vllm --kv-cache-dtype fp8

KV Cache 占用了绝大多数推理显存——把它从 FP16 压到 FP8,显存用量直接减半。

为什么需要量化 KV Cache

LLaMA-70B, batch=16, seq_len=4096, FP16:
  每 token KV Cache ≈ 2.6 MB
  总 KV Cache ≈ 2.6 MB × 16 × 4096 ≈ 170 GB
  → 需要至少 3 张 A100 80GB(仅 KV Cache 就 > 2 张卡!)

LLaMA-70B, batch=16, seq_len=4096, FP8:
  总 KV Cache ≈ 170 GB / 2 = 85 GB
  → 只需要 1 张 A100 80GB(省了一半显存!)
  → 可以把 batch 翻倍,或支持更⻓序列

vLLM 支持的 KV Cache 量化格式

# FP8(推荐,精度损失极小)
--kv-cache-dtype fp8
# 每个元素 1 字节,带宽需求减半
# 需要 H100/H200/B200 硬件支持或模拟

# FP8 E4M3(默认 FP8 格式)
# 精度范围:±448,对 KV Cache 足够

# INT8(兼容性更好)
# 需要 calibration,对 outlier 敏感

精度影响

FP16 → FP8 对 KV Cache 的影响:

优点:
  ✓ 显存减半
  ✓ 访存带宽需求减半 → Decode 速度提升
  ✓ 对生成质量影响通常 < 0.5%(perplexity 损失极小)

注意事项:
  ⚠ FP8 对 outlier(异常值)敏感
  ⚠ 某些 KV head 的特定位置方差极大
  ⚠ 建议配合 KV Cache scaling factor 使用

量化 + 分页 = 更强

PagedAttention 解决了空间利用率问题
FP8 量化减少了每块需要的物理空间

两者组合:
  同样的 80GB 显存
  无量化:~30000 个块(block_size=16)
  FP8 量化:~60000 个块
  → 2 倍容量 = 2 倍 batch size 或 2 倍序列长度

启用方法

python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Meta-Llama-3-70B \
    --kv-cache-dtype fp8 \
    --gpu-memory-utilization 0.95
s10

分布式 KV Cache

优化技术

多 GPU / 多节点场景下 KV Cache 的管理策略|TP + PP + DP

模型太大放不进一张卡?KV Cache 也跟着分散在多卡上。

张量并行(Tensor Parallel)

TP 下 KV Cache 也是分片的:

单卡场景:
  num_heads=32 → 每卡 32 个 head
  KV Cache 大小: 2 × 32 × ...

TP=4 场景(4 卡):
  num_heads=32 → 每卡 8 个 head
  KV Cache 大小: 2 × 8 × ...  ← 每卡减少 4x!

优点:
  ✓ 每卡 KV Cache 减少 = 可以处理更⻓序列/更大 batch
  ✓ 注意力计算也分片
缺点:
  ⚠ 需要 all-reduce 聚合结果
  ⚠ 通信开销随 head 数增长

流水线并行(Pipeline Parallel)

PP 下,不同 GPU 持有不同层的 KV Cache:

GPU 0: layers 0-19 → 只存 layers 0-19 的 KV Cache
GPU 1: layers 20-39 → 只存 layers 20-39 的 KV Cache

总 KV Cache 分布在各卡上
每卡只存自己负责的层

优点:大幅减少单卡 KV Cache 量
缺点:多了一层通信(点对点传输激活值)

vLLM 的分布式调度

# vllm 用 Ray 做分布式调度
from vllm import LLM, SamplingParams

# 自动检测 GPU 并分配 TP/PP
llm = LLM(
    model="meta-llama/Meta-Llama-3-70B",
    tensor_parallel_size=4,   # 4 卡 TP
    pipeline_parallel_size=1, # 1 路 PP
)

# 每个 worker 有自己的 Block Manager
# 但共享同一个调度策略

分布式场景的特殊问题

1. 全局内存协调
   - Scheduler 需要知道所有 GPU 的内存使用情况
   - 不能某张卡内存满了但其他卡还有空

2. 通信带宽
   - TP 中的 attention 需要 all-reduce
   - 跨节点通信(NVLink vs InfiniBand vs Ethernet)影响很大

3. Prefix Caching 跨节点
   - 相同的 prefix 在不同节点上能否共享?
   - vLLM 目前支持单节点内 prefix 共享
   - 跨节点共享仍在开发中
s11

配置调优指南

实践与对比

如何根据你的场景配置 vLLM 的 KV Cache 参数|max_model_len / gpu_memory_utilization

KV Cache 调优的核心是 Trade-off:更多 KV Cache = 更大 batch/序列 vs 留给模型权重的空间更少。

关键配置参数

# 1. GPU 显存分配
--gpu-memory-utilization 0.90  # KV Cache 占可用显存比例
                               # 0.90(default) → 90% 给 KV Cache
                               # 调高 → 更大容量,但留给模型更少
                               # 调低 → 更多余量,但容量减少

# 2. 最大序列长度
--max-model-len 4096  # 模型支持的最大序列长度
                      # 越大 → KV Cache 占用越多
                      # 重要:这个值设得太大会浪费显存
                      # 建议设为你实际需要的最大长度

# 3. Block 大小
--block-size 16  # 每个物理块存储的 token 数
                  # 16(default): 细粒度,碎片少
                  # 32/64: 计算效率高,但碎片更多

# 4. KV Cache 量化
--kv-cache-dtype auto    # auto(FP16) / fp8
                          # fp8 显存减半,精度损失极小

不同场景的推荐配置

场景 1: 短请求 + 高并发(如 Chat API)
  --max-model-len 4096
  --gpu-memory-utilization 0.95
  --max-num-seqs 256
  --enable-prefix-caching  # 大部分请求共享系统提示

场景 2: 长文档处理(如 RAG / 代码分析)
  --max-model-len 32768
  --gpu-memory-utilization 0.90
  --max-num-seqs 32
  --kv-cache-dtype fp8  # 长序列下 KV Cache 巨大,必须量化

场景 3: 严格低延迟(如实时翻译)
  --max-model-len 2048
  --gpu-memory-utilization 0.80  # 留更多余量给计算
  --max-num-batched-tokens 512   # 小 batch 保证低延迟

KV Cache 显存估算公式

KV_Cache_Memory = 2 × L × H_kv × D × S × B × dtype_size

其中:
  L = num_hidden_layers
  H_kv = num_kv_heads (GQA 中可能小于 num_attention_heads)
  D = head_dim
  S = max_seq_len
  B = max_batch_size
  dtype_size = 2 (FP16) / 1 (FP8)

示例(LLaMA-3-70B, FP16):
  L=80, H_kv=8, D=128, S=8192, B=16
  KV Cache = 2 × 80 × 8 × 128 × 8192 × 16 × 2
           = 4,294,967,296 bytes ≈ 4 GB  ← 看起来不大?
  不对!这是单层的!要乘 80 层:
           ≈ 320 GB  ← 需要多张 A100

常见问题

Q: 为什么我设了 max-model-len=4096,但显存用超了?
A: vLLM 预分配 KV Cache 块,即使当前请求很短,也预留了最大长度空间

Q: gpu-memory-utilization 调高后有 OOM?
A: 模型权重 + KV Cache + 临时 buffer > 显存
   尝试调低,或减少 max-model-len

Q: block-size 设多大最好?
A: 16 是最通用的选择。如果你的请求都非常⻓且统一,可以试试 32
s12

与其他系统对比

实践与对比

vLLM vs TensorRT-LLM vs SGLang vs TGI——KV Cache 管理策略的差异|各自的设计哲学

KV Cache 管理的好坏,直接决定了推理系统的吞吐和效率。

主流推理系统对比

特性vLLMTensorRT-LLMSGLangTGI
KV Cache 分配分页(PagedAttention)预分配连续块分页 + RadixAttention预分配连续
内存碎片极低(分页)中等(块内碎片)极低(分页)
前缀共享APC(自动,按块 hash)需手动指定RadixAttention(基于树)不支持
写时复制
量化支持FP8/INT8FP8/INT4/INT8FP8FP8/INT8
调度策略贪心批处理静态批处理贪心 + 预见简单调度

vLLM vs TensorRT-LLM

TensorRT-LLM(NVIDIA 官方)的优势:
  - 更激进的 kernel 优化(使用 NVIDIA 内部 API)
  - 更丰富的量化支持(INT4、FP4)
  - 与 NVIDIA 硬件深度绑定

vLLM 的优势:
  - PagedAttention 的内存效率更高
  - 社区更活跃,更新更快
  - 更容易定制和扩展
  - 开源许可证更友好

vLLM vs SGLang

SGLang 的 RadixAttention:
  - 不是按固定 block 共享前缀
  - 而是构建一个前缀树(Radix Tree)
  - 可以共享任意长度的公共子串
  - 理论上比 APC 更灵活

实际对比:
  - 简单共享前缀场景:两者持平
  - 复杂重复模式(多轮对话频繁切换):SGLang 略优
  - 社区和生态:vLLM 更成熟

选择建议

选 vLLM 如果你:
  - 需要高吞吐的 LLM API 服务
  - 请求共享大量相同前缀
  - 需要活跃的社区和丰富的文档
  - 想快速部署和迭代

选 TensorRT-LLM 如果你:
  - 对延迟有极致要求
  - 使用 NVIDIA 最新硬件(H100/B200)
  - 需要 INT4 等低精度量化

选 SGLang 如果你:
  - prompt 模式复杂多变
  - 多轮对话中有大量上下文切换
s13

监控与调试

实践与对比

如何查看 vLLM 的 KV Cache 使用情况|vllm stats / nvidia-smi

"我的显存去哪了?"——监控 KV Cache 是定位推理性能问题的第一步。

vLLM 统计信息

vLLM 启动时会输出 KV Cache 的配置信息:

#INFO 04-28 10:00:00 cache_engine.py:42] GPU KV cache size: 65.54 GB
#INFO 04-28 10:00:00 cache_engine.py:47] Number of GPU blocks: 51400
#INFO 04-28 10:00:00 cache_engine.py:48] Number of CPU blocks: 10280

运行时通过 API 获取统计:
GET /metrics
→ vllm:num_requests_running    # 当前运行请求数
→ vllm:num_requests_waiting    # 等待请求数
→ vllm:gpu_cache_usage_percent # GPU KV Cache 使用率
→ vllm:cpu_cache_usage_percent # CPU 交换空间使用率
→ vllm:num_preemptions_total   # 被抢占的请求数(越高说明内存越紧张)

关键指标解读

gpu_cache_usage_percent:
  0-70%   健康,还有空间接受更多请求
  70-90%  较高,建议监控
  90-95%  紧张,可能很快触发 preemption
  95%+    高危,大量请求在等待或交换

num_preemptions_total:
  0       完美,不需要抢占
  <10     可接受
  >10     频繁抢占 → 需要调整配置
  >100    严重问题!降低 max_num_seqs 或增加 GPU

诊断内存问题

问题 1: 显存不足,但 gpu_cache_usage 很低
  → 可能是模型权重太大
  → 检查 max-model-len 是否设得太大
  → 检查是否开启了不必要的量化

问题 2: gpu_cache_usage 高但吞吐低
  → KV Cache 不够用,请求被频繁交换
  → 增加 GPU 显存或减少 max_num_seqs
  → 启用 KV Cache 量化

问题 3: 频繁 preemption
  → max_num_seqs 设得太高
  → 请求长度波动太大
  → 降低 max_num_seqs 或启用 APC

问题 4: 长序列 OOM
  → max-model-len 超出 GPU 能力
  → 启用 FP8 量化
  → 使用 TP 将模型分布到多卡

调试工具

# 查看 GPU 显存使用
nvidia-smi

# 查看每个进程的显存
nvidia-smi --query-compute-apps=pid,used_memory --format=csv

# vLLM 内部状态(通过 OpenAI API)
curl http://localhost:8000/v1/metrics | grep vllm

# 查看 Block Manager 状态
# 启动时添加 --log-requests
python -m vllm.entrypoints.openai.api_server \
    --model ... \
    --log-requests  # 打印每个请求的 KV Cache 使用情况

调优检查清单

□ KV Cache 是否占用了 90% 以上的可用显存?
  (检查 gpu_cache_usage_percent)
□ 是否有大量 preemption?
  (检查 num_preemptions_total)
□ 是否开启了 Prefix Caching?
  (如果请求共享前缀)
□ 是否使用了 FP8 KV Cache?
  (减少显存占用最简单的方法)
□ max-model-len 是否合理?
  (不要设得比实际需要大太多)
□ gpu-memory-utilization 是否合适?
  (太大容易 OOM,太小浪费空间)