KV Cache是大模型推理中最关键的优化技术,也是最大的内存瓶颈。在70B模型、128K上下文的典型配置下,KV Cache占用超过40GB显存,远超模型权重本身。2026年,KV Cache管理已成为推理系统的核心竞争力。

1. KV Cache基础

1.1 为什么需要KV Cache

Transformer自回归生成时,每生成一个新token需要关注之前所有token的Key和Value。为了避免重复计算,将历史K,V缓存下来:

$$\text{KV Cache}_t = {(K_1, V_1), (K_2, V_2), …, (K_t, V_t)}$$

1.2 内存占用计算

KV Cache的显存占用公式:

$$\text{Memory}_{KV} = 2 \times L \times H \times S \times d_h \times P \times b$$

其中:

  • $L$:层数
  • $H$:注意力头数
  • $S$:序列长度
  • $d_h$:每头维度
  • $P$:精度字节(fp16=2)
  • $b$:batch size

以Llama-4-70B为例($L=80, H=64, d_h=128$),128K上下文、batch=1:

$$2 \times 80 \times 64 \times 131072 \times 128 \times 2 = 274\text{GB}$$

这个数字远超单GPU显存容量。

2. PagedAttention:分页KV Cache

2.1 核心思想

vLLM提出的PagedAttention借鉴了操作系统的虚拟内存管理,将KV Cache组织为固定大小的(block),按需分配:

传统分配(连续):
┌──────────────────────────────────┐
│  Sequence A (预分配max_len)      │  大量浪费
└──────────────────────────────────┘
┌──────────────────────────────────┐
│  Sequence B (预分配max_len)      │  大量浪费
└──────────────────────────────────┘

PagedAttention(分页):
┌─────┬─────┬─────┬─────┬─────┐
│ A-1 │ A-2 │ B-1 │ A-3 │ B-2 │  按需分配,无浪费
└─────┴─────┴─────┴─────┴─────┘

2.2 实现

class PagedKVCache:
    def __init__(self, num_blocks, block_size, num_heads, head_dim, num_layers):
        self.block_size = block_size  # 通常为16
        self.num_blocks = num_blocks
        self.free_blocks = list(range(num_blocks))
        self.block_tables = {}  # seq_id -> [block_ids]
        
        # 预分配连续显存
        self.k_cache = torch.zeros(
            num_layers, num_blocks, block_size, num_heads, head_dim
        )
        self.v_cache = torch.zeros_like(self.k_cache)
    
    def allocate(self, seq_id, seq_len):
        """为序列分配KV Cache块"""
        num_needed = (seq_len + self.block_size - 1) // self.block_size
        blocks = []
        for _ in range(num_needed):
            if not self.free_blocks:
                # 触发换出
                self._evict()
            blocks.append(self.free_blocks.pop())
        self.block_tables[seq_id] = blocks
    
    def append(self, seq_id, positions, new_k, new_v):
        """追加新的K,V到缓存"""
        blocks = self.block_tables[seq_id]
        for pos, k, v in zip(positions, new_k, new_v):
            block_idx = pos // self.block_size
            offset = pos % self.block_size
            cache_block = blocks[block_idx]
            self.k_cache[:, cache_block, offset] = k
            self.v_cache[:, cache_block, offset] = v

2.3 效果对比

方案显存利用率最大并发碎片率
连续分配20-40%
PagedAttention90-95%<5%

3. KV Cache压缩

3.1 量化压缩

将KV Cache从fp16量化到INT8或INT4:

class QuantizedKVCache:
    def __init__(self, num_bits=4):
        self.num_bits = num_bits
        if num_bits == 4:
            self.cache_k = None  # INT4 tensor
            self.cache_v = None
            self.scales_k = None  # per-channel scale
            self.scales_v = None
    
    def quantize(self, tensor):
        """在线量化KV Cache"""
        # 计算scale
        scale = tensor.abs().max(dim=-1, keepdim=True).values / (2**(self.num_bits-1) - 1)
        scale = scale.clamp(min=1e-8)
        
        # 量化
        quantized = torch.round(tensor / scale).clamp(
            -(2**(self.num_bits-1)), 2**(self.num_bits-1) - 1
        )
        return quantized.to(torch.int8), scale
    
    def dequantize(self, quantized, scale):
        return quantized.float() * scale

不同量化方案的效果:

方案压缩比质量损失延迟开销
FP16 (baseline)1x0%0%
INT8 (per-token)2x<0.5%+3%
INT4 (per-channel)4x~2%+5%
INT4 (per-token + 重要性)4x<1%+8%

3.2 淘汰策略

