27届大模型岗面试准备(九):模型量化实战——INT8/INT4/GPTQ/AWQ 原理、误差账与手写模拟量化

前八篇把训练侧(预训练、SFT、RLHF、PEFT)讲完了,从这篇开始进入推理部署侧。部署侧的第一道面试题几乎必然是量化:「7B 模型怎么塞进一张 12GB 的消费级显卡?」「GPTQ 和 AWQ 有什么区别?」「量化后精度掉了怎么排查?」这些问题看似工程向,实际上面试官想考的是你对数值表示、误差来源和校准逻辑的理解深度。这篇按「为什么要量化 → 数值格式 → 四种主流方案 → 手写模拟量化 → 面试追问」的顺序拆完。

一、先算一笔账:为什么量化是部署的第一杠杆

一个 7B 参数的模型,不同精度下的权重体积:

  • FP32:7B × 4 Byte = 28 GB
  • FP16/BF16:7B × 2 Byte = 14 GB
  • INT8:7B × 1 Byte = 7 GB
  • INT4:7B × 0.5 Byte = 3.5 GB

注意这只是权重。推理时还有 KV Cache(下一篇细讲)和激活值的开销,所以 FP16 的 7B 模型在 16GB 显卡上跑长上下文会直接 OOM。INT4 量化把权重压到 3.5GB,留给 KV Cache 的空间翻了三倍——这就是为什么 llama.cpp、Ollama 这些端侧方案默认都用 4bit。

除了显存,量化的第二个收益经常被忽略:大模型推理的 decode 阶段是访存瓶颈(memory-bound),不是算力瓶颈。每生成一个 token 都要把全部权重从显存搬到计算单元一次,权重体积减半,搬运时间近似减半,token 生成速度就上去了。面试时能主动说出「量化加速的本质是缓解访存带宽压力」,比只会背「省显存」高一个档次。

二、数值格式:从 FP16 到 INT4 到底丢了什么

量化的本质是把高精度浮点数映射到低比特整数网格上。最常用的是对称线性量化:

q = round(x / s),  s = max(|x|) / (2^(b-1) - 1)

其中 s 是缩放因子(scale),b 是比特数。反量化时 x̂ = q × sx - x̂ 就是量化误差。两个关键概念:

粒度(granularity):scale 按什么范围计算。整个张量共用一个 scale 叫 per-tensor,每一行(输出通道)一个叫 per-channel,每连续 128 个元素一组叫 per-group。粒度越细,对离群值越鲁棒,但需要存的 scale 越多。INT4 方案基本都用 group size 64/128。

离群值(outlier):LLM 的激活值中存在少量幅值极大的维度(可以比中位数大几十倍),这是 LLM 量化和 CNN 量化最大的区别。一个离群值会把 scale 撑得很大,导致其余正常值全部被挤在少数几个整数格子里,精度崩掉。几乎所有 LLM 量化方案的核心创新都是在处理离群值——记住这句话,四种方案的对比就有了主线。

三、四种主流方案对比

方案 比特 量化对象 需要校准数据 核心思想 精度保持 典型场景
LLM.int8() W8A8 权重+激活 离群维度拆出来走 FP16,其余走 INT8 矩阵乘 几乎无损 bitsandbytes 一行加载
SmoothQuant W8A8 权重+激活 是(少量) 把激活的离群幅值「平移」给权重,让两边都好量化 服务端 INT8 全链路加速
GPTQ W4A16 仅权重 是(128~1024 条) 逐列量化,用 Hessian 信息把误差补偿到未量化列 显存受限的 GPU 推理
AWQ W4A16 仅权重 是(少量) 按激活幅值找出 1% 显著权重通道,量化前放大保护 高,长尾任务更稳 端侧/边缘部署,vLLM 支持好

三条面试常考的横向结论:

  1. W8A8 和 W4A16 是两条路线:前者激活也量化,能用 INT8 TensorCore 拿到计算加速,适合大 batch 服务端;后者只压权重、计算仍在 FP16 做,赢在显存和访存,适合小 batch、decode 为主的场景。追问「为什么 W4A16 计算没变快还是更快」,答案就是第一节说的 memory-bound。
  2. GPTQ 是「误差补偿」流派:量化第 i 列产生的误差,按 Hessian 逆矩阵加权摊到还没量化的列上,让后面的列「替它还债」。数学上源自 OBS(Optimal Brain Surgeon)剪枝理论。
  3. AWQ 是「重要性保护」流派:观察到只有约 1% 的权重通道对输出影响巨大(对应激活幅值大的通道),量化前给这些通道乘一个放大系数 s,量化后在对应输入除回 s,数学等价但让重要权重占据更多整数格子。AWQ 不做梯度/Hessian 计算,量化速度快,且不容易过拟合校准集。

四、手写模拟量化:把 GPTQ 的核心逻辑跑出来

