KV Cache 优化与内存管理深度解析:PagedAttention 原理与实践

7219 字
36 分钟
KV Cache 优化与内存管理深度解析:PagedAttention 原理与实践

1. 引言:LLM 推理的内存墙#

1.1 自回归生成的挑战#

大语言模型的自回归特性带来了独特的内存挑战:

┌─────────────────────────────────────────────────────────────┐
│ 自回归生成的内存问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Prefill 阶段(一次计算): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Input: [T1, T2, T3, ..., T_n] │ │
│ │ Output: 所有位置的 KV hidden states │ │
│ │ 特点:高度并行,适合 GPU │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Decode 阶段(逐 token 生成): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Step 1: [T1] → T2 │ │
│ │ Step 2: [T1, T2] → T3 │ │
│ │ Step 3: [T1, T2, T3] → T4 │ │
│ │ ... │ │
│ │ 问题:每步都需要访问所有历史 KV! │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 关键问题: │
│ → KV 序列长度随生成不断增长 │
│ → 无法预知最终长度 │
│ → 需要高效存储和检索历史 KV │
│ │
└─────────────────────────────────────────────────────────────┘

1.2 内存墙现象#

class MemoryWallAnalysis:
"""
内存墙分析
"""
def model_vs_memory_growth(self):
"""
模型规模 vs 内存需求增长
"""
return {
"model_scaling": {
"LLaMA-2 7B": "14 GB weights",
"LLaMA-2 13B": "26 GB weights",
"LLaMA-2 70B": "140 GB weights",
},
"kv_cache_scaling": {
"per_token_kv_fp16": "2 × 2 × layers × hidden_size × 2 bytes",
"7B_model_per_token": "512 KB",
"8k_context": "4 GB KV cache",
"32k_context": "16 GB KV cache",
},
"total_memory_7b": {
"weights": "14 GB",
"8k_kv_cache": "4 GB",
"activations": "2 GB",
"total": "20 GB (RTX 3090 barely fits)",
},
}
def gpu_memory_timeline(self):
"""
GPU 显存发展 vs 模型需求
"""
return """
时间线对比:
2020: GPT-3 (175B) 需要 ~350 GB
A100 80GB × 5 = 勉强
2022: LLaMA 65B 需要 ~130 GB
A100 80GB × 2 = 可以
2023: LLaMA-2 70B 需要 ~160 GB
A100 80GB × 2 + 优化 = 可以
2024: Claude 200K context 需要 ~800 GB KV
→ 单 GPU 不可能
趋势:
→ 上下文窗口越来越长
→ KV Cache 成为主要瓶颈
→ 内存优化刻不容缓
"""

1.3 标准 Attention 的内存问题#

class StandardAttentionMemory:
"""
标准 Attention 的内存问题
"""
def attention_computation(self):
"""
标准 Attention 计算
Scaled Dot-Product Attention:
Attention(Q, K, V) = softmax(QK^T / √d) V
"""
return """
对于序列长度 N:
Q: (batch, num_heads, N, head_dim)
K: (batch, num_heads, N, head_dim)
V: (batch, num_heads, N, head_dim)
QK^T: (batch, num_heads, N, N) ← N×N 矩阵!
问题:
→ N = 8192 时,QK^T 需要 8192 × 8192 × 4 bytes = 256 MB per head
→ 32 heads = 8 GB per layer
→ 32 layers = 256 GB total
→ 完全不可接受
"""
def kv_cache_storage(self):
"""
KV Cache 存储问题
"""
return {
"store_all_kv": "需要保存所有历史 K 和 V",
"access_pattern": "Decode 阶段需要 O(1) 随机访问",
"fp16_storage": "每个值 2 bytes",
"example_7b": {
"layers": 32,
"heads": 32,
"head_dim": 128,
"per_token_kv": "2 × 32 × 128 × 2 = 16 KB",
"per_layer_per_token": "512 KB",
"total_per_token": "32 × 512 KB = 16 MB",
},
"problem": "长序列 + 多请求 = 内存爆炸",
}

1.4 本系列文章关联#

文章关联
vLLM 深度解析完整推理引擎架构
FlashAttention 解析高效注意力计算
推理优化系列推理时扩展技术

2. KV Cache 基础#

2.1 KV Cache 的本质#

class KVCacheEssence:
"""
KV Cache 本质
"""
def definition(self):
"""
KV Cache 定义
在 Prefill 阶段预计算并缓存所有历史 Key 和 Value
Decode 阶段直接复用,避免重复计算
"""
return """
无 Cache:
┌─────────────────────────────────────────────────────┐
│ Step 1: Attention([T1]) │
│ Step 2: Attention([T1, T2]) = Attention([T1]) + │
│ Attention(Q2, K1, V1) │
│ Step 3: Attention([T1, T2, T3]) = ... │
│ ↑ 每次都重新计算 K1, V1! │
└─────────────────────────────────────────────────────┘
有 Cache:
┌─────────────────────────────────────────────────────┐
│ Prefill: 计算并缓存 K=[K1,K2,K3], V=[V1,V2,V3] │
│ │
│ Step 4: Attention([T1..T4]) │
│ = Attention(Q4, [K1..K3], [V1..V3]) + │
│ Attention(Q4, K4, V4) │
│ ↑ 只计算 K4, V4! │
└─────────────────────────────────────────────────────┘
"""
def memory_breakdown(self):
"""
内存分解(LLaMA-2 7B)
"""
return {
"weights_fp16": "14 GB",
"kv_cache_8k": {
"per_token_kb": 512,
"total_gb": 4,
"note": "8k tokens × 512 KB",
},
"kv_cache_32k": {
"per_token_kb": 512,
"total_gb": 16,
"note": "32k tokens × 512 KB",
},
"activations": "1-3 GB (取决于 batch size)",
"total_8k": "14 + 4 + 2 = 20 GB",
"total_32k": "14 + 16 + 3 = 33 GB",
"problem_70b": {
"weights": "140 GB",
"kv_cache_8k": "32 GB",
"total": "172 GB",
"a100_80gb_needed": "3 cards minimum",
},
}

