min-max 量化手推

整门量化课唯一的”手推”环节,用 min-max 方法把 0~1000 压到 int8 的 −128~128,把 scale 和 zero_point 这两个量化的核心参数从头算一遍。算通了这一遍,QAT/PTQ/DQ 的区别才有落脚点。

手算量化 min-max 原图

图注:原图分左右两块。左边是映射关系示意图:上轴是原始实数域(-|max| … 0.0 … +|max|),下轴是 int8 整数域(-127 … 0 … 127),中间两条斜线表示线性映射,虚线把实数 0.0 与整数 0 对齐,这条对齐正是 zero_point 干的活。左下灰框给出公式 1:x_q=\mathrm{round}(x/\text{scale} + \text{zero\_point});再下面红字是课后练习:”−123 ~ 999 移动到 1 到 3 范围”。右栏是 min-max 的四步计算过程正文。


一、这份材料解决什么问题

前面《二进制与字节》说明了 int8 只有 256 个离散值,所以必须建立”实数 → 整数”的映射。但映射的具体参数怎么定?这张图回答了这个问题:

  • scale 决定分辨率(一格整数代表多少实数);
  • zero_point 决定对齐方式(实数 0 落在哪个整数上)。

而且它刻意用手算而不是调 API 的方式讲,因为在真实工程里,scale/zero_point 的取值来源(是统计训练数据的 min/max?还是用 KL 散度搜出来的?还是每通道单独统计?)正好是 PTQ 与 QAT、per-tensor 与 per-channel 的分水岭。手算过一遍,才知道那些自动化工具在你背后做了什么。


二、核心结论

  1. 量化的核心公式只有一条:x_q = \mathrm{round}(x/\text{scale} + \text{zero\_point}),反量化是它的逆运算 x \approx \text{scale}\times(x_q – \text{zero\_point})。
  2. min-max 量化分两步走:先缩放(除法,把范围压小)→ 再平移(加法,把起点对齐)。原图的第 2、3 步就是在强调”范围一致了但起始位置还不一样”。
  3. scale = (max − min) / (q_max − q_min)。原图例子里 (1000-0)/(128-(-128)) = 1000/256 = 3.9,所以 scale 也叫”缩放倍数”。
  4. zero_point 由”把实数 0 对齐到某个整数”这个约束反解出来。原图第 4 步用 q_max − max = 128 − 256 = −128 求出平移量,说明要把对齐好的数据整体减 128。
  5. min-max 是最简单但最怕离群值的量化方式:一个极端大值会把 scale 撑大、把其余数据挤到几格整数里,精度断崖式下跌。这就是后来 per-channel / per-group 缩放和 KL 校准存在的理由。

三、知识地图

左栏(图 + 公式 + 练习题)

元素 内容
上轴(实数域) -|max| ←— 中心 0.0,→ +|max|
下轴(int8 整数域) -127 ←— 中心 0,→ 127
连线 两条斜线连接两端,表示线性映射;虚线连接 0.0 与 0
公式 1 x_q = \mathrm{round}\left(\dfrac{x}{\text{scale}} + \text{zero\_point}\right)
练习题(红字) −123 ~ 999 移动到 1 到 3 范围

右栏(标题:最简单的一种量化方式 min-max 量化计算过程)

目的是将一个大范围的数据压缩到小范围去表示。

  1. 计算缩放的比例。假设原数据范围是 0 到 1000,要压缩到 −128 到 128 的区间,需要缩小 (max−min)/(max−min),即 1000/256 = 3.9 倍。
  2. 把原始数据缩小 3.9 倍,原始数据的范围就变成了 0~256。此时两者范围已经一致了,但是起始位置还不一样。
  3. 把 0~256 的数据平移到 −128~+128 位置上,对齐。
  4. q_{max}-max = 128-256 = -128,说明要移动 −128,把 0~256 的原始数据 −128,就变成了 −128~+128。这样,0~1000 的原始数据就变成 −128~128 了。

四、原理详解

4.1 先看清那张示意图在说什么

示意图画的是两个数轴之间的线性映射:

实数域:   -|max| ————————————  0.0 ———————————— +|max|
                \                |                /
                 \               |               /
                  \              |              /
整数域:    -127 —————————————   0 ————————————— 127

要点有三个:

  1. 两条轴的范围都被”归一化”成了对称区间:实数域是 [-|max|, +|max|],整数域是 [-127, 127]。对称意味着 0 在正中间,这让映射公式最简洁。
  2. 中间的虚线段把 0.0 和 0 连起来。这体现的是量化的一条硬性约束:实数零必须精确映射到整数零(或至少映射到一个确定的整数)。原因很实际,神经网络里大量存在 0:ReLU 的输出、padding、稀疏权重。如果 0 被映射到比如 −128,那么任何”输出为 0 的神经元”都会带来一个固定的偏置误差。
  3. 两条斜线的斜率就是 1/\text{scale},斜率越大表示分辨率越高(一格整数代表的实数越小)。

