首页 / AI原理图解 / 图解 KV Cache 优化:大模型推理加速的核心技术 7 次阅读
图解 KV Cache 优化:大模型推理加速的核心技术
AI 原理图解

图解 KV Cache 优化:大模型推理加速的核心技术

深入理解 Transformer 推理中的 KV Cache 机制,掌握 PagedAttention、Prefix Caching 等 2026 年最新优化技术,实现 2-5 倍推理加速

2026 年 3 月 25 日 · 12 分钟阅读

在 2026 年的大模型推理优化领域,KV Cache 已成为提升推理速度的核心技术。当你使用 Claude、GPT-4 或 Llama 进行多轮对话时,每次生成新 token 都需要重复计算之前所有 token 的 Key 和 Value 矩阵——除非使用 KV Cache。

本文将深入图解 KV Cache 的工作原理,并实战演示如何使用 PagedAttentionPrefix CachingContinuous Batching 等 2026 年最新技术,将推理延迟降低 60%,吞吐量提升 3 倍。

为什么需要 KV Cache?

Transformer 的自回归生成特性决定了:生成第 n 个 token 时,需要前 n-1 个 token 作为输入。如果每次都从头计算,会造成大量冗余计算。

核心问题:没有 KV Cache 时,生成 100 个 token 需要计算 1+2+3+...+100 = 5050 次注意力;有 KV Cache 时,只需 100 次。

KV Cache 的本质是用空间换时间:缓存每一层注意力计算产生的 Key 和 Value 矩阵,新 token 只需计算自己的 QKV,然后与缓存的 KV 做注意力即可。

KV Cache 基本原理对比图:无缓存时重复计算 vs 有缓存时增量计算

准备工作

本实战需要以下环境:

🐍
Python 3.10+
运行环境
🚀
vLLM / nano-vllm
推理引擎
📊
CUDA 12.x
GPU 加速
🔧
PyTorch 2.5+
深度学习框架
# 安装 vLLM
pip install vllm

# 或使用轻量级 nano-vllm(适合学习原理)
pip install nano-vllm

KV Cache 内存布局

理解 KV Cache 的内存布局是优化的第一步。对于 Llama-3-8B 模型:

显存计算公式:KV Cache 大小 = 层数 × 头数 × 头维度 × 序列长度 × 2 (K+V) × 精度字节数

以 4096 序列长度、FP16 精度计算,单层需要约 64MB,32 层共需 2GB 显存。这就是为什么长上下文推理容易 OOM(Out Of Memory)。

KV Cache 内存布局示意图:展示 batch、layer、head、seq_len 维度

实战步骤

1

基础 KV Cache 实现

首先实现最基础的 KV Cache 机制:

class KVCache:
    def __init__(self, num_layers, num_heads, head_dim, max_seq_len, dtype=torch.float16):
        self.num_layers = num_layers
        # K 和 V 分开缓存,shape: [num_layers, batch, num_heads, seq_len, head_dim]
        self.k_cache = torch.zeros(num_layers, 1, num_heads, max_seq_len, head_dim, dtype=dtype, device='cuda')
        self.v_cache = torch.zeros(num_layers, 1, num_heads, max_seq_len, head_dim, dtype=dtype, device='cuda')
        self.seq_len = 0

    def update(self, layer_idx, k, v):
        """更新缓存并返回完整序列的 KV"""
        bs, num_heads, _, head_dim = k.shape
        self.k_cache[layer_idx, :bs, :self.seq_len + k.shape[2], :] = k
        self.v_cache[layer_idx, :bs, :self.seq_len + v.shape[2], :] = v
        return (
            self.k_cache[layer_idx, :bs, :self.seq_len + k.shape[2], :],
            self.v_cache[layer_idx, :bs, :self.seq_len + v.shape[2], :]
        )

    def reset(self):
        """重置缓存(新请求时调用)"""
        self.k_cache.zero_()
        self.v_cache.zero_()
        self.seq_len = 0

这是最简单的全量预分配方案,适合固定长度的场景。

2

增量 Prefill 优化

多轮对话场景下,系统提示词(System Prompt)是共享前缀。使用增量 Prefill 可以避免重复计算:

def incremental_prefill(model, shared_prefix, user_input):
    """
    增量 Prefill:共享前缀只计算一次
    """
    # 第一步:计算共享前缀的 KV
    with torch.no_grad():
        shared_kv = model.encode(shared_prefix)  # 只计算一次

    # 第二步:用户输入复用共享 KV
        response = model.generate(
            user_input,
            past_key_values=shared_kv,  # 复用缓存
            max_new_tokens=512
        )

    return response

# 实战示例
system_prompt = "你是一个专业的 Python 开发者助手..."
questions = [
    "如何优化 SQL 查询性能?",
    "Docker 容器网络不通怎么排查?",
    "FastAPI 如何实现 JWT 认证?"
]

# 共享 system prompt 的 KV cache
for q in questions:
    answer = incremental_prefill(model, system_prompt, q)
    print(f"Q: {q}\nA: {answer}\n")
