1. 量化的动机与基础

大模型的推理成本主要由显存和计算量决定。量化通过降低参数精度来同时减少这两者:

模型: Llama-3-70B
┌──────────────┬─────────┬───────────┬──────────────┐
│ 精度         │ 显存    │ 推理速度  │ 质量损失     │
├──────────────┼─────────┼───────────┼──────────────┤
│ FP16 (基准)  │ 140 GB  │ 1.0x      │ 0%           │
│ INT8         │ 70 GB   │ 1.8x      │ <0.5%        │
│ INT4         │ 35 GB   │ 2.5x      │ 1-2%         │
│ INT2         │ 18 GB   │ 3.0x      │ 5-15%        │
└──────────────┴─────────┴───────────┴──────────────┘

1.1 量化的数学定义

量化是将浮点数映射到低精度整数的过程:

$$x_q = \text{clamp}\left(\text{round}\left(\frac{x}{s}\right) + z\right), \quad x \in [x_{min}, x_{max}]$$

反量化:$\hat{x} = s \cdot (x_q - z)$

其中:

  • $s$ (scale):缩放因子
  • $z$ (zero-point):零点偏移
  • 量化范围由位宽决定:$[0, 2^b - 1]$(无符号)或 $[-2^{b-1}, 2^{b-1}-1]$(有符号)
import torch
import torch.nn as nn

def quantize_tensor(x, num_bits=8, symmetric=True):
    """基础量化函数"""
    if symmetric:
        # 对称量化: [-2^(b-1), 2^(b-1)-1]
        qmax = 2 ** (num_bits - 1) - 1
        qmin = -2 ** (num_bits - 1)
        
        # 计算缩放因子
        max_val = x.abs().max()
        scale = max_val / qmax
        zero_point = 0
        
        # 量化
        x_q = torch.clamp(torch.round(x / scale), qmin, qmax).to(torch.int8)
    else:
        # 非对称量化: [0, 2^b - 1]
        qmax = 2 ** num_bits - 1
        qmin = 0
        
        max_val = x.max()
        min_val = x.min()
        scale = (max_val - min_val) / qmax
        zero_point = qmin - torch.round(min_val / scale)
        
        # 量化
        x_q = torch.clamp(torch.round(x / scale) + zero_point, qmin, qmax).to(torch.uint8)
    
    return x_q, scale, zero_point

def dequantize_tensor(x_q, scale, zero_point):
    """反量化"""
    return scale * (x_q.float() - zero_point)

1.2 对称 vs 非对称量化

方案公式优点缺点适用场景
对称$x_q = \text{round}(x/s)$简单、零点固定浪费一个量化级权重(分布近似零均值)
非对称$x_q = \text{round}(x/s) + z$利用全部量化级额外存储零点激活值(ReLU后非负)
# 可视化对比
import numpy as np

# 权重分布 (近似对称, 均值≈0)
weights = np.random.randn(1000) * 0.1

# 激活值分布 (非对称, ReLU后全正)
activations = np.abs(np.random.randn(1000)) * 0.5

# 对称量化权重: 利用率 ~100%
# 非对称量化权重: 利用率 ~50% (负半轴浪费)

# 对称量化激活值: 利用率 ~50% (负半轴浪费)
# 非对称量化激活值: 利用率 ~100%

2. 量化分类

2.1 按时机分类

┌──────────────────────────────────────────────────┐
│              量化时机分类                          │
├──────────────────┬───────────────────────────────┤
│ QAT (训练感知)    │ PTQ (训练后量化)               │
│ - 训练中模拟量化  │ - 训练后直接量化                │
│ - 质量最好       │ - 快速、无需训练                │
│ - 成本高         │ - 质量略低                     │
│ - 代表: QAT, LSQ │ - 代表: GPTQ, AWQ, SmoothQuant │
└──────────────────┴───────────────────────────────┘

2.2 按粒度分类

