MLIR学习笔记

mlir教程

chapter-1 入门

mlir与llvm的最大不同:llvm是单一抽象,同时固定IR结构,不够灵活。mlir是多级抽象能更好的利用高级语言的语义信息。更适合现代复杂软件与硬件的需求。

mlir的原则:“Progressive Lowering”,即存在多个级别的 IR 粒度,并且逐步降低 IR 的不同部分,只有当信息不再对优化有用时才丢弃信息。这种方式相比传统的、一次性的降低方法可以更好地进行优化,因为它提供了更多的上下文信息,让编译器有更大的机会进行智能优化。

MLIR论坛:Discourse threads

**方言:**定义一组操作(ops)、类型、属性和其他实体的命名空间,这些实体共同实现了某个特定的语义或功能域。

**Lowing:**指将高层次的抽象通过pass转换为更低层次、更接近于机器码的表示的过程。(高级方言转向低级方言)

收获:

  • MLIR一部分动机是为了affine 方言(旨在完成多面体优化,这是传统编译器和llvm不具备的)和 linalg 方言(旨在低级机器学习(ML)操作的优化),MLIR的核心理念是:在高层次上保留和优化程序结构,然后在向低层次转换的过程中优化和适应目标平台,从而解决了传统编译技术中的一些根本性难题。多级中间表示 (MLIR) 旨在轻松表达和优化涉及深循环嵌套和高维密集矩阵的计算,所以它特别适合深度学习。

  • MLIR能够表示任意控制流和任意数据访问,并且足够通用来表示几乎所有顺序计算。这是与现有多面体表示实现(例如 LLVM Polly)的一个关键区别。链接

  • MLIR中一个operation有0或者多个region,一个region有1或者多个block。一个block中会有多个操作

    • block中的最后一个操作必须是是终止符操作如:br、cond_br、return、yield等(SSACFG区域?)
    • region有入口块的概念。且region没法直接包含操作,必须通过块。
    • 操作可能会有0或者多个region,且可能没有返回值。
    • 变量名以 % 为前缀,函数以 @ 为前缀,程序中的每个变量/值都有一个类型,通常在冒号后表示。函数也是一种操作。
  • lit和FileCheck仅从语法上检查是否正确。检查结果对不对:实现此目的的一种稍微轻量级的方法是使用 mlir-cpu-runner(最低级别 MLIR 方言的解释器)

  • MLIR的insight:“及时做优化”

    • 这里简单举例,dialect 是如何混合的。例如,我是 pytorch,生成了一些神经网络,

      • 我想要表示这些运算:Tensor 是一块带 shape 的指针:使用 tensor dialect

      • 简单的 elementwise 加减乘除:使用 arith dialect

      • 复杂的 log、exp 等运算:使用 math dialect

      • 矩阵线性代数运算:使用 linalg dialect

      • 可能有一些控制流:使用 scf dialect

      • 整个网络是一个函数:使用 func dialect

    • 接下来,将其逐渐 lower 到 LLVM:

      • 调用 Lowering Pass,把 tensor lowering 到 linalg,而其他的 dialect 不会改变。
      • 继续调用 pass,直到把 linalg 转换到 affine -> scf -> cf,其他表示运算的 dialect 保留不变。

      • 继续 Lowering,把 memref 转换为裸指针、arithfunc 转换为 llvm 内置运算。

      • 最后,所有非 llvm dialect 都被转换为了 llvm dialect,现在可以导出为 llvm ir 交给 llvm 继续编译。

    • 可见,MLIR 编译有一个特点:不同 dialect 是独立的。

    • 例如,在做循环展开等优化的时候,我不需要关心加法和减法可以合并;而在做算数表达式优化的时候,也不需要关心当前在哪个函数里边。
    • MLIR 可以从各个层次优化 IR:例如:

      • affine 层面,可以根据循环大小做展开,向量化

      • scf 层面,可以发现循环不变量

      • arith 层面,可以用算数恒等式优化代码

    • MLIR 的 insight 在于“及时做优化”。很明显,linalg 层次,我们很容易发现矩阵被转置了两次,但一旦 lower 到 scf,所有转置操作都变成循环,优化就很难进行了。

  • MLIR 的用处的方便之处

    • 我们使用 MLIR,主要也是想要复用别人已经写好的代码,一般包括:
    • 复用已有 dialect 作输入,不用自己写前端。

      • 如 Polygeist 能把 C 翻译成 Affine Dialect,这样我们就不用写 C Parser
    • 将已有 dialect混入或作为输出

      • 如 arith 等 dialect,可以直接集成起来,不需要自己写。
      • 要生成 binary 的时候,可以直接生成 LLVM Dialect,复用后端 LLVM 编译管线
    • 复用已有的 Pass。

      • 常见的 Pass 如 CSE,DCE 可以复用
      • Dialect 专用 Pass,如循环展开,也可以复用