4.2 公式 1:正向量化

x_q = \mathrm{round}\!\left(\frac{x}{\text{scale}} + \text{zero\_point}\right)

符号 含义 本例取值
x 待量化的实数(权重/激活值) 例如 500.0
x_q 量化后的整数 int8,范围 −128~127
scale 缩放因子:一格整数代表多少实数 3.9
zero_point 零点偏移:实数 0 对应的整数 −128

反量化(推理时要还原成实数参与计算,或者反量化后只在部分算子里用整数算):

\hat{x} = \text{scale}\times(x_q – \text{zero\_point})

可以验算这个可逆性:代入 x=500,x_q = \mathrm{round}(500/3.9 + (-128)) = \mathrm{round}(128.2-128)=\mathrm{round}(0.2)=0,反量化 \hat x = 3.9\times(0-(-128)) = 3.9\times128 = 499.2。误差 0.8,也就是半个 scale 以内(3.9/2 = 1.95),量化误差上界就是 \text{scale}/2。

4.3 右栏四步的机械复述(这是本图最该背下来的过程)

第 1 步:算缩放比例(确定 scale)

原数据范围 [0, 1000],要压到 int8 的 [-128, 128]:

\text{scale} = \frac{x_{max} – x_{min}}{q_{max} – q_{min}} = \frac{1000 – 0}{128 – (-128)} = \frac{1000}{256} = 3.9

原图这里把分母写成了 (max-min)/(max-min),第二个 max/min 应该是 整数域的上下界 q_{max}/q_{min}。这是课件排版笔误,不影响计算(数字 1000/256 是对的)。记住正确写法就是上面的式子。

第 2 步:缩放(把范围压小)

所有数据除以 3.9:

[0, 1000] \div 3.9 \;\Rightarrow\; [0, 256]

此时跨度已经和整数域一致了(都是 256 格),但起点没对齐,实数 0 落到了 0 的位置,而 int8 的 0 在中间。

第 3 步:平移(把起点对齐)

要让 [0,256] 落到 [-128, 128],整体减 128:

[0, 256] – 128 \;\Rightarrow\; [-128, 128]

第 4 步:反解平移量(它就是 zero_point 的另一种算法)

原图用的写法是:

q_{max} – x_{max}^{\text{(缩放后)}} = 128 – 256 = -128

意思是”整数域上界 128 与缩放后数据上界 256 之间的差是 −128,所以整体移动 −128″。

把这个平移量和公式 1 对照,就能看出二者的等价关系。设 x 缩放 3.9 倍后为 x’ = x/3.9 \in [0,256],我们要求的是 x_q = x’ + zp’ 使 0 \mapsto -128:

0 + zp’ = -128 \Rightarrow zp’ = -128

而公式 1 里的 zero_point 是在缩放前的式子里:x_q = \mathrm{round}(x/\text{scale} + \text{zero\_point})。这里的 x/\text{scale} 就是 x’,所以两者是同一个数:

\boxed{\ \text{zero\_point} = q_{min} – \frac{x_{min}}{\text{scale}} = -128 – \frac{0}{3.9} = -128\ }

这才是 zero_point 的通用求法:让实数域最小值精确落在整数域最小值的那个偏移量。

验证一遍端到端:

实数 x x/3.9 +(-128) round → x_q 反量化 \hat x 误差
0 0.00 −128.00 −128 3.9\times(0) = 0.0 0
500 128.21 0.21 0 3.9\times128 = 499.2 −0.8
1000 256.41 128.41 128 3.9\times256 = 998.4 −1.6

误差都在 \pm\text{scale}/2 = \pm 1.95 以内,和第 4.2 节的结论一致。

4.4 把练习题做完:−123 ~ 999 映射到 1 ~ 3 范围

红字那道题是左下角分布的另一套参数:实数域 [-123, 999],目标整数域 [1, 3](只有 3 个取值,故意做得很小,方便手算)。

第一步,算 scale:

\text{scale} = \frac{x_{max}-x_{min}}{q_{max}-q_{min}} = \frac{999-(-123)}{3-1} = \frac{1122}{2} = 561

第二步,算 zero_point(用 4.3 节推出的通用式):

\text{zero\_point} = q_{min} – \frac{x_{min}}{\text{scale}} = 1 – \frac{-123}{561} = 1 + 0.2193 = 1.2193

第三步,验证两端:

实数 x x/561 +1.2193 round → x_q
−123(最小值) −0.2193 1.0000 1
0 0 1.2193 1
500 0.8913 2.1106 2
999(最大值) 1.7807 3.0000 3

