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

AI瘦身失败事件

不用校准数据 — 动态量化对计算图做了什么

在 TT Lab 中继续学习

一句话总结

动态量化是 只把权重预先固定成 int8,而激活的范围在每次推理时当场测量。所以它不需要校准数据,代价是每个批次都用不同的尺子。

为什么需要这一支

做静态量化,必须先有代表性输入。因为要提前看激活大致分布在什么范围,才能把缩放因子定死。可是在现场,找这份数据是最花时间的事。客户的数据拿不到,合成数据的分布不一样,拿到的数据又和评估数据混在一起。

动态量化把这个问题整个绕开了。它不预先确定激活的范围,而是在运行时看着那个张量来确定。转换只需要一个模型文件,一条命令就完成。所以可以先说“先用这个试试能到什么程度”,它也成了判断做不做静态量化的基线。

工作原理

ONNX Runtime 的量化文档把两支并列说明。动态这一支只在离线时把权重转成整数,激活的缩放因子和零点在运行中计算。

打开计算图,就能立刻看到发生了什么。一个 MatMul 变成了这样。

바뀌기 전   X(float32) ──▶ MatMul(W float32) ──▶ h(float32)

바뀐 뒤     X(float32) ──▶ DynamicQuantizeLinear ──▶ (q uint8, scale, zero_point)
                                 │
                                 ▼
                           MatMulInteger(W int8) ──▶ (int32)
                                 │
                            Cast ──▶ Mul(scale 들) ──▶ h(float32)

MatMul 消失了,取而代之的是 DynamicQuantizeLinear · MatMulInteger · Cast · Mul。前面的节点当场把激活降成 uint8,同时给出缩放因子和零点;整数内核做完乘法后,再乘上两个缩放因子回到实数。

DynamicQuantizeLinear 的定义很短。把数据的最小值和最大值再加上 0 来确定范围,用这个宽度除以 255 得到缩放因子,并把 0.0 所在的整数位置作为零点。这个计算 每次推理都要重新做,这就是这种方式的全部。

文件大小缩小的位置也是固定的。把 initializer 逐个数一遍,会发现 只有矩阵变小,偏置仍然保持 float32。而且缩小后的矩阵旁边,还跟着作为小 initializer 的缩放因子和零点。“量化后变成四分之一”这个常识,只有在矩阵占了文件的大部分时才成立。

在现场相遇的样子

第一,输入大小变了也撑得住。把输入放大 1000 倍,相对误差几乎不变。因为每个批次都重新做一把尺子。静态量化在同样的情况下,会超出校准时见过的范围而被整个截断。没有校准数据与真实分布不符的风险,是这种方式最大的优点。

第二,同一批次里混进来的邻居会互相毁掉对方。缩放因子由该张量整体的最大值决定,所以只要有一行值特别大,其余行能用的刻度就变少。单独放入时好好的输入,一旦混进批次,答案就变差——这意味着 同一个输入,结果会随批次组成而变化,在需要复现的场合,这立刻就成了问题。

第三,小模型的文件反而变大。对权重只有几百字节的模型做动态量化,新增的节点和缩放因子、零点 initializer,比缩减掉的部分还大。不去测量,只觉得“应该变小了”就放过去,就会原样部署出去。

第四,测量范围这件事每次推理都在发生。要求出缩放因子,就得把激活张量从头到尾扫一遍找出最小值和最大值。计算图里有几个 DynamicQuantizeLinear,就是每次推理要扫几遍。在测时间之前 先数一数节点数,就能知道开销落在哪里。

实际工作中真正重要的事

下一项实验要做什么

编写 dyntool.py,对自己的模型做动态量化,数出计算图的变化和每个 initializer 的字节数,亲手实现 DynamicQuantizeLinear 在每次推理时做的计算,把输入倍率放大到 1000 倍来测误差,测一行离群值会如何扰动批次,并在极小的模型上确认文件变大的现象。评分器每次都会用不同的随机种子和形状重新搭建模型和批次,真正运行你的工具,并把同样的计算重做一遍来核对。