2.2 KV Cache 的实现#

class KVCacheImplementation:
"""
KV Cache 实现
"""
def naive_implementation(self):
"""
朴素实现
"""
return '''
class NaiveKVCache:
"""朴素实现 - 每个序列独立缓存"""
def __init__(self, max_seq_len):
self.k_cache = {} # {seq_id: tensor}
self.v_cache = {} # {seq_id: tensor}
self.max_seq_len = max_seq_len
def update(self, seq_id, new_k, new_v):
"""追加新的 KV"""
if seq_id not in self.k_cache:
self.k_cache[seq_id] = []
self.v_cache[seq_id] = []
self.k_cache[seq_id].append(new_k)
self.v_cache[seq_id].append(new_v)
def get(self, seq_id):
"""获取完整 KV"""
return (
torch.cat(self.k_cache[seq_id], dim=1),
torch.cat(self.v_cache[seq_id], dim=1),
)
问题:
→ 列表追加效率低
→ torch.cat 会复制大量数据
→ 内存碎片
→ 不支持随机访问
'''
def preallocated_implementation(self):
"""
预分配实现
"""
return '''
class PreallocatedKVCache:
"""预分配连续内存"""
def __init__(self, max_seq_len, num_heads, head_dim):
self.max_seq_len = max_seq_len
# 预分配最大长度
self.k_cache = torch.zeros(
max_seq_len, num_heads, head_dim
)
self.v_cache = torch.zeros(
max_seq_len, num_heads, head_dim
)
self.seq_lengths = {} # 跟踪每个序列的长度
def update(self, seq_id, position, new_k, new_v):
"""在指定位置更新"""
self.k_cache[position] = new_k
self.v_cache[position] = new_v
self.seq_lengths[seq_id] = position + 1
def get(self, seq_id, length):
"""获取指定长度的 KV"""
return (
self.k_cache[:length],
self.v_cache[:length],
)
问题:
→ 必须预先知道 max_seq_len
→ 不同长度的序列都会占用最大长度
→ 内存浪费严重
'''

2.3 Cache 访问模式#

class CacheAccessPatterns:
"""
Cache 访问模式分析
"""
def prefill_access(self):
"""
Prefill 阶段的访问模式
计算所有 token 的 KV
"""
return """
Prefill: [T1, T2, T3, ..., T_n]
需要计算:
- 每个 token i 的 K_i, V_i
- 每个 token i 的 attention 输出
访问模式:
- Sequential: 顺序遍历所有 tokens
- Heavy compute: 计算密集型
- One-shot: 一次性完成
"""
def decode_access(self):
"""
Decode 阶段的访问模式
只计算新 token 的 KV
"""
return """
Decode step t:
Input: Q_t (单个 token)
Cache: K_[1..t-1], V_[1..t-1]
需要:
1. 计算 K_t, V_t (新 token)
2. 读取 K_[1..t-1], V_[1..t-1] (全部历史)
3. 计算 attention
访问模式:
- Random read: 读取整个 KV cache
- Sequential write: 顺序写入新 KV
- Memory bound: 内存访问是瓶颈
关键洞察:
Decode 是 memory-bound 操作!
"""
def attention_access(self):
"""
Attention 计算的内存访问
"""
return """
Decode step t 时的 Attention 计算:
Q_t: (1, num_heads, head_dim)
K_cache: (t-1, num_heads, head_dim)
V_cache: (t-1, num_heads, head_dim)
内存访问量:
- 读取 Q_t: ~32 KB
- 读取 K_cache: (t-1) × 16 KB
- 读取 V_cache: (t-1) × 16 KB
- 写入 output: ~32 KB
总计: ~32 × (t-1) KB
当 t=4096:
内存访问 = 128 KB
计算量 = num_heads × t × head_dim × 2 = 2M FLOPs
计算/内存比很低 → memory bound
"""

3. PagedAttention 核心机制#

3.1 分页管理的启发#

class PagingInspiration:
"""
分页管理的启发
"""
def os_paging_concept(self):
"""
操作系统分页概念
"""
return """
OS 虚拟内存管理的核心思想:
1. 虚拟地址空间
- 进程看到的是连续的虚拟地址
- 可以远大于物理内存
2. 物理内存管理
- 物理内存被分成固定大小的 pages
- 4KB (x86) 或 2MB/1GB (huge pages)
3. Page Table
- 映射虚拟页到物理页
- 允许虚拟地址连续但物理地址离散
4. 按需加载
- 不需要预先分配整个空间
- 页面按需分配和换入/换出
优势:
→ 消除外部碎片
→ 支持超内存运行
→ 灵活共享内存
→ 高效内存利用
"""
def llm_analogy(self):
"""
LLM 推理中的类比
"""
return """
PagedAttention 的类比:
OS 虚拟内存 LLM 推理
─────────────────────────────────────────────
虚拟地址空间 → Token 序列位置
物理页 → KV Cache Blocks
Page Table → Block Table
虚拟页号 → Token 索引
物理页号 → Block 索引
关键对应:
OS: virtual_page_id → physical_page_id
vLLM: token_position → kv_block_id
Block 0: [token 0-15]
Block 1: [token 16-31]
Block 2: [token 32-47]
...
"""

