PagedAttention 在 vLLM 中的实现分析

1. 概述

1.1 背景:KV Cache 内存浪费是 LLM 服务的首要瓶颈

在 PagedAttention 出现之前,推理引擎(HuggingFace Transformers、FasterTransformer 等)为每个请求预分配一整块连续显存存放 KV cache,大小按 max_model_len × num_kv_heads × head_size × num_layers × 2 计算。

以一个 7B 模型为例(max_model_len=8192,FP16):单个请求的 KV cache 需要约 2 GB 显存。但大多数实际对话只生成 200-500 token——预留了 8192 个位置,却只用了不到 10%。一个请求浪费 90% 的 KV cache 空间,100 个并发请求就能让 200 GB 显存的 H100 集群利用率不到 30%。

更关键的是,传统引擎无法在请求间共享 KV cache。当 100 个用户共享同一个 2000 token 的 system prompt 时,传统引擎需要为每个请求独立计算并存储这 2000 个 token 的 KV——白白浪费了 199 次重复计算和存储。

1.2 PagedAttention 的解决方案

PagedAttention 是 vLLM 的核心创新(Kwon et al., SOSP 2023),其关键思想是将 KV cache 划分为固定大小的 block(页),类似操作系统的虚拟内存分页。这带来了三个核心优势:

  • 零内存碎片:KV cache 按 block 粒度(16/32 tokens)按需分配,用多少占多少
  • Prefix 共享:多个请求的公共前缀(如 system prompt)指向同一物理 block,引用计数管理,一次计算、全员共享
  • 灵活调度:物理内存非连续,block 可任意分配和回收,支持 preemption 和动态扩缩

1.3 PagedAttention 带来的价值

  传统引擎 vLLM + PagedAttention
KV cache 利用率 ~30%(预分配浪费) ~95%(按需分配)
Prefix 重复计算 每次请求独立计算 共享前缀物理 block,只算一次
最大并发请求数 受显存预分配限制 可多支撑 3-10× 并发
调度灵活性 预分配后不可动态调整 block 粒度的 preemption + 回收

在 LLM API 服务场景(数百并发、长 system prompt、序列长度差异大)中,PagedAttention 是实现商业化服务规模化的关键基础设施突破。


2. 整体架构

请求到达
  │
  ├─ KVCacheManager ────────── 分配物理 block,管理 prefix cache
  │     ├─ KVCacheCoordinator ─ 协调多种 attention type
  │     └─ SingleTypeKVCacheManager ─ 单一 attention type 的 block 管理
  │
  ├─ AttentionMetadataBuilder ─ 构建 block_table、slot_mapping 等元数据
  │
  └─ AttentionImpl.forward() ── 执行注意力计算
        └─ CUDA/Triton kernel ─ 通过 block_table 间接寻址 KV cache

核心代码路径:

模块 路径
Backend 注册与选择 registry.py, selector.py
后端抽象类 backend.py
FlashAttention 后端(主路径) flash_attn.py
Triton 后端 triton_attn.py
Triton decode kernel triton_decode_attention.py
Triton prefill kernel triton_prefill_attention.py
Triton unified kernel triton_unified_attention.py
CUDA kernel V1 paged_attention_v1.cu
CUDA kernel V2 paged_attention_v2.cu
CUDA kernel 核心算法 attention_kernels.cuh
Cache CUDA kernel cache_kernels.cu
PagedAttention ops paged_attn.py
Block 池 block_pool.py
KV Cache 工具 kv_cache_utils.py
KV Cache 管理器 kv_cache_manager.py
单一类型管理器 single_type_kv_cache_manager.py
协调器 kv_cache_coordinator.py

3. Block Table 机制 —— 分页的核心

3.1 概念

每个请求维护一个 block table(页表),形状为 [num_seqs, max_num_blocks_per_seq],将逻辑 block 序号映射到物理 block 序号。

KV cache 的寻址路径:

逻辑 token 位置
  │
  ├─ 计算: logical_block = token_idx // block_size
  │         offset = token_idx % block_size
  │
  ├─ 查表: physical_block = block_table[seq_idx][logical_block]
  │
  └─ 访问: kv_cache[physical_block * block_size_stride + offset]

