概述
MLIR Toy Tutorial 的目標(biāo)是通過構(gòu)建一門編程語言編譯器的完整過程(包括前端和后端技術(shù)),教授如何使用 MLIR 的各個(gè)組件來實(shí)現(xiàn)語言的解析峭拘、轉(zhuǎn)換和代碼生成等功能。
Chapter2 介紹了如何為 Toy 語言開發(fā) MLIR 方言(Dialect),并將 Chapter1 生成的 AST 轉(zhuǎn)換成 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 語言寫的源碼轉(zhuǎn)換成 MLIR IR:先通過 parseInputFile()
構(gòu)建源碼的抽象語法樹 AST暂衡,再通過 mlirGen()
將 AST 轉(zhuǎn)換成方言 mlir::toy::ToyDialect
定義的 IR暇屋,最后將 IR 打印出來(AST 的生成過程可以參考: 深入解析 MLIR Toy Tutorial(Chapter 1))炫隶。
方言 Dialect
MLIR 是一個(gè)設(shè)計(jì)完全可擴(kuò)展的基礎(chǔ)設(shè)施存璃,它的可擴(kuò)展性體現(xiàn)在 IR 的各個(gè)元素仑荐,包括 operations、types 和 attributes 等都是可以進(jìn)行擴(kuò)展的纵东。這種可擴(kuò)展性是通過方言(dialect)來實(shí)現(xiàn)的粘招,方言為 IR 提供了一種通過 namespace
分組的機(jī)制,可以賦予操作(operation)新的語義偎球,實(shí)現(xiàn)自定義的行為洒扎。
在 IR 中,如果想要為操作賦予新的語義以實(shí)現(xiàn)自定義行為衰絮,通常需要添加新的屬性或者重新定義輸入輸出袍冷。這時(shí),IR 的可擴(kuò)展性就變得非常重要猫牡。
舉例來說胡诗,考慮一個(gè) matmul
算子,如果想要為它添加一個(gè)屬性以進(jìn)行后端圖優(yōu)化淌友,就需要查看該算子的 attributes
字段是否可擴(kuò)展煌恢,以及 op builder
是否允許傳入該屬性。只有兩者都支持亩进,才能滿足需求症虑。否則,通常需要?jiǎng)?chuàng)建一個(gè)自定義的 matmul 算子归薛,并將原有的 matmul 算子替換為自定義算子谍憔。
而在 MLIR 中,你可以定義一個(gè)特定的方言主籍,比如 toy dialect习贫,然后在該方言中定義一個(gè) matmul 算子,這樣就可以獲得 toy.matmul
算子千元。在圖優(yōu)化過程中苫昌,可以根據(jù)方言的不同對算子進(jìn)行分層優(yōu)化,使用不同方言的轉(zhuǎn)換來實(shí)現(xiàn)多層次的優(yōu)化幸海。
MLIR 提供了許多內(nèi)置的 Dialects祟身,可以組合使用它們,以滿足在不同語義下對操作進(jìn)行圖優(yōu)化的需求物独。通過利用方言的能力袜硫,MLIR 實(shí)現(xiàn)了靈活且可擴(kuò)展的編譯器基礎(chǔ)設(shè)施。
定義 Toy 方言
方言可以被理解為一組操作(op)挡篓,因此定義方言就意味著定義操作婉陷。操作的定義通常包括以下內(nèi)容:
- 定義操作的靜態(tài)信息帚称,包括輸入輸出、屬性和類型等秽澳,這些信息描述了操作的語義闯睹。
- 創(chuàng)建操作的構(gòu)造方法,用于創(chuàng)建一個(gè)操作并將其添加到IR中担神。
- 創(chuàng)建操作的實(shí)現(xiàn)方法楼吃,用于在IR執(zhí)行時(shí)調(diào)用。
MLIR提供了一種領(lǐng)域特定語言(DSL)杏瞻,使得我們可以通過聲明的方式描述和定義操作的輸入輸出所刀、屬性、類型和行為捞挥。利用MLIR提供的 tablegen 工具(mlir-tblgen)浮创,我們可以基于 Operation Definition Specification(ODS)框架自動(dòng)生成操作類的聲明和實(shí)現(xiàn)代碼。這種聲明式的定義風(fēng)格使得操作的定義更加清晰易懂砌函,我們只需要關(guān)注操作的語義定義即可斩披。
在Ch2中,我們分別在 include/toy/Ops.td 和 mlir/Dialect.cpp 中定義了 add讹俊、mul垦沉、call 等操作的靜態(tài)信息和方法。以 call 操作(GenericCallOp)為例仍劈,我們可以進(jìn)一步分析如何創(chuàng)建 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)>
];
}
首先厕倍,我們需要?jiǎng)?chuàng)建一個(gè)方言(Toy_Dialect)和一個(gè)基類操作(Toy_Op)。GenericCallOp 是繼承自 Toy_Op 的操作贩疙,它屬于 toy 方言讹弯。
然后,我們定義操作(op)的具體內(nèi)容:
- arguments 和 results:定義操作的輸入和輸出这溅。
- builders:操作的構(gòu)造方法组民,用于創(chuàng)建操作并將其添加到 IR 中。
- assemblyFormat:定義操作在打印時(shí)的文本格式悲靴,這在對 IR 進(jìn)行輸出時(shí)非常有用臭胜。這里的 `` 表示雙引號(hào)。
在編譯時(shí)癞尚,我們使用 mlir-tblgen 工具生成操作類的聲明和實(shí)現(xiàn)代碼耸三。生成的代碼位于 llvm-project/build/tools/mlir/examples/toy/Ch2/include/toy/*.inc
文件中,并被 mlir/Dialect.cpp
引用浇揩。這些文件是通過使用 -gen-dialect-decls吕晌、-gen-op-decls 和 -gen-op-defs 參數(shù)生成的。例如:mlir-tblgen -gen-dialect-decls llvm-project/mlir/examples/toy/Ch2/include/toy/Ops.td -Illvm-project/mlir/include
临燃。
瀏覽這些 *.inc 文件,我們可以看到生成的內(nèi)容不僅包括操作類和一些通用方法,還包括輔助類(如 OpAdaptor膜廊、OpGenericAdaptor 等)乏沸,用于簡化操作的使用和參數(shù)傳遞。借助 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 中構(gòu)建 call op。
mlirGen(): AST -> IR
在完成方言的定義之后铆铆,下一步是將抽象語法樹(AST)轉(zhuǎn)換為中間表示(IR)蝶缀。讓我們回顧一下 AST 的層次結(jié)構(gòu):
- 程序(ModuleAST)由函數(shù)(FunctionAST)組成。
- 函數(shù)由原型(PrototypeAST)和代碼塊(block薄货,即花括號(hào)中的內(nèi)容)組成翁都。
- 原型由函數(shù)名和參數(shù)列表組成。
- 代碼塊由表達(dá)式列表(ExprASTList)組成谅猾。
IR 的轉(zhuǎn)換是通過 mlirGen() 函數(shù)完成的柄慰,它會(huì)遍歷 AST,并根據(jù) AST 節(jié)點(diǎn)生成相應(yīng)的操作(op)税娜。例如坐搔,ModuleAST 會(huì)被轉(zhuǎn)換為 ModuleOp,F(xiàn)unctionAST 會(huì)被轉(zhuǎn)換為 FuncOp敬矩,CallExprAST 會(huì)被轉(zhuǎn)換為 GenericCallOp 等概行。
我們以 GenericCallOp 為例,來看看它是如何進(jìn)行轉(zhuǎn)換的:
/// 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);
}
在轉(zhuǎn)換過程中弧岳,mlirGen() 函數(shù)從 AST 的調(diào)用節(jié)點(diǎn)中提取了它在源碼中的位置信息(location)凳忙、被調(diào)用函數(shù)名(callee)以及要傳遞的參數(shù)(operands)。然后缩筛,通過調(diào)用 builder.create<GenericCallOp>(location, callee, operands) 創(chuàng)建了調(diào)用操作消略,并將其添加到 IR 中。
location 是創(chuàng)建 MLIR IR 所必需的元素瞎抛,它在錯(cuò)誤報(bào)告艺演、調(diào)試和優(yōu)化等方面提供了更準(zhǔn)確和有用的信息,這是 MLIR 與其他編譯器的區(qū)別之一桐臊。
mlir::Value
表示操作的輸入和輸出值胎撤,它是一種通用的值類型,可以表示多種不同的數(shù)據(jù)類型断凶,例如標(biāo)量伤提、向量和張量等。mlirGen() 函數(shù)通過遞歸的方式找到輸入的葉子節(jié)點(diǎn)(LiteralExprAST)认烁,并為其構(gòu)建 ConstantOp肿男,從而生成相應(yīng)的 mlir::Value介汹。
Module dump
在完成 IR 轉(zhuǎn)換后,Ch2 示例通過調(diào)用 module->dump() 將 IR 打印出來舶沛。這個(gè)過程涉及到操作(op)的 parse()
和 print()
方法的調(diào)用嘹承。使用 tablegen 生成的操作類不僅包含聲明和實(shí)現(xiàn),還生成了一些通用的方法如庭,用于校驗(yàn)叹卷、打印和轉(zhuǎn)換等操作。也就是說坪它,parse() 和 print() 方法默認(rèn)會(huì)由 tablegen 自動(dòng)生成骤竹。如果在操作定義中聲明了 let hasCustomAssemblyFormat = 1
,則需要在 Dialect.cpp 中自己實(shí)現(xiàn)這些方法往毡。
總結(jié)
Chapter 2 介紹了如何為 Toy 語言開發(fā) MLIR 方言蒙揣,并將生成的抽象語法樹(AST)轉(zhuǎn)換為 MLIR 中的自定義方言。方言允許為操作賦予新的語義卖擅,擴(kuò)展了 IR 的功能鸣奔。使用聲明的方式定義操作,并通過 tablegen 自動(dòng)生成基于Operation Definition Specification (ODS) 框架的操作類的聲明和實(shí)現(xiàn)惩阶。最后通過 mlirGen() 將 AST 轉(zhuǎn)換為 MLIR IR挎狸。