跳转至

02 - 推理优化技术

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

📌 定位说明:本章侧重推理优化的算法原理与技术研究。 - 📖 推理优化的工程部署实践请参考 LLM 应用/12-推理优化

学习目标:理解并掌握大模型推理优化的核心技术,包括 KV Cache 、量化、连续批处理、投机解码等。


1. 推理 vs 训练

1.1 核心区别

方面 训练 推理
目标 更新权重,最小化损失 生成输出,最小化延迟
计算 前向 + 反向传播 只有前向传播
内存 参数 + 梯度 + 优化器状态 + 激活 主要是参数 + KV Cache
批处理 固定 batch size 动态批处理
精度 FP32/FP16/BF16 可量化到 INT8/INT4

1.2 推理的性能指标

Text Only
1. 吞吐量 (Throughput): tokens/second
   - 系统整体处理能力

2. 延迟 (Latency): ms/token
   - 单个请求响应时间
   - 首 token 延迟 (Time To First Token, TTFT)
   - 每 token 延迟 (Time Per Output Token, TPOT)

3. 显存占用
   - 模型权重
   - KV Cache
   - 激活值

2. KV Cache :解码加速的核心

2.1 为什么需要 KV Cache

自回归生成的计算冗余

Text Only
生成第1个token:
  输入: [<sos>]
  计算: Q, K, V for position 1

生成第2个token:
  输入: [<sos>, token_1]
  计算: Q, K, V for position 1, 2

生成第3个token:
  输入: [<sos>, token_1, token_2]
  计算: Q, K, V for position 1, 2, 3

问题:每次都要重新计算之前位置的K和V!

解决方案:缓存之前计算的 K 和 V

Text Only
生成第1个token:
  计算: K1, V1
  缓存: [K1], [V1]

生成第2个token:
  计算: K2, V2
  缓存: [K1, K2], [V1, V2]
  注意力: Q2与[K1, K2]计算

生成第3个token:
  计算: K3, V3
  缓存: [K1, K2, K3], [V1, V2, V3]
  注意力: Q3与[K1, K2, K3]计算

2.2 KV Cache 的内存占用

Text Only
假设:
- batch_size = b
- seq_len = s
- num_layers = l
- KV头数 = h_kv
- head_dim = d
- 每个元素占用 = bytes

每层KV Cache大小:
  K: b × h_kv × s × d × bytes
  V: b × h_kv × s × d × bytes
  总计: 2 × b × h_kv × s × d × bytes 字节

总KV Cache:
  2 × b × h_kv × s × d × l × bytes 字节

其中:
- 标准 MHA: h_kv = h
- MQA: h_kv = 1
- GQA: h_kv = 分组后的 KV 头数

示例 (LLaMA-7B):
  b=1, s=2048, l=32, h_kv=32, d=128, bytes=2 (FP16)
  KV Cache = 2 × 1 × 32 × 2048 × 128 × 32 × 2
           = 1,073,741,824 字节
           = 1 GB!

2.3 KV Cache 的实现

Python
class KVCache:
    """KV Cache实现"""

    def __init__(self, num_layers, batch_size, num_heads, max_seq_len, head_dim):
        self.num_layers = num_layers
        self.batch_size = batch_size
        self.num_heads = num_heads
        self.head_dim = head_dim

        # 预分配内存
        self.k_cache = torch.zeros(
            num_layers, batch_size, num_heads, max_seq_len, head_dim
        )
        self.v_cache = torch.zeros(
            num_layers, batch_size, num_heads, max_seq_len, head_dim
        )

        # 当前缓存长度
        self.seq_len = 0

    def update(self, layer_idx, new_k, new_v):
        """
        更新KV Cache

        Args:
            layer_idx: 层索引
            new_k: [batch, num_heads, new_seq_len, head_dim]
            new_v: [batch, num_heads, new_seq_len, head_dim]
        """
        new_seq_len = new_k.size(2)

        # 写入新值
        self.k_cache[layer_idx, :, :, self.seq_len:self.seq_len+new_seq_len] = new_k
        self.v_cache[layer_idx, :, :, self.seq_len:self.seq_len+new_seq_len] = new_v

        # 更新长度
        if layer_idx == 0:  # 只在第一层更新
            self.seq_len += new_seq_len

    def get(self, layer_idx):
        """获取当前层的KV Cache"""
        return (
            self.k_cache[layer_idx, :, :, :self.seq_len],
            self.v_cache[layer_idx, :, :, :self.seq_len]
        )

2.4 Multi-Query Attention (MQA) 和 Grouped-Query Attention (GQA)

问题:标准多头注意力中,每个头有自己的 K 和 V , KV Cache 占用大

MQA 解决方案:所有头共享同一组 K 和 V

Text Only
标准MHA:
  头1: Q1, K1, V1
  头2: Q2, K2, V2
  ...
  KV Cache: b × h × s × d × 2 × l × 2字节

MQA:
  头1: Q1, K_shared, V_shared
  头2: Q2, K_shared, V_shared
  ...
  KV Cache: b × 1 × s × d × 2 × l × 2字节

节省: h倍内存!

GQA 折中方案:头分成 g 组,每组共享 K 和 V

Text Only
GQA (g=4组,h=32头):
  组1 (头1-8): 共享K1, V1
  组2 (头9-16): 共享K2, V2
  组3 (头17-24): 共享K3, V3
  组4 (头25-32): 共享K4, V4

KV Cache: b × 4 × s × d × 2 × l × 2字节
节省: 8倍内存(相比MHA)

3. 量化 (Quantization)

3.1 为什么需要量化

Text Only
FP32模型: 7B × 4字节 = 28 GB
FP16模型: 7B × 2字节 = 14 GB
INT8模型: 7B × 1字节 = 7 GB
INT4模型: 7B × 0.5字节 = 3.5 GB

量化可以:
- 减少显存占用
- 提高推理速度(支持INT8/INT4的硬件)
- 降低功耗

3.2 量化类型

训练后量化 (PTQ)

Text Only
流程:
1. 训练好的FP32模型
2. 校准(可选):用少量数据确定缩放因子
3. 量化权重到INT8/INT4
4. 推理时反量化回FP16计算

优点:
- 简单快速
- 不需要重新训练

缺点:
- 可能损失精度

量化感知训练 (QAT)

Text Only
流程:
1. 在训练时模拟量化
2. 前向传播:权重先量化再反量化
3. 反向传播:梯度传到原始权重
4. 模型学会适应量化误差

优点:
- 精度损失小

缺点:
- 需要重新训练
- 训练时间长

3.3 量化方法

对称量化

Text Only
公式:
  scale = max(|W|) / 127  (INT8)
  W_quant = round(W / scale)
  W_dequant = W_quant × scale

示例:
  W = [-10, -5, 0, 5, 10]
  max_abs = 10
  scale = 10 / 127 ≈ 0.0787
  W_quant = [-127, -64, 0, 64, 127]

非对称量化

Text Only
公式:
  scale = (max(W) - min(W)) / 255
  zero_point = round(-min(W) / scale)
  W_quant = round(W / scale) + zero_point
  W_dequant = (W_quant - zero_point) × scale

适用:权重分布不对称时

GPTQ (Group-wise Post-Training Quantization)

Text Only
核心思想:
- 逐层量化
- 使用OBS (Optimal Brain Surgeon) 方法最小化量化误差
- 分组量化(如每128列一组),每组有自己的缩放因子

优点:
- 4-bit量化精度接近FP16
- 适合大模型离线量化

AWQ (Activation-aware Weight Quantization)

Text Only
核心思想:
- 不是所有权重都同样重要
- 根据激活值大小,保护重要的权重通道
- 对重要通道使用更高精度

优点:
- 比GPTQ更好的4-bit精度
- 推理速度更快(硬件友好)

3.4 量化实现示例

Python
import torch
import torch.nn as nn
import torch.nn.functional as F

def quantize_tensor(x, num_bits=8):
    """
    简单的对称量化(per-tensor)。

    说明:
    - 这是教学版实现,重点展示缩放和裁剪逻辑
    - 4-bit 这里仍用 int8 容器存储,方便演示;真实 4-bit 部署通常会做打包存储
    - 生产环境常见的是 per-channel / per-group 量化,而不是单一全局 scale

    Args:
        x: 输入张量
        num_bits: 量化位数

    Returns:
        x_quant: 量化后的张量
        scale: 缩放因子
    """
    if not 2 <= num_bits <= 8:
        raise ValueError("num_bits must be between 2 and 8")

    x = x.float()
    qmax = 2 ** (num_bits - 1) - 1
    qmin = -(2 ** (num_bits - 1))

    # 计算缩放因子
    max_val = x.abs().max()
    scale = (max_val / qmax).clamp_min(torch.finfo(x.dtype).eps)

    # 量化
    x_quant = torch.clamp(torch.round(x / scale), qmin, qmax)

    return x_quant.to(torch.int8), scale

def dequantize_tensor(x_quant, scale):
    """反量化"""
    return x_quant.float() * scale

class QuantizedLinear(nn.Module):
    """量化线性层"""

    def __init__(self, in_features, out_features, num_bits=8):
        super().__init__()  # super()调用父类方法
        self.in_features = in_features
        self.out_features = out_features
        self.num_bits = num_bits

        # 用int8作为教学示例的通用容器
        self.register_buffer(
            'weight_quant',
            torch.zeros(out_features, in_features, dtype=torch.int8)
        )
        self.register_buffer('weight_scale', torch.tensor(1.0, dtype=torch.float32))

        self.bias = nn.Parameter(torch.zeros(out_features))

    def forward(self, x):
        # 反量化权重
        weight = dequantize_tensor(self.weight_quant, self.weight_scale)

        # 正常计算
        return F.linear(x, weight, self.bias)

    @torch.no_grad()  # 禁用梯度计算,节省内存(推理时使用)
    def quantize(self, weight_fp32):
        """量化权重"""
        weight_quant, scale = quantize_tensor(weight_fp32, self.num_bits)
        self.weight_quant.copy_(weight_quant)
        self.weight_scale.copy_(scale)

4. 批处理策略:从静态到动态

📌 来源参考:本节内容综合自 vLLM 论文、Orca 论文及 Datawhale《LLM部署》教程。

4.1 静态批处理(Static Batching)

Text Only
静态批处理:
├── 一个 batch 中所有序列必须等最长的完成
├── 序列 3 在第 2 次迭代后就完成了,但 GPU 仍在等待
├── 实现简单,但 GPU 利用率低
└── 适合离线批处理场景

批处理大小=3:

请求1: [生成中............] 完成时间: 100ms
请求2: [生成中..]           完成时间: 30ms  ← 完成后GPU空闲等待
请求3: [生成中........]     完成时间: 60ms

问题:
- 请求2完成后,GPU空闲等待其他请求
- GPU利用率低

4.2 连续批处理(Continuous Batching / Iteration-level Scheduling)

Text Only
核心思想(Orca 论文提出):
- 迭代级调度(Iteration-level Scheduling)
- 当一个请求完成,立即加入新请求
- 保持GPU始终满载

时间线:
t=0:  [req1, req2, req3]
t=30: [req1, req3, req4]  (req2完成,加入req4)
t=60: [req1, req4, req5]  (req3完成,加入req5)
t=100:[req4, req5, req6]  (req1完成,加入req6)

关键优势:
├── 减少 TTFT(首 token 延迟)
├── 提高吞吐量
├── 更好的 GPU 资源利用
└── vLLM 采用此方案,相比 HuggingFace 提升 24x

4.3 连续批处理实现

Python
class ContinuousBatchingScheduler:
    """连续批处理调度器"""

    def __init__(self, max_batch_size, max_seq_len):
        self.max_batch_size = max_batch_size
        self.max_seq_len = max_seq_len
        self.active_requests = []
        self.waiting_queue = []

    def add_request(self, request):
        """添加新请求到等待队列"""
        self.waiting_queue.append(request)

    def schedule(self):
        """
        调度请求

        返回当前批次的请求列表
        """
        # 1. 移除已完成的请求
        self.active_requests = [r for r in self.active_requests if not r.is_done()]

        # 2. 从等待队列补充新请求
        while (len(self.active_requests) < self.max_batch_size and
               self.waiting_queue):
            request = self.waiting_queue.pop(0)
            self.active_requests.append(request)

        return self.active_requests

    def step(self):
        """执行一步生成"""
        batch = self.schedule()

        # 构建batch输入
        input_ids = [r.get_next_input() for r in batch]

        # 批量推理
        outputs = model.generate_batch(input_ids)

        # 分发结果
        for request, output in zip(batch, outputs):  # zip按位置配对多个可迭代对象
            request.append_token(output)

4.4 Chunked Prefill

Text Only
Chunked Prefill(Sarathi 论文提出):
├── 让 Prefill 和 Decode 在同一 batch 中执行
├── 通过增加计算密集的 Prefill 请求来充分利用 GPU
├── 平衡 Prefill(计算密集)和 Decode(内存密集)
└── 进一步提升 GPU 利用率

传统方式:
  Prefill 阶段: 需要大量计算 → GPU 满载
  Decode 阶段: 内存带宽瓶颈 → GPU 利用率低

Chunked Prefill:
  将长 prompt 分成多个 chunk,与 decode 请求混合执行
  → 每个 iteration 都有计算密集和内存密集的混合负载
  → GPU 利用率更均匀

5. 投机解码(Speculative Decoding)

📌 来源参考:本节内容综合自 Speculative Decoding 论文及 Datawhale《LLM部署》教程。

5.1 核心思想

Text Only
问题:
- 大模型生成每个token都很慢
- 但小模型生成很快(虽然质量差)

解决方案:
1. 用小模型(draft model)快速生成k个候选token
2. 用大模型(target model)一次性验证这k个token
3. 接受匹配的token,拒绝后重新采样

效果:
- 如果小模型质量尚可,大部分token会被接受
- 推理速度提升2-3倍
- 输出分布与目标模型完全一致(无损加速)

5.2 数学形式化

设当前前缀为 \(x\),draft model 依次提议 \(k\) 个 token:\(y_1, y_2, \dots, y_k\)
对第 \(t\) 个候选 token,定义:

\[ q_t(\cdot) = P_{\text{draft}}(\cdot \mid x, y_{<t}), \quad p_t(\cdot) = P_{\text{target}}(\cdot \mid x, y_{<t}) \]

如果候选 token 为 \(y_t\),则接受概率为:

\[ \alpha_t = \min \left(1, \frac{p_t(y_t)}{q_t(y_t)} \right) \]

如果拒绝,不能直接从 \(p_t - q_t\) 采样,而是要从正部分归一化后的残差分布采样:

\[ r_t(v) = \frac{[p_t(v) - q_t(v)]_+}{\sum_u [p_t(u) - q_t(u)]_+} \]

如果前 \(k\) 个候选全部被接受,还要再从 target model 的下一步分布中补采 1 个 token,这样整体采样分布才与目标模型完全一致。

5.3 算法实现

Python
import torch

def normalize_positive_part(p: torch.Tensor, q: torch.Tensor, eps: float = 1e-12) -> torch.Tensor:
    """
    将 [p - q]_+ 重新归一化为合法分布。
    p, q 均为 shape [vocab_size] 的概率向量。
    """
    diff = torch.clamp(p - q, min=0.0)
    z = diff.sum()
    if z.item() <= eps:
        return p / p.sum().clamp_min(eps)
    return diff / z

@torch.no_grad()
def speculative_accept_reject(draft_tokens, draft_probs, target_probs):
    """
    对一段 draft proposals 执行一次接受/拒绝步骤。

    Args:
        draft_tokens: 长度为 k 的候选 token 序列
        draft_probs: 长度为 k 的列表,第 t 项是 q_t(.)
        target_probs: 长度为 k+1 的列表,第 t 项是 p_t(.),
                      最后一项用于"全部接受后再补采一个 token"
    """
    accepted = []

    for step, draft_token in enumerate(draft_tokens):
        token_id = int(draft_token)
        q_t = draft_probs[step]
        p_t = target_probs[step]

        accept_prob = min(
            1.0,
            (p_t[token_id] / q_t[token_id].clamp_min(1e-12)).item(),
        )

        if torch.rand(()).item() < accept_prob:
            accepted.append(token_id)
            continue

        corrected_dist = normalize_positive_part(p_t, q_t)
        replacement = torch.multinomial(corrected_dist, num_samples=1).item()
        return accepted + [replacement], False

    # 如果 k 个候选都被接受,再从 target 的下一步分布补采 1 个 token
    extra_token = torch.multinomial(target_probs[len(draft_tokens)], num_samples=1).item()
    return accepted + [extra_token], True

def speculative_decoding_step(prefix_ids, draft_model, target_model, k=4):
    """
    教学版接口约定:
    - draft_model.propose(prefix_ids, k) -> (draft_tokens, draft_probs)
    - target_model.score(prefix_ids, draft_tokens) -> target_probs
      其中 target_probs 长度为 k+1
    """
    draft_tokens, draft_probs = draft_model.propose(prefix_ids, k)
    target_probs = target_model.score(prefix_ids, draft_tokens)
    return speculative_accept_reject(draft_tokens, draft_probs, target_probs)

5.4 投机解码实践(vLLM)

Python
from vllm import LLM, SamplingParams

# 使用 n-gram 投机解码(无需额外 draft model)
llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    tensor_parallel_size=1,
    speculative_model="[ngram]",       # 使用 n-gram 模型
    num_speculative_tokens=5,          # 预测 5 个 token
    ngram_prompt_lookup_max=4,         # 用前 4 个 token 预测下一个
)

sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
outputs = llm.generate(["The future of AI is"], sampling_params)

# 也可以使用小型 draft model
# speculative_model="Qwen/Qwen2.5-0.5B"

投机解码的加速比分析

Text Only
理论加速比 = 1 / (1 - acceptance_rate × k / (k + 1))

其中:
- acceptance_rate: 候选 token 的平均接受率
- k: 每次提议的候选 token 数

示例:
  acceptance_rate = 0.8, k = 5
  每次成功接受约 4 个 token,1 次前向传播产出 4-5 个 token
  加速比 ≈ 2-3x

关键因素:
├── draft model 与 target model 的分布越接近,接受率越高
├── n-gram 方法无需额外模型,但接受率较低
├── 小型 neural draft model 接受率高,但需要额外显存
└── 适合 batch_size=1 的低延迟场景

6. PagedAttention 与 KV Cache 内存管理

📌 来源参考:本节内容综合自 vLLM 论文及 Datawhale《LLM部署》教程。

6.1 传统 KV Cache 的内存浪费问题

Text Only
传统部署中的三种内存浪费:

1. 内部碎片(Internal Fragmentation)
   ├── 难以预测生成长度,过度预留最大长度
   └── 实际使用远小于预分配

2. 预留内存(Reserved)
   ├── 为未来使用预留,整个请求期间保留
   └── 大部分时间处于空闲状态

3. 外部碎片(External Fragmentation)
   ├── 不同请求需要不同大小的预分配
   └── 导致内存无法连续利用

示例:上下文长度 2048
├── 请求 A 实际使用 200 tokens → 浪费 1848 slots
├── 请求 B 实际使用 50 tokens → 浪费 1998 slots
└── 4 个请求 × 2048 = 8192 slots,实际仅用 ~1000

6.2 PagedAttention 核心思想

PagedAttention 借鉴操作系统虚拟内存分页机制:

Text Only
核心思想:将 KV Cache 分成固定大小的"块"(Block)

类比:
├── 操作系统页(Page) → KV Cache 块(Block)
├── 字节(Byte) → Token
├── 进程(Process) → 序列(Sequence)
└── 页表(Page Table) → 块表(Block Table)

Block 示例(每块 4 tokens):
┌──────────┬─────────────────────────────┐
│ Block 1  │ Four, Score, and, Seven     │ 完整使用
│ Block 2  │ years, ago, our, <空闲>     │ 内部碎片(仅最后一块)
│ Block 3  │ you, only, live, <空闲>     │ 内部碎片
│ Block 4  │ <空闲>, <空闲>, <空闲>, <空闲> │ 未使用,可立即回收
└──────────┴─────────────────────────────┘

优势:
├── 消除外部碎片
├── 内部碎片仅存在于最后一个块(约 4% 损耗)
├── 块可以非连续存储,灵活分配
└── 支持跨序列共享(Prefix Caching)

6.3 PagedAttention 的解码算法支持

Text Only
1. Parallel Sampling(并行采样)
   ├── 同一 prompt 生成多个输出
   ├── 共享 prompt 的 KV Cache(仅存一份)
   └── 输出部分使用写时复制(Copy-on-Write)

