这个文件承诺了什么 — 读出导出时固化的内容
目标
用 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 文件,真正运行你的工具,把结果与评分器自己读同一文件得到的答案进行核对。形状、名称、版本号和激活函数每次运行都不同。
步骤
- 创建并运行 /root/onnxq-export/build_mlp.py,生成 /root/onnxq-export/mlp.onnx。
- 在 /root/onnxq-export/modelmeta.py 中实现
info,读出版本号、producer、输入输出、initializer 和节点。 - 在
info中加入input_overrides和runtime_inputs,显示出 initializer 与输入的边界。 - 加入
check,分别以默认方式和full_check=True运行onnx.checker两次,并分别记录判定。 - 加入
load,记录 onnxruntime 能否打开会话,打不开时记录是什么异常。 - 加入
stamp,保持算子不变,只替换版本号后重新保存。 - 加入
scan,实际测出该运行时能打开的版本范围,并把自己模型的范围写入 /root/onnxq-export/opset_range.json。 - 生成 /root/onnxq-export/export_report.json 和 /root/onnxq-export/export_report.md 作为交接文档。
参考
- Python 解释器是 /opt/onnx-lab/bin/python。系统自带的
python3中既没有 onnx 也没有 numpy。运行示例:/opt/onnx-lab/bin/python /root/onnxq-export/modelmeta.py info /root/onnxq-export/mlp.onnx - 这个 Pod 没有网络。无法安装任何东西,也没有预先下载好的模型。材料需要自己搭建。
- 模型约定:输入
x是 FLOAT,有 2 个轴,第 0 轴是符号名(字符串),第 1 轴是 8。输出y是 FLOAT,第 1 轴是 4。节点中包含 MatMul、Add、Relu,权重固化为 2 个以上的 initializer。producer_name不能为空。opset 取 7 到 26 之间,ir_version 不超过 13。 - 运行约定:
modelmeta.py <명령> ...(占位符为命令名)。答案以一个 JSON 对象输出到标准输出。成功时退出码为 0,未知命令时为 2。onnxruntime 打印到标准错误的警告不属于答案,请保证标准输出保持干净。 info <모델>(占位符为模型文件)的响应:ir_version为整数,producer_name为字符串,opsets是以域为键的对象,inputs和outputs是{"name", "elem_type", "dims"}的列表,initializers是按名称排序的列表,nodes是 op_type 的列表。dims中的每个轴,固定时为整数,符号时为字符串,什么都没有时为 null。elem_type是onnx.TensorProto.DataType.Name(...)给出的名称(例如 FLOAT)。- 从第 3 步开始,
info的响应中增加input_overrides(在 graph.input 与 initializer 两边都有的名称,按名称排序的列表)和runtime_inputs(会话实际要求提供的输入名称,打不开会话时为 null)。inputs中不要放由 initializer 填充的名称。 check <모델>(占位符为模型文件)的响应:{"checker": "ok"|"error", "full_check": "ok"|"error", "message": 문자열}。失败时 message 中原样写入所捕获异常的第一行。load <모델>(占位符为模型文件)的响应:{"load": "ok"|"error", "error_type": 예외 클래스 이름 또는 null, "message": 문자열}。stamp <모델> <opset> <ir> <출력>(占位符依次为模型文件、opset、IR 版本与输出文件)只修改默认域的 opset 和 ir_version,并保存为另一个文件。不要动节点、权重和 producer_name。响应为{"out", "opset", "ir_version", "nodes"}。scan <모델>(占位符为模型文件)的响应:{"min_ok": 정수 또는 null, "max_ok": 정수 또는 null, "ok": 정수 목록, "failed": 정수 목록}。把版本号从 1 依次替换到 27,只看会话能否打开(判定依据是能否打开会话,而不是检查器的结果)。opset_range.json中至少写入min_ok和max_ok。export_report.json中写入model、ir_version、producer_name、opset、nodes、initializers、runtime_inputs、min_ok_opset、max_ok_opset,以及装有“检查器通过但运行时拒绝”的版本的checker_ok_runtime_error对象(opset、checker、load)。export_report.md分为## 무엇을 내보냈나、## 판이 굳는 자리、## 검사기가 못 잡는 것、## 다음 사람에게 넘길 것四节来写(四个标题为韩文,依次意为“导出了什么”“版本固化的位置”“检查器抓不到的事”“要交给下一个人的事”),并用数字写出能打开的版本的下限和上限。- 官方文档:ONNX Concepts · ONNX Versioning · ONNX IR · ORT Compatibility · ORT Python API
- 常见错误:把
graph.input的个数说成输入个数;只跑默认检查就报告通过;以为调低版本号算子也会跟着降下去;把运行时的警告混进标准输出,导致 JSON 损坏。
亲手搭建模型
创建并运行 /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 去问,写下实际得到的判定——评分器也会生成同样的文件再确认一遍。报告中要用数字写出下限和上限,接收方才能拿来和自己的运行时对照。