最近在部署一个分割模型,训练时 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 条遇到过类似的,多半不是量化问题,检查下有没有用F.interpolate上采样,转ONNX后对齐方式会变。
我之前也踩过类似的坑,最后定位到是上采样层和某些自定义op在导出时被拆成了几个基础算子,精度确实会有损失。你可以试试把模型里用到的插值方式换成ONNX官方支持的resize模式,另外检查下有没有用grid_sample这类容易出问题的层。还有个思路是导出时加上dynamic_axes=True,但固定输入尺寸后反而会触发某些融合优化,导致边缘退化。建议你直接对比onnx和pytorch输出每个中间层的tensor,看看是从哪一层开始漂移的。
这情况太典型了,我当初转分割模型也踩过同一个坑。你试试把torch.onnx.export里的do_constant_folding设成False,再把opset_version固定到12或13,有时候高版本opset反而会触发某些融合优化。另外你检查下模型里有没有用F.interpolate或者nn.Upsample,特别是align_corners这个参数,ONNX对它的实现跟PyTorch不完全一致,边缘细节糊很可能就是上采样层的对齐方式在转换时被改写了。还有个偏门但有效的方法,转完用onnx-simplifier过一遍,它能帮你把一些冗余的reshape和transpose清理掉,有时候精度问题反而是因为图结构太复杂导致runtime执行了不同的计算路径。如果还不行,就看看是不是用了自定义的损失函数或者训练时的数据预处理在推理时没对齐——比如训练时做了归一化但推理时忘了减均值。最后实在没辙,可以试下onnxruntime的CUDAExecutionProvider,CPU和GPU的算子实现精度差异有时也挺明显的。
我之前跑检测模型也遇到过类似情况,边缘糊大概率不是量化的事,更像是某些上采样或者插值算子在ONNX里被替换成了近似实现。你可以先关掉onnxruntime的图优化试试,特别是那些跟内存布局相关的pass,然后再看导出时把opset降到13以下,有些版本对roi_align这类算子的支持会有细微差别。另外检查一下有没有用torch.where或者masked_fill这种容易在导出时被展开成多个算子的操作,有时候精度就是这么一点点丢掉的。你方便的话可以打印一下onnx里每个节点的输出范围和原始pytorch对比一下,能更快定位是哪一层开始出问题的。
我之前也踩过类似的坑,分割模型转ONNX后边缘糊大概率不是量化的问题,而是某些算子的实现差异,尤其是上采样和align_corners相关的参数,ONNX默认行为和PyTorch不完全一致。建议你用onnxruntime的推理结果和PyTorch逐层对比一下,先定位到具体是哪几层开始偏差,我上次就是卡在resize的coordinate_transformation_mode上。另外你说的keep_initializers_as_input这个参数也会影响图优化,但一般不会导致精度掉这么狠,可以试着把导出时的dynamic_axes和opset版本组合再换换,比如用17加上onnxsim简化一下图,有时候能解决一些奇怪的问题。
我之前做检测模型转换也踩过类似的坑,不过没你掉得这么狠。边缘糊这个现象其实挺典型的,我怀疑不一定是量化的事,因为你说的是直接导出,没提动态量化或静态量化,那ONNX默认就是FP32,精度损失不应该这么大。你试过对比一下PyTorch和ONNX Runtime在相同输入下逐层的输出吗?我之前就是靠写个脚本把中间层的feature map导出来对比,最后发现是某个上采样或者插值算子在ONNX里的实现跟PyTorch不一致,尤其是align_corners这个参数,两边默认值不一样就会导致边缘对不齐。另外你提到的keep_initializers_as_inputs,我记得这个参数会影响模型结构,但一般跟精度无关,不过你可以试着设成False看看,有时候能排除一些奇怪的优化干扰。还有一个可能,你是不是用了torch.where或者一些条件控制流?这些在ONNX里容易被拆成多个算子组合,精度就对不上了。建议你先把模型里所有自定义操作都换成标准算子,再试试opset 17,我之前换成17之后有些算子实现确实更准了。如果还是不行,强烈建议你用onnxsimplifier把图简化一下,有时候冗余的reshape和transpose会引入数值抖动。
我之前做检测模型也踩过类似的坑,边缘糊大概率不是量化的问题,更像是某些上采样或者插值算子被ONNX默认转换成了精度较低的版本。你试试把导出时的opset_version固定到12以下,同时显式指定dynamic_axes为None,再对比一下每个中间层输出,看看到底是哪一层开始对不上。另外keep_initializers_as_input那个参数确实会影响图结构,建议设成False试试,有时候初始权重被当成输入会导致精度异常。如果还不行,可以试下用onnxsim简化模型后再跑,有次我就是这么解决的。
我之前也踩过类似的坑,分割模型边缘糊大概率不是量化的问题,更像是上采样或者插值算子在ONNX里的实现跟PyTorch不一致。你可以先单独导出到onnx后,用onnxruntime对比一下中间层的输出,定位到具体哪一层开始出现差异,我上次是卡在F.interpolate的align_corners参数上。另外你提到的keep_initializers_as_input,如果设成True的话,有时候会影响图优化导致的精度变化,建议试试看。还有个小技巧,导出时把opset尽量调高,并且显式指定一下算子集版本,有些老版本对某些层有近似计算。
遇到过,多半是某些算子在ONNX里被重写或者精度模式问题,试试导出时加opset_version=12配合keep_initializers_as_inputs=False。
我之前是换onnxsimplifier精简图后恢复的,你那个边缘糊可能跟上采样算子实现有关,查查输出层用的什么插值。
edge细节糊大概率是roi align或反卷积精度问题,你试试把opset升到17再配onnxsim看看。
遇到过类似,最后锁定是插值算子的坐标映射差异,换成nearest验证下是不是这个原因。
说到这个我太有感触了,之前做检测头的时候也踩过类似的坑,mAP从0.72掉到0.55,当时差点怀疑人生。你提到边缘全糊,我第一反应不是量化,因为纯float32导出一般不会差这么多,更像是有算子在转换时被替换成了低精度实现,或者某些自定义操作被拆成了多个op的近似组合。建议你先用onnxruntime的graph优化开关逐个关掉试试,特别是那些涉及fuse的pass,有时候它们会把几个层合并成一个数学上等价但数值上不完全一致的算子。另外,你试过用onnxsimplifier先简化一遍模型再导出吗?有时候冗余的shape计算和transpose会在转换过程中引入奇怪的数值扰动。还有一个很隐蔽的点,就是导出时如果用了torch.onnx.export的dynamic_axes,即使你固定了输入尺寸,某些内部reshape或resize的坐标计算也可能变成浮点近似,导致边缘对齐出问题。你可以试着在导出时把opset调到13以下,然后手动检查一下输出feature map的统计值,比如每个通道的mean和std,跟PyTorch原模型的输出对比,看偏差是从哪一层开始放大的。如果发现是resize或grid_sample这类算子,那基本就是ONNX的插值算法和PyTorch默认的align_corners行为不一致导致的,需要你在导出前手动把插值模式改成ONNX能精确映射的那种。最后,如果实在找不到原因,可以试试直接用onnxruntime的CUDAExecutionProvider跑一下,有时候CPU和GPU的float计算精度差异也会被放大。
我之前做检测模型也踩过类似的坑,最后发现不是opset的问题,是上采样层在导出时被重写成了双线性插值的近似实现,特别是align_corners这个参数,ONNX默认行为和PyTorch里不一致,边缘像素会差一两个像素,分割任务对边界敏感的话精度就崩了。你试试把模型里的F.interpolate换成nn.Upsample,或者在导出前手动把align_corners设成False看看,有些版本对这两个路径的转换逻辑完全不同。另外你提到keep_initializers_as_inputs,这个一般不影响精度,但如果你用了torch.where或者masked_fill这类带条件分支的操作,ONNX会展开成多个子图,某些子图在静态shape下可能被错误折叠,建议用onnxsim简化后再对比一次输出。还有一个冷门方向,就是你的模型里如果有自适应池化,转ONNX时opset低的话会展开成固定kernel,但opset高反而可能走的是动态路径,建议你直接打印ONNX里每个节点的输出和PyTorch逐层比对,先定位到具体是哪一层开始偏差,别光看最终mIoU,这样能省很多排查时间。最后想问下,你推理时用的onnxruntime是CPU还是GPU版?我遇到过CPU版对某些算子用不同内核实现,精度和GPU版差不少的情况。
我之前也踩过类似的坑,分割模型边缘糊大概率不是opset的问题,重点查一下上采样和插值算子,ONNX对nearest和bilinear的默认对齐方式跟PyTorch不一样,容易出这种细节差异。你可以把模型里所有Upsample换成ONNX原生支持的Resize,然后显式指定coordinate_transformation_mode,基本能解决。另外keep_initializers_as_inputs那个参数一般不影响精度,主要影响图优化,先别纠结它,建议用onnxruntime的graph_optimization_level设为ORT_DISABLE_ALL对比一下,如果精度恢复了就说明是pass里动了算子。如果还不行,就导出时加个dynamic_axes配一下两三个典型shape,有些算子对静态shape会走不同的kernel路径。
我之前也踩过类似的坑,分割模型边缘糊大概率不是opset版本问题,而是某些上采样或插值算子在ONNX里的实现跟PyTorch不一致。你可以先试试把导出时的opset固定到15以下,同时检查一下模型里有没有用F.interpolate的align_corners参数,这个很容易出差异。另外建议用onnx-simplifier过一遍图,有时候是冗余节点导致精度飘,我上次就是靠这个解决的。你那个keep_initializers_as_inputs没写完,它一般不影响精度,主要影响推理时的输入输出接口。
我之前也踩过类似的坑,分割模型转ONNX后边缘糊其实挺典型的,不一定全是量化的问题。你提到opset换过、动态shape也固定了,那大概率是某些算子在导出时被融合或者重写成了低精度近似实现,比如一些上采样或者反卷积里的resize模式,ONNX的默认行为可能和PyTorch里训练时的插值方式不完全一致。我建议你先用onnxruntime的推理日志打开看一下每个节点的输出,对比一下和PyTorch中间张量的差异,定位到具体是哪几层开始偏差变大,这样比盲调参数高效得多。另外,keep_initializers_as_inputs这个参数确实会影响图结构,但一般不会导致精度掉这么狠,你可以把它设成False试试,同时检查一下有没有用到torch.onnx.export里的dynamic_axes,虽然你固定了尺寸,但有时候某些辅助输出还是会被动态化。还有个思路是试一下onnxsim或者polygraphy做图优化前的精度对比,有时候onnxruntime默认的优化pass会触发一些不稳定的算子实现,比如某些版本的EfficientNMS或者Resize的坐标变换模式。如果最后定位到是某些自定义算子或者PyTorch特有的行为,比如grid_sample或者可变形卷积,那基本无解,只能考虑用TensorRT或者把关键层留在PyTorch里跑。不过你这个边缘全糊的现象,我更怀疑是export时opset版本对某些op的默认属性定义变了,比如align_corners在ONNX里不同版本默认值不一样,你可以专门检查一下所有涉及坐标映射的层。
遇到过类似的,但不是分割模型,当时是检测头那边的输出不对劲。你试试把export的时候那个opset_version固定到13以下,然后关掉所有onnx的优化pass,特别是那个graph优化,有时候问题出在算子融合上。另外检查一下upsample或者interpolate的实现,ONNX里有些版本的resize对align_corners处理跟PyTorch不完全一致,边缘糊可能跟这个有关。你导出前用onnxsim简化一下图再跑跑看,我之前这么弄完精度就回来了。
边缘糊多半是resize或上采样算子的坐标对齐方式不一致,试试把onnx的resize模式显式设成bilinear加half_pixel。
之前遇到过类似,检查下模型里有没有grid_sample,那个在onnx里精度很容易翻车。
我之前转检测模型也碰到过,边缘糊多半是RoIAlign或插值算子精度问题,试试把opset降到13以下,或者换torch2onnx的simplify版本。
遇到过,检查下是不是有上采样层被替换成双线性近似了,用onnxruntime的CUDA执行提供器跑,CPU和GPU结果差好多。
这情况我遇过,多半是某些自定义算子在ONNX里被拆成近似实现,试试点开图看看有没有奇怪的子图。
建议对比下onnx和pytorch逐层输出,定位到具体层再针对性处理。
我遇到过类似的,尤其是边缘糊,多半是某些算子在ONNX里被替换成低精度实现了,试试加onnxruntime的use_device或者手动改图。
你这情况建议先对比下ONNX和PyTorch输出的逐层feature map,定位到具体哪层开始飘,大概率是插值或归一化算子的锅。