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 内存
其中 是层数, 是 KV head 数, 是 head 维度, 是字节数(FP16=2), 是最大序列长度。
10.2 Block 利用率
其中 是实际 token 数, 是分配的 blocks 数, 是 block size。
10.3 缓存命中率
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推荐阅读
- PagedAttention Paper(Kwon et al., 2023)—— vLLM 核心论文
- FlashAttention Paper(Dao et al., 2022)—— 高效注意力
- vLLM Blog—— 官方博客与更新
参考资料
- Kwon, W., et al. (2023). “Efficient Memory Management for Large Language Model Serving with PagedAttention.” SOSP.
- Dao, T., et al. (2022). “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.” NeurIPS.
- Dao, T. (2023). “FlashAttention-2: Faster Attention with Better Parallelism.” ICLR.
- Pope, R., et al. (2023). “Efficiently Scaling Transformer Inference.” MLSys.
- Huang, Y., et al. (2024). “Memory Efficient Transformers via Prefix Caching.” arXiv.
- Sheng, Y., et al. (2023). “S3: Increasing Serving Throughput via Elastic KV Cache.” MLSys.
- vLLM Project. “vLLM Documentation.” vllm.readthedocs.io.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
KV Cache 优化与内存管理深度解析:PagedAttention 原理与实践
https://aiattnstudio.link/posts/kv-cache-paged-attention/
