onnxruntime评测模型精度不一致问题排查

gpu精度不一致问题追查
在做模型转换相关工作,但是最近发现转换后的模型精度评测的时候会出现两次评测精度不一致, 模型转换是从caffe转换成量化后的onnx模型,中间会有几个临时模型,分别为original_onnx, 这个是直接转换的模型,一个optimized_onnx,是对原onnx模型进行结构优化/整合后的模型,另一个为quantized_onnx模型,既量化后的模型,我们发现对量化后对模型进行精度评测的时候,多次评测的结果不同,而且是同样的转换代码的情况下,同样的评测图片集,最终评测结果不一致,在以百分比现实的精度结果的小数点后一两位就开始有差别,即误差会达到千分之一以上。

首先说明下这个问题排查的重要性,千分之一,看似不多,实则在深度学习领域,不算少了,能通过对模型对一番更改,某些场景下提升千分之几的精度,已经可以发论文了,不过这不是重点,重点是我们想通过定期精度评测来评估转换代码对精度对影响,既然是评估影响,那么就要排除干扰,显然对统一模型同样数据集进行评测后对精度不一致是一种干扰项,我们必须找到这种不一致的原因,以排除其影响。

1.精度问题首先在一个评测脚本里被发现,而这个脚本会同时执行多个模型的评测任务,自然想到执行单个的模型任务排除干扰,也降低复现开销,由于工程里已经有另外一套单个模型的评测脚本,自然就直接用了那个脚本进行评测,发现用那个单个脚本多次评测并未出现精度不一致的问题。

2. 考虑到评测是对一系列批次的图片进行推理,然后对每批次对图片计算结果的吻合度,想到是不是评测集(我们用对imagenet,5万张)对加载顺序不一致导致对,经过排查,我们的评测集图片始终是相同的,而且评测的顺序也是保证一致的。 

3. 基于2,如果评测结果有异常,那么对同一张图片推理的结果也应该有异常,于是又实验了对同一张图,推理若干次,几万次吧,因为最初观察到对每次评测都会复现精度不一致,而每次评测图片数量5万,那么推理5万是能达到这个量级的。结果没有复现。

4. 那会不会是某些图片造成的呢。我们随机选取了一些图片重复3的实验,没有复现。

5. 综上,目前有问题的是多模型评测的时候出现问题,而单模型评测没复现,考虑到这两者的差异,因为多模型评测脚本是顺序执行,不存在模型之间的干扰,而二者有一个区别是,单模型评测是基于已经转换后的模型进行评测,而多模型评测脚本每次评测的时候会从caffe模型转换开始,转换然后评测,那么往前回溯一下,会不会是转换过程引入的不一致。经过对比生成的几个模型的md5值,发现确实转换出的模型就有差异,浮点模型没有差异,但是定点模型有差异,那么精度不一致的问题很可能出现在模型定点化的过程中,而这个过程中最有可能是校准过程。

6. 要分析校准过程中的问题,先要弄清校准过程。由于模型在量化过程中数值分布会发生变化,浮点模型中对分布范围很随意,从前一个节点到后一个节点对传递过程中都基本能保持仍在合法对浮点范围内,不会溢出或者截断,但是量化模型,比如INT8的量化模型,所有数值范围都是-128~127,很容易在某些节点的运算后发生数值溢出和截断,而这种行为是对模型执行精度有很大影响,因而我们希望通过校准过程,在那些能改变输入数值分布的节点前(比如conv) 插入校准节点以对该节点的输入数据分布进行统计,根据统计结果对原节点的+bias和*scale参数做修改,或者另外添加参数(可能需要修改节点实现),如此在量化模型执行过程中,通过该节点的计算将对数据分布进行改变,以期不显著降低精度。校准过程中会通过校准图片(最好和后续推理和评测同分布的图片)进行推理,在推理的过程中,校准图片会更新自身参数,比如最简单的是max校准方法,统计出流经该节点的输入数值的最大值。
7. 基于6,后面的过程就比较清晰了,把所有节点的输出dump出来,然后比对,看是从哪个节点开始计算结果不一致。在比对前,我们怀疑过,某个节点的实现造成(比较有可能的是校准和卷积,求和节点)(这种计算问题浮点数并行计算然后求和,由于浮点值的表示范围有限,当求和顺序不一致会导致结果不一致(因为中间结果存在溢出和精度截断可能)),c++到python的浮点数传递/转换可能有问题(不清楚具体传递和转换机制,只是怀疑正好遇到了某临界值,或者c++的值传递过程中遭遇溢出将随机值填入尾部等等),为了排除后者,我们直接比对c++部分的节点输出值的二进制表示,发现是某一个conv的节点起计算输出的值会有差异,而之后的节点会将差异传递最终导致结果的不一致。

8. 再分析该conv节点,将该conv节点的input以及其他参数属性都dump出来观察,发现出问题的conv节点的输入和属性都是一样的,但是输出不一样,并且输出的值的差异会按一定概率分布在几个值上,差异并不是完全随机的。

9. 仔细查看该conv节点的计算实现,是onnxruntime的实现,onnxruntime/onnxruntime/core/providers/cuda/nn/conv.cc,将其中影响计算的一些参数dump并比对,最终发现是每次执行的时候gpu卡的卷积算法选择不同导致的。每当节点的输入shape或者weight的shape改变,conv都会重新执行

cudnnFindConvolutionForwardAlgorithmEx搜寻高效的卷积算法,具体说明参见https://docs.nvidia.com/deeplearning/sdk/cudnn-api/index.html#cudnnFindConvolutionForwardAlgorithmEx,可搜索的卷积计算算法类型有https://docs.nvidia.com/deeplearning/sdk/cudnn-api/index.html#cudnnConvolutionFwdAlgo_t其中几种会导致计算结果的不一致,具体原因和gpu架构设计有关,工作内容的原因,暂没有继续深究了。所以要解决精度不一致的问题,有两种办法,一种是将算法选择限定在保证一致性的算法范围内,另一种直接屏蔽算法选择,用默认的

CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_GEMM算法,显然这两种都会导致推理评测性能的损失,不过经过测试,性能损失在5%以内,可以接受。若要追求高性能,可以在精度评测需要保证一致性的时候将算法选择限制住,在推理的时候将算法选择打开,不过我们的应用场景内没有这种需求,所以就粗暴的限制了算法的选择。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章