传统编译器与神经网络编译器
传统编译器
以LLVM(low level virtual machine)为例,输入是高级编程语言源码,输出是机器码,由一系列模块化的编译器组件和工具链组成。
LLVM通过模块分为前端,中端(优化)和后端三部分。每当出现新的编程语言,只需要开发相应的前端,将编程语言转换成LLVM的中间表示;类似地,出现新的硬件架构,只需要开发相应的后端,对接上LLVM的中间表示。
模块化的划分,避免了因编程语言和CPU架构的翻新,而引发的编译器适配性问题,大大简化了编译器的开发工作。
神经网络编译器
输入是深度学习训练框架,训练出来的模型定义文件,输出是能够在不同硬件高效执行的代码。
从上至下由四个层级组成:
1. 最上层对接各个深度学习训练框架,训练出来的算法模型(Tensorflow, Caffe, Pytorch, Mxnet等)。
2. 图层级(High-level IR):神经网络的结构可以表示成计算图,图层级的操作,对计算图进行一些和具体硬件和框架无关的操作,比如算子融合,内存分配优化,数据类型和数据维度的推导等。
可通过算子融合的方式,避免中间数据频繁的在寄存器和内存直接来回读写,从而提升整体推理性能。
Nividia通过把conv, bn, relu这三个算子融合成一个算子 fuse- CBR,实现了三倍的推理性能提升。
3. 算子层级(operator level/kernel level)算子层级,主要是张量计算。为了实现这些计算在硬件上高效实现,发挥芯片的性能,通常硬件芯片配有专门优化的算子计算库,如Intel的MKL, Nvidia的CuDNN, TensorRT。这个层级需要支持每个硬件后端的算子实现。
4. 各硬件后端:GPU, ARM CPU, X86 CPU, NPU等。
自深度学习编译器的概念提出以来,各类编译器变层出不穷的出现。
TVM的前世今生
在编译器快速发展的浪潮中,较为突出的便是TVM(Tensor Virtual Machine)。
TVM最早提出是2017年,深度学习系统的编译器堆栈。
第一代TVM的设计,借鉴了借鉴传统编译器框架LLVM的设计思路,设计抽象出中间表示层,不同的模型,只需要开发相应的前端接口,不同的后端,只需要开发相应的后端接口。
TVM全称为Tensor Virtual Machine,属于算子层级,主要用于张量计算,提供独立于硬件的底层计算中间表示,采用各种方式(循环分块,缓存优化等)对相应的计算进行优化。第一代的图层级表示叫NNVM(Neural Network Virtual Machine)。NNVM的设计目标是:将来自不通深度学习框架的计算图,转换为统一的计算图中间表示(IR),对之进行优化。
第一代的静态图存在一定的缺陷:
1. 不能较好支持控制流,如分支跳转,循环等。
2. 不能支持计算图输入形状,取决于输入tensor大小的模型,比如word2vec等。
虽然Tensorflow有如tf.cond.Tf.while_loop的控制接口,在某种程度上解决第一个问题,tf.fold来解决第二个问题,但是这些方式对刚刚接触深度学习框架的人来说门槛过高。
后面出现的动态图,摒弃了传统的计算图先定义,后执行的方式,采用了计算图在运行时定义的模式,这种计算图就称为动态图。
第二代TVM的图计算层,变为Relay VM,Relay和第一代的图计算表示NNVM的最主要区别,Relay IR除了支持dataflow(静态图), 能够更好地解决control flow(动态图)。不仅是一种计算图的中间表示,也支持自动微分。
总结一下,目前TVM的架构是:
1. 最高层级支持主流的深度学习前端框架,包括TensorFlow,MXNet,Pytorch等。
2. Relay IR支持可微分,该层级进行图融合,数据重排等图优化操作。
3. 基于tensor张量化计算图,并根据后端进行硬件原语级优化,autoTVM根据优化目标探索搜索空间,找到最优解。
4. 后端支持ARM、CUDA/Metal/OpenCL、加速器VTA(Versatile Tensor Accelerator)。
Halide
Algorithm部分主要是算法描述和计算的数学表达式。
Halide于2012年提出,主要用于自动优化。其嵌入到C++中,MIT研究人员专门为图像处理设计的一种程序语言。Halide语言易于编写,语法简单,数据结构清晰,自动对代码进行优化,使得程序获得更好的执行效率。
设计的核心思想是把算法和调度分离。这样做的好处,在给定算法的情况下,只需要去调整Schedule 调度选择,不用重写算法实现不同的Schedule。当调整Schedule、探索设计空间时,也不会担心因为重写算法,而导致计算的正确性会发生变化。
Algorithm部分主要是算法描述和计算的数学表达式。
Schedule部分,告诉机器什么时候分配内存,如何计算(分块计算还是顺序计算)——目前已经提供了一些调度策略。
不同调度策略,考虑重复冗余计算和局部性(locality)的权衡。
AutoKernel
深度学习模型能否成功在终端落地应用,满足产品需求,一个关键的指标就是神经网络模型的推理性能。
目前的高性能算子计算库,主要是由高性能计算优化工程师,进行手工开发。然而新的算法/硬件的不断涌现,导致了算子层级的优化开发工作量巨大。同时优化代码的工作,并不是一件简单的事,要求工程师,既要精通计算机体系架构,又要熟悉算子的计算流程。
人才少,需求多,技术门槛高,算子优化自动化是未来的大趋势。而提出AutoKernel的初衷,便是希望能把这个过程自动化,从小处入手,在算子层级的优化,实现优化代码的自动生成。
AutoKernel的输入是算子的计算描述(如Conv、Poll、Fc),输出是经过优化的加速源码。
这一工具的开发,旨在降低优化工作的门槛,不需要有底层汇编的知识门槛,不用手写优化汇编。可通过直接调用开发的工具包,可生成汇编代码。同时还提供了包含CPU、GPU的docker环境,无需部署开发环境,只需使用docker便可。还可通过提供的插件——plugin,可以把自动生成的算子,一键集成到推理框架中——Tengine。
对应地,算子层级的AutoKernel,主要分为三个模块,
1. Op Generator:算子生成器,采用了开源的Hallide。
2. AutoSearch:目前还在开发中,目标是通过机器学习、强化学习常用算法自动搜索出优化策略。
3. AutoKernel Plugin:把生成的自动算子以插件的形式,插入到Tengine中,和人工定制互为补充。
Tengine对象层对接了不同的神经网络模型,图层级的NNIR包含了模型解析、图层优化,算子层级包括高性能计算库(HCL lib)。
AutoKernel Plugin主要分为生成和部署两部分,生成部分用Hallid填写算法描述和调度策略,执行时指定后端类型(基本覆盖目前的主流后端)。
部署部分则封装为Tengine的库,直接调用。
参考链接:
https://www.163.com/dy/article/G583KIQD0511DPVD.html