mlir toy 教程(2)

本文翻译自MLIR 官网 TOY教程,附带个人理解与批注,内容仅用于个人学习与记录

Chapter 2: Emitting Basic MLIR

Introduction: Multi-Level Intermediate Representation

多级中间表示

Other compilers, like LLVM (see the Kaleidoscope tutorial ), offer a fixed set of predefined types and (usually low-level / RISC-like) instructions. It is up to the frontend for a given language to perform any language-specific type-checking, analysis, or transformation before emitting LLVM IR. For example, Clang will use its AST to perform not only static analysis but also transformations, such as C++ template instantiation through AST cloning and rewrite. Finally, languages with construction at a higher-level than C/C++ may require non-trivial lowering from their AST to generate LLVM IR.

其它的编译器,像是LLVM等等,提供了一系列预定义好的类型和指令(如低级指令或risc指令)。
在发射为LLVM IR之前,需要通过一个语言的前端执行特定语言的类型检查,分析以及转换。例如,Clang会使用AST来执行静态分析(static analysis)和转换,例如,C++模板的实例化是用过AST的clone和重写(rewrite)完成的。最终,一个更高层次的语言(可能比C或C++更高)的语言可能需要一个non-trivial lowering,通过AST生成LLVM IR。

注:IR 就是中间表示的缩写,在编译的过程中,代码会经过frontend(前端)完成词法分析以及语法分析等操作,生成一个IR

As a consequence, multiple frontends end up reimplementing significant pieces of infrastructure to support the need for these analyses and transformation. MLIR addresses this issue by being designed for extensibility. As such, there are few pre-defined instructions (operations in MLIR terminology) or types.

多个前端最终重新实现了这些用于支持语法分析和转换的基础架构。MLIR的设计初衷是为了解决可扩展性的问题。例如,几乎没有预定义的指令或类型。

注:MLIR提供了一个公共的中间表示,比如A语言的前端和B语言的前端经过翻译后,可以得到相同的MLIR

Interfacing with MLIR

MLIR接口

MLIR is designed to be a completely extensible infrastructure; there is no closed set of attributes (think: constant metadata), operations, or types. MLIR supports this extensibility with the concept of Dialects . Dialects provide a grouping mechanism for abstraction under a unique namespace.

MLIR被设计为一个完全可扩展的基础架构;其不存在封闭的属性(常量元数据),操作或类型。MLIR通过方言(Dialects)的概念来支持它的可扩展性。方言在唯一的一个命名空间下提供了一组抽象的机制。

In MLIR, Operations are the core unit of abstraction and computation, similar in many ways to LLVM instructions. Operations can have application-specific semantics and can be used to represent all of the core IR structures in LLVM: instructions, globals (like functions), modules, etc.

Here is the MLIR assembly for the Toy transpose operations:

在MLIR中,操作是抽象与计算的核心单元,在很多方面与LLVM的结构类似。操作可以用于特定应用的语音信息,并且表示所有LLVM的核心IR结构,如指令,全局变量(如函数)以及模块等等。
下面时MLIR中Toy语言transpose操作的汇编代码(assembly)。

%t_tensor = "toy.transpose"(%tensor) {inplace = true} : (tensor<2x3xf64>) -> tensor<3x2xf64> loc("example/file/path":12:1)

Let’s break down the anatomy of this MLIR operation:
下面剖析一下这段代码:

%t_tensor
The name given to the result defined by this operation (which includes a prefixed sigil to avoid collisions ). An operation may define zero or more results (in the context of Toy, we will limit ourselves to single-result operations), which are SSA values. The name is used during parsing but is not persistent (e.g., it is not tracked in the in-memory representation of the SSA value).

给出该操作所定义的结果的名称(包括前缀符号以避免冲突)。一个操作可能可能会定义0个或者多个结果(在Toy的上下文中,我们将会限制结果为单结果操作),即一个SSA值。该名称将在parsing的过程中使用,但不是持久的操作。(例如,未在SSA值的内存表示中被跟踪)。

