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

推荐订阅源

WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
月光博客
月光博客
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
IT之家
IT之家
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
S
SegmentFault 最新的问题
宝玉的分享
宝玉的分享
博客园 - Franky
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Jina AI
Jina AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
博客园 - 司徒正美
爱范儿
爱范儿

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

【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【1】Toy语言和 Toy Dialect 【琐记】你的爱好能坚持多少年 【琐记】烟火与尘埃 【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【2】pattern匹配和重写
XianMu · 2026-08-18 · via Пусть этот камень будет более крепким, чем человек

# 前言

现在开启 MLIR 学习系列。本篇是跟着 Toy 语言学习 MLIR 的第二篇,主要介绍 toy dialect 高层级方言的匹配重写,前述内容请参考【MLIR】跟着 Toy 语言学习 MLIR【1】Toy 语言和 Toy Dialect

学习 MLIR 最好的方式还是照着官方教程来,但是只看 MLIR 官网教程,Ch1 到 Ch6,没有基础的话又让人学着有点吃力。所以本教程没有直接参照官网 Toy 教程,而是以 MLIR 工程的 toy 相关代码入手。欢迎参考。

相关链接: LLVM ProjectMLIR 官方文档MLIR 官网教程【编译器】使用 llvm 编译自定义语言【1】构建 AST【MLIR】跟着 Toy 语言学习 MLIR【1】Toy 语言和 Toy Dialect

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

基本简介

官网 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 Dialect 到 Affine Dialect

前篇博客已经讲解了 toy 语言转换到自定义的 toy dialect 的过程。本篇博客则简单介绍一下后续过程。

主函数调用的 loadAndProcessMLIR 函数完成的,而这个函数里面调用一系列 Pass 实现 dialect 的优化和转换。代码中做了简单的注释。

static int loadAndProcessMLIR(mlir::MLIRContext &context,
                              mlir::OwningOpRef<mlir::ModuleOp> &module) {
  if (int error = loadMLIR(context, module))
    return error;
  mlir::PassManager pm(module.get()->getName());
  
  if (mlir::failed(mlir::applyPassManagerCLOptions(pm)))
    return 4;
  
  bool isLoweringToAffine = emitAction >= Action::DumpMLIRAffine;
  bool isLoweringToLLVM = emitAction >= Action::DumpMLIRLLVM;
  if (enableOpt || isLoweringToAffine) {  
    
    pm.addPass(mlir::createInlinerPass());
 
    
    
    mlir::OpPassManager &optPM = pm.nest<mlir::toy::FuncOp>();
 
    optPM.addPass(mlir::toy::createShapeInferencePass());
  
    optPM.addPass(mlir::createCanonicalizerPass());  
    optPM.addPass(mlir::createCSEPass());  
  }
  if (isLoweringToAffine) {  
    
    pm.addPass(mlir::toy::createLowerToAffinePass());
  
    
    mlir::OpPassManager &optPM = pm.nest<mlir::func::FuncOp>();
    optPM.addPass(mlir::createCanonicalizerPass());
    optPM.addPass(mlir::createCSEPass());
    
    if (enableOpt) {  
      optPM.addPass(mlir::affine::createLoopFusionPass());
      optPM.addPass(mlir::affine::createAffineScalarReplacementPass());
    }
  }
  if (isLoweringToLLVM) {  
    
    pm.addPass(mlir::toy::createLowerToLLVMPass());
 
    
    
    
    pm.addPass(mlir::LLVM::createDIScopeForLLVMFuncOpPass());
 
  }
  if (mlir::failed(pm.run(*module)))
  
    return 4;
  return 0;
}

# High-level 语言特性分析和转换 C++ style

本节内容来自于 教程 Chapter 3: High-level Language-Specific Analysis and Transformation,主要讲解在高层级方言 toy dialect ,如果做 pattern 匹配和重写,用的例子是去除连续的 transpose 操作(连续的两个转置 transpose 可以消除 transpose(transpose(X)) -> X )。

def transpose_transpose(x) {  
  return transpose(transpose(x));
}

上述 toy 代码会得到对应的 toy dialect :

toy.func @transpose_transpose(%arg0: tensor<*xf64>) -> tensor<*xf64> {
  %0 = toy.transpose(%arg0 : tensor<*xf64>) to tensor<*xf64>
  %1 = toy.transpose(%0 : tensor<*xf64>) to tensor<*xf64>
  toy.return %1 : tensor<*xf64>
}

从高层级方言进行优化是比较方便的,一旦降级到低层级方言,再进行优化就变得非常困难,例如,当降级到如下层级的时候,再进行优化将非常困难:

#define N 100
#define M 100
void sink(void *);
void double_transpose(int A[N][M]) {
  int B[M][N];
  for(int i = 0; i < N; ++i) {
    for(int j = 0; j < M; ++j) {
       B[j][i] = A[i][j];
    }
  }
  for(int i = 0; i < N; ++i) {
    for(int j = 0; j < M; ++j) {
       A[i][j] = B[j][i];
    }
  }
  sink(A);
}

教程将一个自定义的 RewritePattern 注册到 MLIR 的 CanonicalizerPass 中,来实现双转置的匹配和重写。前面的代码中提到过,该 pass 用于 [规范化 IR:消除冗余操作、简化常量表达式、应用简单的重写规则]。

ToyCombine.cpp 源码中能够找到对应的自定义 RewritePattern

struct SimplifyRedundantTranspose : public mlir::OpRewritePattern<TransposeOp> {  
  
  
  
  SimplifyRedundantTranspose(mlir::MLIRContext *context)
      : OpRewritePattern<TransposeOp>(context, 1) {}
  
  
  