3.2 Block Table 设计#

class BlockTableDesign:
"""
Block Table 设计
"""
def table_structure(self):
"""
Block Table 结构
"""
return """
概念:
Token 序列: [T0, T1, T2, ..., T67] (68 tokens)
Block Size: 16 tokens/block
需要 Blocks: ceil(68/16) = 5 blocks
Block Table (逻辑 → 物理映射):
┌───────────┬─────────────┬────────────────────┐
│ Block ID │ Physical ID │ Content │
├───────────┼─────────────┼────────────────────┤
│ 0 │ 7 │ [T0, T1, ..., T15] │
│ 1 │ 3 │ [T16,...,T31] │
│ 2 │ 9 │ [T32,...,T47] │
│ 3 │ 2 │ [T48,...,T63] │
│ 4 │ 12 │ [T64,...,T67] │ ← 部分使用
└───────────┴─────────────┴────────────────────┘
特点:
→ Block 物理位置可以不连续
→ 最后一个 block 可以部分填充
→ 动态扩展无需重新分配整个序列
"""
def implementation(self):
"""
实现
"""
return '''
class BlockTable:
"""Block Table 实现"""
def __init__(self, block_size=16):
self.block_size = block_size
self.tables = {} # {seq_id: {"physical_blocks": [], "num_full_slots": int}}
def add_block(self, seq_id, physical_block_id):
"""添加一个新的 block"""
if seq_id not in self.tables:
self.tables[seq_id] = {
"physical_blocks": [],
"num_full_slots": 0,
}
self.tables[seq_id]["physical_blocks"].append(physical_block_id)
def get_physical_block(self, seq_id, block_idx):
"""获取指定 block 的物理位置"""
return self.tables[seq_id]["physical_blocks"][block_idx]
def get_token_block(self, seq_id, token_idx):
"""获取 token 所在的 block"""
block_idx = token_idx // self.block_size
return self.get_physical_block(seq_id, block_idx)
# 使用示例
table = BlockTable(block_size=16)
table.add_block("seq_1", 7) # tokens 0-15 → block 7
table.add_block("seq_1", 3) # tokens 16-31 → block 3
table.add_block("seq_1", 9) # tokens 32-47 → block 9
# 读取 token 25
block_idx = 25 // 16 = 1
physical = table.get_physical_block("seq_1", 1) # → 3
offset = 25 % 16 = 9
# → 读取 block 3, offset 9
'''
def dynamic_growth(self):
"""
动态增长
"""
return """
动态扩展示例:
初始状态(生成 50 tokens):
Block Table: [0, 1, 2, 3] (4 blocks)
继续生成(需要 60 tokens):
Block Table: [0, 1, 2, 3] + [4] (追加)
继续生成(需要 100 tokens):
Block Table: [0, 1, 2, 3] + [4, 5, 6] (继续追加)
优势:
→ 无需预先知道最终长度
→ 按需分配 blocks
→ 已完成的序列释放 blocks
→ 新请求复用释放的 blocks
对比预分配:
preallocate 100 tokens worth → 浪费 50 tokens 的空间
paged 动态增长 → 只用多少分配多少
"""

3.3 PagedAttention Kernel#

class PagedAttentionKernel:
"""
PagedAttention CUDA Kernel
"""
def kernel_design(self):
"""
Kernel 设计
"""
return '''
__global__ void paged_attention_kernel(
const half* __restrict__ query, // (batch, heads, dim)
const half* __restrict__ key_cache, // 存储在 blocks 中
const half* __restrict__ value_cache,
const int* block_tables, // (batch, max_blocks)
const int* seq_lens, // 每个序列的实际长度
half* output,
int block_size,
int num_heads,
int head_dim,
) {
// 获取当前线程负责的 query
int batch_idx = blockIdx.x;
int head_idx = threadIdx.x;
// 获取该序列的 block table
int* table = &block_tables[batch_idx * max_blocks];
int num_blocks = (seq_lens[batch_idx] + block_size - 1) / block_size;
// 分块处理 keys
float acc = 0.0f;
float max_val = -INFINITY;
for (int block = 0; block < num_blocks; block++) {
int physical_block = table[block];
// 从物理 block 读取 keys
half4 k_block = *((half4*)&key_cache[
physical_block * block_size * num_heads * head_dim
+ head_idx * block_size * head_dim
]);
// 计算 partial attention
// ...
}
// 保存结果
// ...
}
'''
def memory_access_optimization(self):
"""
内存访问优化
"""
return {
"coalesced_access": "合并相邻线程的内存访问",
"shared_memory": "使用 shared memory 缓存频繁访问的数据",
"vectorized_load": "使用 float4/half4 向量化加载",
"bank_conflict_free": "避免 shared memory bank 冲突",
"optimal_blocking": "每个 block 处理多个 token",
"thread_per_head": "每个 head 一个 thread block",
"warp_level_reduction": "Warp 级别的 softmax 归约",
}

4. 内存碎片与利用率#

4.1 碎片类型#

