diff --git a/chapter_frontend_and_ir/ad.md b/chapter_frontend_and_ir/ad.md index 51271f0..d2b1aa6 100644 --- a/chapter_frontend_and_ir/ad.md +++ b/chapter_frontend_and_ir/ad.md @@ -6,16 +6,16 @@ ### 自动微分的基本概念 自动微分(Automatic -Differentiation,AD)是一种对计算机程序进行高效且准确求导的技术,在上个世纪六七十年代就已经被广泛应用于流体力学、天文学、数学金融等领域([@10.5555/1455489])。时至今日,自动微分的实现及其理论仍然是一个活跃的研究领域。随着近些年深度学习在越来越多的机器学习任务上取得领先成果([@lecun2015deep]),自动微分被广泛的应用于机器学习领域。许多机器学习模型使用的优化算法都需要获取模型的导数,因此自动微分技术成为了一些热门的机器学习框架(例如TensorFlow和PyTorch)的核心特性。 +Differentiation,AD)是一种对计算机程序进行高效且准确求导的技术,在上个世纪六七十年代就已经被广泛应用于流体力学、天文学、数学金融等领域 :cite:`10.5555/1455489`。时至今日,自动微分的实现及其理论仍然是一个活跃的研究领域。随着近些年深度学习在越来越多的机器学习任务上取得领先成果,自动微分被广泛的应用于机器学习领域。许多机器学习模型使用的优化算法都需要获取模型的导数,因此自动微分技术成为了一些热门的机器学习框架(例如TensorFlow和PyTorch)的核心特性。 -常见的计算机程序求导的方法可以归纳为以下四种([@2015Automatic]):手工微分(Manual +常见的计算机程序求导的方法可以归纳为以下四种 :cite:`2015Automatic`:手工微分(Manual Differentiation)、数值微分(Numerical Differentiation)、符号微分(Symbolic Differentiation)和自动微分(Automatic Differentiation)。 (1)手工微分:需手工求解函数导数的表达式,并在程序运行时根据输入的数值直接计算结果。手工微分需根据函数的变化重新推导表达式,工作量大且容易出错。 -(2)数值微分([@2015Numerical]):数值微分通过差分近似方法完成,其本质是根据导数的定义推导而来。 +(2)数值微分 :cite:`2015Numerical`:数值微分通过差分近似方法完成,其本质是根据导数的定义推导而来。 $$f^{'}(x)=\lim_{h \to 0}\frac{f(x+h)-f(x)}{h}$$ @@ -24,12 +24,12 @@ error)。理论上,数值微分中的截断误差与步长$h$有关,$h$越 Error)。舍入误差会随着$h$变小而逐渐增大。当h较大时,截断误差占主导。而当h较小时,舍入误差占主导。 在截断误差和舍入误差的共同作用下,数值微分的精度将会在某一个$h$值处达到最小值,并不会无限的减小。因此,虽然数值微分容易实现,但是存在精度误差问题。 -(3)符号微分([@2003Computer]):利用计算机程序自动地通过如下的数学规则对函数表达式进行递归变换来完成求导。 +(3)符号微分 :cite:`2003Computer`:利用计算机程序自动地通过如下的数学规则对函数表达式进行递归变换来完成求导。 $$\frac{d}{dx}(f(x)+g(x))\rightsquigarrow\frac{d}{dx}f(x)+\frac{d}{dx}g(x)$$ $$\frac{d}{dx}(f(x)g(x))\rightsquigarrow(\frac{d}{dx}f(x))g(x)+f(x)(\frac{d}{dx}g(x))$$ 符号微分常被应用于现代代数系统工具中,例如Mathematica、Maxima和Maple,以及机器学习框架,如Theano。符号微分虽然消除了手工微分硬编码的缺陷。但因为对表达式进行严格的递归变换和展开,不复用产生的变换结果,很容易产生表达式膨胀(expression -swell([@10.5555/60181.60188]))问题。如图:numref:`symbolic_differentiation`所示,用符号微分计算递归表达式$l_{n+1}=4l_n(1-l_n)$,$l_1=x$的导数表达式,其结果随着迭代次数增加快速膨胀。 +swell :cite:`10.5555/60181.60188` )问题。如 :numref:`symbolic_differentiation` 所示,用符号微分计算递归表达式$l_{n+1}=4l_n(1-l_n)$,$l_1=x$的导数表达式,其结果随着迭代次数增加快速膨胀。 ![符号微分的表达式膨胀问题](../img/ch04/符号微分的表达式膨胀问题.png) :width:`800px` @@ -37,7 +37,7 @@ swell([@10.5555/60181.60188]))问题。如图:numref:`symbolic_differentia 并且符号微分需要表达式被定义成闭合式的(closed-form),不能带有或者严格限制控制流的语句表达,使用符号微分会很大程度上地限制了机器学习框架网络的设计与表达。 -(4)自动微分([@2000An]):自动微分的思想是将计算机程序中的运算操作分解为一个有限的基本操作集合,且集合中基本操作的求导规则均为已知,在完成每一个基本操作的求导后,使用链式法则将结果组合得到整体程序的求导结果。自动微分是一种介于数值微分和符号微分之间的求导方法,结合了数值微分和符号微分的思想。相比于数值微分,自动微分可以精确地计算函数的导数;相比符号微分,自动微分将程序分解为基本表达式的组合,仅对基本表达式应用符号微分规则,并复用每一个基本表达式的求导结果,从而避免了符号微分中的表达式膨胀问题。而且自动微分可以处理分支、循环和递归等控制流语句。目前的深度学习框架基本都采用自动微分机制进行求导运算,下面我们将重点介绍自动微分机制以及自动微分的实现。 +(4)自动微分 :cite:`2000An`:自动微分的思想是将计算机程序中的运算操作分解为一个有限的基本操作集合,且集合中基本操作的求导规则均为已知,在完成每一个基本操作的求导后,使用链式法则将结果组合得到整体程序的求导结果。自动微分是一种介于数值微分和符号微分之间的求导方法,结合了数值微分和符号微分的思想。相比于数值微分,自动微分可以精确地计算函数的导数;相比符号微分,自动微分将程序分解为基本表达式的组合,仅对基本表达式应用符号微分规则,并复用每一个基本表达式的求导结果,从而避免了符号微分中的表达式膨胀问题。而且自动微分可以处理分支、循环和递归等控制流语句。目前的深度学习框架基本都采用自动微分机制进行求导运算,下面我们将重点介绍自动微分机制以及自动微分的实现。 ### 前向与反向自动微分 @@ -52,7 +52,7 @@ $$\frac{dy}{dx}=(((\frac{dy}{da}\frac{da}{db})\frac{db}{dc})\frac{dc}{dx})$$ 我们以下面的函数为例介绍两种模式的计算方式,我们希望计算函数在$(x_1, x_2)=(2,5)$处的导数$\frac{\partial y}{\partial x_1}$: $$y=f(x_1,x_2)=ln(x_1)+{x_1}{x_2}-sin(x_2)$$ -该函数对应的计算图如:numref:`example_compute_graph`: +该函数对应的计算图如 :numref:`example_compute_graph`: ![示例计算图](../img/ch04/自动微分-示例计算图.svg) :width:`800px` @@ -64,7 +64,7 @@ $$y=f(x_1,x_2)=ln(x_1)+{x_1}{x_2}-sin(x_2)$$ :width:`800px` :label:`forward_AD` -前向模式的计算过程如图:numref:`forward_AD`所示,左侧是源程序分解后得到的基本操作集合,右侧展示了运用链式法则和已知的求导规则,从上至下计算每一个中间变量${\dot{v}_i}=\frac{\partial v_i}{\partial x_1}$,从而计算出最后的变量${\dot{v}_5}=\frac{\partial y}{\partial x_1}$。 +前向模式的计算过程如 :numref:`forward_AD`所示,左侧是源程序分解后得到的基本操作集合,右侧展示了运用链式法则和已知的求导规则,从上至下计算每一个中间变量${\dot{v}_i}=\frac{\partial v_i}{\partial x_1}$,从而计算出最后的变量${\dot{v}_5}=\frac{\partial y}{\partial x_1}$。 当我们想要对一个函数求导时,我们想要得到的是该函数的任意一个输出对任意一个输入的偏微分的集合。对于一个带有$n$个独立输入$x_i$和$m$个独立输出$y_i$的函数$f:{\mathbf{R}^n}\to \mathbf{R}^m$,该函数的求导结果可以构成如下的雅克比矩阵(Jacobian Matrix): $$\mathbf{J}_{f}= @@ -101,7 +101,7 @@ $$\mathbf{J}_{f}\mathbf{r}= :width:`800px` :label:`backward_AD` -反向模式的计算过程如上图:numref:`backward_AD`所示,左侧是源程序分解后得到的基本操作集合,右侧展示了运用链式法则和已知的求导规则,从$\bar{v}_5=\bar{y}=\frac{\partial y}{\partial y}=1$开始, +反向模式的计算过程如上 :numref:`backward_AD`所示,左侧是源程序分解后得到的基本操作集合,右侧展示了运用链式法则和已知的求导规则,从$\bar{v}_5=\bar{y}=\frac{\partial y}{\partial y}=1$开始, 由下至上地计算每一个中间变量${\bar{v}_i}=\frac{\partial y_j}{\partial v_i}$,从而计算出最后的变量${\bar{x}_1}=\frac{\partial y}{\partial x_1}$和${\bar{x}_2}=\frac{\partial y}{\partial x_2}$。 反向模式每次计算的是函数$f$的某一个输出对任一输入的偏微分,也就是雅克比矩阵的某一行,如下面的向量所示。因此通过运行m次反向模式自动微分,我们就可以得到整个雅克比矩阵。 @@ -125,11 +125,11 @@ $$\mathbf{r}^{T}\mathbf{J}_{f}= 但是反向模式也存在一定的缺陷。在源程序分解为一系列基本操作后,前向模式由于求导顺序与基本操作的执行顺序一致,输入值可以在执行基本操作的过程中同步获得。而在反向模式中,由于求导顺序与源程序的执行顺序是相反的,计算过程需要分为两个阶段,第一个阶段先执行源程序,且将源程序的中间结果保存起来,在第二阶段才把中间结果取出来去计算导数。因此反向模式会有额外的内存消耗。业界也一直在研究反向模式的内存占用优化方法,例如检查点策略(checkpointing strategies)和数据流分析(data-flow -analysis)([@2006The];[@2017Divide])。 +analysis) :cite:`2006The,2017Divide` 。 ### 自动微分的实现 -上一节我们介绍了自动微分的基本概念,可以总结为将程序分解为一系列微分规则已知的基本操作,然后运用链式法则将它们的微分结果组合起来得到程序的微分结果。而在机器学习的应用中,因为输入的数量远远大于输出的数量,所以反向模式的自动微分更受青睐。虽然自动微分的基本思想是明确的,但是具体的实现方法也分为几类([@2015Automatic]),大体可以划分为基本表达式法(Elemental +上一节我们介绍了自动微分的基本概念,可以总结为将程序分解为一系列微分规则已知的基本操作,然后运用链式法则将它们的微分结果组合起来得到程序的微分结果。而在机器学习的应用中,因为输入的数量远远大于输出的数量,所以反向模式的自动微分更受青睐。虽然自动微分的基本思想是明确的,但是具体的实现方法也分为几类 :cite:`2015Automatic` ,大体可以划分为基本表达式法(Elemental Libraries)、操作符重载法(Operator Overloading,OO)和代码变换法(Source Code Transformation,ST)。 @@ -201,7 +201,7 @@ OO对程序的运行跟踪经过了函数调用和控制流,因此实现起来 (3)代码变换法(Source Transformation,ST):提供对编程语言的扩展,分析程序的源码或抽象语法树(AST),将程序自动地分解为一系列可微分的基本操作,而这些基本操作的微分规则已预定义好,最后使用链式法则对基本操作的微分表达式进行组合生成新的程序表达来完成微分。TensorFlow,MindSpore等机器学习框架都采用了该方式。 -不同于OO在编程语言内部操作,ST需要语法分析器(parser)和操作中间表示的工具。除此以外,ST需要定义对函数调用和控制流语句(如循环和条件等)的转换规则。其优势在于对每一个程序,自动微分的转换只做一次,因此不会造成运行时的额外性能损耗。而且,因为整个微分程序在编译时就能获得,编译器可以对微分程序进行进一步的编译优化。但ST实现起来更加复杂,需要扩展语言的预处理器、编译器或解释器,且需要支持更多的数据类型和操作,需要更强的类型检查系统。另外,虽然ST不需要在运行时做自动微分的转换,但是对于反向模式,在反向部分执行时,仍然需要确保前向执行的一部分中间变量可以被获取到,有两种方式可以解决该问题([@van2018Automatic]): +不同于OO在编程语言内部操作,ST需要语法分析器(parser)和操作中间表示的工具。除此以外,ST需要定义对函数调用和控制流语句(如循环和条件等)的转换规则。其优势在于对每一个程序,自动微分的转换只做一次,因此不会造成运行时的额外性能损耗。而且,因为整个微分程序在编译时就能获得,编译器可以对微分程序进行进一步的编译优化。但ST实现起来更加复杂,需要扩展语言的预处理器、编译器或解释器,且需要支持更多的数据类型和操作,需要更强的类型检查系统。另外,虽然ST不需要在运行时做自动微分的转换,但是对于反向模式,在反向部分执行时,仍然需要确保前向执行的一部分中间变量可以被获取到,有两种方式可以解决该问题 :cite:`van2018Automatic` : (1)基于Tape的方式。该方式使用一个全局的"tape"去确保中间变量可以被获取到。原始函数被扩展为在前向部分执行时把中间变量写入到tape中的函数,在程序执行反向部分时会从tape中读取这些中间变量。除了存储中间变量外,OO中的tape还会存储执行的操作类型。然而因为tape是一个在运行时构造的数据结构,所以需要添加一些定制化的编译器优化方法。且为了支持高阶微分,对于tape的读写都需要是可微分的。而大多数基于tape的工具都没有实现对tape的读写操作的微分,因此它们都不支持多次嵌套执行反向模式的自动微分(reverse-over-reverse)。机器学习框架Tangent采用了该方式。 diff --git a/chapter_frontend_and_ir/common_frontend_optimization_pass.md b/chapter_frontend_and_ir/common_frontend_optimization_pass.md index 344cb95..130924b 100644 --- a/chapter_frontend_and_ir/common_frontend_optimization_pass.md +++ b/chapter_frontend_and_ir/common_frontend_optimization_pass.md @@ -19,7 +19,7 @@ 1\. 无用与不可达代码消除 -如图 :numref:`pass_useless_code_elimination`所示。无用代码是指输出结果没有被任何其他代码所使用的代码。不可达代码是指没有有效的控制流路径包含该代码。删除无用或不可达的代码可以使得中间表示更小,提高程序的编译与执行速度。无用与不可达代码一方面有可能来自于程序编写者的编写失误,也有可能是其他编译优化所产生的结果。 +如 :numref:`pass_useless_code_elimination`所示。无用代码是指输出结果没有被任何其他代码所使用的代码。不可达代码是指没有有效的控制流路径包含该代码。删除无用或不可达的代码可以使得中间表示更小,提高程序的编译与执行速度。无用与不可达代码一方面有可能来自于程序编写者的编写失误,也有可能是其他编译优化所产生的结果。 ![无用代码消除](../img/ch04/编译优化-无用代码消除.svg) :width:`600px` @@ -27,9 +27,9 @@ 2\. 常量传播、常量折叠 -常量传播:如图 :numref:`pass_constant_broadcast`所示,如果某些量为已知值的常量,那么可以在编译时刻将使用这些量的地方进行替换。 +常量传播:如 :numref:`pass_constant_broadcast`所示,如果某些量为已知值的常量,那么可以在编译时刻将使用这些量的地方进行替换。 -常量折叠:如图 :numref:`pass_constant_broadcast`所示,多个量进行计算时,如果能够在编译时刻直接计算出其结果,那么变量将由常量替换。 +常量折叠:如 :numref:`pass_constant_broadcast`所示,多个量进行计算时,如果能够在编译时刻直接计算出其结果,那么变量将由常量替换。 ![常量传播与常量折叠](../img/ch04/编译优化-常量传播与常量折叠.svg) :width:`600px` @@ -37,7 +37,7 @@ 3\. 公共子表达式消除 -如图 :numref:`pass_CSE`所示,如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。 +如 :numref:`pass_CSE`所示,如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。 ![公共子表达式消除](../img/ch04/编译优化-公共子表达式消除.svg) :width:`600px` diff --git a/chapter_frontend_and_ir/index.md b/chapter_frontend_and_ir/index.md index 2f67845..afdbbe2 100644 --- a/chapter_frontend_and_ir/index.md +++ b/chapter_frontend_and_ir/index.md @@ -2,7 +2,7 @@ 在上一章节中,我们详细讨论了计算图的生成和调度,在进阶部分的介绍中简单介绍了深度学习编译器的作用。定义深度学习模型、计算图使用系统为用户提供的高级编程API,我们将用户使用高级编程API编写的程序称为源程序,将与硬件相关的程序称为目标程序,深度学习编译器需要理解输入的源程序并将其映射到目标机。为了实现这两项任务,编译器的设计被分解为两个主要部分:前端和后端。传统编译器的前端专注于理解源程序,后端则专注于将功能映射到目标机。为了将前后端相连接,我们需要一种结构来表示转换后的源代码,这就是中间表示(Intermediate Representation, IR)。 -图 :numref:`compiler_frontend_structure`展示了机器学习编译器的前端的流程。其中,对源程序的解析过程与传统编译器是大致相同的,本章节不对这部分进行更细致的讨论。机器学习框架的编译器前端的独特之处主要在于自动微分功能的支持。为了满足自动微分功能带来的新需求,机器学习框架需要在传统中间表示的基础上设计新的中间表示结构。因此,本章节的介绍重点会放在中间表示以及自动微分这两个部分。最后,我们会简要探讨类型系统,静态分析和前端优化等编译器基础概念。 +:numref:`compiler_frontend_structure`展示了机器学习编译器的前端的流程。其中,对源程序的解析过程与传统编译器是大致相同的,本章节不对这部分进行更细致的讨论。机器学习框架的编译器前端的独特之处主要在于自动微分功能的支持。为了满足自动微分功能带来的新需求,机器学习框架需要在传统中间表示的基础上设计新的中间表示结构。因此,本章节的介绍重点会放在中间表示以及自动微分这两个部分。最后,我们会简要探讨类型系统,静态分析和前端优化等编译器基础概念。 ![编译器前端基础结构](../img/ch04/编译器前端基础架构.svg) :width:`1000px` diff --git a/chapter_frontend_and_ir/intermediate_representation.md b/chapter_frontend_and_ir/intermediate_representation.md index 63d0d93..e488beb 100644 --- a/chapter_frontend_and_ir/intermediate_representation.md +++ b/chapter_frontend_and_ir/intermediate_representation.md @@ -8,7 +8,7 @@ 中间表示(IR),是编译器用于表示源代码的数据结构或代码,是程序编译过程中介于源语言和目标语言之间的程序表示。几乎所有的编译器都需要某种形式的中间表示,来对被分析、转换和优化的代码进行建模。在编译过程中,中间表示必须具备足够的表达力,在不丢失信息的情况下准确表达源代码,并且充分考虑从源代码到目标代码编译的完备性、编译优化的易用性和性能。 -引入中间表示后,中间表示既能面向多个前端,表达多种源程序语言,又能对接多个后端,连接不同目标机器,如图 :numref:`intermediate_representation`所示。在此基础上,编译流程就可以在前后端直接增加更多的优化流程,这些优化流程以现有IR为输入,又以新生成的IR为输出,被称为优化器。优化器负责分析并改进中间表示,极大程度的提高了编译流程的可拓展性,也降低了优化流程对前端和后端的破坏。 +引入中间表示后,中间表示既能面向多个前端,表达多种源程序语言,又能对接多个后端,连接不同目标机器,如 :numref:`intermediate_representation`所示。在此基础上,编译流程就可以在前后端直接增加更多的优化流程,这些优化流程以现有IR为输入,又以新生成的IR为输出,被称为优化器。优化器负责分析并改进中间表示,极大程度的提高了编译流程的可拓展性,也降低了优化流程对前端和后端的破坏。 ![中间表示](../img/ch04/中间表示-中间表示结构.svg) :width:`800px` @@ -18,7 +18,7 @@ ### 中间表示的种类 -上一节介绍了中间表示的基本概念,初步阐述了中间表示的重要作用和发展历程。接下来从组织结构的角度出发,介绍通用编译器的中间表示的类型以及各自特点([@2007Engineering]),如下表所示。中间表示组织结构的设计,对编译阶段的分析优化、代码生成等有着重要影响。编译器的设计需求不同,采用的中间表示组织结构也有所不同。 +上一节介绍了中间表示的基本概念,初步阐述了中间表示的重要作用和发展历程。接下来从组织结构的角度出发,介绍通用编译器的中间表示的类型以及各自特点 :cite:`2020MLIR`,如下表所示。中间表示组织结构的设计,对编译阶段的分析优化、代码生成等有着重要影响。编译器的设计需求不同,采用的中间表示组织结构也有所不同。 ::: {#tab:ch04/ch04-categorize} 组织结构 特点 举例 @@ -35,8 +35,7 @@ 线性中间表示类似抽象机的汇编代码,将被编译代码表示为操作的有序序列,对操作序列规定了一种清晰且实用的顺序。由于大多数处理器采用线性的汇编语言,线性中间表示广泛应用于编译器设计。 常用线性中间表示有堆栈机代码(Stack-Machine Code)和三地址代码(Three -Address Code) ([@2007Compilers]) -。堆栈机代码是一种单地址代码,提供了简单紧凑的表示。堆栈机代码的指令通常只有一个操作码,其操作数存在一个栈中。大多数操作指令从栈获得操作数,并将其结果推入栈中。三地址代码,简称为3AC,模拟了现代RISC机器的指令格式。它通过一组四元组实现,每个四元组包括一个运算符和三个地址(两个操作数、一个目标)。对于表达式a-b\*5,堆栈机代码和三地址代码如图 :numref:`linear_ir`所示。 +Address Code) :cite:`2007Compilers` 。堆栈机代码是一种单地址代码,提供了简单紧凑的表示。堆栈机代码的指令通常只有一个操作码,其操作数存在一个栈中。大多数操作指令从栈获得操作数,并将其结果推入栈中。三地址代码,简称为3AC,模拟了现代RISC机器的指令格式。它通过一组四元组实现,每个四元组包括一个运算符和三个地址(两个操作数、一个目标)。对于表达式a-b\*5,堆栈机代码和三地址代码如 :numref:`linear_ir`所示。 ![堆栈机代码和三地址代码](../img/ch04/中间表示-线性中间表示.svg) :width:`800px` @@ -48,7 +47,7 @@ Address Code) ([@2007Compilers]) Syntax Tree,AST)、有向无环图(Directed Acyclic Graph,DAG)、控制流图(Control-Flow Graph,CFG)等。 -AST抽象语法树采用树型中间表示的形式,是一种接近源代码层次的表示。对于表达式$a*5+a*5*b$,其AST表示如图 :numref:`AST_DAG`所示。可以看到,AST形式包含$a*5$的两个不同副本,存在冗余。在AST的基础上,DAG提供了简化的表达形式,一个节点可以有多个父节点,相同子树可以重用。如果编译器能够证明$a$的值没有改变,则DAG可以重用子树,降低求值过程的代价。 +AST抽象语法树采用树型中间表示的形式,是一种接近源代码层次的表示。对于表达式$a*5+a*5*b$,其AST表示如 :numref:`AST_DAG`所示。可以看到,AST形式包含$a*5$的两个不同副本,存在冗余。在AST的基础上,DAG提供了简化的表达形式,一个节点可以有多个父节点,相同子树可以重用。如果编译器能够证明$a$的值没有改变,则DAG可以重用子树,降低求值过程的代价。 ![AST图和DAG图](../img/ch04/中间表示-ASTDAG.svg) :width:`600px` @@ -56,11 +55,10 @@ AST抽象语法树采用树型中间表示的形式,是一种接近源代码 3、混合中间表示 -混合中间表示是线性中间表示和图中间表示的结合,这里以LLVM IR -([@2004LLVM]) 为例进行说明。LLVM(Low Level Virtual +混合中间表示是线性中间表示和图中间表示的结合,这里以LLVM IR :cite:`2004LLVM` 为例进行说明。LLVM(Low Level Virtual Machine)是2000年提出的开源编译器框架项目,旨在为不同的前端后端提供统一的中间表示。LLVM -IR使用线性中间表示表示基本块,使用图中间表示表示这些块之间的控制流,如图 :numref:`LLVM_IR`所示。基本块中,每条指令以静态单赋值(Static -Single Assignment, SSA) ([@Richard1995A]) +IR使用线性中间表示表示基本块,使用图中间表示表示这些块之间的控制流,如 :numref:`LLVM_IR`所示。基本块中,每条指令以静态单赋值(Static +Single Assignment, SSA) :cite:`Richard1995A` 形式呈现,这些指令构成一个指令线性列表。SSA形式要求每个变量只赋值一次,并且每个变量在使用之前定义。控制流图中,每个节点为一个基本块,基本块之间通过边实现控制转移。 ![LLVM IR](../img/ch04/中间表示-LLVMIR.svg) @@ -84,7 +82,7 @@ IR,能够很好地满足通用编译器的基本功能需求,包括类型系 计算图模式。主流机器学习框架如TensorFlow、PyTorch、MindSpore等都提供了静态图和动态图两种计算图模式,静态计算图模式先创建定义计算图,再显式执行,有利于对计算图进行优化,高效但不灵活。动态计算图模式则是每使用一个算子后,该算子会在计算图中立即执行得到结果,使用灵活、便于调试,但运行速度较低。机器学习框架的中间表示设计同时支持静态图和动态图,可以针对待解决的任务需求,选择合适的模式构建算法模型。 4\) -支持高阶函数和闭包([@2010C])。高阶函数和闭包是函数式编程的重要特性,高阶函数是指使用其它函数作为参数、或者返回一个函数作为结果的函数,闭包是指代码块和作用域环境的结合,可以在另一个作用域中调用一个函数的内部函数,并访问到该函数作用域中的成员。支持高阶函数和闭包,可以抽象通用问题、减少重复代码、提升框架表达的灵活性和简洁性。 +支持高阶函数和闭包 :cite:`2010C`。高阶函数和闭包是函数式编程的重要特性,高阶函数是指使用其它函数作为参数、或者返回一个函数作为结果的函数,闭包是指代码块和作用域环境的结合,可以在另一个作用域中调用一个函数的内部函数,并访问到该函数作用域中的成员。支持高阶函数和闭包,可以抽象通用问题、减少重复代码、提升框架表达的灵活性和简洁性。 5\) 编译优化。机器学习框架的编译优化主要包括硬件无关的优化、硬件相关的优化、部署推理相关的优化等,这些优化都依赖于中间表示的实现。 @@ -101,7 +99,7 @@ IR作为PyTorch模型的中间表示,通过JIT即时编译的形式,将Pytho PyTorch框架采用命令式编程方式,其TorchScript IR以基于SSA的线性IR为基本组成形式,并通过JIT即时编译的Tracing和Scripting两种方法将Python代码转换成TorchScript -IR。图 :numref:`TorchScript_IR`给出了Python示例代码及其TorchScript +IR。如 :numref:`TorchScript_IR`给出了Python示例代码及其TorchScript IR。 ![Python代码及输出的TorchScript IR](../img/ch04/中间表示-torchscript.png) @@ -123,7 +121,7 @@ IR是一种强类型、纯函数的中间表示,其输入、输出都带有类 :label:`Jaxpr` Jaxpr IR的表达采用ANF(A-norm -Form)函数式表达形式,如图 :numref:`Jaxpr`所示。ANF形式将表达式划分为两类:原子表达式(aexp)和复合表达式(cexp)。原子表达式用于表示常数、变量、原语、匿名函数,复合表达式由多个原子表达式组成,可看作一个匿名函数或原语函数调用,组合的第一个输入是调用的函数,其余输入是调用的参数。 +Form)函数式表达形式,如 :numref:`Jaxpr`所示。ANF形式将表达式划分为两类:原子表达式(aexp)和复合表达式(cexp)。原子表达式用于表示常数、变量、原语、匿名函数,复合表达式由多个原子表达式组成,可看作一个匿名函数或原语函数调用,组合的第一个输入是调用的函数,其余输入是调用的参数。 Jax框架结合了Autograd 和 JIT,基于Jaxpr IR,支持循环、分支、递归、闭包函数求导以及三阶求导,并且支持自动微分的反向传播和前向传播。 @@ -132,21 +130,16 @@ IR,支持循环、分支、递归、闭包函数求导以及三阶求导,并 TensorFlow框架同时支持静态图和动态图,是一个基于数据流编程的机器学习框架,使用数据流图作为数据结构进行各种数值计算。TensorFlow机器学习框架的静态图机制更为人所熟知。在静态图机制中,运行TensorFlow的程序会经历一系列的抽象以及分析,程序会逐步从高层的中间表示向底层的中间表示进行转换,我们把这种变换成为lowering。 -为了适配不同的硬件平台,基于静态计算图,TensorFlow采用了多种IR设计,其编译生态系统如图:numref:`TFIR`所示。蓝色部分是基于图的中间表示,绿色部分是基于SSA的中间表示。在中间表示的转换过程中,各个层级的中间表示各自为政,无法互相有效地沟通信息,也不清楚其他层级的中间表示做了哪些优化,因此每个中间表示只能尽力将当前的优化做到最好,造成了很多优化在每个层级的中间表示中重复进行, 从而导致优化效率的低下。尤其是从图中间表示到SSA中间表示的变化过大,转换开销极大。此外,各个层级的相同优化的代码无法复用,也降低了开发效率。 +为了适配不同的硬件平台,基于静态计算图,TensorFlow采用了多种IR设计,其编译生态系统如:numref:`TFIR`所示。蓝色部分是基于图的中间表示,绿色部分是基于SSA的中间表示。在中间表示的转换过程中,各个层级的中间表示各自为政,无法互相有效地沟通信息,也不清楚其他层级的中间表示做了哪些优化,因此每个中间表示只能尽力将当前的优化做到最好,造成了很多优化在每个层级的中间表示中重复进行, 从而导致优化效率的低下。尤其是从图中间表示到SSA中间表示的变化过大,转换开销极大。此外,各个层级的相同优化的代码无法复用,也降低了开发效率。 ![TensorFlow](../img/ch04/中间表示-MLIR.svg) :width:`600px` :label:`TFIR` -针对这个问题,TensorFlow团队提出了MLIR(Multi-Level Intermediate -Represent,多级中间表示) -([@2020MLIR]),允许使用TensorFlow和其它机器学习库的项目编译更有效的代码,从而最大程度地利用基础硬件。MLIR是用于现代优化编译器的灵活基础架构,旨在定义一个通用的中间表示,在统一的基础架构中支持多种不同的需求。MLIR采用混合中间表示,允许在同一编译单元中结合多个层级的抽象来表示、分析和转换计算图,利用其模块化、可扩展的特点,解决了各种中间表示之间转换效率和可迁移性不高的问题,从而适配多种硬件平台。 - 4、MLIR 针对这个问题,TensorFlow团队提出了MLIR(Multi-Level Intermediate -Represent,多级中间表示) -([@2020MLIR])。MLIR不是一种具体的中间表示定义,而是为中间表示提供一个统一的抽象表达和概念。 开发者可以使用MLIR开发的一系列基础设施,来定义符合自己需求的中间表示, 因此我们可以把MLIR理解为“编译器的编译器”。MLIR不局限于TensorFlow框架, 还可以用于构建连接其他语言与后端(如LLVM)的中间表示。 +Represent,多级中间表示) :cite:`2020MLIR`。MLIR不是一种具体的中间表示定义,而是为中间表示提供一个统一的抽象表达和概念。 开发者可以使用MLIR开发的一系列基础设施,来定义符合自己需求的中间表示, 因此我们可以把MLIR理解为“编译器的编译器”。MLIR不局限于TensorFlow框架, 还可以用于构建连接其他语言与后端(如LLVM)的中间表示。 MLIR深受LLVM设计理念的影响,但与LLVM不同的是, MLIR是一个更开放的生态系统。 在MLIR中, 没有预设的操作与抽象类型, 这使得开发者可以更自由地定义中间表示,并更有针对性地解决其领域的问题。MLIR通过Dialect的概念来支持这种可拓展性, Dialect在特定的命名空间下为抽象提供了分组机制,分别为每种中间表示定义对应的产生式并绑定相应的Operation, 从而生成一个MLIR类型的中间表示。Operation是MLIR中抽象和计算的核心单元,其具有特定的语意,可以用于表示LLVM中所有核心的IR结构, 例如指令, 函数以及模块等。 如下就是一个MLIR定义下的Operation: ``` @@ -173,7 +166,7 @@ based)。与TensorFlow类似,程序使用图来表示,使其容易去做 (2)纯函数的(Purely functional)。 -纯函数是指函数的结果只依赖函数的参数。若函数依赖或影响外部的状态,比如,函数会修改外部全局变量,或者函数的结果依赖全局变量的值,则称函数具有副作用([@spuler1994compiler])。若使用了带有副作用的函数,代码的执行顺序必须得到严格的保证,否则可能会得到错误的结果,比如对全局变量的先写后读变成了先读后写。同时,副作用的存在也会影响自动微分,因为反向部分需要从前向部分获取中间变量,需要确保该中间变量的正确。因此需要保证自动微分的函数是纯函数。 +纯函数是指函数的结果只依赖函数的参数。若函数依赖或影响外部的状态,比如,函数会修改外部全局变量,或者函数的结果依赖全局变量的值,则称函数具有副作用 :cite:`spuler1994compiler`。若使用了带有副作用的函数,代码的执行顺序必须得到严格的保证,否则可能会得到错误的结果,比如对全局变量的先写后读变成了先读后写。同时,副作用的存在也会影响自动微分,因为反向部分需要从前向部分获取中间变量,需要确保该中间变量的正确。因此需要保证自动微分的函数是纯函数。 由于Python语言具有高度动态性的特点,纯函数式编程对用户使用上有一些编程限制。有些机器学习框架的自动微分功能只支持对纯函数求导,且要求用户自行保证这一点。如果用户代码中写了带有副作用的函数,那么求导的结果可能会不符合预期。MindIR支持副作用的表达,能够将副作用的表达转换为纯函数的表达,从而在保持ANF函数式语义不变的同时,确保执行顺序的正确性,从而实现自由度更高的自动微分。 @@ -183,19 +176,19 @@ representation)。反向模式的自动微分,需要存储基本操作的中 (4)强类型的(Strongly typed)。每个节点需要有一个具体的类型,这个对于性能最大化很重要。在机器学习应用中,因为算子可能很耗费时间,所以越早捕获错误越好。因为需要支持函数调用和高阶函数,相比于TensorFlow的数据流图,MindIR的类型和形状推导更加复杂且强大。 -在结合MindSpore框架的自身特点后,MindIR的定义如图 :numref:`MindIR`所示。 +在结合MindSpore框架的自身特点后,MindIR的定义如 :numref:`MindIR`所示。 ![MindIR文法。MindIR中的ANode对应于ANF的原子表达式,ValueNode用于表示常数值,ParameterNode用于表示函数的形参,CNode则对应于ANF的复合表达式,表示函数调用](../img/ch04/中间表示-MindIR.svg) :width:`800px` :label:`MindIR` -接下来我们通过图 :numref:`MindIR_example`中的一段程序作为示例,来进一步分析MindIR。 +接下来我们通过 :numref:`MindIR_example`中的一段程序作为示例,来进一步分析MindIR。 ![MindIR的ANF表达](../img/ch04/中间表示-MindIR示例.png) :width:`600px` :label:`MindIR_example` -在ANF中,每个表达式都用let表达式绑定为一个变量,通过对变量的引用来表示对表达式输出的依赖,而在MindIR中,每个表达式都绑定为一个节点,通过节点与节点之间的有向边表示依赖关系。其函数图表示如图 :numref:`MindIR_graph`所示。 +在ANF中,每个表达式都用let表达式绑定为一个变量,通过对变量的引用来表示对表达式输出的依赖,而在MindIR中,每个表达式都绑定为一个节点,通过节点与节点之间的有向边表示依赖关系。其函数图表示如 :numref:`MindIR_graph`所示。 ![MindIR的函数图表示](../img/ch04/中间表示-MindIR图.png) :width:`800px` diff --git a/chapter_frontend_and_ir/type_system_and_static_analysis.md b/chapter_frontend_and_ir/type_system_and_static_analysis.md index 481d2f5..86bcfb9 100644 --- a/chapter_frontend_and_ir/type_system_and_static_analysis.md +++ b/chapter_frontend_and_ir/type_system_and_static_analysis.md @@ -16,9 +16,9 @@ 4)可读性。阅读代码时,明确的类型声明有助于理解程序代码。 机器学习框架一般使用Python语言作为描述网络模型结构的前端语言。Python语言是一门动态弱类型的语言,入门简单易学习,开发代码简洁高效,但由于其解释执行的方式,运行速度往往较慢。Python前端语言给用户带来了动态灵活的语义和高效的开发效率,但是若想要生成运行高效的后端代码,后端框架需要优化友好的静态强类型中间表示。因此,需要一种高效可靠的静态分析方法作为桥梁,将Python前端表示转换成等价的静态强类型中间表示,以此给用户同时带来高效的开发效率和运行效率,例如Hindley--Milner(HM)类型系统。这是一种具有参数多态性的简单类型lambda演算的类型系统。它最初由J. -Roger Hindley 提出([@1969The]),并由Robin Milner -进行扩展和验证([@1978A])。后来,路易斯·达马斯(Luis -Damas)对HM类型推导方法进行了详尽的分析和证明([@1982Principal]),并将其扩展到支持具有多态引用的系统。Hindley--Milner类型系统的目标是在没有给定类型注解的情况下,自动推导出任意表达式的类型。其算法具有抽象性和通用性,采用简洁的符号表示,能够根据表达式形式推导出明确直观的定义,常用于类型推导和类型检查。因此,Hindley--Milner类型系统广泛应用于编程语言设计中,比如Haskell和Ocaml。 +Roger Hindley 提出 :cite:`1969The`,并由Robin Milner +进行扩展和验证 :cite:`1978A` 。后来,路易斯·达马斯(Luis +Damas)对HM类型推导方法进行了详尽的分析和证明 :cite:`1982Principal`,并将其扩展到支持具有多态引用的系统。Hindley--Milner类型系统的目标是在没有给定类型注解的情况下,自动推导出任意表达式的类型。其算法具有抽象性和通用性,采用简洁的符号表示,能够根据表达式形式推导出明确直观的定义,常用于类型推导和类型检查。因此,Hindley--Milner类型系统广泛应用于编程语言设计中,比如Haskell和Ocaml。 ### 静态分析概述 diff --git a/mlsys.bib b/mlsys.bib index 96e85b6..fc14061 100644 --- a/mlsys.bib +++ b/mlsys.bib @@ -445,3 +445,150 @@ series = {ADKDD'14} journal={arXiv e-prints arXiv:1811.08309}, year={2018} } + +@misc{2020MLIR, + title={MLIR: A Compiler Infrastructure for the End of Moore's Law}, + author={ Lattner, C. and Amini, M. and Bondhugula, U. and Cohen, A. and Davis, A. and Pienaar, J. and Riddle, R. and Shpeisman, T. and Vasilache, N. and Zinenko, O. }, + year={2020}, +} + +@book{2007Engineering, + title={Engineering a Compiler}, + author={ Cooper, Keith D. and Torczon, Linda }, + publisher={Engineering A Compiler}, + year={2007}, +} + +@misc{2007Compilers, + title={Compilers: Principles, Techniques, and Tools (Rental), 2nd Edition}, + author={ Aho, A. V. and Lam, M. S. and Ullman, J. D. and Sethi, R. }, + year={2007}, +} + +@inproceedings{2004LLVM, + title={LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation}, + author={ Lattner, C. and Adve, V. }, + booktitle={Code Generation and Optimization, 2004. CGO 2004. International Symposium on}, + year={2004}, +} + +@article{Richard1995A, + title={A correspondence between continuation passing style and static single assignment form}, + author={Richard and A. and Kelsey}, + journal={Acm Sigplan Notices}, + year={1995}, +} + +@article{2010C, + title={C++ lambda expressions and closures}, + author={ Jaervi, Jaakko and Freeman, J. }, + journal={Science of Computer Programming}, + volume={75}, + number={9}, + pages={762-772}, + year={2010}, +} + +@article{spuler1994compiler, + title={Compiler detection of function call side effects}, + author={Spuler, David A and Sajeev, A Sayed Muhammed}, + journal={Informatica}, + volume={18}, + number={2}, + pages={219--227}, + year={1994}, + publisher={Citeseer} +} + +@book{10.5555/1455489, + author = {Griewank, Andreas and Walther, Andrea}, + title = {Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation}, + year = {2008}, + isbn = {0898716594}, + publisher = {Society for Industrial and Applied Mathematics}, + address = {USA}, + edition = {Second}, +} + +@article{2015Automatic, + title={Automatic Differentiation in Machine Learning: a Survey}, + author={ Pearlmutter, B. A. }, + journal={computer science}, + number={February}, + year={2015}, +} + +@article{2015Numerical, + title={Numerical Analysis}, + author={ Burden, R. L. and Faires, Jdd }, + journal={Journal of the Royal Statistical Society}, + volume={71}, + number={1}, + pages={48-50}, + year={2015}, +} + +@book{2003Computer, + title={Computer Algebra Handbook: Foundations * Applications * Systems}, + author={ Grabmeier, J. and Kaltofen, E. and Weispfenning, V. }, + publisher={Computer algebra handbook : foundations, applications, systems}, + year={2003}, +} + +@inbook{10.5555/60181.60188, +author = {Corliss, George F.}, +title = {Applications of Differentiation Arithmetic}, +year = {1988}, +isbn = {0125056303}, +publisher = {Academic Press Professional, Inc.}, +address = {USA}, +booktitle = {Reliability in Computing: The Role of Interval Methods in Scientific Computing}, +pages = {127–148}, +numpages = {22} +} + +@article{2000An, + title={An introduction to automatic differentiation}, + author={ Verma, A. }, + journal={Siam Computational Differentiation Techniques Applications & Tools}, + volume={78}, + number={7}, + pages={804-807}, + year={2000}, +} + +@inproceedings{2006The, + title={The Data-Flow Equations of Checkpointing in Reverse Automatic Differentiation}, + author={ Dauvergne, B. and L Hascoët}, + booktitle={Computational Science-iccs, International Conference, Reading, Uk, May}, + year={2006}, +} + +@article{2017Divide, + title={Divide-and-Conquer Checkpointing for Arbitrary Programs with No User Annotation}, + author={ Siskind, Jeffrey Mark and Pearlmutter, Barak A. }, + journal={Optimization Methods and Software}, + volume={33}, + number={4-6}, + year={2017}, +} + +@article{1969The, + title={The Principal Type-Scheme of an Object in Combinatory Logic}, + author={ Hindley, R. }, + journal={Transactions of the American Mathematical Society}, + volume={146}, + pages={29-60}, + year={1969}, +} + +@article{1978A, + title={A theory of type polymorphism in programming}, + author={ Milner, R. }, + journal={Journal of Computer and System Sciences}, + volume={17}, + number={3}, + pages={348-375}, + year={1978}, +} +