模型量化就是把权重和激活从 FP32/FP16 压成 INT8 甚至 INT4,用更少的比特表示更多的参数。它直接解决两个大模型时代最现实的痛点:显存装不下、访存太慢。但量化不是简单地"四舍五入",对称还是非对称、per-tensor 还是分组、怎么处理离群点——每个选择都在精度和速度之间做权衡。这篇把量化的数学和工程拆清楚。

直觉:为什么少几个比特就够了

FP16 一个数 2 字节,INT8 是 1 字节,INT4 半字节。一个 7B 参数的模型,FP16 权重约 14 GB,INT4 直接降到约 3.5 GB——这是能不能塞进单张消费级显卡的差别。

更深一层的收益在访存。上一篇讲过,大模型推理(尤其 batch 小时)往往是显存带宽受限的:瓶颈不在算,而在把权重从 HBM 搬到计算单元。权重压成 1/4 大小,搬运量就少 3/4,带宽受限的算子能近乎线性提速。这才是量化在 LLM 推理里最实在的价值——省显存是入场券,省带宽是加速器。

而它能成立,是因为神经网络对权重扰动有相当的鲁棒性:训练好的网络是冗余的,权重里的大部分精度对最终输出并不敏感,砍掉低位信息影响有限——前提是量化方案足够讲究。

机制:仿射映射

量化的核心是一个把浮点区间映射到整数区间的仿射变换。给定浮点值 rr,量化后整数 qq

q=round(rs)+zq = \text{round}\!\left(\frac{r}{s}\right) + z

反量化(解码回近似浮点):

r^=s(qz)\hat{r} = s \cdot (q - z)

两个参数:

  • scale ss(缩放因子,浮点):决定一个整数 step 对应多大的浮点跨度。
  • zero-point zz(整数):浮点 0 对应的整数值,保证 0 能被精确表示。

ss 通常由数据范围决定。设要量化的张量浮点范围是 [rmin,rmax][r_{\min}, r_{\max}],整数范围 [qmin,qmax][q_{\min}, q_{\max}](INT8 是 [128,127][-128, 127]):

s=rmaxrminqmaxqmins = \frac{r_{\max} - r_{\min}}{q_{\max} - q_{\min}}

对称 vs 非对称

这是第一个关键分叉。

非对称(asymmetric)量化用上面完整的 sszz,能精确贴合任意 [rmin,rmax][r_{\min}, r_{\max}],对范围不以 0 为中心的数据(典型如 ReLU 后的激活,全非负)更省比特。代价是计算里始终带着 zero-point,矩阵乘展开后会多出涉及 zz 的交叉项,硬件实现更繁琐。

对称(symmetric)量化强制 z=0z = 0,范围取 [max(r),+max(r)][-\max(|r|), +\max(|r|)]

s=max(r)qmax,q=round(r/s)s = \frac{\max(|r|)}{q_{\max}},\qquad q = \text{round}(r / s)

零点固定为 0,矩阵乘里没有 zero-point 交叉项,硬件友好、计算更快。代价是若数据分布偏向一侧,会浪费掉一半整数表示范围。

工程上的常见折中:权重用对称(权重分布通常近似零均值对称,且对称利于加速),激活用非对称或对称视分布而定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np

def quantize_symmetric_int8(w):
qmax = 127
s = np.abs(w).max() / qmax # 对称:scale 由绝对值最大值定
q = np.clip(np.round(w / s), -qmax, qmax).astype(np.int8)
return q, s

def dequantize(q, s):
return q.astype(np.float32) * s

w = np.random.randn(4, 4).astype(np.float32)
q, s = quantize_symmetric_int8(w)
err = np.abs(w - dequantize(q, s)).mean() # 平均量化误差

粒度:per-tensor、per-channel 与分组量化

第二个关键分叉是 scale 的共享粒度。用一个 ss 量化整个张量(per-tensor)最省、最快,但有个致命问题:只要张量里有一个离群点(outlier)max(r)\max(|r|) 就被它撑大,导致绝大多数正常值被挤进极少数几个整数 step 里,有效精度暴跌。而大模型的激活/权重里离群点恰恰很常见。

应对方法是缩小共享范围:

  • per-channel / per-axis: 每个输出通道(或每行/每列)一个独立的 ss。离群点只影响它所在的那个通道,其余通道精度不受牵连。开销是多存一组 scale,几乎可忽略。
  • 分组量化(group-wise / block-wise): 把一行再切成固定大小的小组(如每 64 或 128 个元素一组),每组一个 ss。这是 INT4 量化的标配——4 比特只有 16 个表示档位,容错极低,必须靠细粒度分组把每个小局部的范围卡准,才能压住误差。代价是 scale 的存储和计算开销随分组变细而上升,是典型的精度↔开销权衡。

直觉:粒度越细,每个 ss 服务的数值范围越窄、越均匀,量化越精确;但元数据越多、kernel 越复杂。INT8 常 per-channel 就够,INT4 几乎必须分组。

PTQ vs QAT,以及离群点这个老大难

按"何时量化"分两条路线:

  • PTQ(训练后量化): 模型训完直接量化,只需少量校准数据估计激活范围。快、便宜、无需重训,是 LLM 部署的主流。
  • QAT(量化感知训练): 训练时就插入"伪量化"节点模拟量化误差,让模型学着适应。精度更高、尤其低比特时,但要完整重训,成本高。两者间的取舍本质是"精度 vs 训练成本"。

低比特 PTQ 的最大敌人始终是激活里的离群点。围绕它有一系列经典思路:把难量化的离群通道挑出来保留高精度、其余走低比特(混合精度);或把激活的量化难度"迁移"一部分到权重上让两边都好量化(等价缩放);或借助少量校准数据按二阶信息逐层补偿量化误差。它们的共同目标,都是不让极少数离群值毁掉整体精度。

工程权衡与常见误区

  • 不是所有层都该同等量化。 对量化敏感的层(如某些归一化、最后的输出投影)常保留更高精度;first/last layer 尤其要小心。
  • “权重量化"≠"全量化”。 只量化权重(weight-only)能省显存和带宽,但矩阵乘往往仍在 FP16 域算(运行时反量化);只有权重和激活都量化,才能真正用上 INT8 Tensor Core 的算力。两者收益来源不同,别混淆。
  • 校准数据要有代表性。 PTQ 的激活范围靠校准集估计,分布不匹配会让 scale 失真,精度崩。
  • INT4 别用 per-tensor。 4 比特配粗粒度几乎必然掉点严重,分组是底线。
  • 量化的加速依赖硬件支持。 没有对应整数算子或 Tensor Core 支持时,反量化开销可能吃掉收益,甚至更慢。

小结

模型量化是一个"用比特换显存和带宽"的工程,核心是仿射映射 q=round(r/s)+zq = \text{round}(r/s) + z。三个旋钮定全局:对称还是非对称(零点 vs 硬件效率)、粒度多细(per-tensor → per-channel → 分组,越细越准但元数据越多)、PTQ 还是 QAT(成本 vs 精度)。而贯穿始终的真正难点是离群点——它逼着我们从 per-tensor 走向分组、催生出各种混合精度与误差补偿方案。理解这几组权衡,你就能判断一个量化配置到底是在省钱还是在悄悄掉点。