chapter-2 Writing first pass

收获:两种构建pass的方式

  • 通过 PassWrapper的方式,通过walk遍历语法树匹配

    文章提出OperationPass是有要求的,即只能操作内部变量,是隔离的。

class AffineFullUnrollPass
    : public PassWrapper<AffineFullUnrollPass,
                         OperationPass<mlir::func::FuncOp>> {}
/*
runOnOperation: the function that performs the pass logic.
getArgument: the CLI argument for an mlir-opt-like tool. 即pass的名称
getDescription: the CLI description when running --help on the mlir-opt-like tool. */
  • 通过模式匹配的方式
  LogicalResult matchAndRewrite(AffineForOp op,
                                PatternRewriter &rewriter) const override { //定义模式
    return loopUnrollFull(op); //文章指出这样是不合规范的,必须通过rewriter来更新操作从而保证其原子性
  }

这两种模式哪个更好呢?模式匹配功能更加全面:

  • 模式匹配通过查看同一块中的相邻操作并应用一些过滤逻辑来识别的任何操作。例如,“这个 exp 操作后面是否有一个 log 操作,并且没有对 exp 的输出进行其他用途?“
  • 一些分析和优化需要构建程序的整个数据流才能工作。一个很好的例子是公共子表达式消除。
  • 模式匹配写起来更简单

疑问:

  • 在哪找一个mlir数据类型,例如AffineForOp数据类型,我看官方文档里没有,难道只能在头文件里找吗?#include “mlir/Dialect/Affine/IR/AffineOps.h”

chapter-3 Using tablegen for passes

这章介绍了:tablegen

收获:

  • 对于tablegen生成的代码理应弄得很明白,尤其教程指出的部分。
  • 在自动生成的文件中#ifdef GEN_PASS_REGISTRATION这部分代码决定了在tools/tutorial-opt.cpp中注册pass的函数
  • 对于生成代码后需要补充哪些代码:我们必须构建它并读取编译器错误消息,或者将其与基类 (OperationPass) 及其基类 (Pass) 进行比较,以查看剩下实现的唯一函数是 runOnOperation() 等方式。

疑问:

  • td文件中class和def的区别是什么?

​ 答:

  1. class(类):
    • class 在 MLIR 的 TableGen 文件中用于定义一个模板,这个模板可以包含共有的属性和行为。类似于编程中其它语言的类概念,它定义了一个框架。
    • 类可以包含参数,使得在创建具体实例时可以定制这些参数。
    • 使用类可以避免代码重复,并且可以通过继承机制进行扩展。
    • 类不能直接实例化,它必须通过 def 来实例化或作为其他类的基础。
  2. def(定义):
    • def 在 MLIR 的 TableGen 文件中用于创建具体的实例,这些实例是基于 class 或直接定义的。
    • 当你使用 def 时,你实际上是在创建一个具体的对象,这个对象可以是一个操作符、类型或其他元素。
    • 使用 def 时,可以指定所有必要的属性和参数以完整定义一个元素。
    • def 的实例是具体的,可以直接用于生成代码或其他生成任务中。
// A base class for all types in this dialect
class Poly_Type<string name, string typeMnemonic> : TypeDef<Poly_Dialect, name> {
  let mnemonic = typeMnemonic;
}

def Polynomial : Poly_Type<"Polynomial", "poly"> {
  let summary = "A polynomial with u32 coefficients";

  let description = [{
    A type for polynomials with integer coefficients in a single-variable polynomial ring.
  }];
}

chapter-4 Defining a New Dialect

这章介绍了注册新方言(新方言、新数据类型、新操作):

