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

AI瘦身失败事件

不用校准数据 — 实测动态量化

在 TT Lab 中继续学习

目标

编写 dyntool.py,对自己的模型做动态量化,并全部用数字留下:计算图是怎么变化的,文件在哪里缩小、哪里保持原样,为什么输入分布变了它也撑得住,以及这种方式不适用的情况是什么样子。

为什么重要

静态量化必须先收集代表性输入才能开始,而找这份数据是现场最花时间的事。动态量化不预先确定激活的范围,而是在每次推理时看着那个张量来确定,所以只需要一个模型文件。因此它是在决定做什么之前先试一下的基线。 不过,得知道得到什么、失去什么,才能做出选择。得到的是分布波动时也撑得住的特性——每个批次都重新做一把尺子,就不会超出校准时见过的范围。失去的是可复现性。缩放因子由该批次整体的最大值决定,所以一行离群值会把同一批次里其他行也拖下来。单独放入时好好的输入,会因为邻居而变差。 文件大小也要测了才知道。变小的只有矩阵,偏置仍是 float32,缩小后的矩阵旁边还跟着缩放因子和零点。在权重很小的模型里,新增的节点比缩减的部分还大,文件反而变大。 评分器不会相信你写下的数字。它每次都用不同的随机种子和形状重新搭建模型,真正运行你的工具,并与 DynamicQuantizeLinear 算子的实际结果,以及评分器亲自运行的推理结果进行核对。

步骤

  1. 创建并运行 /root/onnxq-dyn/gen_model.py,生成 /root/onnxq-dyn/fp32.onnx。
  2. 在 /root/onnxq-dyn/dyntool.py 中实现 quantize,生成 /root/onnxq-dyn/dyn.onnx。
  3. 加入 sizes,数出每个 initializer 的字节数,写入 /root/onnxq-dyn/size.json。
  4. 加入 dqparams,亲手实现 DynamicQuantizeLinear 在每次推理时做的计算。
  5. 加入 compare,在改变输入倍率的同时测量误差,写入 /root/onnxq-dyn/batches.json。
  6. 加入 outlier,测量一行离群值是否会扰动批次,写入 /root/onnxq-dyn/outlier.json。
  7. 用 /root/onnxq-dyn/gen_small.py 生成一个极小的模型并量化,写入 /root/onnxq-dyn/unfit.json。
  8. 用 /root/onnxq-dyn/report.json 和 /root/onnxq-dyn/report.md 汇总成一页。

参考

先把要测的对象拿到手

创建并运行 /root/onnxq-dyn/gen_model.py,生成 /root/onnxq-dyn/fp32.onnx。批次轴必须是只有名称的动态轴,这是一个含两个矩阵和两个偏置的 MLP。

用 onnx.helper 搭出计算图,用 onnx.checker 确认后再保存。输入形状的第一个轴不要用数字,而用名称("N")。initializer 用 numpy_helper.from_array 创建。创建后用 onnxruntime 跑一次,就能确定无误。

一条命令,只固定权重

在 /root/onnxq-dyn/dyntool.py 中实现 quantize <입력.onnx> <출력.onnx>(占位符依次为输入文件与输出文件),生成 /root/onnxq-dyn/dyn.onnx。响应中要有变换后的算子数量、消失的算子和新增的算子。

对 onnxruntime.quantization 的 quantize_dynamic 把 weight_type 设为 QInt8 即可。不要放校准数据——这里也没有地方放。变换后的计算图用 onnx.load 打开,数 node 的 op_type 就能看到。确认一下 MatMul 去哪里了。

用名称说清楚哪里变小了

加入 sizes <모델.onnx>(占位符为模型文件),为每个 initializer 数出数据类型、元素数和字节数,并把两个模型比较的结果以 fp32_bytes、dyn_bytes、shrunk、kept_float、weight_bytes_before、weight_bytes_after 写入 /root/onnxq-dyn/size.json。

遍历 onnx.load 得到的 graph.initializer,用 numpy_helper.to_array 转换,就能得到 dtype 和 nbytes。shrunk 是 fp32 中有、而动态模型中名称已消失的 initializer,kept_float 是仍以 float32 保留下来的 initializer。看看偏置在哪一边。

每次推理都重新做尺子的计算

加入 dqparams <입력.npy> <출력.npy>(占位符依次为输入文件与输出文件),亲手实现 DynamicQuantizeLinear 在每次推理时做的计算。输出缩放因子、零点和范围,并保存量化后的 uint8 数组。

范围中必须包含 0——最小值为正时降到 0,最大值为负时升到 0。uint8 有 255 个刻度,零点是实数 0.0 所在的整数位置。分别放入全为正数的数组和全为负数的数组,就能看到零点跑到两端。

把输入放大 1000 倍试试

加入 compare <fp32.onnx> <int8.onnx> <씨앗> <행> <배율>(占位符依次为随机种子、行数与倍率),用随机种子 20260917、行数 32 测量倍率 0.05、1.0、50.0,并以 runs 和 relative_spread 写入 /root/onnxq-dyn/batches.json。

生成批次的规则已在参考部分写死——评分器要重新生成同样的批次,所以请原样遵守。relative_spread 是三个 relative 值的最大值除以最小值所得的值。看看绝对误差会随输入大小增大,而相对误差怎么样。

一行邻居就能扰动批次

加入 outlier <fp32.onnx> <int8.onnx> <씨앗> <행> <튀는값>(占位符依次为随机种子、行数与离群值),用随机种子 20260917、行数 12、离群值 60.0 测量,并写入 /root/onnxq-dyn/outlier.json。

把同样的 12 行放入两次——一次原样,一次在下方接上离群的那一行。误差 只在前面的 12 行上 测量。离群行自身的误差不计入。这样“因为邻居而变差”这句话才成立。

在小模型上反而变大

用 /root/onnxq-dyn/gen_small.py 生成一个极小的模型 /root/onnxq-dyn/small.onnx,量化后得到 /root/onnxq-dyn/small_dyn.onnx,然后把结果以 small_fp32_bytes、small_dyn_bytes、grew、growth_bytes、dynamic_quantize_nodes、kept_float_ops 写入 /root/onnxq-dyn/unfit.json。

权重只有几百字节时,新增的节点和缩放因子、零点 initializer 比缩减的部分还大。kept_float_ops 是 fp32 模型和动态模型 两边都有 的算子——也就是没有改变而保留下来的那些。grew 是布尔值,不是字符串。

把得到的和失去的汇总成一页

在 /root/onnxq-dyn/report.json 中写入 graph_change、file、relative_error_spread、outlier_ratio、small_model_grew,并在 /root/onnxq-dyn/report.md 中用 ## 그래프가 어떻게 바뀌나、## 어디가 줄고 어디가 안 줄었나、## 보정 자료가 필요 없는 이유、## 동적 양자화가 맞지 않는 경우 四节来写(四个标题为韩文,依次意为“计算图是怎么变化的”“哪里缩小了、哪里没缩小”“为什么不需要校准数据”“动态量化不适用的情况”)。

读取前面步骤生成的各个 JSON 来拼装即可。file 中的 ratio 是动态模型的字节数除以 fp32 的字节数所得的值。报告中请在数字旁逐行写下这个数字指的是什么——读的人没有看到你的实验。