← 返回列表

量化粒度:一个 scale 管多大一片地方

1. 先理解量化到底在干什么

假设原始 FP16 权重:

W=[−1.2, −0.5, 0.1, 0.8, 1.5]W = [-1.2,\ -0.5,\ 0.1,\ 0.8,\ 1.5]

我们想把它变成 INT8。

Quantization(量化),就是:

FP16/FP32 → INT8/INT4\mathrm{FP16/FP32} \ \rightarrow\ \mathrm{INT8/INT4}

核心问题只有一个:

怎么把连续的浮点数,映射成有限的整数?

最简单的做法是对称量化:

q=round(x/s)q = \mathrm{round}(x / s)

反量化:

x^=q×s\hat{x} = q \times s

其中:

  • xx:原始浮点数
  • qq:量化后的整数
  • ss:Scale(缩放因子)——就是「刻度宽度」
  • x^\hat{x}:反量化回来的近似值(有误差,误差从哪来看下面)

手把手走一遍。 先定 ss:int8 对称量化只有 [−127,127][-127, 127] 共 255 个格,ss 取「最大绝对值除以 127」:

s=max⁡∣x∣127=1.5127≈0.0118s = \frac{\max|x|}{127} = \frac{1.5}{127} \approx 0.0118

然后逐个算 q=round(x/s)q = \mathrm{round}(x/s),再反量化回去对账:

原始 xx−1.2−0.50.10.81.5
量化 qq−102−42868127
反量化 x^\hat{x}−1.20−0.500.090.801.50

每个数最多差半格(s/2≈0.006s/2 \approx 0.006)——误差的大小由 ss 决定:ss 越大,格越粗。 反量化不是「把量化撤销」,只是用格点代表值近似原值,信息在 round 那一步就已经丢了。

所以:

量化的关键就是:Scale 怎么分配。 用谁的 max 来决定 ss?多少元素共用同一个 ss?

这就产生了 Per-Tensor、Per-Channel、Group-wise 三种粒度(granularity)。

(顺带一句:完整的量化写法 x≈s(q−z)x \approx s(q - z) 还多一个零点 zz,z=0z = 0 就是对称量化——下一节专讲。)

2. 对称 vs 非对称:多一个零点 z,图什么

§1 里用的其实是对称量化(Symmetric Quantization)——它默认了一件事:数据以 0 为中心。完整的量化写法还多一项:

x≈s(q−z)x \approx s(q - z)
  • ss:scale,刻度宽度(§1 里已认识)
  • zz:zero-point(零点)——「原值 0 映射到哪个整数」

量化公式对应变成:

q=round(x/s)+zq = \mathrm{round}(x/s) + z

z=0z = 0 就退化回 §1 的 q=round(x/s)q = \mathrm{round}(x/s)。对称(Symmetric)与非对称(Asymmetric)其实是同一套「仿射量化」(Affine Quantization)公式的两端:z=0z = 0 叫对称,z≠0z \neq 0 叫非对称。 zz 不是来添乱的,是用来解决「数据不以 0 为中心」这个问题的。

对称量化:数轴关于 0 对折

INT8 对称量化把数轴对折到 0 两边,255 格正负各半:

对称(z = 0):数轴关于 0 对折
   -127 ──────────── 0 ──────────── +127

好处:公式最简、计算最省(下面算账)。坏处:如果数据本身不对称,一半格子就白费了。

非对称量化:把数轴挪到数据上

看一个典型场景——ReLU 之后的激活,全部 ≥ 0。假设范围是 [0, 6][0,\ 6]。

对称量化:s=max⁡∣x∣/127=6/127s = \max|x| / 127 = 6/127,量化结果只会落在 0∼1270 \sim 127——负数那一半永远空着,128 个可用格(约 272^7),8-bit 实际只剩 7-bit 精度。

非对称量化:量程取满 [−128, 127][-128,\ 127] 全部 256 格——s=(max⁡−min⁡)/255=6/255s = (\max - \min)/255 = 6/255,零点 z=round(−min⁡/s)−128z = \mathrm{round}(-\min/s) - 128(让 min⁡\min 正好落到 −128-128;本例 z=−128z = -128):

