导出那一刻被固化的东西 — opset、IR 与 initializer
一句话总结
ONNX 文件里装的不只是计算图,它还固化了 用哪个版本(opset)书写、用哪个 IR 版本记录、哪些是输入、哪些是已经确定的值。这四件事在导出的瞬间就定下来了,接收方无法更改这些决定。
为什么需要它
量化失败的报告,通常不是从转换命令开始的,而是从“我们的服务器打不开这个文件”开始的。文件看起来完好,onnx.checker 也一声不吭地放行,运行时却卡在打开会话这一步。
这种时候最耗时间的,是到文件之外去找原因:换转换选项、升级运行时版本、怀疑硬件。其实答案就写在文件开头的几个字节里。
文件中固化的四件事
第一,算子集版本与域。模型在 opset_import 中为每个域写下版本号。域为空字符串表示标准算子(ai.onnx),com.microsoft 这类其他域则是某个运行时的扩展。版本号是“文件里的算子要按哪个版本的定义来读”的指示,而不是性能设置。
第二,IR 版本。这是承载计算图的容器的格式版本。ONNX Versioning 把 opset 和 IR 分开编号,原因就在这里:算子定义的变化速度和文件格式的变化速度不同。实际上,IR 4 之前必须把 initializer 同时声明在 graph.input 中,之后就不必了。因此旧工具生成的文件里,权重至今仍会一并写在输入列表中。
第三,producer 信息。producer_name 和 producer_version 记录是谁生成了这个文件。它们没有任何功能,却是出事故时最先看的一行。收到一个这两项为空的文件,本身就是问题。
第四,initializer 与输入的边界。权重放在 graph.initializer 中,运行时需要人来提供的值留在 graph.input 中。如果同一个名称在两处都有,就表示“带默认值的输入”,运行时不会要求把这个名称作为必填输入。所以只看文件来数输入个数会数错——ONNX Concepts 对这两者做了区分说明。
检查器能抓到的与抓不到的
onnx.checker.check_model(model) 默认检查的是 结构:节点是否按拓扑顺序排列、所引用的名称是否存在、必填字段是否齐全。再加上 full_check=True,就会 连形状推断一起运行。这个差别在实际工作中很关键。矩阵乘法无法成立的 MatMul 会直接通过默认检查,到 full_check 才会被抓到。
还有两种检查都抓不到的情况。未注册域中的算子会被检查器放行——检查器把不认识的域当作“某人的扩展”而跳过。版本号被调低后保存的文件同样会通过。这两种情况都会在运行时卡住。
onnx.checker(기본) 구조만
onnx.checker(full) 구조 + 모양 추론
onnxruntime 세션 열기 구조 + 모양 + 이 런타임에 그 커널이 있는가
第三行的范围最窄,而部署真正要求的正是第三行。
在现场相遇的样子
第一,请求调低版本。接收方的运行时太旧,于是只把 opset 号调低后重新保存,这种事很常见。但版本号只是个印章,算子的定义不会跟着降下去。如果调低后的版本里没有该算子的定义,检查器会放行,运行时则会拒绝。报错信息也不是“版本太低”,而是“找不到该算子的实现”,原因因此更难看出来。
第二,导出方能用的版本比运行方能打开的版本范围更宽。库可以用最新版本来写,运行时却还打不开那个版本。ONNX Runtime Compatibility 之所以用表格列出每个运行时版本所支持的 opset 范围,原因就在这里。所以“用最新版本导出”这个建议本身就有风险。
第三,权重看起来像输入。打开旧工具生成的文件,会看到五个输入,而运行时只要一个。不了解这个差别,写出填入五个输入的代码,它就会在每次调用时把权重重新塞进去。
第四,把检查器通过当作部署依据。很多团队把“checker 通过”写进了发布条件。这个条件是必要条件,不是充分条件。部署门禁里必须包含 用与接收方相同的运行时版本真正打开一次会话 这一步。
实际工作中真正重要的事
- 用从文件中读出来的值说话。opset、ir_version、producer、输入列表不要猜,要打开文件读出来再写。
- 输入个数要问运行时。不要数
graph.input,而要数会话要求提供的名称。 - 量出版本范围再交接。亲自测出这个文件能打开的版本下限和上限,写进交接文档。
- 门禁要走到打开会话。不能只靠检查器就放行。
下一项实验要做什么
用 onnx.helper 亲手搭一个两层 MLP,再一步步扩展读取该文件中固化内容的工具 modelmeta.py。你会遇到把 initializer 同时声明为输入的旧格式文件,把输入和权重区分开;亲自测量默认检查与 full_check 的差别;只替换版本号,找出运行时能打开的范围的下限和上限。评分器每次都会用不同的形状、名称、版本号和激活函数构建自己的文件,真正运行你的工具并核对答案。