0. 前言
你有没有好奇过, 为什么一个 7B 参数的模型要占 14GB 显存?
但在回答之前, 我想先确认一个最基本的问题: “7B” 和 “14GB” 背后, 到底是怎么算出来的?
0.1 先聊点最基础的: bit 和 byte
计算机里最小单位是一个 bit (比特), 它只能表示 0 或 1 两种状态.
但 1 个 bit 能存的数字太少了, 所以通常 8 个 bit 组成一个 byte (字节):
有了 byte 以后, 我们习惯用 byte 来计量内存/显存:
- 1 KB = 1024 bytes
- 1 MB = 1024 KB
- 1 GB = 1024 MB
那一个数字要占多少个 byte 呢? 取决于我们用多少 bit 来表示它.
0.2 n 个 bit 能表示多少种状态?
对于整数来说: n 个 bit 可以表示 种不同的状态.
比如:
| bit 数 | 能表示的状态数 | 取值范围 (无符号) | 取值范围 (有符号) |
|---|---|---|---|
| 1 bit | 2 | 0 ~ 1 | -1 ~ 0 |
| 4 bit | 0 ~ 15 | -8 ~ 7 | |
| 8 bit | 0 ~ 255 | -128 ~ 127 | |
| 16 bit | 0 ~ 65535 | -32768 ~ 32767 |
所以当我说”一个 INT4 数”时, 意思是它用 4 个 bit 来存, 只能取 16 种不同的值. 有符号时范围是 到 .
0.3 二进制怎么转成十进制?
比如 4-bit 二进制数 1101:
如果是有符号数 (用补码表示), 最高位是符号位:
- 最高位 = 0: 正数, 直接算
- 最高位 = 1: 负数, 先取反再加 1, 再算
这就是为什么 INT4 的范围是 (二进制 1000) 到 (二进制 0111).
0.4 那浮点数呢?
深度学习里用的不是整数, 而是浮点数. 浮点数用科学计数法来存:
以 FP32 (32 位浮点数) 为例, 它由 3 部分组成:
| 部分 | 位数 | 含义 |
|---|---|---|
| 符号位 (sign) | 1 bit | 0 为正, 1 为负 |
| 指数位 (exponent) | 8 bits | 决定数值范围, 偏置 127 |
| 尾数位 (mantissa) | 23 bits | 决定数值精度 |
所以 FP32 能表示大约 , 精度约 7 位有效数字. BF16 保留了 FP32 的 8 位指数 (因此范围一样大), 但尾数砍到 7 位 (精度更低但范围不变).
| 格式 | 总 bit | 指数 bit | 尾数 bit | 字节数 | 数值范围 |
|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | 4 bytes | |
| FP16 | 16 | 5 | 10 | 2 bytes | |
| BF16 | 16 | 8 | 7 | 2 bytes | |
| INT8 | 8 | — | — | 1 byte | -128 ~ 127 |
| INT4 | 4 | — | — | 0.5 byte | -8 ~ 7 |
注意: 这里 1 byte = 8 bits, 所以一个 INT4 数是 0.5 个 byte.
0.5 回到最初的问题
个参数, 每个参数用 BF16 存 (2 bytes/参数):
如果量化到 INT4 (0.5 bytes/参数):
一下子省了 4 倍的空间!
于是大家想: 能不能用更少的 bit 来存这些数字, 同时尽量不损失模型的质量?
这听起来像是一个数据压缩问题——用较少的信息表示同样的内容, 尽量保留原始信息. 但和一般的压缩(比如 zip)不同, 量化有一个重要特点: 我们不需要精确还原每个参数, 我们只需要最终模型输出的质量尽量不变.
这就给了我们操作空间.
1. 设定目标
先想清楚我们在做什么.
假设有一个权重矩阵 , 原始精度是 BF16. 我们想把它转成 INT4 来存, 即每个数只占 4 个 bit.
我们的操作流程是:
推理时计算的是 , 而不是 . 所以误差就是:
我们希望这个误差尽量小.
这就是量化问题的核心: 找到一个映射函数 (实数到 4-bit 整数), 使得反量化后的 与 的误差最小.
2. 最简单的尝试: 四舍五入
最直观的想法: 把每个浮点数四舍五入到最近的整数.
比如 , , .
但马上发现问题了: 模型参数的取值范围一般是 左右, 而 INT4 只能表示 到 . 这就像用一把 16 米长的尺子去量 2 米的东西——大部分刻度都浪费了. 四舍五入后, 几乎所有数都变成 0 或 ±1, 信息全丢了.
所以我们需要缩放. 先除以一个 scale, 让数据的范围匹配整数的范围, 再四舍五入. 这就是量化的核心思想.
3. 对称量化: 引入 scale
先考虑最简单的情况: 数据分布在 0 两侧, 大致对称.
3.1 推导 scale
我们希望把 范围内的浮点数映射到 范围内的整数. 对于对称 INT8, , .
目标: 最小化量化误差. 假设有一组数值 , 我们选择的 scale 是 , 量化函数是:
反量化:
量化误差:
这个 MSE 的最小化没有完美的闭式解, 但有一个在实际中效果很好的启发式: 让 scale 刚好覆盖数据的最大范围.
为什么? 如果 太大, 那么 就很小, 四舍五入后很多不同的 会映射到同一个整数, 量化颗粒度太粗. 如果 太小, 会有一部分数据超出 的范围, 发生截断 (clipping), 引入截断误差.
最佳 scale 就是在颗粒度误差和截断误差之间取平衡. 对于接近均匀分布的数据, 覆盖最大范围的 scale 是近似最优的. 但对于长尾分布 (模型权重的分布通常中间密两边疏), 选择稍微小一点的 scale (允许少量截断) 反而能降低整体 MSE——因为截断的是极少数离群点, 而保留的精度给大部分值带来了好处.
我们把这个重要的权衡点记下来, 后面 AWQ 会用到.
3.2 直观例子
假设权重值: , 量化到 INT8 ().
| 原始值 | round | 反量化 | 误差 | |
|---|---|---|---|---|
| 0.1 | 10.58 | 11 | 0.104 | +0.004 |
| -0.3 | -31.75 | -32 | -0.302 | -0.002 |
| 0.7 | 74.07 | 74 | 0.699 | -0.001 |
| -0.9 | -95.24 | -95 | -0.898 | +0.002 |
| 1.2 | 126.98 | 127 | 1.200 | 0.000 |
可以看到误差都在 以内, 相对误差不到 1%. 在小数值上的相对误差会大一些 (0.1 → 0.104, 误差 4%), 但绝对值很小.
3.3 对称量化的局限性
如果数据分布不对称怎么办? 比如 ReLU 之后的激活值全是正数 .
用对称量化, 正半轴用 , 负半轴 完全浪费了, 相当于只用了一半的精度预算. 可以用非对称量化来补救.
4. 非对称量化: 引入 zero_point
4.1 推导
非对称量化不再要求映射关于 0 对称, 而是允许平移. 引入两个参数: scale 和 zero_point .
量化:
反量化:
其中 是一个整数, 使得 映射到 . 参数怎么确定?
假设数据范围是 , 量化范围是 (对 INT8 就是 ).
推导: 把数据范围映射到整数范围:
线性映射就是:
等价于上面 和 的形式 (验证一下: 代入 , 得到 ).
把公式整理成更常用的形式:
其中 , .
4.2 对称 vs 非对称, 选哪个?
| 对称 | 非对称 | |
|---|---|---|
| 参数数量 | 1 个 scale | 2 个 (scale + zero_point) |
| 计算效率 | 高 (无额外减法) | 低 (多一步减法) |
| 适合数据 | 对称分布 (如权重) | 不对称分布 (如激活值) |
| 硬件支持 | 几乎所有硬件 | 部分硬件优化不足 |
实践中: 权重用对称量化, 激活值用非对称量化.
5. 量化的误差到底从哪里来?
搞清楚了公式, 现在深入看看误差来源.
把反量化后的值写成:
括号里的项是四舍五入的误差 , 范围是 . 所以:
量化误差就是 , 最大值 . 这告诉我们两件事:
- scale 越大, 误差越大. 数据范围大的张量, 量化损失也更严重.
- 量化误差的上限是固定的 (), 不管原始值大小.
第二条特别重要: 如果一个权重值本来就很小 (比如 ), 量化误差 可能跟它本身一样大! 而一个大的权重值 (比如 ), 同样 的误差占比不到 1%.
这就引出了一个关键洞察: 不是所有权重对最终结果的影响都一样大.
计算 时, 某个权重 对输出的贡献是 . 如果 很大, 或者它对应的激活值 很大, 那这个权重的量化误差就会被放大. 反过来, 如果一个权重很小, 即使它的相对误差很大, 对最终结果的绝对影响也很小.
这个洞察是后面 AWQ 的核心.
6. 从数学到工程: 量化粒度
上面讲的都是针对一个张量的整体量化. 但一个模型有几十上百层, 每层的参数分布可能差别很大.
量化粒度就是每多少个参数共用一个 scale:
- Per-tensor: 整个权重矩阵 1 个 scale, 最简单但精度最差
- Per-channel: 每个输出通道 1 个 scale (对线性层来说, 就是权重矩阵的每一行), 实际中最常用
- Per-group: 每 个参数 1 个 scale (比如 ), 精度最高但存储开销也大
为什么? 从信息论角度理解: 每组独立算 scale, 相当于给每组分配了独立的”精度预算”, 可以针对该组的实际分布来优化. 缺点是需要额外存储 scale 值 (一般用 FP16 或 FP32), group size 越小, 额外存储占比越大. 例如 group size = 32, 每 32 个值存 1 个 16-bit scale, 额外开销占 bit/参数——对于 4-bit 量化来说, 相当于增加了 12.5% 的存储.
7. GPTQ: 有脑子的量化
上面讲的都是”独立量化每个参数”. GPTQ 做了一个更聪明的选择:量化参数时考虑参数之间的相互作用.
7.1 从 OBS 说起
GPTQ 的前身是 Optimal Brain Quantizer (OBQ), 它的核心思路来自最优脑损伤 (Optimal Brain Damage) 的思想: 衡量移除某个参数对损失函数的影响, 优先量化影响小的参数.
对于一个训练好的网络, 损失函数 在最优参数 附近的二阶泰勒展开是:
其中 是 Hessian 矩阵 (二阶偏导数矩阵, 衡量损失函数在各方向上的曲率). 对角线元素 越大, 说明损失函数在参数 方向上越”陡峭”, 这个参数越重要.
推导: 如果我们量化 (移除) 参数 , 最优的补偿是调整其他参数来最小化损失. OBQ 证明, 最小损失的调整量是 , 对应的损失增量就是 .
注意这里 在分母. 的含义是”移除参数 后, 其他参数能补偿的程度”:
- 如果 很大: 说明损失函数在这个方向上很”平缓”, 移除 后其他参数很容易补偿 → 损失增量小 → 这个参数不重要
- 如果 很小: 说明损失函数在这个方向上很”陡峭”, 的位置很关键 → 损失增量大 → 这个参数很重要
7.2 GPTQ 的工程优化
OBQ 每次量化一个参数后要更新 Hessian 逆矩阵, 复杂度 , 对大规模模型不可行.
GPTQ 做了三个关键工程优化:
- 固定顺序: 不再贪心选择”影响最小的参数”, 而是直接按列从左到右量化
- 懒惰更新: 批量量化多列后再更新 Hessian, 利用矩阵乘法的 GPU 加速
- Cholesky 预分解: 预先对 Hessian 做 Cholesky 分解 (), 避免逐次求逆. Cholesky 分解是一种将对称正定矩阵分解为下三角矩阵乘以其转置的方法, 比直接求逆更高效稳定.
最终 GPTQ 可以在几个小时内量化 175B 的模型, 且 INT4 精度损失极小.
算法流程 (简化的伪代码):
输入: 权重矩阵 W, 校准数据 X, 量化精度 b1. 计算 Hessian: H = 2 X^T X2. 对 H 做 Cholesky 分解3. for 每一列 j in W: a. 量化第 j 列: W_q[:, j] b. 计算量化误差: err = W[:, j] - W_q[:, j] c. 把误差按 Hessian 信息"补偿"到未量化的列上步骤 3c 是关键: 量化第 1 列造成的误差, 会被分摊到后面的列上, 后面的参数会”吸收”前面的量化误差. 这就好比: 你前面做错了事, 后面的人帮你兜着.
7.3 GPTQ 的精度
实际经验: GPTQ INT4 的精度损失一般在 1% 以内 (以 MMLU 为基准). 对于大模型 (70B+), 损失甚至更小, 因为参数越多, 量化误差越容易被”稀释”.
8. AWQ: 重要通道保护
AWQ 的出发点很简单: 不是所有权重都值得平等对待.
8.1 关键观察
AWQ 的作者发现了一个有趣的现象: 权重中约 1% 的通道 (channel) 对模型质量影响巨大. 这些通道的特点是——它们对应的激活值 (activation) 幅度特别大.
回忆前面说的: 计算 时, 某个权重 对输出的贡献是 . 如果激活值 很大, 那么这个通道的微小量化误差都会被放大.
8.2 AWQ 的做法
AWQ 的做法和直觉相反——它不直接保留这些重要通道的高精度, 而是用一个巧妙的技巧来”保护”它们:
- 用少量校准数据跑一遍, 找到激活值幅度大的通道
- 对这些通道的权重乘以一个大于 1 的缩放因子
- 量化缩放后的权重
- 在推理时, 对相应的激活值除以 , 保证计算结果不变
数学上:
其中 是 的量化版本. 由于 变大了, 在同样的量化范围下, scale 相应增大, 但更重要的是: 重要通道的权重被放大后, 量化相对误差变小了 (因为四舍五入的绝对误差 相对于放大后的值变小).
这就像在教育预算中, 给更需要支持的学校多分配资源——总量不变, 但分配更合理.
8.3 AWQ vs GPTQ
| GPTQ | AWQ | |
|---|---|---|
| 核心思路 | 量化误差补偿 | 保护重要通道 |
| 需要校准数据 | 是 | 是 |
| 计算开销 | 高 (需要 Hessian) | 低 (只需要激活值统计) |
| 精度 (INT4) | 略好 | 相当 |
| 主要贡献 | 大规模高效量化 | 简单有效的重要性感知 |
两种方法可以结合使用, 不少实际部署方案会先用 AWQ 的思路找重要通道, 再用 GPTQ 的方法做量化.
9. 总结
回头看看量化到底在做什么:
核心思想: 用更少的 bit 存数字, 最小化对输出的影响.
数学本质: 找到缩放 和偏移 , 使得 的误差最小.
关键权衡:
| 权衡 | 选择 | 效果 |
|---|---|---|
| scale 大小 | 大 → 覆盖范围大但精度粗 | 小 → 精度细但可能截断 |
| 量化粒度 | 粗 → 速度飞快精度低 | 细 → 精度高但开销大 |
| 量化方法 | 独立量化 → 简单 | 关联量化(GPTQ) → 精度好但慢 |
| 通道保护 | 一视同仁 → 简单 | 区别对待(AWQ) → 精度更好 |
一句话: 量化就是用精度换效率, 关键是怎么在尽可能少丢精度的情况下, 做到极致的效率.
参考资料
- Gray & Neuhoff, Quantization. IEEE Transactions on Information Theory, 1998. — 量化信息论的经典综述
- GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. Frantar et al., ICLR 2023. arXiv:2210.17323
- AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. Lin et al., MLSys 2024. arXiv:2306.00978
- Nagel et al., A White Paper on Neural Network Quantization. arXiv:2106.08295
- llama.cpp GGUF format. GitHub