class MemoryFragmentation:
"""
内存碎片类型
"""
def internal_fragmentation(self):
"""
内部碎片
预分配但不使用的空间
"""
return """
场景:预分配 max_seq_len = 8192
Request A: 生成 100 tokens
实际使用: 100 / 8192 = 1.2%
浪费: 99%
Request B: 生成 5000 tokens
实际使用: 5000 / 8192 = 61%
浪费: 39%
问题根源:
→ 必须按最大可能长度分配
→ 大多数请求远小于最大值
→ 大量内存永久浪费
"""
def external_fragmentation(self):
"""
外部碎片
可用但不连续的内存
"""
return """
场景:多请求混合长度
内存池状态随时间变化:
T1: [Req1:100][Req2:500][Req3:2000][ free ]
T2: [Req1:done ][Req2:500][Req3:2500][ free ]
↑释放 ↑增长 ↑增长
但不能 不能 不能
给Req4 扩展 扩展
问题:
→ 虽然有 free 空间
→ 但分散在不连续位置
→ 无法满足大请求的连续需求
→ 只能 OOM 或等待
"""
def paged_solution(self):
"""
Paged 方案如何解决
"""
return """
PagedAttention 的解决方案:
1. 内部碎片 → 消除
- Block Size 固定但较小 (16 tokens)
- 只分配实际需要的 blocks
- 最后一个 block 只用必要的空间
2. 外部碎片 → 消除
- Blocks 可以放在任何位置
- 物理位置可以不连续
- Block Table 提供逻辑连续视图
结果:
→ 内存利用率接近 100%
→ 任意长度的请求都能高效处理
→ 碎片成为历史
"""

4.2 内存利用率对比#

┌─────────────────────────────────────────────────────────────┐
│ 内存利用率对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 预分配策略(max_seq_len = 8192): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Request 1 (100 tokens): ██░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ │ 1.2% used, 98.8% waste │ │
│ │ │ │
│ │ Request 2 (5000 tokens): ██████████████████░░░░░░ │ │
│ │ 61% used, 39% waste │ │
│ │ │ │
│ │ Request 3 (200 tokens): ██░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ │ 2.4% used, 97.6% waste │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ PagedAttention 策略(block_size = 16): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Request 1 (100 tokens): ██████████ │ │
│ │ 100% used (7 blocks) │ │
│ │ │ │
│ │ Request 2 (5000 tokens): ██████████████████████████ │ │
│ │ 100% used (313 blocks) │ │
│ │ │ │
│ │ Request 3 (200 tokens): ██████████████████ │ │
│ │ 100% used (13 blocks) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 总结: │
│ 预分配:平均利用率 ~20-40% │
│ Paged:平均利用率 ~90-95% │
│ 提升:2-4x │
│ │
└─────────────────────────────────────────────────────────────┘

4.3 Block Size 选择#

class BlockSizeSelection:
"""
Block Size 选择
"""
def tradeoffs(self):
"""
Block Size 的权衡
"""
return {
"small_block_8": {
"pros": "内存利用率高,最后一个 block 浪费少",
"cons": "Block table 更大,管理开销高",
"fragmentation": "内部碎片 < 8 tokens",
},
"medium_block_16": {
"pros": "平衡内存利用率和管理开销",
"cons": "略多于 8 的管理开销",
"fragmentation": "内部碎片 < 16 tokens",
"recommendation": "vLLM 默认推荐",
},
"large_block_32": {
"pros": "管理开销低,缓存命中率高",
"cons": "内部碎片最多 32 tokens",
"fragmentation": "对于短序列浪费明显",
},
}
def benchmark_recommendation(self):
"""
基准测试建议
"""
return """
vLLM 官方推荐:
block_size = 16
原因:
1. 大多数请求的 KV cache 远大于 16 tokens
2. 内部碎片 < 16 tokens,可接受
3. Block table 管理开销可忽略
4. CUDA kernel 优化针对 block_size=16
特殊情况:
→ 极短序列(< 32 tokens):考虑 block_size=8
→ 超长序列 + 稀疏访问:考虑 block_size=32
"""

5. 内存池管理#

5.1 内存池架构#

class MemoryPoolArchitecture:
"""
内存池架构
"""
def pool_structure(self):
"""
内存池结构
"""
return """
vLLM GPU 内存布局:
┌──────────────────────────────────────────────────────────┐
│ GPU Memory │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Model Weights (固定) │ │
│ │ - 7B FP16: 14 GB │ │
│ │ - 70B FP16: 140 GB │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ KV Cache Pool (动态) │ │
│ │ │ │
│ │ Block 0 Block 1 Block 2 Block 3 Block 4 ... │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │ KV │ │ KV │ │ KV │ │ KV │ │ KV │ │ │
│ │ │16Tk │ │16Tk │ │16Tk │ │16Tk │ │16Tk │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Activations (临时) │ │
│ │ - Prefill 中间结果 │ │
│ │ - Decode 临时 buffer │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
gpu_memory_utilization 参数控制 KV Cache Pool 大小:
→ 0.9 表示 90% 的剩余内存用于 KV Cache
→ 10% 预留给 Activations
"""
def allocation_strategy(self):
"""
分配策略
"""
return """
KV Cache 分配流程:
1. 初始化时计算可用 blocks 数
available_blocks = gpu_memory × utilization / block_size
2. 维护空闲 blocks 列表
free_blocks = {0, 1, 2, 3, ..., N-1}
3. 请求到来时分配
- 计算需要的 blocks 数
- 从 free_blocks 取出
- 分配给请求
4. 请求完成时释放
- 归还 blocks 到 free_blocks
- 可立即被其他请求复用
关键:无需磁盘交换,纯 GPU 内管理
"""

5.2 Eviction 策略#

