02 - 推理优化技术¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
📌 定位说明:本章侧重推理优化的算法原理与技术研究。 - 📖 推理优化的工程部署实践请参考 LLM 应用/12-推理优化
学习目标:理解并掌握大模型推理优化的核心技术,包括 KV Cache 、量化、连续批处理、投机解码等。
1. 推理 vs 训练¶
1.1 核心区别¶
| 方面 | 训练 | 推理 |
|---|---|---|
| 目标 | 更新权重,最小化损失 | 生成输出,最小化延迟 |
| 计算 | 前向 + 反向传播 | 只有前向传播 |
| 内存 | 参数 + 梯度 + 优化器状态 + 激活 | 主要是参数 + KV Cache |
| 批处理 | 固定 batch size | 动态批处理 |
| 精度 | FP32/FP16/BF16 | 可量化到 INT8/INT4 |
1.2 推理的性能指标¶
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¶
自回归生成的计算冗余:
生成第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
生成第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 的内存占用¶
假设:
- 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 的实现¶
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
标准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
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 为什么需要量化¶
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)¶
流程:
1. 训练好的FP32模型
2. 校准(可选):用少量数据确定缩放因子
3. 量化权重到INT8/INT4
4. 推理时反量化回FP16计算
优点:
- 简单快速
- 不需要重新训练
缺点:
- 可能损失精度
量化感知训练 (QAT)¶
流程:
1. 在训练时模拟量化
2. 前向传播:权重先量化再反量化
3. 反向传播:梯度传到原始权重
4. 模型学会适应量化误差
优点:
- 精度损失小
缺点:
- 需要重新训练
- 训练时间长
3.3 量化方法¶
对称量化¶
公式:
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]
非对称量化¶
公式:
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)¶
核心思想:
- 逐层量化
- 使用OBS (Optimal Brain Surgeon) 方法最小化量化误差
- 分组量化(如每128列一组),每组有自己的缩放因子
优点:
- 4-bit量化精度接近FP16
- 适合大模型离线量化
AWQ (Activation-aware Weight Quantization)¶
3.4 量化实现示例¶
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)¶
静态批处理:
├── 一个 batch 中所有序列必须等最长的完成
├── 序列 3 在第 2 次迭代后就完成了,但 GPU 仍在等待
├── 实现简单,但 GPU 利用率低
└── 适合离线批处理场景
批处理大小=3:
请求1: [生成中............] 完成时间: 100ms
请求2: [生成中..] 完成时间: 30ms ← 完成后GPU空闲等待
请求3: [生成中........] 完成时间: 60ms
问题:
- 请求2完成后,GPU空闲等待其他请求
- GPU利用率低
4.2 连续批处理(Continuous Batching / Iteration-level Scheduling)¶
核心思想(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 连续批处理实现¶
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¶
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 核心思想¶
问题:
- 大模型生成每个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,定义:
如果候选 token 为 \(y_t\),则接受概率为:
如果拒绝,不能直接从 \(p_t - q_t\) 采样,而是要从正部分归一化后的残差分布采样:
如果前 \(k\) 个候选全部被接受,还要再从 target model 的下一步分布中补采 1 个 token,这样整体采样分布才与目标模型完全一致。
5.3 算法实现¶
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)¶
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"
投机解码的加速比分析:
理论加速比 = 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 的内存浪费问题¶
传统部署中的三种内存浪费:
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 借鉴操作系统虚拟内存分页机制:
核心思想:将 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 的解码算法支持¶
1. Parallel Sampling(并行采样)
├── 同一 prompt 生成多个输出
├── 共享 prompt 的 KV Cache(仅存一份)
└── 输出部分使用写时复制(Copy-on-Write)
2. Beam Search(束搜索)
├── 候选者之间共享公共块
├── 引用计数跟踪块的使用者数量
└── 引用计数归零时释放块
3. Prefix Caching(前缀缓存)
├── 多个请求共享相同前缀(如系统提示)
├── 前缀的 KV Cache 只计算一次
└── 特别适合翻译、模板化任务
7. Flash Attention¶
7.1 标准注意力的问题¶
标准 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 的核心思想¶
核心思想:分块计算(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 演进¶
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)¶
8.2 张量并行 (TP)¶
张量并行:
├── 每层切分到多个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)¶
流水线并行:
├── 不同层在不同GPU
├── 像流水线一样处理
├── 通信量小(只在边界层通信)
├── 适合模型太大无法放入单卡
└── 需要微批次(micro-batch)来填充流水线气泡
9. 实践指南¶
9.1 选择合适的优化组合¶
场景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 性能测试¶
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