# 前言

现在开启 MLIR 学习系列。本篇是跟着 Toy 语言学习 MLIR 的第六篇,前述章节已经完整介绍了 toy 语言经 MLIR 降级到 llvm ir,并通过 JIT 进行编译执行的过程,本章节是教程中增添复合类型章节。前述内容请参考【MLIR】跟着 Toy 语言学习 MLIR【1】Toy 语言和 Toy Dialect【MLIR】跟着 Toy 语言学习 MLIR【2】pattern 匹配和重写【MLIR】跟着 Toy 语言学习 MLIR【3】通过接口实现通用转换
相关链接: LLVM ProjectMLIR 官方文档MLIR 官网教程【编译器】使用 llvm 编译自定义语言【1】构建 AST【MLIR】跟着 Toy 语言学习 MLIR【2】pattern 匹配和重写【MLIR】跟着 Toy 语言学习 MLIR【3】通过接口实现通用转换
作为初学者,错误在所难免,还望不吝赐教。

# 基本简介

官网 Toy 教程 展示了如何将自定义语言 Toy 借助 MLIR 一步步编译为可执行机器码的过程。Toy 是一种简单的自定义语言,为了简便,其所有数据类型定义为 fp64 类型的 Tensor,支持 +/* 操作和 transpose 等有限的操作。

以下是整个编译降级的过程:

Toy txt -> Toy AST -> Toy Dialect -> Affine Dialect -> llvm Dialect -> llvm IR -> 机器码(通过 JIT 编译)

编译流程图

# 在 Toy 中定义 Struct 类型

Struct 在 Toy 语言中的定义非常简单,和 C++ 语言中的定义与初始化差不多:

# A struct is defined by using the `struct` keyword followed by a name.
struct MyStruct {  // 这里是定义
  # Inside of the struct is a list of variable declarations without initializers
  # or shapes, which may also be other previously defined structs.
  var a;
  var b;
}

这是给的 toy 语言例子,包含了 struct 的定义初始化和调用。

struct Struct {
  var a;
  var b;
}
# User defined generic function may operate on struct types as well.
def multiply_transpose(Struct value) {
  # We can access the elements of a struct via the '.' operator.
  return transpose(value.a) * transpose(value.b);
}
def main() {
  # We initialize struct values using a composite initializer.
  Struct value = {[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]};
  # We pass these arguments to functions like we do with variables.
  var c = multiply_transpose(value);
  print(c);
}

# 在 MLIR 中定义 struct

在 MLIR 中,我们也需要为结构体类型定义一种表示方式。MLIR 本身没有提供完全满足我们需求的类型,因此我们需要自行定义。
可以简单地将结构体定义为一组元素类型的无名容器。

# 定义 Type Class

当我们需要定义一个新的复杂类型(如 struct)时,需要提供自定义的存储类(Storage Class)
MLIR 中的类型 Type 是 “值类型”,Type 对象本身是一个轻量级的包装器,它不存储实际的数据,而是指向一个内部的存储对象。

// MLIR 中的 Type 类(简化)
class Type {
  TypeStorage *impl;  // 指向内部存储
  //... 方法 ...
};

举例来说,String 在 Java 中也是值类型, 在 Java 中,String 对象是不可变的,多个 String 变量可以共享同一个底层字符数组。
Type 类本身只是 TypeStorage 对象的一个简单包装器,而这个 TypeStorage 对象是在 MLIRContext 中被唯一化的。“唯一化” 意味着:同一种类型在同一个上下文中只会有一个存储实例。这种设计,节省内存:同一种类型只存一份,避免重复;快速比较:比较两个类型只需要比较指针地址,而不是逐字段比较。

Defining the Storage Class

什么时候需要定义存储类?当需要定义一个包含参数化数据的新类型(如 struct 类型,需要存储元素类型列表)时,你需要提供一个派生的存储类。而那些没有额外数据的单例类型(如 index 类型,i32 类型,f64 类型等)则不需要自定义存储类,直接使用默认的 TypeStorage。
类型存储对象包含构建和唯一化类型实例所需的所有数据。派生的存储类必须继承自基础的 mlir::TypeStorage,并提供一组别名和钩子,供 MLIRContext 用于唯一化处理。以下是结构体类型 struct 的存储实例定义,可以在 Dialect.cpp 中找到:

/// This class represents the internal storage of the Toy `StructType`.
struct StructTypeStorage : public mlir::TypeStorage {  // 继承 mlir:: TypeStorage
  /// The `KeyTy` is a required type that provides an interface for the storage
  /// instance. This type will be used when uniquing an instance of the type
  /// storage. For our struct type, we will unique each instance structurally on
  /// the elements that it contains.
  using 核心概念 = llvm::ArrayRef<mlir::Type>;  // 定义键类型  用于唯一化存储实例
  /// A constructor for the type storage instance.  
  StructTypeStorage(llvm::ArrayRef<mlir::Type> elementTypes)  // 构造函数
      : elementTypes(elementTypes) {}
  /// Define the comparison function for the key type with the current storage
  /// instance. This is used when constructing a new instance to ensure that we
  /// haven't already uniqued an instance of the given key.
  bool operator==(const KeyTy &key) const { return key == elementTypes; }  // 相等性比较,用于比较 当前存储实例 和 给定的键 是否相等
  /// Define a hash function for the key type. This is used when uniquing
  /// instances of the storage.
  /// Note: This method isn't necessary as both llvm::ArrayRef and mlir::Type
  /// have hash functions available, so we could just omit this entirely.
  static llvm::hash_code hashKey(const KeyTy &key) {  // 哈希函数 
    return llvm::hash_value(key);
  }
  /// Define a construction function for the key type from a set of parameters.
  /// These parameters will be provided when constructing the storage instance
  /// itself, see the `StructType::get` method further below.
  /// Note: This method isn't necessary because KeyTy can be directly
  /// constructed with the given parameters.
  static KeyTy getKey(llvm::ArrayRef<mlir::Type> elementTypes) {  // 键构建函数
    return KeyTy(elementTypes);
  }
  /// Define a construction method for creating a new instance of this storage.
  /// This method takes an instance of a storage allocator, and an instance of a
  /// `KeyTy`. The given allocator must be used for *all* necessary dynamic
  /// allocations used to create the type storage and its internal.
  static StructTypeStorage *construct(mlir::TypeStorageAllocator &allocator,  // 存储构造函数,MLIR 框架要求提供的一个 “工厂方法”
                                      const KeyTy &key) {
    // Copy the elements from the provided `KeyTy` into the allocator.
    llvm::ArrayRef<mlir::Type> elementTypes = allocator.copyInto(key);  // 数据成员
    // Allocate the storage instance and construct it.
    return new (allocator.allocate<StructTypeStorage>())
        StructTypeStorage(elementTypes);
  }
  /// The following field contains the element types of the struct.
  llvm::ArrayRef<mlir::Type> elementTypes;
};

这个 struct 的存储实例,是为了支持 toy 语言中的 struct 语法。有了它,toy 语言就可以定义各种结构的 struct。
keyTy 是 MLIR 的核心概念,它定义了用于唯一化存储实例(一个存储类型)的键类型。对于 StructType,唯一化的依据是它的元素类型列表,当 MLIRContext 在创建类型时,会检查是否已经存在相同键的存储实例。如果有,直接返回已存在的实例,否则创建新的。
构造函数接收元素类型列表,并将其存储到 elementTypes 成员变量中。元素类型类型列表决定了当前定义的 struct 的唯一性。
相等性比较,看起来好像是比较两个定义实例是否相同的,防止重复定义。
哈希函数:为键计算哈希值,用于在哈希表中快速定位存储实例。
键构建函数 getKey 将键列表构建为 KeyTy ,当然 KeyTy 本身就是键列表。
存储构造函数:MLIR 框架要求提供的一个 “工厂方法”,职责是 “构造存储实例”(Construct a Storage Instance)。

Defining the Type Class
定义类型。
在定义好存储类后,我们可以添加用户可见的 StructType 类的定义。这个类是我们实际进行交互的对象。

/// This class defines the Toy struct type. It represents a collection of
/// element types. All derived types in MLIR must inherit from the CRTP class
/// 'Type::TypeBase'. It takes as template parameters the concrete type
/// (StructType), the base class to use (Type), and the storage class
/// (StructTypeStorage).
class StructType : public mlir::Type::TypeBase<StructType, mlir::Type, StructTypeStorage> {
public:
  /// Inherit some necessary constructors from 'TypeBase'.
  using Base::Base;
  /// Create an instance of a `StructType` with the given element types. There
  /// *must* be at least one element type.
  static StructType get(llvm::ArrayRef<mlir::Type> elementTypes) {  // 创建 StructType 实例,通过 Base::get, 调用 StructTypeStorage::getKey (elementTypes)
    assert(!elementTypes.empty() && "expected at least 1 element type");
    // Call into a helper 'get' method in 'TypeBase' to get a uniqued instance
    // of this type. The first parameter is the context to unique in. The
    // parameters after are forwarded to the storage instance.
    mlir::MLIRContext *ctx = elementTypes.front().getContext();
    return Base::get(ctx, elementTypes);
  }
  /// Returns the element types of this struct type.
  llvm::ArrayRef<mlir::Type> getElementTypes() {  // 数据访问方法,返回类型列表
    // 'getImpl' returns a pointer to the internal storage instance.
    return getImpl()->elementTypes;
  }
  /// Returns the number of element type held by this struct.
  size_t getNumElementTypes() { return getElementTypes().size(); }  // 返回类型列表大小
};

Type 类定义遵循了 MLIR 中 CRTP(奇异递归模板模式,Curiously Recurring Template Pattern) 的设计模式。它帮助实现了一些功能,如唯一化(Uniquing)机制,类型转换(cast、dyn_cast)支持,上下文管理等。
StructType 对象是轻量级的,它们只包含一个指向 StructTypeStorage 的指针。

多个 StructType 对象可以共享同一个 StructTypeStorage 实例(如果它们的 KeyTy 相同)。

然后在 toy dialect 中注册这个自定义类型:

void ToyDialect::initialize() {
  addTypes<StructType>();
}

Exposing to ODS
当定义了一个新的类型,我们需要确保 ODS 框架能够 “看得到” 这个新类型,以便在操作定义和方言中的自动生成工具中使用它。以下是一个简单的示例:

// Provide a definition for the Toy StructType for use in ODS. This allows for
// using StructType in a similar way to Tensor or MemRef. We use `DialectType`
// to demarcate the StructType as belonging to the Toy dialect.
def Toy_StructType :
    DialectType<Toy_Dialect, CPred<"isa<StructType>($_self)">,
                "Toy struct type">;
// Provide a definition of the types that are used within the Toy dialect.
def Toy_Type : AnyTypeOf<[F64Tensor, Toy_StructType]>;

它是如何绑定到 C 代码中的自定义类型 StructType 的呢? DialectType<Toy_Dialect, ...> 指明这是一个属于 Toy_Dialect 方言的类型。CPred 是连接 ODS 和 C 的 “桥梁”,通过 CPred<"isa<StructType>($_self)"> 这个谓词(Predicate)与 C++ 中的 StructType 类绑定在一起的。 isa<StructType>($_self) 是一个 C++ 表达式,用于检查一个类型是否是 StructType

Parsing and Printing
此时,可以在 MLIR 生成和转换过程中使用 StructType,但无法输出或解析 .mlir 文件。为此,我们需要为 StructType 的实例添加解析和打印支持。这可以通过重写 ToyDialect 中的 parseType 和 printType 方法来实现。

class ToyDialect : public mlir::Dialect {
public:
  /// Parse an instance of a type registered to the toy dialect.
  mlir::Type parseType(mlir::DialectAsmParser &parser) const override;
  /// Print an instance of a type registered to the toy dialect.
  void printType(mlir::Type type,
                 mlir::DialectAsmPrinter &printer) const override;
};

根据 MLIR 语言参考文档所述,方言类型通常表示为:! dialect-namespace <type-data>,在某些情况下可提供更美观的格式。所以教程将 struct 类型的打印定义为如下形式:

struct-type ::= `struct` `<` type (`,` type)* `>`

那么解析(Parser)就可以定义为:

/// Parse an instance of a type registered to the toy dialect.
mlir::Type ToyDialect::parseType(mlir::DialectAsmParser &parser) const {  // 解析类
  // Parse a struct type in the following form:
  //   struct-type ::= `struct` `<` type (`,` type)* `>`
  // NOTE: All MLIR parser function return a ParseResult. This is a
  // specialization of LogicalResult that auto-converts to a `true` boolean
  // value on failure to allow for chaining, but may be used with explicit
  // `mlir::failed/mlir::succeeded` as desired.
  // Parse: `struct` `<`
  if (parser.parseKeyword("struct") || parser.parseLess())
    return Type();
  // Parse the element types of the struct.
  SmallVector<mlir::Type, 1> elementTypes;
  do {
    // Parse the current element type.
    SMLoc typeLoc = parser.getCurrentLocation();
    mlir::Type elementType;
    if (parser.parseType(elementType))
      return nullptr;
    // Check that the type is either a TensorType or another StructType.
    if (!isa<mlir::TensorType, StructType>(elementType)) {
      parser.emitError(typeLoc, "element type for a struct must either "
                                "be a TensorType or a StructType, got: ")
          << elementType;
      return Type();
    }
    elementTypes.push_back(elementType);
    // Parse the optional: `,`
  } while (succeeded(parser.parseOptionalComma()));
  // Parse: `>`
  if (parser.parseGreater())
    return Type();
  return StructType::get(elementTypes);
}

解析类,帮助从文本中反序列化回内存对象。
一个打印(Printer)的实现可以这样定义:

/// Print an instance of a type registered to the toy dialect.
void ToyDialect::printType(mlir::Type type, mlir::DialectAsmPrinter &printer) const {  // 打印类
  // Currently the only toy type is a struct type.
  StructType structType = type.cast<StructType>();
  // Print the struct type according to the parser format.
  printer << "struct<";
  llvm::interleaveComma(structType.getElementTypes(), printer);
  printer << '>';
}

让 MLIR 能够将 StructType 序列化为文本(如 .mlir 文件)

这是 toy 语言中的 strcut 定义:

struct Struct {
  var a;
  var b;
}
def multiply_transpose(Struct value) {
}

它将会生成:

module {
  toy.func @multiply_transpose(%arg0: !toy.struct<tensor<*xf64>, tensor<*xf64>>) {
    toy.return
  }
}

它也支持从 mlir 文本到内存对象的双向转换。

Operating on StructType
现在已经定义好了 strcut 这个类型,现在要做的是让我们 toy 方言中已有的操作支持 strcut 这个新的类型。
例如我们已经存在的操作 ReturnOp ,需要进行如下更新:

def ReturnOp : Toy_Op<"return", [Terminator, HasParent<"FuncOp">]> {
  ...
  let arguments = (ins Variadic<Toy_Type>:$input);  // 更新之前 let arguments = (ins Variadic<F64Tensor>:$input);
  ...
}

将原来的类型约束从 F64Tensor 变为了 Toy_Type ,而 Toy_Type 是个复合类型约束,它表示 F64TensorStructType
Adding New Toy Operations
除了现有操作外,还需要添加一些新的操作 Operation 来更好的处理 truct 。
struct 常量操作 toy.struct_constant。

%0 = toy.struct_constant [
    dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64>
  ] : !toy.struct<tensor<*xf64>>

其实在这里我有个疑问,为什么 "教程选择创建一个新操作 struct_constant,而不是扩展现有的 ConstantOp (复用),将其支持 struct" 呢?
问 llm,得到的答复是:解析 / 打印变得复杂、验证逻辑变得复杂、类型推导变得复杂、可读性下降。所以没有复用原来的 常量操作。

struct 访问结构体成员操作 toy.struct_access

// Using %0 from above
  %1 = toy.struct_access %0[0] : !toy.struct<tensor<*xf64>> -> tensor<*xf64>

这个是为了表示 toy 语言中 Value.a, Value.b 这种访问数值的操作。
这时候新的 toy 语言示例:

struct Struct {
  var a;
  var b;
}
# User defined generic function may operate on struct types as well.
def multiply_transpose(Struct value) {
  # We can access the elements of a struct via the '.' operator.
  return transpose(value.a) * transpose(value.b);  // 新增了取值操作
}
def main() {
  # We initialize struct values using a composite initializer.
  Struct value = {[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]};  
  # We pass these arguments to functions like we do with variables.
  var c = multiply_transpose(value);
  print(c);
}

能够得到 mlir 打印输出:

module {
  toy.func @multiply_transpose(%arg0: !toy.struct<tensor<*xf64>, tensor<*xf64>>) -> tensor<*xf64> {
    %0 = toy.struct_access %arg0[0] : !toy.struct<tensor<*xf64>, tensor<*xf64>> -> tensor<*xf64>
    %1 = toy.transpose(%0 : tensor<*xf64>) to tensor<*xf64>
    %2 = toy.struct_access %arg0[1] : !toy.struct<tensor<*xf64>, tensor<*xf64>> -> tensor<*xf64>
    %3 = toy.transpose(%2 : tensor<*xf64>) to tensor<*xf64>
    %4 = toy.mul %1, %3 : tensor<*xf64>
    toy.return %4 : tensor<*xf64>
  }
  toy.func @main() {
    %0 = toy.struct_constant [
      dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>,
      dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>
    ] : !toy.struct<tensor<*xf64>, tensor<*xf64>>
    %1 = toy.generic_call @multiply_transpose(%0) : (!toy.struct<tensor<*xf64>, tensor<*xf64>>) -> tensor<*xf64>
    toy.print %1 : tensor<*xf64>
    toy.return
  }
}

# 操作在 StructType 方面的优化

现在我们已经有一些操作支持了 StructType 类型,也有了一些新的进行常量折叠的机会。
在内联 inline 之后,当前的 mlir 输出会是如下这个样子:

module {
  toy.func @main() {
    %0 = toy.struct_constant [
      dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>,
      dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>
    ] : !toy.struct<tensor<*xf64>, tensor<*xf64>>
    %1 = toy.struct_access %0[0] : !toy.struct<tensor<*xf64>, tensor<*xf64>> -> tensor<*xf64>
    %2 = toy.transpose(%1 : tensor<*xf64>) to tensor<*xf64>
    %3 = toy.struct_access %0[1] : !toy.struct<tensor<*xf64>, tensor<*xf64>> -> tensor<*xf64>
    %4 = toy.transpose(%3 : tensor<*xf64>) to tensor<*xf64>
    %5 = toy.mul %2, %4 : tensor<*xf64>
    toy.print %5 : tensor<*xf64>
    toy.return
  }
}

上述 mlir 中有很多 针对 toy.struct_constanttoy.struct_access ,如果没有常量折叠,值需要在运行时通过 struct_access 操作获取;而有了常量折叠,编译器可以直接将其替换为常量。
在 CH7 章节 ToyCombine.cpp 能找到以下代码。

/// Fold constants.
OpFoldResult ConstantOp::fold(FoldAdaptor adaptor) { return value(); }
/// Fold struct constants.
OpFoldResult StructConstantOp::fold(FoldAdaptor adaptor) {
  return value();
}
/// Fold simple struct access operations that access into a constant.
OpFoldResult StructAccessOp::fold(FoldAdaptor adaptor) {
  auto structAttr = dyn_cast_or_null<mlir::ArrayAttr>(adaptor.getInput());
  if (!structAttr)
    return nullptr;
  size_t elementIndex = index().getZExtValue();
  return structAttr[elementIndex];
}

OPS 时定义指定该操作可以常量折叠。

//===----------------------------------------------------------------------===//
// ConstantOp
//===----------------------------------------------------------------------===//
def ConstantOp : Toy_Op<"constant",
    [ConstantLike, Pure,
     DeclareOpInterfaceMethods<ShapeInferenceOpInterface>]> {
  ...
  // Set the folder bit so that we can implement constant folders.  // 指明该操作可以常量折叠
  let hasFolder = 1;
}

常量物化(Constant Materialization)

常量物化是指在折叠过程中,当需要创建一个新的常量时,如何生成对应的常量操作。也就是折叠 struct_access 后,我们需要创建一个新的常量来表示结果。但是,这个结果可能是一个张量(用 toy.constant)或者一个结构体(用 toy.struct_constant)。MLIR 框架不知道如何为 Toy 方言创建常量,因为它不知道 toy.constant 和 toy.struct_constant 的区别。

mlir::Operation *ToyDialect::materializeConstant(mlir::OpBuilder &builder,
                                                 mlir::Attribute value,
                                                 mlir::Type type,
                                                 mlir::Location loc) {
  if (isa<StructType>(type))
    return StructConstantOp::create(builder, loc, type,
                                            cast<mlir::ArrayAttr>(value));
  return ConstantOp::create(builder, loc, type,
                                    cast<mlir::DenseElementsAttr>(value));
}

此后生成的 mlir :

module {
  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>
    %1 = toy.transpose(%0 : tensor<2x3xf64>) to tensor<3x2xf64>
    %2 = toy.mul %1, %1 : tensor<3x2xf64>
    toy.print %2 : tensor<3x2xf64>
    toy.return
  }
}

# 后记

日落是免费的,春夏秋冬也是、不要觉得人生是那么无望,希望你快乐。 
    我们总是为许多遥不可及的事情奔波,却错过了路边的花开,傍晚落在身上的夕阳。
    忙着生活的同时记得去感受生活中的小细节,生活除了琐碎与平淡,还有可口的美食和无数盛开的花朵。 
    晚风吹人醒、万事藏于心、我没说不公平、也没说苦、我说我知道了。

本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,并指明哪一篇博客,看到一定及时回复!

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

XianMu WeChat Pay

WeChat Pay

XianMu Alipay

Alipay