class EvictionStrategies:
"""
Eviction 策略
"""
def lru_strategy(self):
"""
LRU (Least Recently Used)
驱逐最久未使用的序列
"""
return """
LRU 策略:
场景:内存池满,需要为新请求腾出空间
当前状态:
┌──────────────────────────────────────────┐
│ Req1 (3 blocks) last_used: 10 iterations ago │
│ Req2 (5 blocks) last_used: 2 iterations ago │
│ Req3 (2 blocks) last_used: 50 iterations ago │
└──────────────────────────────────────────┘
LRU 决策:
→ Req3 最久未使用
→ 驱逐 Req3 的所有 blocks
→ 释放 2 blocks
原因:
→ 活跃请求可能继续生成
→ 不活跃请求可能已放弃或完成
→ 优先保护活跃请求
"""
def fifo_strategy(self):
"""
FIFO (First In First Out)
驱逐最早的序列
"""
return """
FIFO 策略:
场景:内存池满
当前状态:
┌──────────────────────────────────────────┐
│ Req1 created: 100s ago (3 blocks) │
│ Req2 created: 50s ago (5 blocks) │
│ Req3 created: 10s ago (2 blocks) │
└──────────────────────────────────────────┘
FIFO 决策:
→ Req1 创建最早
→ 驱逐 Req1
问题:
→ 可能驱逐仍在活跃使用的序列
→ 不如 LRU 智能
"""
def hybrid_strategy(self):
"""
混合策略
"""
return """
vLLM 实际使用的策略:
1. 主要使用 LRU
- 跟踪每个序列的最后访问时间
- 驱逐最久未使用的
2. 优先级队列
- 区分 prefill 和 decode 阶段
- decode 中的序列优先级更高
3. 部分驱逐
- 可以只驱逐部分 blocks
- 保留最近的 blocks
- 用于处理超长序列
4. 拒绝而非驱逐(极端情况)
- 如果所有序列都很活跃
- 拒绝新请求而非驱逐
- 返回队列等待
"""

5.3 CPU-GPU Swap#

class CPUSwap:
"""
CPU-GPU Swap
"""
def when_needed(self):
"""
何时需要 Swap
"""
return """
需要 Swap 的场景:
1. 超长序列
- 序列长度超过 GPU KV Cache 容量
- 部分 KV 需要换出到 CPU
2. 内存压力
- 高并发时内存池耗尽
- 驱逐可能导致结果不正确
3. 优先级场景
- 某些高优先级请求需要更多内存
- 临时换出低优先级请求
权衡:
→ Swap 开销:PCIe 带宽 (~64 GB/s)
→ 但比 OOM 崩溃好
→ 可以处理超长上下文
"""
def implementation(self):
"""
Swap 实现
"""
return '''
class KVCacheSwap:
"""KV Cache CPU-GPU Swap"""
def __init__(self, gpu_pool, cpu_pool_size=10000):
self.gpu_pool = gpu_pool
self.cpu_pool = {} # {seq_id: {blocks: [], in_gpu: bool}}
self.cpu_pool_size = cpu_pool_size
def swap_out(self, seq_id, blocks_to_swap):
"""将 GPU blocks 换出到 CPU"""
for block_id in blocks_to_swap:
# GPU → CPU
cpu_data = self.gpu_pool.get_block_data(block_id)
self.cpu_pool[seq_id]["data"].append(cpu_data)
self.gpu_pool.free_block(block_id)
def swap_in(self, seq_id, blocks_to_swap):
"""将 CPU blocks 换回 GPU"""
for block_id in blocks_to_swap:
# CPU → GPU
gpu_block = self.gpu_pool.allocate_block()
gpu_block.load_data(self.cpu_pool[seq_id]["data"].pop())
'''
def performance_impact(self):
"""
性能影响
"""
return {
"swap_out_latency": "~1-5 ms per block (PCIe)",
"swap_in_latency": "~1-5 ms per block (PCIe)",
"full_swap_1k_tokens": "~40 ms (8 blocks)",
"without_swap": "OOM,任务失败",
"with_swap": "延迟增加但可完成",
"recommendation": "尽量避免 swap,预留足够 GPU 内存",
}

6. 前缀缓存与共享#

6.1 共享前缀场景#

"""
共享前缀场景
"""
def chat_completion(self):
"""
Chat Completion 场景
系统提示 + 对话历史 + 当前查询
"""
return """
典型 Chat 请求:
Request 1:
[System: "你是AI助手..." (500 tokens)]
[History: "用户问题1" (100 tokens)]
[Query: "当前问题" (50 tokens)]
Request 2:
[System: "你是AI助手..." (500 tokens)] ← 相同!
[History: "用户问题2" (120 tokens)]
[Query: "当前问题2" (50 tokens)]
Request 3:
[System: "你是AI助手..." (500 tokens)] ← 相同!
[History: "用户问题3" (80 tokens)]
[Query: "当前问题3" (50 tokens)]
共享情况:
→ System Prompt 完全相同 → KV Cache 可共享
→ History 不同 → 不可共享
内存节省:
→ 无共享:3 × 500 = 1500 tokens 的 KV
→ 有共享:500 + 300 = 800 tokens 的 KV
→ 节省:46%
"""
def few_shot_learning(self):
"""
Few-shot Learning 场景
"""
return """
Few-shot 示例:
Request:
[Example 1: Q + A] (200 tokens)
[Example 2: Q + A] (200 tokens) ← 相同格式
[Example 3: Q + A] (200 tokens) ← 相同格式
[Example N: Q] (100 tokens)
[Query] (50 tokens)
优化:
→ Examples 1..N-1 的 KV 可以共享
→ 只计算 Example N 和 Query 的 KV
"""
def document_summarization(self):
"""
文档摘要场景
"""
return """
批量摘要任务:
Request 1:
[Document A summary] + [User instruction] + [Document A]
Request 2:
[Document B summary] + [User instruction] + [Document B]
Request 3:
[Document C summary] + [User instruction] + [Document C]
共享:
→ "User instruction" 完全相同
→ Summary 格式相同
潜在优化:
→ 识别结构化前缀
→ 只计算文档内容的 KV
"""

6.2 Hash-based Caching#