  llvm::LogicalResult  matchAndRewrite(TransposeOp op,  
                  mlir::PatternRewriter &rewriter) const override {
    
    mlir::Value transposeInput = op.getOperand();  
    TransposeOp transposeInputOp = transposeInput.getDefiningOp<TransposeOp>(); 
    
    if (!transposeInputOp)  
      return failure();
    
    rewriter.replaceOp(op, {transposeInputOp.getOperand()});  
    return success();
  }
};

matchAndRewrite 是该 RewritePattern 的核心方法,当 MLIR 框架会在遍历 toy dialect 时,对每个 TransposeOp 调用这个方法。核心思路很简单,即匹配到一个 TransposeOp 的时候,会检查前一个节点是否也是 TransposeOp,不是的话匹配失败,是的话怎直接去掉两个 Transpose 操作。

CanonicalizerPass 以贪婪的、迭代的方式应用由操作定义的转换。为了保证 CanonicalizerPass 能够使用到该 pattern ,在 Ops.td 文件定义 Transpose 操作的时候,能够找到 let hasCanonicalizer = 1; 字段,以及需要通过 Canonicalization framework 注册这个转换 pattern:

void TransposeOp::getCanonicalizationPatterns(
    RewritePatternSet &results, MLIRContext *context) {
  results.add<SimplifyRedundantTranspose>(context);
}

# High-level 语言特性分析和转换 Declarative style

以上是一种 C++ 的方式实现匹配重写,MLIR 还提供了声明的方式,其提供了一套 rprovides a table-based syntax for pattern-match and rewrite rules,从 td 文件中声明匹配和重写规则。

以下是 toy 教程中针对 ReshapeOp 操作定义的声明式重写规则,其位于 ToyCombine.td 文件中。该文件主要定义了三个针对 toy.reshape 操作的优化,同上述 C++ 风格的匹配重写函数一样,这些优化在 CanonicalizerPass 运行时会被自动调用。

#ifndef TOY_COMBINE
#define TOY_COMBINE
include "mlir/IR/PatternBase.td"
include "toy/Ops.td"
def ReshapeReshapeOptPattern : Pat<(ReshapeOp(ReshapeOp $arg)),
                                   (ReshapeOp $arg)>;
def ReshapeConstant :
  NativeCodeCall<"$0.reshape(::llvm::cast<ShapedType>($1.getType()))">;
def FoldConstantReshapeOptPattern : Pat<
  (ReshapeOp:$res (ConstantOp $arg)),
  (ConstantOp (ReshapeConstant $arg, $res))>;
def TypesAreIdentical : Constraint<CPred<"$0.getType() == $1.getType()">>;
def RedundantReshapeOptPattern : Pat<
  (ReshapeOp:$res $arg), (replaceWithValue $arg),
  [(TypesAreIdentical $res, $arg)]>;
#endif 

我们来分别看看这三个重写规则:

1. 消除连续的 Reshape

def ReshapeReshapeOptPattern : Pat<(ReshapeOp(ReshapeOp $arg)),
                                   (ReshapeOp $arg)>; 

其实当有两个连续的 Reshape 存在的时候,内层的 reshape 就没有作用了,所以将源模式 (ReshapeOp(ReshapeOp $arg)) 替换为新模式 (ReshapeOp $arg) ,这样就完成了 连续 reshape 的替换。而且保留的 reshape 参数是外层的 reshape 参数,这是符合逻辑的。

2. 常量折叠 + reshape

def ReshapeConstant :  
  NativeCodeCall<"$0.reshape(::llvm::cast<ShapedType>($1.getType()))">;
def FoldConstantReshapeOptPattern : Pat<
  (ReshapeOp:$res (ConstantOp $arg)),  
  (ConstantOp (ReshapeConstant $arg, $res))>;

匹配的是 Reshape (Constant (x)) 结构,匹配之后变成单个 ConstantOp。

NativeCodeCall 调用一段手写 C++ 代码片段,将常量 $arg 重塑为 $res 所要求的形状。

举个例子,假设优化前的源代码如下:

%0 = toy.constant dense<[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]> : tensor<6xf64>
%1 = toy.reshape(%0) : tensor<6xf64> -> tensor<2x3xf64>

之后 CanonicalizerPass 会匹配 reshape + Constant 的结构,然后调用写好的重写规则,将其变成新的:

%0 = toy.constant dense<[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]> : tensor<6xf64>
%1 = toy.constant dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64>

3. 消除多余的 reshape

当输入和 reshape 的输出,他们的数据类型相同,即维度 shape 相同的时候,去掉 reshape 算子。

def TypesAreIdentical : Constraint<CPred<"$0.getType() == $1.getType()">>;
def RedundantReshapeOptPattern : Pat<
  (ReshapeOp:$res $arg), (replaceWithValue $arg),
  [(TypesAreIdentical $res, $arg)]>;

举了例子:

%0 = toy.reshape(%arg0) : tensor<2x3xf64> -> tensor<2x3xf64>

多 td 文件中声明匹配和重写规则的细节,可以参考 Table-driven Declarative Rewrite Rule (DRR)

上述就是两种匹配重写的方式:声明式和 C++ 手写式,相比较而言,声明式更简洁方便,代码量少,但是调试难度高,不够灵活,适用于简单的匹配场景,而 C++ 方式更灵活,灵活性高,能实现复杂逻辑,但是写起来更复杂。

# 后记

I have seen things you people wouldn't believe.
Attack ships on fire off the shoulder of Orion.
I have watched C-beams glitter in the dark near the Tannhauser Gate.
All those ... moments will be lost in time, like tears ... in rain.
Time to die ...
                              《银翼杀手》

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