class QuantizationGranularity:
    """不同的量化粒度"""
    
    # 1. 逐张量 (Per-Tensor): 整个张量用一个 scale
    def per_tensor(self, w):
        scale = w.abs().max() / 127
        return torch.round(w / scale).to(torch.int8), scale
    
    # 2. 逐通道 (Per-Channel): 每个输出通道一个 scale
    def per_channel(self, w):
        # w: (out_features, in_features)
        scales = w.abs().max(dim=1).values / 127  # (out_features,)
        return torch.round(w / scales.unsqueeze(1)).to(torch.int8), scales
    
    # 3. 逐组 (Per-Group): 每 group 个连续元素共用一个 scale
    def per_group(self, w, group_size=128):
        # w: (out_features, in_features)
        assert w.size(1) % group_size == 0
        w_reshaped = w.view(-1, group_size)
        scales = w_reshaped.abs().max(dim=1).values / 7  # INT4: 7 = 2^3-1
        w_q = torch.round(w_reshaped / scales.unsqueeze(1))
        return w_q.view_as(w).to(torch.int8), scales
粒度Scale 数量质量额外存储适用场景
Per-Tensor1最低最小边缘设备
Per-Channelout_features中等INT8 权重
Per-Group (128)out×in/128最高中等INT4 权重

3. SmoothQuant:平滑激活值

3.1 问题分析

LLM 量化面临的核心挑战是激活值的离群点(Outliers)。部分通道的激活值远大于其他通道,导致整体量化精度下降。

def analyze_outliers(activations):
    """分析激活值离群点"""
    # activations: (batch, seq_len, hidden_dim)
    channel_max = activations.abs().max(dim=(0, 1)).values  # (hidden_dim,)
    
    # 典型现象: ~0.1% 的通道有极大的激活值
    median = channel_max.median()
    outliers = (channel_max > 10 * median).sum()
    
    print(f"通道数: {len(channel_max)}")
    print(f"中位数: {median:.4f}")
    print(f"最大值: {channel_max.max():.4f}")
    print(f"离群通道: {outliers} ({100*outliers/len(channel_max):.1f}%)")
    print(f"离群/中位: {channel_max.max()/median:.1f}x")

3.2 SmoothQuant 的平滑策略

SmoothQuant (Xiao et al., 2022) 的核心思想:将激活值的难度转移到权重上

$$\hat{W} = W / s, \quad \hat{X} = X \cdot s$$

由于 $Y = XW = (X \cdot s)(W / s)$,数学上等价,但量化友好度改变:

class SmoothQuant:
    """SmoothQuant: 平滑激活值离群点"""
    def __init__(self, alpha=0.5):
        """
        alpha: 平滑强度, 0.5 是经验最优值
        - alpha=0: 不平滑
        - alpha=1: 完全转移到权重
        """
        self.alpha = alpha
    
    def compute_smoothing_scales(self, weight, activation_stats):
        """
        计算每个通道的平滑因子
        
        weight: (out_features, in_features)
        activation_stats: (in_features,) - 激活值的绝对最大值统计
        """
        # 权重的每列最大值
        weight_max = weight.abs().max(dim=0).values  # (in_features,)
        
        # 平滑因子: s = (activation_max^alpha / weight_max^(1-alpha))^0.5
        # 平衡激活值和权重的量化难度
        scales = (activation_stats.pow(self.alpha) / 
                  weight_max.pow(1 - self.alpha)).clamp(min=1e-5)
        
        return scales
    
    def apply_smoothing(self, weight, scales):
        """应用平滑: W' = W/s, 在推理时 X' = X*s"""
        smoothed_weight = weight / scales.unsqueeze(0)  # 广播到行
        return smoothed_weight, scales

3.3 SmoothQuant 效果

量化前:
  激活值范围: [0, 150.0]  ← 离群点导致大范围
  权重范围:   [-0.8, 0.8]  ← 本身量化友好
  
  激活值量化精度: 差 (离群点压缩了有效精度)
  权重量化精度: 好

SmoothQuant (alpha=0.5) 后:
  激活值范围: [0, 12.3]   ← 离群点被抑制
  权重范围:   [-2.1, 2.1]  ← 略微增大但仍可控
  
  激活值量化精度: 好
  权重量化精度: 仍可接受

4. GPTQ:基于二阶信息的量化

4.1 核心思想

GPTQ (Frantar et al., 2022) 是一种基于 OBQ (Optimal Brain Quantization) 的逐列量化方法,利用二阶信息(Hessian 矩阵)来最小化量化误差。

