模型量化:从手算推演到可视化验证

量化是将浮点权重/激活值映射到低比特整数的压缩技术。核心矛盾是范围 vs 精度——scale 越小精度越高但覆盖范围越小,一个 outlier 就能拉大 scale 导致大量正常值被”挤压”到几个量化级别。解法依次递进:对称量化(权重,零点固定为 0)→ 非对称量化(激活值,zero_point 补偿偏斜分布)→ 逐通道量化(各通道独立 scale)→ group-wise(INT4 必须,每 128 个值一组独立量化)→ GPTQ(逐列量化 + 逆 Hessian 误差补偿)。7B 模型 INT4 后仅需 ~3.5 GB,910B3 的 64 GB HBM 可同时驻留多个模型。


1. 背景

1.1 为什么需要量化

模型推理时权重和激活值通常使用 FP16(2 字节)或 FP32(4 字节)。量化的目标是将这些值映射到更低比特的整数表示:

精度 每参数字节 7B 模型 HBM 相对 FP16
FP32 4 ~28 GB
FP16/BF16 2 ~14 GB 1×(基线)
INT8 1 ~7 GB 0.5×
INT4 0.5 ~3.5 GB 0.25×

7B 模型 INT4 量化后仅需 ~3.5 GB,910B3 的 64 GB HBM 可以同时运行多个模型。

1.2 量化的核心问题

用一个类比理解量化的本质:

用 256 个整数(INT8)来表示一段连续的实数范围
≈ 用 256 级台阶来逼近一条平滑的斜坡

每一级台阶的高度(scale)决定了能分辨多小的变化。scale 越小 → 台阶越密 → 精度越高 → 但覆盖范围越小(最大可表示值 = scale × 127)。这是量化的核心矛盾:范围 vs 精度,不可兼得

量化不是无损压缩——它引入了不可逆的信息损失。就像把一张高清照片缩小后再放大,细节无法恢复。但它也不是简单的四舍五入——需要针对不同的数据分布(权重是正态分布,激活值是偏斜分布)选择不同的映射策略。

1.3 当前 NPU 栈的限制

CANN 8.0.1 + torch_npu 2.1.0 组合下,主流量化库均不可用:

原因
bitsandbytes CUDA kernel 专用,aarch64 不可用
GPTQ / AWQ CUDA 依赖
torch_npu quantization 2.1.0 无此模块

本文聚焦理论学习,用 Python/NumPy 演示量化原理。


2. 对称量化

2.1 数学原理

对称量化将浮点值线性映射到有符号整数范围,零点固定在 0:

量化:   q = round(x / scale)
       q = clamp(q, -128, 127)    # INT8 范围

反量化: x' = q × scale

其中: scale = max(|x|) / 127

关键性质

  • 浮点 0.0 在量化空间恰好对应整数 0——这是对称量化的定义特征
  • 正负对称:量化范围是 [-127, 127],正负各 127 级 + 中间点 0
  • 对称量化实际使用 [-127, 127](不使用 -128),共 255 个可表示值(127 正 + 127 负 + 0)——恰好对应 INT8 的 2^8=256 中去掉未使用的 -128

为什么适合权重:神经网络权重通常初始化为均值为 0 的正态分布,训练后仍然大致对称。对称量化不需存储 zero_point,节省计算——反量化时只需 x = q × scale,无需减法。

2.2 手算示例

假设一组权重:[0.03, -0.05, 0.12, -0.08, 0.01],做 INT8 对称量化(范围 -127 ~ 127):

Step 1: max(|x|) = max(0.03, 0.05, 0.12, 0.08, 0.01) = 0.12

Step 2: scale = 0.12 / 127 ≈ 0.000945

Step 3: 量化(除 scale → 取整)
  0.03  → round( 0.03 / 0.000945) = round(31.7)  =  32
 -0.05  → round(-0.05 / 0.000945) = round(-52.9) = -53
  0.12  → round( 0.12 / 0.000945) = round(127.0) = 127  ← 用满范围
 -0.08  → round(-0.08 / 0.000945) = round(-84.7) = -85
  0.01  → round( 0.01 / 0.000945) = round(10.6)  =  11