面试手撕环节不会让你写 CUDA kernel,但可能让你写「模拟量化」(fake quantization:量化再反量化,观察误差)。下面这段纯 NumPy 代码实现了 per-group 对称量化,并演示 GPTQ 风格的逐列误差补偿与朴素 RTN(round-to-nearest)的对比:

import numpy as np

def quantize_rtn(W, bits=4, group_size=64):
    """朴素 RTN:逐组独立量化,不做任何补偿"""
    Wq = np.zeros_like(W)
    qmax = 2 ** (bits - 1) - 1          # int4 对称: [-7, 7]
    for g in range(0, W.shape[1], group_size):
        blk = W[:, g:g + group_size]
        s = np.abs(blk).max(axis=1, keepdims=True) / qmax + 1e-8
        Wq[:, g:g + group_size] = np.round(blk / s) * s   # 量化+反量化
    return Wq

def quantize_gptq_style(W, X, bits=4, group_size=64):
    """GPTQ 风格:逐列量化,把误差按相关性补偿给未量化列
    W: [out, in] 权重矩阵   X: [n, in] 校准激活"""
    W = W.copy().astype(np.float64)
    qmax = 2 ** (bits - 1) - 1
    H = X.T @ X + 1e-2 * np.eye(W.shape[1])   # Hessian ≈ 2X^TX(阻尼防奇异)
    Hinv = np.linalg.inv(H)
    for g in range(0, W.shape[1], group_size):
        blk_end = min(g + group_size, W.shape[1])
        s = np.abs(W[:, g:blk_end]).max(axis=1, keepdims=True) / qmax + 1e-8
        for j in range(g, blk_end):
            w = W[:, j]
            q = np.clip(np.round(w / s[:, 0]), -qmax, qmax) * s[:, 0]
            err = (w - q) / Hinv[j, j]        # 本列量化误差
            W[:, j] = q
            if j + 1 < W.shape[1]:            # 摊给后续所有未量化列
                W[:, j + 1:] -= np.outer(err, Hinv[j, j + 1:])
    return W.astype(np.float32)

if __name__ == "__main__":
    rng = np.random.default_rng(0)
    W = rng.normal(0, 0.02, (256, 512)).astype(np.float32)
    W[:, rng.choice(512, 5, replace=False)] *= 8     # 人为注入离群通道
    X = rng.normal(0, 1.0, (128, 512)).astype(np.float32)
    Y = X @ W.T                                       # 原始输出作为基准
    for name, Wq in [("RTN", quantize_rtn(W)),
                     ("GPTQ-style", quantize_gptq_style(W, X))]:
        rel = np.linalg.norm(X @ Wq.T - Y) / np.linalg.norm(Y)
        print(f"{name:12s} 输出相对误差: {rel:.4%}")

在我机器上跑出来 RTN 的输出相对误差约是 GPTQ 风格的 1.5~2 倍。两个值得在面试里点出的细节:一是 GPTQ 优化的目标不是「权重误差最小」而是「层输出误差最小」,所以必须要校准数据来估计 Hessian;二是代码里的阻尼项(1e-2 * I)对应 GPTQ 论文里的 dampening,没有它 Hessian 求逆经常数值爆炸——这也是实际量化工具里 damp_percent 参数的来源。

五、精度掉了怎么排查:一套可复述的方法论

「量化后 benchmark 掉了 3 个点怎么办」是高频追问,给一套顺序化的排查框架:

  1. 先定位层:逐层替换(一层量化其余保持 FP16),用少量样本测 PPL,找出误差贡献最大的层。经验上首尾几层和 down_proj 最敏感,很多方案默认跳过 lm_head 不量化。
  2. 调粒度:group size 从 128 降到 64/32,scale 存储开销略增,离群鲁棒性明显提升。
  3. 换校准集:校准数据要贴近推理分布。用 C4 校准却拿去跑中文客服,掉点大概率是分布不匹配,换成业务语料重新量化。
  4. 混合精度兜底:把最敏感的 1~2 层保持 FP16 或 INT8,其余 INT4,显存多花几百 MB 换回精度。
  5. 升级方案:RTN → GPTQ/AWQ → 带微调的量化(QAT 或 QLoRA 式「量化+适配器补偿」)。

最后一个常见追问:「量化和上一篇讲的 QLoRA 什么关系?」答:QLoRA 的 NF4 是训练时把冻结基座压缩以省显存,反向传播时反量化回 BF16 计算,LoRA 分支始终高精度;GPTQ/AWQ 是推理时量化,目标是部署。一个管训练成本,一个管推理成本,两者可以串联——QLoRA 训完合并权重,再做 GPTQ 量化上线。

下一篇(A10)讲推理加速的另一半:KV Cache、PagedAttention 与 vLLM,和本篇的量化拼在一起,才是完整的推理优化答卷。

更多推荐