TT Lab
开始
学习 学习路径 课程

AI瘦身失败事件

自己造一把尺 — 缩放、零点与往返误差

在 TT Lab 中继续学习

目标

编写 qmath.py,按 QuantizeLinear 和 DequantizeLinear 的定义计算缩放因子和零点,实现量化与反量化,确认往返误差在缩放因子的一半以内,并在同一份数据上比较对称与非对称、按通道的缩放因子与整个张量的缩放因子。

为什么重要

转换工具给出的 int8 模型准确率下降时,要选对修复的地方,就得知道那个工具在里面做的算术。缩放因子是观察范围除以刻度数所得的值,零点是实数 0.0 落下的整数位置。如果范围里没有 0,填充和 ReLU 产生的 0 就会变成不是 0 的值。 往返误差有上限。只要值在范围之内,误差就不会超过缩放因子的一半——因为量化就是把值移到缩放因子间隔的网格上。所以如果误差远远超过这一半,问题不在舍入,而在范围。这一把尺子能让原因判断变快。 数据的形状也决定方式。ReLU 之后的激活全部大于等于 0,对它用对称 int8,负数一侧的刻度就整个闲置,缩放因子变粗两倍。权重矩阵里只要有一个离群值,整个张量的缩放因子就会被这个值拖着走,其余的全被糊掉——拆分通道,损害就被困在那个通道里。 评分器不会相信你写下的数字。它每次都会用不同的随机种子生成数组,真正运行你的 qmath.py,并把同样的输入原样交给 ONNX 的 QuantizeLinear、DequantizeLinear 算子去运行,再与得到的结果核对。

步骤

  1. 创建并运行 /root/onnxq-scale/gen_data.py,在 /root/onnxq-scale/data 下生成三份数组,并在 /root/onnxq-scale/qmath.py 中实现 params。
  2. 加入 q,把实数数组降成整数数组。
  3. 加入 dq,把整数数组还原成实数。
  4. 加入 roundtrip,同时给出往返误差及其上限。
  5. 故意把范围收窄制造饱和,把结果写入 /root/onnxq-scale/saturate.json。
  6. 对全部大于等于 0 的数据分别用对称和非对称,写入 /root/onnxq-scale/symmetry.json。
  7. 加入 channel,比较按通道的缩放因子和整个张量的缩放因子,写入 /root/onnxq-scale/channel.json。
  8. 用 /root/onnxq-scale/report.json 和 /root/onnxq-scale/report.md 汇总成一页。

参考

把观察范围变成缩放因子和零点

创建并运行 /root/onnxq-scale/gen_data.py,在 /root/onnxq-scale/data 下生成三个数组,并在 /root/onnxq-scale/qmath.py 中实现 params <lo> <hi> <mode>。范围中没有 0 时必须补上。

非对称是把 (hi - lo) 除以 255,零点是实数 0.0 落下的整数位置——用 qmin 减去 lo/scale 后再舍入。对称是把左右宽度中较大的一侧除以 127,零点为 0。先处理 lo 大于 0 或 hi 小于 0 的情况。

把实数降成整数刻度

加入 q <입력.npy> <출력.npy> <mode> [lo hi](占位符依次为输入文件、输出文件与模式),把实数数组降成整数数组,并把结果保存为 .npy。响应中要有 scale、zero_point、clipped、dtype、count。

定义是 saturate(round(x / scale) + zero_point)。先除、再舍入、再加零点,最后截断到数据类型的两端。顺序换了,答案就不同——不能先加零点再舍入。数出被截断的元素个数,作为 clipped 输出。

把整数还原成实数

加入 dq <양자.npy> <출력.npy> <scale> <zero_point> <mode>(占位符依次为量化数组文件与输出文件),把整数数组还原成 float32 实数数组。响应中要有 min、max、count。

定义是 (q - zero_point) * scale。这个方向上既没有舍入,也没有饱和——前面被截掉的值不会在这里恢复。先把整数数组升成 float32 再做减法,才能避免 uint8 出现负数回绕。

往返误差及其上限

加入 roundtrip <입력.npy> <mode> [lo hi](占位符为输入文件与模式),先量化再立刻反量化,同时输出最大绝对误差和上限。bound 是缩放因子的一半,within_bound 表示误差是否在其范围内。

量化就是把值移到缩放因子间隔网格上最近的点。网格间隔就是缩放因子,所以最远也只有一半。由于浮点数的缘故,可能会略微超出一点,所以比较时要留一点余地。这个上限只在值处于范围之内时才成立。

把范围收窄会坏掉什么

把 data/spread.npy 分别按观察范围原样往返一次,以及收窄到 -1.0 1.0 往返一次,并把两个结果以 full、narrow、error_ratio 写入 /root/onnxq-scale/saturate.json。

范围收窄会让缩放因子变得更细——这本身是好事。问题在于跑到范围之外的值会被贴到墙上。误差若远远超过上限,原因就不在舍入,而在范围。error_ratio 是收窄一侧的误差除以原来的误差所得的值。

对只有 0 以上的数据用对称方式会怎样

对 data/positive.npy 分别使用 asym-u8 和 sym-i8,把 asym、sym、scale_ratio 写入 /root/onnxq-scale/symmetry.json。每一项都要有 scale、zero_point、max_abs_error、usable_levels。

usable_levels 是落在范围 [lo, hi] 内的刻度个数——把范围宽度除以缩放因子,加上 1 后舍入即可得到。对于全部大于等于 0 的数据,非对称应该得到 256,对称应该得到 128。确认一下缩放因子之比是否就是误差之比。

把一个离群值困住

加入 channel <입력.npy> <axis>(占位符为输入文件与轴),用对称 int8 比较按通道的缩放因子和整个张量的缩放因子,并把 data/weights.npy 按 axis 0 运行的结果写入 /root/onnxq-scale/channel.json。

把一个轴看作通道,为每个通道分别求出 min 和 max,生成缩放因子。按通道的缩放因子是一维数组,为了广播,要调整形状,只让对应的轴有长度。channels_improved 是按通道一侧的误差小于整个张量一侧的通道数。关键在于,含有离群值的那个通道不会变好。

把测得的汇总成一页

在 /root/onnxq-scale/report.json 中写入 bound_rule、spread、positive_asym_over_sym、channel_gain、saturation_ratio,并在 /root/onnxq-scale/report.md 中用 ## 스케일과 영점은 어디서 나오나、## 왕복 오차의 상한、## 대칭과 비대칭、## 이상값 하나가 하는 일 四节来写(四个标题为韩文,依次意为“缩放因子和零点从哪里来”“往返误差的上限”“对称与非对称”“一个离群值做了什么”)。

channel_gain 不是把整个张量的误差除以按通道的误差,而是以 改善最多的通道 为基准更诚实。含有离群值的通道保持不变,所以整体的最大误差几乎相同。报告中请在数字旁逐行写下这个数字指的是什么。