数据 [0, 6],两种量法:
   对称:   s = 6/127  → q 落在 0 ~ 127,128 格 ≈ 7 bit,一半空着
   非对称: s = 6/255  → q 落在 -128 ~ 127,256 格全压进 [0, 6],格点密一倍

同样 8 bit,格点密一倍。数据有多偏,对称量化就浪费多少;非对称用 zz 把数轴挪到数据身上。

§1 的 WW 对称量法够用,是因为它最大 1.5、最小 −1.2,两边幅度接近;真正吃大亏的是这种单边数据。

分工原则一句话:数据大致关于 0 对称 → 对称量化;单边/偏斜 → 非对称量化。

代价:z 会溜进矩阵乘

非对称不是白嫖的。把 W=sw(Wq−zw)W = s_w(W^q - z_w)、x=sx(xq−zx)x = s_x(x^q - z_x) 代进 yi=∑jWijxjy_i = \sum_j W_{ij} x_j:

yi=swsx(∑jWijqxjq  −  zx∑jWijq  −  zw∑jxjq  +  zwzx)y_i = s_w s_x \left( \sum_j W^q_{ij} x^q_j \;-\; z_x \sum_j W^q_{ij} \;-\; z_w \sum_j x^q_j \;+\; z_w z_x \right)

对称(zw=zx=0z_w = z_x = 0)只剩第一项,一遍整数 GEMM 搞定。非对称多出三个修正项,但贵贱不一样:

  • −zx∑jWijq-z_x \sum_j W^q_{ij}:权重是提前知道的,每行的整数和可以预先算好——一个每行常数,并进 bias 就完事,几乎免费。
  • −zw∑jxjq-z_w \sum_j x^q_j:要对激活做全列求和,多一次额外归约——这才是贵的那个。

所以工程上的分工几乎是被这份账决定的:

  • 权重:几乎总用对称(zw=0z_w = 0)。一来权重分布本来就对称(初始化均值 0,训练后也大致对称),zw=0z_w = 0 不亏精度;二来直接砍掉最贵的那一项。
  • 激活:常非对称。ReLU/GELU 之后是单边分布,非对称把精度找回来,代价只是并入 bias 的每行常数。
  • 于是「权重对称 + 激活非对称」成了最常见的组合;SmoothQuant(§8)把激活抹平成对称分布之后,才敢全对称——W8A8 只剩一条 GEMM。

和「粒度」的关系

zz 和 ss 一样按粒度共享:per-tensor 一个 zz、group-wise 每组一个 zz(§8 里 GPTQ 说的「每组一套 scale/zero-point」就是这个)。本文后面所有「scale 管多大一片」的讨论对 zz 同样成立——一把尺子其实是一个 (s,z)(s, z) 对。

3. Per-Tensor:整个 Tensor 一个 Scale

Per-Tensor Quantization(逐张量量化):整个矩阵只算一个 ss,所有元素共用。(从本节起只谈 ss,zz 永远按同样粒度跟着走。)

假设 W∈R4×4W \in \mathbb{R}^{4\times4}:

整个矩阵 → 一个 scale

┌────┬────┬────┬────┐
│    │    │    │    │
├────┼────┼────┼────┤
│    │    │    │    │
├────┼────┼────┼────┤
│    │    │    │    │
├────┼────┼────┼────┤
│    │    │    │    │
└────┴────┴────┴────┘

即 W→sW \to s,所有元素共享 ss。

优点:

  • 非常简单:Scale 数量最少(就 1 个)
  • 存储开销最小
  • Kernel 简单,硬件实现容易

缺点:如果不同区域的数值范围差别很大,就很吃亏。

比如:

  • 区域 A:[−0.1, 0.1][-0.1,\ 0.1]
  • 区域 B:[−10, 10][-10,\ 10]

整个 Tensor 用一个 scale。为了覆盖 [−10,10][-10, 10],ss 被撑大,区域 A 的精度就会很差——A 里的数连一格都占不满,全被舍成 0。

