vLLM KV Cache 实现机制

一、核心机制:PagedAttention 是如何实现的

1.1 背景:传统 KV Cache 的三大问题

在自回归解码过程中,每个输入 token 都会生成 Key(K)和 Value(V)张量,这些张量被缓存起来以避免重复计算,称为 KV Cache

传统框架(如 HuggingFace Transformers)的 KV Cache 存在三个核心问题:

问题 描述 影响
内存占用过大 单序列 LLaMA-13B 的 KV Cache 约 1.7GB 并发能力极低
内存碎片化 连续物理存储导致大量"内存空洞" GPU 利用率 < 40%
缺乏共享机制 不同请求间相同前缀的 KV Cache 无法复用 计算资源浪费

1.2 PagedAttention 核心原理

vLLM 的 PagedAttention 借鉴了操作系统虚拟内存的分页思想,将 KV Cache 分割为固定大小的块(通常为 16KB)进行管理:

逻辑视角(连续):  [Block0] [Block1] [Block2] [Block3] ... [BlockN]
                      ↓        ↓        ↓        ↓
物理视角(分散):  GPU Block #3 | GPU Block #0 | GPU Block #7 | CPU RAM Block #2

核心创新点:

  1. 逻辑块与物理块解耦:逻辑上连续的空间可以映射到物理上非连续的存储区域
  2. 按需分配:只保留当前计算所需的 KV 块在 GPU 显存中
  3. 高效换入换出:当 GPU 显存不足时,可以以块为单位将 KV Cache 迁移到 CPU 内存
  4. 消除外部碎片:块大小固定,物理内存不会产生小碎片
  5. 减少内部碎片:最后一个块不满时,浪费空间最多为一个块大小(约 16KB)

1.3 PagedAttention 计算流程

有空闲

无空闲

新请求到来

Scheduler 分配逻辑块

检查可用物理块

映射逻辑块→物理块

启用Offloading?

将低优先级块Offload到CPU

LRU驱逐低优先级请求KV Cache

执行 PagedAttention 计算

新 Token 生成

K/V 写入对应物理块

序列继续?

释放逻辑块映射


二、存储架构:缓存存在哪里

2.1 存储层级

vLLM 的 KV Cache 存储分为三层:

┌──────────────────────────────────────────────────────┐
│  Layer 1: GPU VRAM(主要存储)                       │
│  ├── 存储所有活跃序列的 KV Cache                     │
│  ├── 块大小:16KB / 32KB(可配置)                   │
│  └── 容量受 gpu_memory_utilization 控制              │
├──────────────────────────────────────────────────────┤
│  Layer 2: CPU RAM(Offloading 可选)                 │
│  ├── 当 GPU 显存不足时,LRU 驱逐的块可 Offload 到此  │
│  ├── 需通过 --kv-offloading-size 参数启用            │
│  └── 恢复时需要重新拷贝回 GPU                        │
├──────────────────────────────────────────────────────┤
│  Layer 3: Disk(swap_space 可选,v0.x 旧机制)        │
│  ├── 通过 --swap-space 参数配置                      │
│  ├── 主要用于多级调度时的极端情况                    │
│  └── v1.x 推荐使用 CPU Offloading 替代               │
└──────────────────────────────────────────────────────┘

2.2 存储容量估算

单个序列的 KV Cache 估算公式:

KV Cache(字节) = 2 × num_layers × num_heads × seq_len × head_dim × dtype_bytes

LLaMA-13B(BF16)为例,典型配置:

  • num_layers = 40num_heads = 40head_dim = 128
  • 单 token 的 KV Cache:2 × 40 × 40 × 2 × 128 = 819,200 bytes ≈ 0.8 MB
  • 1024 token 上下文:0.8 MB × 1024 ≈ 800 MB/序列
  • 并发 16 序列:800 MB × 16 = 12.8 GB 仅 KV Cache

Qwen-72B 为例,单序列 KV Cache 可达 3-5 GB,并发能力严重受限于 GPU 显存。

2.3 关键存储参数