注:SSA,为静态单一赋值static single assignment的缩写 ,是LLVM IR的形式,简单描述就是每个SSA形式的变量都只进行一次赋值,使用这样的表示方式可以进行编译上的优化。具体可参看链接中的文章
SSA静态单一赋值

"toy.transpose"
The name of the operation. It is expected to be a unique string, with the namespace of the dialect prefixed before the “.”. This can be read as the transpose operation in the toy dialect.
操作的名称。该操作的字符串应该是唯一的,在方言命名空间以”."开头。可以理解成在toy方言中的transpose操作。

(%tensor)
A list of zero or more input operands (or arguments), which are SSA values defined by other operations or referring to block arguments.
一个包含0个或多个操作(或者是参数)的列表,它们是由其他操作定义或引用到基本块参数的SSA值。

{ inplace = true }
A dictionary of zero or more attributes, which are special operands that are always constant. Here we define a boolean attribute named ‘inplace’ that has a constant value of true.
一个包含0个或者多个属性的字典,这些属性是常量的特殊操作数。这里定义了一个布尔值的属性,叫做inplace。

(tensor<2x3xf64>) -> tensor<3x2xf64>
This refers to the type of the operation in a functional form, spelling the types of the arguments in parentheses and the type of the return values afterward.
以函数的形式表示操作的类型,括号中表示输入数据的参数类型,箭头后面是返回值类型。

注:2x3xf64表示一个2x3的张量,数据是float64类型的

loc(“example/file/path”:12:1)
This is the location in the source code from which this operation originated.
此操作再代码中的位置

Shown here is the general form of an operation. As described above, the set of operations in MLIR is extensible. Operations are modeled using a small set of concepts, enabling operations to be reasoned about and manipulated generically. These concepts are:
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中的一组操作是可扩展的。 使用少量概念对操作进行建模,从而可以对操作进行一般性的推理和操纵。 这些概念是:

  • 操作的名称
  • SSA操作数值的列表
  • 属性列表
  • 结果值类型的列表
  • 原代码的位置(用来调试)
  • 后继block列表(多数用于分支判断)
  • region列表(用于结构化操作,比如函数)

In MLIR, every operation has a mandatory source location associated with it. Contrary to LLVM, where debug info locations are metadata and can be dropped, in MLIR, the location is a core requirement, and APIs depend on and manipulate it. Dropping a location is thus an explicit choice which cannot happen by mistake.
在MLIR中,每个操作都有与其强关联的原位置。与LLVM相反,调试信息位置元数据,并且可以被删除。在MLIR中,位置是核心的需求,API依赖位置信息并对其进行操控。因此,丢弃位置信息是一个明确的选择,且不会发生错误。

注:这段话好迷惑

To provide an illustration: If a transformation replaces an operation by another, that new operation must still have a location attached. This makes it possible to track where that operation came from.

举例说明:如果一个转换将一个操作替换成了另一个,那么新的操作必须有一个附加的位置信息。这样才能追踪操作的来源。

It’s worth noting that the mlir-opt tool - a tool for testing compiler passes - does not include locations in the output by default. The -mlir-print-debuginfo flag specifies to include locations. (Run mlir-opt --help for more options.)

值得注意的是,在默认情况下,mlir-opt工具在输出时不包括位置。mlir输出的调试信息会指定到包含的位置。

Opaque API

非透明API

MLIR is designed to allow most IR elements, such as attributes, operations, and types, to be customized. At the same time, IR elements can always be reduced to the above fundamental concepts. This allows MLIR to parse, represent, and round-trip IR for any operation. For example, we could place our Toy operation from above into an .mlir file and round-trip through mlir-opt without registering any dialect:

MLIR的旨在允许自定义大多数的IR组件,如属性,操作以及类型。同时,IR组件可以简化成上述的基本概念。这使得MLIR为任何操作进行解析、表示以及往返IR(round-trip)。例如,我们可以将我们的Toy语言的operation放在一个.mlir文件中,并且通过mlir-opt实现往返,而且不需要注册任何的方言(dialect)。

注:往返round-trip 是什么概念?