白话:一把尺子要同时量得了蚂蚁和大象,就只能按大象做刻度,蚂蚁量出来全是 0。 LLM 权重里恰恰就有这种「大象」——outlier(离群值),极少数通道比别的通道大几十上百倍。所以 per-tensor 用在 LLM 权重上经常翻车。

4. Per-Channel:每个 Channel 一个 Scale

Per-Channel Quantization(逐通道量化):不让整个矩阵共享 scale,而是每个通道一把尺子。

比如一个 Linear 权重:

W∈Rout×inW \in \mathbb{R}^{out \times in}

通常按 Output Channel(输出通道) 切——每一行一个 scale:

             Input
        c0  c1  c2  c3
         ↓   ↓   ↓   ↓
Output 0 ──────────── → s0
Output 1 ──────────── → s1
Output 2 ──────────── → s2
Output 3 ──────────── → s3

于是量化公式变成:

qij=round(Wij/si)q_{ij} = \mathrm{round}(W_{ij} / s_i)

每一行使用自己的 sis_i——第 ii 行的 max 只决定第 ii 行的刻度,别的行管不着。

5. 为什么 Per-Channel 通常更准

假设三个通道的动态范围:

  • Channel 0:[−0.1, 0.1][-0.1,\ 0.1]
  • Channel 1:[−5, 5][-5,\ 5]
  • Channel 2:[−20, 20][-20,\ 20]

Per-Tensor:三个通道共用一把尺子,ss 由最大的 [−20,20][-20, 20] 决定。Channel 0 的动态范围 [−0.1,0.1][-0.1, 0.1] 相对 [−20,20][-20, 20] 小 200 倍——刻度是按 20 做的,0.1 的量级连一格都不到,量化精度损失殆尽。

Per-Channel:三个通道各用自己的 ss,各按各的动态范围做刻度:

  • Channel 0 → s0s_0(量程只到 0.1,刻度极细)
  • Channel 1 → s1s_1
  • Channel 2 → s2s_2

每个通道的信息都保住了。所以 Per-Channel 通常比 Per-Tensor 精度更好。

代价就是:Scale 更多。但算一笔账:out 个 FP16 scale 只占 2×out2 \times out 字节,相对整个 [out×in][out \times in] 的权重矩阵(比如 4096×4096)只有 0.1%——几乎免费。

更进一步:为什么偏偏是「输出通道」这个方向?

两个原因,缺一不可:

  1. 数学上通道天然独立:yi=∑jWijxjy_i = \sum_j W_{ij} x_j——输出第 ii 个元素只用到第 ii 行权重。第 3 行有 outlier,只坑 yy 的第 3 个元素,别的通道不该为它买单。所以「每行一把尺子」是误差隔离的天然边界。
  2. 硬件上几乎免费:把量化权重代回矩阵乘:
yi=∑jXij si Wijq=si∑jXijWijqy_i = \sum_j X_{ij}\, s_i\, W^q_{ij} = s_i \sum_j X_{ij} W^q_{ij}

sis_i 只依赖输出下标 ii,可以提到求和号外面——整数 GEMM 照常算(int8 乘、int32 累加),算完再给每个输出行乘上自己的 sis_i 就行。反过来,如果按输入方向(按列)切,ss 的下标就留在求和号里,提不出来,整数 GEMM 没法直接做。

判断口诀:scale 的下标只要不出现在求和号(规约维度)里,就是免费的。 这也是所有标准实现(TensorRT、cuBLAS int8、ONNX 的 per-axis)都沿输出通道的原因。

6. Group-wise:组大小可调的那一档

Group-wise Quantization(分组量化):每 gg 个元素共用一把尺子,gg 叫 group size(组大小)。

先给一个统一视角——三种粒度其实是同一件事:把矩阵按「组」切,差别只是组多大:

组 = 整个矩阵   → per-tensor    1 把尺子
组 = 一行       → per-channel   out 把尺子
组 = g 个元素   → group-wise    out·in/g 把尺子

组大小连续可调。比如 16 个权重:

  • group size = 16(整个张量)→ 1 个 scale(就是 per-tensor)
  • group size = 8 → 2 个 scale
  • group size = 4 → 4 个 scale
16 个权重,group size = 4:xxxx xxxx xxxx xxxx → s0 s1 s2 s3(4 个 scale)
16 个权重,group size = 8:xxxxxxxx xxxxxxxx     → s0 s1(2 个 scale)

组越小,尺子越多、每把尺子看到的值域越窄、格点越密。注意:组也可以切得比一行还粗(几个通道一组,当 per-tensor 和 per-channel 之间的中间档用);但 LLM 里说的 GroupSize = 128 是比一行更细的切法(一行通常有几千个元素):

128 个权重 → 共享一个 scale;下一个 128 个 → 共享另一个 scale

为什么 4-bit 时代需要它? int8 有 127 格,格点密,per-channel 一把尺子量一整行也够用。但 int4 只有 15 格,比 int8 粗约 8.5 倍——同样的行内不均匀,8-bit 吃点亏不碍事,4-bit 直接崩。所以 4-bit 权重的尺子必须比 per-channel 更贴身:bit 数越少,尺子必须切得越细。

代价:尺子自己开始占地。group size 128 时,每 64 字节的权重背 4 字节的 scale+zero(FP16),元数据约 6%;另外 int4 没有原生整数 GEMM 硬件,group-wise 要靠「先反量化回 FP16 再算」或专用 kernel(GPTQ 的 Marlin、AWQ kernel 等)——粒度越细,kernel 越难写。

7. 三者放一起

权重矩阵 W(4 行 × 8 列)的三种切法
per-tensor           per-channel          group-wise(g=4)
┌───────────────┐   ┌───────────────┐    ┌─────┬─────┐
│               │   │ s1 ────────── │    │ s1  │ s2  │
│   一把 (s,z)   │   │ s2 ────────── │    ├─────┼─────┤
│   量整个矩阵   │   │ s3 ────────── │    │ s3  │ s4  │
│               │   │ s4 ────────── │    ├─────┼─────┤
│               │   │               │    │ s5  │ s6  │
│               │   │               │    ├─────┼─────┤
│               │   │               │    │ s7  │ s8  │
└───────────────┘   └───────────────┘    └─────┴─────┘
1 把尺子             4 把尺子              8 把尺子

结论链:

GroupSize↓ ⇒ Scale 数量↑ ⇒ 粒度更细 ⇒ 通常精度更好\mathrm{GroupSize} \downarrow \ \Rightarrow\ \text{Scale 数量} \uparrow \ \Rightarrow\ \text{粒度更细} \ \Rightarrow\ \text{通常精度更好}

三种粒度的本质 trade-off:

粒度Scale 数量精度额外开销
Per-Tensor最少(1 个)较低最低
Per-Channel较多(out 个)较高很低(约 0.1%)
Group-wise(g=128)最多(out·in/128 个)最高中等(约 6%)

所以可以画成一条谱:

粒度由粗到细(组大小由大到小)
per-tensor   → 组 = 整个矩阵   → 1 把尺子
per-channel  → 组 = 一行       → out 把尺子
group-wise   → 组 = 128 元素   → out·in/128 把尺子
越往右:尺子越多 · 值域越窄 · 格点越密 · 精度越好 · 元数据越多

8. 实际工作里谁在用

8-bit 时代(格点密,per-channel 就是甜点):

  • 标准套餐:权重 per-channel + 激活 per-tensor(CNN 和 QAT 时代的默认组合)。
  • SmoothQuant:把激活里的 outlier「搬」到权重——激活第 jj 列除小、权重第 jj 行乘大。这招能成立,正是因为权重本来就是 per-channel 的:被抬高的那一行有自己的尺子,不会被别的行拖累;激活被抹平后可以安心 per-tensor,凑出硬件最爱的 W8A8。
  • LLM.int8():激活按 token、权重按输出通道量化(都满足 §5 的口诀,scale 能提出求和号);再把 outlier 所在的个位数隐藏维度抠出来单独走 FP16——「少数维度用高精度」,是与粒度正交的另一条路。