Step 4: 反量化(乘 scale)
   32 × 0.000945 =  0.0302  (原值  0.03, 误差 +0.0002)
  -53 × 0.000945 = -0.0501  (原值 -0.05, 误差 -0.0001)
  127 × 0.000945 =  0.1200  (原值  0.12, 误差  0)
  -85 × 0.000945 = -0.0803  (原值 -0.08, 误差 -0.0003)
   11 × 0.000945 =  0.0104  (原值  0.01, 误差 +0.0004)

0.0 恰好对应量化值 0——无需额外的零点参数。误差来自浮点到整数的取整损失。

2.3 Python 演示

import numpy as np

def symmetric_quantize(x, bits=8):
    qmax = 2 ** (bits - 1) - 1
    scale = np.max(np.abs(x)) / qmax
    if scale == 0: scale = 1e-9
    q = np.clip(np.round(x / scale), -qmax, qmax).astype(np.int8 if bits <= 8 else np.int32)
    return q, scale

def symmetric_dequantize(q, scale):
    return q.astype(np.float32) * scale

2.4 量化误差的来源

对称量化的误差来自两个不可兼得的需求:

  1. 表示范围:scale = max( x ) / 127。如果存在离群值(outlier),scale 被拉大,大量正常值被粗粒度表示
  2. 表示精度:scale 越小精度越高,但范围越小——超出范围的值被截断(clipping error)

示例:假设一个权重矩阵有 1000 个值,其中 999 个在 [-0.5, 0.5] 之间,但有一个值是 5.0:

无 outlier 时:      有 outlier 时:
max(|w|) = 0.5      max(|w|) = 5.0
scale = 0.5/127     scale = 5.0/127
      ≈ 0.00394           ≈ 0.0394

值 0.01 → q ≈ 3     值 0.01 → q = round(0.01/0.0394) = 0
值 0.05 → q ≈ 13    值 0.05 → q = round(0.05/0.0394) = 1
值 0.10 → q ≈ 25    值 0.10 → q = round(0.10/0.0394) = 3

一个 outlier 把 scale 放大了 10 倍,导致所有正常值被”挤压”到仅 0-3 这几个量化级别——原本 256 级的精度,现在只用了不到 4 级。这是量化中最重要的工程问题之一:如何处理 outlier 以保护精度

LLM.int8() 论文 [3] 发现:transformer 模型中约 0.1% 的特征维度包含了极大的 outlier 值(比其他维度大 10-100 倍),这些 outlier 如果和其他值一起量化,会严重破坏精度。解决方案:对 outlier 维度单独用 FP16,其余用 INT8。


3. 非对称量化

3.1 数学原理

非对称量化引入零点(zero point)来处理偏斜分布:

量化:   q = round(x / scale) + zero_point
       q = clamp(q, 0, 255)        # UINT8 范围

反量化: x' = (q - zero_point) × scale

其中: scale  = (x_max - x_min) / 255
      zero_point = round(-x_min / scale)

关键性质

  • zero_point 将浮点空间的最小值映射到量化空间的 0——浮点 0.0 对应量化值 zero_point(通常不是 0)
  • 量化范围为 [0, 255](UINT8),全都是非负数——适合全是非负值的激活数据
  • 量化范围完全被利用:从 0(对应 x_min)到 255(对应 x_max),不像对称量化那样正负不对称

为什么适合激活值:ReLU 激活函数将所有负值截断为 0,输出全是非负数且分布偏斜。如果强制套用对称量化,下半段(-128 到 0)的量化区间完全无法利用——浪费了一半精度。非对称量化通过 zero_point 将整个量化范围精准映射到激活值的实际分布范围,不浪费任何一个量化级别。

3.2 手算示例

假设 ReLU 后的激活值:[0.0, 0.35, 1.20, 0.08, 2.50](全是非负数),做 UINT8 非对称量化(范围 0 ~ 255):

Step 1: x_min = 0.0,  x_max = 2.50

Step 2: scale = (2.50 - 0.0) / 255 ≈ 0.00980
        zero_point = round(-0.0 / 0.00980) = 0

