传统编译器和DL编译器的调研和理解

文章目录

  • Part One : 传统编译器
    • 1.1 前端
    • 1.2 中端
      • 常见的优化
    • 1.3 后端
      • 指令的选择
      • 寄存器分配
      • 指令重排
    • 1.4 总结
  • Part Two:深度学习编译器
    • 2.1 为什么需要 DL Compiler
    • 2.2 DL Compiler 的通用架构设计
      • 前端
      • 后端
    • 2.3 主流深度学习编译框架对比
    • 2.4 传统编译器 vs DL 编译器
    • 2.5 深度学习相关研究方向
    • 2.6 我的理解

Part One : 传统编译器

编译器就是一个将编程语言所编写的程序翻译成另一种目标语言的程序。传统编译器的执行流程如下所示

传统编译器和DL编译器的调研和理解_第1张图片

编译器的前端技术分为词法分析,语法分析,语义分析三个部分,后端部分从生成中间代码,到各种优化,到最终生成目标代码的过程,有时又会将中间代码和优化部分称之为中端。

下文将从前端,中端和后端三个角度来阐述。

1.1 前端

传统编译器和DL编译器的调研和理解_第2张图片
词法分析器 scanner 以源代码作为输入,将源代码转换成 token stream,然后传递给 parser 进行处理,parser 按照语法规则,对 token 进行处理,然后生成一棵抽象语法树。在抽象语法树的基础上,进行类型检查,比如类型绑定,类型推导,变量消解等语义相关操作。我们可以直观的看下这个过程。比如对下面这段简单的代码片段进行词法和语法解析

int main(int argc, char **argv)
{
    printf("Hello, World!\n");
    return 0;
}

生成的tokens如下所示,每一个token都会有其编号、所在文件中的位置、内容以及类型信息,经过语法分析器处理后生成抽象语法树

传统编译器和DL编译器的调研和理解_第3张图片传统编译器和DL编译器的调研和理解_第4张图片

抽象语法树中,整个源代码片段被组织成了一棵树的形式,源代码中的每一行语句都对应了树中的一个有实际含义的节点。

以上这两个截图,出自我之前基于 antlr4 编写的一个 cbc-cpp 编译器前端,语法与 c 语言类似,仓库地址为 https://github.com/small-cat/cbc-cpp。最近在 csdn 准备写一个关于 antlr4 的专栏,专门介绍一下 antlr4 这个强大的词法语法生成器的使用以及上下文无关文法相关的内容,希望对编译器前端感兴趣的小伙伴能带来一点帮助。专栏地址

1.2 中端

编译器经过前端部分处理之后,生成抽象语法树,然后将抽象语法树转换成中间表示(IR, intermediate representation)。从抽象层次上,可以将 IR 归结为 HIR,MIR 和 LIR 这三类。

抽象语法树可以算作一种 HIR,在这个层次上可以做一些高层次的优化,比如常数折叠,内联等。

MIR 是独立于源语言和硬件架构的,所作的优化都是机器无关的优化工作,常见的形式有三地址代码(three address code, TAC)的形式。TAC的特点是,最多有三个地址(也就是变量),其中赋值符号的左边是用来写入的,而右边最多可以有两个地址和一个操作符,用于读取数据并计算。

x = y op z
x = uop y
x = y
goto I
if x goto L
if x op y goto L

比如

do {
	i = i + 1;
	a[i]++;
} while (a[i] < v)

TAC 的形式为

L: i = i + 1
t1 = a[i]
t1 = t1 + 1
if t1 < v goto L

在 TAC 基础上,在三地址代码上再加一些限制,就能得到另一种重要的代码,即静态单赋值代码(Static Single Assignment, SSA),在静态单赋值代码中,一个变量只能被赋值一次,来看个例子。

y = x1 + x2 + x3 + x4 的普通三地址代码如下:

y = x1 + x2;
y = y + x3;
y = y + x4;

其中,y被赋值了三次,如果写成SSA的形式,就只能写成下面的样子:

t1 = x1 + x2;
t2 = t1 + x3;
y  = t2 + x4; 

