惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
F
Fortinet All Blogs
小众软件
小众软件
D
Docker
U
Unit 42
博客园 - 聂微东
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
IT之家
IT之家
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
有赞技术团队
有赞技术团队
腾讯CDC

Пусть этот камень будет более крепким, чем человек

【MLIR】Transform Dialect【0】保持计算结构 【MLIR】跟着Toy语言学习MLIR【6】支持复合结构类型 【MLIR】跟着Toy语言学习MLIR【5】降级到LLVM及CodeGen 【MLIR】跟着Toy语言学习MLIR【4】部分降级到低层方言 【MLIR】跟着Toy语言学习MLIR【3】通过接口实现通用转换 【MLIR】跟着Toy语言学习MLIR【2】pattern匹配和重写 【琐记】你的爱好能坚持多少年 【琐记】烟火与尘埃 【Triton】Triton实现矩阵乘 【LLM推理加速】FlashAttention 【LLM推理加速】PagedAttention 【LLM推理加速】Online Softmax LLM基础知识【1】 Transformer模型 【AI编译】LayerGroup Tiling Tile的疑惑和思考 【AI编译】深度优先的Tile调度,万事大吉? 【AI编译】多级流水线Tile调度策略 【CUDA C++】GPU内存使用【3】 【AI编译】Cache缓存地址映射 【CUDA C++】GPU存储【2】 【CUDA C++】GPU基本介绍【1】 【00】0序章-不受欢迎的来客 【转载】我来了——持续低熵 【Halide】调度优化【2】 【感想】写作进度报告5 【Halide】调度优化【1】 【转载】北大中文男足战报2 【BYOC】TVM切分子图 【转载】北大中文男足战报1 【AI编译】张量生命周期管理
【MLIR】跟着Toy语言学习MLIR【1】Toy语言和 Toy Dialect
XianMu · 2026-08-07 · via Пусть этот камень будет более крепким, чем человек

# 前言

现在开启 MLIR 学习系列。本章介绍 Toy 语言和 Toy Dialect 这一步骤的转换。当然,是从 Toy AST 到 Toy Dialect 这一过程得转换。从 Toy 文本语言到 AST 这个转换过程,需要编译原理的知识和非常麻烦的词法分析器,且这个过程与 MLIR 无关,所以该文章不会包含这个过程。
学习 MLIR 最好的方式还是照着官方教程来,但是只看 MLIR 官网教程,Ch1 到 Ch6,没有基础的话又让人学着有点吃力。所以本教程没有直接参照官网 Toy 教程,而是以 MLIR 工程的 toy 相关代码入手。欢迎参考。
相关链接: LLVM ProjectMLIR 官方文档MLIR 官网教程【编译器】使用 llvm 编译自定义语言【1】构建 AST

作为初学者,错误在所难免,还望不吝赐教。

# 基本简介