Step 3: 量化(除 scale → 取整 → 加 zero_point)
  0.0  → round(0.0  / 0.00980) + 0 = round(0.0)   + 0 =   0
  0.35 → round(0.35 / 0.00980) + 0 = round(35.7)  + 0 =  36
  1.20 → round(1.20 / 0.00980) + 0 = round(122.4) + 0 = 122
  0.08 → round(0.08 / 0.00980) + 0 = round(8.2)   + 0 =   8
  2.50 → round(2.50 / 0.00980) + 0 = round(255.1) + 0 = 255 ← 用满

Step 4: 反量化(减 zero_point → 乘 scale)
  (0   - 0) × 0.00980 = 0.0000  (原值 0.00, 误差 0)
  (36  - 0) × 0.00980 = 0.3528  (原值 0.35, 误差 +0.0028)
  (122 - 0) × 0.00980 = 1.1960  (原值 1.20, 误差 -0.004)
  (8   - 0) × 0.00980 = 0.0784  (原值 0.08, 误差 -0.0016)
  (255 - 0) × 0.00980 = 2.5000  (原值 2.50, 误差 0)

3.3 同一组数据:对称 vs 非对称对比

用同一组 ReLU 激活值 [0.0, 0.35, 1.20, 0.08, 2.50],分别用两种量化方式:

对称量化 (不适合此数据):              非对称量化 (适合):
  scale = 2.50 / 127 = 0.01969         scale = 2.50 / 255 = 0.00980

  0.0  → q=0    → 0.0000  ✓             0.0  → q=0   → 0.0000  ✓
  0.35 → q=18   → 0.3544  (Δ0.0044)     0.35 → q=36  → 0.3528  (Δ0.0028) ← 更精确
  1.20 → q=61   → 1.2011  (Δ0.0011)     1.20 → q=122 → 1.1960  (Δ0.0040)
  0.08 → q=4    → 0.0788  (Δ0.0012)     0.08 → q=8   → 0.0784  (Δ0.0016)
  2.50 → q=127  → 2.5000  ✓             2.50 → q=255 → 2.5000  ✓

对称量化的浪费:
  可表示范围: [-2.50, +2.50] → 跨度 5.0
  实际数据:   [ 0.00, +2.50] → 跨度 2.5
  浪费:       50% — 负数区间 (-2.50 ~ 0) 不会被 ReLU 激活值用到

非对称量化的优势:
  可表示范围: [0.00, 2.50] → 精准匹配
  利用率:     100%
  scale 减半 → 精度翻倍

3.4 对称 vs 非对称

特性 对称量化 非对称量化
零点 固定为 0 动态计算
适用 均值为 0 的分布(权重) 偏斜分布(激活值)
量化范围 [-127, 127] [0, 255]
计算效率 更高(无 zero_point 运算) 需要减 zero_point
精度 对离群值敏感 更好地利用量化范围

实践中,LLM 权重通常用对称量化(均值为 0),激活值用非对称量化(ReLU 后全是非负)。


4. 逐张量与逐通道量化

4.1 粒度差异

方式 粒度 scale 数量 存储开销 (4096×4096) 精度
逐张量 整个矩阵共享 1 个 scale 1 4 bytes 受 outlier 通道影响大
逐通道 每行(输出通道)独立 scale 4096 16 KB 各通道自主最优

4.2 手算示例

假设一个 2 行 3 列的权重矩阵(2 个输出通道 × 3 个输入通道):

W = [ [0.05, -0.02,  0.10],    ← 通道 0: 值都较小
      [1.20, -0.80, -2.50] ]   ← 通道 1: 值较大

逐张量量化(整个矩阵共享一个 scale):

Step 1: max(|W|) = max(0.05, 0.02, 0.10, 1.20, 0.80, 2.50) = 2.50

Step 2: scale = 2.50 / 127 ≈ 0.01969

Step 3: 量化
  通道 0: 0.05→3, -0.02→-1, 0.10→5
  通道 1: 1.20→61, -0.80→-41, -2.50→-127

Step 4: 反量化
  通道 0: 3×0.01969=0.059 (原0.05, Δ+0.009)       ← 误差超过原值的 10%!
          -1×0.01969=-0.020 (原-0.02, Δ0)           ← 巧合准确
          5×0.01969=0.098 (原0.10, Δ-0.002)
  通道 1: 61×0.01969=1.201 (原1.20, Δ+0.001)
          -41×0.01969=-0.807 (原-0.80, Δ-0.007)
          -127×0.01969=-2.501 (原-2.50, Δ-0.001)    ← 大值准确