两个端点都精确落在目标区间上,说明 scale 和 zero_point 算对了。

从这道题能读出的三条道理:

  1. q 只有 3 个取值(1/2/3),意味着整个 [-123, 999] 区间的数据被压成了 3 个桶,分辨率就是 561。这就是”低位宽量化掉精度”的极端演示。
  2. 当 q_{min} \neq 0 时,zero_point 一般不是整数(这里算出 1.2193)。工程实现里 zero_point 必须取整(取 round 或 ceil),取整后会引入一个”零点偏移误差”,这也是量化误差的一部分来源。
  3. 如果目标区间是对称的 [-128, 127],那么 q_{min} = -128,公式退化成 zp = -128 – x_{min}/\text{scale},和 4.3 节完全一致。

4.5 为什么要分”按块量化”(per-block / per-group)

课堂上紧接这张图还讲到按块量化:把张量按 (T, D) 的形状切块,块内共享一套 min-max 统计出来的 scale/zero_point。

直觉对比:
– per-tensor(整层一套参数):图省事,但如果某一层里有一个通道数值特别大,整层 scale 都被它撑大,其余通道全被压扁。
– per-channel(每通道一套):解决通道间的差异,是卷积网络的标配。
– per-block / per-group(每组一套):在 LLM 里最常用(例如 128 个元素一组),因为 LLM 的权重里存在大量离群值(outlier),细粒度分组能把它们隔离在少数组里。

代价是量化参数本身占空间:如果每 128 个权重就要存一对 scale/zero_point,这部分开销需要计入总压缩率。这就是为什么前面讲”实测压缩率低于理论 4 倍”。


五、易错点与常见误解

常见说法 正确说法 为什么
scale = (max−min)/(max−min) 分母是整数域的 q_{max}-q_{min} 原图这里有排版笔误;分母写错就永远是 1,量化无从谈起
量化就是”除以一个数” 是先缩放、再平移两步 只除不平移,起点对不上,0 会跑偏,引入系统偏差
zero_point 就是 (q_{max}+q_{min})/2 这类固定值 它由”实数最小值对齐到整数最小值”反解出来,随数据范围变化 对称量化下它恰好是 q_{min},非对称量化下不是
量化误差是随机的,平均一下就没有了 误差有上界(≤ scale/2)且是系统性的 因为 round 的方向由数据分布决定,不是随机噪声
min-max 是”最简单所以最好” 最简单,但最怕离群值 单个极端值会撑大 scale,其余数据的有效比特数骤降
反量化后数值应该和原来一样 只能还原到 \pm\text{scale}/2 的误差内,且不可逆 量化是有损压缩,信息已经丢了
zero_point 可以是小数 实现时必须取整,取整会带来额外偏差 整数域本身只有 256 个格,零点必须是其中一个

六、横向对比与研究脉络

量化参数定标(calibration)的三条粒度路线:

粒度 量化参数数量 精度 额外开销 典型场景
per-tensor 每张量 1 对 最低 几乎为 0 简单 CNN、快速验证
per-channel 每输出通道 1 对 中 很小 ResNet 等 CNN 的静态量化
per-block/group 每 N 个元素 1 对 高 与组大小成反比 LLM 权重(离群值多)

min-max 之后出现的方法,以及各自解决的痛点:

方法 解决的问题 核心思路
min-max(本图) 最基础,能跑通 直接用数据的全局极值定区间
移动平均 min-max 训练中逐 batch 统计抖动大 用滑动平均平滑历史极值
KL 散度校准 极值把 scale 撑爆 在候选截断阈值里挑一个让”量化前后分布最接近”的
直方图校准 分布非均匀 用激活值直方图找更合适的截断点
per-channel / per-group 通道间、组间尺度差异大 细粒度各自定标
QAT(伪量化) 静态校准无法适配训练 训练时插入伪量化算子,让模型自己学容忍误差

脉络一句话:min-max 定义了”量化的形式”,后面所有方法都在解决同一个问题,怎么在 256 个格里塞进数据的真实分布。数据越”长尾 + 有离群值”,就需要越细的粒度或越聪明的截断。