对于超长上下文,不可能缓存所有token的KV。需要智能淘汰低价值token:

H2O (Heavy-Hitter Oracle):基于注意力分数识别重要token

def h2o_eviction(cache_k, cache_v, attention_scores, keep_ratio=0.3):
    """保留注意力分数最高的token"""
    # attention_scores: [batch, heads, query_len, key_len]
    # 计算每个key token的累积注意力分数
    importance = attention_scores.sum(dim=(0, 1, 2))  # [key_len]
    
    # 选择top-k保留
    num_keep = int(len(importance) * keep_ratio)
    _, keep_indices = torch.topk(importance, num_keep)
    keep_indices = keep_indices.sort().values
    
    return cache_k[keep_indices], cache_v[keep_indices]

StreamingLLM:保留attention sink(前几个token)+ 滑动窗口

def streaming_llm_cache(cache_k, cache_v, window_size=4096, sink_size=4):
    """Attention sink + 滑动窗口策略"""
    # 保留前sink_size个token(attention sink)
    sink_k, sink_v = cache_k[:sink_size], cache_v[:sink_size]
    
    # 保留最近window_size个token
    window_k = cache_k[-window_size:]
    window_v = cache_v[-window_size:]
    
    return torch.cat([sink_k, window_k]), torch.cat([sink_v, window_v])

4. 跨请求KV Cache共享

4.1 Prefix Caching

多个请求共享相同前缀时(如system prompt),可以缓存前缀的KV:

class PrefixCacheManager:
    def __init__(self, max_cache_size=10 * 1e9):
        self.cache = {}  # hash -> (k_cache, v_cache)
        self.max_size = max_cache_size
        self.current_size = 0
    
    def get_or_compute(self, prefix_tokens, model):
        """获取或计算前缀的KV Cache"""
        prefix_hash = hash_tokens(prefix_tokens)
        
        if prefix_hash in self.cache:
            return self.cache[prefix_hash]
        
        # 计算KV Cache
        k_cache, v_cache = model.compute_kv(prefix_tokens)
        
        # 存入缓存
        cache_size = k_cache.nbytes + v_cache.nbytes
        if self.current_size + cache_size > self.max_size:
            self._evict(cache_size)
        
        self.cache[prefix_hash] = (k_cache, v_cache)
        self.current_size += cache_size
        return k_cache, v_cache

4.2 Radix Tree缓存

2026年的前沿方案使用Radix Tree组织共享前缀,实现细粒度的缓存复用:

Radix Tree结构:
            [System Prompt]
           /              \
    [User Query 1]    [User Query 2]
      /        \
  [Context A] [Context B]

每个节点缓存对应前缀的KV Cache,新请求只需计算不匹配的部分。

5. KV Cache Offloading

5.1 GPU-CPU分层缓存

对于超长上下文,将KV Cache分层存储:

class LayeredKVCache:
    def __init__(self, gpu_capacity, cpu_capacity):
        self.gpu_cache = GPUCache(gpu_capacity)  # 热数据
        self.cpu_cache = CPUCache(cpu_capacity)  # 温数据
        self.disk_cache = DiskCache()             # 冷数据
        
    def get(self, layer, position):
        # 优先从GPU获取
        if self.gpu_cache.contains(layer, position):
            return self.gpu_cache.get(layer, position)
        
        # 从CPU获取并预取到GPU
        if self.cpu_cache.contains(layer, position):
            value = self.cpu_cache.get(layer, position)
            self.gpu_cache.put(layer, position, value)
            return value
        
        # 从磁盘获取
        value = self.disk_cache.get(layer, position)
        self.cpu_cache.put(layer, position, value)
        self.gpu_cache.put(layer, position, value)
        return value
    
    def prefetch(self, positions):
        """预取即将访问的KV到GPU"""
        for pos in positions:
            if not self.gpu_cache.contains(pos):
                value = self.cpu_cache.get_async(pos)
                self.gpu_cache.put_async(pos, value)

5.2 性能数据

方案支持上下文吞吐量延迟
纯GPU32K100%1x
GPU+CPU Offload128K85%1.3x
三层(GPU/CPU/Disk)1M+60%2.1x
智能预取1M+80%1.5x

6. 2026年趋势

  1. 稀疏KV Cache:结合MoE思想,仅激活部分KV Cache
  2. 学习型压缩:使用小型网络学习最优KV Cache压缩策略
  3. 跨模型共享:不同模型间的KV Cache复用(需架构对齐)
  4. 硬件协同设计:专用KV Cache管理芯片

KV Cache管理已从单纯的工程优化发展为需要算法、系统和硬件协同设计的综合课题,是大模型推理效率提升的核心杠杆。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。