func @toy_func(%tensor: tensor<2x3xf64>) -> tensor<3x2xf64> {
  %t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>
  return %t_tensor : tensor<3x2xf64>
}

In the cases of unregistered attributes, operations, and types, MLIR will enforce some structural constraints (SSA, block termination, etc.), but otherwise they are completely opaque. For instance, MLIR has little information about whether an unregistered operation can operate on particular datatypes, how many operands it can take, or how many results it produces. This flexibility can be useful for bootstrapping purposes, but it is generally advised against in mature systems. Unregistered operations must be treated conservatively by transformations and analyses, and they are much harder to construct and manipulate.

对于未注册的属性,操作和类型,MLIR将强制执行某些结构性约束(SSA,block termination等),否则它们是完全不透明的。 例如,MLIR几乎没有关于未注册的操作是否可以对特定数据类型进行操作,其可以采用多少操作数或产生多少结果的信息。 这种灵活性可用于引导目的,但通常建议不要在成熟的系统中使用。 未注册的操作必须通过转换和分析来保守地对待,并且它们的构造和操纵要困难得多。

注:就是未注册的属性,操作和类型MLIR会添加一些初始信息,但是这样做不推荐

This handling can be observed by crafting what should be an invalid IR for Toy and seeing it round-trip without tripping the verifier:

该种处理可以通过在没有触发verifier的情况下,实现一个无效的IR并观察它round-trip时被观察到:

func @main() {
  %0 = "toy.print"() : () -> tensor<2x3xf64>
}

There are multiple problems here: the toy.print operation is not a terminator; it should take an operand; and it shouldn’t return any values. In the next section, we will register our dialect and operations with MLIR, plug into the verifier, and add nicer APIs to manipulate our operations.

这里存在很多问题,toy.print不是一个terminator;操作应该有一个操作数;它不能返回任何值。
在下一个部分,我们将会使用MLIR注册我们的dialect和操作,嵌入verifier并添加一个更好的API实现控制我们的operation。

注:terminator应该理解成LLVM中的终端指令的概念,表示一个基本语句块结束(block)

Defining a Toy Dialect

定义一个Toy方言

To effectively interface with MLIR, we will define a new Toy dialect. This dialect will model the structure of the Toy language, as well as provide an easy avenue for high-level analysis and transformation.

为了有效的与MLIR进行交互,我们将定义一个新的Toy方言。这个方言将对Toy语言的结构进行建模,并且提供一个捷径用来进行高层的分析和转换。

/// This is the definition of the Toy dialect. A dialect inherits from
/// 这里定义了一个Toy方言,继承自mlir::Dialect类并注册了自定义属性,操作和类型(在构造函数中)
/// mlir::Dialect and registers custom attributes, operations, and types (in its
/// constructor). It can also override virtual methods to change some general
/// ToyDialect同样重载了一些虚函数来改变一些通用的行为,这些行为将在后面的教程中展示。
/// 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. This is used by
  /// several utilities.
  static llvm::StringRef getDialectNamespace() { return "toy"; }
};

The dialect can now be registered in the global registry:

该方言可以被注册在全局的注册表中。

  mlir::registerDialect<ToyDialect>();

注:没在教程里面搜到这段代码所在的文件

Any new MLIRContext created from now on will contain an instance of the Toy dialect and invoke specific hooks for things like parsing attributes and types.

从现在开始创建的任何新MLIRContext都将包含Toy方言的实例,并为诸如解析属性和类型之类的事情调用特定的钩子。

**Defining Toy Operations **

定义一个Toy操作

Now that we have a Toy dialect, we can start registering operations. This will allow for providing semantic information that the rest of the system can hook into. Let’s walk through the creation of the toy.constant operation:
现在我们有了Toy方言,我们可以开始注册操作了。 这将允许提供系统其余部分可以插入的语义信息。 让我们逐步完成toy.constant操作的创建:

 %4 = "toy.constant"() {value = dense<1.0> : tensor<2x3xf64>} : () -> tensor<2x3xf64>