class HashBasedCaching:
"""
基于哈希的缓存
"""
def block_hashing(self):
"""
Block 哈希计算
"""
return '''
class BlockHashCache:
"""基于哈希的 Block 缓存"""
def __init__(self):
self.hash_to_block = {} # hash → block_id
self.block_to_hash = {} # block_id → hash
def compute_block_hash(self, tokens):
"""计算 block 内容哈希"""
import hashlib
content = bytes(tokens.cpu().numpy().tobytes())
return hashlib.sha256(content).hexdigest()[:16]
def add_block(self, tokens, block_id):
"""添加 block 并记录哈希"""
h = self.compute_block_hash(tokens)
if h in self.hash_to_block:
# 内容相同,共享已有的 block
return self.hash_to_block[h]
else:
# 新内容
self.hash_to_block[h] = block_id
self.block_to_hash[block_id] = h
return block_id
def lookup(self, tokens):
"""查找是否存在相同内容"""
h = self.compute_block_hash(tokens)
return self.hash_to_block.get(h)
'''
def cache_key_calculation(self):
"""
Cache Key 计算
"""
return """
Cache Key 策略:
1. 内容哈希(精确匹配)
hash = SHA256(block_tokens)
- 完全相同的内容才能命中
- 精确但可能错过相似前缀
2. 前缀哈希(前缀匹配)
hash = SHA256(prefix_tokens)
- 相同前缀的所有内容可复用
- 需要额外的数据结构
3. 多级哈希
- 第一级:block 内容哈希
- 第二级:token 级别哈希
- 支持部分前缀共享
vLLM 使用:
→ 第一级内容哈希
→ Block 为粒度的精确匹配
→ Prefix 共享通过 hash 碰撞检测
"""
def prefix_lookup(self):
"""
前缀查找
"""
return """
前缀查找流程:
新请求:tokens = [A, B, C, D, E, F]
Step 1: 计算每个 block 的哈希
hash([A,B]) = abc
hash([C,D]) = def
hash([E,F]) = ghi
Step 2: 查找缓存
if abc in cache → 命中 block 7
if def in cache → 命中 block 3
if ghi not in cache → 需要计算
Step 3: 构建 Block Table
Block Table: [7, 3, new_block]
结果:
→ 前缀 [A,B,C,D] 的 KV 来自缓存
→ 只需计算 [E,F] 的 KV
"""

6.3 共享实现#

class SharedCacheImplementation:
"""
共享缓存实现
"""
def reference_counting(self):
"""
引用计数
跟踪每个 block 被多少序列共享
"""
return '''
class ReferenceCountedBlocks:
"""引用计数的 Block 管理"""
def __init__(self):
self.blocks = {} # block_id → data
self.ref_counts = {} # block_id → count
self.seq_blocks = {} # seq_id → [block_ids]
def allocate_shared(self, tokens):
"""分配可能被共享的 block"""
h = self.compute_hash(tokens)
if h in self.hash_to_block:
block_id = self.hash_to_block[h]
self.ref_counts[block_id] += 1
else:
# 创建新 block
block_id = self.create_block(tokens)
self.hash_to_block[h] = block_id
self.ref_counts[block_id] = 1
return block_id
def release(self, seq_id):
"""释放序列的所有 blocks"""
if seq_id not in self.seq_blocks:
return
for block_id in self.seq_blocks[seq_id]:
self.ref_counts[block_id] -= 1
if self.ref_counts[block_id] == 0:
# 没人用了,可以回收
self.recycle_block(block_id)
del self.seq_blocks[seq_id]
'''
def garbage_collection(self):
"""
垃圾回收
"""
return """
垃圾回收策略:
1. 引用计数为 0 时回收
- 最简单直接
- 立即释放未使用的内存
2. 延迟回收
- 引用计数为 0 时标记为可回收
- 等待 LRU 淘汰时真正回收
- 避免频繁分配/释放
3. 定期扫描
- 后台线程定期扫描
- 清理孤立 blocks
- 更新内存统计
vLLM 实现:
→ 主要使用引用计数
→ 结合 LRU 淘汰策略
→ 后台垃圾回收线程
"""

7. 多模态场景的 KV Cache#

7.1 视觉 Token 的特殊处理#

class VisionTokenHandling:
"""
视觉 Token 处理
"""
def image_tokens(self):
"""
图像 Token 的特点
VLM (Vision Language Model) 中的图像 token:
"""
return """
LLaVA/VQVAE 风格的图像 token:
图像 → Visual Encoder → 576 tokens (24×24 grid)
特点:
→ 数量固定(不随文本增长)
→ 内容固定(同一图像产生相同 token)
→ 通常在序列开头
KV Cache 策略:
→ 图像 KV 可以跨请求共享
→ 只计算一次,缓存复用
→ 大幅节省内存
"""
def shared_image_cache(self):
"""
共享图像 Cache
"""
return """
多模态请求场景:
Request 1: [Image A] + [Question 1]
Request 2: [Image A] + [Question 2] ← 同一图像!
Request 3: [Image A] + [Question 3] ← 同一图像!
优化:
→ Image A 的 KV 只计算一次
→ 所有请求共享图像部分的 KV
→ 只计算各请求独特的问题部分
内存节省:
→ 576 tokens × 512 KB = 288 MB per image
→ 3 requests: 288 MB × 1 vs 288 MB × 3
→ 节省 66%
"""

7.2 多模态 Block 管理#