2. Beam Search(束搜索)
   ├── 候选者之间共享公共块
   ├── 引用计数跟踪块的使用者数量
   └── 引用计数归零时释放块

3. Prefix Caching(前缀缓存)
   ├── 多个请求共享相同前缀(如系统提示)
   ├── 前缀的 KV Cache 只计算一次
   └── 特别适合翻译、模板化任务

7. Flash Attention

7.1 标准注意力的问题

Text Only
标准 Attention 的计算流程:
  S = QK^T           → shape [batch, heads, seq_len, seq_len]  ← O(n²) 显存!
  P = softmax(S)     → shape [batch, heads, seq_len, seq_len]  ← O(n²) 显存!
  O = PV              → shape [batch, heads, seq_len, head_dim]

问题:
├── 注意力矩阵 S 和 P 需要 O(n²) 的 HBM(高带宽内存)存储
├── 对于 seq_len = 8192,每个 head 的 S 矩阵就需要 256MB (FP16)
├── 多 head × 多层 → 显存爆炸
└── 大量数据在 HBM 和 SRAM 之间搬运 → 速度瓶颈

7.2 Flash Attention 的核心思想

Text Only
核心思想:分块计算(Tiling),避免物化完整的 n×n 注意力矩阵

关键洞察:
├── HBM(高带宽内存): 容量大但速度慢(~2TB/s)
├── SRAM(片上缓存): 容量小但速度快(~19TB/s)
└── 通过分块让计算在 SRAM 中完成,减少 HBM 读写

分块算法(简化版):
1. 将 Q, K, V 按序列维度分成小块(如每块 64 或 128 tokens)
2. 在 SRAM 中计算一块 Q 与一块 K 的注意力
3. 使用 online softmax 技巧,逐块累积结果
4. 最终输出与标准注意力数学等价

效果:
├── 显存占用从 O(n²) 降到 O(n)(不再存储完整注意力矩阵)
├── 算术复杂度仍为 O(n²d),但 HBM 读写减少约 5-10x
├── 实际速度提升 2-4x
└── 支持更长序列(128K+)

7.3 Flash Attention 演进

Text Only
Flash Attention 1 (2022):
├── 分块计算 + online softmax
├── 显存 O(n²) → O(n)
└── 速度提升 2-4x

Flash Attention 2 (2023):
├── 优化并行策略(按序列长度而非 batch/heads 分配线程)
├── 减少非矩阵乘法操作(利用 Tensor Core)
├── 速度比 FA1 快 2x
└── 接近理论最优的注意力速度

Flash Attention 3 (2024):
├── 针对 H100 GPU 优化
├── 利用异步 + FP8 Tensor Core
├── 在 H100 上达到 1.5-2x 于 FA2 的速度
└── 主要受益于新硬件特性

使用方式:
  # PyTorch 2.0+ 内置支持
  from torch.nn.functional import scaled_dot_product_attention
  # 自动选择最优实现(Flash Attention / Memory-Efficient / 标准)

8. 模型并行

8.1 数据并行 (DP)

Text Only
数据并行:
├── 每个GPU有完整模型副本
├── 不同GPU处理不同batch
├── 梯度同步后平均
└── 适合模型能放入单卡的情况

8.2 张量并行 (TP)

Text Only
张量并行:
├── 每层切分到多个GPU
├── 每GPU只存储部分权重
├── 通信量大(每层都需要 all-reduce)
├── 适合单机多卡(NVLink 高带宽)
└── 常见配置: TP=2, 4, 8

示例 (MLP层):
  GPU1: W1_left,  计算 x @ W1_left  → h1
  GPU2: W1_right, 计算 x @ W1_right → h2
  All-Reduce: h = h1 + h2(需要通信)

8.3 流水线并行 (PP)

Text Only
流水线并行:
├── 不同层在不同GPU
├── 像流水线一样处理
├── 通信量小(只在边界层通信)
├── 适合模型太大无法放入单卡
└── 需要微批次(micro-batch)来填充流水线气泡

9. 实践指南

9.1 选择合适的优化组合

Text Only
场景1: 单卡推理,追求最低延迟
  → FP16 + KV Cache + Flash Attention

场景2: 单卡推理,追求最大吞吐
  → INT8/INT4量化 + Continuous Batching

