LLM 推理优化:KV Cache、FlashAttention 与投机解码

LLM 推理优化:KV Cache、FlashAttention 与投机解码

1. 引言

LLM 推理的核心瓶颈在于自回归解码------每个 token 都依赖前面所有 token 的 KV 值。本文将深入讲解三大推理优化技术:KV Cache 管理、FlashAttention 加速和投机解码(Speculative Decoding)。

推理性能瓶颈:

复制代码
LLM 推理分为两个阶段:
1. Prefill 阶段:处理全部输入 token(计算密集)
2. Decode 阶段:逐个生成输出 token(内存密集)

主要瓶颈:
- KV Cache 内存随序列长度线性增长
- 注意力计算的内存访问瓶颈(Memory-bound)
- 自回归解码无法并行

2. KV Cache 原理与优化

2.1 基础 KV Cache

python 复制代码
class KVCache:
    """基础 KV Cache 实现"""

    def __init__(self, num_layers, num_heads, head_dim, max_seq_len, batch_size=1):
        self.cache = {}
        for layer in range(num_layers):
            self.cache[layer] = {
                "k": torch.zeros(batch_size, num_heads, max_seq_len, head_dim),
                "v": torch.zeros(batch_size, num_heads, max_seq_len, head_dim),
            }
        self.seq_len = 0

    def update(self, layer, key, value):
        """更新 KV Cache"""
        self.cache[layer]["k"][:, :, self.seq_len:self.seq_len+key.size(2)] = key
        self.cache[layer]["v"][:, :, self.seq_len:self.seq_len+value.size(2)] = value

    def get(self, layer):
        """获取当前层的 KV"""
        return (
            self.cache[layer]["k"][:, :, :self.seq_len],
            self.cache[layer]["v"][:, :, :self.seq_len],
        )

2.2 GQA (Grouped Query Attention) KV Cache 压缩

python 复制代码
# GQA:多个 Query Head 共享一组 KV Head
# 8 个 Query Head → 2 个 KV Head(KV Cache 减少 4x)

class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, num_q_heads=32, num_kv_heads=8):
        super().__init__()
        self.num_q_heads = num_q_heads
        self.num_kv_heads = num_kv_heads
        self.num_groups = num_q_heads // num_kv_heads
        self.head_dim = d_model // num_q_heads

        self.q_proj = nn.Linear(d_model, num_q_heads * self.head_dim)
        self.k_proj = nn.Linear(d_model, num_kv_heads * self.head_dim)
        self.v_proj = nn.Linear(d_model, num_kv_heads * self.head_dim)
        self.o_proj = nn.Linear(d_model, d_model)

    def forward(self, x, kv_cache=None):
        B, L, _ = x.shape

        q = self.q_proj(x).view(B, L, self.num_q_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, L, self.num_kv_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, L, self.num_kv_heads, self.head_dim).transpose(1, 2)

        # 更新 KV Cache
        if kv_cache is not None:
            k_cache, v_cache = kv_cache
            k = torch.cat([k_cache, k], dim=2)
            v = torch.cat([v_cache, v], dim=2)

        # 扩展 KV 以匹配 Q 的头数
        k = k.repeat_interleave(self.num_groups, dim=1)
        v = v.repeat_interleave(self.num_groups, dim=1)

        # 注意力计算
        attn = (q @ k.transpose(-2, -1)) / (self.head_dim ** 0.5)
        attn = attn.softmax(dim=-1)
        out = (attn @ v).transpose(1, 2).reshape(B, L, -1)

        return self.o_proj(out), (k[:, :, -L:], v[:, :, -L:])

2.3 PagedAttention(vLLM)

python 复制代码
# PagedAttention 将 KV Cache 分为固定大小的块(page)
# 类似操作系统的虚拟内存分页机制

class PagedKVCache:
    """分页 KV Cache"""

    def __init__(self, num_blocks, block_size, num_heads, head_dim):
        self.block_size = block_size
        # 预分配物理块
        self.k_blocks = torch.zeros(num_blocks, num_heads, block_size, head_dim)
        self.v_blocks = torch.zeros(num_blocks, num_heads, block_size, head_dim)
        self.free_blocks = list(range(num_blocks))
        self.block_tables = {}  # seq_id -> [block_indices]

    def allocate(self, seq_id):
        """为序列分配新块"""
        block_idx = self.free_blocks.pop()
        if seq_id not in self.block_tables:
            self.block_tables[seq_id] = []
        self.block_tables[seq_id].append(block_idx)
        return block_idx

    def free(self, seq_id):
        """释放序列的所有块"""
        if seq_id in self.block_tables:
            self.free_blocks.extend(self.block_tables[seq_id])
            del self.block_tables[seq_id]

3. FlashAttention

3.1 原理

复制代码
标准注意力:
  S = Q @ K^T          → 需要存储 N×N 矩阵(显存 O(N²))
  P = softmax(S)
  O = P @ V

FlashAttention:
  将 Q, K, V 分为小块(tile)
  在 SRAM 中完成注意力计算
  通过在线 softmax 算法避免存储完整注意力矩阵
  显存 O(N),速度提升 2-4x

3.2 使用 FlashAttention

python 复制代码
# PyTorch 2.0+ 原生支持
import torch.nn.functional as F

# 方式1:使用 torch.nn.functional.scaled_dot_product_attention
output = F.scaled_dot_product_attention(
    query, key, value,
    attn_mask=None,
    is_causal=True,  # 因果注意力
)

# 方式2:使用 flash-attn 库
from flash_attn import flash_attn_func

output = flash_attn_func(
    query, key, value,
    causal=True,
    softmax_scale=1.0 / (head_dim ** 0.5),
)

