前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MLIR入门教程2-基本MLIR表示

MLIR入门教程2-基本MLIR表示

作者头像
hunterzju
发布2021-12-09 14:10:54
2.7K0
发布2021-12-09 14:10:54
举报
文章被收录于专栏:编译器开发

第2章:发射基本MLIR

现在我们已经熟悉了我们的语言和AST,让我们看看MLIR如何帮助编译Toy。

简介:多级中间表示法

其他编译器,如llvm(参见Kaleidcope tutorial),])提供了一组固定的预定义类型和(通常是低级/risc-like)指令。在发出LLVM IR之前,由给定语言的前端执行任何特定于语言的类型检查、分析或转换。例如,Clang将不但使用其AST执行静态分析,还执行转换,例如通过AST克隆和重写进行C++模板实例化。最后,具有比C/C++更高级别结构的语言可能需要从其AST降低很多,才能生成LLVM IR。

因此,多个前端最终重新实现重要的基础设施,以支持这些分析和转换的需求。MLIR通过为可扩展性而设计,从而解决了这个问题。因此,预定义的指令(MLIR术语中的operations)或类型很少。

与MLIR接口

语言参考

MLIR被设计为一个完全可扩展的基础设施;没有封闭的属性集(想一想:常量元数据)、操作或类型。MLIR通过方言(Dialects)的概念支持这种可扩展性。方言提供了一种分组机制,用于在唯一的“命名空间”下进行抽象。

在MLIR中,Operations是抽象和计算的核心单元,在很多方面类似于LLVM指令。操作可以具有特定于应用程序的语义,并且可以用来表示LLVM中的所有核心IR结构:指令、全局变量(如函数)、模块等。

以下是toytransspose操作的MLIR程序集:

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

让我们来剖析一下这个MLIR操作:

  • %t_tensor
  • 为此操作定义的结果指定的名称(包括前缀符号以避免碰撞collisions)。一个操作可以定义零个或多个结果(在Toy的上下文中,我们将自己限制为单结果操作),它们是SSA值。该名称在解析期间使用,但不是持久的(例如,在SSA值的内存表示中不跟踪该名称)。
  • "toy.transpose"
  • 操作的名称。它应该是唯一的字符串,在“.”之前加上方言的命名空间。这可以理解为toy方言中的transspose操作。
  • (%tensor)
  • 零个或多个输入操作数(或参数)的列表,它们是由其他操作定义或指向Block参数的SSA值。
  • {inplace=true}
  • 一种包含零个或多个属性的字典,这些属性是始终恒定的特殊操作数。在这里,我们定义了一个名为‘inplace’的布尔属性,该属性的常量值为true。
  • (tensor<2x3xf64>) -> tensor<3x2xf64>
  • 这指的是函数形式中的操作类型,即括号中的参数类型和随后返回值的类型。
  • loc("example/file/path":12:1)
  • 这是源代码中发起此操作的位置。

这里显示的是操作的一般形式。如上所述,MLIR中的操作集是可扩展的。使用一小组概念对操作进行建模,从而能够对操作进行一般的推理和操作。这些概念是:

  • 操作的名称。
  • SSA操作数值的列表。
  • 属性列表attributes
  • 结果值的类型列表。
  • 用于调试目的的源码位置
  • 后继列表(主要用于分支)。
  • 区域Regions列表(用于函数等结构化操作)。

在MLIR中,每个操作都有一个与之关联的必需的源码位置。这与LLVM相反,在LLVM中,调试信息源码位置是元数据,可以删除,而在MLIR中,位置是核心需求,API依赖并操作它。因此,丢弃位置是一种显示的选择,不能误删。

举例说明:如果转换将一个操作替换为另一个操作,则该新操作必须仍附加有位置。这使得追踪操作的来源成为可能。

值得注意的是,mlir-opt工具-用于测试编译器通道的工具-默认情况下不包括输出中的位置。-mlir-print-debuginfo标志指定包含位置。(更多选项请运行mlir-opt --help。)

Opaque API

MLIR旨在允许自定义大多数IR元素,如属性、操作和类型。同时,IR元素始终可以归结为上述基本概念。这使得对于任何操作,允许MLIR解析、表示和round-tripIR。例如,我们可以将上面的Toy操作放到一个.mlir文件中,并在mlir-opt中遍历,而无需注册任何方言:

代码语言:javascript
复制
func @toy_func(%tensor: tensor<2x3xf64>) -> tensor<3x2xf64> {
  %t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>
  return %t_tensor : tensor<3x2xf64>
}

对于未注册的属性、操作和类型,MLIR将强制执行一些结构约束(ssa、Block终止等),但在其他情况下,它们是完全不透明的。例如,MLIR几乎没有关于未注册操作是否可以操作特定数据类型、可以接受多少操作数或产生多少结果的信息。这种灵活性对于引导目的(bootstrapping purposes)很有用,但在成熟的系统中通常不建议这样做。未注册的操作必须通过转换和分析保守地对待,而且它们更难构造和操作。

这种情况可以通过为Toy构造无效的IR,并在不触发验证器的情况下在round trip中查看它来验证:

代码语言:javascript
复制
func @main() {
  %0 = "toy.print"() : () -> tensor<2x3xf64>
}

这里有多个问题:toy.print操作不是终止符;它应该接受一个操作数;并且它不应该返回任何值。在下一节中,我们将使用MLIR注册我们的方言和操作,插入验证器,并添加更好的API来操作我们的operations。

定义toy方言

为了有效地与MLIR交互,我们将定义一个新的toy方言。这种方言将模拟toy语言的结构,并为高级分析和转换提供一条简单的途径。

代码语言:javascript
复制
/// This is the definition of the Toy dialect. A dialect inherits from
/// mlir::Dialect and registers custom attributes, operations, and types (in its
/// constructor). 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. This is used by
  /// several utilities.
  static llvm::StringRef getDialectNamespace() { return "toy"; }
};

现在可以在全局注册表中注册该方言:

代码语言:javascript
复制
mlir::registerDialect<ToyDialect>();

从现在开始创建的任何新的MLIRContext都将包含toy方言的一个实例,并调用特定的钩子hooks来解析属性和类型。

定义toy操作

既然我们有了toy dialect,我们就可以开始注册操作了。这将允许提供语义信息给剩余系统调用。下面我们来介绍一下toy.constant操作的创建过程:

代码语言:javascript
复制
%4 = "toy.constant"() {value = dense<1.0> : tensor<2x3xf64>} : () -> tensor<2x3xf64>

该操作的操作数为零,dense elements属性名为value,返回TensorType的单个结果。操作继承自CRTPmlir::op类,该类还需要一些可选的特性Traits来自定义其行为。这些特征可以提供额外的访问器、验证等功能。

代码语言:javascript
复制
class ConstantOp : public mlir::Op<ConstantOp,
                     /// The ConstantOp takes no inputs.
                     mlir::OpTrait::ZeroOperands,
                     /// The ConstantOp returns a single result.
                     mlir::OpTrait::OneResult,
                     /// The result of getType is `Type`.
                     mlir::OpTraits::OneTypedResult<Type>::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.
  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.
  LogicalResult verify();

  /// Provide an interface to build this operation from a set of input values.
  /// This interface is used by the builder 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);
};

并且我们在Toy Dialect构造函数中注册此操作:

代码语言:javascript
复制
ToyDialect::ToyDialect(mlir::MLIRContext *ctx)
    : mlir::Dialect(getDialectNamespace(), ctx) {
  addOperations<ConstantOp>();
}

Op VS Operation:使用MLIR操作

既然我们已经定义了一个操作,我们将需要访问和转换它。在MLIR中,与操作相关的类主要有两个:OperationOpOperation类用于对所有操作进行通用建模。它是“不透明的”,因为它没有描述特定操作或操作类型的属性。相反,“Operation”类为操作实例提供了一个通用API。另一方面,每种特定类型的操作都由一个Op派生类表示。例如,ConstantOp表示零输入、一输出的操作,始终设置为相同的值。Op派生类充当operation*的智能指针包装器,提供特定于操作的访问器方法,以及操作的类型安全属性。这意味着当我们定义toy操作时,我们只是定义了一个干净的、语义上有用的接口,用于构建Operation类并与其交互。这就是为什么我们的ConstantOp没有定义类字段;所有的数据结构都存储在引用的Operation中。一个副作用是,我们总是通过值传递Op派生类,而不是通过引用或指针(按值传递是一种常见的习惯用法,类似于属性、类型等)。给定一个通用的operation*实例,我们始终可以使用LLVM的强制转换基础设施获取具体的Op实例:

代码语言:javascript
复制
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)框架

MLIR除了专门化mlir::opC++模板外,还支持声明式定义操作。这是通过操作定义规范框架实现的。关于操作的事实被简明地指定到TableGen记录中,该记录将在编译时展开为等效的mlir::Op专用C++模板。考虑到面对C++API更改时的简洁性、简明性和一般稳定性,使用ODS框架是在MLIR中定义操作的理想方式。

让我们看看如何定义ConstantOp的ODS等效项:

要做的第一件事是定义一个指向我们用C++定义的toy方言的链接。它用于将我们将定义的所有操作链接到我们的方言:

代码语言:javascript
复制
// 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";

  // The C++ namespace that the dialect class definition resides in.
  let cppNamespace = "toy";
}

现在我们已经定义了到toy方言的链接,我们可以开始定义操作了。ODS中的操作是通过继承Op类来定义的。为了简化我们的操作定义,我们将用toy方言为操作定义一个基类

代码语言:javascript
复制
// 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>;

定义了所有的初始部分后,我们可以开始定义常量操作。

我们通过继承上面的“Toy_Op”基类来定义toy操作。在这里,我们提供了助记符和操作的特征列表。这里的mnemonicConstantOp::getOperationName中给出的没有方言前缀toy.匹配。我们的C++定义中缺少ZeroOperandsOneResult特性;这些特性将根据我们稍后定义的argumentsResults字段自动推断出来。

代码语言:javascript
复制
def ConstantOp : Toy_Op<"constant"> {
}