4-bit 时代(格点只剩 15 个,必须切到组):

  • GPTQ:group size 128 逐组量化,每组一套 scale/zero-point;再用二阶 Hessian 信息「每量化一列,把误差补偿给还没量化的列」——粒度负责尺子贴身,Hessian 补偿负责剩下的误差。
  • QLoRA:NF4 权重按块 64 存 scale;「双重量化」再把 scale 本身按 256 块量化成 8-bit,元数据总开销压到 0.127 bit/参数(约 3%)。
  • AWQ:观察到约 1% 的「显著通道」承载激活幅度主体,按通道搜索缩放因子保护它们——4-bit 权重 + 16-bit 激活即可不掉点。
  • MX 微缩放格式:块大小固定 32、scale 取 2 的幂,Blackwell 起硬件原生支持——粒度第一次进了硬件标准。

选择三句话:

  1. bit 数决定粒度下限:8-bit 用 per-channel 就够;4-bit 必须 group-wise(或 AWQ 式激活感知缩放)兜底。
  2. outlier 决定切不切:值域均匀就粗切甚至不切;有 outlier 就切到「每把尺子只看到均匀的一片」为止。
  3. 再细没有意义:每元素一把尺子 = 元数据和数据一样大,等于没量化。所以实际停在 group 32~128。

9. 边界与延伸

  • 粒度 vs 裁剪(clipping)是正交的两件事:粒度决定「谁和谁共用尺子」,clipping 决定「量程取到极值的百分之几」(比如只量到 99.9% 分位,把最野的 outlier 裁掉)。GPTQ、AWQ 都是切细 + 裁剪并用。
  • 激活侧的粒度:per-tensor 激活同样怕 outlier token;LLM.int8、ZeroQuant 用 per-token(每行现算 max)。但 per-token 要多一个运行时算 max 的 kernel——SmoothQuant 抹平后能退回 per-tensor,正是它的卖点。
  • 与本站其他文章联动:8-bit Adam 的动量量化也是 block-wise(块 2048),把优化器状态从 8Ψ 压到 2Ψ,见 训练显存账本;QLoRA 把 NF4 双重量化与 LoRA 结合,见 LoRA 那篇。

全称速查

缩写/术语全称含义
PTQPost-Training Quantization训练后量化,不训练直接量
QATQuantization-Aware Training量化感知训练,训练时就模拟量化
GEMMGeneral Matrix Multiply通用矩阵乘
scale—量化步长/缩放因子
zero-point—零点
对称量化Symmetric Quantization零点固定为 0(z=0),适合以 0 为中心的分布
非对称量化Asymmetric Quantization也叫仿射量化(Affine),带零点 z,量程覆盖 [min, max]
outlier—离群值
MXMicroscaling微缩放格式,块大小 32 的硬件量化格式

10. 总结

  • 量化的精度 = 值域 ÷ 格数;scale 怎么分配是量化的一切。
  • 对称 vs 非对称:z=0z = 0 公式最省但要求数据以 0 为中心;单边数据(如 ReLU 后的激活)用非对称把 256 格全用上。权重几乎总对称、激活常非对称,是矩阵乘修正项的账算出来的。
  • 三种粒度是同一个「组」概念的三个档位:组 = 全张量(per-tensor)、组 = 一行(per-channel)、组 = g 个元素(group-wise)。
  • per-tensor 死于 outlier;per-channel 是数学和硬件共同选出的甜点(通道独立 + scale 后乘免费);group-wise 是 4-bit 时代的刚需,代价是约 6% 的元数据和专用 kernel。
  • 一句话:粒度做的事,就是让每一把尺子的「值域」尽量窄、尽量均匀;而尺子越多,尺子自己也越占地。

参考文献

  1. Jacob et al. Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. arXiv:1712.05877.
  2. Krishnamoorthi. Quantizing Deep Convolutional Networks for Efficient Inference: A Whitepaper. arXiv:1806.08342.
  3. Dettmers et al. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339.
  4. Xiao et al. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438.
  5. Frantar et al. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323.
  6. Lin et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978.
  7. Dettmers et al. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314.