图优化改变了什么 — 数一数节点
一句话总结
onnxruntime 在打开会话时会 重写 计算图。哪些内容被改到什么程度,由优化级别决定,其结果可以导出成文件,数一数节点 就能确认。
为什么需要它
在比较量化前后速度的实验中,数字怎么都对不上。同一个模型、同一台机器,每次测量结果都不一样,有时量化后的反而更慢。
原因常常出在优化级别上。如果一次测量用的是默认值(全部开启),另一次因为某种原因关闭了优化,那这个比较测的就不是量化,而是优化。更常见的事故是把优化后的文件保存下来去部署——这个文件离开生成它的环境之后,可能就打不开了。
四个级别分别做什么
Graph optimizations 文档把级别分成四个。
- ORT_DISABLE_ALL:什么都不做。计算图原样运行。
- ORT_ENABLE_BASIC:不改变语义的整理。提前折叠只靠常量就能算出的部分(常量折叠),删除可有可无的节点,把常见的形状合并成标准算子。
- ORT_ENABLE_EXTENDED:加入针对特定执行提供程序的融合。非标准域的节点就是在这里出现的。
- ORT_ENABLE_ALL:在此基础上再加上布局转换。默认值就是这个级别。
这些说明是文档上的话,实际能做到哪一步,由模型和机器决定。所以与其读文档,导出来数一数 更快也更准确。
导出并数节点的方法
给 SessionOptions.optimized_model_filepath 指定一个路径,打开会话时重写后的计算图就会保存到该路径。用 onnx.load 打开这个文件,数一数节点,发生了什么就一目了然。
원본 Add(상수,상수) MatMul Add Relu Identity Mul 노드 6개
ORT_DISABLE_ALL Add MatMul Add Relu Identity Mul 노드 6개
ORT_ENABLE_BASIC Gemm Relu Mul 노드 3개
ORT_ENABLE_EXTENDED FusedGemm Mul 노드 2개
这里三件事同时可见。只对常量做加法的节点消失了,结果变成 initializer 留了下来。Identity 可有可无,所以被删掉了。MatMul 后面的 Add 被合并成一个 Gemm,到下一个级别又把 Relu 也吞进去,变成 FusedGemm。
节点减少了,并不意味着答案会变。把同一个输入放进四个级别,比较结果就能确认。只是 float32 的有效位数只有 7 位,融合后的内核如果改变了乘法的顺序,最后一位可能会有抖动。所以要问的不是“是否完全相同”,而是 先定好容差,看是否在容差之内。
在现场相遇的样子
第一,部署优化后的文件。导出的文件小、节点少,让人觉得“发这个就行了”。但这个文件里有 com.microsoft 之类的非标准域节点。只认识标准算子的其他运行时打不开它。ORT 自己在保存时也会发出警告,提醒只能在相同环境中使用。
第二,onnx.checker 让这个文件通过了。因为检查器把不认识的域当作某人的扩展而放行。所以用“检查器通过”作为可移植的依据,抓不到这个事故。依据应该是 节点的域列表。
第三,没有对齐比较的基线。要测量量化的效果,两次测量必须处于同一优化级别。把基线定在 ORT_DISABLE_ALL,就不会抖动。而真正要上线的数字,要用与线上相同的级别另外测量。
第四,把节点数当成性能。节点减少不一定会变快。像这台 Mac 这样夹着模拟层,时间最多会抖动到两倍。所以这个实验不测时间——只判定 结构发生了怎样的变化 和 答案是否相同。时间要在与线上相同的机器上另外测量。
实际工作中真正重要的事
- 导出来数。不要靠文档去猜,把优化后的计算图导出成文件,数节点。
- 同时测答案是否相同。定好容差,并把依据写下来。
- 用域来判断能否移植。检查器通过不是依据。
- 部署时发送原始文件。优化交给接收方在自己的环境里做。
下一项实验要做什么
把常量折叠、可有可无的节点、会被融合的三个节点汇集到一个计算图中亲手搭建,再一步步扩展工具 optlevel.py。把四个级别全跑一遍,数出节点并排摆放,写下每个级别消失了什么、新增了什么;用评分器定的随机种子,把同一个输入放进四个级别,测量答案是否相同。最后根据导出文件的域列表,判定能否移植。评分器每次都会用不同的形状、权重和随机种子构建自己的计算图,真正运行你的工具并核对答案。