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

AI瘦身失败事件

节点从六个减到两个 — 数清优化改动了什么

在 TT Lab 中继续学习

目标

亲手搭建汇集了优化可以动手的内容的 /root/onnxq-optimize/graph.onnx,并编写工具 /root/onnxq-optimize/optlevel.py,它运行 ORT 的四个优化级别,把计算图导出成文件并数节点。写下每个级别消失了什么、新增了什么,测量答案是否相同,并用域名证明导出的文件为什么不能搬到别的机器上。

为什么重要

onnxruntime 在打开会话时会重写计算图。提前折叠只靠常量就能算出的部分,删除可有可无的节点,把常见的形状合并成一个。哪些内容被改到什么程度,由优化级别决定。 比较量化前后速度时数字对不上,原因常常就在这里。如果两次测量的优化级别不同,那个比较测的就不是量化,而是优化。把基线固定为 ORT_DISABLE_ALL,这种抖动就消失了。 代价更高的事故是把优化后的文件保存下来去部署。那个文件里有非标准域的节点,别的运行时打不开。可是 onnx.checker 却让这个文件通过了——因为检查器会放行不认识的域。所以能否移植的依据不是检查器,而是节点的域列表。 这个实验不测时间。这台机器是模拟环境,同样的事也会抖动到两倍。所以只判定结构发生了怎样的变化和答案是否相同。 评分器不会相信你写下的文字。它会在临时目录里摆出自己亲手搭建的计算图,真正运行你的工具,把结果与评分器自己用四个级别优化同一文件得到的值进行核对。形状、权重和随机种子每次运行都不同。

步骤

  1. 创建并运行 /root/onnxq-optimize/build_graph.py,生成 /root/onnxq-optimize/graph.onnx。
  2. 在 /root/onnxq-optimize/optlevel.py 中实现 nodes,读出当前计算图的节点和个数。
  3. 加入 optimize,按给定的级别优化计算图,并导出成文件。
  4. 加入 levels,把四个级别全跑一遍,数出节点并排摆放。
  5. 加入 fusion,写下每个级别消失了什么、新增了什么。
  6. 加入 equal,把用给定的随机种子生成的同一个输入放进四个级别,测量答案是否相同。
  7. 加入 portable,判定导出的文件是否只使用标准算子。
  8. 生成 /root/onnxq-optimize/opt_report.json 和 /root/onnxq-optimize/opt_report.md 作为报告。

参考

汇集优化可以动手的内容

创建并运行 /root/onnxq-optimize/build_graph.py,生成 /root/onnxq-optimize/graph.onnx。其中必须有一个输入全是 initializer 的节点、一个 Identity,以及 MatMul、Add、Relu、Mul。

想看到常量折叠,放入一个把两个 initializer 相加的节点,并在后面使用它的结果即可。Identity 什么都不做,正好是可有可无的节点的例子。在 MatMul 后面放 Add,再在后面放 Relu,就成了会被融合的形状。保存之前,用 onnx.checker.check_model(model, full_check=True) 过滤一遍。

先把当前的计算图数一遍

在 /root/onnxq-optimize/optlevel.py 中实现 nodes <모델>(占位符为模型文件),输出 {"nodes", "count", "initializers"}。nodes 的每一项中放入 op_type、domain、name。

标准算子的节点 domain 是空字符串。现在应该全是空字符串,但打开优化后的文件,就会出现不是的。要到那时才能比较,所以现在先把当前的数好。顺序保持文件中写的样子。

把优化后的计算图导出成文件

加入 optimize <모델> <단계> <출력>(占位符依次为模型文件、级别与输出文件),把按该级别优化后的计算图保存到输出路径,再重新读取保存的文件,数出节点输出。

给 SessionOptions.optimized_model_filepath 指定路径,打开会话期间重写的计算图就会保存到该路径。不打开会话,文件也不会生成。用 ORT_DISABLE_ALL 导出的是基线,应该得到与原文件相同的节点才正常。

把四个级别并排摆放

加入 levels <모델>(占位符为模型文件),把四个级别全跑一遍,为每个级别输出装有 {"nodes", "count"} 的对象。

在临时目录里每个级别导出一个再读取即可,用完后删掉。把四行并排摆放,每个级别发生了什么一目了然——这比读文档更快。

什么消失了,什么新增了

加入 fusion <모델>(占位符为模型文件),为每个级别输出 {"removed", "added", "total"}。removed 是原文件中有、现在消失了的 op_type,added 是新出现的 op_type。

比较原文件的 op_type 集合和每个级别的 op_type 集合即可。只靠常量计算的节点会消失,结果变成 initializer 留下来。MatMul 后面的 Add 会被合并成一个 Gemm,到下一个级别就会出现连激活也吞进去的名字。也请留意那个名字所在的域是什么。

计算图变了,答案还相同吗

加入 equal <모델> <씨앗> <행수>(占位符依次为模型文件、随机种子与行数),把用该随机种子生成的同一个输入放进全部四个级别,输出 {"seed", "rows", "levels", "max_abs_diff"}。

输入用 numpy.random.RandomState(씨앗).standard_normal((행수, 입력너비))(占位符依次为随机种子、行数与输入宽度)生成 float32。输入宽度从模型声明的第 1 轴读取。为每个级别写下整个输出的总和与绝对值最大值,还要写下与基线的最大绝对差。不要期望完全相同,而要先定好容差——float32 的有效位数只有 7 位。

这个文件可以搬走吗

加入 portable <모델>(占位符为模型文件),读取用 ORT_ENABLE_ALL 导出的文件,输出 {"nondefault_domains", "opset_domains", "checker", "portable"}。

标准域只有空字符串和 ai.onnx。只要有一个节点属于其他域,这个文件就只能用于那个环境。onnx.checker 连这样的文件也会放行,所以要把检查器的结果和域列表一起记下来——二者互相矛盾,正是这一步的关键。把 log_severity_level 设为 2,还能看到 ORT 在保存时发出的警告。

用一页报告四个级别

在 /root/onnxq-optimize/opt_report.json 中写入 levels、nondefault_domains、portable、equality,并把 /root/onnxq-optimize/opt_report.md 写成 ## 무엇이 줄었나、## 어느 단계에서 무엇이 바뀌나、## 답은 같은가、## 이 파일을 옮겨도 되나 四节(四个标题为韩文,依次意为“什么减少了”“哪个级别改变了什么”“答案是否相同”“这个文件能搬走吗”)。

评分器会亲自用四个级别优化同一个计算图并数节点,再用 equality 中写的随机种子和行数重新运行,测量差值。所以要实际测量后再写。报告中要把优化前的节点数和剩余的域名按数字和名称原样写下来,接收方才能据此判断。