# 方式3:在模型中启用
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    attn_implementation="flash_attention_2",  # 启用 FlashAttention
)

3.3 FlashAttention 性能对比

序列长度 标准注意力 FlashAttention v2
1K 8ms 4ms
4K 45ms 12ms
8K 180ms 25ms
16K OOM 52ms
32K OOM 108ms

4. 投机解码(Speculative Decoding)

4.1 原理

复制代码
核心思想:用小模型快速生成多个候选 token,大模型一次性验证

传统解码(7B模型,每步1个token):
  Step1 → t1 → Step2 → t2 → Step3 → t3 → Step4 → t4
  耗时:4 × 20ms = 80ms

投机解码(160M小模型生成 + 7B大模型验证):
  小模型快速生成: t1, t2, t3, t4 (4ms)
  大模型一次验证: [t1✓, t2✓, t3✗, t4-] (22ms)
  修正: t1, t2, t3', t4' (4ms)
  耗时:30ms(接受3个token)

4.2 实现代码

python 复制代码
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

class SpeculativeDecoder:
    """投机解码器"""

    def __init__(self, draft_model_id, target_model_id):
        self.tokenizer = AutoTokenizer.from_pretrained(target_model_id)

        # 小模型(Draft Model)
        self.draft_model = AutoModelForCausalLM.from_pretrained(
            draft_model_id, device_map="auto", torch_dtype=torch.float16
        )

        # 大模型(Target Model)
        self.target_model = AutoModelForCausalLM.from_pretrained(
            target_model_id, device_map="auto", torch_dtype=torch.float16
        )

    @torch.no_grad()
    def generate(self, prompt, max_tokens=256, gamma=5):
        """投机解码生成"""
        input_ids = self.tokenizer.encode(prompt, return_tensors="pt").to("cuda")
        generated = input_ids.clone()

        total_accepted = 0
        total_speculated = 0

        while generated.size(1) < max_tokens + input_ids.size(1):
            # 1. Draft Model 生成 gamma 个候选 token
            draft_tokens = []
            draft_probs = []
            current = generated.clone()

            for _ in range(gamma):
                draft_output = self.draft_model(current)
                draft_logits = draft_output.logits[:, -1, :]
                draft_prob = torch.softmax(draft_logits, dim=-1)
                token = torch.multinomial(draft_prob, 1)
                draft_tokens.append(token)
                draft_probs.append(draft_prob)
                current = torch.cat([current, token], dim=1)

            draft_tokens = torch.cat(draft_tokens, dim=1)  # (1, gamma)

            # 2. Target Model 一次性验证所有候选 token
            verify_input = torch.cat([generated, draft_tokens], dim=1)
            target_output = self.target_model(verify_input)
            target_logits = target_output.logits[:, -gamma-1:-1, :]
            target_probs = torch.softmax(target_logits, dim=-1)

            # 3. 逐个验证 token
            accepted = 0
            for i in range(gamma):
                token = draft_tokens[0, i].item()
                draft_p = draft_probs[i][0, token].item()
                target_p = target_probs[0, i, token].item()

                # 接受/拒绝采样
                if torch.rand(1).item() < min(1, target_p / max(draft_p, 1e-10)):
                    accepted += 1
                    generated = torch.cat([generated, draft_tokens[:, i:i+1]], dim=1)
                else:
                    # 从修正分布中采样
                    corrected = torch.clamp(target_probs[0, i] - draft_probs[i][0], min=0)
                    corrected = corrected / corrected.sum()
                    new_token = torch.multinomial(corrected, 1).unsqueeze(0)
                    generated = torch.cat([generated, new_token], dim=1)
                    break

            total_accepted += accepted
            total_speculated += gamma

            # 如果所有 token 都被接受,还可以加上 target model 的额外 token
            if accepted == gamma:
                extra_logits = target_output.logits[:, -1, :]
                extra_probs = torch.softmax(extra_logits, dim=-1)
                extra_token = torch.multinomial(extra_probs, 1)
                generated = torch.cat([generated, extra_token], dim=1)

            if generated[0, -1].item() == self.tokenizer.eos_token_id:
                break

        acceptance_rate = total_accepted / total_speculated if total_speculated > 0 else 0
        print(f"接受率: {acceptance_rate:.2%}")

        return self.tokenizer.decode(generated[0][input_ids.size(1):], skip_special_tokens=True)

4.3 投机解码性能

配置 速度提升 接受率
160M + 7B 2.0-2.5x 70-80%
1B + 7B 2.5-3.0x 80-90%
1B + 13B 2.0-2.5x 70-80%
7B + 70B 1.5-2.0x 60-70%

5. 量化推理优化

python 复制代码
# INT8 量化推理
from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_8bit=True,
    llm_int8_threshold=6.0,
)

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
)

6. vLLM 高性能推理

python 复制代码
from vllm import LLM, SamplingParams

# 初始化 vLLM 引擎
llm = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    tensor_parallel_size=1,       # GPU 数量
    gpu_memory_utilization=0.9,   # GPU 显存利用率
    max_model_len=4096,           # 最大序列长度
    dtype="half",                 # FP16
)

# 批量推理
prompts = ["Hello, how are you?", "What is AI?"]
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=256,
)

outputs = llm.generate(prompts, sampling_params)
for output in outputs:
    print(output.outputs[0].text)

7. 总结

LLM 推理优化的核心技术:

技术 优化目标 提升幅度
KV Cache 避免重复计算 基础必备
GQA KV Cache 内存 减少 4x
PagedAttention KV Cache 利用率 提升 2-4x
FlashAttention 注意力计算 加速 2-4x
投机解码 解码并行度 加速 2-3x
量化 模型大小和计算 加速 1.5-3x
vLLM 综合优化 吞吐提升 5-24x