通道 0 的 RMSE: 0.0054  ← 小值被粗粒度表示
通道 1 的 RMSE: 0.0038  ← 大值主导了 scale

逐通道量化(每行独立的 scale):

通道 0: max(|w₀|) = 0.10
        scale₀ = 0.10 / 127 ≈ 0.000787
        0.05→64, -0.02→-25, 0.10→127
        反量化: 64×0.000787=0.0504 (Δ+0.0004)
                -25×0.000787=-0.0197 (Δ+0.0003)
                127×0.000787=0.1000 (Δ0)            ← 精度大幅提升!

通道 1: max(|w₁|) = 2.50
        scale₁ = 2.50 / 127 ≈ 0.01969
        (与逐张量对通道 1 的结果相同)

通道 0 的 RMSE (逐通道): 0.0003  ← 比逐张量 (0.0054) 提升 18×
通道 1 的 RMSE (逐通道): 0.0038  ← 与逐张量相同(本来就是它主导的)

额外开销: 2 个 scale × 4 bytes = 8 bytes (可忽略)

关键洞察:逐通道让每个通道按自己的范围选择 scale——通道 0 的 scale 缩小了 25 倍(0.000787 vs 0.01969),精度同等提升。而存储开销(每行 4 字节的 scale)对于 4096×4096 的权重矩阵来说只是 16 KB,完全可以忽略。

LLM 推理中,权重用逐通道对称量化激活值用逐张量非对称量化是标准配置。


5. 校准

5.1 为什么需要校准

量化激活值时需要知道 x_maxx_min 来确定 scale 和 zero_point。但激活值依赖输入数据——没有 “固定的” max/min。

校准:用代表性数据跑一遍模型,收集每层激活值的统计信息。

5.2 校准方法

方法 做法 适用
MinMax 直接用校准数据的全局 min/max 简单,但对 outlier 敏感
MSE 最小化量化前后输出的均方误差 更精确
Percentile 取 99.9% 分位数,裁掉极端 outlier 实践中常用

校准数据量:通常 128-512 个样本即可。太少统计不稳定,太多收益递减。

5.3 校准流程

1. 准备校准数据集(128-512 个代表性样本)
2. 用 FP16 模型跑 inference,hook 每层的输入/输出
3. 收集激活值的 min/max 或 histogram
4. 计算每层的 scale 和 zero_point
5. 将 scale/zp 保存为量化参数表

6. INT4 量化

6.1 INT4 的核心挑战

INT4 每个值只有 4 位(16 个离散值),量化误差比 INT8 大得多。

INT8: 256 个离散值 → 相对精度 ≈ 1/256 ≈ 0.4%
INT4: 16 个离散值  → 相对精度 ≈ 1/16 ≈ 6.25%

INT4 的极低精度使得 分组量化(group-wise quantization) 几乎成为必须——将权重矩阵分成更小的组(如 128 个值一组),每组独立量化和反量化。

6.1.1 手算示例:INT4 逐张量 vs 分组量化

假设 8 个权重值,INT4 对称量化(范围 -7 ~ 7,共 16 级):

权重: [0.15, -0.08, 0.03, 0.12, 2.10, -1.80, 1.50, -0.95]
                  ↑ 前 4 个值较小         ↑ 后 4 个值较大

逐张量 INT4(全部 8 个值共享一个 scale):

Step 1: max(|w|) = 2.10
Step 2: scale = 2.10 / 7 = 0.30

Step 3: 量化:
  前 4 个: 0.15→1, -0.08→0, 0.03→0, 0.12→0  ← 全部"塌缩"到 0 或 1
  后 4 个: 2.10→7, -1.80→-6, 1.50→5, -0.95→-3 ← 较好地保留了信息

Step 4: 反量化:
  前 4 个: 1×0.30=0.30(原0.15), 0, 0, 0  ← 几乎全部丢失
  RMSE (前4): 0.126 ← 信息几乎完全丢失!
  RMSE (后4): 0.039 ← 可接受

Group-Wise INT4(每 4 个值一组,独立 scale):