目标:找到量化后的权重 $\hat{W}$,使得 $\hat{W}X$ 与 $WX$ 的差距最小。

$$\arg\min_{\hat{W}} |WX - \hat{W}X|_2^2$$

4.2 算法流程

class GPTQ:
    """GPTQ: 基于二阶信息的逐列量化"""
    def __init__(self, num_bits=4, group_size=128):
        self.num_bits = num_bits
        self.group_size = group_size
    
    def quantize(self, W, X, block_size=128):
        """
        W: (out_features, in_features) - 权重矩阵
        X: (calib_samples, in_features) - 校准数据
        """
        # Step 1: 计算 Hessian 矩阵 H = X^T X
        H = X.T @ X  # (in_features, in_features)
        H = H + 0.0001 * torch.diag(torch.diag(H))  # 正则化
        
        # Step 2: Cholesky 分解
        # H = L L^T, 用于高效的误差补偿
        L = torch.linalg.cholesky(H)
        H_inv = torch.cholesky_inverse(L)
        
        # Step 3: 逐列量化 (从最后一列开始)
        W_q = W.clone()
        dQ = torch.zeros_like(W)  # 累积补偿
        
        n_features = W.size(1)
        
        for i in range(n_features):
            # 量化第 i 列
            col = W[:, i] - dQ[:, i]  # 减去之前列的补偿
            
            # 量化
            q_col = self._quantize_values(col)
            
            # 计算量化误差
            err = (col - q_col) / H_inv[i, i]
            
            # 将误差补偿到后续列
            for j in range(i + 1, n_features):
                dQ[:, j] += err * H_inv[i, j]
            
            W_q[:, i] = q_col
        
        return W_q
    
    def _quantize_values(self, x):
        """简单的组内量化"""
        if self.group_size is None:
            return self._round_and_clip(x)
        
        # 分组量化
        orig_shape = x.shape
        x_grouped = x.view(-1, self.group_size)
        for g in range(x_grouped.size(0)):
            x_grouped[g] = self._round_and_clip(x_grouped[g])
        return x_grouped.view(orig_shape)
    
    def _round_and_clip(self, x):
        qmax = 2 ** (self.num_bits - 1) - 1
        qmin = -2 ** (self.num_bits - 1)
        scale = x.abs().max() / qmax
        return torch.clamp(torch.round(x / scale), qmin, qmax) * scale

4.3 GPTQ 的分块处理

对于大模型,Hessian 矩阵的逆计算代价很高。GPTQ 采用分块策略

def gptq_blockwise(W, X, block_size=128, num_bits=4):
    """
    分块 GPTQ: 将列分成多个块, 块内精确处理, 块间用补偿
    """
    in_features = W.size(1)
    W_q = W.clone()
    
    for start in range(0, in_features, block_size):
        end = min(start + block_size, in_features)
        
        # 当前块的 Hessian
        X_block = X[:, start:end]  # (samples, block_size)
        H_block = X_block.T @ X_block
        H_block += 0.0001 * torch.diag(torch.diag(H_block))
        
        try:
            H_inv_block = torch.linalg.inv(H_block)
        except:
            H_inv_block = torch.linalg.pinv(H_block)
        
        # 块内逐列量化
        dQ = torch.zeros(W.size(0), end - start, device=W.device)
        
        for local_i in range(end - start):
            global_i = start + local_i
            col = W[:, global_i] - dQ[:, local_i]
            
            q_col = quantize_round_and_clip(col, num_bits)
            err = (col - q_col) / H_inv_block[local_i, local_i]
            
            # 补偿到块内后续列
            for local_j in range(local_i + 1, end - start):
                dQ[:, local_j] += err * H_inv_block[local_i, local_j]
            
            W_q[:, global_i] = q_col
        
        # 块间补偿: 当前块的量化误差对后续块的影响
        # (通过更新校准数据来隐式处理)
        err_block = W[:, start:end] - W_q[:, start:end]
        X[:, end:] -= X[:, start:end] @ err_block  # 更新后续块的输入
    
    return W_q

5. AWQ:激活感知权重量化

5.1 核心发现

AWQ (Lin et al., 2023) 的核心发现是:并非所有权重都同等重要,与大幅激活值对应的权重通道更重要