场景3: 多卡推理,大模型
  → 张量并行 + Pipeline并行 + KV Cache

场景4: 服务化部署
  → vLLM (PagedAttention + Continuous Batching + Prefix Caching)

场景5: 极致速度
  → 投机解码 + 量化 + Flash Attention + Continuous Batching

9.2 性能测试

Python
def benchmark(model, tokenizer, prompts, max_tokens=100):
    """
    基准测试
    """
    import time

    total_tokens = 0
    total_time = 0

    for prompt in prompts:
        inputs = tokenizer(prompt, return_tensors="pt")

        start = time.time()
        outputs = model.generate(**inputs, max_new_tokens=max_tokens)
        end = time.time()

        num_tokens = outputs.shape[1] - inputs.input_ids.shape[1]
        total_tokens += num_tokens
        total_time += end - start

    throughput = total_tokens / total_time
    latency = total_time / len(prompts)

    print(f"吞吐量: {throughput:.2f} tokens/s")
    print(f"平均延迟: {latency:.2f} s")
    print(f"每token延迟: {total_time/total_tokens*1000:.2f} ms")

10. 🤔 思考题

💡 思考题参考解答 **Q1: 为什么 KV Cache 只缓存 K 和 V,不缓存 Q?** 在自回归解码中,每一步只生成一个新 token。该新 token 成为当前步的 Q,而之前所有 token 的 K 和 V 需要与这个 Q 做注意力计算。因此: - Q 只需要当前 token 的表示,不需要缓存 - K 和 V 需要之前所有 token 的表示,避免重复计算,所以必须缓存 **Q2: INT4 量化一定会损失模型精度吗?有没有不损失精度的量化方案?** INT4 量化通常会带来一定的精度损失,但损失程度取决于: - 模型规模:越大模型对量化越鲁棒(7B 的 INT4 损失 > 70B 的 INT4 损失) - 量化方法:GPTQ/AWQ 通过校准数据集最小化重建误差,损失较小 - 量化粒度:per-group 量化比 per-channel 更精细 对于某些任务,INT4 量化后的模型甚至可能表现更好(量化起到正则化作用),但通常在复杂推理任务上会有 1-3% 的性能下降。 **Q3: 投机解码为什么能保证输出分布与原始模型完全一致?** 投机解码使用**拒绝采样(rejection sampling)**机制: 1. Draft model 生成 k 个候选 token 2. Target model 并行验证这些 token 3. 对于每个 token,以 $\min(1, p_{target}(x)/p_{draft}(x))$ 的概率接受 4. 如果某个 token 被拒绝,从调整后的分布中重新采样 数学上可以证明,这种接受-拒绝机制保证了最终采样分布与直接从 target model 采样完全一致(在温度采样下)。 **Q4: 在什么场景下应该优先选择 PagedAttention 而非 FlashAttention?** 两者解决不同问题,可以同时使用: - **FlashAttention**:优化单次注意力计算的速度和显存(算法级优化) - **PagedAttention**:优化多个请求的 KV Cache 内存管理(系统级优化) 优先选择 PagedAttention 的场景:多用户并发推理服务(如 API 服务),KV Cache 内存碎片严重。实际上 vLLM 同时使用了两者。 **Q5: 为什么连续批处理比静态批处理更适合 LLM 推理服务?** LLM 推理的输出长度差异很大(50 tokens vs 2000 tokens)。静态批处理中,短序列完成后必须等待最长序列完成,造成严重的计算浪费(padding 浪费)。连续批处理在 iteration 级别调度,序列完成后立即移出并加入新请求,GPU 利用率从 30-50% 提升到 90%+。

11. 下一步

完成本节后,你应该: - [x] 理解 KV Cache 的原理和实现 - [x] 理解量化的原理和方法(对称/非对称/GPTQ/AWQ) - [x] 了解 PagedAttention 的分页内存管理 - [x] 了解从静态批处理到连续批处理、Chunked Prefill 的演进 - [x] 掌握投机解码的数学原理和实践方法 - [x] 理解 Flash Attention 的分块计算思想和演进 - [x] 能够选择合适的优化策略组合

下一步03-大模型预训练 - 学习大模型预训练的目标、数据管线和分布式训练


最后更新日期: 2026-04-21 适用版本: LLM 学习教程 v2026