Deep Learning Compiler 之自我理解

前序


接触DL算起来也有几年了,但未真正深入。因工作关系,主要接触的是DL的inference架构以及ASIC的Soc部分,比较琐碎。后认为DL compiler编译器在整个DL的业务流程中,处于承上启下的地位,虽读了些相关的论文以及代码,但依然缺少系统性。

近一年前接触MLIR,当时只是粗略了解,以及在nGraph上玩了玩MLIR,仅此而已。后发现公司的DL编译器有融合MLIR的计划,MLIR有一统江湖的趋势,又花了些时间看MLIR在实现一个具体compiler时的作用。

就想着花些时间做个小总结,写给自己看的。不定期更新,有兴趣了就来。


编译器狂想


完全是个编译器门外汉,抛开AST、SSA、三地址码等专业术语,就以外行的眼光来朴素的看看什么是编译器。

编译器的产生是将编程语言转换为机器码的天然需求;而编程语言则是对自然语言的逻辑抽象。将想法或设计思路以文字的形式表达出来,然后由程序员将之转换为编程语言,接着编译器将其转换为机器码。

所以站在程序员的角度看,编译器就是将由编程语言表达式在不改变内在逻辑的前提下转换为机器码表达式,就是个convert的过程。在各种转换过程中,就涉及到计算机科学的方方面面。而编译原理和计算机体系架构在编译器的实现中有具体的体现。

而llvm的横空出世,带来了除GCC之外另一个阵营。后来者没有历史包袱,可以采用最新的技术和全新的架构,带来了颠覆性的编译器实现思路。llvm所提出的frontend、IR,、backend结构,可以极大的利用已有的backend,只需实现自己的frontend即可。可以轻松的自己DIY一个自己的编译器。而这种可扩展性影响力也波及到了DL编译器领域。


DL编译器


Deep Learning的兴起,不仅带来了各种DL的framework,可以说每个大厂都有自己的一套轮子;还带了各种新处理器,既有在原CPU中添加计算加速器这种新瓶装老酒,也有GPU这种老瓶装老酒,更有专为DL而生的各种ASIC。

百花齐放带来DL繁荣的同时,也带来了互通的问题。同时随着DL的发展,各种graph结构和算子层出不穷,需要能否在各种后端运行,带来了所谓的XX爆炸现象。
在这里插入图片描述

TASO: Optimizing Deep Learning with Automatic Generation of Graph Substitutions

依靠优化工程师去重复造轮子有着巨大的工作量,且优化工程师也是稀缺资源。

人类的进步是因为会制造工具、使用工具,于是DL编译器顺其自然的登场。要解决的问题是将以graph表示的DNN最终转化为各种硬件后端的runtime。

The-Deep-Learning-Compiler–A-Comprehensive-Survey-v4这篇综述对主流的DL编译器都有覆盖,且各自的技术重点也有提及。主流的DL framework一般都有自己的编译器。
在这里插入图片描述
而下图则是对各家编译器采用技术的overview,可见也明显的分为frontend和backend。一般将与硬件无关的转换和优化放在frontend,而将与硬件有关的转换和优化放在各个硬件平台的backend中。
在这里插入图片描述
以OpenVino为例,有一个统一的frontend,针对CPU、GPU、VPU等又有各自的plugin(backend)。

但是frontend/backend和hardware-independent/hardware-dependent 是否就是一一对应的,这就难说了。

深度学习编译技术的现状和未来可以对深度学习编译器的地位有个全面的了解。


什么是DL编译器


回归本源,DL编译器就是个转换工具,不仅功能性要完备,更重要的是性能上和handcraft要有可比性。好像如果DL编译器的性能达到handcraft的85%(不是准确数字,不深究)以上就算个不错的工具了。

抛开优化不谈,只说编译器的基本功能,则要先看看DNN是怎么表示的的。基本上来说,DNN可视为计算图(computer graph)或数据流图(data flow),把一个很大的计算式子分layer或stage逐步计算。

在这里插入图片描述

一般的DNN编译过程如下图所示:
在这里插入图片描述

An In-depth Comparison of Compilers for Deep Neural Networks on Hardware

但DNN有其特殊之处,以常见的convolution为例,在计算时将它视为一个整体conv还是当作矩阵乘或向量乘对待?这就和backend有关了,对于CPU,没有conv加速器,只能将conv转为矩阵乘等,在运行时调用对应的kernel;而对于ASIC,一般都有conv加速器,需保持conv的整体性,将conv整体喂给加速器。

这样会带来一个问题,backend有时会需要比较原始的graph结构,但是这些information可能会在frontend阶段被丢失或优化掉。在选定后端之后,需要选择一个合适的前端来满足特定的要求。

实践出真知,Pytorch转ONNX详解 中描述的ONNX和Caffe的operation颗粒度差异,会影响后端可优化手段的多少。如果后端是ASIC,可见Caffe是更优的选择。

其中最重要的区别就是op的粒度。举个例子,如果对Bert的Attention层做转换,ONNX会把它变成MatMul,Scale,SoftMax的组合,而Caffe可能会直接生成一个叫做Multi-Head Attention的层。


DL编译器的融合


各家编译器受llvm的影响,通过引入IR,最大程度的利用已有的各种转换方法和优化方法。
在这里插入图片描述

MLIR:Multi-Level Intermediate Representation Compiler Infrastructure

其实llvm中的概念在DL编译器中很常见。比如OpenVino的演进就挺有代表性,之初没有pass的概念,一条路走到底;后期做代码重构,引入了frontend,backend以及pass,清晰了流程和架构。

而公司为一款ASIC做的编译器更是将pass概念发展到极致,把编译转换的各个步骤都抽象为pass,然后由xml脚本来定义pass的执行次序。