This operation takes zero operands, a dense elements attribute named value, and returns a single result of TensorType . An operation inherits from the CRTP mlir::Op class which also takes some optional traits to customize its behavior. These traits may provide additional accessors, verification, etc.
该operation 带有0个操作数,一个名为name的dense elements属性,返回一个TensorType。operation从CRTP mlir :: Op类继承,该类还具有一些可选的特征来自定义其行为。 这些特征可能会提供其他如访问器,验证信息等内容。

CRTP是奇异递归模板模式的缩写,dense elements的注释见①, TensorType见②

① A dense elements attribute is an elements attribute where the storage for the constant vector or tensor value has been densely packed. The attribute supports storing integer or floating point elements, with integer/index/floating element types. It also support storing string elements with a custom dialect string element type.
dense elements属性是可以用来存储常量向量或者张量值的属性,其中数据以密集存储的形式存在(densely packed) 属性支持存储int和float元素,也可存储字符串

*②Values with tensor type represents aggregate N-dimensional data values, and have a known element type. It may have an unknown rank (indicated by ) or may have a fixed rank with a list of dimensions. Each dimension may be a static non-negative decimal constant or be dynamically determined (indicated by ?).
tensor type类型数据使用n纬度数据加上一个已知类型表示。tensor type数据的纬度可能是未知的,或者是用列表形式表示出来的固定纬度。每个纬度可能是静态的非负十进制常量或者是动态的纬度(用?表示) ->例子 tensor<? x ? x 13 x ? x f32>

class ConstantOp : public mlir::Op<ConstantOp,
                     /// The ConstantOp takes no inputs.
                     /// const op没有输入
                     mlir::OpTrait::ZeroOperands,
                     /// The ConstantOp returns a single result.
                     /// const op 有一个返回值
                     mlir::OpTrait::OneResult,
                     /// The result of getType is `Type`.
                     /// 返回值类型是Type
                     mlir::OpTraits::OneTypedResult<Type>::Impl> {

 public:
  /// Inherit the constructors from the base Op class.
  /// 从base Op 继承的构造函数
  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.
  /// 给Op提供唯一的名字。mlir用这个名字进行注册该op并唯一标识。
  static llvm::StringRef getOperationName() { return "toy.constant"; }

  /// Return the value of the constant by fetching it from the attribute.
  /// 通过属性获取获取并返回常量的值。
  mlir::DenseElementsAttr getValue();

  /// Operations can provide additional verification beyond the traits they
  /// define. Here we will ensure that the specific invariants of the constant
  /// operation are upheld, for example the result type must be of TensorType.
  /// op可以提供超出定义traits的额外verification。这里我们保证const op的不变特性可以支持。例如结果类型一定是一个TensorType
  LogicalResult verify();

  /// Provide an interface to build this operation from a set of input values.
  /// 提供一个接口从一系列输入值中来建立op
  /// This interface is used by the builder to allow for easily generating
  /// instances of this operation:
  /// 这个接口通过builder调用,使其可以容易的生成该op的实例。
  ///   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.
  /// 该方法填充MLIR用于创建给定的'state' op。该state是一系列由一个op包含的离散元素。
  /// Build a constant with the given return type and `value` attribute.
  /// 建立一个const 带有给定的返回类型的和'value'属性。
  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'.
  /// 创建一个const 并重复使用给定'value'的类型。
  static void build(mlir::OpBuilder &builder, mlir::OperationState &state,
                    mlir::DenseElementsAttr value);
  /// Build a constant by broadcasting the given 'value'.
  /// 创建一个const 通过广播给定'value'的方式
  static void build(mlir::OpBuilder &builder, mlir::OperationState &state,
                    double value);
};

and we register this operation in the ToyDialect constructor:
我们在ToyDialect构造函数里面注册这个op

ToyDialect::ToyDialect(mlir::MLIRContext *ctx)
    : mlir::Dialect(getDialectNamespace(), ctx) {
  addOperations<ConstantOp>();
}

**Op vs Operation: Using MLIR Operations **

Op vs Operation:使用MLIR的操作符号

