编译器本质上是一种提高开发效率的工具,将高级语言转换为低级语言(通常是二进制机器码),使得程序员不需要徒手写二进制。转换过程中,首要任务是保证正确性,同时需要进行优化以提升程序的运行效率。传统意义上的编译器的输入通常是某种高级语言,输出是可执行程序。在实际工作中接触到了深度学习编译器开发,其设计思想与传统编译器非常类似,所以本文以深度学习编译器的开发、结合我们实际开发的深度学习编译器 MegCC 为例,来说明如何写一个编译器。本文主要分为以下两个部分: 深度学习编译器简介
与传统编译器不同,深度学习编译器的输入是神经网络模型、输出是可运行在不同平台的表达了输入的神经网络模型的计算过程的可执行程序。但深度学习编译器又与传统编译器类似,都分为前端和后端,前端负责执行硬件无关的优化,后端负责执行硬件相关的优化。对编译器来说,最重要的两个概念是 IR(intermediate representation, 中间表示)和 Pass。对于人类来说,抽象是理解复杂事物的一种重要方式,IR 就是对编译过程中间产物的抽象,IR 通常有多级,越高级的 IR 越抽象,越低级的 IR 越具体。Pass 定义了如何将高级 IR 逐步 lowering 到低级 IR,并负责进行优化。下面根据前端和后端进行分类,介绍优化的方法。 前端优化方法
前端首先需要根据输入的模型构建计算图,生成 high-level IR,然后进行一系列的优化。由于优化是基于计算图的,并不涉及具体计算,所以该优化是后端无关的。常见的优化手段有可分为三类:node-level optimizations;block-level optimizations;dataflow-level optimizations。
node-level optimizations。节点层面的优化主要是消除一些不必要的节点以及将某些节点替换为代价更小的节点。比如使用矩阵 A 与一个 0 维矩阵相加,则可消除该加法操作。
block-level optimizations。块层面的优化主要有代数简化和算子融合。
a. 代数简化,例如 A^T 和 B^T 进行矩阵乘,则可使用 B 与 A 矩阵乘之后进行转置进行替换,可节约一次转置运算。
b. 算子融合是常见的深度学习的优化手段。算子融合虽然不能减少计算量,但是可以减少访存量,提高计算访存比,从而提升性能。
dataflow-level optimizations。数据流层面的优化主要有静态内存规划等
a. 静态内存规划通过在不发生内存重叠的前提下尽可能复用内存,使得程序运行时所使用的内存尽可能小。