明确了 use-define 的关系,每一个变量只会定义一次,可以多次使用,这种特点使得基于SSA更容易做数据流分析,而数据流分析又是很多代码优化技术的基础,所以,几乎所有语言的编译器、解释器或虚拟机中都使用了SSA,因为有利于做代码优化。

而基于 MIR 所作的优化方法很多,这里只是介绍几个跟我们后面理解 AI 编译器有关的几个优化方法,提供一点思路。

常见的优化

思路1: 把常量提前计算出来

比如表达式 x = 2 * 3,就可以提前将表达式的值计算出来,优化成 x = 6。这种优化方法就叫做常量折叠(constant folding)。

对于 x 这个变量,已经知道了它的值就是 6,在后面表达式计算中如果使用到了 x,就可以直接将 x 的值替换成 6,这种优化方式叫做常量传播(constant propagation)。而替换 x 后,可能又会找到新的常量折叠和常量传播的优化机会。

思路2: 用低代价的方法做计算

比如 x = x + 0,操作前后 x 没有任何变化,这行代码可以直接删掉。又比如 x = x * 0,可以简化成 x = 0,这种优化方法就叫做代数简化(algebra simplification)。对于有些 cpu 来说,乘法运算改成移位运算会更快,比如 x * 2 优化成 x << 1x * 9 优化成 x << 3 + x,这种就叫做强度消弱(strength reduction)

思路3: 消除重复的计算

x = a + b
y = x
z = 2 * y

上面代码中,z 中的表达式 y 可以直接替换成 x,因为y的值就等于x。这个时候,可能x的值已经在寄存器中,所以直接采用x,运算速度会更快。这种优化叫做拷贝传播(Copy Propagation)。

值编号(Value Numbering)也能减少重复计算。值编号是把相同的值,在系统里给一个相同的编号,并且只计算一次即可。比如

w = 3
x = 3
y = x + 4
z = w + 4

w 和 x 的值相同,因此他们的编号相同,这又导致 y 和 z 的编号相同,那么加法计算只需要计算一次即可。

还有一种优化方法叫做公共子表达式消除(Common Subexpression Elimination,CSE),也会减少计算次数。下面这两行代码,x和y右边的形式是一样的,如果这两行代码之间,a和b的值没有发生变化(比如采用SSA形式),那么x和y的值一定是一样的。

x = a + b
y = a + b

那我们就可以让y等于x,从而减少了一次对“a+b”的计算,这就是公共子表达式消除。

思路4: 针对循环的优化

第一种:归纳变量优化(Induction Variable Optimization)。

看下面这个循环,其中的变量j是由循环变量派生出来的,这种变量叫做该循环的归纳变量。归纳变量的变化是很有规律的,因此可以尝试做强度折减优化。示例代码中的乘法可以由加法替代。

int j = 0;
for (int i = 1; i < 100; i++) {
    j = 2*i;  //2*i可以替换成j+2
}
return j;

第二种:边界检查消除(Unnecessary Bounds-checking Elimination)。

当引用一个数组成员的时候,通常要检查下标是否越界。在循环里面,如果每次都要检查的话,代价就会相当高(例如做多个数组的向量运算的时候)。如果编译器能够确定,在循环中使用的数组下标(通常是循环变量或者基于循环变量的归纳变量)不会越界,那就可以消除掉边界检查的代码,从而大大提高性能。

第三种:循环展开(Loop Unrolling)。

把循环次数减少,但在每一次循环里,完成原来多次循环的工作量。比如:

for (int i = 0; i< 100; i++){
  sum = sum + i;
}

优化后可以变成:

for (int i = 0; i< 100; i+=5){
  sum = sum + i;
  sum = sum + i + 1;
  sum = sum + i + 2;
  sum = sum + i + 3;
  sum = sum + i + 4;
}

进一步,循环体内的5条语句就可以优化成1条语句:sum = sum + i*5 + 10;

减少循环次数,本身就能减少循环条件的执行次数。同时,它还会增加一个基本块中的指令数量,从而为指令排序的优化算法创造机会。

第四种:循环向量化(Loop Vectorization)。