Now that we have defined an operation, we will want to access and transform it. In MLIR, there are two main classes related to operations: Operation and Op. The Operation class is used to generically model all operations. It is ‘opaque’, in the sense that it does not describe the properties of particular operations or types of operations. Instead, the ‘Operation’ class provides a general API into an operation instance. On the other hand, each specific type of operation is represented by an Op derived class. For instance ConstantOp represents a operation with zero inputs, and one output, which is always set to the same value. Op derived classes act as smart pointer wrapper around a Operation*, provide operation-specific accessor methods, and type-safe properties of operations. This means that when we define our Toy operations, we are simply defining a clean, semantically useful interface for building and interfacing with the Operation class. This is why our ConstantOp defines no class fields; all the data structures are stored in the referenced Operation. A side effect is that we always pass around Op derived classes by value, instead of by reference or pointer (passing by value is a common idiom and applies similarly to attributes, types, etc). Given a generic Operation* instance, we can always get a specific Op instance using LLVM’s casting infrastructure:

现在我们定义了一个操作(operation),我们希望获得该操作的权限并可以对其进行转换。在MLIR中,主要由两个与操作相关的类:Operation类与Op类。Operation类用来对所有操作进行一般情况下的建模。从某种意义上说,它不描述特定操作或操作类型的属性,因此它是“不透明的”。取而代之的是,“ Operation”类在操作实例中提供了通用API。一方面,每个特定类型的操作可以通过一个Op派生类来表示。例如ConstantOp表示一个有0个输入信息,一个输出信息,并且永远设置成相同值的操作。Op派生类充当Operation的智能指针的封装,提供特定于操作的访问器方法以及操作的类型安全属性。这意味着当我们定义我们的Toy操作时,我们可以简单的定义一个简洁的,语义上有用的接口来构建和与Operation类连接。这也是为什么我们的ConstantOp定义了一个没有类字段的原因;所有的数据结构存储在一个引用的Operation中。有一个副作用是,我们总是按值传递Op派生类,而不是按引用或指针传递(按值传递是常见的习惯用法,并且类似地应用于属性,类型等)。给定一个通用的Operation实例,我们总是可以使用LLVM的投射基础结构来获得特定的Op实例:


注:* op和operation的关系时,operation类是各个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");
}

Using the Operation Definition Specification (ODS) Framework

使用ODS框架:

In addition to specializing the mlir::Op C++ template, MLIR also supports defining operations in a declarative manner. This is achieved via the Operation Definition Specification framework. Facts regarding an operation are specified concisely into a TableGen record, which will be expanded into an equivalent mlir::Op C++ template specialization at compile time. Using the ODS framework is the desired way for defining operations in MLIR given the simplicity, conciseness, and general stability in the face of C++ API changes.

Lets see how to define the ODS equivalent of our ConstantOp:

The first thing to do is to define a link to the Toy dialect that we defined in C++. This is used to link all of the operations that we will define to our dialect:

除了使用mlir::Op的C++模板,MLIR也支持以声明的方式定义操作。这可以通过操作定义规范框架实现。在TableGen记录中指定了关于定义的op的详细内容,这些内容将在编译时扩展为等效的mlir :: Op C ++模板专业化。在MLIR中使用ODS框架定义op是一个理想的选择,相对于C++ api容易改变的特性,ODS更加简单、简洁和稳定。
让我们看看如何使用ODS定义ConstantOp:
首先要做的是定义到我们在C ++中定义的Toy方言的链接。,用于我们将定义的所有操作链接到方言:

// Provide a definition of the 'toy' dialect in the ODS framework so that we
// can define our operations.
// 提供一个在ODS框架下toy方言的定义,我们可以定义自己的操作
def Toy_Dialect : Dialect {
  // The namespace of our dialect, this corresponds 1-1 with the string we
  // provided in `ToyDialect::getDialectNamespace`.
  // 我们自己定义的方言的命名空间,与我们提供的ToyDialect::getDialectNamespace是一一对应的
  let name = "toy";

  // The C++ namespace that the dialect class definition resides in.
  // 方言所在的c++命名空间。
  let cppNamespace = "toy";
}

