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

AI瘦身失败事件

这个文件承诺了什么 — 读出导出时固化的内容

在 TT Lab 中继续学习

目标

用 onnx.helper 亲手搭一个 MLP,生成 /root/onnxq-export/mlp.onnx,并编写读取 ONNX 文件中固化内容的工具 /root/onnxq-export/modelmeta.py。替换版本号,依次询问 onnx.checker 和 onnxruntime,亲自找出“检查器通过但运行时拒绝”的位置,并以文档的形式留下来。

为什么重要

ONNX 文件里装的不只是计算图。它用哪个算子集版本书写、IR 版本是多少、由谁生成、哪些是输入、哪些是已经确定的权重,都在导出的瞬间一并固化。接收方无法更改这些决定,要改就只能重新导出。 所以“在我们的服务器上打不开”这类报告,原因往往不在转换选项,而在文件开头。即使把版本号调低后重新保存,算子定义也不会跟着降下去,于是会生成“检查器通过、只有运行时拒绝”的文件。报错信息不是“版本太低”,而是“没有该算子的实现”,原因更难看出来。 onnx.checker 也不是一层。默认检查只看结构,必须加上 full_check=True 才会运行形状推断。矩阵乘法无法成立的 MatMul 会直接通过默认检查。而且两种检查都拦不住未注册域中的算子。 评分器不会相信你写下的文字。它会在临时目录里摆出自己亲手搭建的 ONNX 文件,真正运行你的工具,把结果与评分器自己读同一文件得到的答案进行核对。形状、名称、版本号和激活函数每次运行都不同。

步骤

  1. 创建并运行 /root/onnxq-export/build_mlp.py,生成 /root/onnxq-export/mlp.onnx。
  2. 在 /root/onnxq-export/modelmeta.py 中实现 info,读出版本号、producer、输入输出、initializer 和节点。
  3. 在 info 中加入 input_overrides 和 runtime_inputs,显示出 initializer 与输入的边界。
  4. 加入 check,分别以默认方式和 full_check=True 运行 onnx.checker 两次,并分别记录判定。
  5. 加入 load,记录 onnxruntime 能否打开会话,打不开时记录是什么异常。
  6. 加入 stamp,保持算子不变,只替换版本号后重新保存。
  7. 加入 scan,实际测出该运行时能打开的版本范围,并把自己模型的范围写入 /root/onnxq-export/opset_range.json。
  8. 生成 /root/onnxq-export/export_report.json 和 /root/onnxq-export/export_report.md 作为交接文档。

参考

亲手搭建模型

创建并运行 /root/onnxq-export/build_mlp.py,生成 /root/onnxq-export/mlp.onnx。输入 x 为 [符号, 8],输出 y 为 [符号, 4],用 MatMul、Add、Relu 搭两层。权重固化为 initializer。

在 helper.make_tensor_value_info 的形状列表中放入字符串,该轴就成为符号名;放入整数则该轴固定。权重用 numpy_helper.from_array(배열, 이름)(占位符依次为数组与名称)创建,放到 make_graph 的第五个参数中。把这个名称用作节点的输入,但不要放进 make_graph 的输入列表。保存之前,先用 onnx.checker.check_model(model, full_check=True) 过滤一遍。

读出文件中固化的内容

在 /root/onnxq-export/modelmeta.py 中实现 info <모델>(占位符为模型文件),以 JSON 输出 ir_version、producer_name、opsets、inputs、outputs、initializers、nodes。

onnx.load(path) 会返回 ModelProto。model.opset_import 是域与版本号的列表,默认域是空字符串。轴有 dim_param 时记为符号,有 dim_value 时记为整数,两者都没有时记为 null。inputs 中要去掉 initializer 的名称——那些值已经包含在文件里了。

权重不是输入

在 info 响应中加入 input_overrides(graph.input 与 initializer 两边都有的名称)和 runtime_inputs(会话实际要求提供的输入名称)。打不开会话时,runtime_inputs 为 null。

IR 4 之前必须把 initializer 同时声明在 graph.input 中。所以旧工具生成的文件里,权重也写在输入列表中,这些名称表示“带默认值的输入”。直接问运行时,就能立刻看到它不会把这些名称当作必填输入。打开会话的代码要吞掉异常并返回 null——打不开的文件也必须能用 info 读出来。

检查器不是一层

加入 check <모델>(占位符为模型文件),分别以默认方式和 full_check=True 运行 onnx.checker,输出 {"checker", "full_check", "message"}。失败时,把捕获到的异常的第一行原样写入 message。

默认检查只看结构。加上 full_check=True 会运行形状推断,能抓到矩阵乘法无法成立的 MatMul,以及声明的输出形状与推断出的形状不一致的情况。不要自己编造异常文案,请原样转写 str(exc) 的第一行——评分器会看它埋在里面的名称是否出现在其中。

直接问运行时

加入 load <모델>(占位符为模型文件),尝试打开 onnxruntime 会话,输出 {"load", "error_type", "message"}。error_type 是所捕获异常的类名,打开成功时为 null。

通过了检查器的文件,运行时也可能拒绝。未注册域中的算子、版本号太低导致该版本中没有定义的算子,以及运行时尚未支持的高版本,都是这样的情况。拒绝的种类各不相同,所以把异常类名也记下来,下一个人就能立刻分辨原因。

只替换版本号

加入 stamp <모델> <opset> <ir> <출력>(占位符依次为模型文件、opset、IR 版本与输出文件),只修改默认域的 opset 和 ir_version,并保存为另一个文件。节点、权重和 producer_name 必须保持原样。

遍历 model.opset_import,把域为空字符串的项的 version 改掉,没有就新增一项。这不是转换,而是盖印章——算子的定义既不会跟着降下去,也不会跟着升上去。你会在下一步亲眼看到这一点。

测出该运行时能打开的版本范围

加入 scan <모델>(占位符为模型文件),把版本号从 1 依次替换到 27,测量会话能否打开,输出 {"min_ok", "max_ok", "ok", "failed"}。再把自己模型的范围以 min_ok、max_ok 写入 /root/onnxq-export/opset_range.json。

把前面步骤的 stamp 和 load 直接接起来就行。只需生成到临时目录并尝试打开,不要动原文件。下限取决于模型所用算子从哪个版本开始有定义,上限取决于运行时支持到哪个版本。这两个数字来源不同,正是这一步的关键。

留下一页交接文档

在 /root/onnxq-export/export_report.json 中写入从模型读出的值、版本范围,以及装有“检查器通过但运行时拒绝”的版本的 checker_ok_runtime_error,并把 /root/onnxq-export/export_report.md 写成四节。

checker_ok_runtime_error 的 opset 只要是低于能打开的下限的版本即可。用该版本生成文件,分别交给 check 和 load 去问,写下实际得到的判定——评分器也会生成同样的文件再确认一遍。报告中要用数字写出下限和上限,接收方才能拿来和自己的运行时对照。