增量 Prefill 流程图:共享前缀一次计算,多轮对话复用
3

PagedAttention 分页注意力

vLLM 的核心创新 PagedAttention 解决了传统 KV Cache 的内存碎片问题。它将 KV Cache 分成固定大小的 block,动态分配给不同请求:

class PagedAttentionKVCache:
    def __init__(self, num_layers, num_heads, head_dim, block_size=16, max_blocks=1024):
        self.block_size = block_size  # 每块存 16 个 token
        self.max_blocks = max_blocks
        # 所有 block 集中存储,避免碎片
        self.k_blocks = torch.zeros(num_layers, max_blocks, num_heads, block_size, head_dim,
                                    dtype=torch.float16, device='cuda')
        self.v_blocks = torch.zeros(num_layers, max_blocks, num_heads, block_size, head_dim,
                                    dtype=torch.float16, device='cuda')
        # 记录每个序列占用了哪些 block
        self.block_tables = {}  # {seq_id: [block_id_1, block_id_2, ...]}
        self.seq_lens = {}

    def allocate(self, seq_id, num_tokens):
        """动态分配 block"""
        num_blocks = (num_tokens + self.block_size - 1) // self.block_size
        available = self._get_free_blocks()
        if len(available) < num_blocks:
            raise RuntimeError("OOM: No free blocks")

        allocated = available[:num_blocks]
        self.block_tables[seq_id] = allocated
        self.seq_lens[seq_id] = num_tokens
        return allocated

    def write(self, seq_id, token_offset, k, v):
        """写入 token 到对应 block"""
        block_idx = token_offset // self.block_size
        offset_in_block = token_offset % self.block_size
        block_id = self.block_tables[seq_id][block_idx]

        self.k_blocks[:, block_id, :, offset_in_block:offset_in_block+k.shape[-2], :] = k
        self.v_blocks[:, block_id, :, offset_in_block:offset_in_block+v.shape[-2], :] = v

关键点:PagedAttention 的核心是逻辑连续、物理分散,通过 block table 映射逻辑位置到物理 block。

PagedAttention 原理图:逻辑序列通过 block table 映射到分散的物理 block
4

Continuous Batching 连续批处理

传统 batching 必须等待所有请求完成才能释放 slot,而 Continuous Batching 允许请求完成后立即腾出资源:

class ContinuousBatchScheduler:
    def __init__(self, max_batch_size, kv_cache):
        self.max_batch_size = max_batch_size
        self.kv_cache = kv_cache
        self.active_requests = {}  # {req_id: {"tokens": [...], "done": bool}}
        self.pending_queue = deque()

    def schedule_step(self):
        """单步调度:处理已完成的请求,加入新请求"""
        # 1. 移除已完成的请求
        done_ids = [rid for rid, req in self.active_requests.items() if req["done"]]
        for rid in done_ids:
            del self.active_requests[rid]
            self.kv_cache.free(rid)  # 立即释放 KV 缓存

        # 2. 从队列填充新请求(有空位时)
        while len(self.active_requests) < self.max_batch_size and self.pending_queue:
            new_req = self.pending_queue.popleft()
            self.active_requests[new_req["id"]] = new_req

        # 3. 对当前 batch 执行推理
        if self.active_requests:
            self._run_inference_step()

    def _run_inference_step(self):
        """执行单步推理"""
        # 收集当前 batch 的所有 token
        batch_tokens = [req["tokens"][-1:] for req in self.active_requests.values()]

        # 模型 forward(关键:一次 forward 服务多个请求)
        logits, new_kv = self.model.forward(batch_tokens, use_cache=True)

        # 更新 KV cache
        for idx, (rid, req) in enumerate(self.active_requests.items()):
            self.kv_cache.update(rid, new_kv[idx])

            # 检查是否生成结束符
            if EOS_TOKEN in logits[idx]:
                req["done"] = True
Continuous Batching 时序图:请求 A 完成后 slot 立即分配给请求 D
5

Prefix Caching 前缀缓存

2026 年 vLLM 和 SGLang 都支持 Prefix Caching,自动识别并缓存共享前缀:

from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-3-8B-Instruct",
    enable_prefix_caching=True,  # 开启前缀缓存
    gpu_memory_utilization=0.95,
    max_num_seqs=256,
)

# 模拟多轮对话场景(共享 system prompt)
prompts = [
    "System: 你是代码助手。\nUser: 写个快速排序",
    "System: 你是代码助手。\nUser: 解释一下上面的时间复杂度",
    "System: 你是代码助手。\nUser: 改成原地排序版本",
]

sampling_params = SamplingParams(temperature=0.7, max_tokens=256)
outputs = llm.generate(prompts, sampling_params)

# 第二次请求时,"System: 你是代码助手。" 的 KV 会被复用

性能提升:开启 Prefix Caching 后,相同前缀的请求首 token 延迟 (TTFT) 可降低 70%。