Group 0 (前4): max(|w|) = 0.15, scale₀ = 0.15 / 7 ≈ 0.0214
  0.15→7, -0.08→-4, 0.03→1, 0.12→6
  反量化: 7×0.0214=0.150, -4×0.0214=-0.086, 1×0.0214=0.021, 6×0.0214=0.128
  RMSE: 0.011 ← 精度恢复!

Group 1 (后4): max(|w|) = 2.10, scale₁ = 2.10 / 7 = 0.30
  (与逐张量对这组的结果相同)
  RMSE: 0.039

额外开销: 2 个 scale × 4 bytes = 8 bytes(总共 8 个值 × 4bit = 4 bytes 的数据)

结论:分组量化 INT4 用极小的存储代价(每 128 个值 1 个 scale,增加约 3% 存储),换回了逐通道级别的精度。这就是为什么 INT4 量化必须搭配分组量化。

6.2 GPTQ 的核心思想

GPTQ(Generalized Post-Training Quantization)[2] 是一种数据相关的权重量化算法。它不只是一个 CUDA 实现——首先是一个算法思想。

问题:简单地对整个权重矩阵做逐通道量化,虽然每个通道各自最优,但不同通道的量化误差会累积,最终对模型输出产生不可忽略的影响。

GPTQ 的解决思路——逐列量化 + 误差补偿:

for 权重矩阵 W 的每一列 (从左到右):
  1. 量化这一列的权重 → 得到整数表示 q_i
  2. 计算量化误差: Δw_i = w_i - dequantize(q_i)
  3. 将 Δw_i 乘以逆 Hessian 矩阵 H^{-1}
     → 得到这一列误差应该"分摊"到后续列的量
  4. 调整后续未量化的列: w_j -= H^{-1}[i,j] × Δw_i / H^{-1}[i,i]
     (后续列在量化前先减去误差补偿)

为什么需要 Hessian 矩阵:H^{-1} 编码了模型对每个权重的敏感度——敏感度高的权重,量化误差影响大,需要更精细的补偿。GPTQ 继承自 OBQ(Optimal Brain Quantizer)框架——两者都使用逆 Hessian 指导误差补偿。GPTQ 的核心改进是将 OBQ 的逐权重量化改为逐列量化(lazy batch update),利用行间独立性实现并行化,将复杂度从 O(n³) 降到 O(n²)。

GPTQ 的一个 epoch:对整个权重矩阵扫描一遍即可——不需要多轮迭代。这使得量化一个 7B 模型可以在数分钟内完成(需要一张 GPU/NPU 跑校准数据 + CPU 做量化计算)。

GPTQ 本身是算法——可以用 NumPy 实现。此外还有 LLM.int8()(bitsandbytes 库,混合 INT8+FP16 精度)、AWQ(基于 activation 感知的权重量化)、NF4(4-bit NormalFloat)等不同的量化方案,各有其技术路线和工程实现。


7. 量化后的推理加速原理

量化不只是省显存——还能加速计算:

层面 原理
显存带宽 INT8 数据量是 FP16 的 1/2,从 HBM 读取更少字节
计算吞吐 NPU Cube 单元对 INT8 矩阵乘法的吞吐是 FP16 的 2-4×
缓存命中 更小的数据 → 更多权重可以放入 L2/on-chip 缓存

具体计算:7B 模型单次推理约读取全部 14 GB 权重一次。在 910B3 上:

精度 权重大小 HBM 读取时间 (1538 GB/s) 相对 FP16
FP16 14 GB 9.1 ms
INT8 7 GB 4.6 ms 2× faster
INT4 3.5 GB 2.3 ms 4× faster

但加速需要硬件原生支持 INT8/INT4 计算。当前 CANN 8.0.1 的 Cube 单元支持 INT8,但 torch_npu 2.1.0 没有暴露 Hugging Face 模型的 INT8 推理路径。


8. 演示脚本

quantization_demo.py 包含:

  • 对称量化和反量化的完整实现
  • 非对称量化和反量化
  • Per-tensor vs Per-channel 精度对比
  • 量化误差的可视化分析(RMS、Max Error)

运行:python3 quantization_demo.py

另外,quantization_viz.html 提供交互式量化精度对比——调整 outlier 强度,并排对比 INT8/4/3/2 的 Scale、RMSE、利用率、HBM 节省等指标,含误差分布柱状图。

9. 参考链接