七、在新闻分类项目中的落地

  1. 讲清”动态量化”为什么不用校准数据:项目里 torch.quantization.quantize_dynamic() 一行就能量化 BERT。原因是,动态量化里权重的 scale/zero_point 用本文的 min-max 直接从权重张量算出来(权重是静态的,随时可统计);而激活值在运行时才按每个 batch 的实际 min/max 现算。所以它不需要提前准备校准数据集,代价是每次前向都要多算一次统计。
  2. 理解静态量化(PTQ)为什么需要一批数据:因为静态量化要把激活值的 min/max 提前定死,只能拿一批有代表性的数据跑一遍前向去做统计,这一步正是在做本文第 1~4 步,只不过把”手算”换成了”统计 + 遍历候选阈值”。
  3. 讲解/汇报时能给数量级:算出 scale = 3.9 后,能明确说出”int8 量化后每格代表 3.9 个单位,误差上界 1.95″,这比只说”量化会掉一点精度”专业得多。评估量化模型时,观察准确率下降是否和”误差上界 × 层数”的累积量级相符,也是排查”是量化问题还是别的问题”的手段。
  4. 按块量化 → 压缩率的诚实计算:如果对 BERT 的 Linear 层做 per-channel 量化,多出的 scale/zero_point 开销大约是”每个输出通道 2 个 float32″,相对权重本身可以忽略;但如果做 per-group(如每 64 个一组),开销就不可忽略了。做压缩率汇报时要交代粒度,否则实测值和理论值对不上。

八、重点问答

Q1:量化公式是什么?scale 和 zero_point 分别是什么含义?
A:正向量化是 x_q = \mathrm{round}(x/\text{scale} + \text{zero\_point}),反量化是 x \approx \text{scale}\times(x_q-\text{zero\_point})。scale 是缩放因子,表示一个整数格代表多少实数,也就是分辨率;zero_point 是零点偏移,作用是把实数 0 精确映射到某个整数上。反解式是 zp = q_{min} – x_{min}/\text{scale},也就是让实数域最小值精确对齐整数域最小值。

Q2:现场把 0~1000 量化到 int8 的 −128~127,你怎么算?
A:两步。第一步算 scale:1000/(127-(-128)) = 1000/255 \approx 3.92;如果按课件口径用 256 格,就是 1000/256=3.9。第二步算 zero_point:zp = q_{min} – x_{min}/\text{scale} = -128 – 0 = -128。校验:x=500 时 500/3.9-128 = 0.2,round 后是 0,反量化 3.9\times(0+128)=499.2,误差 0.8,在半个 scale 之内。

Q3:为什么量化一定要先缩放再平移,只做缩放行不行?
A:不行。只缩放能让数据跨度和整数域一致,但起点对不上,比如 0~1000 除以 3.9 变成 0~256,而 int8 的区间是 -128~127,起点差了 128。所以必须再来一步平移把起点对齐。课件里第 2 步和第 3 步就是在强调”范围一致了但起始位置还不一样”。

Q4:zero_point 为什么必须让实数 0 精确落在某个整数上?
A:因为神经网络里有大量真实的零,ReLU 的输出、padding、稀疏权重。如果 0 被映射到一个非零整数,那么所有”本该是 0″的地方都会带上一个固定偏置,误差会随层数累积。所以量化实现里会强制约束零点对齐,这也是 zero_point 取整会产生额外误差的原因。

Q5:min-max 量化有什么缺点?后来怎么改进的?
A:缺点是怕离群值。它直接用全局极值定区间,如果有几个特别大的值,scale 会被撑得很大,剩下 99% 的数据只能挤在几个整数格里,精度断崖下跌。改进方向有三条:一是细粒度定标,从 per-tensor 到 per-channel 再到 per-group/per-block;二是换更聪明的截断策略,比如 KL 散度校准、直方图校准,找一个比极值更小的截断点;三是 QAT,在训练时插入伪量化算子让模型自己学会容忍误差。

Q6:量化误差上界是多少?
A:反量化后误差不超过 scale 的一半。因为 round 最多偏离 0.5 个整数单位,乘回 scale 就是 \text{scale}/2。比如 scale=3.9 时误差上界是 1.95。注意这个误差是系统性的、有上界的,不是随机噪声,所以不能靠平均消掉。

Q7:为什么大模型量化常用按块(per-group)而不是整层(per-tensor)?
A:因为 LLM 权重里有明显的离群值,少数通道数值特别大。如果整层共用一套 scale,这些离群值会把 scale 撑大,其余权重的有效精度被严重压缩。按块量化把 128 个元素分一组、组内各自定标,能把离群值隔离在少数组里,整体精度好得多。代价是要多存量化参数,做压缩率统计时必须把这部分开销算进去。


九、延伸阅读

  • Jacob et al., Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference,量化公式与 scale/zero_point 的经典出处
  • PyTorch 官方量化文档:torch.quantization 观察器(observer)与校准流程(per-tensor / per-channel)
  • 本目录《【掌握】二进制与字节》研究报告,位宽与数据类型基础
  • 本目录《【掌握】动态静态量化》研究报告,DQ / PTQ / QAT 三条路线的分工
  • 本目录《【掌握】模型剪枝》研究报告,另一种压缩手段(完整 → 残缺)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