第 7 章 LLM异常值LLM.int8()

第 7 章 大语言模型量化实战

第 7 章 大语言模型量化实战

前六章的方法在小模型(ResNet、BERT、MobileNet)上很好用。但到了大语言模型(LLM),事情变了——LLM 的激活里会出现小模型没有的异常值(outliers),把之前所有量化方法打得措手不及。这一章是全书的分水岭:从"通用量化"进入"LLM 专用量化"。

原书仓库 ch7/ 目录:ch7_outlier_profiling.py、ch7_llm_int8_decomposition.py、ch7_llm_int8_flow.py、ch7_gptq_quantization.py、ch7_awq_quantization.py、ch7_turboquant_kv_cache.py、ch7_quant_decision_tree.py。

7.1 异常值问题:LLM 量化和普通量化为什么不同

ch7_outlier_profiling.py 在 OPT 系列模型(125m / 1.3b / 6.7b)上做了系统的异常值画像。核心发现:

1. 异常值随模型规模增长而涌现。 小模型(125m)的激活分布相对干净;当模型大到 6.7B,某些隐藏维度(hidden dimension)的激活值会比其他维度大一个数量级,而且同一个维度在所有 token 上都持续偏大——不是偶发噪声,是结构性的。

# 来自 ch7/ch7_outlier_profiling.py(结构)
def profile_outliers(model_key="opt-6.7b", ...):
    # 1. 加载模型,前向跑校准数据,抓取各层激活
    # 2. 按通道(hidden dim)统计激活幅度
    # 3. 找出"持续偏大"的异常通道,画热力图
    ...

def outlier_emergence(models_to_compare=None, ...):
    # 对比 125m / 1.3b / 6.7b 的异常值涌现曲线
    ...

2. 异常值是"跨层一致"的。 同一个 hidden dimension 在模型每一层都是异常值。这让异常值问题无法通过"某几层特殊处理"来绕开——它存在于整个模型深度上。

3. 为什么 INT8 对 LLM 失效。 回到第 2 章的原理:per-tensor 量化的 scale 由 absmax 决定。一个异常维度把 absmax 撑到几百,其他 99% 的维度都被压进极小的整数区间,量化精度崩塌。quantization_error_decomposition 把这种"异常值导致的误差主导"定量地展示出来——误差被少数异常通道垄断,而非均匀分布。

4. KV cache 的额外压力。 kv_cache_memory_analysis 计算了 KV cache 的内存占用:长上下文下,KV cache 往往比权重更占显存。这呼应了第 3.4 节——KV cache 量化是 LLM 长上下文推理的必答题。

7.2 LLM.int8():用混合精度"绕开"异常值

ch7_llm_int8_decomposition.py 从头实现了 LLM.int8()(Dettmers et al., 2022),它的思路非常巧妙:不试图用 INT8 覆盖异常值,而是把异常值单独拎出来用 FP16 算,正常值用 INT8 算。

# 来自 ch7/ch7_llm_int8_decomposition.py(核心)
def quantize_absmax_int8(tensor):
    # 标准 absmax INT8 量化
    ...

def mixed_precision_matmul(X, W, threshold):
    """LLM.int8() 的混合精度矩阵乘。
    1. 找出激活中超过 threshold 的异常列
    2. 异常列:保持 FP16,直接 FP16 矩阵乘
    3. 正常列:量化到 INT8,INT8 矩阵乘
    4. 两个结果相加
    """
    outlier_mask = X.abs() > threshold          # 异常位置
    # 分离异常特征(FP16)与正常特征(INT8)
    ...
graph TB
    A[激活矩阵 X] --> B{abs 超过阈值?}
    B -->|是| C[异常特征 保留 FP16]
    B -->|否| D[正常特征 量化 INT8]
    C --> E[FP16 矩阵乘]
    D --> F[INT8 矩阵乘]
    E --> G[结果相加]
    F --> G

为什么有效:异常值的数量通常很少(约 0.1%-1% 的特征),所以 FP16 分支的开销可控,而 INT8 分支覆盖了绝大多数计算,整体依然接近 INT8 的速度,精度却几乎无损。

