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

AI瘦身失败事件

数一数 float 孤岛 — 算子支持与替换

在 TT Lab 中继续学习

目标

把同一个模型导出成五种版本之后,编写分析器 graphscan.py,数出计算图中被 int8 包裹的算子和仍为 float 的孤岛。用数字确认两种收窄范围的选项的效果,并把模型改成 Gemm 来减少 Q/DQ 往返。

为什么重要

量化工具只会把自己认识的算子改成 int8 的样子。在 QDQ 格式中,算子名称原样保留,只是在每个张量上插入 QuantizeLinear 和 DequantizeLinear。所以“量化了”和“以 int8 运行”是两回事,要区分它们,就得数计算图。 这个实验把判定规则固定如下:所有输入都来自 DequantizeLinear、所有输出都只流向 QuantizeLinear 的算子,视为被 int8 包裹。其余的仍为 float,相连的合在一起就成了 float 孤岛。 孤岛的问题在边界而不在个数。每个孤岛都会附带流入的 DQ 和流出的 Q,每个边界上都要多一次值的转装和舍入。减少孤岛有两条路:收窄范围,直接去掉不合格的节点;或者改模型,让它变成可量化的样子。 评分器不会相信你写下的数字。它每次都会用不同的层数、形状和排除节点,在临时目录里构建自己的模型,真正运行你的分析器,并与评分器用同样规则数出的值核对。第 6、7 步里,评分器会亲自读取你生成的模型文件重新计数。

步骤

  1. 创建并运行 /root/ops/gen_models.py,在 /root/ops 下生成五个模型。
  2. 在 /root/ops/graphscan.py 中实现 nodes,统计算子 census 和初始值的数据类型。
  3. 加入 qdq,统计 QuantizeLinear、DequantizeLinear 和 Q/DQ 往返。
  4. 加入 islands,区分被 int8 包裹的算子和仍为 float 的孤岛。
  5. 加入 boundary,统计每个孤岛进出的边界。
  6. 加入 diff,给出两个模型的差异,并把收窄范围的结果写入 /root/ops/scope.json。
  7. 生成把 MatMul 和 Add 合并成 Gemm 的 /root/ops/gemm.onnx,以及把它量化后的 /root/ops/gemm_full.onnx,写入 /root/ops/fuse.json。
  8. 把 /root/ops/ops_report.md 写成四节。

参考

把同一个模型导出成五种版本

创建并运行 /root/ops/gen_models.py,在 /root/ops 下生成 fp32.onnx、full.onnx、matmul_only.onnx、excluded.onnx、dynamic.onnx。

用 onnx.helper 把 MatMul、Add、Relu 连起来,并给每个节点起名字。有名字,后面才能用 nodes_to_exclude 去掉其中一个。静态量化需要 CalibrationDataReader,同一份校准数据要多次使用时,读取器每次都要重新创建——读完一遍的读取器不会再给你数据。

先数数里面有什么

在 /root/ops/graphscan.py 中实现 nodes <model>(占位符为模型文件),输出算子 census 和初始值数据类型 census。

初始值的数据类型,用 onnx.TensorProto.DataType.Name(init.data_type) 得到名称。把静态 QDQ 版本和动态版本并排数一遍,两种格式差别有多大就一目了然——动态版本里根本没有 MatMul。

数出 Q/DQ 往返

加入 qdq <model>(占位符为模型文件),统计 quantize、dequantize、round_trips、weight_dequantize。

挂在权重上的 DequantizeLinear 没有与之配对的 QuantizeLinear。因为权重已经以 int8 存在文件里了。所以要从 DequantizeLinear 的个数里减去这部分,才能与激活值的往返数对上。

找出仍为 float 的孤岛

加入 islands <model>(占位符为模型文件),区分被 int8 包裹的算子和仍为 float 的算子,并把相连的合在一起,作为孤岛输出。

先建好生产者、消费者的表,后面就容易了。包裹的判定原样使用参考部分的规则。合并孤岛时,只沿着 float 节点之间的连接走——经过 Q 或 DQ 就是另一个孤岛。

数出每个孤岛的边界

加入 boundary <model>(占位符为模型文件),统计每个孤岛流入的 DequantizeLinear 个数和流出的 QuantizeLinear 个数,并输出整体的边界数。

同一个 DequantizeLinear 可能流入一个孤岛的两个节点。不要按节点数,要用集合汇总后再数。如果孤岛的输出直接作为计算图输出,流出的 QuantizeLinear 可能为 0。

收窄范围会带来什么变化

加入 diff <a> <b>(占位符为两个模型文件),输出两个模型的差异,并把三个版本的数值和两个 diff 按参考部分的形式写入 /root/ops/scope.json。

不要以为给了选项就照办了。在按节点名称去掉的版本里,该节点的权重仍是 float 初始值;在按算子类型收窄的版本里,该类型之外的算子整个成了孤岛。两种收窄方式在计算图里留下的痕迹不同。

改模型来减少往返

生成把 MatMul 和 Add 合并成一个 Gemm 的 /root/ops/gemm.onnx,再用相同的设置把它量化成 /root/ops/gemm_full.onnx,然后把两个版本的数值和最大绝对误差写入 /root/ops/fuse.json。

要遵守合并的条件——只有 MatMul 的输出只被那个 Add 使用、并且 Add 的另一个输入是初始值时才合并。合并之后要通过 onnx.checker.check_model,并且一定要用与原始模型相同的输入来运行,确认答案相同。如果值变了,就是合并错了。

写到能用计算图作答

把 /root/ops/ops_report.md 写成 ## 무엇이 int8 로 갔나、## float 로 남은 섬、## 범위를 좁히면 무엇이 달라지나、## 모델을 고쳐 얻은 것 四节(四个标题为韩文,依次意为“什么走了 int8”“仍为 float 的孤岛”“收窄范围会带来什么变化”“改模型得到了什么”)。孤岛的个数和消失的 Q/DQ 个数要以数字写进去。

这份报告回答的是“为什么没有缩小到预期”这个问题。把数过的五个数字(算子 census、Q/DQ 往返、孤岛个数、孤岛大小、边界数)原样写上,就是答案。也要写下没有测时间这件事。