参数 含义 建议值
--gpu-memory-utilization vLLM 预分配 GPU 显存比例 0.85-0.92(根据模型大小调整)
--kv-cache-dtype KV Cache 数值精度 fp8(需 H100+)/ fp16 / half
--swap-space CPU 磁盘交换空间(GB) 50-200 GB
--kv-offloading-size v1.x CPU Offloading 容量 根据 CPU RAM 配置
--block-size 单个 KV Cache 块大小 16(默认)/ 32

三、BlockManager 内存管理体系

3.1 核心数据结构

# vLLM 内部块管理结构(简化示意)
class Block:
    def __init__(self, block_size=16):
        self.block_id: str        # 全局唯一块ID
        self.device: str          # "gpu" / "cpu" / "disk"
        self.content: Tensor      # 实际 KV 数据
        self.last_accessed: float # LRU 追踪时间戳
        self.num_tokens: int      # 块内有效 token 数
        self.ref_count: int       # 引用计数(支持多序列共享)

class BlockTable:
    # 逻辑块ID → 物理块ID 的映射表
    logical_block_ids: List[int]
    physical_block_ids: List[int]

3.2 块分配算法(LRU + RefCount)

vLLM 采用 LRU(最近最少使用) 策略进行块驱逐:

  1. 正常分配:请求到来时,从空闲块池中分配物理块,建立逻辑→物理映射
  2. 内存不足时的抢占顺序
    • 优先驱逐已完成请求的 KV Cache
    • 其次驱逐低优先级等待请求
    • 最后抢占正在运行但优先级较低的请求(触发重计算
  3. RefCount 引用计数:被多个序列共享的块不会单独被驱逐(如 Prefix Caching 场景)

3.3 两种处理模式的对比

机制 触发条件 处理方式 代价
重计算抢占(RECOMPUTE) GPU 显存不足以分配新块(默认) 直接释放 KV 块,请求从头重算 重复计算,但恢复快
KV Offloading(v1.x) 显式配置 --kv-offloading-size 将 KV 块迁移到 CPU RAM 迁移开销大,但避免重计算

四、缓存池与缓存命中率优化

4.1 是否需要"缓存池"?

vLLM 内置了缓存池机制,无需额外部署。但理解其机制有助于调优。

vLLM 缓存池的本质:

┌─────────────────────────────────────────────────────┐
│              GPU Memory Pool(预分配)               │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐       │
│  │ Block#0│ │ Block#1│ │ Block#2│ │ Block#3│ ...  │
│  └────────┘ └────────┘ └────────┘ └────────┘       │
│       ↑                                          ↑  │
│   已分配给                               空闲可用  │
│   活跃序列                                  块池    │
└─────────────────────────────────────────────────────┘

4.2 Prefix Caching(前缀缓存复用)

vLLM 支持零开销的前缀缓存,当多个请求共享相同的前缀 prompt 时,直接复用已计算的 KV Cache:

# 启用前缀缓存
llm = LLM(
    model="meta-llama/Llama-3-8B",
    enable_prefix_caching=True  # 关键参数
)

复用逻辑(Hash 匹配):

请求 0: "你好,请帮我写一封道歉信给[用户],内容是..." 
         → 计算完整 KV Cache,hash = H0

请求 1: "你好,请帮我写一封道歉信给[公司],内容是..."  
         → 前缀 "你好,请帮我写一封道歉信给" 完全相同
         → 复用前 16 个 token 的 KV Cache,仅计算新增部分

请求 2: "帮我翻译成英文:"Hello, how are you?""
         → 前缀完全不同,全部从头计算

Hash 匹配三要素:

  1. 父块哈希值(父块的 hash)
  2. 块内词元(精确 token 序列,防止哈希碰撞)
  3. 多模态附加哈希(多模态场景的额外 embedding hash)

⚠️ 已知限制:FP8 量化的 KV Cache 与 Prefix Caching 存在兼容性问题(见 GitHub Issue #3880

4.3 提升缓存命中率的实践策略

策略 操作方法 预期收益
启用 Prefix Caching --enable-prefix-caching 或代码中设置 前缀相同请求减少 50-90% 计算量
请求合并(Batching) 将前缀相同的请求合并提交 自动提升前缀复用概率
Prompt 模板化 固化系统 Prompt 格式,减少变动 提高长期缓存命中率
调整块大小 --block-size 16(默认小块) vs 32(大块) 小块提升细粒度复用,大块减少碎片
足够的 GPU 显存 提高 gpu_memory_utilization 至 0.90 减少 LRU 驱逐,保障缓存池深度

4.4 缓存池深度与命中率的关系

缓存池块数 = gpu_memory × gpu_memory_utilization / block_size

示例(24GB GPU,LLaMA-13B):
  → 23000 MB × 0.90 / 16 KB ≈ 1290 个块
  → 每序列约需 800 MB / 16 KB = 50 个块
  → 最大并发 ≈ 1290 / 50 ≈ 25 个序列

缓存池过小的表现:

  • 频繁的 LRU 驱逐,缓存反复重建
  • 高并发时吞吐量骤降
  • preemption 告警频繁出现

五、生产环境调优参数汇总

5.1 启动参数配置建议

python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-70B-instruct \
    --tensor-parallel-size 4 \
    --gpu-memory-utilization 0.90 \       # 高并发场景可调至 0.92-0.93
    --max-num-seqs 256 \                  # 最大并发序列数
    --block-size 16 \                     # 小块提升复用率
    --enable-prefix-caching \             # 启用前缀缓存
    --kv-cache-dtype fp8 \                # FP8 量化 KV Cache(H100+)
    # --kv-offloading-size 64             # v1.x 可选:启用 CPU Offloading
    # --swap-space 100                    # 旧版 swap 空间配置

5.2 调优流程建议

第一步:保守配置启动
  gpu_memory_utilization=0.70,观察日志中实际 KV Cache 使用率

第二步:逐步提升至稳定阈值
  每次 +0.05,调到不再出现频繁 preemption 为止

第三步:优化并发数
  max_num_seqs 从 32 开始,逐步提升直到 GPU 利用率饱和

第四步:启用高级特性
  enable_prefix_caching → fp8 量化 → kv_offloading(按需)

5.3 监控关键指标

指标 含义 健康范围
num_preemptions 被抢占重计算的请求数 越少越好(<5%)
gpu_memory_utilization 实际 GPU 显存使用率 接近配置值
prefix_cache_hit_rate 前缀缓存命中率 高并发时 >30% 优秀
cache_access_total 总缓存访问次数 越高越好
num_blocks 当前活跃块数 / 总块数 接近满负荷时注意驱逐

六、回答摘要

问题 答案
vLLM 缓存 KV 是如何实现的? 通过 PagedAttention,将 KV Cache 按 16KB 固定块管理,逻辑块映射到物理块,消除连续存储限制
需要准备存储吗? 不需要额外准备。vLLM 启动时自动预分配 gpu_memory_utilization 比例的 GPU 显存作为 KV Cache 池
缓存存储在哪里? 主要在 GPU VRAM;通过 --kv-offloading-size 配置后可 Offload 到 CPU RAM;极端情况可通过 --swap-space 使用磁盘
需要维护缓存池提升命中率吗? vLLM 已内置缓存池管理。提升命中率的关键是:启用 Prefix Caching + 合并相同前缀请求 + 配置足够的 GPU 显存

参考来源

  1. vLLM 官方文档:https://docs.vllm.ai/
  2. vLLM Prefix Caching 设计文档:https://github.com/vllm-project/vllm/blob/main/docs/design/prefix_caching.md
  3. Block Manager 深度解析:https://cloud.tencent.com/developer/article/2623508
  4. vLLM 核心模块 kv_cache.py 源码解析:https://www.cnblogs.com/security-hyacinth/p/19771675
  5. vLLM PagedAttention 原理:https://blog.csdn.net/v_july_v/article/details/144218958
  6. KV Cache 三大问题与解决方案:https://blog.csdn.net/weixin_43667338/article/details/145038093
  7. GPU Memory 管理与 OOM 解析:https://blog.csdn.net/hadoop5ranger/article/details/148728475
  8. vLLM 吞吐量优化 10 个调优方法:https://new.qq.com/rain/a/20251009A07RV400
  9. FP8 KV Cache 实践:https://blog.csdn.net/weixin_28939623/article/details/156141283
  10. KV Offloading 机制解析:https://blog.csdn.net/qq_38662930/article/details/158843637
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