run_threshold_sweep 实验会告诉你阈值的敏感性——阈值定得太松,异常值漏进 INT8,精度崩;定得太紧,太多特征走 FP16,加速没了。好在异常值和正常值之间有明显的"断层",阈值在合理区间内都不敏感。

7.3 GPTQ:海森矩阵感知的逐列量化

ch7_gptq_quantization.py 实现了 GPTQ(Frantar et al., ICLR 2023)——目前最主流的 LLM 权重量化算法之一。它和 LLM.int8() 思路完全不同:LLM.int8() 是"绕开"异常值,GPTQ 是"利用误差补偿"把量化误差吃掉。

GPTQ 的核心观察:量化某一列权重时产生的误差,可以用海森矩阵(Hessian)引导,把它"推"到还没量化的列上,让误差互相抵消。

# 来自 ch7/ch7_gptq_quantization.py 的 gptq_quantize(核心流程)
def gptq_quantize(W, H, bits=4, blocksize=128, groupsize=-1, damp_percent=0.01):
    # W: 权重矩阵 [out, in]; H: 海森矩阵 = 2 * X @ X^T(X 是校准激活)
    # Step 0: 通过 Cholesky 分解预计算 H^{-1}(数值稳定)
    L = torch.linalg.cholesky(H)
    Hinv = torch.cholesky_inverse(L)
    Hinv = torch.linalg.cholesky(Hinv, upper=True)

    # 逐块(block)处理,每块 B 列
    for block_start in range(0, cols, blocksize):
        for j in range(block_start, block_end):
            # 量化第 j 列(RTN 到网格)
            q_col = quantize_to_nearest_col(W[:, j], ...)
            Q[:, j] = q_col
            # 关键:用海森矩阵把误差"推"到后续列
            err = (w_col - q_col) / Hinv[j, j]
            W[:, j:block_end] -= err.unsqueeze(1) * Hinv[j, j:block_end]
        # 块级误差累积,更新所有剩余列
        W[:, block_end:] -= Err @ Hinv[block_start:block_end, block_end:]

GPTQ 相对朴素 OBQ 的三个关键改进(都写在了源码注释里):

  1. 固定列顺序:不贪心选列(那是 OBQ),固定从左到右,从而让所有行共享同一个 H^{-1}。
  2. Lazy Batch Update(延迟批量更新):不是每量化一列就更新所有剩余列,而是攒一个 block(blocksize=128)再统一更新,GPU 友好。
  3. Cholesky 重构:用 Cholesky 分解稳定地算 H^{-1},避免原始 OBQ 反复高斯消元的数值不稳定。

代码里还有个细节值得注意:damp_percent = 0.01——给海森矩阵对角线加 1% 的阻尼,防止奇异。这是 GPTQ 工程落地的关键小 trick。

RTN 是什么:Round-to-Nearest(就近取整),即第 2 章的朴素量化。GPTQ 的基线就是 RTN,quantize_to_nearest 函数实现了它。GPTQ 论文的核心卖点就是"我们比 RTN 强多少"。

7.4 AWQ:激活感知的权重保护

ch7_awq_quantization.py 实现了 AWQ(Lin et al., MLSys 2024)——另一种主流的 4bit 权重量化方法。AWQ 的洞察非常反直觉:权重的重要性不该由权重本身决定,而该由它乘以的激活决定。

# 来自 ch7/ch7_awq_quantization.py(核心)
def compute_activation_scales(X):
    """每个输入通道的重要性 = 激活绝对值的均值。"""
    return X.abs().mean(dim=0)   # [in_features]

def awq_scale_search(W, X, bits=4, groupsize=128, n_grid=20, max_shrink=0.8):
    # 对每个权重列分组,搜索最优缩放因子 s
    # s_j = (act_scale_j / max(act_scale))^α,  α ∈ [0,1]
    for alpha in grid:
        scales = act_norm.pow(alpha)
        W_scaled = W_group * scales            # 权重放大:重要通道更精确
        X_descaled = X_group / scales          # 激活缩小:数学上等价
        W_q = quantize_rtn(W_scaled, bits=4)   # 量化放大后的权重
        mse = reconstruction_error(W_q, W, X)  # 评估重建误差
    return best_scales