官网 Toy 教程 展示了如何将自定义语言 Toy 借助 MLIR 一步步编译为可执行机器码的过程。Toy 是一种简单的自定义语言,为了简便,其所有数据类型定义为 fp64 类型的 Tensor,支持 +/* 操作和 transpose 等有限的操作。
以下是整个编译降级的过程:
Toy txt -> Toy AST -> Toy Dialect -> Affine Dialect -> llvm Dialect -> llvm IR -> 机器码(通过 JIT 编译)

编译流程图

编译过程,下载 github llvm 工程,按照官网编译,值得一提的是,编译整个工具用时太久,我们重点关注 CH* 教程内容,在修改了教程中代码之后,可以只对该部分进行编译:

ninja -j 4 toyc-ch6

第六节已经是较为完整的代码,第七节增加了一个复杂类型的支持。我们主要看第六节的代码。
一些执行指令:

/your/path/llvm-project/build/bin/toyc-ch6 /your/path/llvm-project/mlir/test/Examples/Toy/Ch5/affine-lowering.mlir -emit=mlir-llvm
/your/path/llvm-project/build/bin/toyc-ch6 /your/path/llvm-project/mlir/test/Examples/Toy/Ch5/affine-lowering.mlir -emit=jit
/your/path/llvm-project/build/bin/toyc-ch6 /your/path/llvm-project/mlir/test/Examples/Toy/Ch6/codegen.toy -emit=mlir
/your/path/llvm-project/build/bin/toyc-ch6 /your/path/llvm-project/mlir/test/Examples/Toy/Ch6/codegen.toy -emit=mlir -opt
/hyour/path/llvm-project/build/bin/toyc-ch6 /your/path/llvm-project/mlir/test/Examples/Toy/Ch6/codegen.toy -emit=mlir-affine
/your/path/llvm-project/build/bin/toyc-ch6 /your/path/llvm-project/mlir/test/Examples/Toy/Ch6/codegen.toy -emit=mlir-llvm

简单看一下入口函数。该函数用来解析命令行,完成对应的指令。指令需要输入 toy 文本或者 toy 文本转成的 mlir 文本 (MLIR 支持序列化和反序列化,所以 mlir txt 和 toy dialect 可以互相转换)。他们都会生成同样的 Toy Dialect。

编译流程图

int main(int argc, char **argv) {
  
  mlir::registerAsmPrinterCLOptions();   
  mlir::registerMLIRContextCLOptions();  
  mlir::registerPassManagerCLOptions();  
  cl::ParseCommandLineOptions(argc, argv, "toy compiler\n");  
  if (emitAction == Action::DumpAST)
    return dumpAST();
 
  
  mlir::DialectRegistry registry;
  mlir::func::registerAllExtensions(registry);
  mlir::LLVM::registerInlinerInterface(registry);
  mlir::MLIRContext context(registry);
  
  context.getOrLoadDialect<mlir::toy::ToyDialect>();  
toy dialect,用到的其他方言延迟加载,即访问到对应方言的时候才加载
  mlir::OwningOpRef<mlir::ModuleOp> module;
  if (int error = loadAndProcessMLIR(context, module))  
    return error;
  
  bool isOutputingMLIR = emitAction <= Action::DumpMLIRLLVM;
  if (isOutputingMLIR) {
    module->dump();
    return 0;
  }
  
  if (emitAction == Action::DumpLLVMIR)
    return dumpLLVMIR(*module);  
  
  if (emitAction == Action::RunJIT)
    return runJit(*module);  
  llvm::errs() << "No action specified (parsing only?), use -emit=<action>\n";
  return -1;
}

我们不关注 Toy 文本到 AST 的转换过程,感兴趣的可以去看 MLIR TOY 教程的源码。其涉及到的是编译原理和较为复杂的词法分析器,相关知识也可以参考 llvm 教程中的万花筒语言 My First Language Frontend with LLVM 编译过程,以及博客【编译器】使用 llvm 编译自定义语言【1】构建 AST 等内容,其内容与当前 文本转 AST 类似,这里不再赘述。
我们着重关注 AST 转 toy dialect 的过程。
转换过程位于 mlir/examples/toy/Ch6/mlir/MLIRGen.cpp 文件中。

想要理解转换过程,还是先回顾一下 AST 抽象语法数的结构:在之前学习万花筒语言中,代码语言的顶层结构有三种,分别是 函数 Function ,外部函数调用 Extern 、顶层表达式 TopLevelExpr
Toy 语言有点类似于 C 这些常见的语言,最外层都是一些函数,比如 main () 和其他一些自定义函数,所以 AST 最外层是一些函数的集合。
AST 结构图

AST 转 toy Dialect 代码: 输入 ModuleAST ,输出 mlir::ModuleOp

mlir::ModuleOp mlirGen(ModuleAST &moduleAST) {  
    
    
    theModule = mlir::ModuleOp::create(builder.getUnknownLoc());
    for (FunctionAST &f : moduleAST)  
      mlirGen(f);
    
    
    
    if (failed(mlir::verify(theModule))) {
      theModule.emitError("module verification error");
      return nullptr;
    }
    return theModule;  
  }

mlirGen() 拥有多个重载函数,用来处理 函数、常量、返回、调用、表达式等,深度递归地构建 toy dialect。我们来看一下处理函数:
从上方地 AST 结构图中可以看到 Function 包含原型 Proto 和躯干 body 两部分,原型 Proto 包含函数名和若干参数,躯干 body 包含多个表达式。下方地代码和比较清晰, Function 的原型部分交给 mlirGen(*funcAST.getProto()) 去工作,躯干部分交给 mlirGen(*funcAST.getBody()) 去工作。将这两部分工作的结果构建成 mlir::toy::FuncOp function

  mlir::toy::FuncOp mlirGen(FunctionAST &funcAST) {
    
    ScopedHashTableScope<llvm::StringRef, mlir::Value> varScope(symbolTable);
    
    builder.setInsertionPointToEnd(theModule.getBody());
    mlir::toy::FuncOp function = mlirGen(*funcAST.getProto());  
    if (!function)
      return nullptr;
    
    mlir::Block &entryBlock = function.front();
    auto protoArgs = funcAST.getProto()->getArgs();
    
    for (const auto nameValue :
         llvm::zip(protoArgs, entryBlock.getArguments())) {
      if (failed(declare(std::get<0>(nameValue)->getName(),
                         std::get<1>(nameValue))))
        return nullptr;
    }
    
    
    
    builder.setInsertionPointToStart(&entryBlock);  
    
    if (mlir::failed(mlirGen(*funcAST.getBody()))) {  
      function.erase();
      return nullptr;
    }
    
    
    
    ReturnOp returnOp;
    if (!entryBlock.empty())
      returnOp = dyn_cast<ReturnOp>(entryBlock.back());
    if (!returnOp) {
      ReturnOp::create(builder, loc(funcAST.getProto()->loc()));
    } else if (returnOp.hasOperand()) {
      
      
      function.setType(builder.getFunctionType(
          function.getFunctionType().getInputs(), getType(VarType{})));
    }
    
    if (funcAST.getProto()->getName() != "main")
      function.setPrivate();
    return function;
  }

这些深度递归调用,最终会归根到最基本的表达式。比如 常量 Expr,Add Expr 等。比如递归到下面的变量表达式,就不会再往下递归了。

  
  
  mlir::Value mlirGen(VariableExprAST &expr) {  
    if (auto variable = symbolTable.lookup(expr.getName()))
      return variable;
    emitError(loc(expr.loc()), "error: unknown variable '")
        << expr.getName() << "'";
    return nullptr;
  }

# TableGen 和 Toy Dialect 定义

Toy Dialect 方言是自定义方言,一般通过编写 td 文件,然后通过 TableGen 工具生成对应的 CPP 代码。
TableGen 是 LLVM/MLIR 生态中的 “代码生成器”,支持我们用声明式的语言写 “规则”,然后自动生成大量重复的 C++ 代码。
td 文件 位于 mlir/examples/toy/Ch6/include/toy/Ops.td

方言 Dialect 定义包含哪些部分:

ToyOps.td
├── 头文件保护 (#ifndef TOY_OPS)
├── 导入依赖 (include)
├── 方言定义 (Toy_Dialect)
├── 基类定义 (Toy_Op)
└── 操作定义 (ConstantOp, AddOp, FuncOp, ...)

首先,定义一种方言:

def Toy_Dialect : Dialect {  
  let name = "toy";
  let cppNamespace = "::mlir::toy";
}

之后定义操作的基类:

class Toy_Op<string mnemonic, list<Trait> traits = []> :
  
    Op<Toy_Dialect, mnemonic, traits>;  

名称,比如 ConstantOp 指定名称 为 "constant", 则其在 MLIR 中的完整名称变成 "toy.constant"。
特性,比如 def ConstantOp : Toy_Op<"constant", [Pure]>
常见的特性 :
常见的特性

之后定义具体操作,MLIR 中定义一个 Operation,你需要:

  • a. 定义一个 C++ 类
  • b. 实现构造方法(Builder)
  • c. 实现访问器(Getter/Setter)
  • d. 实现解析 / 打印方法(Parser/Printer)
  • e. 实现验证方法(Verifier)
  • f. 实现接口方法(如形状推断)

td 文件中 ConstantOp 的定义:

def ConstantOp : Toy_Op<"constant", [Pure]> {
  
  
  let summary = "constant";
  let description = [{
    Constant operation turns a literal into an SSA value. The data is attached
    to the operation as an attribute. For example:
    ```mlir
      %0 = toy.constant dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]>
                        : tensor<2x3xf64>
    ```
  }];
  
  let arguments = (ins F64ElementsAttr:$value);  
  
  let results = (outs F64Tensor);
 
  
  let hasCustomAssemblyFormat = 1;
  
  
  
  let builders = [
    
    OpBuilder<(ins "DenseElementsAttr":$value), [{
      build($_builder, $_state, value.getType(), value);
    }]>,
    
    OpBuilder<(ins "double":$value)>
  ];
  
  let hasVerifier = 1;
}

我发现 td 文件的定义生成的 inc 代码还是比较复杂的。大家可以精简一下 td 文件的内容,然后通过以下指令,生成新的 inc 文件,将生成内容和 td 定义对照一下。

/your/path/llvm-project/build/bin/mlir-tblgen -gen-op-decls \
  -I /your/path/llvm-project/mlir/include \
  -I /your/path/llvm-project/build/build/tools/mlir/include \
  /your/path/llvm-project/mytest/Ops.td \
  -o ./MyGeneratedOps.h.inc

我觉得初步学习的话,只需要知道为什么这么定义,有什么功能就行了:

def ConstantOp : Toy_Op<"constant", [Pure]>  
class ConstantOp : public Op<...> { ... };  
static StringRef getOperationName() { return "toy.constant"; }  
let arguments = (ins F64ElementsAttr:$value);
DenseElementsAttr getValue();  
void setValue(DenseElementsAttr newValue);  
let results = (outs F64Tensor);
Type getType();
void setType(Type newType);

该 ConstantOp 节点只有一个输入和一个输出,所以声明输入的时候 let arguments = (ins F64ElementsAttr:$value); 只有一个输入,类型为 “F64 的密集张量常量”。只有一个输出 let results = (outs F64Tensor); 输出类型是 F64Tensor
该 ConstantOp 定义了两个 builder,用来帮助构造该类,其中第二个 OpBuilder<(ins "double":$value)> ,生成的 C++ 相关代码如下:

OpBuilder<(ins "double":$value)>
  
  
  static void build(::mlir::OpBuilder &odsBuilder,::mlir::OperationState &odsState, double value);  
  static ConstantOp create(::mlir::OpBuilder &builder, ::mlir::Location location, double value);
  
  static ConstantOp create(::mlir::ImplicitLocOpBuilder &builder, double value);  
  
  
  
void ConstantOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
                       double value) {
  auto dataType = RankedTensorType::get({}, builder.getF64Type());
  auto dataAttribute = DenseElementsAttr::get(dataType, value);
  ConstantOp::build(builder, state, dataType, dataAttribute);
}

build 是 ODS 直接生成的,是最底层、最通用的创建方式。它不直接返回 ConstantOp,而是将属性填入 odsState 中。用户调用: builder.create<ConstantOp>(loc, 42.0) ,也就是里面的 create 函数,create 函数会调用 build。
例如,你能在 MLIRGen.cpp 文件中找到 AST 转 toy dialect 的函数中调用 ConstantOp::create(builder, loc(num.loc()), num.getValue());
虽然自动生成了 build 函数,但是这个函数的输入 Double 不是一个 MLIR 属性,它只是一个 C++ 基本类型。MLIR 不知道如何将一个 double 存储到 OperationState 的 attributes 中。你必须手写一段代码来 “包装” 它,所以你能在 Dialect.cpp 中找到自定义的函数定义。也在上方代码中。

td 定义中要有个 hasCustomAssemblyFormat 字段,表明需要自定义打印输出方法。该方法由开发者在 CPP 文件中自己定义。同样校验方法也需要自己定义。

  let hasCustomAssemblyFormat = 1;
  
  
  let hasVerifier = 1;

然后你能在 Dialect.cpp 中找到自定义的输出方法和自定义的验证方法:

void ConstantOp::print(mlir::OpAsmPrinter &printer) {
  printer << " ";
  printer.printOptionalAttrDict((*this)->getAttrs(), {"value"});
  printer << getValue();
}
llvm::LogicalResult ConstantOp::verify() {
  
  
  auto resultType =
      llvm::dyn_cast<mlir::RankedTensorType>(getResult().getType());
  if (!resultType)
    return success();
  
  
  auto attrType = llvm::cast<mlir::RankedTensorType>(getValue().getType());
  if (attrType.getRank() != resultType.getRank()) {
    return emitOpError("return type must match the one of the attached value "
                       "attribute: ")
           << attrType.getRank() << " != " << resultType.getRank();
  }
  
  for (int dim = 0, dimE = attrType.getRank(); dim < dimE; ++dim) {
    if (attrType.getShape()[dim] != resultType.getShape()[dim]) {
      return emitOpError(
                 "return type shape mismatches its attribute at dimension ")
             << dim << ": " << attrType.getShape()[dim]
             << " != " << resultType.getShape()[dim];
    }
  }
  return mlir::success();
}

以上是 td 及其生成文件的部分内容,总结来说,该 td 文件主要完成了以下内容:

Toy_Dialect  // 定义一个方言  
Toy_Op    // 一个 base OP
ConstantOp   AddOp     CastOp    FuncOp        // 定义一系列操作
GenericCallOp    MulOp    PrintOp    ReshapeOp    
ReturnOp     TransposeOp

这些操作构成了一套 “最小完备集”。每个操作的详细定义可以参考 td 文件。

# 后记

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