Table-driven Declarative Rewrite Rule (DRR)

发布于:2024-07-06 ⋅ 阅读:(36) ⋅ 点赞:(0)

好处

模式创建者只需要声明性地指定重写模式,而不必担心调用具体的C++方法。
消除样板代码,展示重写的核心:mlir::RewritePattern已经能够很好地隐藏定义重写规则的样板代码。但是我们仍然需要编写C++编程语言所需的类和函数结构,检查操作符以进行匹配,并调用操作符的build()方法进行构建。这些语句通常非常简单且相似,因此可以进一步通过自动生成来简化。由于我们将样板代码减少到最低限度,声明性的重写规则将只包含重写的核心要素。这使得模式非常容易理解。

规则定义

重写规则的核心构造是在[PatternBase.td][PatternBase]中定义的:

class Pattern<
    dag sourcePattern, list<dag> resultPatterns,
    list<dag> additionalConstraints = [],
    list<dag> supplementalPatterns = [],
    dag benefitsAdded = (addBenefit 0)>;

一个声明式的重写规则包含两个主要组件:

  • 源模式,用于匹配操作的DAG。
  • 一个或多个结果模式,用于生成操作的DAG以替换匹配到的DAG。

我们允许多个结果模式以支持多结果操作和辅助操作,但通常我们只是想将一个操作的DAG转换为另一个操作的DAG。有一个方便的Pattern包装器,Pat,它接受一个单一的结果模式:

class Pat<
    dag sourcePattern, dag resultPattern,
    list<dag> additionalConstraints = [],
    dag benefitsAdded = (addBenefit 0)> :
  Pattern<sourcePattern, [resultPattern], additionalConstraints, benefitsAdded>;

每个模式被指定为一个TableGen的DAG对象,语法为(operator arg0, arg1, …)。

  • operator 通常是一个MLIR操作,但它也可以是其他指令。
  • argN 用于匹配(如果在源模式中使用)或生成(如果在结果模式中使用)operator的第N个参数。如果操作是某个MLIR操作,这意味着第N个参数如操作定义的参数列表中所指定。因此,我们说模式中的操作参数规范是基于位置的:它们出现的位置很重要。

argN 本身可以是一个DAG对象,因此我们可以有嵌套的DAG树来建模操作之间的定义-使用关系。

比如加法重写为乘法

def : Pat<
    (AddI32 $x, $y),
    (MulI32 $x, $y)
>;

假设我们有一个32位整数加法操作 AddI32,我们希望将其转换为一个乘法操作 MulI32 和一个减法操作 SubI32。以下是一个声明式重写规则的例子:

def : Pattern<
    (AddI32 $x, $y), 
    [(MulI32 $x, $y), (SubI32 $x, $y)]
>;
  • def : Pat:用于定义简单的匹配和替换规则,适用于简单的操作模式。
  • def : Pattern:用于定义更复杂的匹配和替换规则,可以包含更多的属性和逻辑,适用于复杂的操作模式。

原模式

源模式用于匹配操作的有向无环图(DAG)。DAG对象中的参数旨在捕获操作的参数。它们还可以用于进一步限制匹配条件。捕获是通过指定以 $ 符号开头的符号来完成的,而进一步的约束是通过指定 TypeConstraint(对于操作数)或 AttrConstraint(对于属性)来引入的。

在这个上下文中,我们可以将操作的参数捕获下来。捕获的方式是通过使用 $ 符号开头的标识符,例如 $a_input 和 $a_attr。我们还可以通过 TypeConstraint(类型约束)和 AttrConstraint(属性约束)来进一步限制这些参数。

def AOp : Op<"a_op"> {
    let arguments = (ins
      AnyType:$a_input,
      AnyAttr:$a_attr
    );

    let results = (outs
      AnyType:$a_output
    );
}

在这个例子中,我们定义了一个操作 AOp,它有两个参数:

  • $a_input,可以是任何类型(AnyType)。
  • $a_attr,可以是任何属性(AnyAttr)。

操作的结果定义为一个输出($a_output),它也是任意类型。

接下来,我们定义一个模式:

def : Pat<(AOp $input, F32Attr:$attr), ...>;

这个模式匹配一个 AOp 操作,其中:

  • $input 可以是任何有效的输入。
  • $attr 必须是一个浮点属性(F32Attr)。

如果这个模式匹配成功,我们会将 $input 绑定到操作的输入 $a_input,并将 $attr 绑定到操作的属性 $a_attr。之后,我们可以在其他模式和约束中使用这些绑定。

位置匹配和符号使用

在定义模式(pattern)的时候,你不需要严格遵循操作(operation)定义中使用的符号名称。换句话说,你可以使用不同的符号名称来匹配操作定义中的参数,只要它们的位置对应即可。让我们通过一个具体的例子来说明这一点

假设我们有一个操作定义如下:

def AOp : Op<"a_op"> {
    let arguments = (ins
      AnyType:$a_input,
      AnyAttr:$a_attr
    );

    let results = (outs
      AnyType:$a_output
    );
}

基于位置的匹配

当我们定义一个模式来匹配这个操作时,位置(顺序)比符号名称更重要。也就是说,你可以在模式中使用不同的符号名称来引用这些参数,只要它们的位置正确。例如:

def : Pat<(AOp $input, F32Attr:$attr), ...>;

在这个模式中:

  • $input 对应于 AOp 操作的第一个参数 $a_input。
  • $attr 对应于 AOp 操作的第二个参数 $a_attr,并且施加了浮点属性(F32Attr)的约束。

尽管符号名称不同),但因为它们的位置与 AOp 操作定义中的参数位置一致,所以匹配依然有效。

操作的匹配有向无环图(DAG)

要匹配一个操作的有向无环图(DAG),使用嵌套的DAG对象:

def BOp : Op<"b_op"> {
    let arguments = (ins);

    let results = (outs
      AnyType:$b_output
    );
}

def : Pat<(AOp (BOp), $attr), ...>;

(AOp (BOp), $attr):

这部分表示我们想要匹配的操作图结构。

  • AOp 是我们想要匹配的主要操作(Op)。
  • (BOp) 表示 AOp 的输入必须由 BOp 生成。换句话说,AOp 的唯一输入必须是 BOp 的输出。
  • $attr 是一个占位符,表示 AOp 操作可能有某些属性,我们用 $attr 来匹配这些属性。
    所以,我们可能会匹配到这样的两行IR
%0 = "b_op"() : () -> f32
%1 = "a_op"(%0) {attr = "example"} : (f32) -> i32

绑定操作的结果

在编写某种模式匹配的代码时,有时候我们需要将一个符号(变量)与某个操作的结果进行绑定,以便在后续的代码中可以引用这个结果。通过在操作定义中附加这个符号,我们就可以实现这种绑定。

举个MLIR(多级中间表示)的例子:
假设我们有两个操作AOp和BOp,并且BOp有一个结果需要在AOp中使用。我们可以这样定义一个模式(pattern):

def : Pat<(AOp (BOp:$b_result), $attr), /* 替换规则 */>;

在这个例子中,$b_result 就被绑定到 BOp 的结果上,这样我们在后续的模式或替换规则中可以引用 $b_result。

func @example_func() -> i32 {
  %0 = "BOp"() : () -> i32
  %1 = "AOp"(%0) : (i32) -> i32
  return %1 : i32
}

在这个示例中,BOp的结果(%0)被绑定到变量$b_result,并在AOp中作为输入使用。这种绑定方式在模式匹配和转换中非常有用。


网站公告

今日签到

点亮在社区的每一天
去签到