最近在做一个检测模型部署,用PyTorch训练好的模型转ONNX,用onnxruntime推理发现输出和原模型差很多,不是小误差,是那种完全对不上的。我检查了输入预处理、归一化参数,都是对齐的,动态轴也设了。模型里有F.interpolate和自定义的ROIAlign,不知道是不是这些算子转换有问题。另外我试了opset 11和12,结果都差不多。有没有大佬遇到过类似情况?是不是我转的时候漏了什么参数,还是说某些层必须用onnx-script重写?求指点,部署卡在这好几天了。
PyTorch转ONNX后推理结果和原模型不一致,是哪里出了问题?
全部回复
共 29 条试过逐层对比输出没?多半是ROIAlign自定义算子转错了,用onnx-script重写试试。
这种情况挺常见的,尤其是检测模型里带自定义算子的。F.interpolate一般还好,但如果你用的是双线性插值且align_corners设置和导出时不一致,输出就会偏,虽然不至于完全对不上,但会累积误差。ROIAlign基本可以确定是罪魁祸首,PyTorch的torchvision.ops.roi_align导出到ONNX经常出问题,要么被拆成乱七八糟的算子,要么直接走fallback路径,结果onnxruntime算出来和原版差得离谱。你可以先试试把ROIAlign换成用grid_sample手写实现,或者干脆用mmdeploy、torchvision自带的onnx导出工具链,它们对这类算子做过适配。另外建议你逐层对比,把模型截断到ROIAlign之前,看输出是否一致,这样能快速定位到底是哪一层炸的。还有个小坑是eval模式,如果忘了model.eval(),BN和dropout会让结果完全乱掉,虽然这个比较基础但还是提一嘴。opset 11和12差异不大,真正影响大的是算子版本和导出时的dynamic_axes配置,建议开onnxruntime的verbose日志看每个节点的输出。
自定义ROIAlign基本都会翻车,建议先单独导出这部分跑个对比,八成就是它。
自定义ROIAlign大概率是坑,建议先单独导出这部分对比输出。我当初也是卡在这,换成onnx-script重写就对齐了。
试试把F.interpolate换成固定尺寸的Upsample,ROIAlign建议用onnx-script导出自定义算子,我之前也卡在这。
F.interpolate转ONNX确实容易踩坑,特别是align_corners和mode参数,你确认下导出时是不是和PyTorch里完全一致,默认值有时候会对不上。ROIAlign的话原生torchvision那个导出一般没问题,但如果是自己写的实现,大概率得用symbolic重写或者换成torchvision的版本。建议你先把模型拆开,逐层比对ONNX和PyTorch的中间输出,很快能定位到是哪一层开始飘的,比盲猜opset版本高效多了。
我之前也踩过这个坑,最后发现是F.interpolate在导出时默认用了half_pixel的坐标变换,跟PyTorch训练时的align_corners设置对不上,输出会整体偏移。你检查下转ONNX时是不是没显式指定align_corners和mode,这俩参数容易在导出时丢。另外自定义ROIAlign如果用了动态shape,建议单独抽出来用onnx-script导出再拼回去,比直接torch.onnx.export靠谱。可以先用固定输入跑一版,逐层对比ONNX和PyTorch的中间输出,一般能快速定位到是哪一层炸了。
F.interpolate 和自定义 ROIAlign 确实是重灾区,尤其是 interpolate 在 opset 11 以下默认走 nearest 或者坐标系对齐方式不一致,很容易导致后面全乱。建议你把这两个模块单独拎出来做个小测试,固定输入对比 PyTorch 和 ORT 的输出,看哪个先崩。ROIAlign 如果没对应 ONNX 算子,一般是导出时被拆成 arange、floor 这些组合,精度和边界处理跟原实现差挺多,可能得用 onnxscript 或自定义 op 重写。另外 opset 11 和 12 差别不大,可以试试直接上 16 或 17,有些 interpolate 的语义修正了。
F.interpolate和自定义ROIAlign确实是重灾区,尤其ROIAlign这种没有标准ONNX算子的,torch.onnx.export大概率是给你按trace时的固定输入shape硬编码了坐标计算逻辑,换个输入尺寸或者框的数量对不上就崩了。你可以先单独把这两个模块拎出来做个最小复现,固定同一份输入分别跑PyTorch和onnxruntime,看看到底是哪个先炸。另外别只比最终输出,中间每层都dump出来对一遍,很多时候是某个reshape或者transpose在导出时维度语义变了,误差会一路放大。opset 11和12对interpolate的支持确实有差别,但如果是nearest模式且align_corners没显式指定,两边默认行为可能就不一样,这个坑我踩过。自定义算子那块建议直接用torch.onnx.register_custom_op或者干脆在导出前把ROIAlign换成可导出的实现,比如用grid_sample近似或者自己写个纯张量版本。还有个小技巧是导出时开verbose,看看有没有warning被刷过去,经常有算子被静默降级成constant或者fallback到不支持的实现。如果实在搞不定,可以试试先把模型拆成backbone和head分别导出,定位到具体是哪一段开始飘的。