AWQ 的数学直觉(源码注释里有清晰的推导):量化误差 Err = Δ · RoundErr(w/Δ) · x。如果把重要通道的权重 w 放大到 w·s、激活缩小到 x/s,量化对重要通道的相对误差就变小了。而且因为是仅权重量化(weight-only),缩放因子可以提前固化,推理零额外开销。

AWQ 和 GPTQ 的对比:

维度 GPTQ AWQ
核心思想 海森矩阵引导误差补偿 激活感知的权重缩放保护
校准依赖 需要海森矩阵(二阶信息) 只需要激活统计(一阶)
计算开销 高(H 的 Cholesky) 低(网格搜索缩放)
部署友好 好 更好(无梯度、易固化)

两者都在仓库里被"从零实现 + 与生产库对照"验证过(GPTQ 对照 gptqmodel,AWQ 对照 autoawq)——这是全书反复强调的方法论:自己实现一遍,再和工业实现对拍,才能真正理解算法。

7.5 TurboQuant:KV cache 的向量量化

ch7_turboquant_kv_cache.py 实现了 TurboQuant——针对 KV cache 的量化方法。前面第 3.4 节已经铺垫了 KV cache 的 Key/Value 统计不对称,TurboQuant 进一步用**向量量化(Vector Quantization)**来处理。

# 来自 ch7/ch7_turboquant_kv_cache.py(核心)
def compute_lloyd_max_codebook(d, bits, max_iter=2000, tol=1e-12):
    """Lloyd-Max 算法生成码本:迭代优化码本使量化误差最小。"""
    ...

class TurboQuantizer:
    # 用码本(codebook)量化 KV cache,而不是均匀网格
    # 码本从真实 KV 分布学习而来,比均匀量化更能贴合分布
    ...

向量量化 vs 标量量化的区别:标量量化(前面所有方法)对每个数值单独量化;向量量化把若干数值组成一个向量,用"码本"里最接近的码字表示它。码本是离线从数据分布学出来的(Lloyd-Max 迭代),所以能贴合真实分布的形状,比均匀网格效率更高——尤其适合 KV cache 这种"大量重复模式"的数据。

结合第 3.4 节的发现:Key 有通道级异常值、Value 有 token 级波动,TurboQuant 的码本设计会针对这两种统计特性分别做处理,在极小精度损失下把 KV cache 压到 2-3 bit。

7.6 选型决策树

ch7_quant_decision_tree.py 把这一章的方法串成一张决策树。选型逻辑大致是:

graph LR
    A[要量化 LLM] --> B{显存瓶颈在哪?}
    B -->|权重| C{能接受多少精度损失?}
    C -->|几乎无损| D[LLM.int8 混合精度]
    C -->|需要 4bit| E{要训练/校准成本?}
    E -->|低| F[AWQ]
    E -->|可接受| G[GPTQ]
    B -->|KV cache| H[TurboQuant / 分组量化]
  • 权重放不下 → 权重量化(GPTQ / AWQ / LLM.int8)。
  • 上下文太长 KV cache 爆显存 → KV cache 量化(TurboQuant)。
  • 两者都要 → 组合使用(第 12 章的生产案例就是 4bit 权重 + 优化 KV cache 的典型配置)。

7.7 本章小结

  1. LLM 量化的分水岭是异常值:跨层一致的激活异常通道让小模型时代的量化方法失效。
  2. LLM.int8():混合精度分解,异常值走 FP16、正常值走 INT8,绕开而非硬扛。
  3. GPTQ:海森矩阵引导的误差补偿,逐列量化把误差"推"到未量化列,4bit 主流选手。
  4. AWQ:激活感知的权重缩放,只量权重、零推理开销,4bit 另一个主流选手。
  5. TurboQuant:KV cache 的向量量化,解决长上下文显存瓶颈。

下一章把位宽推到极致——sub-8-bit 格式:FP8、FP4、NF4,以及只有 1.58 bit 的三元网络。


作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI人工智能时代,转载请注明出处。