LLM 量化方法原理与实现详解:GPTQ、AWQ、SmoothQuant、FP8
全面解析 LLM 量化:数据类型基础、对称/非对称量化数学、GPTQ Hessian 误差补偿、AWQ 激活感知缩放、SmoothQuant 激活平滑、FP8 与 GGUF 量化体系。
1. 量化在做什么
量化的核心:把模型权重(有时也包括激活值)从高精度转换为低精度,减少显存、降低计算量、提高推理吞吐。代价是精度损失。
FP16 权重: 每个参数 2 字节 → 7B 模型 ≈ 14 GB
INT8 权重: 每个参数 1 字节 → 7B 模型 ≈ 7 GB
INT4 权重: 每个参数 0.5 字节 → 7B 模型 ≈ 3.5 GB
本质:在"精度损失"和"效率提升"之间找平衡。
2. 数据类型基础
2.1 浮点数表示
FP32 (32-bit): 1位符号 + 8位指数 + 23位尾数 → 精度最高,最慢
FP16 (16-bit): 1位符号 + 5位指数 + 10位尾数 → 范围 ±65504
BF16 (16-bit): 1位符号 + 8位指数 + 7位尾数 → 范围同 FP32,精度低于 FP16
FP8 E4M3: 1位符号 + 4位指数 + 3位尾数 → 范围 ±448
FP8 E5M2: 1位符号 + 5位指数 + 2位尾数 → 范围 ±57344,精度更低
BF16 vs FP16:
BF16: 指数位多 → 表示范围大 → 不容易溢出 → 训练更稳定
FP16: 尾数位多 → 精度更高 → 但容易溢出
LLM 训练: 通常用 BF16(不怕溢出)
LLM 推理: FP16 或 BF16 均可
2.2 整数量化的数学
将浮点数 $x$ 映射为整数 $x_q$:
对称量化(Symmetric):
$$x_q = \text{round}\left(\frac{x}{s}\right), \quad s = \frac{\max(|x|)}{2^{b-1} - 1}$$
$$x \approx x_q \times s$$
# 对称量化: 以 0 为中心,scale 对称
def symmetric_quantize(x, bits=8):
qmax = 2 ** (bits - 1) - 1 # INT8: 127
scale = x.abs().max() / qmax # scale = max(|x|) / 127
x_q = torch.round(x / scale).clamp(-qmax, qmax).to(torch.int8)
return x_q, scale
def symmetric_dequantize(x_q, scale):
return x_q.float() * scale
非对称量化(Asymmetric):
$$x_q = \text{round}\left(\frac{x - z}{s}\right), \quad s = \frac{\max(x) - \min(x)}{2^b - 1}, \quad z = \min(x)$$
$$x \approx x_q \times s + z$$
# 非对称量化: 有 zero_point,能更好地利用量化范围
def asymmetric_quantize(x, bits=8):
qmin, qmax = 0, 2 ** bits - 1 # INT8 unsigned: 0~255
x_min, x_max = x.min(), x.max()
scale = (x_max - x_min) / (qmax - qmin)
zero_point = torch.round(-x_min / scale).clamp(qmin, qmax)
x_q = torch.round(x / scale + zero_point).clamp(qmin, qmax).to(torch.uint8)
return x_q, scale, zero_point
def asymmetric_dequantize(x_q, scale, zero_point):
return (x_q.float() - zero_point) * scale
2.3 Per-tensor vs Per-channel vs Per-group
量化粒度决定精度和开销的平衡:
Per-tensor: 整个权重矩阵共用一个 scale
scale 数量: 1
精度: 最低(一个 outlier 影响整个矩阵)
开销: 最小
Per-channel: 每个输出通道一个 scale
scale 数量: output_channels
精度: 中等
开销: 中等
Per-group: 每 g 个元素一个 scale(如 g=128)
scale 数量: numel / g
精度: 最高
开销: 最大(需要存储更多 scale)
# Per-group 量化 (group_size=128)
def per_group_quantize(weight, bits=4, group_size=128):
"""
weight: (out_features, in_features)
将 in_features 维度按 group_size 分组,每组独立量化
"""
out_f, in_f = weight.shape
assert in_f % group_size == 0
# 重塑为 (out_features, num_groups, group_size)
w = weight.reshape(out_f, -1, group_size)
qmax = 2 ** (bits - 1) - 1
# 每组一个 scale: (out_features, num_groups, 1)
scales = w.abs().amax(dim=-1, keepdim=True) / qmax
# 量化
w_q = torch.round(w / scales).clamp(-qmax, qmax).to(torch.int8)
return w_q, scales.squeeze(-1)
3. 按量化时机分类
3.1 训练后量化(PTQ, Post-Training Quantization)
拿到已训练好的模型直接量化,不需要重新训练。
流程:
训练好的 FP16 模型
↓
准备少量校准数据 (通常 128~512 条)
↓
运行量化算法 (GPTQ/AWQ/SmoothQuant)
↓
得到量化模型
优势:成本低,几小时内完成 劣势:低 bit(如 2-bit)精度损失明显
3.2 量化感知训练(QAT, Quantization-Aware Training)
训练过程中模拟量化误差,让模型学会适应低精度:
# QAT 的核心: Fake Quantization(伪量化)
def fake_quantize(x, scale, bits=8):
"""前向: 模拟量化误差; 反向: 直通估计器 (STE)"""
qmax = 2 ** (bits - 1) - 1
x_q = torch.round(x / scale).clamp(-qmax, qmax)
x_deq = x_q * scale # 反量化回浮点
return x_deq # 带上量化噪声的浮点值
# 训练时:
# forward: W_fake = fake_quantize(W) → 输出带量化噪声
# backward: 梯度直通 (STE),∂L/∂W ≈ ∂L/∂W_fake
# update: W = W - lr * ∂L/∂W → W 仍是浮点
优势:低 bit 下精度保留好 劣势:需要完整训练流程,大模型成本极高
4. 主流 PTQ 方法详解
4.1 GPTQ
最早被广泛使用的大模型量化方法,基于 OBQ(Optimal Brain Quantization) 改进。
核心思路:逐列量化权重,每量化一列后用 Hessian 信息补偿剩余列的误差。
数学原理:
量化目标是最小化输出误差:
$$\min_{\hat{W}} | WX - \hat{W}X |^2_F$$
对于逐列量化,第 $j$ 列的最优量化值和误差补偿为:
$$\hat{w}j = \text{quant}(w_j)$$ $$\delta_j = \frac{w_j - \hat{w}j}{H{jj}^{-1}}$$ $$W{:, j+1:} \mathrel{+}= \delta_j \cdot H_{j, j+1:}^{-1}$$
其中 $H = 2X X^T$ 是 Hessian 矩阵。
# GPTQ 简化伪代码
def gptq_quantize(W, X_calib, bits=4, group_size=128):
"""
W: (out_features, in_features) 原始权重
X_calib: 校准数据的激活值
"""
H = 2 * X_calib @ X_calib.T # Hessian 矩阵 (in_f, in_f)
H_inv = torch.inverse(H) # Hessian 逆(实际用 Cholesky 分解)
W_q = W.clone()
scales = []
# 逐列(或逐 block)量化
for j in range(0, in_features, block_size):
# 1. 量化当前 block
block = W_q[:, j:j+block_size]
scale = block.abs().max() / (2**(bits-1) - 1)
block_q = torch.round(block / scale).clamp(...)
# 2. 计算量化误差
quant_error = block - block_q * scale
# 3. 用 Hessian 逆补偿剩余列
W_q[:, j+block_size:] += quant_error @ H_inv[j:j+block_size, j+block_size:]
W_q[:, j:j+block_size] = block_q
scales.append(scale)
return W_q, scales
特点:
- 精度好(有误差补偿)
- 速度中等(需要 Hessian 计算)
- 支持 INT4/INT3/INT2
- 代表工具:AutoGPTQ
4.2 AWQ(Activation-Aware Weight Quantization)
核心观察:权重中有少数"重要通道",这些通道对应的激活值特别大。保护这些重要通道可以大幅减少量化损失。
核心思路:不直接保护重要权重(那会让格式不均匀),而是对权重乘一个 scale 因子,让重要通道的有效精度更高。
原始: W = [..., 0.5, 100.0, 0.3, ...] ← 100.0 是重要权重(对应激活大)
直接 INT4 量化 → 100.0 误差很大
AWQ: 先乘 scale → [..., 0.5/s₁, 100.0×s₂, 0.3/s₃, ...]
量化后 → 100.0×s₂ 的有效精度更高
推理时激活除以对应 scale → 结果等价
数学原理:
对于权重 $w$ 和对应的激活值 $x$,寻找 per-channel 缩放因子 $s$:
$$\min_s | Q(w \cdot s) \cdot (x / s) - w \cdot x |$$
最优 $s$ 与激活幅度正相关:
$$s_j = \left(\frac{\max(|X_j|)}{\max(|W_j|)}\right)^\alpha, \quad \alpha \in [0, 1]$$
# AWQ 简化伪代码
def awq_quantize(W, X_calib, bits=4, group_size=128, alpha=0.5):
"""
W: (out_features, in_features)
X_calib: (num_samples, in_features) 校准数据激活值
alpha: 平衡因子,通常 grid search 找最优 (0~1)
"""
# 1. 计算激活值的 per-channel 重要性
act_scales = X_calib.abs().mean(dim=0) # (in_features,)
# 2. 计算 per-channel 缩放因子
w_scales = W.abs().mean(dim=0) # (in_features,)
s = (act_scales / w_scales).pow(alpha) # 重要通道 s 大
# 3. 对权重应用 scale
W_scaled = W * s.unsqueeze(0) # 重要通道放大
# 4. 量化 scaled 权重
W_q, q_scales = per_group_quantize(W_scaled, bits, group_size)
# 5. 推理时: output = W_q @ (X / s)
# scale 融合到前一层的 LayerNorm 或 linear 中,零开销
return W_q, q_scales, s
特点:
- 速度快(不需要 Hessian)
- 精度好(保护重要通道)
- 推理时 scale 可融合,零额外开销
- 代表工具:AutoAWQ
4.3 SmoothQuant
目标:同时量化权重和激活值(W8A8),最大化推理加速。
核心问题:激活值中存在 outlier(极端值),直接量化激活会产生巨大误差。
核心思路:把激活中的"难量化部分"转移到权重中(权重更容易量化)。
原始:
激活 X: [..., 0.1, 0.2, 100.0, 0.3, ...] ← 100.0 是 outlier,难量化
权重 W: [..., 0.5, 0.3, 0.01, 0.4, ...] ← 分布均匀,好量化
SmoothQuant 变换:
X' = X / s → [..., 0.1, 0.2, 1.0, 0.3, ...] ← outlier 被平滑
W' = W * s → [..., 0.5, 0.3, 1.0, 0.4, ...] ← 吸收了激活的 scale
(保证 X'W' = XW,数学等价)
scale s 的选取: s_j = max(|X_j|)^α / max(|W_j|)^(1-α), α=0.5
$$Y = XW = (X \text{diag}(s)^{-1}) \cdot (\text{diag}(s) W) = X'W'$$
# SmoothQuant 简化伪代码
def smooth_quant(W, X_calib, alpha=0.5):
"""
将激活的量化难度转移到权重
"""
# 1. 统计激活的 per-channel 最大值
act_max = X_calib.abs().amax(dim=0) # (in_features,)
# 2. 统计权重的 per-channel 最大值
w_max = W.abs().amax(dim=0) # (in_features,)
# 3. 计算平滑因子
s = (act_max.pow(alpha) / w_max.pow(1 - alpha)).clamp(min=1e-5)
# 4. 平滑变换
W_smooth = W * s.unsqueeze(0) # 权重吸收 scale
# 推理时: X_smooth = X / s # 激活除以 scale
# 5. 对 W_smooth 和 X_smooth 分别做 INT8 对称量化
W_q, w_scale = symmetric_quantize(W_smooth, bits=8)
# X 在推理时动态量化
return W_q, w_scale, s
特点:
- W8A8:权重和激活都量化为 INT8
- 可以用 INT8 GEMM(如 cuBLAS 的
cublasGemmEx),理论 2x 加速 - 精度损失极小
- TensorRT-LLM 原生支持
4.4 GPTQ vs AWQ vs SmoothQuant 对比
| 方法 | 量化位宽 | 量化对象 | 核心技术 | 速度 | 精度 |
|---|---|---|---|---|---|
| GPTQ | W4/W3/W2 | 仅权重 | Hessian 误差补偿 | 中 | 好 |
| AWQ | W4 | 仅权重 | 激活感知缩放 | 快 | 好 |
| SmoothQuant | W8A8 | 权重+激活 | 激活平滑转移 | 最快推理 | 最好 |
4.5 QuIP / QuIP#
核心思路:对权重做随机正交变换,让分布更"均匀",消除维度间相关性。
原始权重: 某些维度范围大、某些小 → 量化不均匀
QuIP 变换:
W' = U W V^T (U, V 是随机正交矩阵)
W' 的各维度分布更均匀 → 量化更容易
推理: Y = XW = X(U^T W' V) = (XU^T) W' V
U^T 和 V 可以预计算或融合
QuIP# 进一步引入**格码(lattice codebook)**做向量量化,能做到 2-bit 量化还保持不错精度。
4.6 FP8 量化
不同于整数量化,FP8 保留了浮点的指数结构:
FP8 E4M3: 1位符号 + 4位指数 + 3位尾数
范围: ±448
精度: 约 3-4 位有效数字
用途: 权重和激活(DeepSeek-V3 使用)
FP8 E5M2: 1位符号 + 5位指数 + 2位尾数
范围: ±57344
精度: 约 2-3 位有效数字
用途: 梯度(训练时)
# FP8 量化相对简单:直接类型转换 + scale
def fp8_quantize(x, dtype=torch.float8_e4m3fn):
scale = x.abs().max() / torch.finfo(dtype).max
x_fp8 = (x / scale).to(dtype)
return x_fp8, scale
优势:
- 硬件原生支持(NVIDIA H100 的 FP8 Tensor Core)
- 比 INT8 更简单(不需要 zero_point)
- 精度损失极小
5. GGUF / llama.cpp 量化体系
面向 CPU 推理的量化方案,在消费级硬件上跑 LLM 的事实标准。
5.1 分 block 量化
将权重分成小块(如 32 个一组),每块有独立的 scale 和 zero_point:
权重: [w0, w1, w2, ..., w31 | w32, w33, ..., w63 | ...]
├── block 0 ──────────┤ ├── block 1 ──────────┤
Block 0: scale_0, zero_0, [q0, q1, ..., q31] (每个 qi 是 4-bit)
Block 1: scale_1, zero_1, [q32, q33, ..., q63]
5.2 常见量化类型
| 类型 | 精度 | 说明 | 推荐场景 |
|---|---|---|---|
| Q2_K | ~2.6 bit | 极限压缩 | 显存极其有限 |
| Q3_K_M | ~3.4 bit | 低精度但可用 | 小模型 |
| Q4_K_M | ~4.8 bit | 精度/大小平衡 | 最常用 |
| Q5_K_M | ~5.5 bit | 接近原始精度 | 追求质量 |
| Q6_K | ~6.6 bit | 几乎无损 | 不差显存时 |
| Q8_0 | 8 bit | 基本无损 | 基线对比 |
5.3 K-quant(重要性感知)
带 "K" 的变体根据层的重要性分配不同精度:
Q4_K_M:
第一层和最后一层 attention: 使用 Q6_K (更高精度)
中间的 FFN 层: 使用 Q4_K (标准精度)
原理: 首尾层对模型质量影响更大,给更多 bit
6. 按量化精度分类总结
6.1 INT8 (W8A8 / W8A16)
精度损失: 极小,几乎无感
显存节省: 2x (vs FP16)
推理加速: W8A8 可用 INT8 GEMM → 理论 2x
代表方法: SmoothQuant, LLM.int8()
硬件要求: 几乎所有 GPU 支持
6.2 INT4 (W4A16)
精度损失: 小到中等(取决于方法)
显存节省: 4x (vs FP16)
推理加速: 受限于 dequant 开销,实际 ~1.5-2x
代表方法: GPTQ, AWQ
硬件要求: 需要特殊 kernel (Marlin, ExLlama)
6.3 FP8 (W8A8)
精度损失: 极小
显存节省: 2x (vs FP16)
推理加速: H100 上 FP8 Tensor Core → 理论 2x
代表方法: 直接类型转换 + per-tensor scale
硬件要求: H100/H200 (Ada Lovelace 也部分支持)
6.4 INT2/INT3
精度损失: 明显,需要高级方法
显存节省: 8x/5.3x (vs FP16)
代表方法: QuIP#, AQLM
适用场景: 极端显存受限
7. 量化在 vLLM 中的使用
# AWQ 量化模型推理
from vllm import LLM, SamplingParams
llm = LLM(
model="TheBloke/Llama-2-7B-AWQ",
quantization="awq", # 指定量化方法
dtype="float16",
)
# GPTQ
llm = LLM(
model="TheBloke/Llama-2-7B-GPTQ",
quantization="gptq",
)
# FP8
llm = LLM(
model="neuralmagic/Meta-Llama-3-8B-FP8",
quantization="fp8",
)
对应教学代码:vllm_learn/code/course10/ 中的量化示例。
8. 实际选型建议
场景 推荐方案
──────────────────────────────────────────────────────
GPU 推理,追求精度 FP8 W8A8 (H100) 或 SmoothQuant W8A8
GPU 推理,显存有限 AWQ W4A16 或 GPTQ W4A16
GPU 推理,最大吞吐 SmoothQuant W8A8 + TensorRT-LLM
CPU 推理(本地笔记本) llama.cpp Q4_K_M
极端显存受限 QuIP# 2-bit 或 GGUF Q2_K
MoE 模型 (DeepSeek-V3) FP8 W8A8(Router 不量化)
9. 量化方法速查表
| 方法 | 量化对象 | 位宽 | 核心技术 | 需要校准数据 | 推理加速 |
|---|---|---|---|---|---|
| GPTQ | 权重 | W4/W3/W2 | Hessian 误差补偿 | 是 (128条) | 中 |
| AWQ | 权重 | W4 | 激活感知缩放 | 是 | 中 |
| SmoothQuant | 权重+激活 | W8A8 | 激活平滑 | 是 | 高 |
| QuIP# | 权重 | W2/W4 | 正交变换+格码 | 是 | 中 |
| FP8 | 权重+激活 | W8A8 | 直接转换 | 否 | 高 (H100) |
| GGUF K-quant | 权重 | W2~W8 | 分block+重要性 | 否 | CPU 友好 |
| LLM.int8() | 权重+激活 | W8A8 | 混合精度分解 | 否 | 低 |
| QAT | 权重 | 任意 | 训练时伪量化 | 需训练 | 高 |
10. 一句话总结
量化 = 高精度浮点 → 低精度整数/浮点 + scale/zero_point 补偿。GPTQ 靠 Hessian 补偿误差,AWQ 靠保护重要通道,SmoothQuant 靠平滑激活 outlier,FP8 靠硬件原生支持。选型核心:GPU 推理优先 AWQ/FP8,CPU 推理用 GGUF Q4_K_M,追求极致吞吐用 W8A8。