Now that we have defined a link to the Toy dialect, we can start defining operations. Operations in ODS are defined by inheriting from the Op class. To simplify our operation definitions, we will define a base class for operations in the Toy dialect.

现在我们定义了一个Toy方言相关联的类,现在可以开始定义操作。在ODS框架下,操作来自于继承而来的Op类。为了简化我们的操作定义,我们定义一个基类,用来给Toy方言提供operation。

// 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<OpTrait> traits = []> :
    Op<Toy_Dialect, mnemonic, traits>;

With all of the preliminary pieces defined, we can begin to define the constant operation.
定义了所有初步组件之后,我们就可以开始定义常量操作了。

We define a toy operation by inheriting from our base ‘Toy_Op’ class above. Here we provide the mnemonic and a list of traits for the operation. The mnemonic here matches the one given in ConstantOp::getOperationName without the dialect prefix; toy… Missing here from our C++ definition are the ZeroOperands and OneResult traits; these will be automatically inferred based upon the arguments and results fields we define later.

我们通过继承上面的“ Toy_Op”类来定义toy方言操作。 在这里,我们为操作提供了助记符(mnemonic)和特征列表(traits)。 这里的助记符匹配在ConstantOp :: getOperationName中给定的助记符,但没有dialect前缀toy。 在我们的C ++定义中缺少的是ZeroOperands和OneResult特性; 这些将根据我们稍后定义的参数和结果字段自动推断。

def ConstantOp : Toy_Op<"constant"> {
}

At this point you probably might want to know what the C++ code generated by TableGen looks like. Simply run the mlir-tblgen command with the gen-op-decls or the gen-op-defs action like so:
使用如下命令可以得到TableGen得到的C++代码:

${build_root}/bin/mlir-tblgen -gen-op-defs ${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td -I ${mlir_src_root}/include/

Depending on the selected action, this will print either the ConstantOp class declaration or its implementation. Comparing this output to the hand-crafted implementation is incredibly useful when getting started with TableGen.

根据选择的操作,这将打印ConstantOp类声明或其实现。使用TableGen在对比输出和手工实现的代码结果比较有用。

Defining Arguments and Results

定义参数和结果

With the shell of the operation defined, we can now provide the inputs and outputs to our operation. The inputs, or arguments, to an operation may be attributes or types for SSA operand values. The results correspond to a set of types for the values produced by the operation:

定义了操作的外壳之后,我们现在可以为操作提供输入和输出。 操作的输入或参数可以是SSA操作数值的属性或类型。 结果对应于该操作产生的值的一组类型:

def ConstantOp : Toy_Op<"constant"> {
  // The constant operation takes an attribute as the only input.
  // `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.
  // ins表示输入,F64ElementsAttr表示输入数据是类型,value是参数名
  let arguments = (ins F64ElementsAttr:$value);

  // The constant operation returns a single value of TensorType.
  // F64Tensor corresponds to a 64-bit floating-point TensorType.
  // 返回一个F64Tensor类型
  let results = (outs F64Tensor);
}

By providing a name to the arguments or results, e.g. $value, ODS will automatically generate a matching accessor: DenseElementsAttr ConstantOp::value().

通过为参数或结果提供名称,例如 $value,ODS将自动生成一个匹配的访问器(accessor):DenseElementsAttr ConstantOp :: value()。

**Adding Documentation **

添加文档

The next step after defining the operation is to document it. Operations may provide summary and description fields to describe the semantics of the operation. This information is useful for users of the dialect and can even be used to auto-generate Markdown documents.

定义操作后的下一步是对其进行记录。 操作可以提供摘要和描述字段,以描述操作的语义。 该信息对方言用户很有用,甚至可以用于自动生成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 attached
    to 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);
}

**Verifying Operation Semantics **

验证操作语义

At this point we’ve already covered a majority of the original C++ operation definition. The next piece to define is the verifier. Luckily, much like the named accessor, the ODS framework will automatically generate a lot of the necessary verification logic based upon the constraints we have given. This means that we don’t need to verify the structure of the return type, or even the input attribute value. In many cases, additional verification is not even necessary for ODS operations. To add additional verification logic, an operation can override the verifier field. The verifier field allows for defining a C++ code blob that will be run as part of ConstantOp::verify. This blob can assume that all of the other invariants of the operation have already been verified:

至此,我们已经涵盖了大多数原始C ++操作定义。 下一个要定义的部分是验证器。 幸运的是,与命名访问器一样,ODS框架将根据我们给出的约束条件自动生成许多必要的验证逻辑。 这意味着我们不需要验证返回类型的结构,甚至不需要验证输入属性的值。 在许多情况下,ODS操作甚至不需要额外的验证。 要添加其他验证逻辑,操作可以覆盖验证者字段。 验证程序字段允许定义一个C ++代码Blob,它将作为ConstantOp :: verify的一部分运行。 此Blob可以假定该操作的所有其他不变量已被验证:

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 attached
    to 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. Here we invoke
  // a static `verify` method in a C++ source file. This codeblock is executed
  // inside of ConstantOp::verify, so we can use `this` to refer to the current
  // operation instance.
  let verifier = [{ return ::verify(*this); }];
}

**Attaching build Methods **

添加build方法:

The final missing component here from our original C++ example are the build methods. ODS can generate some simple build methods automatically, and in this case it will generate our first build method for us. For the rest, we define the builders field. This field takes a list of OpBuilder objects that take a string corresponding to a list of C++ parameters, as well as an optional code block that can be used to specify the implementation inline.

我们原始的C ++示例中最后缺少的组件是build方法。 ODS可以自动生成一些简单的构建方法,在这种情况下,它将为我们生成我们的第一个build方法。 对于其余部分,我们定义builder字段。 该字段包含一个OpBuilder对象列表,该对象包含一个与C ++参数列表相对应的字符串,以及一个可用于指定内联实现的可选代码块。

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.
    OpBuilderDAG<(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.
    OpBuilderDAG<(ins "double":$value)>
  ];
}

注:OpBuilderDAG,DAG这里应该是指有向无环图的含义,在编译原理中会使用DAG对语句的基本块进行优化。根据这些理论的猜测,应该是该Op执行完成后,后面不会引用该变量(没有从该节点指向另一个节点的边)

Specifying a Custom Assembly Format

指定一个定制化的程序集格式

At this point we can generate our “Toy IR”. For example, the following:
我们可以生成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 {
  func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>) -> tensor<*xf64> {
    %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)
  } loc("test/Examples/Toy/Ch2/codegen.toy":4:1)
  func @main() {
    %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)
  } loc("test/Examples/Toy/Ch2/codegen.toy":8:1)
} loc(unknown)

One thing to notice here is that all of our Toy operations are printed using the generic assembly format. This format is the one shown when breaking down toy.transpose at the beginning of this chapter. MLIR allows for operations to define their own custom assembly format, either declaratively or imperatively via C++. Defining a custom assembly format allows for tailoring the generated IR into something a bit more readable by removing a lot of the fluff that is required by the generic format. Let’s walk through an example of an operation format that we would like to simplify.

这里要注意一个问题,我们的Toy 语言的operation 使用的是通用程序集格式。这个输出格式,输出的格式是当toy.transpose这个operation停止后得到的结果。MLIR允许自定义一个程序输出的格式,要么使用C++进行声明要么进行定义。定义一个自定义的程序及格式可以去掉通用格式中的大量冗余信息,将生成的IR简化为更加可读的形式。现在看一个例子。

toy.print

The current form of toy.print is a little verbose. There are a lot of additional characters that we would like to strip away. Let’s begin by thinking of what a good format of toy.print would be, and see how we can implement it. Looking at the basics of toy.print we get:

toy.print %5 : tensor<*xf64> loc(...)

当前的toy.print有点冗长。有不少可以去掉的信息。可以考虑把print变成一个更加合理的程序格式,基本的toy.print信息是上面那样
Here we have stripped much of the format down to the bare essentials, and it has become much more readable. To provide a custom assembly format, an operation can either override the parser and printer fields for a C++ format, or the assemblyFormat field for the declarative format. Let’s look at the C++ variant first, as this is what the declarative format maps to internally.
这里把大仅保留最重要的信息,使其更加可读。一个定制化的程序格式,可以有两种实现方案,一种是可以使用C++重载parser和printer的域,另一种是assemblyFormat 域的声明性格式。先看看C++的变体,这是声明性格式在内部映射的内容。