3.2 Slot Mapping

每个 token 被映射为一个全局 slot:

slot = block_number * block_size + block_offset
  • 正数 slot:有效的 token 位置
  • 负数 slot:padding token(被忽略)

3.3 KV Cache 布局

v1 采用紧凑布局:

shape: [2, num_blocks, block_size, num_kv_heads, head_size]
        ↑
    key / value 分离

或者 HND 布局(head-major):
shape: [2, num_blocks, num_kv_heads, block_size, head_size]

对比 v0 的 legacy 布局(用于 paged_attention_v1/v2 kernel):

key_cache:   [num_blocks, num_kv_heads, head_size//x, block_size, x]
value_cache: [num_blocks, num_kv_heads, head_size, block_size]
              其中 x = 16 / sizeof(element),用于 16 字节对齐读取

3.4 Block Table 的本质:纯软件抽象,不是 GPU 硬件支持

3.4.1 Kernel 中的地址翻译是纯手工完成的

block_table 在 CUDA kernel 层面就是一个普通的 const int* 数组,存放在 GPU 全局内存中。地址翻译的全过程只有三步普通的 GPU 指令:

// attention_kernels.cuh:202 — 第一步:拿到当前序列的 block_table 指针
const int* block_table = block_tables + seq_idx * max_num_blocks_per_seq;

// attention_kernels.cuh:252-253 — 第二步:软件数组查表,拿到物理 block 号
const int64_t physical_block_number =
    static_cast<int64_t>(block_table[block_idx]);

// attention_kernels.cuh:268-270 — 第三步:手工乘加,算出物理地址
const cache_t* k_ptr =
    k_cache + physical_block_number * kv_block_stride +
    kv_head_idx * kv_head_stride + physical_block_offset * x;

不存在以下任何 GPU 硬件机制:

  • 没有 GPU MMU 参与 — GPU 的页表走查(page table walk)仅用于 CUDA 虚拟地址空间管理,不会参与 k_cache + offset 这种指针算术
  • 没有 TLB — 地址翻译不是通过 TLB 查询完成的,而是 ALU 的整数乘法
  • 没有纹理单元 — 整个 csrc/ 目录中不存在 tex1Dfetch、cudaTextureObject_t 等纹理硬件的使用
  • 没有 surface object — 同样不存在任何 surf2Dwrite 之类的用法

这在所有计算平台上是完全一致的 —— CUDA kernel(attention_kernels.cuh)、ROCm kernel(rocm/attention.cu)、CPU kernel(cpu/cpu_attn_impl.hpp)全都是同样的 block_table[block_idx] 软件查表模式。

3.4.2 类比:这和 OS 虚拟内存的类比仅停留在概念层面

PagedAttention 借用了 OS 虚拟内存的元语和命名,但两者的实现机制完全不同:

  OS 虚拟内存 PagedAttention
页表 硬件页表(多级树,MMU 走查) GPU 全局内存中的 int32 数组
地址翻译 硬件 TLB + 页表走查,每个 load/store 自动触发 kernel 软件显式查表 + 整数乘加
页大小 4KB / 2MB / 1GB(硬件定义) 16/32 tokens(应用层可配)
缺页 硬件 page fault → OS 处理 不存在缺页概念;block 未分配时 token 尚未生成
换页 OS 换出到磁盘 不支持 swap;KV cache 全量驻留 GPU 显存
隔离 硬件保护(user/kernel mode) 纯软件隔离(不同请求访问不同的 block_table 行)
TLB shootdown 硬件/OS 处理 不适用;无 TLB,无同步问题

关键的差异在于:OS 用虚拟内存是为了”欺骗”进程(让每个进程以为自己有独立的大地址空间);PagedAttention 用 block table 是为了消除 KV cache 的内部碎片和实现 prefix 共享。 两者的动机不同,实现层次也不同。

3.4.3 那 cuMemCreate/cuMemMap 是干什么的?

代码库中确实有 CUDA Virtual Memory API 的使用,位于 cumem_allocator.cpp,但它的角色和 block table 截然不同:

CUDA Virtual Memory API (cuMemCreate / cuMemMap / cuMemSetAccess)
└── cumem_allocator.cpp
    └── 作为 torch CUDAPluggableAllocator
        └── 管理整个 KV cache tensor 的底层物理内存分配
            (替代 cudaMalloc,提供更精细的显存控制)

PagedAttention block_table
└── attention_kernels.cuh
    └── 在已分配好的 KV cache tensor 内部
        └── 用 int32 数组做逻辑 block → 物理 block 的映射
            (纯软件,kernel 内手工地址计算)

这两个是完全解耦的:底层分配器用不用虚拟内存 API,block_table 机制都不受影响。PagedAttention 的”分页地址翻译”发生在 kernel 内部的指针算术中,而不是 GPU 的 MMU 层级上。


4. KV Cache 内存管理

4.1 数据结构

KVCacheBlock — kv_cache_utils.py:116

@dataclass(slots=True)
class KVCacheBlock:
    block_id: int           # 物理 block 编号 (0..num_gpu_blocks-1)
    ref_cnt: int = 0        # 引用计数(用于 prefix 共享)
    _block_hash: ...        # 链式哈希(用于 prefix caching 查找)
    prev_free_block: ...    # 双向链表(空闲队列)
    next_free_block: ...
    is_null: bool = False   # 空占位

FreeKVCacheBlockQueue — kv_cache_utils.py:164

所有空闲 block 组成双向链表,关键操作均为 O(1):

  • popleft() / popleft_n(n) — FIFO 分配(缓存使能时实现 LRU 淘汰)
  • append(block) / append_n(blocks) — 归还到队尾
  • remove(block) — 从中间移除(用于 prefix cache 命中时增加引用计数)

BlockHashToBlockMap

哈希表:(block_hash, group_id) → KVCacheBlock,用于 prefix caching 的快速查找。成员变量在 block_pool.py:171 初始化。

4.2 分配流程 — kv_cache_manager.py:110

新请求到达
  │
  ├─ 1. 计算 token hashes: hash((parent_hash, token_ids, extra_keys))
  │      └─ hash_block_tokens() 位于 [kv_cache_utils.py:541](https://github.com/vllm-project/vllm/blob/5bb8d2767a2829b56e58c68fa8f380e9e4e2bd3e/vllm/v1/core/kv_cache_utils.py#L541)
  │
  ├─ 2. find_longest_cache_hit() — [single_type_kv_cache_manager.py:373](https://github.com/vllm-project/vllm/blob/5bb8d2767a2829b56e58c68fa8f380e9e4e2bd3e/vllm/v1/core/single_type_kv_cache_manager.py#L373)
  │     ├─ 从左到右扫描 logical block
  │     ├─ 查找 BlockHashToBlockMap
  │     └─ 找到最长连续匹配 → 命中 N 个 block
  │
  ├─ 3. touch(命中 block) — [block_pool.py:402](https://github.com/vllm-project/vllm/blob/5bb8d2767a2829b56e58c68fa8f380e9e4e2bd3e/vllm/v1/core/block_pool.py#L402)
  │     └─ ref_cnt += 1(共享引用)
  │
  ├─ 4. allocate_new_blocks(num_new_blocks) — [single_type_kv_cache_manager.py:243](https://github.com/vllm-project/vllm/blob/5bb8d2767a2829b56e58c68fa8f380e9e4e2bd3e/vllm/v1/core/single_type_kv_cache_manager.py#L243)
  │     ├─ 从 FreeKVCacheBlockQueue popleft()
  │     └─ 记录新 block 的 slot_mapping
  │
  ├─ 5. cache_blocks(新 block) — [single_type_kv_cache_manager.py:278](https://github.com/vllm-project/vllm/blob/5bb8d2767a2829b56e58c68fa8f380e9e4e2bd3e/vllm/v1/core/single_type_kv_cache_manager.py#L278)
  │     └─ 将 block hash 写入 BlockHashToBlockMap
  │
  └─ 6. 返回 KVCacheBlocks(包含 block_table + 新分配信息)

请求结束时:

free(request) — [kv_cache_manager.py:429](https://github.com/vllm-project/vllm/blob/5bb8d2767a2829b56e58c68fa8f380e9e4e2bd3e/vllm/v1/core/kv_cache_manager.py#L429)
  ├─ ref_cnt -= 1(递减引用计数)
  ├─ 若 ref_cnt == 0 → 归还到 FreeKVCacheBlockQueue
  └─ 若 block 在缓存哈希表中 → 从哈希表移除

4.3 Prefix Caching 的链式哈希 — kv_cache_utils.py:541

def hash_block_tokens(hash_function, parent_block_hash, curr_block_token_ids, extra_keys):
    return hash((parent_hash, token_ids, extra_keys))

extra_keys 包含:LoRA 名称、多模态特征哈希、cache salt、prompt embed 哈希,保证不同配置下的缓存隔离。

4.4 Attention Spec 类型 — kv_cache_interface.py

Spec 行号 用途 特殊行为
FullAttentionSpec L188 标准全注意力 请求结束前持有所有 block
SlidingWindowSpec L435 滑动窗口注意力 窗口外的旧 block 自动释放
ChunkedLocalAttentionSpec L407 分块局部注意力 仅保留当前 chunk 的 block
SinkFullAttentionSpec L615 带 sink token 的注意力 sink block 固定不释放
MLAAttentionSpec L337 DeepSeek MLA 类似全注意力
CrossAttentionSpec L602 编码器-解码器交叉注意力 不支持 prefix caching
MambaSpec L563 Mamba SSM 状态缓存 特殊的 per-request state block
SlidingWindowMLASpec L498 MLA + 滑动窗口 DeepSeek V4 混合模式
HiddenStateCacheSpec L400 Hidden state 缓存 用于 extract_hidden_states

对应的管理器(single_type_kv_cache_manager.py):

Manager 行号 对应的 Spec
SingleTypeKVCacheManager (基类) L31 —
FullAttentionManager L481 FullAttentionSpec, MLAAttentionSpec, HiddenStateCacheSpec
SlidingWindowManager L542 SlidingWindowSpec, SlidingWindowMLASpec
ChunkedLocalAttentionManager L692 ChunkedLocalAttentionSpec
MambaManager L842 MambaSpec
CrossAttentionManager L1122 CrossAttentionSpec
SinkFullAttentionManager L1176 SinkFullAttentionSpec

4.5 多种 Attention Type 的协调 — kv_cache_coordinator.py

协调器 行号 说明
KVCacheCoordinatorNoPrefixCache L276 禁用 prefix caching,直接返回空命中
UnitaryKVCacheCoordinator L324 单一 attention type,直接委托
HybridKVCacheCoordinator L392 多种 attention type,固定点迭代协调

HybridKVCacheCoordinator 处理一个模型存在多种 attention type(如 sliding window + full attention 混合)的情况:

  1. 各 type 的 manager 独立计算 cache hit
  2. 若任何 type 缩短了 hit 长度,触发固定点迭代重新计算
  3. 最终 hit 长度必须是所有 type 的 block size 的 LCM 的倍数
  4. 使用 BlockHashListWithBlockSize 在不同 block size 粒度间转换哈希

4.6 Block 大小与内存计算

page_size_bytes = block_size * num_kv_heads * head_size * dtype_size * 2
# ×2 是因为 key + value 各一份
num_blocks = available_memory // page_size // num_layers

常见 block size:

  • 16:默认值,平衡灵活性和粒度
  • 32:更大的 block,减少 block table 开销
  • FlashAttention 要求 16 的倍数
  • Triton 某些配置支持 [16, 32, 64]

5. Backend 体系

5.1 Backend 注册 — registry.py:34

通过 AttentionBackendEnum 枚举注册:

class AttentionBackendEnum(enum.Enum):
    FLASH_ATTN = "vllm.v1.attention.backends.flash_attn.FlashAttentionBackend"
    TRITON_ATTN = "vllm.v1.attention.backends.triton_attn.TritonAttentionBackend"
    FLASHINFER = "vllm.v1.attention.backends.flashinfer.FlashInferBackend"
    FLEX_ATTENTION = "vllm.v1.attention.backends.flex_attention.FlexAttentionBackend"
    ROCM_ATTN = "vllm.v1.attention.backends.rocm_attn.RocmAttentionBackend"
    CPU_ATTN = "vllm.v1.attention.backends.cpu_attn.CPUAttentionBackend"
    # ... 还有 MLA 变体

5.2 Backend 选择 — selector.py:52

get_attn_backend() 根据以下因素选择后端:

  • head_size, dtype, kv_cache_dtype, block_size
  • use_mla(DeepSeek MLA 模型)
  • has_sink, use_sparse
  • attn_type, use_non_causal, use_batch_invariant
Backend 行号 特点
FlashAttention flash_attn.py:68 主路径,性能最优,原生支持 block table 和 cascade attention
Triton triton_attn.py:271 纯 Triton 实现,支持 ALiBi、sliding window、FP8 量化、block sparsity
FlashInfer — 第三方 FlashInfer 库
FlexAttention — PyTorch 原生 flex attention
ROCm — AMD GPU 专用
CPU — CPU 后端

5.3 抽象类设计 — backend.py

抽象类 行号 职责
AttentionBackend L55 静态方法:能力检测、KV cache shape、支持的 block size / head size / dtype
AttentionMetadataBuilder L516 构建 per-step 元数据(block_table, slot_mapping 等)
AttentionImpl L763 per-layer 实现:forward() 和 do_kv_cache_update()
CommonAttentionMetadata L353 所有 backend 共享的元数据
class AttentionBackend:
    """静态方法,能力检测"""
    get_name() → str
    get_impl_cls() → AttentionImpl
    get_builder_cls() → AttentionMetadataBuilder
    get_kv_cache_shape(num_blocks, block_size, ...) → tuple
    get_supported_kernel_block_sizes() → list
    validate_configuration() → None

class AttentionMetadataBuilder:
    """构建 per-step 元数据(block_table, slot_mapping 等)"""
    build(common_prefix_len, common_attn_metadata) → metadata

class AttentionImpl:
    """per-layer 实现"""
    forward(layer, query, key, value, kv_cache, attn_metadata, output)
    do_kv_cache_update(layer, key, value, kv_cache, slot_mapping)

5.4 CommonAttentionMetadata — backend.py:353

所有 backend 共享的元数据:

class CommonAttentionMetadata:
    query_start_loc      # 每个请求的 query 起始位置(varlen)
    seq_lens             # 每个请求的序列长度
    num_reqs             # 当前 batch 中的请求数
    num_actual_tokens    # 实际 token 数(不含 padding)
    block_table_tensor   # [num_seqs, max_num_blocks] 页表
    slot_mapping         # 每个 token 对应的缓存 slot
    max_query_len        # 最大 query 长度(decode 时为 1)
    max_seq_len          # 最大序列长度

6. FlashAttention 后端(主路径)

路径:flash_attn.py

6.1 类总览

类 行号 职责
FlashAttentionBackend L68 Backend 入口,能力检测
FlashAttentionMetadata L223 包含 block_table、slot_mapping、seq_lens 等
FlashAttentionMetadataBuilder L276 构建元数据,支持 cascade attention、AOT 调度
FlashAttentionImpl L592 per-layer 实现

6.2 核心 forward 流程 — flash_attn.py:667

class FlashAttentionImpl.forward():
    key_cache, value_cache = kv_cache.unbind(dim=0)
    # key_cache: [num_blocks, block_size, num_kv_heads, head_size]
    # value_cache: [num_blocks, block_size, num_kv_heads, head_size]

    # 使用 flash_attn_varlen_func(原生支持 block table)
    output = flash_attn_varlen_func(
        query, key_cache, value_cache,
        cu_seqlens_q=query_start_loc,   # varlen 索引
        max_seqlen_q=max_query_len,
        seqlen_k=seq_lens,
        block_table=block_table,         # ← 分页的关键参数
        softmax_scale=scale,
        causal=True,
        ...
    )

FlashAttention 库原生支持 block_table 参数,在 kernel 内部完成逻辑→物理 block 的间接寻址。

6.3 KV Cache 更新 — flash_attn.py:850

class FlashAttentionImpl.do_kv_cache_update():
    # 将新计算的 key/value 散射写入 paged cache
    reshape_and_cache_flash(
        key, value, key_cache, value_cache,
        slot_mapping,    # 每个 token 的目标 slot
        kv_cache_dtype,  # FP8 量化时会做格式转换
        k_scale, v_scale
    )

6.4 Cascade Attention

当一个 batch 中所有请求共享相同前缀时:

共享 prefix                    各请求独有的 suffix
┌─────────────────────┐  ┌──────┐ ┌──────┐ ┌──────┐
│ non-causal attention │  │ req0 │ │ req1 │ │ req2 │
│ (只算一次)            │  │causal│ │causal│ │causal│
└─────────────────────┘  └──────┘ └──────┘ └──────┘
         │                    │        │        │
         └────── merge ───────┴────────┴────────┘
              (online softmax 校正)

关键逻辑:

  1. get_num_common_prefix_blocks() — kv_cache_manager.py:476 计算所有请求的最长公共前缀
  2. Prefix 部分用 causal=False 计算一次
  3. 各请求的 suffix 用 causal=True 分别计算
  4. merge_attn_states() 用 online softmax 公式合并两部分

7. CUDA Kernel 实现

7.1 Grid 组织

kernel<<<grid(num_heads, num_seqs), block(NUM_THREADS=128)>>>(...)

每个 thread block 处理 一个 head × 一个 sequence。128 个线程,按 warp (32 threads) 组织。

7.2 核心算法 — attention_kernels.cuh:85

paged_attention_kernel() 是 V1 使用的 device 函数,五个阶段:

┌─────────────────────────────────────────────────┐
│ 1. 加载 Query 到共享内存                         │
│    q_vecs[THREAD_GROUP_SIZE][NUM_VECS_PER_THREAD]│
├─────────────────────────────────────────────────┤
│ 2. QK 点积                                      │
│    for each logical block:                      │
│      physical_block = block_table[block_idx]  ← 分页的核心!│
│      key = k_cache[physical_block * stride + offset]        │
│      scores = Q @ K^T                            │
│      (可选) + ALiBi bias                        │
├─────────────────────────────────────────────────┤
│ 3. Online Softmax                               │
│    - warp shuffle 归约 → qk_max                 │
│    - 跨 warp 归约 → 全局 qk_max                 │
│    - exp_sum 跨所有 token 求和                   │
│    - softmax = exp(score - max) / sum           │
├─────────────────────────────────────────────────┤
│ 4. Value 累加                                   │
│    for each token:                              │
│      v = v_cache[physical_block * v_stride + ...]│
│      acc += softmax(token) * v                  │
├─────────────────────────────────────────────────┤
│ 5. 输出                                         │
│    - warp 内归约                                │
│    - 跨 warp 树形归约(shared memory)           │
│    - 写入 output[num_seqs, num_heads, head_size] │
└─────────────────────────────────────────────────┘

核心的 QK 点积使用 Qk_dot 模板结构体,在 attention_kernels.cuh:289 处调用。

7.3 V1 vs V2 Kernel

  V1 V2
入口 paged_attention_v1.cu:160 paged_attention_v2.cu
Kernel 定义 attention_kernels.cuh:85 (device 函数)
attention_kernels.cuh:497 (global kernel)
attention_kernels.cuh:529 (forward)
attention_kernels.cuh:562 (reduce)
适用场景 中短序列 长序列
策略 单 kernel,串行遍历所有 block partition 并行 + reduce 合并
Partition 大小 N/A 512 tokens
Grid (num_heads, num_seqs) (num_heads, num_seqs, max_num_partitions)
合并方式 N/A online softmax 校正

V2 的优势:对于超长上下文,将 KV cache 分成多个 partition 并行计算,每个 partition 独立做 attention,最后通过 rescale + online softmax 校正合并。

7.4 KV Cache 写入 Kernel — cache_kernels.cu

Kernel 行号 说明
reshape_and_cache_kernel L244 基础版本:[num_blocks, num_heads, head_size//x, block_size, x] 布局
reshape_and_cache_flash_kernel L304 灵活版本:支持 NHD/HND 布局 + FP8 量化
// reshape_and_cache_kernel
// key_cache: [num_blocks, num_heads, head_size//x, block_size, x]
// value_cache: [num_blocks, num_heads, head_size, block_size]
// 其中 x = 16 / sizeof(element), 16 bytes per memory transaction

for each token:
    slot = slot_mapping[token_idx]
    if slot >= 0:  // 跳过 padding
        write key[token] → key_cache[slot]
        write value[token] → value_cache[slot]

8. Triton 实现

路径:vllm/v1/attention/ops/

Kernel 行号 用途
decode_attention_fwd triton_decode_attention.py:729 自回归 decode 阶段(1 query token)
context_attention_fwd triton_prefill_attention.py:191 预填充阶段(多 query tokens)
unified_attention triton_unified_attention.py:763 统一的 prefill + decode kernel
triton_reshape_and_cache_flash triton_reshape_and_cache_flash.py:319 KV cache 写入

额外功能:

  • ALiBi 位置编码
  • Sliding window 掩码
  • Attention sink tokens
  • FP8 KV cache 量化
  • Block sparsity 支持

9. PagedAttention Ops — paged_attn.py:15

PagedAttention 类提供两个关键静态方法:

方法 行号 功能
split_kv_cache L17 将 flat KV cache tensor 拆分为 key/value cache 视图(v0 布局)
write_to_paged_cache L32 调用 ops.reshape_and_cache() 散射写入 KV 数据

10. 补充特性

10.1 Decode Context Parallelism (DCP)

将 KV cache 分布到多张 GPU 上,每张 GPU 持有 KV cache 的一个不重叠分区。配合 paged_attention_v2 的 partition 机制,支持超长 context 的 decode。

10.2 Batch Invariance 模式

强制 num_splits=1,保证相同输入在多次运行中产生完全相同的输出(用于 CUDA graph 捕获)。

10.3 AOT Scheduling (FlashAttention 3)

预计算 FlashAttention 3 的调度器元数据,避免运行时开销。


11. 传统的推理引擎为什么没有这种机制

11.1 传统做法的设计假设

HuggingFace Transformers、FasterTransformer、TensorRT-LLM(早期版本)等引擎,在处理 KV cache 时共享同一个设计前提:

每个请求 = 一段连续预分配的内存
KV cache shape: [batch_size, max_seq_len, num_heads, head_size]
                 ↑
        按最大长度预分配,连续存放

这个假设直接继承自 Transformer 训练 —— 训练时 batch 内所有样本 pad 到相同长度,KV cache 天然是连续方阵。推理引擎对此没有质疑。

# 传统引擎的典型做法(伪代码)
kv_cache = torch.empty(batch_size, max_seq_len, num_heads, head_size)
# 每个请求占一行连续内存:kv_cache[req_id, :seq_len, :, :]

11.2 为什么这个假设在推理时是有害的

  训练 推理(传统引擎) 推理(理想情况)
序列长度 固定(pad 到 max) 动态变化 动态变化
分配时机 一次性 预留最大长度 按需逐步分配
共享 无 无 Prefix 可以共享
内存浪费来源 padding token 预留但从未使用的空间 几乎为零

传统引擎为每个请求预留 max_model_len 的空间。但实际场景中:

  • 一个 max_model_len=8192 的部署,大多数请求可能只生成 200-500 token
  • 每多预留一个位置,就浪费了 2 × num_layers × num_kv_heads × head_size × dtype_size 字节
  • 当并发请求数达到 100+ 时,预留空间的浪费可以让显存利用率降到 30% 以下

11.3 为什么之前没有人这样做

a) Kernel 复杂度的惯性思维

连续内存下的 attention kernel 是经典的 tiled softmax matmul —— 学术界和工业界已经优化了十年。加上 block table 间接寻址后:

  • 每个 key/value token 的地址需要额外一次 global memory load(查 block_table)+ 一次整数乘加(算偏移)
  • 指针不再是规则的 i += stride 递增模式,编译器自动向量化的机会减少
  • 需要在正确性和性能之间重新权衡 tile 大小、warp 分配等参数

对于大部分团队来说,用额外 5-10% 的 kernel 开销换取内存利用率提升,在单请求或少请求场景下是不划算的 —— 连续分配的浪费在 batch size 小时不明显。

b) 训练代码的文化惯性

大多数推理引擎是训练代码库(Fairseq、HuggingFace Transformers)的”推理模式”衍生版本。”KV cache 是连续的” —— 这个假设在训练代码中无处不在,模型的 forward() 方法、attention 实现、甚至 checkpoint 格式都隐含依赖于此。在训练框架上打补丁做推理优化时,很少有团队会质疑这个底层前提。

c) 收益场景的滞后出现

LLM 推理服务的规模化部署(数百并发请求、序列长度差异极大、大量共享 system prompt)是 2023 年下半年才成为普遍需求的。在此之前:

  • 单用户对话场景 → 浪费不严重
  • 批量离线推理 → 可以按 max_seq_len 做 padding
  • API 服务的并发规模较小 → 简单的预分配够用

vLLM 正是在这个转折点上出现的 —— 当 OpenAI、Anthropic 等 API 服务商开始服务百万级用户时,KV cache 显存浪费从”可以忽略”变成了”首要瓶颈”。

d) 这不是单点优化,而是系统工程

实现 PagedAttention 需要的不只是一个带 block_table 的 kernel:

Kernel 层:attention 计算 + KV cache 读写全部支持间接寻址
    ↓
分配层:block pool、空闲链表、引用计数
    ↓
缓存层:prefix caching 哈希表、链式哈希、跨 block size 协调
    ↓
调度层:prefix-aware 调度、preemption / recomputation
    ↓
混合模型层:多种 attention type 的 joint prefix caching 协调

这整套系统需要从 kernel 到 scheduler 的垂直重构。投入成本高,且成功的可能性在 2023 年初并不明显。vLLM 团队的核心贡献正是识别出这个问题值得投入系统性工程,并将 OS 虚拟内存的经典思想映射到了 KV cache 管理这个新领域。

11.4 从”类比”到”工程”的关键一跳

PagedAttention 和 OS 虚拟内存的关系本质上是概念类比,而非实现复用:

  OS 虚拟内存 PagedAttention
解决的问题 物理内存不足、进程隔离 KV cache 内部碎片、prefix 无法共享
“虚拟地址” CPU 发出的地址,经 MMU 翻译 token 的逻辑位置 (seq_idx, block_idx)
“物理地址” DRAM 中的实际位置 KV cache tensor 中的物理 block 偏移
“页表” 多级硬件页表,MMU/TLB 加速 int32 数组,kernel 手工查表
“页” 4KB (x86),硬件定义 16/32 tokens,应用层超参数
“缺页中断” 硬件 page fault → OS 分配物理页 不存在;block 未分配时对应的 token 尚未生成
“换页” 换出到磁盘 不支持;全量驻留 GPU 显存

PagedAttention 的创新不在于”发现了 GPU 硬件支持分页”,而在于用软件实现了分页所需的间接寻址,并将整套系统工程的复杂性控制在可接受范围内。这是 2023 年 LLM 服务化的关键基础设施突破。


12. 总结:关键设计决策

决策 代码位置 设计 理由
Block 粒度 — 16/32 tokens/block 平衡内存碎片和管理开销
间接寻址 attention_kernels.cuh:85 block_table 在 kernel 内查表(纯软件) 实现物理/逻辑解耦的根本机制
Online softmax attention_kernels.cuh:305-341 分块计算 + 在线归一化 不需要一次性加载全部 KV cache
引用计数 kv_cache_utils.py:116 ref_cnt 管理共享 多个请求安全共享 prefix block
空闲链表 kv_cache_utils.py:164 双向链表 O(1) 分配/释放 高频分配场景的性能关键
链式哈希 kv_cache_utils.py:541 考虑父 block + 额外 key 前缀隔离 + 精准缓存匹配
Cascade attention flash_attn.py:276 共享前缀一次计算 批量场景减少重复 KV 计算
V2 长序列 attention_kernels.cuh:529 Partition 并行 + reduce 超长 context 的并行加速
多 type 协调 kv_cache_coordinator.py:392 固定点迭代 混合 attention type 模型的正确性保证

本站访问量

This site uses Just the Docs, a documentation theme for Jekyll.