深度学习推理引擎的又一些思考

深度学习推理引擎的又一些思考

大约在7个月前,我曾写过一篇文章:深度学习推理引擎的一些思考,该文章也引起了一些积极的讨论。于是,我想接着再写一篇有关我自己继续在这个领域工作更多时间后的一些感悟。

前端与使用体验:

如我之前的文章一样,我还是想再将前端这个事情拿来起头。很多推理引擎号称性能很好,但是一旦算子不支持,那么一切都白搭。针对这一点,很多引擎提出了一种机制,就是Custom Op,允许用户来实现他们引擎不支持的算子。但是,根据我能得到的信息,基本上大家遇到这种提示,是不可能去写Custom Op的,我也不认为写Custom Op是一种正确的做法。如我的模型含有Conv3D,你的引擎不支持,你是让我用Custom Op来实现Conv3D吗?这不现实。使用AI引擎的其实大部分是负责应用集成或者算法工程师,而不是引擎开发工程师,所以我是非常不喜这种做法的。提及到这一点是因为我观测太多的引擎这样来做,而且还做得非常鸡肋,如我想跑的是DSP,我就算愿意来配合你写Custom Op了,但是你的Custom Op机制却也告诉我只能支持CPU,那完全失去意义了。那么以TF这样为代表的框架,算子上千,一个一个去支持会带来巨大的负担,那么这时候我们要去解这个问题如何去解?如TVM社区曾提到了一种提案,那就是走类似TF-TensorRT的方案(discuss.tvm.ai/t/rfc-ad),让TVM能优化的算子给TVM,不支持的算子还是TF本身执行。这种解决能解决部分问题,但是我觉得最大的硬伤在于Edge端,Edge端我不可能塞一个TensorFlow进去。所以,我更喜欢另外一种解法,那就是让我们要支持的算子变少,即这篇提出的我们可以去支持HLO / MLIR: discuss.tvm.ai/t/rfc-ml。但是这里面的支持办法也有很多,一种是全依赖TF,全变为HLO后塞给TVM Relay,但是其实天奇提的MultiStage Lowering个人觉得是更好的解法。为什么呢?因为当你拿到一个更低层次的东西后,你总是会丢掉一些信息,而对于我们TVM更期望的是拿到更丰富的一些信息,从而可以更好地做优化,于是我们可以尽量的翻译GraphDef,翻译不了的再走HLO这条路。

接下来想再谈谈前端的下一步,就是计算图。以TVM为例,我们拿到TF / MXNet / PyTorch等模型后,我们会转化为Relay。但是,目前有个很有意思的提案是将Relay转回为ONNX。初看是一件很矛盾的事情,但是其实我觉得他能解决一个非常有意思的业务场景:外部的引擎不开放计算图API。让我们设想一下,我们目前有一个NPU的引擎,它不开放指令集,只开放工具链,而且也不开放类似TensorRT的计算图构建API。那么,我们现在就可以做一件事情,就是我们拿到模型以后通过TVM转为Relay,随后进行子图切分,然后NPU不支持的子图走TVM自身(如CPU),NPU支持的子图我们就存为ONNX,随后交给它来执行。不要认为这个很少见,SNPE就是这样的,而且很多NPU厂商现在也像SNPE这样的玩法,性质越来越恶劣,什么都抱的死死的,搞得异常封闭。

外部引擎:

既然上文谈到了子图切分与外部引擎,就接着这部分再说一下。在我之前的那篇文章就说到过一个场景:“现在我业务部门有一个模型我需要你在GPU跑起来,并且我需要在XX ms 跑完。而很遗憾的是TensorRT不支持这个模型的一些算子,并且在一些层比TVM慢,一些层比TVM快” ,于是TVM + TensorRT就是很自然的一个组合,目前TVM 有提出 BYOC 机制来解决这个场景。说实话,这其实是很多AI引擎没有的“胸怀”,目前很多AI引擎其实走的路线都是走我自己的路,让你们无路可走,宣传起来是各种吊打友商,结果自己在实际业务场景中表现其实也不咋样,只停留在Mobilenet。对于业务场景来说,这其实很多时候是强需求,尤其是有加速器(NPU / DSP等)场景,它们提供的引擎就是真的无法涵盖住业务场景的所有模型算子,我必须要切分子图丢一部分给他们,然后另一部分交给我自己,再强调一句,业务场景模型不仅仅是Mobilenet,等你外部引擎把我的算子支持完,我的项目可能早就结束了,黄花菜都凉了。

性能:

毫无疑问,性能目前依然是去衡量AI引擎的一个硬指标。目前这时候经常出现两派,手写算子派和以TVM为代表的AI Compiler派。有关这一点,每个人有每个人的看法,如果你要限定场景,你手写算子自然可以最大极限的压榨硬件。但是,我的个人看法是自动调优与代码生成是更广阔的未来,而手写算子会是自动调优与代码生成的一项补充。手写算子派的优点很明显,前文已经提到。但是它有什么弱点:一:耗时耗力,需要有非常有经验的工程师精细的去”玩弄“汇编,并且难以涵盖所有的业务场景;第二:如果你做的是第三方引擎(非类似NV / Intel这样专门只做一个端),你需要每个设备后端都这样写。于是,经常看见一些引擎在CPU可以支持这个算子,但是OpenCL就不支持了,或者OpenCL支持了,Vulkan又不支持了。这种情况在TVM这样的引擎很少见,因为对于TVM来说,只要能到TVM IR这层,其实Code Gen要翻译的IR就短短数十条,很容易就每个端实现完了,这和我们之前谈前端支持的思路类似,既然太多了,我们就想办法能不能变少。但是TVM目前的做法其实也有被人抨击,1: AutoTVM你其实依然也要写计算模板(门槛其实也不低),为何要说我手写就更复杂?2: Tuning时间太长了,无法及时反馈。针对第一个问题,我的答案是认为我们应该往前走一步,那就是让我们自动生成优化的代码。目前业界已经有Halide Auto Scheduler / Flex Tensor这样的成果出来。当然,我们在这方面也有我们的一些独特想法,希望后面能与大家具体分享。针对第二个问题,一种解法是建立庞大的Tuning数据库,同时也提高我们的Fallback性能,另外一种解法可能是能建立一个更好的离线Cost Model来应对需要及时反馈,但却并不是要极致性能的地方。

有关性能,我想再多谈一点点。我认为性能绝不仅仅是单独的一个算子的性能,也不仅仅是模型的执行性能。首先,单独的一个算子性能若不能体现在模型上,是没有任何意义的,因为大家看的是整网。这一点最近我深有体会,最近发现类似TVM无法准确的将单算子的性能反馈到整网,一个原因就是单算子Tuning,你的Weight会在Cache中,但是跑整网的时候,其实你的Weight不在Cache中。而为什么要谈到不仅仅是模型的执行性能呢?其实在整个业务场景中,我们不仅仅关注推理性能,还关注整体执行的时间。比如图像从摄像头过来,我们会处理图像,假如AI推理在GPU上执行,我们还需要考虑CPU到GPU的拷贝时间,以及从GPU拷贝回CPU的时间,而这拷贝时间可能比你执行时间还长。那么每一个环节都是性能的组成部分,而如一些移动GPU,其实GPU和CPU是内存共享的,那么我们可以将这一部分Copy进行优化。

异构:

异构是一个很时髦的词,做引擎的基本上都会把这个谈到。支持异构,主流的有以下的一些方式: 1. 支持整个模型在GPU等设备运行中 2. 模型可以进行子图拆分,将一些算子放在CPU,一些算子放在GPU等异构设备。但是在设备端上,目前很少有人这样玩,因为这样做以后整体性能不一定比单独在GPU高,所以很多时候会说这个功能在设备端很鸡肋,最大的适用场景反而变为了GPU不好实现一些复杂的算子,只能CPU来做的地方。但是我认为异构在设备端其实不仅仅是只有这样的适用场景,我目前在实际业务支持中发现了它的另外一种适用场景,但是为了避免吹大话,可以等我做成功了再来反馈。这个业务场景的反馈让我体会到AI引擎最忌讳闭门造车,只有实际业务场景驱动,才能知道有哪些有趣的问题需要去解。

工具:

在传统编译器开发中,我们有调试器,性能分析工具来帮助我们开发。那么在AI引擎开发中是否也有?当然手写算子库这种方式沿用以前传统的工具倒也可以理解,但是对于AI Compiler这一层,只用传统的调试器与性能分析工具给人的感觉总是隔靴挠痒,因为AI Compiler会隔着一层抽象的IR,而不是直接的C++。当然,将IR变成交互型,可以随时看到各种状态变换信息可能可以缓解部分这样的情况,但是如何能开发一个应对这种IR的调试器和性能分析工具我认为是一个非常有意思的工作。

Misc:

现在很多做AI推理引擎的会更多的去触及周边的一些东西,如会建立一个Model Zoo,这一点我其实觉得有不错,但是没有也没关系,因为基本上能叫得上名的训练框架都有Model Zoo。说到训练,最近还有一个点冒出来就是端上训练,即大家看到端上算力越来越强,同时端上训练有很强的隐私保护性,于是有冒出这个想法。关于这一点,我个人认为大家目前都处于一个探索的阶段,我能了解到的渠道大致都是这个反应,我也希望有同学能告诉我这一点已经有哪些成功的应用。

最近还有一些情景出现,就是端上推理与训练框架的结合。如在TVM大会上演示的天猫精灵情况(tvmconf.org/slides/2019),即在训练的时候进行Overflow-Aware Quantization,让我们的计算不再仅仅是INT8 * INT8 + INT8 -> INT32,而是可以用INT16. 正常你当然不能这样搞,但是若有训练框架配合,那么这一点就是可以做到的了,我认为这个方向未来会是很重要的一点,即我为了加速,我希望反过来推,让训练框架帮我做一些事情,而不再仅仅是目前的单向输入(训练框架->推理框架)。

下一次再见。

发布于 05-29

文章被以下专栏收录