收获:

  • 新定义的方言需要放到register里注册一下才能找到,并且不是mlir原装的方言用的时候需要加!如:!poly.poly

  • tablegen 具有 include 语句(include .td),可以跨文件拆分定义。作者喜欢将每种概念类型的事物放在自己的 tablegen 文件中(类型、操作、属性等),但不同项目的约定有所不同。

  • 新定义的数据类型会自动加上type字段,例如Polynomial会变成PolynomialType。cpp中的数据类型需要用到GET_TYPEDEF_LIST标签来包含自动生成代码里的数据类型。定义类型步骤如下:

    • 定义PolyTypes.td以便生成 PolyTypes.h.inc,同时 PolyTypes.h include PolyTypes.h.inc
    • PolyTypes.cpp.inc 包含在 PolyDialect.cpp 中,以及 PolyTypes.cpp.inc 中自动生成的实现所需的任何附加 #include(例如:PolyTypes.cpp.inc中默认解析器/打印机使用 llvm/include/llvm/ADT/TypeSwitch.h 中的类型切换函数,需要在Polydialect.cpp中引用)
    • 如果需要,请添加 PolyTypes.cpp,其中包含 tablegen 声明的无法自动生成的函数所需的任何其他实现。
  • 为新定义的数据类型添加成员(参数):

      let parameters = (ins "int":$degreeBound); //数据成员的定义
      let assemblyFormat = "`<` $degreeBound `>`"; //汇编格式,即操作在MLIR文本表示中如何显示。
    

    ​ 如果我们有一个更复杂的参数(成员),比如需要分配的数组,我们就必须实现特殊的类来定义这些语义。在最极端的情况下,如果我们有一个完全自定义的类型参数,我们必须手动实现一个存储类(storage),实现 hash_code 之类的东西,并将其注册到方言中。例如heir

疑问:

  • 为什么在bazel里面lib/Dialect/Poly里面文件可以通过 #include “lib/Dialect/Poly/PolyTypes.cpp.inc” 去使用 bazel-bin/lib/Dialect/Poly里的PolyTypes.cpp.inc?

    答:这是由于bazel的沙盒化管理,使之可以正确找到文件。

  • 既然PolyTypes.h.inc是声明,PolyTypes.cpp.inc是实现,那么在我们自己写的cpp文件不是只需要include PolyTypes.h.inc就好了吗?为啥还需要include PolyTypes.cpp.inc?

    答:这种做法在传统的 C++ 开发中比较少见,它是在复杂的代码生成和编译器前端工具,如 MLIR,中处理特定类型的代码重用和动态注册机制的有效策略。这要求开发者对包含 .cpp.inc 文件的具体原因和背后的设计决策有深入的理解。

  • parser/printer在mlir中是必须的吗?

    答:是的

    • Parser:parser 负责将文本形式的 MLIR 代码转换为内存中的 IR 结构。例如,当你编写 MLIR 文本文件,并希望将它加入到编译流程中时,MLIR 的 parser 能够读取这些文本并构建出相应的 IR 实体(如操作、类型、属性等)。
    • Printer:与 parser 相对,printer 作用是将内存中的 IR 转换回文本格式的 MLIR 代码。这对于调试、展示编译过程中的中间状态、或将处理后的结果输出为可读格式等场景非常重要。

chapter-5 Using Traits

这章介绍了traints

收获:

  • traint:特征在 MLIR 中用来指定操作的某些固有属性或行为,它们是编译时的静态属性,与操作的具体实现紧密相关。例如,一些特征可能表明一个操作是可逆的、有副作用或是具有特定的内存访问模式。接口则是类似于c++中的接口概念。(traint用于标注操作或类型具有的固定属性和行为。这些是编译时静态的,而接口则是动态的)(traint通常通过模版类实现的,接口主要是C++的纯虚函数以及继承机制来实现的)
  • traint也能自己定义(通过继承traintbase,或者定义traintlist来定义)
  • 这章告诉我,得熟读源码才行。,比如各类pass,以及定义的traint之类的。

