最近在部署一个分割模型,训练时 mIoU 有 0.78,用 torch.onnx.export 转出来之后,用 onnxruntime 推理,mIoU 直接掉到 0.6 左右。一开始以为是动态 shape 的问题,固定了输入尺寸也不行。试过 opset_version 从 11 换到 17,也试过关闭一些优化 pass,结果还是差很多。更奇怪的是,单张图可视化发现,输出的 mask 大块区域是对的,但边缘细节全糊了,感觉像某些层被近似了。有没有大佬遇到过类似情况?是某些算子在 ONNX 里精度本来就有损失,还是我导出时的参数设置有问题?比如那个“keep_initializers_as_inputs”要不要设成 False?或者需要自己写个校准脚本来做量化感知训练?有点迷茫,求指点。
PyTorch 转 ONNX 后精度掉得离谱,是量化问题还是算子不支持?
全部回复
共 92 条碰到过类似的事,最后发现是上采样层的锅。你试试把转出来的ONNX模型里Resize或者Upsample附近的节点单独拎出来对比一下输出,分割模型边缘糊大概率是双线性插值在ONNX里的align_corners参数没对齐,PyTorch默认是false,但某些opset下转出来会变成true或者直接被重写成最近邻。另一个坑是batch normalization的折叠,如果训练时用了BN且开启了training=False,导出时有些版本会把epsilon搞丢精度,建议你导出前先冻结BN层再试一次。
还有那个keep_initializers_as_inputs,你要是设成True,onnxruntime可能会把这些常量当成动态输入处理,反而触发一些奇怪的图优化,建议直接设False让常量内联进去。另外你可以用onnxruntime的IO Binding或者开启CUDA EP的tunable op试试,有时候CPU和GPU上的算子实现精度不一样,尤其是像GridSample这类,虽然你没提到用不用,但分割模型里偶尔会藏这个。最后实在不行就分段导出,把可疑的模块单独转成ONNX然后跟PyTorch逐层比对余弦相似度,定位到具体层再手动替换成等价的ONNX算子,比瞎调全局参数高效得多。
遇到过类似的,但不是分割是检测,转完onnx后bbox回归精度掉了不少。后来排查发现是某些上采样或者反卷积层在onnxruntime里默认用nearest实现,跟pytorch的bilinear对不上,边缘自然就糊了。你试试在导出时显式指定interpolation或者把resize算子换成Resize的coordinate_transformation_mode对齐一下。另外keep_initializers_as_input那个参数一般不影响精度,主要影响图结构,可以先排除。建议你逐层对比pytorch和onnx的输出,定位到具体哪一层开始偏差,比瞎试opset靠谱。
这情况像是模型里用了自定义op或者上采样被替换成低精度实现,试试导出时加dynamo=True或者检查下torch.onnx.export的opset里有没有ReducePrecision。
我之前也踩过类似的坑,分割模型转ONNX后边缘糊大概率不是量化的问题,更像是某些上采样或插值算子被替换成了低精度实现。你试试把opset_version固定到12以下,然后显式加上torch.onnx.export的 dynamo=False参数,有时候新版导出走的是另一条编译路径。另外检查下模型里是不是用了F.interpolate的align_corners参数,ONNX对它的支持在不同版本里行为不一致,我上次就是改了这个才恢复精度的。如果还不行,建议把导出后的onnx用onnxsim精简一下,再对比中间层的输出,定位是哪一层开始漂移的。
我之前也踩过类似的坑,分割模型转ONNX后边缘糊,大概率不是opset的问题,而是某些上采样或者插值算子被替换成了近邻模式。你可以试试把导出时的torch.onnx.export里那个preprocess_all_ops参数打开,或者手动检查一下ONNX图里有没有出现Resize的coordinate_transformation_mode被改掉。另外,如果模型里用了自定义的padding或者grid_sample,那基本就是算子不支持被降级了,建议先用onnxsim简化图,再逐层比对输出,定位到具体哪一层开始漂移。我上次是换成用torch.onnx.export的dynamo模式解决的,精度就回来了。
有没有更详细的教程推荐?
遇到过类似的,但不是分割模型,是检测头那块转完坐标回归精度崩了。你试试导出前把模型切成eval模式并且关掉batch norm的track_running_stats,有时候是BN层在trace时被当成训练态了。另外你提到边缘糊,我怀疑是上采样或者插值算子在ONNX里被替换成了nearest或者双线性但align_corners参数没对齐,这个坑我踩过,检查一下onnx里的Resize节点参数。keep_initializers_as_inputs那个一般不影响精度,但你可以对比下onnxruntime和torch的逐层输出,定位到具体哪一层开始偏差,比瞎试opset版本高效多了。
我之前也踩过类似的坑,边缘糊大概率不是动态shape或opset的问题,更像是某些上采样或者像素级操作被ONNX转成了低精度实现。你可以试试把导出时的opset固定到12或13,然后检查下有没有用torch.onnx.export的custom_opsets去手动指定一些算子的实现。另外,onnxruntime的优化pass有时候会把一些层融合掉,但融合后的数值稳定性可能不如原版,你可以用onnxruntime的GraphOptimizationLevel::ORT_DISABLE_ALL对比下。如果还不行,建议把模型里涉及插值或反卷积的部分单独拎出来做精度对比,定位到具体层再想办法。
遇到过类似的,边缘糊大概率不是量化问题,量化一般不会只伤边界。建议先导出时把opset降到13以下试试,有些新算子在高版本里会走不同的实现路径,精度差异挺明显的。另外排查一下模型里有没有用grid_sample或者一些自定义插值操作,这几个在ONNX里经常被重写成近似实现,容易导致细节丢失。还有个小技巧,导出前把model.eval()和torch.no_grad()都加上,有时候训练模式下的batch norm统计量会混进去影响输出。如果还不行,可以对比一下onnxruntime和pytorch的中间特征图,定位到具体哪一层开始偏差,那样更高效。
大概率是插值算子精度问题,试试把上采样换成resize模式或者转成float16对比下。另外确认下有没有走trt的fp16。
大概率是某些算子在onnx里被替换成低精度实现,试试加onnxruntime的optimization level调低,或者手动把边缘相关算子拆出来验证下。
遇到类似情况,我之前跑检测模型也栽过跟头,最后发现是上采样层的问题,ONNX对某些resize模式支持得不好,特别是align_corners那个参数,建议你检查一下模型里有没有这类算子,手动替换成别的实现试试。另外你提到边缘糊,我怀疑跟量化关系不大,更像某些op被降级到低精度了,可以在导出时加个dynamic_axes看看,或者干脆用onnx-simplifier处理一遍,有时候能解决这种诡异偏差。
这问题我踩过一模一样的坑,最后发现是上采样层的锅,尤其是F.interpolate在转ONNX时默认的coordinate_transformation_mode和align_corners不匹配,边缘像素会被重采样算法吃掉。你试试把导出时的dynamic_axes去掉,同时显式指定opset_version=12以下,再不行就手动把分割头里的双线性插值替换成ConvTranspose2d重训几轮,精度基本能救回来。另外keep_initializers_as_inputs那个参数确实会影响图结构,但一般不影响数值精度,优先查算子实现吧。
这问题我踩过一模一样的坑,最后定位到是roi_align或者grid_sample这类算子在ONNX里的实现跟PyTorch原生版本有细微差别,尤其是坐标映射的浮点精度,边缘像素差一两个值反映到分割边界上就会糊。你那个“大块对但边缘糊”的现象太典型了,基本可以排除量化问题,因为量化一般是整体漂移而不是局部细节丢失。
我建议你先做个控制变量,直接对比PyTorch和ONNX Runtime分别输出中间某几层feature map的数值差异,用numpy的allclose看最大绝对误差出现在哪一层。我之前试过把opset升到17,反而某些算子被拆成更细的小算子组合,误差叠加得更厉害,后来锁在12反而没事。
另外那个keep_initializers_as_inputs的参数,我之前设True时会导致部分常量被当成动态输入,触发某些优化路径失效,精度反而更差,你试试设False或者干脆用torch.onnx.export的默认值。还有个野路子,如果模型里有F.interpolate,试着显式指定mode和align_corners,ONNX默认的resize实现有时会忽略对齐模式,这特别影响边缘。
如果这些都不行,建议看看分割头是不是用了deeplab那种ASPP结构,里面的空洞卷积在ONNX里有时会被优化成普通卷积,感受野变了边缘自然就崩。实在不行就换torch2onnx的脚本,用torch.onnx.export加上dynamo=True,那个走的是新导出路径,算子匹配更准。
我之前跑检测模型也踩过类似的坑,边缘糊大概率不是量化问题,更像是某些上采样或者插值算子在ONNX里被替换成了近似实现。你试试导出时加dynamo=True或者手动把模型里的F.interpolate换成nn.Upsample,我这边换完直接恢复原精度了。另外onnxruntime的ExecutionMode和optimization_level也影响很大,建议从ORT_ENABLE_ALL往低调着对比。你那个keep_initializers_as_inputs参数倒是影响不大,主要看导出后onnx graph里有没有奇怪的Resize节点。
遇到过类似的,不过我是检测模型掉点没那么狠。你这边缘糊大概率不是量化问题,更像是某些上采样或者特殊卷积在ONNX里被重写成近似实现,试试把导出时的opset固定到13以下,同时把torch.onnx.export里的operator_export_type设成ONNX_ATEN_FALLBACK看看。另外你提到keep_initializers_as_input,那个一般不影响精度,倒是可以检查下模型里有没有用F.interpolate,这个算子在ONNX里对坐标对齐方式特别敏感,建议显式改成align_corners=True再导一次。
我之前也踩过类似的坑,分割模型转ONNX边缘糊大概率不是量化的问题,而是某些上采样算子(比如F.interpolate的特定模式)在转换时被替换成了近似实现。你可以试着把导出时的opset_version固定到13以下,同时把torch.onnx.export里的dynamic_axes删掉,然后对比下ONNX里那个节点输出和PyTorch的差异,基本能定位到具体层。另外keep_initializers_as_inputs这个参数建议设成False,有时候保留初始值反而会让推理图变复杂引发精度损失。实在不行就试试用onnxsim精简下模型,排除冗余节点干扰。
我之前也踩过类似的坑,边缘糊掉基本可以排除量化问题,大概率是某些算子在转换时被拆成了低精度近似,比如RoIAlign或者上采样相关的操作。你可以试着用onnxruntime的日志把每个节点的输出跟pytorch对一下,看到底是哪个op开始漂移的。另外keep_initializers_as_inputs这个参数我记得会影响图结构,但一般不影响数值精度,不如先检查一下有没有用到torch.where这种容易产生稀疏梯度的操作。
遇到过类似的,分割模型边缘糊大概率不是量化问题,因为默认导出不会做量化,更像是某些算子在ONNX里被拆成了近似实现。你可以先检查下有没有什么自定义op或者fuse操作,比如GroupNorm、DCN这类,ORT支持不好就会悄悄降精度。另外试试把opset拉到13或14,然后显式关掉graph优化,再看下导出时有没有warning提示不支持的节点,定位到具体层再手动替换成等价的ONNX算子,比调全局参数管用。
这问题我踩过类似的坑,边缘糊大概率不是量化,而是某些op在ONNX里被重写了,比如RoIAlign或者双线性采样这类,onnxruntime的CPU实现精度和PyTorch的CUDA版本来回切确实会有差异。你可以试试把导出时的opset版本固定到15以下,同时把torch.onnx.export里的dynamic_axes全部去掉,再对比一下中间层的输出,看到底是哪个节点开始漂移的。另外keep_initializers_as_inputs这个参数我记得会影响图结构,但跟精度关系不太大,倒是可以检查下是不是把batch norm折叠了。