最近在部署一个分割模型,训练时 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 后精度掉得离谱,是量化问题还是算子不支持?
全部回复
共 107 条遇到过类似的,但不是分割模型,是检测头回归坐标的时候精度崩了。后来查出来是某些上采样或者插值算子在ONNX里的实现跟PyTorch不完全一致,尤其是align_corners这个参数,默认值两边就不一样,你检查下这个。另外keep_initializers_as_inputs那个参数建议设成False试试,有时候会把权重变成输入导致精度异常,虽然看着不相关但真有人踩过坑。边缘糊的话,也可能是转的时候把一些fused的bn层拆了,试试把模型设成eval模式再导出。
这个大概率是某些算子在ONNX里被重写了,试试把opset降到13以下,另外检查下有没有用到torchvision的插值算子,那个导出容易出问题。
大概率是某些算子在onnx里被重写了,比如插值或归一化,试试逐层对比中间输出定位下。
遇到过,多半是某些算子在ONNX里被替换成低精度实现了,试试导出时加opset_version=17配合dynamo模式,或者检查下preprocess的归一化参数有没有被错误折叠。
之前跑检测模型也碰到过类似情况,建议先看看是不是某些特殊算子被拆成了低精度近似实现,比如上采样或者插值类操作。你可以试着把导出时的opset版本固定到15以下,同时关掉onnx的graph优化,再用onnxruntime的log_severity_level调成0看看有没有warning提示具体哪个节点有问题。另外最好对比一下pytorch和onnx的逐层输出,定位到第一个差异大的层,我上次就是这么发现是resize模式导致的偏差。
大概率是某些上采样或插值算子不兼容,试试显式转成resize或者换opset版本对比下输出。
我遇到过类似的,边缘糊大概率不是量化问题,而是某些算子在ONNX里被拆成了低精度近似实现,尤其是上采样和反卷积这块。你试试把导出时的opset版本固定到12,然后显式关掉那个“eliminate_dead_connections”之类的pass,或者干脆用onnx-simplifier处理一下图结构。另外检查下有没有用到grid_sample这类算子,ONNX的算子实现和PyTorch原生行为经常有细微差异,尤其对坐标敏感的操作,边缘误差会被放大。要是方便的话,可以对比下ONNX模型和PyTorch模型逐层输出的最大误差,定位到具体哪一层开始漂移,这样比盲调参数高效得多。
遇到过类似的,但不是分割是检测模型,转完ONNX后边界框回归精度掉得厉害。你这个问题大概率不是量化,因为ONNX默认导出就是FP32,重点怀疑是某些上采样或者插值算子的实现差异,尤其是align_corners这种参数在ONNX里不同opset下行为可能不一致。建议你先把模型里所有涉及grid_sample、interpolate的层单独拎出来做输入输出对比,看看是不是在某个节点开始特征图就出现偏差。另外torch.onnx.export里有个dynamic_axes的配置,如果你输入输出都设了动态维度,即使固定了实际尺寸,某些算子也可能走不同的优化路径,试试只对batch维度设动态,其他全静态。还有个野路子,用onnx-simplifier过一遍,有时候能暴露出隐藏的精度坑。
遇到过类似的,但不是分割是检测,后来发现是插值层的align_corners在ONNX里默认参数对不上,尤其是上采样倍数大的时候边缘会糊,你检查下F.interpolate的配置。另外你提到keep_initializers_as_inputs,这个一般不影响精度,但可以试试把opset降到13以下,有些算子在更高版本会走不同的实现路径。还有个坑是batch norm在eval模式下导出时会被folding,但某些自定义实现会保留成训练模式,导致分布偏移,建议导出前确认模型已经切到eval且所有bn都冻结了。如果还不行,可以逐层对比onnx和pytorch的输出,定位到第一个差异大的节点,基本就能锁定问题算子。
遇到过类似的,但不是分割模型,当时是检测头那边掉点。你这情况我第一反应不是量化,因为onnx默认导出是fp32,量化得自己显式开,更像是某些op被替换成了低精度实现,比如InstanceNorm或者插值层的转换差异。建议先对比一下onnx和pytorch输出的逐层feature map,定位到具体哪一层开始漂移,另外试试把opset降到13以下,有些高版本op的近似算法反而更激进。边缘糊这个特征,我猜是上采样层或者边界相关的卷积被融合了,可以关掉graph优化里的fuse_bn和eliminate_deadend再跑一次看看。
我上次转Deeplab也碰到过类似情况,后来发现是batch norm在eval和train模式下导出的差异,你把模型切到eval再导出试试。另外可以检查下有没有用到grid_sample或者自定义op,这类算子在onnx里经常会被拆成近似实现,边界自然就糊了。如果确认不是这两点,可以试试用onnx-simplifier过一遍图,有时候冗余节点会导致精度异常。
我之前也踩过类似的坑,分割模型转ONNX后边缘模糊大概率不是量化问题,而是某些上采样或插值算子在ONNX里的默认模式跟PyTorch不一致,比如align_corners没显式传的话,导出时会用错误的坐标映射。你可以检查一下模型里有没有用F.interpolate或nn.Upsample,试试在导出时加上opset_version=12以上,并且在torch.onnx.export里显式指定opset的算子版本,或者试一下用onnxsim简化后对比。另外,keep_initializers_as_inputs这个参数我通常设False,避免多余输入干扰,但更关键的是把模型切成几段分别转,定位是哪一层开始崩的。
我之前做检测模型转换也踩过类似的坑,mIoU从0.78掉到0.6确实有点夸张,但边缘糊掉这个现象很典型,八成不是量化的问题,因为默认导出不开启量化,更像是某些op在转换时被拆解或融合出了问题。你可以先检查一下模型里有没有用GridSample、F.interpolate这类对齐敏感的算子,尤其是上采样方式,ONNX对双线性插值的坐标对齐实现跟PyTorch不完全一致,边缘差几个像素就足以让mIoU崩掉。另外,你提到的keep_initializers_as_input,这个参数如果设成True,会让一些常数被当作输入保留,反而可能引入额外的精度偏差,通常建议设False并配合动态shape一起处理。我建议你在导出时加上torch.onnx.export的check_model=True,然后逐个对比中间层输出,先定位是哪几层开始出现数值漂移,再针对性地替换算子或者用onnx-simplifier重写图结构。还有一个思路是绕过ONNX的某些融合pass,直接试一下onnxruntime的CUDAExecutionProvider跟CPU结果差多少,如果GPU上更差,那大概率是算子实现差异而不是模型本身的问题。顺便问一句,你用的分割模型是不是带了解码器里的反卷积或者分组卷积?这些在ONNX里有时会被优化成等效但数值不完全一样的结构。
我之前跑检测模型也遇到过类似的,边缘糊大概率不是量化的问题,更像是某些上采样或者插值算子转换时被替换成了近似实现。你可以试试把导出时的preprocess_all_optimizations关掉,或者手动把模型里的bilinear改成nearest看看差异。另外检查一下onnxruntime的execution_mode,有时候CPU和CUDA的kernel实现精度也不一样,我上次就是换成CUDA执行器就好了。
我之前跑检测模型也遇到过类似的,边缘糊大概率不是量化的问题,更像是某些上采样或者插值算子转换时被替换成了近似实现。你可以试试把导出时的preprocess_all_optimizations关掉,或者手动把模型里的bilinear改成nearest看看差异。另外检查一下onnxruntime的execution_mode,有时候CPU和CUDA的kernel实现精度也不一样,我上次就是换成CUDA执行器就好了。
之前跑检测模型也踩过类似的坑,精度掉得没你这么狠但现象很像,边缘糊大概率不是量化的问题,因为onnx导出默认fp32,量化是你自己主动做才会触发。我怀疑是某些上采样或者反卷积层在onnx里的实现跟pytorch不完全一致,尤其是align_corners这个参数,onnx的resize算子对坐标映射的处理有历史bug,opsert版本换到13以上会好点但也不是全解决。你试试把模型里涉及grid_sample或者F.interpolate的部分单独拎出来对比下输出,我之前发现pytorch的bilinear和onnx的bilinear在half-pixel中心对齐上差了一个像素,边缘自然就花了。另外“keep_initializers_as_input”那个选项如果设成True,会让推理时多出很多冗余输入,某些runtime会做额外优化反而引入误差,建议设False再试。还有个偏门思路,用onnxsimplifier把图精简一遍,有些pass会把卷积和bn融合,但融合时对epsilon的处理有精度差异,你可以对比下简化前后的输出。如果还不行,干脆考虑转成torchscript再用traced的onnx导出,有时候trace路径能保留更多原始语义。最后实在没辙就绕开onnx,直接用tensorrt的pytorch后端,虽然麻烦但至少算子行为可控。
遇到过,先别甩锅给量化,检查下有没有用adaptive avgpool或者grid_sample,这俩转ONNX经常边缘精度崩。
我之前也踩过类似的坑,最后发现不是opset的问题,是上采样层在导出时被替换成了双线性插值的近似实现。你检查一下模型里有没有用F.interpolate或者nn.Upsample,ONNX对这两者的支持有细微差别,尤其align_corners这个参数,很多教程里都没强调,但真的会影响边缘精度。另外你说大块区域对但边缘糊,这个特征其实很典型,我猜是转ONNX时某些卷积层被融合了,比如把BN和Conv合并,理论上数值应该等价,但实际浮点运算顺序变了,累积误差在边界上会被放大。你可以试试导出前把模型切成几段,分别转ONNX再对比每段的输出,定位到底是哪一层开始漂移的。还有,如果你用onnxruntime的CUDA执行提供方,有时候FP16会被自动开启,虽然你没显式设置,但某些版本会默认在部分算子用FP16,这会显著影响边缘检测的精度,可以在session options里把enable_cpu_mem_arena和execution_mode都调成最保守的试试。至于keep_initializers_as_inputs,我印象中它主要影响模型文件大小和动态shape灵活性,跟精度关系不大,但你可以试着把它设成False再用onnx-simplifier瘦身一下,排除冗余节点带来的干扰。最后想问下你用的onnxruntime版本是多少?我之前用1.16.0的时候遇到过某个算子的bug,升级到1.17.1就正常了,这种玄学问题有时候真是版本坑。
这情况我熟,多半不是量化,是某些算子在onnx里被重写后精度丢了,试试把导出时的算子切成高精度版本。
之前我转模型也遇到过,边缘糊基本是上采样层的问题,你查查bilinear插值是不是被替换成了nearest。
我之前也踩过类似的坑,分割模型的边缘糊掉大概率不是量化的问题,而是某些算子在ONNX转换时被重写成了低精度近似实现。你试过opset和优化pass都没用的话,建议先定位一下是哪个子图导致的精度下降,可以用onnxruntime的graph optimization逐层开/关来二分排查。我上次遇到类似情况,最后发现是模型里的grid_sample或者双线性插值在ONNX里被拆成了多个基础算子,浮点累加顺序变了,数值误差被放大。另外你提到keep_initializers_as_inputs,这个参数确实会影响推理时的权重加载方式,但一般不至于让mIoU掉这么多,除非你后面还做了动态shape的优化。一个比较笨但有效的办法是:把导出的ONNX每个节点单独用onnxruntime跑一遍,和PyTorch的输出对比,很快就能揪出罪魁祸首。还有个小细节,如果你用了torch的某些自定义autogradFunction,ONNX导出时可能直接走默认的近似路径,建议检查下模型里有没有类似F.interpolate的align_corners参数设置,这个经常被忽略。最后实在不行的话,试试torch.onnx.export的dynamic_axes配成False,然后输入用固定值导出,有些op在动态shape下会触发不同的实现分支。
这个精度落差确实挺典型的,边缘糊掉更像是在导出时某些上采样或者插值算子被替换成了近似实现。我建议你检查一下模型里的resize/upsample用的mode,ONNX默认的nearest和bilinear在坐标转换上跟PyTorch的align_corners逻辑不完全一致,这个很容易踩坑。另外可以试试把onnxruntime的execution_mode改成CPU,排除一下CUDA这边算子kernel的精度问题,或者直接用onnxruntime的Python API逐层对比中间tensor,定位是哪个节点开始分叉的。我之前遇到过类似情况,最后发现是RoIAlign的采样点计算在ONNX里被简配了。