模型量化:从手算推演到可视化验证
量化是将浮点权重/激活值映射到低比特整数的压缩技术。核心矛盾是范围 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 | 2× |
| 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 量化误差的来源
对称量化的误差来自两个不可兼得的需求:
-
表示范围:scale = max( x ) / 127。如果存在离群值(outlier),scale 被拉大,大量正常值被粗粒度表示 - 表示精度: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_max 和 x_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 | 1× |
| 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. 演示脚本
- 对称量化和反量化的完整实现
- 非对称量化和反量化
- Per-tensor vs Per-channel 精度对比
- 量化误差的可视化分析(RMS、Max Error)
运行:python3 quantization_demo.py
另外,quantization_viz.html 提供交互式量化精度对比——调整 outlier 强度,并排对比 INT8/4/3/2 的 Scale、RMSE、利用率、HBM 节省等指标,含误差分布柱状图。
9. 参考链接
- Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference (Jacob et al., 2018)
- GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers (Frantar et al., 2022)
- LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale (Dettmers et al., 2022)
- A Visual Guide to Quantization (Maarten Grootendorst)