虽然这两款编译器没有明确提出IR的概念,但是在graph的转换过程中,仍会定义自己的graph表示方式,然后基于此presentation做优化并向硬件的运行时抽象转换。

而公司除了OpenVino之外,还有nGraph和PlaidML,主要都是面向edge inference的,整合是必然的,最终都统一在OpenVino下。nGraph成了OpenVino的前端表示,并利用nGraph已有的一些优化pass;而PlaidML则成为了一个Plugin,专门面向CPU/GPU后端。

编译器相关之间借鉴和参考,因门槛太高而举步维艰。Google看不下了,提出了MLIR(Multi-Level Intermediate Representation),呼吁大家使用MLIR做轮子,别再自己造了;且MLIR这个轮子可以从这个大厂走到那个大厂,可同行无阻。

MLIR的最厉害之处在于多种IR可以在MLIR的框架下共存;high -level IR和low-level IR可以在同一个IR文件中体现。

MLIR的文字游戏玩得好,将DL编译器常见的parten match, rewrite, Operation definition等工作由DSL描述,然后由相关工具自动生成C++语言。从而可以将工程师从琐碎的体力劳动中解放出来,可以专注graph本身的优化。

MLIR中的语句很自由,可以只declare而不define;即可以指定某OP是legal的,但可能在某个阶段其根本就没有对应的implemention。就和在代码中引用lib库中的函数或symbol类似,至于库中是否有对应的实现,只有在runtime时才会知晓,报错或正常通过。

MLIR只是一个compiler infrastructure,出发点就是开放,让更多的人使用。所以架构上的可扩展性和灵活性是毋庸置疑的。至于MLIR能统一多大的江湖,再看。

MLIR所有的设计idea都在论文MLIR: A Compiler Infrastructure for the End of Moore’s Law中。

MLIR的出现也带了一些讨论和争议,反方我将之归为理想派,认为mlir并没有提出新的算法或重大的优化方法,也只是重复造轮子,并且反而推动的轮子数量的增加;而正方我认为主要来自产业界,认为mlir是为轮子制定了生产标准,在同一个标准下生产轮子,有利于轮子的融合和相互使用。


DL编译器的优化方法


不同的阶段有不同有不同的优化方法。前端的优化方法一般都比较经典,比如CSE、Constant folding等,属于经典CS范畴。本章主要聊聊后端的优化和实现。

现今AI芯片多种多样,一文吃透AI芯片技术路线,清华尹首一教授演讲全文:GTIC2020中对此作了很好的总结和展望。

深度神经网络处理器,从计算架构的角度可分成四种不同的类型:(1)指令集处理器(2)数据流处理器(3)存内计算处理器(4)可重构处理器

因后端硬件的千姿百态,后端的优化方法也是千差万别,一般由Auto scheduling(Polyhedral) 和Manual scheduling(Halide)占主导地位。两者都追求最大的普遍性,以灵活的架构尽可能覆盖多种硬件。

MLIR自带Polyhedral的Dialect。但好像用得比较多的还是cost model + searching space。公司针对ASIC的编译器就是采用的后者,但效果不理想。

而TVM的ML-Based Cost Model则另辟蹊径,创新性的将ML引入了了cost model。抛弃了繁琐的Parameterization,以统计方法解决cost model问题,提高了适用性和稳定性。参见TVM: An Automated End-to-End Optimizing Compiler for Deep Learning

另外一个问题是怎么创建searching space,即Network/Neural Architecture Search (NAS)。Efficient Processing of Deep Neural Networks:from Algorithms to Hardware Architectures中相关部分对NAS的方法和技巧有很好的总结。其中也对Manual Network Design给出了建议。

  • 对于large filter,或者使用separable filter替代,或者用smaller filter替换;
  • 使用1x1 filter减少channels数;
  • 使用group conv或者depthwise conv减少channels数;
  • 利用global pooling去减少FC的input的长/宽的size;

至于这些policy后面的原因,则需自己细细品味

TASO: Optimizing Deep Learning Computation with Automatic Generation of Graph Substitutions 则给出了如何得到更优的等效网络结构的具体实现。

To improve the runtime performance of a computation graph, the most common form of optimization is graph substitutions that replace a subgraph matching a specific pattern with a functionally equivalent subgraph with improved performance

例如如下的替换:
在这里插入图片描述


Fuse和Tile


前端的Operation fuse是常见的优化手段,目的是将可关联的Operation一次处理完,减少op的调度,以及减少数据的换出换入,毕竟Data movement是很贵的。但是否Fuse得越多越好,可能也未必,还要看具体的硬件情况。如果后端的并发计算能力很强,那么Fuse的Policy的就不能破坏网络架构原有的并发性。

后端常用的Tile可以说是经典的内存交换(Swapping)功能的再利用,再扯远一点,因片内scratchpad的大小限制,DNN的Meta data则一般采用内存覆盖(Overlay)技术复用运行空间。Tiling也是使能并行化的一个手段,将workload分布在尽可能多的PE上,可充分发布硬件的潜力。

Memory的规划

正因为Data movement很贵,一个优化的方向就是最大化Computing和数据搬移的并行,即memory latency hiding,避免出现计算单元等待数据ready的情况。而Tensor data的Inplace优化则是常见的手段,优化后能够减少数据的换出换入,性能收益是很明显的。

至于如何在受限的Ram中调度DNN tensor的Ram 分配,Parallel scheduling of DAGs under memory constraints 给出了一个Idea。公司的编译器就是利用此技术来避免多并发情况下,Runtime所需Ram超出其阀值的情况。

未完,有兴趣了再更新。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值