一致性问题pipeline: 一致性问题梳理
截止到目前,工作中遇到至少5次的一致性比较任务。其中4次是自己亲身参与的,有1次则是旁观者的身份。本篇文章是对这一典型任务的梳理。
一致性比较的问题来源
可以将这5次一致性比较问题的来源划分为4大类:编程语言gap(x2)、框架gap(x1)、空间gap(x1)、时间gap(x1)。
编程语言gap:python开发方便,c++性能较高,对于算法工程师来讲,这两者是日常工作中使用的语言。两次编程语言gap导致的一致性问题,恰好集齐了python库向c++库对齐和c++库向python库对齐两种模式。编程语言gap1是c++库先存在,并且已经成为公司的主流部署库很长一段时间。然而隔壁组由于定制项目较多,对快速开发、修复bug从而相应客户的重视程度高于对库的运行性能好坏。(事实上,这种who cares的定制现象在算法领域是一个较普遍的现象)。因此隔壁组希望开发一个对等的pyhton-engine库。编程语言gap2是自己亲身经历的一个任务,产品侧同事希望能够在模型训练完毕后,快速的提供一个可运行的服务版本,并且能够有较完善的效果可视化功能。以检测任务-yolov11为例,如果要写一个基于python语言的推理脚本,仅需from ultralytics import YOLO即可。即使有附加的后处理业务逻辑代码或者可视化效果需求,python语言也能够较快的开发出来,这样可以快速的让对方看到效果便于测试。但产品同事最终希望在不久的将来高性能的c++版本也能够提供,并且保持与python版本完全的一致性,无感替换。
框架gap: 目前随着开源的兴起,国内以商汤mm系列、百度paddle系列为代表的开源社区,几乎囊括了所有经典的算法。与之对应的,所在的公司也有自己的自研训练库,有的时候需要在开源框架中吸收代码到自研训练库中。这里称之为框架gap。
空间gap:算法的开发者与算法的使用者不是同一拨人。就会出现典型的开发侧的指标很好,但产品侧反映效果一般。
时间gap: 自研算法推理库当前测试结果无法与一年前推理结果一致,且有较大的指标下降。这一般是由于某次的代码merge测试不够充分导致的。
不同一致性比较场景的解决经验
编程语言gap1场景由于仅是一个旁观者的角度,细节不是很了解,在此不做阐述。针对编程语言gap2,其痛点在于c++版本的推理库本身流程和逻辑不复杂,但c++语言导致该事物的复杂性上升,具体体现为如果直接由该库向yolov11库对齐,c++代码的添加、改动和调试都会较困难;另外一方面,以python语言编写的yolov11库本身语言不复杂,但由于pip的安装方式,以及所需要的推理代码仅仅是yolov11库的一部分,导致代码的调式方式复杂和信噪比较低。总结下来,就是在一个复杂的事物上向另外一个复杂事务靠近。结果就可能掉在一个“毛线团”里面,在这个毛线团里面,当无法对齐的时候,会怀疑一切:c++的opencv和python的opencv会引入差异?有什么超参没注意到?c++的内存分配是否有问题导致某些内存中的数据被异常改动、覆盖。最后的解决方案是:不要同时对付两个复杂的事物。具体说来,首先,用python语言写一个仅基于onnx库的裸推理脚本(这里的裸是指不适用yolov11库),来尝试向c++的推理结果对齐,此时python的快速开发性能被利用起来,较快就对齐上c++的推理结果。这说明c++的opencv和python的opencv没什么差异,且c++的代码无内存相关的缺陷。接下来,就是用该裸的python推理脚本去对齐yolov11推理,此时python的易于开发、更新和调试的特性依然存在,同样较快的找到了超参以及一些具体的操作实现差异。最后,c++对应的代码同样的修复掉这些超参和代码逻辑操作差异,就可以做到结果小数点后级别的一致性对齐。总结起来就是,1. 不要同时处理两个复杂的事物;2. 将一个复杂的任务拆解为3个简单的任务;3. 每一次都是易于开发、调试的对象向不易开发、调试的对象对齐。
框架gap场景与编程语言gap2场景其本质是一样的:试图从一个复杂的事物向另外一个复杂的事物对齐。自研库的复杂性在于其需要考虑到“矩形框”–“旋转矩形框”–“任意四边形”、“agnostic_nms”—"specific_nms"等等兼容场景。尝试“硬刚”的时候,一旦结果不符合预期,同样的会有众多的因素值得怀疑:场景兼容问题、代码逻辑问题、训练的随机性问题、多卡分布式问题等。其较好的解决思路,仍旧是先通过python实现一个与自研库无关、但是和框架的训练结果能够对齐的版本。然后再反过来,该版本向自研库对齐。最后找到差异,更新自研库。
空间gap场景的大致原因在于硬件/系统环境原因和接口/配置参数原因。针对硬件/系统环境原因建议通过镜像的方式来交付;而接口/配置参数则通过openapi和公共规范的文档的方式来避免。
时间gap场景其本质可以归结为软件工程领域中的“回归测试”的缺失。该问题的避免,首先一定要有git-tag这样的代码版本管理机制在,这样便于找到首个出现异常的tag版本,缩小代码变动范围。其次就是快速过有问题测试集,筛选出问题的图片。最后则是取一张异常测试图片,排查出未经过回归测试代码的位置。时间gap的问题,当遇到的时候会给人比较大的心理压力,一方面年代久远另外一方面感觉无从查起。然而看似比较复杂,但其实最终找到的原因大概率是一个不经意的代码改动。通过这三板斧,大概率能找到原因,但可能更多的是需要一个强大的心态以及需要投入一点精力和耐心。
比较好的一致性问题pipeline
一致性问题可以划分为四部分。一致性问题的避免、一致性问题的解决、一致性问题的验证和一致性问题的善后。
如果要想避免一致性问题,首先是“结对编程”,体现在代码的每次merge要有第三方的review;每个新的feat(尽量)需要有配套的单元测试;代码版本的git-tags管理必不可少方便回溯;文档尤其是对外接口文档要严谨;代码交付要通过镜像交付。
一致性问题的解决,如果感觉陷入“毛线团”困境,可能对问题进行拆解、降维:化难为多易、以易对难。再有就是看似很大的不一致性,每次仅需要抓住一个case,极有可能,大的不一致性只是由几个典型的case导致的(有点类似于解决编译器爆出来的程序错误,可能一下子爆出100个error,但真正的解决仅仅需要2~3轮,每次解决遇到的第一个error即可)。
一致性问题的验证,对于一致性问题一个必不可少的操作是在代码的合适位置添加“桩代码”,适时的保存中间结果。原本这一操作时排查问题的手段,但经验是它大概率是一个验证手段。
一致性问题的善后。一致性问题解决后,记得善后。值得存档的以文档的形式存档,值得添加单元测试用例的以测试用例的方式添加,防止再犯。
rethink
找好合适的切入角度,原本一周甚至数周的问题,可能1~2天就能解决。