在循环展开的基础上,我们有机会把多次计算优化成一个向量计算。比如,如果要循环16万次,对一个包含了16万个整数的数组做汇总,就可以变成循环1万次,每次用向量化的指令计算16个整数。

第五种:重组(Reassociation)。

在循环结构中,使用代数简化和重组,能获得更大的收益。比如,如下对数组的循环操作,其中数组 a[i,j] 的地址是a+i*N+j。但这个运算每次循环就要计算一次,一共要计算 M*N 次。但其实,这个地址表达式的前半截a+i*N不需要每次都在内循环里计算,只要在外循环计算就行了。

for (i = 0; i< M; i++){
  for (j = 0; j

优化后的代码相当于:

for (i = 0; i< M; i++){
  t=a+i*N;
  for (j = 0; j

第六种:循环不变代码外提(Loop-Invariant Code Motion,LICM)。

在循环结构中,如果发现有些代码其实跟循环无关,那就应该提到循环外面去,避免一次次重复计算。

以上这些优化方法,在 AI 编译器的表达式部分,也会经常使用到,尤其是针对循环的优化,因为深度学习模型本身就是计算密集型的,包含大量的张量和循环计算。

1.3 后端

前面说到,按照层次划分,可以将 IR 分为 HIR,MIR 和 LIR,而跟后端相关的自然就是 LIR 了,这部分的优化与目标硬件相关。编译器的后端功能,就是能够针对不同的计算机硬件,生成优化的代码。主要需要考虑以下三点

  • 指令的选择 instruction selection
  • 寄存器的分配 register allocation
  • 指令的重排 instruction reorder

指令的选择

我们在使用 gcc 或者 llvm 编译器时,通常会使用各种优化等级。同一段代码,不使用优化和使用 O1 级别的优化,生成的代码是不相同的,性能也不同。也就是说,对于一个 CPU 来说,完成同样的任务可以采用多种不同的指令集合,而每种方式对应的生成的代码,以及代码的性能都是不相同,这也是为什么需要选择合适的指令的原因。指令选择的方法,通常是在 LIR 的基础上进行树覆盖的算法进行的。

传统编译器和DL编译器的调研和理解_第5张图片

寄存器分配

在 MIR 中做优化时,使用的寄存器是一种抽象意义上的寄存器,有无限多个(因为 MIR 是机器无关的)。但是目标硬件的寄存器数量总是有限的,在 LIR 中做寄存器分配时,就需要解决这个问题,如何最大程度的利用寄存器,且不超过寄存器总数的限制。

在数据流分析中,经常使用到的一种优化分析方法叫做变量活跃度分析(variable liveness)。就是说在程序的某一个点,计算出所有活跃变量和非活跃变量的集合,针对不活跃的变量,就可以复用它们的内存或者寄存器。

寄存器分配算法通常使用的是图染色算法,这种算法常见于 AOT 编译器中,还有一种线性分配算法,一般在解释器或者 JIT 编译器中使用。

指令重排

通用 CPU 中的指令采用的是流水线的执行方式,而一条指令的执行会用到多个功能部件,分成多个阶段,以典型的 RISC 指令为例,执行过程一般分为五个步骤

  • 取值 IF/instruction fetch
  • 译码 ID/instruction decode
  • 执行 EX
  • 访存 ME
  • 写回 WB/write back

在同一时刻,不同的功能单元可以服务于不同的指令。

传统编译器和DL编译器的调研和理解_第6张图片

这样的话,多条指令实质上是并行执行的,从而减少了总的执行时间,这种并行叫做指令级并行。

那么,对指令进行分析,按照他们之间的数据依赖关系,构建一张指令数据依赖图,将图中没有数据依赖关系的指令拿出来,可以并行执行。这些指令结束时,从依赖图中删除,然后再选出没有依赖关系的指令继续并行执行,从而加快执行速度。

1.4 总结

我们分三个部分,从前端,中端和后端三个方面介绍了传统编译器,并介绍了一些编译器中常见的优化方法。同时,介绍了编译器后端部分主要考虑的三个方面。

也就说,如果基于 llvm 增加一个新的后端,就需要重新实现后端指令选择,寄存器分配,指令重排这些后端代码生成和优化的功能。

传统编译器和DL编译器的调研和理解_第7张图片

Part Two:深度学习编译器

2.1 为什么需要 DL Compiler

深度学习编译器以深度学习框架训练出的模型作为输入,对模型进行优化和编译,输出能够在特定硬件比如 CPU,GPU或者加速器上执行的二进制文件或者库。

随着AI的不断发展,涌现了很多AI框架。

传统编译器和DL编译器的调研和理解_第8张图片

相对的国内也有不少优秀的DL框架,比如

  • 华为的 MindSpore
  • 百度的 PaddlePaddle
  • 旷视的 MegEngine

从推理框架角度来看,无论我们选择何种训练框架训练模型,我们最终都是要将训练好的模型部署到实际场景的。现在主流的深度学习训练框架比如 PyTorch/TensorFlow/MxNet/CNTK 等,对 CPU/CUDA 支持得很好,但是针对 Arm CPU/Arm GPU/FPGA/NPU(华为海思)/BPU(地平线)/MLU(寒武纪)等这些设备,手写一个用于推理的框架在所有可能部署的设备上都达到良好的性能并且易于使用是一件非常困难的事。

而现在Deep Learning有这么多不同前端(framework),有这么多不同的后端(hardware),是否能找到一个桥梁更有效实现他们之间的优化和映射呢?

这种情况跟传统编译器最开始遇到的问题非常相似。传统编译器,抽象出了前端,中端和后端的概念,来解决不同语言,不同硬件之间转换的问题,以 llvm 为例

传统编译器和DL编译器的调研和理解_第9张图片

换句话说,这也正是重演了LLVM 出现时的场景:大量不同的编程语言和越来越多的硬件架构之间需要一个桥梁。LLVM 的出现,让不同的前端后端使用统一的 LLVM IR ,如果需要支持新的编程语言或者新的设备平台,只需要开发对应的前端和后端即可。

受此启发,我们可以将 DL 框架训练出来的深度模型看成是各种编程语言,作为深度学习编译器的输入,转换成图IR后,经过图优化和转换,生成针对特定目标硬件/加速器的代码。

2.2 DL Compiler 的通用架构设计

跟传统编译器类似,DL 编译器也分成前端和后端两个维度。DL模型在DL编译器中被转换成多级IR,其中高级IR位于前端,低级IR位于后端。基于高级IR,编译器前端负责硬件无关的转换和优化。基于低级IR,编译器后端负责特定于硬件的优化、代码生成和编译。

传统编译器和DL编译器的调研和理解_第10张图片

前端

前端将 DL model 转换成 Graph IR,即 High-IR,这部分是对计算和控制流的高度抽象,将多种模型都转换成同一种 IR 结构,与目标硬件无关。前端优化也就是计算图优化,是与机器无关的优化,可以分为三个维度

计算图的优化,可以分为三个维度:

  1. node-level

    • Nop Elimination 删除没有操作的指令的节点,也就是冗余代码消除
    • Zero-dim-tensor Elimination 删除输入是 zero-dim tensor 的操作
  2. block-level

    • Algebraic simplification 代数化简,化简后可以做常量折叠,同时可以将某些算子用低代价的算子进行替换
    • Operator fusion
  3. dataflow-level

    • Common Subexpression Elimination 通用表达式消除,避免同样的表达式多次计算
    • Dead Code Elimination
    • Static memory planning
    • Layout transformation 找出计算图中存储张量的最佳数据布局

后端

后端将 HIR 转换成 LIR,并做很多针对目标硬件的优化工作。一方面,它可以直接将高级IR转换为第三方工具链(如LLVM IR),以利用现有的基础设施进行通用优化和代码生成。另一方面,它可以利用DL模型和硬件特性,定制编译过程来更高效地生成代码。

后端优化针对特定硬件,与机器相关。

  • memory allocation and fetch

  • memory latency hiding,原理与传统编译器中指令重排算法相同

  • loop oriented optimization

    • loop fusion
    • sliding Windows
    • tiling
    • loop reordering
    • loop unrolling
  • parallelization 多线程和SIMD

  • auto-tunning 为程序生成一个优化的schedule的搜索空间,使用机器学习算法,在搜索空间中选择最优优化策略,当前有两种,一种是polyhedral 算法,一种是 tvm 中的 ansor ,auto schdule

2.3 主流深度学习编译框架对比

当前主流的DL编译器,有 apache TVM,Intel 的 nGraph,Facebook 的 TC (Tensor Comprehension) 和 Glow,以及 Google 的 XLA,下图引用自 The Deep Learning Compiler: A Comprehensive Survey

传统编译器和DL编译器的调研和理解_第11张图片

传统编译器和DL编译器的调研和理解_第12张图片

相对来说,开源的 TVM 是最早在工业界完成技术验证的,其所支持的功能和社区活跃度都是处于前列的。

2.4 传统编译器 vs DL 编译器

  1. 侧重点不同。

    传统编译器注重于优化寄存器使用和指令集匹配,其优化往往偏向于局部。而深度学习编译器的优化往往需要涉及到全局的改写,包括之前提到的内存,算子融合等。目前深度学习框架的图优化或者高层优化(HLO)部分和传统编译的pass比较匹配,这些优化也会逐渐被标准的pass所替代。

    理解:传统编译器中的优化,在函数内有局部优化和全局优化,而在函数间叫做 inter procedural optimization。在函数内,局部优化,更多的是针对表达式,basic block 内的优化,而全局优化是在 basic block 间的优化,有数据流分析,控制流分析等。这种就跟深度学习编译器很类似,将整个模型看作是一个function的话,在整个function上探索最佳的优化策略。

  2. 自动代码生成上,传统编译器的目标是生成比较优化的通用代码。比如 gcc 中经常使用的 O1,O2,O3 等优化选项,每一种不同的优化级别对应不同的优化选项,经过优化和生成的代码是性能较好的通用代码,而没有专门指定某些优化策略可以达到更好的性能。

    而深度学习编译器的目标是生成接近手写或者更加高效的特定代码(卷积,矩阵乘法等)。相对的,在一些情况下深度学习编译器可以花费更多的时间去寻找这些解决方案。

  3. 优化思路上。传统编译器采用的是启发式算法,从前端,到中端,到后端不停的优化和lower,生成目标代码的过程。而 DL 编译器,再最终生成代码前,会使用 auto tuning 的技术,使用机器学习的思路,再搜索空间内寻找最佳的优化配置进行优化。

2.5 深度学习相关研究方向

  1. 动态模型的支持

    动态模型是指输入形状可能在执行过程中发生变化。特别是在NLP领域,模型可以接受各种形状的输入,这对DL编译器来说是一个挑战,因为数据的形状直到运行时才是已知的。tvm 中采用了nimble的设计思路,引入了新的类型系统,shape function和各种用于优化和代码生成的原语,同时为了支持移植性问题,引入了虚拟机进行处理。

    • arXiv:2019,Relay: A High-Level Compiler for Deep Learning.
    • Glow: Graph Lowering Compiler Techniques for Neural Networks.
    • 2018, Dynamic Control Flow in Large-Scale Machine Learning
    • arXiv:2021,The CoRa Tensor Compiler: Compilation for Ragged Tensors with Minimal Padding
    • arXiv:2021,Nimble: Efficiently Compiling Dynamic Neural Networks for Model Inference
    • DISC: A Dynamic Shape Compiler for Machine Learning Workloads
    • Cortex: A Compiler for Recursive Deep Learning Models
  2. 高级自动调整

    现有的自动调谐技术集中在单个运算符的优化上。然而,局部优化的组合并不能导致全局优化。例如,两个相邻的运算符适用于不同的数据布局,可以一起进行调整,而不需要在两者之间引入额外的内存转换。此外,随着边缘计算的兴起,执行时间不仅是DL编译器的优化目标,在自动调整中也应考虑新的优化目标,如内存占用和能源消耗。而自动调整技术,通过构建优化的schedule搜索空间,利用机器学习算法,在搜索空间内搜索最佳的优化配置,目前主要有两种方案,一种是polyhedral算法,一种是ansor的方式。

    在 TVM 中,最开始的版本使用的是一种基于模板的 Auto TVM 的方案,需要手动编写优化的模板,模板中的 placeholder 表示需要调整的参数,但是这种做法最终的性能与所编写的模板有很大关联,使用的难度比较高。在最新的版本中,实现了 ansor 的方案,自动构建搜索空间,自动搜索最佳配置。

    • A Survey on Compiler Autotuning using Machine Learning
    • David E. Goldberg. 1989. Genetic Algorithms in Search, Optimization and Machine Learning (1st ed.). Addison-Wesley Longman Publishing Co., Inc., USA
    • Samuel Kaufman, Phitchaya Mangpo Phothilimthana, and Mike Burrows. 2019. Learned TPU Cost Model for XLA Tensor Programs. In Proceedings of the Workshop on ML for Systems at NeurIPS 2019. Curran Associates, Vancouver, Canada, 1–6
    • OSDI,2020,Ansor: Generating High-Performance Tensor Programs for Deep Learning
    • A Full-Stack Search Technique for Domain Optimized Deep Learning Accelerators
    • FlexTensor: An Automatic Schedule Exploration and Optimization Framework for Tensor Computation on Heterogeneous System
    • ProTuner: Tuning Programs with Monte Carlo Tree Search
    • A Sparse Iteration Space Transformation Framework for Sparse Tensor Algebra
    • Optimizing the Memory Hierarchy by Compositing Automatic Transformations on Computations and Data
    • COBAYN: Compiler autotuning framework using bayesian networks
    • Automatic Tuning of Compilers Using Machine Learning
    • Design Space Exploration of Compiler Passes: A Co-Exploration Approach for the Embedded Domain
    • 2017, SCOPES, Stencil autotuning with ordinal regression: Extended abstract
    • Autotuning algorithmic choice for input sensitivity, 2015
    • 2018, A collective knowledge workflow for collaborative research into multi-objective autotuning and machine learning techniques
    • Automating compiler-directed autotuning for phased performance behavior
  3. polyhedral model

    将多面体模型和自动调谐技术结合起来,以提高DL编译器的效率,是一个很有前途的研究方向。一方面,自动调谐可以通过重复使用以前的配置来减少多面体JIT编译的开销。另一方面,多面体模型可以被用来进行自动调度,这可以减少自动调谐的搜索空间。在DL编译器中应用多面体模型的另一个挑战是如何支持稀疏张量。

    • Roberto Bagnara, Patricia M Hill, and Enea Zaffanella. 2006. The Parma Polyhedra Library: Toward a complete set of numerical abstractions for the analysis and verification of hardware and software systems.

    • Chun Chen. 2012. Polyhedra scanning revisited. In Proceedings of the 33rd ACM SIGPLAN conference on Programming Language Design and Implementation. ACM, Beijing, China, 499–508

    • Tobias Grosser. 2000. Polyhedral Compilation. https://polyhedral.info. Accessed February 4, 2020

    • Vincent Loechner. 1999. PolyLib: A library for manipulating parameterized polyhedra. https://repo.or.cz/polylib.git/blob_plain/HEAD:/doc/parampoly-doc.ps.gz

    • 2006, Polyhedral code generation in the real world

    • 2015, Tensor-matrix products with a compressed sparse tensor

    • 2015, Loop and Data Transformations for Sparse Matrix Code

    • 2010, isl: An integer set library for the polyhedral model

    • 2013, Polyhedral parallel code generation for CUDA

    • 2008, PLDI, A practical automatic polyhedral parallelizer and locality optimizer

  4. Subgraph partitioning

    支持子图划分的DL编译器可以将计算图划分为若干个子图,并以不同的方式处理这些子图。子图划分为DL编译器提供了更多的研究机会。首先,它为整合图库进行优化提供了可能性。其次,它为异构和并行执行提供了可能。一旦计算图被分割成子图,不同子图的执行就可以分配给不同硬件目标。

    子图划分神经网络的特性优化,神经网络结构是一种层次结构关系,前后layer之间是一种输入输出的数据依赖关系,这也是能够对计算图进行分层划分的一个原因。

    relay 中就对计算图进行了划分,然后对子图进行处理后转化成 tensor expression 进行调度处理。

    • Tvm: an automated end-to-end optimizing compiler for deep learning
    • Relay: a highlevel compiler for deep learning.
  5. Unified optimizations

    尽管现有的 DL 编译器在计算图优化和特定硬件优化方面都采用了类似的设计,但每个编译器都有自己的优势。目前,谷歌 MLIR 是朝着这个方向的一个很有希望的举措。它提供了多级IRs的基础设施,并包含IR规范和工具包,以便在每个级别的IRs之间进行转换。它还提供了灵活的方言,因此每个DL编译器都可以为高层和低层构建自己的方言。通过跨方言的转换。一个DL编译器的优化可以被另一个编译器重新使用。MLIR 可以理解成是一个编译器的编译器,IREE 就是对这个目标的一个尝试,它将 DL 模型转化成统一的 IR,以满足移动和边缘部署的特殊限制。

    Chris Lattner, Mehdi Amini, Uday Bondhugula, Albert Cohen, Andy Davis, Jacques Pienaar, River Riddle, Tatiana Shpeisman, Nicolas Vasilache, and Oleksandr Zinenko. 2020. MLIR: A Compiler Infrastructure for the End of Moore’s Law. arXiv:cs.PL/2002.11054

  6. Privacy protection – 这属于未来研究方向 ,不是研究热点

    在边缘-云系统中,DL 模型通常被分成两部分,分别在边缘设备和云服务上运行,这可以提供更好的响应延迟和减少通信带宽。然而,用户隐私的保护是边缘-云系统需要解决的一个问题之一。攻击者通过截获边缘设备发送到云端的中间结果,然后使用这些中间结果来训练一个可以暴露隐私信息的模型。为了保护边缘云系统的隐私,最新研究提出了一种端到端的框架Shredder,在不改变预训练网络的拓扑结构或权重的情况下,通过在中间结果中加入具有特殊统计特性的噪声,减少通信数据的信息内容,同时保持推理准确性,这样可以降低攻击者任务的准确性。

    然而,困难在于要确定插入噪声的层,这需要大量的人力来确定最佳层。编译器保持着DL模型的丰富信息,如果通过编译器自动指导各层的噪声插入,那将是极为方便的。

    这里可以这么理解,程序分析领域,经常有需要对程序进行加壳和模糊化的处理,其实就是对原有程序进行转换,使得逆向反汇编出来的代码难以阅读,尽量去阻止他人从二进制文件中获取有用的信息。同理,通过编译器在网络层中插入噪音,使得攻击者很难从中间结果中获取到有用的信息,当获取信息的成本大于收益时,做的人自然就少了。

    • Ruiyuan Gao, Ming Dun, Hailong Yang, Zhongzhi Luan, and Depei Qian. 2019. Privacy for Rescue: A New Testimony Why Privacy is Vulnerable In Deep Models
    • 2020, Shredder: Learning noise distributions to protect inference privacy.
    • Seyed Ali Osia, Ali Taheri, Ali Shahin Shamsabadi, Kleomenis Katevas, Hamed Haddadi, and Hamid R Rabiee. 2018. Deep private-feature extraction. IEEE Transactions on Knowledge and Data Engineering 32, 1 (2018), 54–66

2.6 我的理解

我们经常使用的编程语言翻译到目标代码,通常要经过编译,汇编,连接的过程。而深度学习模型翻译到目标代码是怎么来理解呢?

传统编译器和DL编译器的调研和理解_第13张图片

下图是一个典型的卷积神经网络的抽象结构图

传统编译器和DL编译器的调研和理解_第14张图片

卷积,激活,池化,全连接这些操作,都是一个个的张量计算过程,重点就是数据的计算。而卷积神经网络的另一个特征,就是数据之间的层次关系。前一层的计算结果作为下一层的输入,层与层之间存在着一种输入输出的数据依赖关系。

传统编译器和DL编译器的调研和理解_第15张图片

在深度学习框架或者深度学习编译器中,通常会将模型转换成计算图,而计算图也反映了这种数据流的依赖关系。在 tvm 中会将计算图切分成子图,没有数据依赖关系的子图可以并行执行,有数据依赖关系的子图顺序执行。relay 中更进一步,将子图转换成 tensor expression。这一步让人非常愉悦,因为成了表达式,我们前面介绍的传统编译器里面的那么多的对表达式的优化方法,就都可以使用上了。

对 tensor expression 进行各种优化,每一种优化对应的最终生成的目标代码都会有所差异。这里跟之前介绍的指令选择时的道理是类似的。在 auto tuning 中,每一个 tensor expression,都会有很多种不同的优化方式的集合,这些就称之为 schedule,这些构成了一个搜索空间,在这个搜索空间中搜索出一个最佳的优化方案,就确定了最终该 expression 的代码生成,也就是说找到一个 tensor expression + schedule 的组合。

传统编译器和DL编译器的调研和理解_第16张图片

最终,一个 dl 模型翻译成的一个 IR 结果,就是一个 function,有输入和输出,函数体就是一个个表达式的计算。我们可以看下 tvm 生成的一个 IR 表示

def @main(%data: Tensor[(1, 3, 224, 224), float32], %bn_data_gamma: Tensor[(3), float32], %bn_data_beta: Tensor[(3), float32], %bn_data_moving_mean: Tensor[(3), float32], %bn_data_moving_var: Tensor[(3), float32], %conv0_weight: Tensor[(64, 3, 7, 7), float32], %bn0_gamma: Tensor[(64), float32], %bn0_beta: Tensor[(64), float32],...)
  %1 = %0.0;
  %2 = nn.conv2d(%1, %conv0_weight, strides=[2, 2], padding=[3, 3, 3, 3], channels=64, kernel_size=[7, 7]) /* ty=Tensor[(1, 64, 112, 112), float32] */;
  %3 = nn.batch_norm(%2, %bn0_gamma, %bn0_beta, %bn0_moving_mean, %bn0_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 112, 112), float32], Tensor[(64), float32], Tensor[(64), float32]) */;
  %4 = %3.0;
  %5 = nn.relu(%4) /* ty=Tensor[(1, 64, 112, 112), float32] */;
  ...

这个结构,与我们前面介绍的 SSA 非常相似,其实就是生成的一个基于 SSA 的中间表示结果 IR。我们可以看下 llvm ir 的样子,

define i32 @fun1(i32, i32) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  store i32 10, i32* %5, align 4
  %6 = load i32, i32* %3, align 4
  %7 = load i32, i32* %4, align 4
  %8 = add nsw i32 %6, %7
  %9 = load i32, i32* %5, align 4
  %10 = add nsw i32 %8, %9
  ret i32 %10
}

上面这个 llvm ir 片段是对

int fun1(int a, int b){
    int c = 10;
    return a+b+c;
}

这个代码片段编译生成出来的 IR,可以看到 tvm ir 和 llvm ir 非常相似,而 llvm ir 更接近汇编代码,但是 ir 比汇编代码能表达的更多,无论是 tvm ir 还是 llvm ir 都是保留了类型信息的。

总结:对 DL 模型来说,本质上就是一系列的数学计算,通过将这些数学计算抽象成表达式的形式,可以使用很多编译器中的优化算法对这些表达式进行优化,同时,将整个模型看成是一个有输入参数和输出的函数,就可以理解为什么可以使用编译的方式对 DL 模型进行处理了。比如我们在做算法题时,也是将数学问题抽象,用计算机高级编程语言的形式进行表达,说白了就是对数学问题进行抽象后,用计算机编程语言的方式来表示对这个抽象问题的计算过程,得到一个预期的输出结果,而这个计算过程的表达,我们在这里可以理解成两种形式,一种使用高级编程语言,一种使用 DL 模型。

同时,既然是编译器,那么首先需要保证编译后的模型的输入和输出保持原有的期望不变,也就是说,DL 编译器只是将模型翻译成了更加适合在特定硬件上运行的二进制程序,而不会改变模型推理的结果。

你可能感兴趣的:(思考感悟,学习总结,antlr4,编译器,深度学习编译器)