疑问:

  • 作者样例中用了Pure这个traint,这是在一个内部文件(include “mlir/Interfaces/SideEffectInterfaces.td”)中定义的traintlist,这在官方文档中并没有提及,这是因为作者对源码比较熟才直接这么引用的吗?还是说应当对源码比较熟悉。

    答:是的,就使用特征而言,就是这样。然而,要弄清楚每个特征的作用,您必须深入研究pass的实现。所有像 Pure 这样结合了多个特征的“帮助器”定义都没有记录,可用特征和接口的完整列表也没有记录(特征列表缺少很多,比如 ConstantLike、Involution、幂等等)。当我第一次起草这篇文章时,我只将 AlwaysSpeculatable 特征列表应用于 Poly_BinOp,当 --loop-invariant-code-motion 是无操作时,我感到很困惑。我必须深入研究这里的 pass 实现,才能真正看到它还需要使用 MemoryEffectOpInterface 的 isMemoryEffectFree。

  • traint可以自己在.h通过继承traitbase来定义,那么td文件可以直接使用这个trait吗?

    答:这通常比较复杂,我现在的理解是:td中使用的trait通常是官方自己提前定制的。不太支持td文件中使用自己写的triant以及类。因为tablegen本质上是一个生成代码的工具,这个功能对于其来说过于牵强了。

  • 对于使的标量变向量或者张量的实现中def PolyOrContainer : TypeOrContainer<Polynomial, "poly-or-container">;这里的TypeOrContainer是哪里冒出来的用法?

chapter-6 Folders and Constant Propagation

这章介绍了floder和不断传播

什么是floder:fold 属性是 MLIR 操作类的一个成员函数,它的目标是尝试将操作简化成更简洁的形态。如果操作的结果可以被预先计算出来,那么fold 函数可以返回一个代替原操作结果的常数值或更简单的表达形式。fold 可用于向像 sccp 这样的传递发出信号,表明操作的结果是恒定的(静态已知的)。

收获:

  • 关于怎么添加floder优化的步骤:

    • 首先为操作数添加一个def Poly_ConstantOp : Op<Poly_Dialect, "constant", [Pure, ConstantLike]>Constantlike的trait。
    • 在需要fold优化的操作(td文件)中添加:let hasFolder = 1;,并在对应的cpp文件中实现OpFoldResult <OpName>::fold(<OpName>::FoldAdaptor adaptor); fold优化的具体操作。
    • 在方言中添加let hasConstantMaterializer = 1;并在cpp文件中实现materializeConstant方法。
  • floder的局限:折叠只能修改正在折叠的单个操作,使用现有的 SSA 值,并且可能不会创建新操作。因此,他们的权力和明显的本地行动都受到限制。

疑问:

  • 关于在cpp中怎么实现fold方法?,比如说其中的FoldAdaptor是啥?以及怎么实现materializeConstant方法?在哪里去学习?

    答:我目前的理解:有两种可能性,一是fold这个比较常用?所以我看官方教程中也有介绍,难道看看官方教程就会了?二是google一下这个会发现mlir的源代码中也有不少实现fold的操作,难道看看源代码就可以学会了?

  • 关于像ConstantLike以及AnyIntElementsAttr特性的td声明在哪个文件怎么快速找到呢?现在只能顺藤摸瓜,慢慢的找到。,

chapter-7 Verifiers

这章介绍了验证者

**目的:**验证者确保具体 MLIR 程序中的类型和操作格式良好。验证器在每次传递之前和之后运行,有助于确保各个传递、文件夹、重写模式等发出正确的 IR。

收获:

  • inferReturnTypes 函数被应用,可以自动进行类型替换,发生以下替换,简便一点:

    //(type, type) -> type 到 type
       // %2 = poly.mul %p0, %p0 : (!poly.poly<10>, !poly.poly<10>) -> !poly.poly<10>
        %2 = poly.mul %p0, %p0 : !poly.poly<10>
    
  • 每一个trait都有verifyTrait方法,其会覆盖基类的verifyInvariants方法

  • 定义verifier的步骤:

    • 定义一个custom verifier

      • 在td文件需要验证的操作中加入let hasVerifier = 1;
      • 在h头文件中对应的类中实现verify()这个函数
    • 定义一个trait-based custom verifier

      • 定义一个trait,例如

        def Has32BitArguments:  NativeOpTrait<"Has32BitArguments"> {
          let cppNamespace = "::mlir::tutorial::poly";
        }
        
      • 在td文件中对应的操作后面添加这个trait。

      • 在cpp文件中实现这个triat,实现verifyTrait()这个函数。

  • 两种verifier对比:trait-based cuntom verifier显然更加通用,但是需要笨拙的转换来支持特定的操作及其命名参数。

chapter-8 Canonicalizers and Declarative Rewrite Patterns

