Table-driven Declarative Rewrite Rule (DRR
- 好處
- 規則定義
- 原模式
- 基于位置的匹配
- 操作的匹配有向無環圖(DAG)
- (AOp (BOp), $attr):
- 綁定操作的結果
好處
模式創建者只需要聲明性地指定重寫模式,而不必擔心調用具體的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 = (insAnyType:$a_input,AnyAttr:$a_attr);let results = (outsAnyType:$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 = (insAnyType:$a_input,AnyAttr:$a_attr);let results = (outsAnyType:$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 = (outsAnyType:$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) -> i32return %1 : i32
}
在這個示例中,BOp的結果(%0)被綁定到變量$b_result,并在AOp中作為輸入使用。這種綁定方式在模式匹配和轉換中非常有用。