def modality_aware_blocks(self):
"""
模态感知的 Block 管理
"""
return '''
class ModalityAwareBlocks:
"""区分不同模态的 Block"""
def __init__(self):
self.text_blocks = {} # 文本 blocks
self.image_blocks = {} # 图像 blocks(可共享)
def allocate(self, tokens, modality="text"):
"""根据模态分配"""
if modality == "image":
# 检查是否已存在相同的图像
img_hash = self.compute_image_hash(tokens)
if img_hash in self.image_blocks:
return self.image_blocks[img_hash]
block_id = self.allocate_new()
self.image_blocks[img_hash] = block_id
return block_id
else:
# 文本 block 不共享
return self.allocate_new()
def get_shared_blocks(self, seq_id):
"""获取序列的共享 blocks"""
text = self.text_blocks[seq_id]
images = [self.image_blocks[h] for h in self.image_hashes[seq_id]]
return images + text
'''

8. 性能分析与调优#

8.1 性能指标#

class PerformanceMetrics:
"""
性能指标
"""
def memory_metrics(self):
"""
内存指标
"""
return {
"gpu_memory_utilization": "GPU 显存使用率 (目标 85-95%)",
"kv_cache_utilization": "KV Cache blocks 实际使用率",
"cache_hit_rate": "前缀缓存命中率",
"fragmentation_rate": "内存碎片率 (PagedAttention 应该 < 5%)",
"example": {
"allocated_blocks": 10000,
"used_blocks": 9500,
"utilization": "95%",
},
}
def latency_metrics(self):
"""
延迟指标
"""
return {
"time_to_first_token": "TTFT,首 token 延迟",
"inter_token_latency": "ITL,token 间延迟",
"total_generation_time": "总生成时间",
"cache_impact": {
"cache_hit": "prefix 跳过 prefill,更快 TTFT",
"cache_miss": "需要完整计算",
},
}
def throughput_metrics(self):
"""
吞吐指标
"""
return {
"tokens_per_second": "每秒生成 token 数",
"requests_per_second": "每秒处理请求数",
"concurrent_requests": "同时处理的请求数",
"batch_utilization": "Batch 利用率",
}

8.2 调优参数#

class TuningParameters:
"""
调优参数
"""
def critical_params(self):
"""
关键参数
"""
return {
"--gpu-memory-utilization": {
"default": 0.9,
"range": "0.8 - 0.95",
"recommendation": {
"single_request": "0.9",
"multi_request": "0.85",
"long_context": "0.8",
},
"tradeoff": "高利用率 = 更多缓存,但可能 OOM",
},
"--max-model-len": {
"default": "模型定义",
"increase_needed": "长上下文任务",
"memory_impact": "max_len × 512 KB × 32 layers",
},
"--block-size": {
"default": 16,
"options": [8, 16, 32],
"recommendation": "16 (最佳平衡)",
},
}
def memory_estimation(self):
"""
内存估算公式
"""
return """
精确内存估算:
KV Cache Memory =
layers × num_kv_heads × head_dim × 2 (K+V)
× bytes_per_param (2 for FP16)
× max_model_len
× gpu_memory_utilization
示例 (LLaMA-2 7B, max_len=8192):
KV Memory =
32 × 32 × 128 × 2 × 2 × 8192 × 0.9
= 32 × 32 × 128 × 2 × 2 × 7372.8
≈ 3.8 GB
总 GPU Memory =
weights: 14 GB
KV: 3.8 GB
activations: ~2 GB
overhead: 1 GB
= ~21 GB
"""
def monitoring_commands(self):
"""
监控命令
"""
return """
# 查看 vLLM 指标
curl http://localhost:8000/metrics
# 关键指标
vllm:num_available_blocks # 可用 KV blocks
vllm:gpu_cache_usage # GPU 缓存使用率
vllm:num_requests_waiting # 等待中的请求
# nvidia-smi 监控
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
# Python 监控
from vllm.engine.llm_engine import LLMEngine
stats = engine.get_stats()
print(stats.num_available_blocks)
"""

8.3 常见问题排查#

class Troubleshooting:
"""
常见问题排查
"""
def oom_issues(self):
"""
OOM 问题
"""
return {
"symptom": "CUDA out of memory",
"causes": [
"gpu-memory-utilization 过高",
"max-model-len 过长",
"batch size 过大",
"并发请求过多",
],
"solutions": [
"降低 gpu-memory-utilization 到 0.85",
"减少 max-model-len",
"增加 --numcheduled-seq 限制",
"启用 CPU offload (实验性)",
],
}
def low_throughput(self):
"""
低吞吐问题
"""
return {
"symptom": "tokens/s 远低于预期",
"causes": [
"频繁 cache eviction",
"CPU-GPU swap",
"小 batch size",
"GPU 利用率低",
],
"solutions": [
"增加 gpu-memory-utilization",
"检查 swap 指标",
"增加 max-num-batched-tokens",
"优化请求分布",
],
}
def high_latency(self):
"""
高延迟问题
"""
return {
"symptom": "首 token 延迟高",
"causes": [
"长 prefill 阶段",
"前缀未命中",
"队列积压",
],
"solutions": [
"使用 prefix caching",
"限制 max-model-len",
"增加实例数量",
],
}

9. 与 FlashAttention 的协同#

9.1 协同工作原理#