此时,您可能想知道TableGen生成的C++代码是什么样子。只需使用Gen-op-declsGen-op-defs操作运行mlir-tblgen命令,如下所示:

代码语言:javascript
复制
${build_root}/bin/mlir-tblgen -gen-op-defs ${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td -I ${mlir_src_root}/include/

根据选择的操作,这将打印ConstantOp类声明或其实现。在开始使用TableGen时,将此输出与手工创建的实现进行比较非常有用。

定义参数和结果

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

代码语言:javascript
复制
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);
}

通过给参数或结果命名,如$value,ODS会自动生成匹配的访问器:DenseElementsAttr ConstantOp::value()

添加文档

定义操作后的下一步是对其进行文档记录。操作可以提供[="https://zhuanlan.zhihu.com/OpDefinitions.md#operation-documentation">summary and description]字段来描述操作的语义。此信息对该方言的用户很有用,甚至可以用来自动生成Markdown文档。

代码语言:javascript
复制
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);
}

验证操作语义

至此,我们已经介绍了原始C++操作定义的大部分。下一个要定义的部分是验证器。幸运的是,与命名访问器非常相似,ODS框架将根据我们给出的约束自动生成大量必要的验证逻辑。这意味着我们不需要验证返回类型的结构,甚至不需要验证输入属性value。在许多情况下,对于ODS operations来说不需要额外验证。要添加其他验证逻辑,operation可以重载verifier字段。verifier字段允许定义一个C++代码blob,它将作为ConstantOp::verify的一部分运行。此BLOB可以假设该操作的所有其他不变量都已经过验证:

代码语言:javascript
复制
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); }];
}

附加build方法

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

代码语言:javascript
复制
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)>
  ];
}

指定自定义装配格式

到现在,我们可以生成我们的“toyIR”。例如,以下内容:

代码语言:javascript
复制
# 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:

代码语言:javascript
复制
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)

这里需要注意的一件事是,我们所有的Toy操作都是使用通用装配格式打印的。此格式是本章开头分解toy.transspose时显示的格式。MLIR允许操作定义它们自己的自定义程序集格式,可以是declaratively,也可以是通过C++。定义自定义程序集格式允许将生成的IR裁剪成更具可读性的内容,方法是去掉通用格式所需的大量乱七八糟的东西。让我们来演练一个我们想要简化的操作格式的示例。

toy.print

当前的toy.print形式有点冗长。我们想要去掉很多额外的字符。让我们首先考虑一下一个好的toy.print格式是什么样,然后看看如何实现它。看一下toy.print的基本内容,我们会得到:

代码语言:javascript
复制
toy.print %5 : tensor<*xf64> loc(...)

在这里,我们剥离了大部分格式,使其只剩下最基本的部分,可读性也大大提高。为了提供自定义的装配格式,操作可以重载C++格式化操作的parserprinter字段,也可以覆盖声明性格式的AssemyFormat字段。让我们首先看一下C++变体,因为这是声明性格式的内部映射方式。

代码语言:javascript
复制
/// 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
  // 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); }];
}

parserprinter的C++实现如下所示:

代码语言:javascript
复制
/// The 'OpAsmPrinter' class is a stream that will allows for formatting
/// strings, attributes, operands, types, etc.
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.
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.
  if (parser.resolveOperand(inputOperand, inputType, result.operands))
    return mlir::failure();

  return mlir::success();
}

定义了C++实现之后,让我们看看如何将其映射到declarative format.声明性格式主要由三个不同的组件组成:

  • 指令
  • 一种内置函数,具有一组可选的参数。
  • 文字
  • 用``括起来的关键字或标点符号。
  • 变量
  • 已在操作本身上注册的实体,即PrintOp中的参数(属性或操作数)、结果、后继等,在上面的PrintOp示例中,变量应该是$input

我们的C++格式的直接映射类似于:

代码语言:javascript
复制
/// 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)";
}

declarative format有更多有趣的特性,因此在用C++实现自定义格式之前一定要检查它。在美化了几个操作的格式之后,我们现在得到一个可读性更强的:

代码语言:javascript
复制
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)

上面我们介绍了几个在ODS框架中定义操作的概念,但是还有更多我们还没有机会介绍的概念:区域(regions)、各种操作数(variadic operands)等。有关更多详细信息,请查看完整规范

完整的toy示例

我们现在可以生成我们的“toyIR”了。您可以构建toyc-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-print-debuginfo 2>codegen.mlir后跟toyc-CH2 codegen.mlir -emit=mlir。您还可以对最终的定义文件使用mlir-tblgen,并研究生成的C++代码。

到现在为止,MLIR知道我们的toy方言和操作。在下一章中,我们将利用我们的新方言实现一些针对toy语言的高级语言特定分析和转换。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介:多级中间表示法
  • 与MLIR接口
    • Opaque API
    • 定义toy方言
    • 定义toy操作
      • Op VS Operation:使用MLIR操作
        • 使用操作定义规范(ODS)框架
          • 定义参数和结果
            • 添加文档
              • 验证操作语义
                • 附加build方法
                  • 指定自定义装配格式
                    • toy.print
                    • 完整的toy示例
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档