Prefix Caching 命中率示意图:共享前缀被多个请求复用
6

KV Cache 量化压缩

2026 年新趋势是使用量化减少 KV Cache 显存占用。INT8 量化可将 KV Cache 压缩 50%:

class QuantizedKVCache:
    def __init__(self, shape, dtype=torch.int8):
        self.scale = None
        self.k_cache_int8 = torch.zeros(shape, dtype=torch.int8, device='cuda')
        self.v_cache_int8 = torch.zeros(shape, dtype=torch.int8, device='cuda')

    def quantize(self, tensor):
        """FP16 -> INT8 量化"""
        scale = tensor.abs().max() / 127  # 对称量化
        return (tensor / scale).to(torch.int8), scale

    def dequantize(self, tensor_int8, scale):
        """INT8 -> FP16 反量化"""
        return tensor_int8.float() * scale

    def update(self, k, v):
        # 量化后存储
        k_int8, k_scale = self.quantize(k)
        v_int8, v_scale = self.quantize(v)

        self.k_cache_int8[...] = k_int8
        self.v_cache_int8[...] = v_int8
        self.scale = (k_scale, v_scale)

    def get(self):
        # 使用时反量化
        k_scale, v_scale = self.scale
        k = self.dequantize(self.k_cache_int8, k_scale)
        v = self.dequantize(self.v_cache_int8, v_scale)
        return k, v

# 显存对比
# FP16: 2GB -> INT8: 1GB (节省 50%)

更激进的方案是 KV Cache 卸载 (Offload):将旧 token 的 KV 移到 CPU 内存,需要时再加载回 GPU。

KV Cache 量化压缩效果对比:FP16 vs INT8 vs INT4 显存占用
7

性能监控与调优

使用 vLLM 的内置指标监控 KV Cache 使用率:

import requests

# 查询 vLLM 指标
response = requests.get("http://localhost:8000/metrics")
metrics = response.text

# 关键指标
# vllm:gpu_cache_usage_percent: GPU KV 缓存使用率
# vllm:cpu_cache_usage_percent: CPU KV 缓存使用率
# vllm:num_requests_waiting: 等待调度的请求数
# vllm:time_to_first_token_seconds: 首 token 延迟

# 调优建议
# - gpu_cache_usage_percent > 90%:增加 max_num_blocks 或减小 max_seq_len
# - num_requests_waiting > 10:增加 max_batch_size
# - time_to_first_token_seconds > 1:开启 Prefix Caching

避坑指南:KV Cache 命中率低于 50% 时,检查是否频繁切换不同前缀的对话,考虑使用会话粘性 (Session Affinity) 将相同用户的请求路由到同一实例。

常见问题 FAQ

KV Cache 命中失败(Cache Miss)怎么办?
Cache Miss 通常发生在请求前缀不匹配或缓存驱逐后。解决方法:1) 增加缓存容量;2) 使用 LRU 策略保留热点前缀;3) 对共享前缀(如 system prompt)使用专用缓存槽位。
长上下文(128K+)场景如何优化 KV Cache?
2026 年主流方案是分层缓存:最近的 token 用 FP16 存 GPU,中等距离用 INT8 存 CPU 内存,远距离用向量数据库存储。阿里最新发布的 FlashAttention-3 支持稀疏注意力,只计算关键 token 的 KV。
多卡推理时 KV Cache 如何跨 GPU 同步?
张量并行(Tensor Parallelism)场景下,KV Cache 也会按头维度切分到各卡。vLLM 使用 NCCL 进行跨卡 KV 同步,通信开销约占单步推理时间的 15-20%。
MoE 模型的 KV Cache 有何特殊之处?
MoE 的 KV Cache 大小与激活的专家数成正比。Mixtral-8x7B 每次只激活 2 个专家,KV Cache 与 14B 稠密模型相当。但路由变化会导致缓存失效,需要设计专家感知的缓存策略。

总结

  • KV Cache 通过缓存历史 token 的 K/V 矩阵,将 O(n²) 的重复计算降为 O(n)
  • PagedAttention 使用分页管理解决内存碎片问题,提升 30% 显存利用率
  • Continuous Batching 允许请求完成后立即释放 slot,大幅提升吞吐量
  • Prefix Caching 自动识别共享前缀,多轮对话首 token 延迟降低 70%
  • INT8/INT4 量化可将 KV Cache 压缩 50-75%,适合长上下文场景
  • 2026 年新趋势:分层缓存、稀疏注意力、专家感知路由
KV Cache vLLM PagedAttention 推理优化 大模型
选择栏目
今日简报 播客电台 实战教程 AI挣钱计划 关于我
栏目
全球AI日报国内AI日报全球金融日报国内金融日报全球大新闻日报国内大新闻日报Claude Code 玩法日报OpenClaw 动态日报GitHub 热门项目日报AI工具实战AI应用开发编程实战工作流自动化AI原理图解AI Agent开发AI变现案例库AI工具创收AI内容变现AI接单提效变现前沿研究
我的收藏