注:有两种方法自定义输出的IR形式,一种是自己重载parser和printer,另一种是利用assemblyFormat

/// Consider a stripped definition of `toy.print` here.
def PrintOp : Toy_Op<"print"> {
  let arguments = (ins F64Tensor:$input);

  // Divert the printer and parser to static functions in our .cpp
  // printer和parser转换为c++ 的静态函数
  // file that correspond to 'print' and 'printPrintOp'. 'printer' and 'parser'
  // here correspond to an instance of a 'OpAsmParser' and 'OpAsmPrinter'. More
  // details on these classes is shown below.
  // 注意上面的$符号
  let printer = [{ return ::print(printer, *this); }];
  let parser = [{ return ::parse$cppClass(parser, result); }];
}

A C++ implementation for the printer and parser is shown below:
c++实现的printer和parser如下

/// The 'OpAsmPrinter' class is a stream that will allows for formatting
/// strings, attributes, operands, types, etc.
/// 没啥好说的,和c++重载输出流一样
static void print(mlir::OpAsmPrinter &printer, PrintOp op) {
  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.
/// OpAsmParaser类提供了很多解析各种符号的方法,如属性,操作数,类型等待,
/// 这些方法返回一个ParserResult类型。该类型可以转换成布尔类型。
static mlir::ParseResult parsePrintOp(mlir::OpAsmParser &parser,
                                      mlir::OperationState &result) {
  // Parse the input operand, the attribute dictionary, and the type of the
  // input.
  mlir::OpAsmParser::OperandType 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.
  // 将输入操作数解析为我们parse的类型
  if (parser.resolveOperand(inputOperand, inputType, result.operands))
    return mlir::failure();

  return mlir::success();
}

With the C++ implementation defined, let’s see how this can be mapped to the declarative format . The declarative format is largely composed of three different components:

Directives
    A type of builtin function, with an optional set of arguments.
Literals
    A keyword or punctuation surrounded by ``.
Variables
    An entity that has been registered on the operation itself, i.e. an argument(attribute or operand), result, successor, etc. In the PrintOp example above, a variable would be $input.

A direct mapping of our C++ format looks something like:

定义了C ++实现后,让我们看看如何将其映射为声明性格式。 声明性格式主要由三个不同部分组成:
指令
一种内置函数,带有一组可选参数。
字面量
被``包围的关键字或标点符号。
变量
在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)";
}

The declarative format has many more interesting features, so be sure to check it out before implementing a custom format in C++. After beautifying the format of a few of our operations we now get a much more readable:

声明性格式具有许多有趣的功能,因此请确保在使用C ++实现自定义格式之前先进行检查。 在美化了一些operation的格式后,得到了一个可读性更好的IR

module {
  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)
  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)

Above we introduce several of the concepts for defining operations in the ODS framework, but there are many more that we haven’t had a chance to: regions, variadic operands, etc. Check out the full specification for more details.

上面我们介绍了几种在ODS框架中定义操作的概念,但是还有许多我们没有机会去做的概念:region,可变参数操作数等。有关更多详细信息,请查看完整的规范。

完整Toy 例子:

We can now generate our “Toy IR”. You can build toyc-ch2 and try yourself on the above example: toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo. We can also check our RoundTrip: toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo 2> codegen.mlir followed by toyc-ch2 codegen.mlir -emit=mlir. You should also use mlir-tblgen on the final definition file and study the generated C++ code.

生成Toy IR,可以使用toy2 ch2种的代码,得到上面的结果,编译的方法为
toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo
我们还可以检查RoundTrip:
toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo 2> codegen.mlir,后面跟着toyc-ch2 codegen.mlir -emit=mlir命令。
也可以使用mlir-tblgen在最后定义的文件上使用,并研究生成的c++代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值