深入解析 MLIR Toy Tutorial(Chapter 2):AST to IR

概述

MLIR Toy Tutorial 的目标是通过构建一门编程语言编译器的完整过程(包括前端和后端技术),教授如何使用 MLIR 的各个组件来实现语言的解析、转换和代码生成等功能。

Chapter2 介绍了如何为 Toy 语言开发 MLIR 方言(Dialect),并将 Chapter1 生成的 AST 转换成 MLIR IR。
源码:https://github.com/llvm/llvm-project/tree/main/mlir/examples/toy/Ch2

AST -> IR

// Ch2/toyc.cpp@dumpMLIR()
mlir::MLIRContext context;
context.getOrLoadDialect<mlir::toy::ToyDialect>();

auto moduleAST = parseInputFile(inputFilename);
mlir::OwningOpRef<mlir::ModuleOp> module = mlirGen(context, *moduleAST);
module->dump();

Ch2 的 example 演示了如何将基于 toy 语言写的源码转换成 MLIR IR:先通过 parseInputFile() 构建源码的抽象语法树 AST,再通过 mlirGen() 将 AST 转换成方言 mlir::toy::ToyDialect 定义的 IR,最后将 IR 打印出来(AST 的生成过程可以参考: 深入解析 MLIR Toy Tutorial(Chapter 1))。

方言 Dialect

MLIR 是一个设计完全可扩展的基础设施,它的可扩展性体现在 IR 的各个元素,包括 operations、types 和 attributes 等都是可以进行扩展的。这种可扩展性是通过方言(dialect)来实现的,方言为 IR 提供了一种通过 namespace 分组的机制,可以赋予操作(operation)新的语义,实现自定义的行为。

在 IR 中,如果想要为操作赋予新的语义以实现自定义行为,通常需要添加新的属性或者重新定义输入输出。这时,IR 的可扩展性就变得非常重要。

举例来说,考虑一个 matmul 算子,如果想要为它添加一个属性以进行后端图优化,就需要查看该算子的 attributes 字段是否可扩展,以及 op builder 是否允许传入该属性。只有两者都支持,才能满足需求。否则,通常需要创建一个自定义的 matmul 算子,并将原有的 matmul 算子替换为自定义算子。

而在 MLIR 中,你可以定义一个特定的方言,比如 toy dialect,然后在该方言中定义一个 matmul 算子,这样就可以获得 toy.matmul 算子。在图优化过程中,可以根据方言的不同对算子进行分层优化,使用不同方言的转换来实现多层次的优化。

MLIR 提供了许多内置的 Dialects,可以组合使用它们,以满足在不同语义下对操作进行图优化的需求。通过利用方言的能力,MLIR 实现了灵活且可扩展的编译器基础设施。

定义 Toy 方言

方言可以被理解为一组操作(op),因此定义方言就意味着定义操作。操作的定义通常包括以下内容:

  • 定义操作的静态信息,包括输入输出、属性和类型等,这些信息描述了操作的语义。
  • 创建操作的构造方法,用于创建一个操作并将其添加到IR中。
  • 创建操作的实现方法,用于在IR执行时调用。

MLIR提供了一种领域特定语言(DSL),使得我们可以通过声明的方式描述和定义操作的输入输出、属性、类型和行为。利用MLIR提供的 tablegen 工具(mlir-tblgen),我们可以基于 Operation Definition Specification(ODS)框架自动生成操作类的声明和实现代码。这种声明式的定义风格使得操作的定义更加清晰易懂,我们只需要关注操作的语义定义即可。

在Ch2中,我们分别在 include/toy/Ops.tdmlir/Dialect.cpp 中定义了 add、mul、call 等操作的静态信息和方法。以 call 操作(GenericCallOp)为例,我们可以进一步分析如何创建 MLIR 的操作(op):

def Toy_Dialect : Dialect {
  let name = "toy";
  let cppNamespace = "::mlir::toy";
} 

class Toy_Op<string mnemonic, list<Trait> traits = []> :
    Op<Toy_Dialect, mnemonic, traits>;

//===----------------------------------------------------------------------===//
// GenericCallOp
//===----------------------------------------------------------------------===//

def GenericCallOp : Toy_Op<"generic_call"> {
  let summary = "generic call operation";
  let description = [{
    ......
  }];

  // The generic call operation takes a symbol reference attribute as the
  // callee, and inputs for the call.
  let arguments = (ins FlatSymbolRefAttr:$callee, Variadic<F64Tensor>:$inputs);

  // The generic call operation returns a single value of TensorType.
  let results = (outs F64Tensor);

  // Specialize assembly printing and parsing using a declarative format.
  let assemblyFormat = [{
    $callee `(` $inputs `)` attr-dict `:` functional-type($inputs, results)
  }];

  // Add custom build methods for the generic call operation.
  let builders = [
    OpBuilder<(ins "StringRef":$callee, "ArrayRef<Value>":$arguments)>
  ];
}

首先,我们需要创建一个方言(Toy_Dialect)和一个基类操作(Toy_Op)。GenericCallOp 是继承自 Toy_Op 的操作,它属于 toy 方言。

然后,我们定义操作(op)的具体内容:

  • arguments 和 results:定义操作的输入和输出。
  • builders:操作的构造方法,用于创建操作并将其添加到 IR 中。
  • assemblyFormat:定义操作在打印时的文本格式,这在对 IR 进行输出时非常有用。这里的 `` 表示双引号。

在编译时,我们使用 mlir-tblgen 工具生成操作类的声明和实现代码。生成的代码位于 llvm-project/build/tools/mlir/examples/toy/Ch2/include/toy/*.inc 文件中,并被 mlir/Dialect.cpp 引用。这些文件是通过使用 -gen-dialect-decls、-gen-op-decls 和 -gen-op-defs 参数生成的。例如:mlir-tblgen -gen-dialect-decls llvm-project/mlir/examples/toy/Ch2/include/toy/Ops.td -Illvm-project/mlir/include

浏览这些 *.inc 文件,我们可以看到生成的内容不仅包括操作类和一些通用方法,还包括辅助类(如 OpAdaptor、OpGenericAdaptor 等),用于简化操作的使用和参数传递。借助 tablegen 的辅助功能,我们只需在 Dialect.cpp 中定义少量的方法,就能完成操作的定义:

// mlir/Dialect.cpp

//===----------------------------------------------------------------------===//
// GenericCallOp
//===----------------------------------------------------------------------===//

void GenericCallOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
                          StringRef callee, ArrayRef<mlir::Value> arguments) {
  // Generic call always returns an unranked Tensor initially.
  state.addTypes(UnrankedTensorType::get(builder.getF64Type()));
  state.addOperands(arguments);
  state.addAttribute("callee",
                     mlir::SymbolRefAttr::get(builder.getContext(), callee));
}

GenericCallOp::build() 用于在 IR 中构建 call op。

mlirGen(): AST -> IR

在完成方言的定义之后,下一步是将抽象语法树(AST)转换为中间表示(IR)。让我们回顾一下 AST 的层次结构:

  • 程序(ModuleAST)由函数(FunctionAST)组成。
  • 函数由原型(PrototypeAST)和代码块(block,即花括号中的内容)组成。
  • 原型由函数名和参数列表组成。
  • 代码块由表达式列表(ExprASTList)组成。

IR 的转换是通过 mlirGen() 函数完成的,它会遍历 AST,并根据 AST 节点生成相应的操作(op)。例如,ModuleAST 会被转换为 ModuleOp,FunctionAST 会被转换为 FuncOp,CallExprAST 会被转换为 GenericCallOp 等。

我们以 GenericCallOp 为例,来看看它是如何进行转换的:

  /// Emit a call expression. It emits specific operations for the `transpose`
  /// builtin. Other identifiers are assumed to be user-defined functions.
  mlir::Value mlirGen(CallExprAST &call) {
    llvm::StringRef callee = call.getCallee();
    auto location = loc(call.loc());

    // Codegen the operands first.
    SmallVector<mlir::Value, 4> operands;
    for (auto &expr : call.getArgs()) {
      auto arg = mlirGen(*expr);
      if (!arg)
        return nullptr;
      operands.push_back(arg);
    }

    ......

    // Otherwise this is a call to a user-defined function. Calls to
    // user-defined functions are mapped to a custom call that takes the callee
    // name as an attribute.
    return builder.create<GenericCallOp>(location, callee, operands);
}

在转换过程中,mlirGen() 函数从 AST 的调用节点中提取了它在源码中的位置信息(location)、被调用函数名(callee)以及要传递的参数(operands)。然后,通过调用 builder.create<GenericCallOp>(location, callee, operands) 创建了调用操作,并将其添加到 IR 中。

location 是创建 MLIR IR 所必需的元素,它在错误报告、调试和优化等方面提供了更准确和有用的信息,这是 MLIR 与其他编译器的区别之一。

mlir::Value 表示操作的输入和输出值,它是一种通用的值类型,可以表示多种不同的数据类型,例如标量、向量和张量等。mlirGen() 函数通过递归的方式找到输入的叶子节点(LiteralExprAST),并为其构建 ConstantOp,从而生成相应的 mlir::Value。

Module dump

在完成 IR 转换后,Ch2 示例通过调用 module->dump() 将 IR 打印出来。这个过程涉及到操作(op)的 parse()print() 方法的调用。使用 tablegen 生成的操作类不仅包含声明和实现,还生成了一些通用的方法,用于校验、打印和转换等操作。也就是说,parse() 和 print() 方法默认会由 tablegen 自动生成。如果在操作定义中声明了 let hasCustomAssemblyFormat = 1,则需要在 Dialect.cpp 中自己实现这些方法。

总结

Chapter 2 介绍了如何为 Toy 语言开发 MLIR 方言,并将生成的抽象语法树(AST)转换为 MLIR 中的自定义方言。方言允许为操作赋予新的语义,扩展了 IR 的功能。使用声明的方式定义操作,并通过 tablegen 自动生成基于Operation Definition Specification (ODS) 框架的操作类的声明和实现。最后通过 mlirGen() 将 AST 转换为 MLIR IR。

END

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容