这章主要介绍了规范化器和声明性重写模式:对canonicalize pass的添加额外操作以及对pattern模式进行td文件(tablegen)声明式重写。与folder不同的是,规范化可以包含更复杂的重写规则,例如合并相似的操作、消除冗余的操作、重新排序操作以提高后续优化的机会等。

收获:

  • 写规范化通常由三种方式:

    • 通过在 tablegen 中声明操作canonicalizer,然后实现生成的 C++ 函数声明

      • 在td文件中加入let hasCanonicalizer = 1;声明

      • 并在cpp文件中实现相应的模式并应用上。

      • 完全基于tablegen

        • 在td文件中加入let hasCanonicalizer = 1;声明
        • 在td文件中通过pat类声明一个pattern,并通过mlir-opt加上-gen-rewriters标签进行生成pattern。
        • 在cpp文件中通过populateWithGenerated或者自己手动添加模式的方法加入
        • 必要的时候需要读一下源代码写的很好:PatternBase.td以及普通文档
    • 基于PDLL,还在学习中。

疑问:

  • 这个complex类型是mlir自带的数据类型还是以前定义好的?

       // poly_syntax.mlir
       %z = complex.constant [1.0, 2.0] : complex<f64>
        // CHECK: poly.eval
        %complex_eval = poly.eval %4, %z : (!poly.poly<10>, complex<f64>) -> complex<f64>
    

    答:这是mlir自带的。

  • 似乎这两种规划法的定义会自动集成在canonicalize这个pass中,mlir将该pass设计成可扩展类型的。

  • 在td文件中定义约束(对于pattern的使用),同时对这一段的补充解释似乎很重要,但是不是很懂。[https://mlir.llvm.org/docs/Dialects/ArithOps/]

    // PolyPatterns.td
    def HasOneUse: Constraint<CPred<"$_self.hasOneUse()">, "has one use">;
    
    // Rewrites (x^2 - y^2) as (x+y)(x-y) if x^2 and y^2 have no other uses.
    def DifferenceOfSquares : Pattern<
      (Poly_SubOp (Poly_MulOp:$lhs $x, $x), (Poly_MulOp:$rhs $y, $y)),
      [
        (Poly_AddOp:$sum $x, $y),
        (Poly_SubOp:$diff $x, $y),
        (Poly_MulOp:$res $sum, $diff),
      ],
      [(HasOneUse:$lhs), (HasOneUse:$rhs)]
    >;
    

chapter-9 Dialect Conversion

本章节介绍了在mlir中如何进行方言转换

收获:

  • MLIR 的所有 Op 都有一个统一的储存格式,叫 OperationOperation 里面存了 OpName 和所有的 operands, results, attributes 和其它的东西。

    • 用户定义的 arith.addi 等等 Op,本质上都是 Operation 的指针。但与 Operation* 不同的是,AddIOp 定义了 Operation 里储存的数据的解释方式。如 AddOp,自己是一个 Operation 的指针,也定义了一个函数 getLhs 用来返回第一个值,当作 lhs。
      在这里插入图片描述
  • 如果不是类型,方言转换与普通的pass无异(因为有类型转换,一旦您更改了特定值的类型,例如,在降低产生该值作为输出的操作时,那么该值的所有下游代码仍然期望旧类型,这会使操作的验证器报错),每个操作都会有一个需要lower的重写模式。

  • mlir的方言转换框架:

    • 首先它分离了操作和类型的转换,使用户只需专注考虑操作的转换即可。
    • 定义的重写模式(rewriting patterns)包含了如何转换特定操作和类型的逻辑。访问每个操作时,不仅可以看到操作的原始类型,也能看到正在进行的转换类型。
    • 框架内部会按一定的排序顺序处理操作的降低。(这点有待后续探索,应该是会自动判断操作之间的依赖关系实现的)
    • 通过 bufferization pipeline将按值的操作的 IR 转换为具有“指针语义”的操作。
  • 如何使用方言转换框架

    • 首先使用tablegen申明一个pass,记得使用 let dependentDialects声明好依赖
    • 定义一个TypeConverter(定义操作数的类型转换规则),类型转换器是配合着OpConversionPattern来的,它指示了何时进行操作数的类型转换。每一个TypeConverter可能会定义多个规则,例如下面则定义了两个规则,一个操作数的类型如果匹配不到相对应的特定规则,则会执行默认规则
    class PolyToStandardTypeConverter : public TypeConverter {
     public:
      PolyToStandardTypeConverter(MLIRContext *ctx) {
        addConversion([](Type type) { return type; }); //默认规则,表示类型有效
        addConversion([ctx](PolynomialType type) -> Type { //特定规则,对PolynomialType进行转换
          int degreeBound = type.getDegreeBound();
          IntegerType elementTy =
              IntegerType::get(ctx, 32, IntegerType::SignednessSemantics::Signless);
          return RankedTensorType::get({degreeBound}, elementTy);
        });
      }
    };
    
    • 定义OpConversionPattern,定义何时进行操作的类型替换。其中matchAndRewrite函数有三个参数:
      • 第一个 OpAdaptor 是 AddOp::Adaptor 的别名,它是通过tablegen生成的 C++ 代码的一部分,在方言转换期间保存类型转换后的操作数。比getOperand 更加方便使用。
      • AddOp 参数(与正常重写模式相同)包含原始的、未类型转换的操作数和结果。
      • ConversionPatternRewriter 类似于 PatternRewriter,但它具有与方言转换相关的其他方法,例如 ConvertRegionTypes
    struct ConvertAdd : public OpConversionPattern<AddOp> {
      using OpConversionPattern::OpConversionPattern; //关键步骤,直接加载父类的构造函数
    
      LogicalResult matchAndRewrite(
          AddOp op, OpAdaptor adaptor,
          ConversionPatternRewriter &rewriter) const override {
        // ...
        return success();
      }
    };
    
    • 定义 ConversionTarget,用于标记哪些操作或者方言是不合法的,通过target.addIllegalDialect来声明不合法的方言,(不合法的包括方言中操作,类型)或者 target.addIllegalOp来声明不合法的操作,更复杂的也可以有条件的合法操作如: addDynamicallyLegalDialect
    • 定义RewritePatternSet,与一般的RewritePatternSet不同的是,它的参数增加了typeConverter,它将类型转换器作为输入并将其传递给 ConvertAdd 的构造函数,ConvertAdd 的父类将类型转换器作为输入并将其存储为成员变量。(这一切都是因为ConvertAdd类加载了父类的构造函数,所以可以自动调用)。
      RewritePatternSet patterns(context); //定义模式
      PolyToStandardTypeConverter typeConverter(context);
      patterns.add<ConvertAdd>(typeConverter, context); //添加模式
    
    • 使用遍历函数来降低操作(applyPartialConversionapplyPatternsAndFoldGreedily等)(applyPartialConversion错误消息更好。如果 applyFullConversion 失败,即使在调试模式下,您也无法获得有关出错原因的大量信息。)
        if (failed(applyPartialConversion(module, target, std::move(patterns)))) {   // <-- new thing 遍历
          signalPassFailure();}
    

疑问:

  • 关于实践中的一个错误:当进行类型转换时,会卡错在builtin.unrealized_conversion_cast,官方文档对这个函数指出:

    • This operation should not be attributed any special representational or execution semantics, and is generally only intended to be used to satisfy the temporary intermixing of type systems during the conversion of one type system to another.

    作为一个类型系统,需要满足对func/call/return if/then/else等支持,所以需要在代码中补充以下代码:

        populateFunctionOpInterfaceTypeConversionPattern<func::FuncOp>( //以func为例
            patterns, typeConverter);
        target.addDynamicallyLegalOp<func::FuncOp>([&](func::FuncOp op) { //增加对func::FuncOp的合法性规则
          return typeConverter.isSignatureLegal(op.getFunctionType()) &&
                 typeConverter.isLegal(&op.getBody()); });
    

    当然也可以使用Materialization hooks绕过出错的类型(仅当类型冲突必须在多次传递中持续存在时才有必要——这句话目前理解不是很深刻),

    // Convert from a tensor type to a poly type: use from_tensor
    addSourceMaterialization([](OpBuilder &builder, Type type, ValueRange inputs, Location loc) -> Value { 
        //这段代码会在lower的过程中自动判断是否以及在合适的位置去执行
      return builder.create<poly::FromTensorOp>(loc, type, inputs[0]);
    });
    
  • 关于lower过程中不同的RewritePatern之间或许存在依赖关系(例如需要a和b全部转换为c类型,但是a可能只转换为b类型,然后b再同一转换为c类型),同时mlir也会自动判断并按照正确的执行顺序去执行。这其中的机理有待探索。

  • 27
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值