class AWQ:
    """AWQ: 激活感知权重量化"""
    def __init__(self, num_bits=4, group_size=128, w_bit=4):
        self.num_bits = num_bits
        self.group_size = group_size
        self.w_bit = w_bit
    
    def compute_importance(self, weight, activation_stats):
        """
        计算每个通道的重要性分数
        
        关键洞察: 如果激活值大, 那么对应权重的量化误差会被放大
        importance = |activation| * |weight|
        """
        # 权重每列的最大值
        w_max = weight.abs().max(dim=0).values  # (in_features,)
        
        # 重要性 = 激活值幅度 × 权重幅度
        importance = activation_stats * w_max  # (in_features,)
        
        return importance
    
    def search_scaling_factor(self, weight, activation_stats, grid=100):
        """
        搜索最优的逐通道缩放因子
        
        类似 SmoothQuant, 但方向相反: 只缩放权重, 通过反缩放补偿
        """
        best_scale = None
        best_loss = float('inf')
        
        # 重要性最高的前 0.1% 通道
        importance = self.compute_importance(weight, activation_stats)
        threshold = torch.quantile(importance, 0.999)
        salient_mask = importance > threshold
        
        # 网格搜索缩放因子
        for ratio in torch.linspace(0.0, 1.0, grid):
            # scale = (activation_max / weight_max)^ratio
            scale = (activation_stats.mean() / weight.abs().max(dim=0).values.mean()) ** ratio
            scale = scale.clamp(min=1e-4)
            
            # 应用缩放: W' = W * s, 然后量化, 反量化
            w_scaled = weight * scale.unsqueeze(0)
            w_quantized = self._fake_quantize(w_scaled)
            w_dequantized = w_quantized / scale.unsqueeze(0)
            
            # 计算量化损失 (重点关注重要通道)
            loss_mse = ((weight - w_dequantized) ** 2).mean()
            loss_salient = ((weight[:, salient_mask] - w_dequantized[:, salient_mask]) ** 2).mean()
            
            # 加权损失
            total_loss = loss_mse + 10 * loss_salient
            
            if total_loss < best_loss:
                best_loss = total_loss
                best_scale = scale
        
        return best_scale
    
    def _fake_quantize(self, w):
        """模拟量化-反量化过程"""
        orig_shape = w.shape
        w_grouped = w.view(-1, self.group_size)
        
        scales = w_grouped.abs().max(dim=1).values / (2 ** (self.w_bit - 1) - 1)
        w_q = torch.clamp(
            torch.round(w_grouped / scales.unsqueeze(1)),
            -(2 ** (self.w_bit - 1)),
            2 ** (self.w_bit - 1) - 1
        )
        w_dq = w_q * scales.unsqueeze(1)
        
        return w_dq.view(orig_shape)

5.2 AWQ vs GPTQ 对比

维度GPTQAWQ
方法类型基于二阶信息的逐列量化基于激活感知的缩放量化
需要校准数据
计算复杂度较高(Hessian 逆)低(网格搜索)
量化速度慢(逐列)
推理速度基准快(融合缩放)
质量INT4 质量高与 GPTQ 接近
泛化性依赖校准数据更鲁棒
主要框架AutoGPTQAutoAWQ
# 实际使用对比
# GPTQ
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

quantize_config = BaseQuantizeConfig(
    bits=4,
    group_size=128,
    desc_act=False,  # 是否按激活降序排列
)
model_gptq = AutoGPTQForCausalLM.from_pretrained(
    "model_path",
    quantize_config=quantize_config,
)
model_gptq.quantize(calibration_dataset)

# AWQ
from awq import AutoAWQForCausalLM

model_awq = AutoAWQForCausalLM.from_pretrained("model_path")
model_awq.quantize(
    quant_config={
        "w_bit": 4,
        "q_group_size": 128,
        "zero_point": True,
    },
    run_search=True,
    run_quant=True,
)

6. 其他量化方法

6.1 GGUF/GGML 量化

llama.cpp 的量化格式,专为 CPU/混合推理优化:

# GGUF 量化级别
gguf_quant_levels = {
    "Q8_0":  {"bits": 8,  "size_ratio": 1.0,  "quality": "最佳"},
    "Q6_K":  {"bits": 6,  "size_ratio": 0.79, "quality": "优秀"},
    "Q5_K":  {"bits": 5,  "size_ratio": 0.68, "quality": "很好"},
    "Q4_K":  {"bits": 4,  "size_ratio": 0.58, "quality": "好"},
    "Q4_0":  {"bits": 4,  "size_ratio": 0.50, "quality": "可接受"},
    "Q3_K":  {"bits": 3,  "size_ratio": 0.45, "quality": "有损失"},
    "Q2_K":  {"bits": 2,  "size_ratio": 0.38, "quality": "明显损失"},
}

# 关键特性: k-quant 使用混合精度
# 注意力层用更高精度, FFN 层用更低精度

6.2 FP8 量化

NVIDIA H100 支持的原生 FP8 格式:

# FP8 有两种格式
# E4M3: 4位指数, 3位尾数 → 动态范围小, 精度高
# E5M2: 5位指数, 2位尾数 → 动态范围大, 精度低

# 使用 TransformerEngine 进行 FP8 推理
import transformer_engine.pytorch as te

# FP8 自动量化
model_fp8 = te.Linear(4096, 4096)
# 训练/推理时自动选择 FP8 格式
格式位宽动态范围精度适用场景
E4M38±4483位尾数前向/权重
E5M28±573442位尾数梯度/激活
INT88±127均匀权重/激活
INT44±7均匀权重

7. 量化质量评估

7.1 评估指标

def evaluate_quantization(model_fp, model_quant, eval_dataset):
    """量化质量评估"""
    results = {}
    
    # 1. 权重级指标
    w_fp = model_fp.layers[0].mlp.up_proj.weight
    w_q = model_quant.layers[0].mlp.up_proj.weight
    
    # MSE
    results['weight_mse'] = ((w_fp - w_q) ** 2).mean().item()
    
    # 余弦相似度
    results['weight_cosine'] = F.cosine_similarity(
        w_fp.flatten(), w_q.flatten(), dim=0
    ).item()
    
    # 2. 任务级指标
    # 困惑度 (PPL)
    ppl_fp = compute_perplexity(model_fp, eval_dataset)
    ppl_q = compute_perplexity(model_quant, eval_dataset)
    results['ppl_delta'] = ppl_q - ppl_fp
    
    # 3. 功能性指标
    # 准确率变化
    acc_fp = evaluate_accuracy(model_fp, eval_dataset)
    acc_q = evaluate_accuracy(model_quant, eval_dataset)
    results['acc_delta'] = acc_q - acc_fp
    
    return results

7.2 各方法质量对比

以 LLaMA-2 7B 为例(4-bit 量化):

方法PPL 变化MMLU 变化人类评估
RTN (Round-to-Nearest)+0.72-3.1%明显下降
GPTQ+0.21-1.2%几乎不变
AWQ+0.25-0.9%几乎不变
SmoothQuant (INT8)+0.03-0.1%无感知
QAT (INT4)+0.08-0.3%无感知

8. 总结与选型指南

量化方案选择决策树:

是否需要训练 (QAT)?
├─ 是, 质量优先 → QAT + INT4/INT8
└─ 否, 快速部署 (PTQ)
    推理硬件?
    ├─ NVIDIA GPU (H100)
    │   └─ FP8 (TransformerEngine)
    ├─ NVIDIA GPU (A100/V100)
    │   └─ GPTQ INT4 或 AWQ INT4
    ├─ CPU
    │   └─ GGUF Q4_K_M (llama.cpp)
    └─ 移动端
        └─ AWQ INT4 + 融合层
场景推荐方案理由
服务端 GPU 推理GPTQ/AWQ INT4质量好, vLLM 支持
极致质量SmoothQuant INT8几乎无损
CPU 推理GGUF Q4_K_Mllama.cpp 生态完善
实时低延迟FP8 (H100)硬件原生加速
边缘设备AWQ INT2/INT4显存最小化
混合精度W4A8 (INT4权重+INT8激活)平衡质量与速度

量化技术是大模型落地的关键基础设施。当前 INT4 PTQ(GPTQ/AWQ)已经在质量和效率之间取得了良好平衡,而 FP8 正在成为新一代 GPU 上的标准方案。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。