如何上手 mlir
- MLIR出现的背景:模型算法越来越复杂,计算量越来越大;
- Dialect的演进:Dialect解决什么问题,需要什么基本模块,基本模块需要哪些扩展。
1 算法模型发展带来的问题
算法模型的发展带来两个问题:
- 算子越来越多,越来越复杂;
- 计算量越来越大。
为了解决这两个问题,需要从两个方面去着手:
- 软件方面:各个框架需要增加更多的算子,方便模型研究人员去使用和验证算法;
- 硬件方面:需要算力更高的硬件去满足模型训练的需求,为使能硬件,需要软件开发人员去弥补框架和硬件之间的gap。
1.1 软件方面
对第一点,软件方面框架适配,各大框架都有一套自己的opset标准,以至于出现了ONNX,想要统一这个标准。软件层面我们面临两大问题:a. 如何方便算法人员使用框架;b. 不同框架的模型如何转换。对a.,算法人员一般使用python,对应的框架也都有python接口,这里不存在问题。对b.,不同框架之间,目前大部分情况是把ONNX作为中间接口,比如TensorFlow <-> ONNX <-> PyTorch.
有了ONNX,问题貌似就解决了,不需要MLIR。这里,我们假设所有框架都统一了,只有一种框架,那也就不需要ONNX了,我们是否还需要一套中间模块?我们同样需要一套中间模块,原因是随着计算量增加,除了NV GPU,也出现了一批创业公司去做AI芯片,每家芯片都有自己不同的intrinsic,为了使能硬件,可以像NV那样做一套库,提供一套API。这种方式带来的问题是需要大量的人力投入,不适合创企。
MLIR出现的核心背景即:提供一套中间模块(即IR),这个中间模块有两个作用:1. 对接不同的软件框架;2. 对接软件框架和硬件芯片。
1.2 硬件方面
硬件方面,对接软硬件,主要是为了对接opset。
MLIR提供的解决方案是Dialect和DialectConversion。Dialect用来抽象opset,DialectConversion用来做转换。
来自各种不同框架(ftlite,tensorflow,pytorch)等框架的算法模型抽象为不同的 dialect,各种不同的 dialect 通过DialectConversion转换为同一层级的 dialect,最终再 lowering 到不同的目标硬件(GPU,TPU,…)dialect。
Dialect为了能达到对框架和硬件的抽象,提供了如下模块:Type, Attribute, Operation,这三者缺一不可,是MLIR最基本的模块。
DialectConversion为了能达到转换的目的,提供如下模块:ConversionTarget, ConversionPattern, TypeConverter。这里前两个模块是最基本的模块,一个用来表示进行转换的两个Dialect,一个用来匹配符合转换的operation。这三个模块不一定是必需的,只要能完成conversion的功能即可。
Dialect和DialectConversion便是MLIR最基础的两个模块。
2 MLIR 基本模块
为了进一步丰富Dialect的表达功能,MLIR提供了transformation模块,用来提供Dialect内部operation的转换变形。同时MLIR给Dialect提供了canonicalization模块,也是做内部转换。
接着为了增加对Dialect和Operation的标准化和功能扩展,MLIR增加了Constraint, Interface, Trait,方便对Operation进行限制和扩展。
Operation作为Dialect的核心元素,提供对算子的抽象,引入两个模块:Region和Block。同时,为了对在哪里做transformation和DialectConversion进行管理,提供了Pass模块。
为了统一管理MLIR的Dialect这些模块,让各个Dialect能更好的进行Conversion,MLIR提供了两个tablegen模块,即:ODS和DRR。
- ODS:统一Dialect,Operation等Dialect内部类的创建;
- DRR:统一Canonicalization, Transformation和Conversion的创建,即PatternRewritter的管理(除此之外,也提供对Pass的管理)。
只提供Dialect和DialectConversion解决不了实际应用问题。好比C++语法和STL库,以上这些模块可以比作C++语法,描述如何采用MLIR来实现Dialect和DialectConversion,但所实现的Dialect提供哪些功能,以及如何去弥补软硬件之间的gap,设计一套合适的架构,则是MLIR的另一大贡献。MLIR提供了一些基础的Dialect,方便开发人员去使用,这些Dialect各有侧重。
3 mlir 学习过程
MLIR的学习过程如下:
- 学习MLIR基本模块;
- 学习MLIR提供的Dialects,各个Dialects的定位,以及为弥补软硬件gap,提供的这些gap的分类和关联。
MLIR基本模块学习过程如下: 1、Dialect, Attribute, Type, Operation;想象如果自己去实现,该怎么设计类; 2、DialectConversion;想象在自己实现的前四个模块上,如何实现DialectConversion; 3、Interface, Constraint, Trait;同样,想象自己会怎么增加这些功能; 4、Transformation, Concalization; 5、Region, Block:基于1. 设计的Operation,以及4. 增加的Transformation,想象如何对Operation进行抽象,提取出Region和Block的概念; 6、Pass; 7、最后是ODS和DRR。
MLIR中Dialects分类及关联
1 Dialect分类
MLIR中Dialect分类可以通过两个坐标轴来看:tensor/buffer和payload/structre。
tensor/buffer维度含义是:Dialect主要数据类型是按照机器学习框架中的Tensor表示的(tensor),还是底层编译器中的Memory Buffer表示的(buffer)。很多方言的操作既有基于Tensor的也有基于Buffer的,比如Linalg和Standard。结合具体用例会更好理解一些(参考Toy中ch5转换到Linalg部分)。
payload/structure维度含义是:payload表示Dialect中操作描述执行什么计算(What);structure表示Dialect中操作描如何执行计算(How)。比如Math Dialect描述了执行什么计算,属于payload类型,SCF Dialect描述了如何执行计算,属于structure类型。
2 Dialect抽象层级
Linalg Dialect
: 对结构化数据进行结构化处理的通用表示。既可以将tensor作为操作数,也可以将buffer作为操作数;Operation中既有表示执行具体运算的payload类型操作,也有表示如何进行运算的struct类型操作。实际应用中外部Dialect很多情况下会先转换到Linalg Dialect再执行后续优化。
Vector Dialect
:对SIMD或者SIMT模型的抽象。其作为一种向量的中间表示,可以被转换到不同Target对应的底层表示,从而实现Tensor在不同平台的支持。
Affine Dialect
:对面体编译(polyhedral compilation)的实现。其主要包含了多维数据结构的控制流操作,比如:多维数据的循环和条件控制,存储映射操作等。其目标是实现多面体变换,比如:自动并行化、用于局部改进的循环融合和平铺,以及 MLIR 中的循环矢量化。
SCF(Structured Control Flow) Dialect
:比控制流图CFG更高层的抽象,比如并行的for和while循环以及条件判断。通常Affine和Linalg会降低到SCF,SCF也可以降低为Standard(貌似被拆分)中的CFG。
Async Dialect
:通常用来表示异步操作模型,一般为一些操作的集合,在不同的抽象层次含义有所变化。
最终,底层抽象Dialect被转换为特定平台的Dialect执行,比如:LLVM
, NVVM
, AVX
, Neon
, SVE
等。
3 Dialect转换通路
这里参考tensorflow中的Dialect转换来说明MLIR中Dialect的转换:
在Tensorflow层,先从TF Dialet
转换到HLO Dialect
, 在HLO(High-Level Optimizer) Dialect
中完成一些高层优化,之后转换到MHLO(Meta HLO)
。
在MLIR层,基本标量运算和Tensor运算被分解到不同的转换流程。标量运算被转换为Standard中的基本数学运算算子,进而下降到LLVM Dialect
;标量运算中的控制流图也被转换到对应的Standard CFG中,进而下降到LLVM的CFG。Tensor运算部分被转换到Linalg;然后基本运算转换到Vector,控制流降低到Affine后转换为SCF,SCF根据运行模型转换到相应的Dialect。
MLIR 介绍与上手项目
1 MLIR 和 LLVM 的简介
MLIR 是 Multi-Level Intermediate Representation 的简称。
MLIR 目前是在 LLVM 项目中,位于 mlir/ 文件夹下。LLVM 的作者,同时也是 TensorFlow 开发团队的主导者,带领团队创建了 MLIR 项目,期望统一这些编译相关的生态系统。
MLIR 是一个构建编译器的框架(不仅仅局限于深度学习编译器领域),其基本理念是,大型编译器应分解为子语言之间的许多小编译器(子语言被编译器相关开发人员称为 “中间表示” 或 IR),IR 设计的目标就是使某种特定的优化更为自然的表达。
MLIR 和 TensorFlow 也是相关的,因为训练过程和推理过程都可以认为是以 “2d convolution” 和 ”softmax” 等指令构成的程序,所以 MLIR 可以将 TensorFlow 训练或推理代码表示为 “conv-2d” 或 ”softmax” 等指令的组合。而在 “conv-2d” 等指令优化过程中,MLIR 又可以将其表示为更低层次的硬件指令(如 TPU 加速器相关指令),这个过程就更像是一个典型的编译器问题。TensorFlow 的例子表明,MLIR 可以将流程分解为不同抽象级别的 IR,如 Tensor Operations, linear algebra, 和 lower-level control flow.
2 mlir-tutorial
项目
2.1 build system
原文链接:(MLIR — Getting Started – Math ∩ Programming (jeremykun.com))[https://link.zhihu.com/?target=https%3A//jeremykun.com/2023/08/10/mlir-getting-started/]
GitHub 链接:(j2kun/mlir-tutorial (github.com))[https://link.zhihu.com/?target=https%3A//github.com/j2kun/mlir-tutorial]
使用 bazel build。也可以使用 cmake 编译,readme 中有 cmake 编译的说明。
- 首先,确保系统安装了 bazel 。
bazel --versions
- clone
mlir-tutorial
git clone https://github.com/j2kun/mlir-tutorial.git
- build llvm-project
bazel build @llvm-project//mlir:IR
- build mlir-opt
bazel run @llvm-project//mlir:mlir-opt -- --help
- 验证是否安装成功
# 执行 > echo 'func.func @main(%arg0: i32) -> i32 { %0 = math.ctlz %arg0 : i32 func.return %0 : i32 }' > ctlz.mlir > bazel run @llvm-project//mlir:mlir-opt -- $(pwd)/ctlz.mlir # 输出 INFO: Analyzed target @llvm-project//mlir:mlir-opt (0 packages loaded, 0 targets configured). INFO: Found 1 target... Target @llvm-project//mlir:mlir-opt up-to-date: bazel-bin/external/llvm-project/mlir/mlir-opt INFO: Elapsed time: 0.276s, Critical Path: 0.01s INFO: 1 process: 1 internal. INFO: Build completed successfully, 1 total action INFO: Running command line: bazel-bin/external/llvm-project/mlir/mlir-opt /home/xxxx/mlir-tutorial/ctlz.mlir module { func.func @main(%arg0: i32) -> i32 { %0 = math.ctlz %arg0 : i32 return %0 : i32 }
学习资料
-
【设计】先看下张磊大佬的MLIR设计思路分享 https://link.zhihu.com/?target=https%3A//www.lei.chat/zh/posts/compilers-and-irs-llvm-ir-spirv-and-mlir/
-
【Tutorials】按照官网Tutorials快速过一遍 https://link.zhihu.com/?target=https%3A//mlir.llvm.org/docs/Tutorials/Toy/
-
MLIR 文章视频汇总 https://zhuanlan.zhihu.com/p/141256429
-
【Practise】然后就是自己在Tutorials上改 添加一些新的算子:https://www.zhihu.com/column/c_1416415517393981440 基于Toy添加And算子支持直到JIT出来,看新增一个很简单的算子到底改了哪些代码:https://zhuanlan.zhihu.com/p/599281935
bazel 编译 C++ 项目
1 bazel 安装
参考官方的安装说明文档,ubuntu 安装说明文档。安装的方法比较多,这里记录了使用 Bazel 的 apt 代码库安装和从源码编译安装两中方式。
1.1 使用 Bazel 的 apt 代码库安装
- 将 Bazel 分发 URI 添加为软件包来源
sudo apt install apt-transport-https curl gnupg -y curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor >bazel-archive-keyring.gpg sudo mv bazel-archive-keyring.gpg /usr/share/keyrings echo "deb [arch=amd64 signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
组件名称“jdk1.8”仅出于传统原因保留,与受支持或包含的 JDK 版本无关。Bazel 版本与 Java 版本无关。更改“jdk1.8”组件名称将破坏代码库的现有用户。
- 安装和更新 Bazel
sudo apt update && sudo apt install bazel sudo apt update && sudo apt full-upgrade
bazel 软件包会自动安装最新的稳定版 Bazel。除了最新的 Bazel 之外,还可以安装其他的旧版本,例如:
sudo apt install bazel-1.0.0
可以通过创建符号链接将 bazel 设置为特定版本:
sudo ln -s /usr/bin/bazel-1.0.0 /usr/bin/bazel
bazel --version # 1.0.0
- 安装 JDK(可选)
Bazel 包含一个专用捆绑 JRE 作为其运行时,不需要安装任何特定版本的 Java。但是,如果使用 Bazel 构建 Java 代码,则必须安装 JDK。
sudo apt install default-jdk
1.2 从源代码编译 Bazel
-
下载 Bazel 的源代码(分发归档) 从 GitHub 下载 bazel-
-dist.zip,例如 bazel-0.28.1-dist.zip。 注意:
- 有一个独立于架构的分发归档。没有特定于架构或操作系统的发行版归档。
-
这些源代码与 GitHub 源代码树不同。必须使用发行版归档文件来引导 Bazel。无法使用从 GitHub 克隆的源代码树。(分发归档包含引导所需的生成的源文件,它们不属于常规 Git 源代码树。)
- 将分发归档解压缩到磁盘上的某个位置。 验证 Bazel 的发布密钥所做的签名。
-
在 Ubuntu Linux、macOS 和其他类似 Unix 的系统上引导 Bazel
- 安装必备项
- Bash
- zip、unzip
- C++ 构建工具链
- JDK.。版本 21 是必需的。
- Python。支持版本 2 和 3,安装其中一个就足够了。
在 Ubuntu Linux 上,可以使用以下命令安装这些要求:
sudo apt-get install build-essential openjdk-21-jdk python zip unzip
- 在 Unix 上引导 Bazel
- 打开 shell 或终端窗口。
- 通过 cd 方法解压缩该分发归档所在的目录。
- 运行编译脚本:
env EXTRA_BAZEL_ARGS="--tool_java_runtime_version=local_jdk" bash ./compile.sh
编译后的输出会放在output/bazel
中。这是一个独立的 Bazel 二进制文件,无嵌入式 JDK。可以在任意位置复制它,也可以直接使用。为方便起见,可以将此二进制文件复制到 PATH 上的目录(例如 Linux 上的/usr/local/bin
)。若要以可重现的方式构建 bazel 二进制文件,还应在“运行编译脚本”步骤中设置SOURCE_DATE_EPOCH
。
- 安装必备项
2 bazel 编译 C++ 项目
Bazel是一个类似于Make的编译工具,是Google为其内部软件开发的特点量身定制的工具,如今Google使用它来构建内部大多数的软件。相比 make,Bazel的规则层级更高。
参考官方示例项目:git clone https://github.com/bazelbuild/examples
2.1 项目结构
使用Bazel管理的项目一般包含以下几种Bazel相关的文件:WORKSPACE(同WORKSPACE.bazel),BUILD(同BUILD.bazel),.bzl 和 .bazelrc 等。
具体结构如下:
examples
└── cpp-tutorial
├──stage1
│ ├── main
│ │ ├── BUILD
│ │ └── hello-world.cc
│ └── WORKSPACE
├──stage2
│ ├── main
│ │ ├── BUILD
│ │ ├── hello-world.cc
│ │ ├── hello-greet.cc
│ │ └── hello-greet.h
│ └── WORKSPACE
└──stage3
├── main
│ ├── BUILD
│ ├── hello-world.cc
│ ├── hello-greet.cc
│ └── hello-greet.h
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
└── WORKSPACE
例子分三个 stage,由简单到复杂介绍了一些 Bazel 构建的基本概念。第一个例子是如何构建单个package中的单个target,第二个例子把整个项目拆分成单个package的多个target,第三个例子则将项目拆分成多个package,用多个target编译。
2.1.1 WORKSPACE
Bazel的编译是基于工作区(workspace)的概念。一个 workspace 可以认为就是一个 project。譬如上面 cpp-tutorial 目录下分别由 stage1、stage2 和 stage3 三个项目,每个项目的根目录下有一个 WORKSPACE 文件,空的就行,Bazel 就会将包含一个 WORKSPACE 文件的目录识别为一个项目。每个项目之间互不干扰是完全独立的。一个 workspace 里可以包含多个 packages。
工作区是一个存放了所有源代码和Bazel编译输出文件的目录,也就是整个项目的根目录。同时它也包含一些Bazel认识的文件:
- WORKSPACE文件,用于指定当前文件夹就是一个Bazel的工作区。所以WORKSPACE文件总是存在于项目的根目录下。WORKSPACE文件也可能定义加载 Bazel 工具和 rules 集,以及包含对构建输出所需的外部依赖项的引用。
- 一个或多个BUILD文件。BUILD 文件采用 Starlark 语言对模块构建进行描述,包含 Bazel 的几种不同类型的指令。每个 BUILD 文件都需要至少一条规则作为一组指令,告诉 Bazel 如何构建所需的输出,例如可执行文件或库。BUILD 文件中的 build 规则的每个实例都称为一个目标target,并指向一组特定的源文件和依赖项。 目标还可以指向其他目标。从逻辑上来说即每个 package 可以包含多个 Targets,而具体的 target 则采用 Starlark 语法定义在一个 BUILD 文件中。
要指定一个目录为Bazel的工作区,就只要在该目录下创建一个空的WORKSPACE文件即可。
当Bazel编译项目时,所有的输入和依赖项都必须在同一个工作区。属于不同工作区的文件,除非linked否则彼此独立。
2.2 使用Bazel编译项目
以 examples 中的 stage3 项目为例,stage3/lib/BUILD 文件内容如下:
cc_library(
name = "hello-time",
srcs = ["hello-time.cc"],
hdrs = ["hello-time.h"],
visibility = ["//main:__pkg__"],
)
stage3/main/BUILD 文件:
cc_library(
name = "hello-greet",
srcs = ["hello-greet.cc"],
hdrs = ["hello-greet.h"],
)
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
deps = [
":hello-greet",
"//lib:hello-time",
],
)
在此示例中,hello-world 目标会实例化 Bazel 的内置 cc_binary rule。该规则指示 Bazel 从hello-world.cc 源文件和两个依赖项hello-greet和//lib:hello-time构建独立的可执行文件。
为使构建成功,使用可见性属性让 lib/BUILD 中的 //lib:hello-time 目标明确显示给 main/BUILD 中的目标。这是因为默认情况下,目标仅对同一 BUILD 文件中的其他目标可见。Bazel 使用目标可见性来防止出现包含有实现细节的库泄露到公共 API 等问题。
2.2.1 target定义–目标
大多数目标是两种主要类型之一:文件和规则。如示例中的hello-world和hello-greet等。
文件进一步分为两种。源文件通常由用户编写并签入代码库。生成的文件(有时称为派生文件或输出文件)不会被签入,但是从源文件生成的。
第二种目标使用规则声明。每个规则实例都用于指定一组输入文件与一组输出文件之间的关系。规则的输入可以是源文件,也可以是其他规则的输出。
target是某个 rule 的一个实例。Rule规定了 一类构建规则。从 cc_library 这个规则名称上我们很容易猜测出来这 一类规则 描述了如何构建一个采用 C/C++ 编程语言编写的库(library,可以是静态库也可能是动态库)。
定义 target 就是实例化了这个 rule,上面这段代码实际上就是定义了一个 target,每个实例必须要有一个名字在同一个 package 中和其他 target 实例进行区分。所以 name 这个 attribute 是必须有的,其他 attribute 是可选的,不写则按默认值定义。
2.2.2 标签 –label
label 可以认为是在一个 Bazel 的 workspace 范围中唯一标识一个 target 的 ID。我们可以用这个 label 来引用一个 target。label 的语法如下:
//path/to/package:target-name
以 //
开始,接下来的 path/to/package
也就是这个 target 所在 package 在 workspace 中的相对路径。然后是一个 :
后面跟着一个 target-name
即上面说的一个 target 中的 name 那个属性的字符串值。
要构建 examples/cpp-tutorial/stage3 这个 workspace 下的 main 这个 package 中的 “hello-greet” 这个 target。那么我们要做的就是先 cd 到 stage3 这个 workspace 下然后用 label 引用这个 target 执行构建。具体命令如下:
cd examples/cpp-tutorial/stage3
bazel build //main:hello-greet
就会生成 libhello-greet.a 和 libhello-greet.so
2.2.3 依赖–dependency
各个 target 之间存在依赖关系,这是在所有构建系统中都存在的概念,同样在 Bazel 中也缺少不了。在 stage3 这个例子中,target //main:hello-world
依赖于 target //main:hello-greet
,背后的含义就是我们要构建最终的可执行程序 hello-world
,则首先要构建成功 hello-greet
这个规则的 obj
文件,这种依赖关系在 BUILD 文件中体现为 deps
这个 attribute 的描述。
注意以下两点:
":hello-greet"
这里也是一个 label 描述,由于hello-greet
和hello-world
这两个 target 在一个 package 中,所以前面的path/to/package
可以省略。- 这里如果直接执行
bazel build //main:hello-world
并不会生成libhello-greet.a
和libhello-greet.so
,原因是目前例子中上面的描述并没有给出构建hello-world
需要依赖so
或者.a
。所以默认只是依赖于hello-greet
的 obj 文件。
2.2.4 .bzl
文件
如果项目有一些复杂构造逻辑、或者一些需要复用的构造逻辑,那么可以将这些逻辑以函数形式保存在.bzl
文件,供WORKSPACE
或者BUILD
文件调用。其语法跟Python类似。
2.2.5 .bazelrc
文件
对于Bazel来说,如果某些构建动作都需要某个参数,就可以将其写在此配置中,从而省去每次敲命令都重复输入该参数。Bazel 会按以下顺序读取可选的 bazelrc 文件:
- 系统级文件,位于
etc/bazel.bazelrc
。 - 位于
$workspace/tools/bazel.rc
的 Workspace rc 文件。 - 主目录文件位于
$HOME/.bazelrc
中
此处列出的每个 bazelrc
文件都有一个对应的标志,可用于停用这些标志(例如 --nosystem_rc
、--noworkspace_rc
和 --nohome_rc
)。还可以通过传递 --ignore_all_rc_files
启动选项让 Bazel 忽略所有 Bazelrcs。
2.2.6 如何工作
当运行构建或者测试时,Bazel会:
- 加载和目标相关的BUILD文件
- 分析输入及其依赖,应用指定的构建规则,产生一个
Action
图。这个图表示需要构建的目标、目标之间的关系,以及为了构建目标需要执行的动作。Bazel依据此图来跟踪文件变动,并确定哪些目标需要重新构建 - 针对输入执行构建动作,直到最终的构建输出产生出来
2.2.7 构建
切换到 stage3 目录并使用 bazel build:
cd stage3
bazel build //main:hello-world
在目标标签中,//main:
部分是BUILD文件相对于工作区根目录的位置,hello-world
是BUILD文件中的目标名称。
Bazel 会生成如下所示:
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 0.167s, Critical Path: 0.00s
运行以下命令可删除输出文件:
bazel clean
最终构建生成的可执行文件会保存在:bazel-bin/main/hello-world
2.2.8 依赖图
生成依赖图:
# 查找target为 //main:hello-world 的所有依赖
# --nohost_deps 表示不包括host依赖
# --noimplicit_deps 表示不包括隐式依赖 e.g: @bazel_tools//tools/cpp:stl
bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph
将生成的输出图文字描述, 粘贴到 GraphViz, 生成的依赖图如下
3 bazel 构建原理
3.1 调度模型
传统构建系统有很多是基于任务的,用户可以自定义任务(Task),例如执行一段 shell 脚本。用户配置它们的依赖关系,构建系统则按照顺序调度。这种模式对使用者很友好,他可以专注任务的定义,而不用关心复杂的调度逻辑。构建系统通常给予任务制定者极大的”权利”
如果一个任务,在输入条件不变的情况下,永远输出相同的结果,我们就认为这个任务是”封闭”(Hermeticity) 的。构建系统可以利用封闭性提升构建效率,例如第二次构建时,跳过某些输入没变的 Task,这种方式也称为增量构建。
不满足封闭性的任务,则会导致增量构建失效,例如 Task 访问某个互联网资源,或者 Task 在执行时依赖随机数或时间戳这样的动态特征,这些都会导致多次执行 Task 得到不同的结果。
Bazel 采用了不同的调度模型,它是基于目标Target。Bazel 官方定义了一些规则 (rule),用于构建某些特定产物,例如 c++ 的 library 或者 go 语言的 package,用户配置和调用这些规则。他仅仅需要告诉 Bazel 要构建什么 target,而由 Bazel 来决定如何构建它。
bazel基于 Target 的调度模型如下图所示:
File 表示原始文件,Target 表示构建时生成的文件。当用户告诉 Bazel 要构建某个 Target 的时候,Bazel 会分析这个文件如何构建(构建动作定义为 Action,和其他构建系统的 Task 大同小异),如果 Target 依赖了其他 Target,Bazel 会进一步分析依赖的 Target 又是如何构建生成的,这样一层层分析下去,最终绘制出完整的执行计划。
3.2 并行编译
Bazel 精准的知道每个 Action 依赖哪些文件,这使得没有相互依赖关系的 Action 可以并行执行,而不用担心竞争问题。基于任务的构建系统则存在这样的问题:两个 Task 都会向同一个文件写一行字符串,这就造成两个 Task 的执行顺序会影响最终的结果。要想得到稳定的结果,就需要定义这两个 Task 之间的依赖关系。
Bazel 的 Action 由构建系统本身设计,更加安全,也不会出现类似的竞争问题。因此我们可以充分利用多核 CPU 的特性,让 Action 并行执行。
通常我们采用 CPU 逻辑核心数作为 Action 执行的并发度,如果开启了远端执行,则可以开启更高的并发度。
3.3 增量编译
Bazel 将构建拆分为独立的步骤,这些步骤称为操作(Action)。每项操作都有输入、输出名称、命令行和环境变量。系统会为每个操作明确声明所需的输入和预期输出。
对 Bazel 来说,每个 Target 的构建过程,都对应若干 Action 的执行。Action 的执行本质上就是”输入文件 + 编译命令 + 环境信息 = 输出文件”的过程。
如果本地文件系统保留着上一次构建的 outputs,此时 Bazel 只需要分析 inputs, commands 和 envs 和上次相比有没有改变,没有改变就直接跳过该 Action 的执行。
这对于本地开发非常有用,如果你只修改了少量代码,Bazel 会自动分析哪些 Action 的 inputs 发生了变化,并只构建这些 Action,整体的构建时间会非常快。
不过增量构建并不是 Bazel 独有的能力,大部分的构建系统都具备。但对于几万个文件的大型工程,如果不修改一行代码,只有 Bazel 能在一秒以内构建完毕,其他系统都至少需要几十秒的时间,这简直就是 降维打击 了。
Bazel 是如何做到的呢?
首先,Bazel 采用了 Client/Server 架构,当用户键入 bazel build 命令时,调用的是 bazel 的 client 工具,而 client 会拉起 server,并通过 grpc 协议将请求 (buildRequest) 发送给它。由 server 负责配置的加载,ActionGraph 的生成和执行。
构建结束后,Server 并不会立即销毁,而 ActionGraph 也会一直保存在内存中。当用户第二次发起构建时,Bazel 会检测工作空间的哪些文件发生了改变,并更新 ActionGraph。如果没有文件改变,就会直接复用上一次的 ActionGraph 进行分析。
这个分析过程完全在内存中完成,所以如果整个工程无需重新构建,即便是几万个 Action,也能在一秒以内分析完毕。而其他系统,至少需要花费几十秒的时间来重新构建 ActionGraph。
4 bazel 外部依赖管理
大部分项目都没法避免引入第三方的依赖项。构建系统通常提供了下载第三方依赖的能力。为了避免重复下载,Bazel 要求在声明外部依赖的时候,需要记录外部依赖的 hash,Bazel 会将下载的依赖,以 CAS 的方式存储在内置的 repository_cache 目录下。可以通过bazel info repository_cache 命令查看目录的位置。
Bazel 认为通过 checksum 机制,外部依赖应该是全局共享的,因此无论本地有多少个工程,哪怕使用的是不同的 Bazel 版本,都可以共享一份外部依赖。
除此之外,Bazel 也支持通过 1.0.0 这样的 SerVer 版本号来声明依赖,这是 Bazel6.0 版本加入的功能,也是官方推荐使用的。
MLIR
1 MLIR 介绍
MLIR 上手 MLIR Tutorial
1 MLIR 介绍
MLIR 设计了一套可复用的编译管线,包括可复用的 IR、Pass 和 IO 系统。
在 IR 中,多个 Dialect 可以混合存在。
1.1 常见 dialect
MLIR 的 Dialect 是相对独立的,例如,这里有一些常见的 dialect:
- func:处理函数的 dialect,包含函数定义、调用、返回等基本操作
- arith:处理加减乘除移位等运算
- math:更复杂的运算,如 log, exp, tan 等
- affine:处理循环嵌套,实现了循环展开、多面体变换等一些算法
- scf:(structured control flow) 结构化控制流,保留 for,if 等语句
- cf:无结构控制流,只有条件跳转命令
- llvm:LLVM IR 的 binding,可以直接翻译给 LLVM 做后续编译
MLIR的编译从高层次的 tensor 到 低层次的 scf,cf,每个阶段都是多个 dialect 的混合体,每次 lowering 往往只针对一个 dialect 进行。
1.2 insight,及时优化
MLIR 的 insight 在于及时优化。很明显,在 linalg 层级,很容易发现矩阵被转置了两次,但是一旦 lower 到 scf,所有转置操作都变成循环,优化就很难进行了。因此需要在方便优化的层级及时把优化处理掉。
- dialect 的混合举例,假如使用 pytorch 生成了一个神经网络:
- Tensor 是一块带 shape 的指针:使用 tensor dialect
- 简单的 elementwise 加减乘除:使用 arith dialect
- 复杂的 log、exp 等运算:使用 math dialect
- 矩阵线性代数运算:使用 linalg dialect
- 可能有一些控制流:使用 scf dialect
- 整个网络是一个函数:使用 func dialect
接下来,将其逐渐 lower 到 LLVM:
- 调用 Lowering Pass,把 tensor lowering 到 linalg,而其他的 dialect 不会改变。
- 继续调用 pass,直到把 linalg 转换到 affine -> scf -> cf,其他表示运算的 dialect 保留不变。
- 继续 Lowering,把 memref 转换为裸指针、arith 和 func 转换为 llvm 内置运算。
- 最后,所有非 llvm dialect 都被转换为了 llvm dialect,现在可以导出为 llvm ir 交给 llvm 继续编译。
在上面的过程中不难看出,MLIR 编译过程中,不同的 dialect 是相互独立的。例如,在做循环展开等优化的时候,不需要关心加法和减法可以合并;而在做算数表达式优化的时候,也不需要关心当前在哪个函数里边。
另外,MLIR 可以从各个层次优化 IR:
- 在 affine 层面,可以根据循环大小做展开,向量化
- 在 scf 层面,可以发现循环不变量
- 在 arith 层面,可以用算数恒等式优化代码
1.3 MLIR 的优点和不足
- 优点,这里主要说明复用已有代码的优点
- 复用已有 dialect 作为 输入,不用自己写前端。
- 如 Polygeist 能把 C 翻译成 Affine Dialect,这样我们就不用写 C Parser
- 将已有 dialect 混入或作为输出。
- 如 arith 等 dialect,可以直接集成起来,不需要自己写。
- 要生成 binary 的时候,可以直接生成 LLVM Dialect,复用后端 LLVM 编译管线
- 复用已有的 Pass。
- 常见的 Pass 如 CSE,DCE 可以复用
- Dialect 专用 Pass,如循环展开,也可以复用
- 复用已有 dialect 作为 输入,不用自己写前端。
- 缺点
- 太过笨重,编译、链接时间长(可能会连接出上百M的文件)
- Dialect 定义不灵活,定义较复杂 Op 时比较麻烦
2 MLIR 基本使用
2.1 IR 基本结构
MLIR 是 树形结构,每个节点是 Operation,Op 可以组成 Block,Block 组成 Region,Region 又可以嵌套在 Op 内部。
- Operation 指单个运算,运算内可以嵌套 Region
- Block 指基本块,基本块包含一个或多个 Operation
- Region 指区域,类似于循环体或函数体,包含若干 Block
MLIR 的基本块使用 “基本块参数” 来取代“phi函数”:
- 块参数:每个基本块都带有参数,块内可以使用
- 终止符:每个基本块的一般为跳转或返回,跳转时需要附上块参数
- module: 默认情况下,mlir 最外层是 builtin.module,作为 IR 的根。
2.2 构建 MLIR 工程
- 构建第一个 mlir 项目往往会有点困难,下面是一个基本的工程模板:
mlir-tutorial ├── install # Install Prefix,把 MLIR 编译后安装到这里 ├── llvm-project # MLIR 项目路径 └── mlir-toy # 自己的 MLIR 工程路径
- 按照 MLIR 的 (getting started )[https://mlir.llvm.org/getting_started/] 安装 MLIR,
git clone https://github.com/llvm/llvm-project.git cd llvm-project mkdir build && cd build cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=../../install/ -DLLVM_ENABLE_PROJECTS=mlir -DLLVM_BUILD_EXAMPLES=ON -DLLVM_ENABLE_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=Debug -DLLVM_TARGETS_TO_BUILD="Native;NVPTX;AMDGPU" cmake --build . --target check-mlir
build 完成之后,安装到 install:
ninja install
至此,mlir 把所有二进制文件、库文件都装到了 install 目录下。设置环境变量export PATH=/path/to/install/bin:$PATH
,方便调用 bin 里面的 mlir-opt 等程序。
- 接下来,在 mlir-toy 里面建立一个简单的工程
mlir-toy ├── CMakeLists.txt └── main.cpp
CMakeLists.txt 文件写法比较固定:
cmake_minimum_required(VERSION 3.13.4) project(mlir-toy VERSION 0.0.1) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成 compile_commands.json 便于代码高亮 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED YES) find_package(MLIR REQUIRED CONFIG) list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}") list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") include(TableGen) include(AddLLVM) include(AddMLIR) include(HandleLLVMOptions) include_directories(${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS}) add_executable(mlir-toy main.cpp)
在 main.cpp 里面写一个 main 函数,然后先 build 一下。注意,必须要写上 CMAKE_INSTALL_PREFIX,这样 cmake 可以自动找到 MLIR。
cd mlir-toy mkdir build && cd build cmake .. -GNinja -DCMAKE_INSTALL_PREFIX=/path/to/install ninja
其实为了方便熟悉不同的功能,可以在 mlir-toy 下面建立各个功能的工程目录,这样每一个工程目录就可以很方便的玩耍了。
2.3 配置 clangd
使用 vscode 默认的 lint 工具跑 mlir 会非常卡,使用 clangd 会丝滑很多。
- 在 vscode 扩展里安装 clangd 插件
- cmake 的时候加上 -DCMAKE_EXPORT_COMPILE_COMMANDS=ON(上面的CMakeLists.txt 已经加上了)
- 把 compile_commands.json 拷贝到工程根目录,或者在 vscode 设置里配置一下
- 一旦发现高亮炸了,vscode里 Ctrl + Shift + P,输入 clangd: restart language server
- 有时候,mlir 的编译选项与 clangd 冲突,在 mlir-toy 目录下建立 .clangd 文件,去掉相关的选项:
CompileFlags: Remove: - -fno-lifetime-dse
2.4 MLIR 的读入、输出
- 在 mlir-toy 下面新建目录 io_test:
mkdir io_test && cd io_test
,并准备好测试 MLIR 读入输出 mlir 和 cpp 代码:- io.mlir
func.func @test(%a: i32, %b: i32) -> i32 { %c = arith.addi %a, %b : i32 func.return %c : i32 }
- io.cpp
#include "mlir/IR/AsmState.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/MLIRContext.h" #include "mlir/Parser/Parser.h" #include "mlir/Support/FileUtilities.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "llvm/Support/raw_ostream.h" using namespace mlir; int main(int argc, char ** argv) { MLIRContext ctx; // 首先,注册需要的 dialect ctx.loadDialect<func::FuncDialect, arith::ArithDialect>(); // 读入文件 auto src = parseSourceFile<ModuleOp>(argv[1], &ctx); // 输出dialect,也可以输出到 llvm::errs(), llvm::dbgs() src->print(llvm::outs()); // 简单的输出,在 debug 的时候常用 src->dump(); return 0; }
- io.mlir
- 准备
CMakeLists.txt
,CMakelists.txt 的写法比较固定,这里需要连接上所有依赖的文件:cmake_minimum_required(VERSION 3.13.4) project(mlir-toy VERSION 0.0.1) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成 compile_commands.json 便于代码高亮 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED YES) find_package(MLIR REQUIRED CONFIG) list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}") list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") include(TableGen) include(AddLLVM) include(AddMLIR) include(HandleLLVMOptions) include_directories(${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS}) add_executable(io_test io.cpp) target_link_libraries( io_test MLIRIR MLIRParser MLIRFuncDialect MLIRArithDialect )
- build 并执行测试
mkdir build && cd build cmake .. -GNinja -DCMAKE_INSTALL_PREFIX=/path/to/install/ ninja ./io_test ../io.mlir
2.5 用代码生成 MLIR
也可以直接使用 cpp 代码来生成 mlir,而不需要单独准备一份 mlir 的文件。
- 在 mlir-toy 下面新建目录 mlir_gen:
mkdir mlir_gen && cd mlir_gen
,并准备好cpp 代码:
mlir_gen.cpp
#include "mlir/IR/AsmState.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/MLIRContext.h"
#include "mlir/Parser/Parser.h"
#include "mlir/Support/FileUtilities.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "llvm/Support/raw_ostream.h"
using namespace mlir;
int main(int argc, char ** argv) {
MLIRContext ctx;
ctx.loadDialect<func::FuncDialect, arith::ArithDialect>();
// 创建 OpBuilder
OpBuilder builder(&ctx);
auto mod = builder.create<ModuleOp>(builder.getUnknownLoc());
// 设置插入点
builder.setInsertionPointToEnd(mod.getBody());
// 创建 func
auto i32 = builder.getI32Type();
auto funcType = builder.getFunctionType({i32, i32}, {i32});
auto func = builder.create<func::FuncOp>(builder.getUnknownLoc(), "test", funcType);
// 添加基本块
auto entry = func.addEntryBlock();
auto args = entry->getArguments();
// 设置插入点
builder.setInsertionPointToEnd(entry);
// 创建 arith.addi
auto addi = builder.create<arith::AddIOp>(builder.getUnknownLoc(), args[0], args[1]);
// 创建 func.return
builder.create<func::ReturnOp>(builder.getUnknownLoc(), ValueRange({addi}));
mod->print(llvm::outs());
return 0;
}
- 准备
CMakeLists.txt
,CMakelists.txt 的写法比较固定,和前面的写法类似:cmake_minimum_required(VERSION 3.13.4) project(mlir-toy VERSION 0.0.1) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成 compile_commands.json 便于代码高亮 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED YES) find_package(MLIR REQUIRED CONFIG) list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}") list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") include(TableGen) include(AddLLVM) include(AddMLIR) include(HandleLLVMOptions) include_directories(${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS}) add_executable(mlir_gen mlir_gen.cpp) target_link_libraries( mlir_gen MLIRIR MLIRParser MLIRFuncDialect MLIRArithDialect )
- build 并执行测试
mkdir build && cd build cmake .. -GNinja -DCMAKE_INSTALL_PREFIX=/path/to/install/ ninja ./mlir_gen
可以看到此时输出的 ir 和前面通过 mlir 文件读入的 ir 时一致的。
3 MLIR Op 的结构
MLIR 的一个 Operation 里包含以下内容:
- Operand:这个 Op 接受的操作数
- Result:这个 Op 生成的新 Value
- Attribute:可以理解为编译器常量
- Region:这个 Op 内部的 Region
MLIR 中,Attribute 是高度灵活的,允许插入原来不存在的 attr,允许不同 dialect 互相插入 attribute。
3.1 Attribute 和 Operand
Attribute 和 Operand 有一些区别,Attribute 指的编译器已知的量,而 Operand 指只有运行时才能知道的量。
例如,%c0 = arith.constant 0 : i32
里面,0 就是一个 Attribute,而不是 Operand。
3.2 Attribute, Value 和 Type
Value 必然包含 Type,Type 也可以作为 Attribute 附加在 Operation 上。
如下,func OP,
func.func @test(%a: i32, %b: i32) -> i32 {
%c = arith.addi %a, %b : i32
func.return %c : i32
}
%a, %b 在参数表里,但它们实际上是函数类型的一部分,算是 Type Attribute。使用mlir-opt --mlir-print-op-generic
来打印这里的代码,得到下面的代码。参数名被隐去,只有 function_type 作为 attribute 保留了下来。
"builtin.module"() ({
"func.func"() <{function_type = (i32, i32) -> i32, sym_name = "test"}> ({
^bb0(%arg0: i32, %arg1: i32):
%0 = "arith.addi"(%arg0, %arg1) : (i32, i32) -> i32
"func.return"(%0) : (i32) -> ()
}) : () -> ()
}) : () -> ()
4 MLIR 的类型转换
4.1 OP 类型转换
MLIR 的所有 OP 都按照统一的存储格式存储,即Operation
。Operation
里面包含OpName
,operands
,results
,attributes
以及其他信息。
自定义OP,例如arith.addi
,本质上都是Operation
的指针。但与Operation*
不同的是,AddIOp
定义了Operation
里储存的数据的解释方式。如 AddOp
,自己是一个 Operation
的指针,也定义了一个函数 getLhs
用来返回第一个值,当作 lhs
。
- DownCast:如何在拿到
Operation*
的情况下,将其转换为AddOp
?llvm 提供了一些转换函数,这些函数会检查 Operation 的 OpName,并进行转换。using namespace llvm; void myCast(Operation * op) { auto res = cast<AddOp>(op); // 直接转换,失败报错 auto res = dyn_cast<AddOp>(op); // 尝试转换,失败返回 null,op为null时报错 auto res = dyn_cast_if_present<AddOp>(op); // 类似 dyn_cast,op为null时返回null }
-
相等关系:两个
Operation*
相等,指的是它们指向同一个 Operation 实例,而不是这个 Operation 的 operand,result,attr 相等。 - Hashing:在不修改 IR 的情况下,每个
Operation
有唯一地址。于是,可以直接用Operation*
当作值建立哈系表,用来统计 IR 中数据或做分析:#include "llvm/ADT/DenseMap.h" llvm::DenseMap<Operation*, size_t> numberOfReference;
4.2 Type / Attribute 的类型转换
MLIR 的 Type 和 Attribute 与 Op 类似。Type
是TypeStorage
的指针,Attribute
是AttributeStorage
的指针。
-
TypeStorage 里面会存 Type 的参数,如 Integer 会存 width,Array 会存 Shape。 |专用指针|通用指针|值(存放在 context 中)| |—|—|—|—| |AddOp|Operation*|Operation| |IntegerType|Type|TypeStorage| |IntegerAttr|Attribute|AttrStorage|
- 全局单例:与 Op 不同的是,MLIR Context 会完成 Type 和 Attribute 的去重工作。Type相等,它们的TypeStorage也一定相等。
- DownCast:Type 的 DownCast 与 Op 相同。
- Hashing:与 Op 类似,Type 也可以作为 Key 来建哈系表,但不那么常用。
5 MLIR 的图结构
MLIR 里,有两个层次的图:
- 第一个是 Region 嵌套构成的树,这个图表示 控制流
- 第二个是 Op/Value 构成的图,这个图表示 数据流
5.1 MLIR 数据流图结构
MLIR 的数据流图是由 Operation 和 Value 构成的。
- Operation 的连接
- Value 要么来自于 Operation 的 Result 要么来自于 BlockArgument
- 每个 Operation 的 Operand 都是到 Value 的指针
- 修改 Operand 的时候,实际修改的应该是 OpOperand
- Value 的 use-chain
- 每个 Value 都将其 User 连接在一起了
MLIR 的图是一个双向的图结构,在遍历尤其是修改的时候需要特别小心。
- 在修改 OpOpeand 的时候,对应 value 的 use-chain 会暗中被 MLIR 改掉
- 在调用
value->getDefiningOp()
的时候,BlockArgument 会返回 null
- 每个 Value 都将其 User 连接在一起了
5.2 MLIR 数据流图的遍历与修改
MLIR 数据流图的遍历往往遵循这样一种模式:Operation 调用函数找 Value,再用 Value 调用函数找 Operation,交替进行。
Operation 找 Value 的方法有:
- getOperands、getResults:这两个非常常用,如下面的代码可以用来 Op 找 Value:
for(auto operand: op->getOperands()) { if(auto def = operand.getDefiningOp()) { // do something } else { // block argument } }
- getOpOperands:这个在需要更改 operands 的时候非常有用,例如下面的代码将 value 做替换:
IRMapping mapping; // 将 op1 的 results 映射到 op2 的 results mapping.map(op1->getResults(), op2->getResults()); for(auto &opOperand: op3->getOpOperands()) { // 将 op3 的参数里含有 op1 results 的替换为 op2 的 // lookupOrDefault 指找不到 mapping 就用原来的 opOperand.set(mapping.lookupOrDefault(opOperand.get())); }
Value 找 Op 的方法有:
- getDefiningOp:可能返回 null
- getUses:返回 OpOperand 的迭代器
- getUsers:返回 Operation 的迭代器
Op的getUses和getUser:operation 也有 getUses 和 getUsers 函数,等价于把这个 op 的所有 result 的 Uses 或 Users 拼在一起。
Value的修改:Value 支持 replaceAllUseWith 修改,一种看起来等价的代码是:
for(auto & uses: value.getUses()) {
uses.set(new_value);
}
需要注意,上面的代码是非常危险的。因为在 uses.set 的时候,会修改 value 的 use chain,而 value 的 use-chain 正在被遍历,可能一修改就挂了。最好用 mlir 提供好的 replaceAllUseWith 来修改。
5.3 MLIR 控制流图的遍历与修改
控制流图遍历常用的一些函数:
- op.getParentOp, op.getParentOfType:获取父亲Op。父亲 OP 的type
- op.getBlock:返回父亲block,而不是函数block
- op.getBody:这个是返回内部 block / region
遍历儿子的方法:
- op.walk:递归地遍历所有子孙op:
// 递归遍历所有儿子 func.walk([](Operation * child) { // do something }); // 递归遍历所有是 `ReturnOp` 类型的儿子 func.walk([](ReturnOp ret) { // do something })
- block:直接就是一个 iterator,可以直接遍历:
Block * block = xxx for(auto & item: *block) { // do something }
还有一些其他的遍历方式,后面再实验。
控制流图的修改主要用 OpBuilder
完成。常用的函数:
- builder.create:创建op
- builder.insert:插入remove的op
- op->remove():从当前块移除,但不删除,可以插入到其他块内
- op->erase():从当前块移除,并且删除
删除顺序:在删除一个 op 的时候,这个 op 不能存在 user,否则会报错。
6 基本的 Dialect 工程
使用 tablegen 定义自己的 dialect,使用 mlir 自带的通用程序入口 MlirOptMain
,生成 toy-opt
。
这只是一个简单的工程模板,toy-opt
只能识别 Op 的 generic 格式。但先不着急,我们先构建出工程的骨架,在往上不断添加 feature。
%c = "toy.add"(%a, %b): (i32, i32) -> i32 // 可以读取
%c = toy.add %a, %b : i32 // 无法读取
6.1 TableGen 工程模板
文件结构:
ex3-dialect
├── CMakeLists.txt # 控制其他各个部分的 CMakeList
├── include
│ └── toy
│ ├── CMakeLists.txt # 控制 Dialect 定义的 CMakeList
│ ├── ToyDialect.h # Dialect 头文件
│ ├── ToyDialect.td # Dialect TableGen 文件
│ ├── ToyOps.h # Op 头文件
│ ├── ToyOps.td # Op TableGen 文件
│ └── Toy.td # 把 ToyDialect.td 和 ToyOps.td include 到一起,用于 tablegen
├── lib
│ ├── CMakeLists.txt
│ └── toy.cpp # Dialect library
└── tools
└── toy-opt
├── CMakeLists.txt
└── toy-opt.cpp # Executable Tool