跳到主要内容

LLM 量化方法原理与实现详解:GPTQ、AWQ、SmoothQuant、FP8

· 阅读需 13 分钟
Zhiyuan Pan
Blog Author

全面解析 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 对比

方法量化位宽量化对象核心技术速度精度
GPTQW4/W3/W2仅权重Hessian 误差补偿
AWQW4仅权重激活感知缩放
SmoothQuantW8A8权重+激活激活平滑转移最快推理最好

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_08 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/W2Hessian 误差补偿是 (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。