class FlashAttentionSynergy:
"""
FlashAttention 与 PagedAttention 协同
"""
def integration(self):
"""
集成方式
"""
return """
vLLM 的 Attention 实现:
┌────────────────────────────────────────────────────────┐
│ Attention Layer │
├────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ PagedAttention (内存管理) │ │
│ │ - Block Table 映射 │ │
│ │ - KV Cache 分页存储 │ │
│ │ - 碎片消除 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ FlashAttention (计算加速) │ │
│ │ - IO 高效 │ │
│ │ - 融合 kernel │ │
│ │ - 显存节省 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↓ │
│ Attention Output │
└────────────────────────────────────────────────────────┘
关系:
→ PagedAttention 管理"在哪里取数据"
→ FlashAttention 负责"如何高效计算"
→ 两者互补,不是竞争
"""
def paged_flash_attention(self):
"""
Paged FlashAttention
"""
return '''
# 伪代码:Paged + Flash
def paged_flash_attention(
query, # (total_tokens, num_heads, head_dim)
block_tables, # 逻辑 → 物理映射
seq_lens, # 各序列长度
block_size, # 16
):
# 分块加载 K, V
for block_idx in range(num_blocks):
physical_block = block_table[block_idx]
# FlashAttention 风格的分块计算
# 从 physical_block 加载 K, V
k_block = load_k_from_block(physical_block)
v_block = load_v_from_block(physical_block)
# 分块 attention
partial_attn = flash_attn_partial(q, k_block, v_block)
acc_attn += partial_attn
return normalize(acc_attn)
'''
def memory_vs_compute(self):
"""
内存 vs 计算优化
"""
return {
"flash_attention": {
"优化": "计算效率",
"效果": "减少 HBM 访问 2-4x",
"局限": "仍需要高效的内存管理",
},
"paged_attention": {
"优化": "内存管理",
"效果": "消除碎片,提高利用率",
"局限": "需要配合高效计算才能发挥全部潜力",
},
"combined": {
"优化": "全栈优化",
"效果": "内存高效 + 计算高效 = 最佳性能",
},
}

10. 核心公式汇总#

10.1 KV Cache 内存#

MKV=2×L×Nkv×D×B×SmaxM_{\text{KV}} = 2 \times L \times N_{\text{kv}} \times D \times B \times S_{\text{max}}

其中 LL 是层数,NkvN_{\text{kv}} 是 KV head 数,DD 是 head 维度,BB 是字节数(FP16=2),SmaxS_{\text{max}} 是最大序列长度。

10.2 Block 利用率#

Ublock=SactualNblocks×BU_{\text{block}} = \frac{S_{\text{actual}}}{N_{\text{blocks}} \times B}

其中 SactualS_{\text{actual}} 是实际 token 数,NblocksN_{\text{blocks}} 是分配的 blocks 数,BB 是 block size。

10.3 缓存命中率#

Hit Rate=NhitNhit+Nmiss\text{Hit Rate} = \frac{N_{\text{hit}}}{N_{\text{hit}} + N_{\text{miss}}}

11. 总结#

11.1 核心要点#

┌─────────────────────────────────────────────────────────────┐
│ KV Cache 优化核心要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. PagedAttention │
│ → 分页管理 KV Cache │
│ → 消除内存碎片 │
│ → 支持动态增长 │
│ │
│ 2. Block Table │
│ → 逻辑地址到物理地址的映射 │
│ → 灵活的内存分配 │
│ → 高效的共享机制 │
│ │
│ 3. 前缀缓存 │
│ → 哈希检测相同前缀 │
│ → 跨请求共享 KV │
│ → 显著节省计算和内存 │
│ │
│ 4. 内存池管理 │
│ → 预分配可用 blocks │
│ → LRU 淘汰策略 │
│ → CPU-GPU Swap 备选 │
│ │
│ 5. 与 FlashAttention 协同 │
│ → PagedAttention 管内存 │
│ → FlashAttention 管计算 │
│ → 两者结合达到最佳性能 │
│ │
└─────────────────────────────────────────────────────────────┘

11.2 调优清单#

KV Cache 优化检查清单:
□ 评估实际 max_seq_len 需求
□ 估算 KV Cache 内存需求
□ 设置合理的 gpu-memory-utilization (0.85-0.9)
□ 使用 block_size=16
□ 监控 vllm:num_available_blocks
□ 监控 cache hit rate
□ 识别可共享的系统提示前缀
□ 避免频繁的 eviction
□ 长序列场景考虑 CPU Swap
□ 定期检查 GPU 显存利用率

11.3 未来方向#

KV Cache 优化的未来:
1. 更智能的 Prefix 识别
→ 自动识别可共享的前缀
→ 跨请求的结构化共享
2. 分层 Cache
→ L1: GPU HBM
→ L2: CPU DRAM
→ L3: NVMe SSD
→ 自动冷热分层
3. 稀疏 KV Cache
→ 选择性保存重要的 KV
→ 丢弃不重要的 tokens
→ 近似 attention
4. 硬件协同设计
→ 专用的 KV Cache 硬件
→ Near-memory computing
→ 更高效的 memory hierarchy
推荐阅读
  1. PagedAttention Paper(Kwon et al., 2023)—— vLLM 核心论文
  2. FlashAttention Paper(Dao et al., 2022)—— 高效注意力
  3. vLLM Blog—— 官方博客与更新

参考资料#

  1. Kwon, W., et al. (2023). “Efficient Memory Management for Large Language Model Serving with PagedAttention.” SOSP.
  2. Dao, T., et al. (2022). “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.” NeurIPS.
  3. Dao, T. (2023). “FlashAttention-2: Faster Attention with Better Parallelism.” ICLR.
  4. Pope, R., et al. (2023). “Efficiently Scaling Transformer Inference.” MLSys.
  5. Huang, Y., et al. (2024). “Memory Efficient Transformers via Prefix Caching.” arXiv.
  6. Sheng, Y., et al. (2023). “S3: Increasing Serving Throughput via Elastic KV Cache.” MLSys.
  7. vLLM Project. “vLLM Documentation.” vllm.readthedocs.io.

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

KV Cache 优化与内存管理深度解析:PagedAttention 原理与实践
https://aiattnstudio.link/posts/kv-cache-paged-attention/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签