??? 現在我們已經熟悉 Toy 語言和它的AST表示,現在讓我們看看 MLIR 是怎樣幫助編譯 Toy 源程序的。
簡介:多層中間表示
??????? 其他的編譯器,像 LLVM,是提供一個固定的預定義類型和指令(通常是底層的像 RISC的指令)。對于一個給定的語言,在發出 LLVM IR之前,執行任何的語言特定的類型檢查和分析,或者變換等,是由編譯器前端來決定的。例如Clang 將會使用它的 AST 做靜態分析 和 變換,例如 通過對 AST 的克隆和重寫完成C++ 模版實例化。最后,一個比 C/C++ 還高的層級上且帶有構造特性的語言,從它們的 AST 到生成 LLVM IR 可能需要做一個非常重要的下降。因此,多種前端,導致需要重新實現大量的基礎設施部件,以便能夠支持這些前端的分析和轉換。MLIR 通過把 MLIR 設計得具有可擴展性,來應對這個問題。例如,MLIR 中幾乎沒有預定義的指令(用MLIR的術語,這叫做 operations 操作)和預定義的類型。
與 MLIR 的接口
MLIR 語言參考手冊:https://mlir.llvm.org/docs/LangRef/
??????? MLIR 被設計成為一個完全可伸縮的基礎設施,這里沒有封閉的屬性、操作或類型的集合。MLIR 通過 Dialects這個概念來實現其可擴展性。Dialects 為唯一的命名空間下的抽象提供一組機制。
??????? 在MLIR中,Operations 是進行抽象和計算的核心單元,這在很多方面都挺像 LLVM 中的 instructions。 Operations 可以具有應用特定的語義,也可以用來表示 LLVM 中所有的核心的 IR 結構:instruction、globals(類似 functions)、modules等等。
下面是 Toy 語言中的 transpose operations 的 MLIR 匯編:
?
%t_tensor = "toy.transpose"(%tensor) {inplace = true} : (tensor<2x3xf64>) -> tensor<3x2xf64> loc("example/file/path":12:1)
「注:這里只是聲明了transpose操作,具體的實現要在 Chapter5中通過 lowering 來下降到其進一步的實現細節」?
讓我們來解析一下這個 MLIR operations:
. %t_tensor
給這個 operation 的結果所定義的名字(為了避免沖突,它包含一個前綴)。一個 operation 可能定義一個或者多個結果(在 Toy 的上下文中,我們將限制在不超過一個結果的 operations),這些結果都是 SSA 值。這個名字將會在解析時被使用,但是并不是持久的(這個 SSA 值的內存表示不會被追蹤)。
. "toy.transpose"
operation 的名字。在其中“.”前面的前綴所定義的命名空間中,這個字符串被期待是獨一無二的。這個寫法可以讀作:toy dialect 中的 transpose operation。
. (%tensor)
0個或多個輸入的操作數(或者參數)構成的列表,它們是由其他的 operations 或者對參數 block 的引用所定義的 SSA 值。
. { inplace = true }
0個或多個屬性構成的字典,它們是特殊的操作數,永遠是常數。這里我們定義了一個boolean 類型的命名為 inplace 的屬性,它具有常數值 true。
. (tensor<2x3xf64>) -> tensor<3x2xf64>
這個代表了用函數形式表示的操作類型,圓括號中拼寫出了參數的類型,后邊跟著返回值的類型。
. loc("example/file/path":12:1)
這是這個操作的定義開頭在源碼中的位置。
這里展示了一個操作的通常的形式。如上所述,MLIR 中的操作是可擴展的。我們使用了一個最小組的概念來建模 operations,使得 operations 可以被推導和一般性的修改。這些概念如下:
A name for the operation.
A list of SSA operand values.
A list of attributes.
A list of types for result values.
A source location for debugging purposes.
A list of successors blocks (for branches, mostly).
A list of regions (for structural operations like functions).
??????? 在 MLIR中,每一個 operation 都有一個強制性的源代碼位置與其關聯。與 LLVM 不同,debug 使用的 位置信息是元數據,可以背拋棄,在MLIR中,位置信息是一個核心要求,并且又一些API是依賴這些信息的,而且可以修改它們。所以,丟棄位置信息是一個顯式的選擇,它不會因為失誤而發生。
??????? 這里提供一個說明:如果一個變換中替換了一個 operation,那么,新的 operation 必須也附帶有位置信息。這使得追蹤這個 operation 的出處成為可能。
??????? 值得注意的 mlir-tool 這個工具——它是用來測試 編譯器 passes 的工具——在其輸出中,默認并不包含位置信息。而 -mlir-print-debuginfo 這個標志可以指定其輸出中包含位置信息。(運行 mlir-opt --help 可以看到更多的選項)
不透明的 API
??????? MLIR 被設計成為可以允許一切 IR 元素,例如,自定義的屬性,operations,和類型。同時, IR 元素可以總是規約為上述這些基本概念。如此一來,這就允許 MLIR 去對任意 operation 進行解析,表示和遍歷 IR。例如,我們可以把我們的toy 的? operation 從上方移進一個 .mlir 文件,并且通過 mlir-opt 遍歷它,這里不需要注冊任何與 toy 語言關聯的 dialect:
func.func @toy_func(%tensor: tensor<2x3xf64>) -> tensor<3x2xf64> {%t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>return %t_tensor : tensor<3x2xf64>
}
??????? 在 屬性、operations和類型在沒有被注冊的情況下, MLIR 將會強制一些結構性的約束(例如,dominance 等等),但是在其他方面,它們就是完全不透明的。例如,MLIR 關于一個未注冊的 operation 在如下情況中,只知道很少的信息:這個 operation 是否可以作用在一些特別的數據類型上,這個 operation 可以接收多少個操作數,這個 operation 可以產生多少個結果。這種靈活性對于引導性的目的是有用的,但是它通常不建議在成熟的系統中使用。未注冊的 operations 在變換和分析時必須保守對待,并且它們在構造和修改上非常困難。
??????? 通過精心設計什么是給Toy語言的非法的IR,并且通過觀察它不走檢測器時的遍歷,這個處理的意境可以被觀察到:
?
func.func @main() {%0 = "toy.print"() : () -> tensor<2x3xf64>
}
??????? 這里有多個問題:toy.print operation 不是一個終結操作;它應該接收一個操作數;而且,它不應該返回任何值。在下一節中,我們會使用 MLIR 注冊我們 dialect 和 operations,安裝進 verifier中,并且添加更好的 APIs 來修改我們的 operations。
定義 Toy 的一個 Dialect
為了與 MLIR 有效地聯系,我們 將會定義一個新的 Toy dialect。這個 dialect 將會對 Toy 語言的結構進行建模,同時為高級層次的分析和變換提供一個容易的途徑。
/// This is the definition of the Toy dialect. A dialect inherits from
/// mlir::Dialect and registers custom attributes, operations, and types. It can
/// also override virtual methods to change some general behavior, which will be
/// demonstrated in later chapters of the tutorial.
class ToyDialect : public mlir::Dialect {
public:explicit ToyDialect(mlir::MLIRContext *ctx);/// Provide a utility accessor to the dialect namespace.static llvm::StringRef getDialectNamespace() { return "toy"; }/// An initializer called from the constructor of ToyDialect that is used to/// register attributes, operations, types, and more within the Toy dialect.void initialize();
};
??????? 上面為 dialect 的 C++ 定義方式,但是 MLIR 也支持通過 tablegen 聲明性地定義 dialects。使用 td 聲明性的說明是更干凈的,因為在定義一個新的 dialect 時,它移除了對大量樣板文件的需要。它也使得 dialect 文檔的生成變得更容易,它可以直接在 dialect 的旁邊做描述。在這種 聲明性的格式中,toy 的 dialect 應該如下這樣做說明:
// Provide a definition of the 'toy' dialect in the ODS framework so that we
// can define our operations.
def Toy_Dialect : Dialect {// The namespace of our dialect, this corresponds 1-1 with the string we// provided in `ToyDialect::getDialectNamespace`.let name = "toy";// A short one-line summary of our dialect.let summary = "A high-level dialect for analyzing and optimizing the ""Toy language";// A much longer description of our dialect.let description = [{The Toy language is a tensor-based language that allows you to definefunctions, perform some math computation, and print results. This dialectprovides a representation of the language that is amenable to analysis andoptimization.}];// The C++ namespace that the dialect class definition resides in.let cppNamespace = "toy";
}
??????? 為了看到這會生成什么內容,我們可以運行 mlir-tblgen 命令,帶上? gen-dialect-decls 功能,像這樣:
${build_root}/bin/mlir-tblgen -gen-dialect-decls \
${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td \
-I ${mlir_src_root}/include/
在 dialect 被定義之后,它現在可以被加載進一個 MLIRContext之中:
?
context.loadDialect<ToyDialect>();
默認的話,一個 MLIRContext 只能加載一個 Builtin Dialect,它提供一些核心的 IR 組件,也就是說,其他的 dialects,例如我們自己定義的 Toy dialect,必須被顯式地加載才行。
定義 Toy operations
現在我們已經擁有一個 Toy dialect,我們可以開始定義 operations 了。這將允許提供寓意信息,以便系統的其余部分可以連接進去。作為示例,讓我們一起瀏覽一遍 toy.constant operation 的創建。這個 operation 將會在 Toy 語言中表示一個常數值。
%4 = "toy.constant"() {value = dense<1.0> : tensor<2x3xf64>} : () -> tensor<2x3xf64>
這個 operation 接收0個操作數,一個稠密的元素構成的屬性,稱之為 value,用來表示常熟值,并且返回一個 RankedTensorType 類型的結果。這個操作繼承自 CRTP mlir::Op 類,它還有幾個可選的 trait 屬性來定義它的行為。 Traits 是一個機制,可以用它向 operation 注入額外的行為,例如額外的 訪問器,驗證和其他行為。關于上邊我們講到的常量 operation,讓我們一起看看下邊這個可能的定義:
class ConstantOp : public mlir::Op</// `mlir::Op` is a CRTP class, meaning that we provide the/// derived class as a template parameter.ConstantOp,/// The ConstantOp takes zero input operands.mlir::OpTrait::ZeroOperands,/// The ConstantOp returns a single result.mlir::OpTrait::OneResult,/// We also provide a utility `getType` accessor that/// returns the TensorType of the single result.mlir::OpTrait::OneTypedResult<TensorType>::Impl> {public:/// Inherit the constructors from the base Op class.using Op::Op;/// Provide the unique name for this operation. MLIR will use this to register/// the operation and uniquely identify it throughout the system. The name/// provided here must be prefixed by the parent dialect namespace followed/// by a `.`.static llvm::StringRef getOperationName() { return "toy.constant"; }/// Return the value of the constant by fetching it from the attribute.mlir::DenseElementsAttr getValue();/// Operations may provide additional verification beyond what the attached/// traits provide. Here we will ensure that the specific invariants of the/// constant operation are upheld, for example the result type must be/// of TensorType and matches the type of the constant `value`.LogicalResult verifyInvariants();/// Provide an interface to build this operation from a set of input values./// This interface is used by the `builder` classes to allow for easily/// generating instances of this operation:/// mlir::OpBuilder::create<ConstantOp>(...)/// This method populates the given `state` that MLIR uses to create/// operations. This state is a collection of all of the discrete elements/// that an operation may contain./// Build a constant with the given return type and `value` attribute.static void build(mlir::OpBuilder &builder, mlir::OperationState &state,mlir::Type result, mlir::DenseElementsAttr value);/// Build a constant and reuse the type from the given 'value'.static void build(mlir::OpBuilder &builder, mlir::OperationState &state,mlir::DenseElementsAttr value);/// Build a constant by broadcasting the given 'value'.static void build(mlir::OpBuilder &builder, mlir::OperationState &state,double value);
};
???????? 然后,我們可以在 ToyDialect 的 initializer 函數中注冊這個 operation:
?
void ToyDialect::initialize() {addOperations<ConstantOp>();
}
Op 與 Operation: 使用 MLIR Operations
??????? 現在我們已經定義了一個 operation,我們想要訪問并變換它。 在 MLIR 中,這里有兩個主要的 class 跟 operations 相關聯:Operation 和 Op。Operation 類用于一般意義上建模所有的 operations。它是不透明的,也就是說,它并不描述 特定 operation 的 properties 和 operations 的 types。相反,Operation class 提供通用的 API 給一個operation 實例。另一方面,operation 的每一個特定的類型是由 Op 的派生類來表示的。例如,ConstantOp 的表示一個0輸入和一個輸出的 operation,它總是被設置成為同樣的值。Op的派生類扮演智能指針封裝著 Operation*,提供 特定 operation 訪問器方法,同時提供operations 的 類型安全的 properties。這意味著,當我們定義我們的Toy operations 時,我們簡單地定義一個干凈的、語義用途的接口來構建和對接 Operation class。這是為什么我們的 ConstantOp 沒有定義class 的字段,這個 operation 的所有數據都存儲在所引用的 Operation class 之中。這個設計的一個副作用是我們總是值傳遞 Op 的派生類,而不是傳遞引用或指針(值傳遞是MLIR的特色,同樣應用于 attributes和types等)。給定一個一般的 Operation*實例,使用 LLVM 的類型變換設施,我們總是可以得到一個特定的 Op 的實例:
void processConstantOp(mlir::Operation *operation) {ConstantOp op = llvm::dyn_cast<ConstantOp>(operation);// This operation is not an instance of `ConstantOp`.if (!op)return;// Get the internal operation instance wrapped by the smart pointer.mlir::Operation *internalOperation = op.getOperation();assert(internalOperation == operation &&"these operation instances are the same");
}
使用 ODS Framework
使用 Operation Definition Specification (ODS) Framework
出了可以特化 mlir::Op 模版,MLIR 還支持使用聲明性的方式定義 operations。這是通過 Operation Definition Specificaiton framework 做到的。
也就是把一個 operaiton 通過一個簡潔的 TableGen 紀錄來特化,在編譯的時候,它將被展開成為等價的 mlir::Op C++模版的特化。
使用 ODS framework 是在 MLIR 中定義operations的提倡的方式,因為這很簡單,簡潔,同時對接 C++ API 的變化表現是穩定的。
讓我們一起看看我們 ConstantOp 的等價的 ODS 定義:
使用 ODS時, operations 是通過繼承 Op class 來定義的。為了簡化我們的 operation 定義,我們將在 Toy dialect中定義一個 operation的基類(因為它們都是 toy dialect 中的 operation,所以這本身是一個共性,故可以存在一個基類。比如它們都會被注冊進 同一個 toy dialect)。
?
// Base class for toy dialect operations. This operation inherits from the base
// `Op` class in OpBase.td, and provides:
// * The parent dialect of the operation.
// * The mnemonic for the operation, or the name without the dialect prefix.
// * A list of traits for the operation.
class Toy_Op<string mnemonic, list<Trait> traits = []> :Op<Toy_Dialect, mnemonic, traits>;
結合這個初步的定義的代碼,我們可以開始定義constant operation。
我們通過繼承上述基類 Toy_Op 來定義一個 toy operation。這里我們給 operation 提供了 mnemonic(助記符) 和一個traits 列表。
這個助記符與 Constant::OperationName 這個成員方法提供的名字相匹配,只是需要去掉 dialect 前綴: toy..
與我們的 C++ 定義少了的部分是 ZeroOperands 和 OneResult traits. 這些將會基于我們稍后定義的 arguments 和 results 字段自動推導出來。
def ConstantOp : Toy_Op<"constant"> {
}
到此為止,你可能想知道 TableGen 生成的代碼看起來會是什么樣子的。帶著 -gen-op-decls 或者 -gen-op-defs 動作,簡單地運行 mlir-tblgen 命令,具體如下所示:
?
${build_root}/bin/mlir-tblgen -gen-op-defs ${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td -I ${mlir_src_root}/include/
依賴于所選擇的動作,這將會打印出 ConstantOp class 的聲明和它的實現。比較輸出的內容和手寫的實現,對于開始使用 TableGen 是非常有益的。
定義 Arguments 和 Results
結合剛剛定義的這個 operation 的殼,我們可以給我們的 operation 提供輸入和輸出。
一個 operation 的輸入或者 arguments 可能會是 SSA 的操作數值的 attributes 或者 types。
這個結果對應到一組該 operation 產生的值的類型:
def ConstantOp : Toy_Op<"constant"> {// The constant operation takes an attribute as the only input.// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.let arguments = (ins F64ElementsAttr:$value);// The constant operation returns a single value of TensorType.// F64Tensor corresponds to a 64-bit floating-point TensorType.let results = (outs F64Tensor);
}
通過提供一個給 arguments 或 results 的名字,例如 $value, ODS 將會自動生成一個對應的訪問器:DenseElementsAttr ConstantOp::value().
添加文檔
定義操作的下一個步驟是文檔化它。Operations 可以提供 summary 和 description 字段來描述這個 operation 的語義。
這些信息對于這個 dialect 的用戶是有益的,甚至可以用來自動生成 Markdown 文檔。
def ConstantOp : Toy_Op<"constant"> {// Provide a summary and description for this operation. This can be used to// auto-generate documentation of the operations within our dialect.let summary = "constant operation";let description = [{Constant operation turns a literal into an SSA value. The data is attachedto the operation as an attribute. For example:%0 = "toy.constant"(){ value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }: () -> tensor<2x3xf64>}];// The constant operation takes an attribute as the only input.// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.let arguments = (ins F64ElementsAttr:$value);// The generic call operation returns a single value of TensorType.// F64Tensor corresponds to a 64-bit floating-point TensorType.let results = (outs F64Tensor);
}
驗證操作的語義
到此為止,我們已經覆蓋了原先的 C++ 的 operation 的定義的大部分內容。下一個需要定義的部分是 verifier。
幸運的是,很想剛才命名的訪問器,ODS framework 將會基于我們給定的約束自動生成一大堆必要的驗證邏輯。
這意味著我們不需要驗證返回類型的結構,甚至輸入的 attribute value。在大多數情況下,對于 ODS operations,額外的驗證是不需要的。
添加額外的 驗證邏輯,一個 operation 可以重載 verifier 字段。這個 verifier 字段允許定義一個? C++ 代碼塊,作為ConstantOp::verify的一部分來運行。
這個額外的代碼塊可以假設這個 operation 的所有其他的不變量都已經檢查過了:
def ConstantOp : Toy_Op<"constant"> {// Provide a summary and description for this operation. This can be used to// auto-generate documentation of the operations within our dialect.let summary = "constant operation";let description = [{Constant operation turns a literal into an SSA value. The data is attachedto the operation as an attribute. For example:%0 = "toy.constant"(){ value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }: () -> tensor<2x3xf64>}];// The constant operation takes an attribute as the only input.// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.let arguments = (ins F64ElementsAttr:$value);// The generic call operation returns a single value of TensorType.// F64Tensor corresponds to a 64-bit floating-point TensorType.let results = (outs F64Tensor);// Add additional verification logic to the constant operation. Setting this bit// to `1` will generate a `::llvm::LogicalResult verify()` declaration on the// operation class that is called after ODS constructs have been verified, for// example the types of arguments and results. We implement additional verification// in the definition of this `verify` method in the C++ source file.let hasVerifier = 1;
}
附加 build 方法
跟起初 C++ 定義 operation 的例子中相比,最后還缺少的組件是 build methods。
ODS 可以自動地產生一些簡單的 build 方法,而且在本例子中,它將會產生我們的第一個 build 方法。其余的情況,我們通過定義 builders 字段來定義。這個字段接收一系列 OpBuilder 對象,這些對象會對應地接受一個字符串,作為C++ 參數列表的一個部分,同時可選的 代碼塊可以用來指定這個 builder 的內聯實現。
def ConstantOp : Toy_Op<"constant"> {...// Add custom build methods for the constant operation. These methods populate// the `state` that MLIR uses to create operations, i.e. these are used when// using `builder.create<ConstantOp>(...)`.let builders = [// Build a constant with a given constant tensor value.OpBuilder<(ins "DenseElementsAttr":$value), [{// Call into an autogenerated `build` method.build(builder, result, value.getType(), value);}]>,// Build a constant with a given constant floating-point value. This builder// creates a declaration for `ConstantOp::build` with the given parameters.OpBuilder<(ins "double":$value)>];
}
指定自定義的匯編格式
到此為止,我們可以產生我們的 “Toy IR”。例如如下代碼:
# User defined generic function that operates on unknown shaped arguments.
def multiply_transpose(a, b) {return transpose(a) * transpose(b);
}def main() {var a<2, 3> = [[1, 2, 3], [4, 5, 6]];var b<2, 3> = [1, 2, 3, 4, 5, 6];var c = multiply_transpose(a, b);var d = multiply_transpose(b, a);print(d);
}
可以產生如下的 IR:
module {"toy.func"() ({^bb0(%arg0: tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":4:1), %arg1: tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":4:1)):%0 = "toy.transpose"(%arg0) : (tensor<*xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:10)%1 = "toy.transpose"(%arg1) : (tensor<*xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25)%2 = "toy.mul"(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25)"toy.return"(%2) : (tensor<*xf64>) -> () loc("test/Examples/Toy/Ch2/codegen.toy":5:3)}) {sym_name = "multiply_transpose", type = (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>} : () -> () loc("test/Examples/Toy/Ch2/codegen.toy":4:1)"toy.func"() ({%0 = "toy.constant"() {value = dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>} : () -> tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:17)%1 = "toy.reshape"(%0) : (tensor<2x3xf64>) -> tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:3)%2 = "toy.constant"() {value = dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64>} : () -> tensor<6xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:17)%3 = "toy.reshape"(%2) : (tensor<6xf64>) -> tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:3)%4 = "toy.generic_call"(%1, %3) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":11:11)%5 = "toy.generic_call"(%3, %1) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":12:11)"toy.print"(%5) : (tensor<*xf64>) -> () loc("test/Examples/Toy/Ch2/codegen.toy":13:3)"toy.return"() : () -> () loc("test/Examples/Toy/Ch2/codegen.toy":8:1)}) {sym_name = "main", type = () -> ()} : () -> () loc("test/Examples/Toy/Ch2/codegen.toy":8:1)
} loc(unknown)
??????? 有一個事情需要注意,我們所有的 Toy operations 都是使用通用的匯編個是打印的。這種格式跟本章開頭分析 toy.transpose 的時候顯示的一樣。MLIR 允許 operations 定義它們獨有的匯編格式,或者是聲明式的,或者是通過 C++ 命令式的定義。用戶自定義的匯編格式允許裁剪調一些通用 IR,使其變得更可讀,這通常是刪除一些通用格式中需要的一些次要的枝椏。讓我們一起瀏覽一個我們將會簡化其 operation 格式的例子。
toy.print
當前的 toy.print 的歌是有點冗長。這里有許多額外的字符是我們想要剝離掉的。讓我們先想一下更好的 toy.print 的格式應該是什么樣的,然后再看看我們如何實現它。看著基本的 toy.print 的格式,我們得到:
toy.print %5 : tensor<*xf64> loc(...)
這里我們已經剝離掉了不是很有必要的成分,而且變得更加可讀了。為了提供一個自定義的匯編格式,一個 operation 可以用 C++ 重寫 hasCustomAssemblyFormat 字段,或者重寫聲明式的 assemblyFormat 字段(tableGen)。我們先看看 C++ 的變體,因為這是聲明式的格式內部映射成的樣子。
/// Consider a stripped definition of `toy.print` here.
def PrintOp : Toy_Op<"print"> {let arguments = (ins F64Tensor:$input);// Divert the printer and parser to `parse` and `print` methods on our operation,// to be implemented in the .cpp file. More details on these methods is shown below.let hasCustomAssemblyFormat = 1;
}
一個 C++ 實現的 printer 和 parser如下所示:
/// The 'OpAsmPrinter' class is a stream that will allows for formatting
/// strings, attributes, operands, types, etc.
void PrintOp::print(mlir::OpAsmPrinter &printer) {printer << "toy.print " << op.input();printer.printOptionalAttrDict(op.getAttrs());printer << " : " << op.input().getType();
}/// The 'OpAsmParser' class provides a collection of methods for parsing
/// various punctuation, as well as attributes, operands, types, etc. Each of
/// these methods returns a `ParseResult`. This class is a wrapper around
/// `LogicalResult` that can be converted to a boolean `true` value on failure,
/// or `false` on success. This allows for easily chaining together a set of
/// parser rules. These rules are used to populate an `mlir::OperationState`
/// similarly to the `build` methods described above.
mlir::ParseResult PrintOp::parse(mlir::OpAsmParser &parser,mlir::OperationState &result) {// Parse the input operand, the attribute dictionary, and the type of the// input.mlir::OpAsmParser::UnresolvedOperand inputOperand;mlir::Type inputType;if (parser.parseOperand(inputOperand) ||parser.parseOptionalAttrDict(result.attributes) || parser.parseColon() ||parser.parseType(inputType))return mlir::failure();// Resolve the input operand to the type we parsed in.if (parser.resolveOperand(inputOperand, inputType, result.operands))return mlir::failure();return mlir::success();
}
「注,parser 部分比較巧妙,根據實際代碼感受true-failure false-success 的效果」
??????? 結合 C++ 定義的實現,讓我們一起看看這將怎樣映射到 聲明式的格式。
??????? 聲明式的格式主要有三個不同的組件構成:
指令(Directives)
??????????????????????????????? 一類內置函數,帶有可選的一組參數。
字面量(Literals)
??????????????????????????????? 一個關鍵字或者符號,用 ‘’包裹。
變量(Variables)
??????????????????????????????? 一個實體,已經通過 operation 自身注冊過的,例如,一個參數(屬性或者操作數),結果,后繼者等。在上述 PrintOp 例子中,一個變量可以是其中的 $input.一個對應 C++ 格式的直接映射是如下這樣:
/// Consider a stripped definition of `toy.print` here.
def PrintOp : Toy_Op<"print"> {let arguments = (ins F64Tensor:$input);// In the following format we have two directives, `attr-dict` and `type`.// These correspond to the attribute dictionary and the type of a given// variable represectively.let assemblyFormat = "$input attr-dict `:` type($input)";
}
??????? 聲明格式中還有很多有趣的特性,在實現一個自定義的 C++ 格式之前,務必要先查看理解一下它們。在對我們的 operation 做了一些格式美化之后,我們現在可以得到一個更可讀的 toy IR:
module {toy.func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>) -> tensor<*xf64> {%0 = toy.transpose(%arg0 : tensor<*xf64>) to tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:10)%1 = toy.transpose(%arg1 : tensor<*xf64>) to tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25)%2 = toy.mul %0, %1 : tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25)toy.return %2 : tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:3)} loc("test/Examples/Toy/Ch2/codegen.toy":4:1)toy.func @main() {%0 = toy.constant dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:17)%1 = toy.reshape(%0 : tensor<2x3xf64>) to tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:3)%2 = toy.constant dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:17)%3 = toy.reshape(%2 : tensor<6xf64>) to tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:3)%4 = toy.generic_call @multiply_transpose(%1, %3) : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":11:11)%5 = toy.generic_call @multiply_transpose(%3, %1) : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":12:11)toy.print %5 : tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":13:3)toy.return loc("test/Examples/Toy/Ch2/codegen.toy":8:1)} loc("test/Examples/Toy/Ch2/codegen.toy":8:1)
} loc(unknown)
??????? 上述,我們介紹了在 ODS framework中定義 operations 的幾個概念,但是這里還有好幾個概念我們沒有機會涉及到:regions,variadic 操作數,等等。查看一下 全部的說明可以找到更多細節。
完整的 toy 示例
???????? 現在我們可以生成我們的 “Toy IR”。你可以構建 toyc-ch2 然后自己嘗試上邊的示例:
toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo
我們也可以檢查我們的遍歷:
toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo 2> codegen.mlir
你也應該在最終的定義文件上使用 mlir-tblgen,并且仔細研究生成的C++代碼。
??????? 到此為止,MLIR 已經知道了我們的 dialect 和 operations。在下一章中,我們將利用我們的dialect ,為 toy 語言實現一些高級的特定于語言的分析和變換。
注意
「注:根據之前的環境搭建步驟,toyc-ch2 示例中,由 mlir-tblgen 生成于如下文件夾:llvm-project/build_mlir/tools/mlir/examples/toy/Ch2/include/toy/Dialect.cpp.inc
其中 Toy_Dialect 的 td定義:
include "mlir/IR/OpBase.td"
include "mlir/IR/FunctionInterfaces.td"
include "mlir/IR/SymbolInterfaces.td"
include "mlir/Interfaces/SideEffectInterfaces.td"// Provide a definition of the 'toy' dialect in the ODS framework so that we
// can define our operations.
def Toy_Dialect : Dialect {let name = "toy";let cppNamespace = "::mlir::toy";let useFoldAPI = kEmitFoldAdaptorFolder;
}
生成指令大體上如此:
inc := -I /home/hipper/ex_mlir/tmp2/llvm-project/mlir/examples/toy/Ch2/include/toy -I/home/hipper/ex_mlir/tmp2/llvm-project/build_mlir/include -I/home/hipper/ex_mlir/tmp2/llvm-project/llvm/include -I/home/hipper/ex_mlir/tmp2/llvm-project/mlir/include -I/home/hipper/ex_mlir/tmp2/llvm-project/build_mlir/tools/mlir/include
$(inc)
input := /home/hipper/ex_mlir/tmp2/llvm-project/mlir/examples/toy/Ch2/include/toy/Ops.td
$(input)
output := tools/mlir/examples/toy/Ch2/include/toy/
$(output)
build_dir := /home/hipper/ex_mlir/tmp2/llvm-project/build_mlir
$(build_dir)[1/8] $(build_dir)/bin/mlir-tblgen -gen-dialect-decls $(inc) $(input) --write-if-changed -o $(output)/Dialect.h.inc -d $(output)/Dialect.h.inc.d
[2/8] $(build_dir)/bin/mlir-tblgen -gen-op-decls $(inc) $(input) --write-if-changed -o $(output)/Ops.h.inc -d $(output)/Ops.h.inc.d
[3/6] $(build_dir)/bin/mlir-tblgen -gen-dialect-defs $(inc) $(input) --write-if-changed -o $(output)t/Dialect.cpp.inc -d $(output)/Dialect.cpp.inc.d
[4/6] $(build_dir)/bin/mlir-tblgen -gen-op-defs $(inc) $(input) --write-if-changed -o $(output)/Ops.cpp.inc -d $(output)/Ops.cpp.inc.d
生成的 decl 代碼如下:(defs 代碼在對應的 Dialect.cpp.inc中)
namespace mlir {
namespace toy {class ToyDialect : public ::mlir::Dialect {explicit ToyDialect(::mlir::MLIRContext *context);void initialize();friend class ::mlir::MLIRContext;
public:~ToyDialect() override;static constexpr ::llvm::StringLiteral getDialectNamespace() {return ::llvm::StringLiteral("toy");}
};
} // namespace toy
} // namespace mlir
MLIR_DECLARE_EXPLICIT_TYPE_ID(::mlir::toy::ToyDialect)
」
Chapter1: 生成 toy 語言源程序的 AST
Chapter2: 能夠把AST變成SSA的MLIR Dialect IR : toy IR
Chapter3: toy IR 層的優化 opt pass
Chapter4:
Chapter5:
Chapter6:
Chapter7: