4、ONNX模型调试:ONNX GraphSurgeon工具、ONNX模型可视化、常见导出错误与修复
说实话,ONNX 这个中间格式,看着挺美,用起来却经常让人头疼。
我见过太多人,模型在 PyTorch 里跑得好好的,一导出 ONNX 就报错。或者导出成功了,转 TensorRT 又出幺蛾子。问题出在哪?
说白了,ONNX 就是个中间人。它不生产推理,它只是模型的搬运工。但搬运过程中,稍不留神就会把模型结构搞变形。
这一章,我就带你手把手搞定 ONNX 的调试。从可视化到手术刀级别的修改,再到那些常见的坑,咱们一次性说清楚。
4.1 为什么要调试 ONNX?
你可能觉得,模型能跑就行,管它 ONNX 长什么样?
嗯,我以前也这么想。直到有一次,我导出一个分类模型,在 PyTorch 里精度 95%,转成 ONNX 后精度掉到了 60%。查了两天,最后发现是一个 Reshape 节点把张量维度搞乱了。
所以,调试 ONNX 的核心目的就两个:
- 验证结构正确性:确保计算图和你设计的网络一致,没有多出奇怪的节点,也没有丢掉关键的算子。
- 检查数值一致性:用相同的输入,对比 PyTorch 和 ONNX 的输出,误差必须在可接受范围内(一般 1e-5 以内)。
核心原则:永远不要相信 ONNX 的导出是完美的。每次导出后,必须做一次结构检查和数值比对。
4.2 ONNX 模型可视化
调试的第一步,是看清模型长什么样。我习惯用两个工具:Netron 和 ONNX GraphSurgeon 的打印功能。
4.2.1 使用 Netron 在线查看
Netron 是最直观的可视化工具。你只需要把 .onnx 文件拖进去,就能看到完整的计算图。
我个人习惯用它做快速检查:
- 看输入输出的名称和形状对不对
- 看有没有奇怪的节点类型(比如突然冒出一个 Cast 或 Gather)
- 看张量的流向是否合理
小技巧:Netron 支持点击节点查看属性。比如 Conv 节点的 kernel_shape、strides、pads 都能看到。这在你怀疑某个算子参数不对时特别有用。
4.2.2 使用 ONNX 自带 API 打印图结构
Netron 虽然方便,但没法集成到脚本里。这时候我会用 ONNX 的 Python API 来打印节点信息。
import onnx
model = onnx.load("model.onnx")
graph = model.graph
print(f"输入: {[inp.name for inp in graph.input]}")
print(f"输出: {[out.name for out in graph.output]}")
for i, node in enumerate(graph.nodes):
print(f"节点 {i}: {node.op_type}")
print(f" 输入: {node.input}")
print(f" 输出: {node.output}")
# 打印属性
for attr in node.attribute:
print(f" 属性 {attr.name}: {attr}")
这段代码会输出每个节点的类型、输入输出名称和属性。我在项目中经常用它来定位「哪个节点出了问题」。
4.3 ONNX GraphSurgeon 工具
可视化只是第一步。真正要动刀子修改 ONNX 图,还得靠 ONNX GraphSurgeon(简称 GS)。
这个工具就像 ONNX 的手术刀。你可以用它来:
- 删除无用的节点
- 替换不支持的算子
- 合并连续的节点
- 修改张量的形状和数据类型
4.3.1 安装与基本用法
pip install onnx-graphsurgeon
导入后,核心操作就是三步:加载、修改、保存。
import onnx_graphsurgeon as gs
import onnx
# 1. 加载模型
graph = gs.import_onnx(onnx.load("model.onnx"))
# 2. 修改图(比如删除所有 Identity 节点)
for node in graph.nodes:
if node.op == "Identity":
node.outputs = node.inputs # 跳过 Identity
# 3. 清理并导出
graph.cleanup().toposort()
onnx.save(gs.export_onnx(graph), "model_cleaned.onnx")
注意:修改图之后,一定要调用 cleanup() 和 toposort()。cleanup 会删除悬空的节点和未使用的张量,toposort 会重新排序节点保证拓扑顺序。我刚开始用的时候经常忘,结果导出的模型推理结果全是错的。
4.3.2 实战:删除无用节点
PyTorch 导出的 ONNX 经常包含一些冗余节点,比如 Identity、Cast(类型转换)、Shape 等。这些节点在推理时完全没用,反而会增加计算开销。
我曾经导出一个 ResNet50,ONNX 里居然有 200 多个 Identity 节点。用 GS 一键清理后,模型大小从 100MB 降到了 98MB,推理速度还快了 5%。
import onnx_graphsurgeon as gs
import onnx
graph = gs.import_onnx(onnx.load("resnet50.onnx"))
# 删除所有 Identity 节点
for node in graph.nodes:
if node.op == "Identity":
# 把 Identity 的输入直接接到它的输出上
node.outputs[0].inputs = node.inputs[0].inputs
node.inputs = []
# 清理悬空节点
graph.cleanup().toposort()
onnx.save(gs.export_onnx(graph), "resnet50_cleaned.onnx")
4.3.3 实战:替换不支持的算子
TensorRT 对 ONNX 算子有严格的限制。比如,它不支持动态 Reshape 中的某些模式,或者不支持某些激活函数。
遇到这种情况,我的做法是用 GS 把不支持的算子替换成等价的组合。
# 假设有一个不支持的 HardSigmoid 节点
# 我们可以把它替换成 Clip + Mul + Add
import onnx_graphsurgeon as gs
import numpy as np
graph = gs.import_onnx(onnx.load("model.onnx"))
for node in graph.nodes:
if node.op == "HardSigmoid":
# 获取输入和输出
inp = node.inputs[0]
out = node.outputs[0]
# 创建新的子图:alpha * clip(x, 0, 1) + beta
alpha = gs.Constant("alpha", np.array(0.2, dtype=np.float32))
beta = gs.Constant("beta", np.array(0.5, dtype=np.float32))
clip_node = gs.Node("Clip", "clip_0", inputs=[inp], outputs=[gs.Variable("clip_out")])
mul_node = gs.Node("Mul", "mul_0", inputs=[clip_node.outputs[0], alpha], outputs=[gs.Variable("mul_out")])
add_node = gs.Node("Add", "add_0", inputs=[mul_node.outputs[0], beta], outputs=[out])
# 替换原节点
graph.nodes.extend([clip_node, mul_node, add_node])
node.inputs = []
node.outputs = []
graph.cleanup().toposort()
onnx.save(gs.export_onnx(graph), "model_fixed.onnx")
经验之谈:替换算子时,一定要保证输入输出的张量名称完全对应。我踩过最大的坑就是输出名称写错了,结果后面的节点找不到输入,直接报错。
4.4 常见导出错误与修复
下面这几个错误,我敢说你迟早会遇到。提前了解,能省下不少 debug 时间。
4.4.1 动态轴不支持
错误现象:导出时提示 "ONNX does not support dynamic axes for this operator"。
原因:某些算子(如 Reshape、Gather)在动态 batch 或动态尺寸下无法导出。
修复方法:
- 如果只是 batch 维度动态,设置
dynamic_axes={'input': {0: 'batch'}} - 如果是其他维度动态,考虑固定输入尺寸,或者用 GS 手动修改图
4.4.2 算子不支持导出
错误现象:"RuntimeError: Exporting the operator XXX to ONNX opset version YY is not supported"。
原因:你用的某个 PyTorch 算子,在 ONNX 里没有对应的实现。
修复方法:
- 升级 PyTorch 和 ONNX 版本,新版本通常支持更多算子
- 用
torch.onnx.export的custom_opsets参数注册自定义算子 - 最直接的办法:在 PyTorch 里用原生算子重写这个操作
4.4.3 输入输出名称混乱
错误现象:转 TensorRT 时提示 "Input tensor name not found"。
原因:ONNX 的输入输出名称和 TensorRT 期望的不一致。
修复方法:
- 导出时显式指定
input_names=['input']和output_names=['output'] - 用 GS 修改节点的名称
# 用 GS 修改输入输出名称
graph.inputs[0].name = "input"
graph.outputs[0].name = "output"
4.4.4 常量折叠导致精度问题
错误现象:ONNX 推理结果和 PyTorch 不一致,误差很大。
原因:ONNX 在导出时会对常量进行折叠(Constant Folding),如果折叠过程中有精度损失,就会导致结果偏差。
修复方法:
- 导出时设置
do_constant_folding=False - 或者用 GS 手动检查并修复被折叠的常量
重要提醒:关闭常量折叠可能会让模型变大,但能保证精度。我一般建议在调试阶段关闭,确认没问题后再开启。
4.5 数值一致性验证
最后一步,也是最关键的一步:验证 ONNX 和原始框架的输出是否一致。
import onnxruntime as ort
import torch
import numpy as np
# 准备输入数据
dummy_input = torch.randn(1, 3, 224, 224)
# PyTorch 推理
model.eval()
with torch.no_grad():
torch_output = model(dummy_input)
# ONNX 推理
ort_session = ort.InferenceSession("model.onnx")
ort_inputs = {ort_session.get_inputs()[0].name: dummy_input.numpy()}
ort_output = ort_session.run(None, ort_inputs)
# 对比结果
np.testing.assert_allclose(torch_output.numpy(), ort_output[0], rtol=1e-03, atol=1e-05)
print("数值一致性验证通过!")
如果验证不通过,别慌。先用 Netron 看结构,再用 GS 定位问题节点。我遇到过的情况,90% 都是某个 Reshape 或 Transpose 搞乱了维度顺序。
嗯,ONNX 调试这块,说白了就是「多看、多改、多验证」。工具就那几个,但经验需要慢慢积累。你踩过的坑越多,后面就越顺。