이 문서는 MLIR를 대상으로 하는 패턴 재작성 작성을 위한 사용자 정의 프런트엔드 언어인 PDL Language(PDLL)를 자세히 설명합니다.
이 문서는 MLIR를 대상으로 하는 패턴 재작성 작성을 위한 사용자 정의 프런트엔드 언어인 PDL Language(PDLL)를 자세히 설명합니다.
참고: 이 문서는 MLIR 개념에 대한 익숙함을 전제로 합니다. 보다 구체적으로는 MLIR Pattern Rewriting 및 Operation Definition Specification (ODS) 문서에 설명된 개념을 전제로 합니다.
패턴 매칭은 MLIR 내에서 매우 중요한 구성 요소입니다. 이는 컴파일러의 다양한 측면을 포괄하기 때문입니다. 정준화에서 최적화, 변환에 이르기까지 MLIR 기반 컴파일러는 어떤 형태로든 패턴 매칭 인프라에 크게 의존하게 됩니다.
PDL Language(PDLL)는 MLIR 패턴 재작성을 표현하기 위해 처음부터 설계된 선언적 패턴 언어를 제공합니다. PDLL은 MLIR의 모든 구성 요소에 대한 매처를 직관적인 인터페이스로 네이티브하게 작성할 수 있도록 설계되었으며, 이 인터페이스는 AOT(ahead-of-time) 및 JIT(just-in-time) 패턴 컴파일 모두에 사용할 수 있습니다.
이 절에서는 PDLL을 설계할 때의 여러 설계 결정, 그 근거, 그리고 고려했던 대안에 대한 자세한 내용을 제공합니다. 소프트웨어 개발의 특성상, 이 절에는 더 이상 존재하지 않는 MLIR 컴파일러 영역에 대한 참조가 포함될 수 있습니다.
참고: 이 절은 TDRR에 대한 익숙함을 전제로 하므로, 계속하기 전에 관련 문서를 참조하십시오.
Tablegen DRR(TDRR), 즉 Table-driven Declarative Rewrite Rules는 TableGen 언어 내에서 MLIR 패턴 재작성을 정의하기 위한 선언적 DSL입니다. 이 인프라는 현재 MLIR 내에서 패턴을 선언적으로 정의하는 주요 방식입니다. TDRR은 TableGen의 dag 지원을 활용하여 DAG 구조에 잘 맞는 MLIR 패턴을 정의할 수 있게 해주며, 이는 LLVM의 백엔드 인프라(SelectionDAG/Global Isel/etc.)를 위한 패턴을 tablegen으로 정의해 온 방식과 유사합니다. 그러나 불행히도 TableGen 언어는 LLVM에서 그랬던 것만큼 MLIR 패턴의 구조에 잘 맞지 않습니다.
TDRR의 문제는 대부분 DSL의 호스트 언어로 TableGen을 사용한다는 점에서 비롯됩니다. 이러한 문제는 TableGen 구조와 MLIR 구조 사이의 불일치, 그리고 TableGen이 MLIR과 다른 동기와 목표를 가진다는 점에서 발생했습니다. 지금까지 TDRR에서 마주친 문제의 대부분은(고집의 정도에 따라 전부일 수도 있습니다) 어떤 형태로든 해결 가능했습니다. 다만 문제는 이러한 해법이 종종 우리가 원하는 것보다 더 “기발한” 방향이었다는 점입니다. 이것이 문제였고, 우리가 TDRR 개선에 더 큰 노력을 투자하지 않기로 한 이유이기도 합니다. 사용자는 일반적으로 “기발한” API를 원하지 않습니다. 읽고 쓰기에 직관적인 것을 원합니다.
이러한 문제 중 일부를 강조하기 위해, 아래에서는 발생했던 몇 가지 문제와 우리가 그것들을 어떻게 “해결”했는지 살펴보겠습니다.
MLIR은 가변 개수의 연산 결과를 네이티브하게 지원합니다. TDRR의 DAG 기반 구조에서는 다중 결과(이 경우 연산)의 어떤 형태든 문제가 됩니다. 이는 DAG가 단일 루트 노드를 원하고, 여러 결과에 인덱스를 붙이거나 이름을 지정하는 좋은 수단이 없기 때문입니다. 이것이 어떻게 드러나는지 간단한 예를 보겠습니다.
// Suppose we have a three result operation, defined as seen below.
def ThreeResultOp : Op<"three_result_op"> {
let arguments = (ins ...);
let results = (outs
AnyTensor:$output1,
AnyTensor:$output2,
AnyTensor:$output3
);
}
// To bind the results of `ThreeResultOp` in a TDRR pattern, we bind all results
// to a single name and use a special naming convention: `__N`, where `N` is the
// N-th result.
def : Pattern<(ThreeResultOp:$results ...),
[(... $results__0), ..., (... $results__2), ...]>;
TDRR에서는 다중 결과에 접근하는 문제를 “해결”했지만, 이것은 사용자에게 그다지 직관적인 인터페이스가 아닙니다. 마법 같은 명명 규칙은 코드를 흐리게 만들고 버그나 기타 오류를 쉽게 유발할 수 있습니다. 이 상황을 개선하기 위해 시도할 수 있는 여러 가지가 있지만, TableGen dag 구조의 한계 때문에 우리가 할 수 있는 일에는 근본적인 제한이 있습니다. 반면 PDLL에서는 구조와 무관하게 연산에 대한 올바른 인터페이스를 제공할 수 있는 자유와 유연성이 있습니다.
// Import our definition of `ThreeResultOp`.
#include "ops.td"
Pattern {
...
// In PDLL, we can directly reference the results of an operation variable.
// This provides a closer mental model to what the user expects.
let threeResultOp = op<my_dialect.three_result_op>;
let userOp = op<my_dialect.user_op>(threeResultOp.output1, ..., threeResultOp.output3);
...
}
TDRR에서는 match dag가 매칭할 입력 IR의 일반 구조를 정의합니다. 입력에 대한 비구조적/비타입 제약조건은 일반적으로 rewrite dag 뒤에 지정되는 제약조건 목록으로 밀려납니다. 매우 단순한 패턴에는 이것으로 충분할 수 있지만, 패턴이 커질수록 제약조건이 적용 대상과 분리되어 패턴의 가독성에 악영향을 주기 때문에 꽤 문제가 됩니다. 예로, 입력에 추가 제약조건을 더하는 간단한 패턴을 보겠습니다.
// Suppose we have a two result operation, defined as seen below.
def TwoResultOp : Op<"two_result_op"> {
let arguments = (ins ...);
let results = (outs
AnyTensor:$output1,
AnyTensor:$output2
);
}
// A simple constraint to check if a value is use_empty.
def HasNoUseOf: Constraint<CPred<"$_self.use_empty()">, "has no use">;
// Check if two values have a ShapedType with the same element type.
def HasSameElementType : Constraint<
CPred<"cast<ShapedType>($0.getType()).getElementType() == "
"cast<ShapedType>($1.getType()).getElementType()">,
"values have same element type">;
def : Pattern<(TwoResultOp:$results $input),
[(...), (...)],
[(HasNoUseOf:$results__1),
(HasSameElementType $results__0, $input)]>;
위 예시에서 제약조건을 살펴볼 때, 우리는 입력 dag 전체를 뒤져 해당 입력을 찾아야 합니다(다중 결과에 대한 마법 같은 명명 규칙도 염두에 두어야 합니다). 이 단순한 패턴에서는 불과 몇 줄 위에 있을 수 있지만, 복잡한 패턴은 종종 수십 줄까지 길어집니다. PDLL에서는 이러한 제약조건을 적용 대상에 직접 또는 바로 옆에 적용할 수 있습니다.
// The same constraints that we defined above:
Constraint HasNoUseOf(value: Value) [{
return success(value.use_empty());
}];
Constraint HasSameElementType(value1: Value, value2: Value) [{
return success(cast<ShapedType>(value1.getType()).getElementType() ==
cast<ShapedType>(value2.getType()).getElementType());
}];
Pattern {
// In PDLL, we can apply the constraint as early (or as late) as we want. This
// enables better structuring of the matcher code, and improves the
// readability/maintainability of the pattern.
let op = op<my_dialect.two_result_op>(input: Value);
HasNoUseOf(op.output2);
HasSameElementType(input, op.output2);
// ...
}
패턴은 종종 N개의 입력 연산을 N개의 결과 연산으로 변환합니다. PDLL에서는 두 개의 replace 문을 추가하는 것만으로 여러 연산을 교체할 수 있습니다. TDRR에서는 상황이 조금 더 미묘합니다. TableGen dag의 단일 루트 구조 때문에, 루트가 아닌 연산을 교체하는 것은 깔끔하게 지원되지 않습니다. 현재는 이를 네이티브하게 수행할 수 없고, 대신 여러 패턴을 사용해야 합니다. 다른 특수 rewrite 지시어를 추가하거나 replaceWithValue를 확장할 수도 있겠지만, 이것은 기본적인 IR 변환조차 호스트 언어의 복잡성 때문에 흐려진다는 점을 보여줄 뿐입니다.
그렇습니다! 정확히는 그렇기도 하고 아니기도 합니다. 그 이유를 이해하려면 우리가 어떤 종류의 사용자를 지원하려는지, 그리고 그들에게 어떤 제약을 강제하는지를 고려해야 합니다. PDLL의 목표는 호스트 환경과 무관하게 MLIR의 모든 사용자가 즉시 상호작용할 수 있는 기본적이고 효과적인 MLIR 패턴 언어를 제공하는 것입니다. 이 언어는 추가 의존성 없이 사용할 수 있으며 MLIR과 함께 “무료로” 제공됩니다. 기존 호스트 언어를 사용해 새로운 DSL을 만든다면, 그 언어에 따라 여러 절충을 해야 합니다. 어떤 경우에는 일치하는 실행 환경을 어떻게 강제할 것인지(python2인가 python3인가?, 어느 버전인가?), 성능 고려사항, 통합 등과 같은 문제가 있습니다. LLVM 프로젝트의 일환으로서는, MLIR 사용자에게 새로운 언어 의존성을 강제하는 것을 의미할 수도 있습니다(그들 중 다수는 그렇지 않으면 그런 의존성을 원하지 않거나 필요로 하지 않을 수 있습니다). 또한 다른 언어에 내장된 DSL이라면 언제나 따라오는 또 다른 문제도 있습니다. 즉, 사용자가 호스트 언어에서 기대하는 것과 우리의 “백엔드”가 지원하는 것 사이의 불일치를 완화해야 합니다. 예를 들어, PDL IR 추상화는 제어 흐름에 대해 제한적인 지원만 제공합니다. 우리가 Python 안에 DSL을 만든다면, 복잡한 제어 흐름이 완전히 처리되거나 적절히 오류를 내도록 보장해야 합니다. 이상적인 오류 처리조차 있더라도, 기대한 기능을 사용할 수 없다는 사실은 사용자에게 좌절감을 줍니다. 환경 제약 외에도 언어 도구화 문제도 있습니다. PDLL에서는 패턴 개발자의 요구를 충족하도록 설계된 매우 견고하고 현대적인 도구 세트를 구축하려고 하며, 여기에는 코드 완성, 시그니처 도움말, 그리고 우리가 해결하려는 문제에 특화된 많은 기능이 포함됩니다. 기존 언어에 사용자 정의 언어 도구를 통합하는 것은 어렵고, 어떤 경우에는 불가능하기도 합니다(우리 DSL은 기존 언어의 작은 부분집합에 불과할 수 있기 때문입니다).
이러한 여러 요인은 우리가 사용자에게 제공할 수 있는 가장 효과적인 도구가 해당 문제를 위해 설계된 사용자 정의 도구라는 초기 결론으로 이어졌습니다. 그렇다고 해서 모든 사용자가 우리가 스스로에게 부과한 제약을 동일하게 가지는 것은 아니라는 점도 이해합니다. 우리는 서로 다른 언어로 정의된 다양한 PDL 프런트엔드의 존재를 분명히 장려하고 지원합니다. 이것이 애초에 PDL IR 추상화를 만든 원래의 동기 중 하나였습니다. 사용자(그리고 그들의 사용자)를 위한 혁신과 유연성을 가능하게 하기 위해서입니다. 예를 들어 연구나 Machine Learning 분야의 일부 사용자들은 이미 특정 언어(예: Python)를 워크플로에 깊게 통합해 두었을 수 있습니다. 이러한 사용자에게는 그들의 언어 안에 있는 PDL DSL이 이상적일 수 있으며, 우리는 인프라 관점에서 이를 계속 지원하고 지지할 것입니다.
참고: PDLL은 여전히 활발히 개발 중이며, 아래에서 논의하는 설계는 반드시 최종안이 아니고 변경될 수 있습니다.
PDLL의 설계는 PDL IR 추상화의 영향을 크게 받고 그것을 중심으로 구성되며, 이 추상화 자체는 핵심 MLIR 구조의 추상 모델로 설계되었습니다. 이로 인해 마치 매칭하려는 IR을 직접 작성하는 것과 매우 유사하게 느껴지는 설계와 구조가 만들어집니다.
PDLL은 다른 소스 파일에 정의된 내용을 가져오기 위한 include 지시어를 지원합니다. 포함할 수 있는 파일에는 두 가지 유형이 있습니다: .pdll 파일과 .td 파일입니다.
.pdll include¶.pdll 파일을 포함하면, 해당 파일의 내용은 현재 처리 중인 파일에 직접 복사됩니다. 이는 그 파일 안에 정의된 패턴, 제약조건, rewrite 등이 현재 파일의 내용과 함께 처리된다는 뜻입니다.
.td include¶.td 파일을 포함하면, PDLL은 그 파일 안의 관련 ODS 정보를 자동으로 가져옵니다. 여기에는 정의된 연산, 제약조건, 인터페이스 등이 포함되며, PDLL 내에서 암묵적으로 접근할 수 있게 됩니다. 이것은 중요합니다. ODS 정보가 있으면 operation 표현식과 같은 특정 PDLL 구성 요소가 훨씬 더 강력해질 수 있기 때문입니다.
어떤 패턴 기술 언어에서든 패턴 정의는 핵심입니다. PDLL에서 패턴은 Pattern으로 시작하며, 뒤에 선택적으로 이름과 패턴 메타데이터 집합이 오고, 마지막으로 패턴 본문으로 끝납니다. 아래에 몇 가지 간단한 예를 보여줍니다.
// Here we have defined an anonymous pattern:
Pattern {
// Pattern bodies are separated into two components:
// * Match Section
// - Describes the input IR.
let root = op<toy.reshape>(op<toy.reshape>(arg: Value));
// * Rewrite Section
// - Describes how to transform the IR.
// - Last statement starts the rewrite.
replace root with op<toy.reshape>(arg);
}
// Here we have defined a pattern named `ReshapeReshapeOptPattern` with a
// benefit of 10:
Pattern ReshapeReshapeOptPattern with benefit(10) {
replace op<toy.reshape>(op<toy.reshape>(arg: Value))
with op<toy.reshape>(arg);
}
패턴 메타데이터 정의 뒤에는 패턴 본문을 지정합니다. 패턴 본문의 구조는 match 절과 rewrite 절이라는 두 개의 주요 절로 이루어집니다. 패턴의 match 절은 기대하는 입력 IR을 설명하고, rewrite 절은 해당 IR을 어떻게 변환할지를 설명합니다. 이 구분은 중요합니다. PDLL은 서로 다른 절 안에서 특정 변수와 표현식을 다르게 처리하기 때문입니다. 아래 각 절에서 관련되는 경우, 동작 차이를 명시적으로 설명하겠습니다.
match 절과 rewrite 절의 일반적인 배치는 다음과 같습니다. 패턴 본문의 마지막 문은 반드시 operation rewrite statement여야 하며, 이것이 rewrite 절을 나타냅니다. 그 이전의 모든 문은 match 절을 나타냅니다.
MLIR의 rewrite 패턴에는 특정 동작을 제어하고, 패턴을 적용하는 rewrite 드라이버에 정보를 제공하는 메타데이터 집합이 있습니다. PDLL에서는 패턴 이름 뒤에 이 메타데이터의 기본값이 아닌 값을 제공할 수 있습니다. 아래에는 지원되는 다양한 메타데이터 유형의 예를 보여줍니다.
Pattern의 benefit은 해당 패턴이 매칭되었을 때의 “이점”을 나타내는 정수값입니다. 패턴 드라이버는 이를 사용하여 적용 시 패턴들의 상대적 우선순위를 결정합니다. 일반적으로 benefit이 더 높은 패턴이 더 낮은 패턴보다 먼저 적용됩니다.
PDLL에서 패턴의 기본 benefit은 match 절에 있는 입력 연산 수, 즉 서로 구별되는 Op 표현식/변수의 개수로 설정됩니다. 이 규칙은 더 큰 매치가 더 작은 매치보다 더 유익하며, 작은 것이 먼저 적용되면 큰 것이 더 이상 적용되지 않을 수 있다는 관찰에 기반합니다. 패턴은 메타데이터 절에서 benefit을 지정하여 이 동작을 재정의할 수 있습니다.
// Here we specify that this pattern has a benefit of `10`, overriding the
// default behavior.
Pattern with benefit(10) {
...
}
패턴 적용 중에는, 동일한 패턴이 이전에 그 패턴이 적용된 결과에 다시 적용될 수 있는 상황이 있습니다. 패턴이 이러한 재귀적 적용을 올바르게 처리하지 못하면, 패턴 드라이버는 무한 적용 루프에 빠질 수 있습니다. 이를 방지하기 위해, 기본적으로 패턴은 적절한 재귀 경계 처리가 없다고 가정되며 재귀적으로 적용되지 않습니다. 패턴은 패턴 메타데이터 절에 recusion 플래그를 지정함으로써 재귀를 적절히 처리한다고 알릴 수 있습니다.
// Here we signal that this pattern properly bounds recursive application.
Pattern with recusion {
...
}
패턴은 일반적으로 아래와 같이 복합 문 블록으로 본문을 정의합니다.
Pattern {
replace op<my_dialect.foo>(operands: ValueRange) with operands;
}
패턴은 단순한 단일 행 본문을 지정하기 위한 람다 유사 문법도 지원합니다. Pattern의 람다 본문은 단일 operation rewrite statement를 기대합니다.
Pattern => replace op<my_dialect.foo>(operands: ValueRange) with operands;
PDLL의 변수는 Value, Operation, Type 등과 같은 IR 엔터티의 특정 인스턴스를 나타냅니다. 아래의 간단한 패턴을 보겠습니다.
Pattern {
let value: Value;
let root = op<mydialect.foo>(value);
replace root with value;
}
이 패턴에서는 let 문을 사용해 value와 root라는 두 변수를 정의합니다. let 문은 변수를 정의하고 제약을 부여할 수 있게 해줍니다. PDLL의 모든 변수는 특정 타입을 가지며, 이 타입은 해당 변수가 어떤 종류의 IR 엔터티를 나타내는지 정의합니다. 변수의 타입은 제약조건이나 초기화 표현식으로 결정될 수 있습니다.
타입을 갖는 것 외에도, 변수는 초기화 표현식 또는 패턴의 match 절 안에서 네이티브가 아닌 제약조건이나 rewrite 사용을 통해 반드시 “바인딩”되어야 합니다. 변수의 “바인딩”은 그 변수가 입력 IR(match 절) 또는 출력 IR(rewrite 절)에서 무엇을 가리키는지를 문맥적으로 식별합니다. match 절에서는 이를 통해 패턴의 루트 연산으로부터 매치 트리를 구성할 수 있으며, 이 루트 연산은 패턴의 rewrite 절을 나타내는 operation rewrite statement에 “바인딩”되어야 합니다. match 절의 모든 비루트 변수는 어떤 방식으로든 “루트” 연산에 바인딩되어야 합니다. 개념을 더 잘 설명하기 위해 간단한 예를 보겠습니다. 아래의 .mlir 조각을 생각해 보십시오.
func.func @baz(%arg: i32) {
%result = my_dialect.foo %arg, %arg -> i32
}
my_dialect.foo를 매칭하고 이를 유일한 입력 인수로 교체하는 패턴을 작성하고 싶다고 가정합시다. PDLL에서 이 패턴을 순진하게 작성한 예는 아래와 같습니다.
Pattern {
// ** match section ** //
let arg: Value;
let root = op<my_dialect.foo>(arg, arg);
// ** rewrite section ** //
replace root with arg;
}
위 패턴에서 arg 변수는 root 연산의 첫 번째와 두 번째 피연산자에 “바인딩”됩니다. arg의 모든 사용은 동일한 Value여야 한다는 제약을 갖습니다. 즉 root의 첫 번째와 두 번째 피연산자는 동일한 입력 Value를 참조해야 합니다. root 연산도 마찬가지로 패턴의 루트 연산에 바인딩됩니다. 왜냐하면 그것이 패턴의 rewrite 절의 최상위 replace 문의 입력으로 사용되기 때문입니다. C++ API로 이 패턴을 작성하면 “바인딩” 개념이 더 분명해집니다.
struct Pattern : public OpRewritePattern<my_dialect::FooOp> {
LogicalResult matchAndRewrite(my_dialect::FooOp root, PatternRewriter &rewriter) {
Value arg = root->getOperand(0);
if (arg != root->getOperand(1))
return failure();
rewriter.replaceOp(root, arg);
return success();
}
};
변수가 올바르게 “바인딩”되지 않으면, PDLL은 그것이 IR에서 어떤 값에 해당하는지 식별할 수 없습니다. 마지막 예로, 바인딩되지 않은 변수를 생각해 보겠습니다.
Pattern {
// ** match section ** //
let arg: Value;
let root = op<my_dialect.foo>
// ** rewrite section ** //
replace root with arg;
}
이 패턴을 그대로 C++로 작성하면 다음과 같게 됩니다.
struct Pattern : public OpRewritePattern<my_dialect::FooOp> {
LogicalResult matchAndRewrite(my_dialect::FooOp root, PatternRewriter &rewriter) {
// `arg` was never bound, so we don't know what input Value it was meant to
// correspond to.
Value arg;
rewriter.replaceOp(root, arg);
return success();
}
};
// This statement defines a variable `value` that is constrained to be a `Value`.
let value: Value;
// This statement defines a variable `value` that is constrained to be a `Value`
// *and* constrained to have a single use.
let value: [Value, HasOneUse];
변수 선언 시 단일 엔터티 제약조건을 원하는 만큼 직접 변수에 붙일 수 있습니다. matcher 절 안에서는 이러한 제약조건이 입력 IR에 대한 추가 검사를 더할 수 있습니다. rewriter 절 안에서는 제약조건이 변수의 타입을 정의하는 데에만 사용됩니다. 핵심 MLIR 구성 요소에 대응하는 여러 내장 제약조건이 있습니다: Attr, Op, Type, TypeRange, Value, ValueRange. 이와 함께 사용자는 PDLL 내부 또는 네이티브하게(즉 PDLL 외부에서) 구현되는 사용자 정의 제약조건을 정의할 수 있습니다. 보다 자세한 내용은 일반 제약조건 절을 참조하십시오.
let 문과 함께, 변수를 처음 사용하는 위치에 원하는 변수 이름과 제약조건 목록을 함께 지정하여 인라인으로 정의할 수도 있습니다. 정의 후에는 그 시점 이후 모든 위치에서 변수가 보입니다. 아래 예를 보십시오.
// `value` is used as an operand to the operation `root`:
let value: Value;
let root = op<my_dialect.foo>(value);
replace root with value;
// `value` could also be defined "inline":
let root = op<my_dialect.foo>(value: Value);
replace root with value;
인라인 변수의 정의 시점은 참조 시점이라는 점에 유의하십시오. 즉 인라인 변수는 정의된 동일한 부모 표현식 내에서도 즉시 사용할 수 있습니다.
let root = op<my_dialect.foo>(value: Value, _: Value, value);
replace root with value;
인라인으로 변수를 정의할 때, 그 변수를 패턴의 다른 곳에서 사용할 의도가 없는 경우가 자주 있습니다. 예를 들어 변수에 제약조건을 붙이고 싶지만 다른 용도는 없을 수 있습니다. 이러한 경우에는 이름을 제공할 필요를 없애기 위해 “와일드카드” 변수를 사용할 수 있습니다. “와일드카드” 변수는 정의 지점 밖에서는 보이지 않기 때문입니다. 예는 아래와 같습니다.
Pattern {
let root = op<my_dialect.foo>(arg: Value, _: Value, _: [Value, I64Value], arg);
replace root with arg;
}
위 예에서 두 번째 피연산자는 패턴에 필요하지 않지만, 두 번째 피연산자가 실제로 존재한다는 것을 나타내기 위해 제공해야 합니다(단지 이 패턴에서는 그것이 무엇인지 중요하지 않을 뿐입니다).
PDLL의 연산 표현식은 MLIR 연산을 나타냅니다. 패턴의 match 절에서는 이 표현식이 패턴의 입력 연산 중 하나를 모델링합니다. 패턴의 rewrite 절에서는 생성할 연산 중 하나를 모델링합니다. 연산 표현식의 일반 구조는 텍스트 MLIR 어셈블리의 “generic form”과 매우 유사합니다.
let root = op<my_dialect.foo>(operands: ValueRange) {attr = attr: Attr} -> (resultTypes: TypeRange);
표현식의 각 구성 요소를 차례로 살펴보겠습니다.
연산 이름은 이 연산이 어떤 유형의 MLIR Op에 해당하는지를 나타냅니다. 패턴의 match 절에서는 이름을 생략할 수 있습니다. 이렇게 하면 이 패턴은 연산의 나머지 제약조건을 만족하는 어떤 연산 타입과도 매칭됩니다. rewrite 절에서는 이름이 필수입니다.
// `root` corresponds to an instance of a `my_dialect.foo` operation.
let root = op<my_dialect.foo>;
// `root` could be an instance of any operation type.
let root = op<>;
피연산자 절은 연산의 피연산자들에 대응합니다. 연산 표현식의 이 절은 생략할 수 있으며, match 절에서 생략되면 피연산자들이 어떤 방식으로도 제약되지 않는다는 뜻입니다. rewrite 절에서 생략되면 그 연산은 피연산자가 없는 것으로 취급됩니다. 피연산자가 존재할 때, 연산 표현식의 피연산자는 다음과 같이 해석됩니다.
ValueRange 타입의 단일 인스턴스:이 경우, 단일 범위가 연산의 모든 피연산자로 취급됩니다.
// Define an instance with single range of operands.
let root = op<my_dialect.foo>(allOperands: ValueRange);
Value 또는 ValueRange의 가변 개수:이 경우, 입력은 ODS에서 연산에 정의된 피연산자 그룹에 대응해야 합니다.
다음과 같은 ODS 연산 정의가 있다고 합시다.
def MyIndirectCallOp {
let arguments = (ins FunctionType:$call, Variadic<AnyType>:$args);
}
피연산자는 다음과 같이 매칭할 수 있습니다.
let root = op<my_dialect.indirect_call>(call: Value, args: ValueRange);
결과 절은 연산의 결과 타입에 대응합니다. 연산 표현식의 이 절은 생략할 수 있으며, match 절에서 생략되면 결과 타입이 어떤 방식으로도 제약되지 않는다는 뜻입니다. rewrite 절에서 생략되면 연산의 결과는 추론됩니다. 결과 타입이 존재할 때, 연산 표현식의 결과 타입은 다음과 같이 해석됩니다.
TypeRange 타입의 단일 인스턴스:이 경우, 단일 범위가 연산의 모든 결과 타입으로 취급됩니다.
// Define an instance with single range of types.
let root = op<my_dialect.foo> -> (allResultTypes: TypeRange);
Type 또는 TypeRange의 가변 개수:이 경우, 입력은 ODS에서 연산에 정의된 결과 그룹에 대응해야 합니다.
다음과 같은 ODS 연산 정의가 있다고 합시다.
def MyOp {
let results = (outs SomeType:$result, Variadic<SomeType>:$otherResults);
}
결과 타입은 다음과 같이 매칭할 수 있습니다.
let root = op<my_dialect.op> -> (result: Type, otherResults: TypeRange);
패턴의 rewrite 절 안에서는, 연산의 결과 타입이 생략되었거나 이전에 바인딩되지 않은 경우 추론됩니다. 위의 “변수 바인딩” 절에서는 “바인딩” 개념을 더 자세히 설명합니다. 아래에는 결과 타입이 어떻게 “바인딩”될 수 있는지 보여주는 여러 예가 있습니다.
op<my_dialect.op> -> (type<"i32">);
match 절 내부의 타입에 바인딩:Pattern {
replace op<dialect.inputOp> -> (resultTypes: TypeRange)
with op<dialect.outputOp> -> (resultTypes);
}
Pattern {
rewrite root: Op with {
// `resultTypes` here is *not* yet bound, and will be inferred when
// creating `dialect.op`. Any uses of `resultTypes` after this expression,
// will use the types inferred when creating this operation.
op<dialect.op> -> (resultTypes: TypeRange);
// `resultTypes` here is bound to the types inferred when creating `dialect.op`.
op<dialect.bar> -> (resultTypes);
};
}
Native Rewrite 메서드 결과에 바인딩:Rewrite BuildTypes() -> TypeRange;
Pattern {
rewrite root: Op with {
op<dialect.op> -> (BuildTypes());
};
}
아래는 결과 타입 추론이 지원되는 문맥들의 목록입니다.
교체에는, 교체 값의 타입이 입력 연산의 결과 타입과 일치해야 한다는 불변식이 있습니다. 즉 한 연산을 다른 연산으로 교체할 때, 교체 연산의 결과 타입은 교체되는 연산의 결과 타입으로부터 추론될 수 있습니다. 예를 들어 다음 패턴을 보겠습니다.
Pattern => replace op<dialect.inputOp> with op<dialect.outputOp>;
이 패턴은 더 명시적으로 다음과 같이 쓸 수 있습니다.
Pattern {
replace op<dialect.inputOp> -> (resultTypes: TypeRange)
with op<dialect.outputOp> -> (resultTypes);
}
InferTypeOpInterface는 연산이 입력 속성, 피연산자, region 등으로부터 자신의 결과 타입을 추론할 수 있게 하는 인터페이스입니다. 연산의 결과 타입을 다른 어떤 문맥으로도 추론할 수 없을 때, 이 인터페이스가 호출되어 해당 연산의 결과 타입을 추론합니다.
연산 표현식의 속성 절은 연산의 속성 딕셔너리에 대응합니다. 이 절은 생략할 수 있으며, 이 경우 속성은 어떤 방식으로도 제약되지 않습니다. 이 구성 요소의 구성은 MLIR 텍스트 어셈블리 형식에서 속성 딕셔너리가 구조화되는 방식과 정확히 일치합니다.
let root = op<my_dialect.foo> {attr1 = attrValue: Attr, attr2 = attrValue2: Attr};
{} 안의 속성 항목은 식별자 또는 문자열 이름으로 지정되며, 이는 속성 이름에 대응합니다. 그 뒤에 속성 값으로의 대입이 이어집니다. 속성 값이 생략되면, 그 속성의 값은 암묵적으로 UnitAttr로 정의됩니다.
let unitConstant = op<my_dialect.constant> {value};
다중 연산 패턴에서는 한 연산의 결과가 다른 연산의 입력으로 들어가는 경우가 많습니다. 연산의 결과 그룹은 . 연산자를 통해 이름이나 인덱스로 접근할 수 있습니다.
참고: 연산 정의가 PDLL에서 보이도록 하려면 include를 통해 해당 정의를 가져오는 것을 잊지 마십시오.
다음과 같은 ODS 연산 정의가 있다고 합시다.
def MyResultOp {
let results = (outs SomeType:$result);
}
def MyInputOp {
let arguments = (ins SomeType:$input, SomeType:$input);
}
MyResultOp가 MyInputOp로 전달되는 패턴은 다음과 같이 작성할 수 있습니다.
// In this example, we use both `result`(the name) and `0`(the index) to refer to
// the first result group of `resultOp`.
// Note: If we elide the result types section within the match section, it means
// they aren't constrained, not that the operation has no results.
let resultOp = op<my_dialect.result_op>;
let inputOp = op<my_dialect.input_op>(resultOp.result, resultOp.0);
결과 이름 접근과 함께, Op 타입 변수는 암묵적으로 Value 또는 ValueRange로 변환될 수 있습니다. 이러한 변수가 등록되어 있다면(ODS 항목이 있다면), 결과가 하나뿐임이 알려진 경우 Value로 변환되고, 그렇지 않으면 ValueRange로 변환됩니다.
// `resultOp` may also convert implicitly to a Value for use in `inputOp`:
let resultOp = op<my_dialect.result_op>;
let inputOp = op<my_dialect.input_op>(resultOp);
// We could also inline `resultOp` directly:
let inputOp = op<my_dialect.input_op>(op<my_dialect.result_op>);
등록되지 않은 op의 변수도 숫자 결과 인덱싱에 사용할 수 있습니다. 결과 그룹에 대한 지식이 없기 때문에, 숫자 인덱싱은 주어진 인덱스의 개별 결과에 대응하는 Value를 반환합니다.
// Use the index `0` to refer to the first result value of the unregistered op.
let inputOp = op<my_dialect.input_op>(op<my_dialect.unregistered_op>.0);
속성 표현식은 리터럴 MLIR 속성을 나타냅니다. 이를 통해 해당 속성의 텍스트 형식을 지정하여 사용할 MLIR 속성을 정적으로 지정할 수 있습니다.
let trueConstant = op<arith.constant> {value = attr<"true">};
let applyResult = op<affine.apply>(args: ValueRange) {map = attr<"affine_map<(d0, d1) -> (d1 - 3)>">}
타입 표현식은 리터럴 MLIR 타입을 나타냅니다. 이를 통해 해당 타입의 텍스트 형식을 지정하여 사용할 MLIR 타입을 정적으로 지정할 수 있습니다.
let i32Constant = op<arith.constant> -> (type<"i32">);
PDLL은 튜플에 대한 네이티브 지원을 제공하며, 튜플은 여러 요소를 하나의 복합 값으로 묶는 데 사용됩니다. 튜플 안의 값은 어떤 타입이든 될 수 있으며, 서로 같은 타입일 필요도 없습니다. 또한 튜플이 담을 수 있는 요소 수에도 제한이 없습니다. 튜플의 요소는 인덱스로 접근할 수 있습니다.
let tupleValue = (op<my_dialect.foo>, attr<"10 : i32">, type<"i32">);
let opValue = tupleValue.0;
let attrValue = tupleValue.1;
let typeValue = tupleValue.2;
튜플의 요소에 이름을 붙이고, 그 이름으로 각 요소의 값에 접근할 수도 있습니다. 요소 이름은 식별자 뒤에 즉시 등호(=)가 오는 형태입니다.
let tupleValue = (
opValue = op<my_dialect.foo>,
attr<"10 : i32">,
typeValue = type<"i32">
);
let opValue = tupleValue.opValue;
let attrValue = tupleValue.1;
let typeValue = tupleValue.typeValue;
튜플은 제약조건이나 rewrite에서 여러 결과를 표현하는 데 사용됩니다.
제약조건은 패턴의 match 절 안에서 입력 IR에 대한 추가 검사를 주입할 수 있는 기능을 제공합니다. 제약조건은 match 절 안의 어느 곳에나 적용할 수 있으며, 타입에 따라 변수의 제약조건 목록을 통해 적용하거나 호출 연산자(예: MyConstraint(...))를 통해 적용할 수 있습니다. 제약조건에는 세 가지 주요 범주가 있습니다.
PDLL은 IR 엔터티의 타입을 제약하는 여러 핵심 제약조건을 정의합니다. 이러한 제약조건은 변수의 제약조건 목록을 통해서만 적용할 수 있습니다.
Attr (< type >) ?mlir::Attribute에 대응하는 단일 엔터티 제약조건입니다. 이 제약조건은 선택적으로 속성의 결과 타입을 제약하는 타입 구성 요소를 가질 수 있습니다.
// Define a simple variable using the `Attr` constraint.
let attr: Attr;
let constant = op<arith.constant> {value = attr};
// Define a simple variable using the `Attr` constraint, that has its type
// constrained as well.
let attrType: Type;
let attr: Attr<attrType>;
let constant = op<arith.constant> {value = attr};
Op (< op-name >) ?mlir::Operation *에 대응하는 단일 엔터티 제약조건입니다.
// Match only when the input is from another operation.
let inputOp: Op;
let root = op<my_dialect.foo>(inputOp);
// Match only when the input is from another `my_dialect.foo` operation.
let inputOp: Op<my_dialect.foo>;
let root = op<my_dialect.foo>(inputOp);
Typemlir::Type에 대응하는 단일 엔터티 제약조건입니다.
// Define a simple variable using the `Type` constraint.
let resultType: Type;
let root = op<my_dialect.foo> -> (resultType);
TypeRangemlir::TypeRange에 대응하는 단일 엔터티 제약조건입니다.
// Define a simple variable using the `TypeRange` constraint.
let resultTypes: TypeRange;
let root = op<my_dialect.foo> -> (resultTypes);
Value (< type-expr >) ?mlir::Value에 대응하는 단일 엔터티 제약조건입니다. 이 제약조건은 선택적으로 값의 결과 타입을 제약하는 타입 구성 요소를 가질 수 있습니다.
// Define a simple variable using the `Value` constraint.
let value: Value;
let root = op<my_dialect.foo>(value);
// Define a variable using the `Value` constraint, that has its type constrained
// to be same as the result type of the `root` op.
let valueType: Type;
let input: Value<valueType>;
let root = op<my_dialect.foo>(input) -> (valueType);
ValueRange (< type-expr >) ?mlir::ValueRange에 대응하는 단일 엔터티 제약조건입니다. 이 제약조건은 선택적으로 값 범위의 결과 타입을 제약하는 타입 구성 요소를 가질 수 있습니다.
// Define a simple variable using the `ValueRange` constraint.
let inputs: ValueRange;
let root = op<my_dialect.foo>(inputs);
// Define a variable using the `ValueRange` constraint, that has its types
// constrained to be same as the result types of the `root` op.
let valueTypes: TypeRange;
let inputs: ValueRange<valueTypes>;
let root = op<my_dialect.foo>(inputs) -> (valueTypes);
핵심 제약조건 외에도, 추가 제약조건을 PDLL 안에서 정의할 수 있습니다. 이를 통해 여러 다른 패턴에서 조합해 사용할 수 있는 매처 조각을 만들 수 있습니다. PDLL의 제약조건은 전통적인 프로그래밍 언어의 함수와 유사하게 정의됩니다. 이름, 입력 인수 집합, 결과 타입 집합, 본문을 포함합니다. 제약조건의 결과는 return 문으로 반환합니다. 아래에 몇 가지 예를 보입니다.
/// A constraint that takes an input and constrains the use to an operation of
/// a given type.
Constraint UsedByFooOp(value: Value) {
op<my_dialect.foo>(value);
}
/// A constraint that returns a result of an existing operation.
Constraint ExtractResult(op: Op<my_dialect.foo>) -> Value {
return op.result;
}
Pattern {
let value = ExtractResult(op<my_dialect.foo>);
UsedByFooOp(value);
}
제약조건은 값의 튜플을 반환함으로써 여러 결과를 반환할 수 있습니다. 여러 결과를 반환할 때, 각 결과에는 해당 튜플 요소를 인덱싱할 때 사용할 이름을 지정할 수도 있습니다. 튜플 요소는 인덱스 번호로 참조할 수 있고, 이름이 지정된 경우 이름으로도 참조할 수 있습니다.
// A constraint that returns multiple results, with some of the results assigned
// a more readable name.
Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) -> (Value, result1: Value) {
return (op.result1, op.result2);
}
Pattern {
// Return a tuple of values.
let result = ExtractMultipleResults(op: op<my_dialect.foo>);
// Index the tuple elements by index, or by name.
replace op<my_dialect.foo> with (result.0, result.1, result.result1);
}
제약조건 시그니처를 통해 제약조건의 결과를 명시적으로 지정하는 것 외에도, PDLL에서 정의된 제약조건은 return 문으로부터 결과 타입을 추론하는 것도 지원합니다. 제약조건이 결과 제약 없이 정의되면 결과 타입 추론이 활성화됩니다.
// This constraint returns a derived operation.
Constraint ReturnSelf(op: Op<my_dialect.foo>) {
return op;
}
// This constraint returns a tuple of two Values.
Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) {
return (result1 = op.result1, result2 = op.result2);
}
Pattern {
let values = ExtractMultipleResults(op<my_dialect.foo>);
replace op<my_dialect.foo> with (values.result1, values.result2);
}
제약조건은 일반적으로 아래와 같이 복합 문 블록으로 본문을 정의합니다.
Constraint ReturnSelf(op: Op<my_dialect.foo>) {
return op;
}
Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) {
return (result1 = op.result1, result2 = op.result2);
}
제약조건도 단순한 단일 행 본문을 지정하기 위한 람다 유사 문법을 지원합니다. Constraint의 람다 본문은 단일 표현식을 기대하며, 이는 암묵적으로 반환됩니다.
Constraint ReturnSelf(op: Op<my_dialect.foo>) => op;
Constraint ExtractMultipleResults(op: Op<my_dialect.foo>)
=> (result1 = op.result1, result2 = op.result2);
제약조건은 PDLL 외부에서 정의되어 C++ API 안에서 네이티브하게 등록될 수도 있습니다.
외부에서 정의된 제약조건은 제약조건 “선언”을 지정하여 PDLL로 가져올 수 있습니다. 이는 PDLL 형태의 제약조건 정의와 유사하지만 본문이 생략됩니다. 이 형태로 선언을 가져오면, PDLL은 예상되는 입력 및 출력 타입을 정적으로 알 수 있습니다.
// Import a single entity value native constraint that checks if the value has a
// single use. This constraint must be registered by the consumer of the
// compiled PDL.
Constraint HasOneUse(value: Value);
// Import a multi-entity type constraint that checks if two values have the same
// element type.
Constraint HasSameElementType(value1: Value, value2: Value);
Pattern {
// A single entity constraint can be applied via the variable argument list.
let value: HasOneUse;
// Otherwise, constraints can be applied via the call operator:
let value: Value = ...;
let value2: Value = ...;
HasOneUse(value);
HasSameElementType(value, value2);
}
외부 제약조건은 C++ PDL API를 통해 RewritePatternSet에 명시적으로 등록되는 것들입니다. 예를 들어 위의 제약조건은 다음과 같이 등록할 수 있습니다.
static LogicalResult hasOneUseImpl(PatternRewriter &rewriter, Value value) {
return success(value.hasOneUse());
}
static LogicalResult hasSameElementTypeImpl(PatternRewriter &rewriter,
Value value1, Value Value2) {
return success(cast<ShapedType>(value1.getType()).getElementType() ==
cast<ShapedType>(value2.getType()).getElementType());
}
void registerNativeConstraints(RewritePatternSet &patterns) {
patternList.getPDLPatterns().registerConstraintFunction(
"HasOneUse", hasOneUseImpl);
patternList.getPDLPatterns().registerConstraintFunction(
"HasSameElementType", hasSameElementTypeImpl);
}
네이티브 제약조건을 가져오는 것 외에도, PDLL은 C++용 AOT 컴파일 시 네이티브 제약조건을 직접 정의하는 것도 지원합니다. 이러한 제약조건은 제약조건 선언 뒤에 문자열 코드 블록을 지정하여 정의할 수 있습니다.
Constraint HasOneUse(value: Value) [{
return success(value.hasOneUse());
}];
Constraint HasSameElementType(value1: Value, value2: Value) [{
return success(cast<ShapedType>(value1.getType()).getElementType() ==
cast<ShapedType>(value2.getType()).getElementType());
}];
Pattern {
// A single entity constraint can be applied via the variable argument list.
let value: HasOneUse;
// Otherwise, constraints can be applied via the call operator:
let value: Value = ...;
let value2: Value = ...;
HasOneUse(value);
HasSameElementType(value, value2);
}
제약조건의 인수는 동일한 이름으로 코드 블록 안에서 접근할 수 있습니다. PDLL 타입이 네이티브 타입으로 어떻게 변환되는지에 대한 자세한 내용은 아래의 “타입 변환”을 참조하십시오. PDLL 인수 외에도, 코드 블록은 rewriter를 통해 현재 PatternRewriter에 접근할 수 있습니다. 네이티브 제약조건 함수의 결과 타입은 암묵적으로 ::llvm::LogicalResult로 정의됩니다.
위에서 정의한 제약조건을 예로 들면, 이러한 함수는 대략 다음과 같이 변환됩니다.
LogicalResult HasOneUse(PatternRewriter &rewriter, Value value) {
return success(value.hasOneUse());
}
LogicalResult HasSameElementType(Value value1, Value value2) {
return success(cast<ShapedType>(value1.getType()).getElementType() ==
cast<ShapedType>(value2.getType()).getElementType());
}
TODO: 네이티브 제약조건도 특정 경우에는 값을 반환할 수 있도록 허용되어야 합니다.
인수 및 결과 변수의 타입은 일반적으로 사용된 제약조건의 해당 MLIR 타입으로 매핑됩니다. 아래에는 다양한 제약조건 타입에 대해 변수의 매핑 타입이 어떻게 결정되는지에 대한 자세한 설명이 있습니다.
이들은 모두 핵심 제약조건이며, 이름이 시사하는 MLIR 동등 타입으로 직접 매핑됩니다. 즉 다음과 같습니다.
Attr -> “::mlir::Attribute”
Op -> “::mlir::Operation *”
Type -> “::mlir::Type”
TypeRange -> “::mlir::TypeRange”
Value -> “::mlir::Value”
ValueRange -> “::mlir::ValueRange”
Op<dialect.name>
이름 있는 연산 제약조건은 고유한 변환을 가집니다. 참조된 연산의 ODS 등록이 포함되어 있으면 정규화된 C++ 타입이 사용됩니다. ODS 정보를 사용할 수 없으면, 이 제약조건은 이름 없는 변형과 유사하게 “::mlir::Operation *”로 매핑됩니다. 예를 들어 다음을 보겠습니다.
// `my_ops.td` provides the ODS definition of the `my_dialect` operations, such as
// `my_dialect.bar` used below.
#include "my_ops.td"
Constraint Cst(op: Op<my_dialect.bar>) [{
return success(op ... );
}];
op에 사용되는 네이티브 타입은 기본값인 ::mlir::Operation * 대신 my_dialect::BarOp 형태일 수 있습니다. 아래는 위 제약조건의 번역 예시입니다.
LogicalResult Cst(my_dialect::BarOp op) {
return success(op ... );
}
핵심 제약조건 외에도, ODS에서 가져온 특정 제약조건은 고유한 네이티브 타입을 사용할 수 있습니다. 이 고유 타입을 활성화하는 방법은 가져온 ODS 제약조건 구성 요소에 따라 달라집니다.
Attr 제약조건
Attr 제약조건은 네이티브 타입 변환에 storageType 필드를 사용합니다.Type 제약조건
Type 제약조건은 네이티브 타입 변환에 cppClassName 필드를 사용합니다.AttrInterface/OpInterface/TypeInterface 제약조건
cppInterfaceName 필드를 사용합니다.전역 범위 외에도, PDLL 안에서 정의된 PDLL 제약조건과 네이티브 제약조건은 어떤 중첩 수준에서든 인라인으로 지정할 수 있습니다. 이는 패턴, 다른 제약조건, rewrite 등 안에서 정의될 수 있음을 의미합니다.
Constraint GlobalConstraint() {
Constraint LocalConstraint(value: Value) {
...
};
Constraint LocalNativeConstraint(value: Value) [{
...
}];
let someValue: [LocalConstraint, LocalNativeConstraint] = ...;
}
인라인으로 정의된 제약조건은 직접 사용할 때 이름도 생략할 수 있습니다.
Constraint GlobalConstraint(inputValue: Value) {
Constraint(value: Value) { ... }(inputValue);
Constraint(value: Value) [{ ... }](inputValue);
}
인라인으로 정의할 때, PDLL 제약조건은 이전에 정의된 어떤 변수든 참조할 수 있습니다.
Constraint GlobalConstraint(op: Op<my_dialect.foo>) {
Constraint LocalConstraint() {
let results = op.results;
};
}
리라이터는 패턴의 rewrite 절 안에서 수행될 변환의 집합, 보다 구체적으로는 성공적인 패턴 매치 이후 입력 IR을 어떻게 변환할지를 정의합니다. 모든 PDLL rewrite는 패턴의 rewrite 절 안에 정의되어야 합니다. rewrite 절은 Pattern 본문 안의 마지막 문으로 표시되며, 이는 반드시 operation rewrite statement여야 합니다. PDLL의 rewrite에는 두 가지 주요 범주가 있습니다. operation rewrite 문과 사용자 정의 rewrite입니다.
Operation rewrite 문은 루트 연산이 주어졌을 때 IR 변환을 수행하는 내장 PDLL 문입니다. 이러한 문은 패턴의 rewrite 절을 시작할 수 있는 유일한 문입니다. 왜냐하면 패턴의 루트 연산을 올바르게 “바인딩”할 수 있기 때문입니다.
erase 문¶// A pattern that erases all `my_dialect.foo` operations.
Pattern => erase op<my_dialect.foo>;
erase 문은 주어진 연산을 삭제합니다.
replace 문¶// A pattern that replaces the root operation with its input value.
Pattern {
let root = op<my_dialect.foo>(input: Value);
replace root with input;
}
// A pattern that replaces the root operation with multiple input values.
Pattern {
let root = op<my_dialect.foo>(input: Value, _: Value, input2: Value);
replace root with (input, input2);
}
// A pattern that replaces the root operation with another operation.
// Note that when an operation is used as the replacement, we can infer its
// result types from the input operation. In these cases, the result
// types of replacement operation may be elided.
Pattern {
// Note: In this pattern we also inlined the `root` expression.
replace op<my_dialect.foo> with op<my_dialect.bar>;
}
replace 문은 주어진 루트 연산을 다른 연산 또는 입력 Value와 ValueRange 값 집합으로 교체할 수 있게 해줍니다. 연산이 교체 대상으로 사용될 때는 입력 연산으로부터 결과 타입을 추론할 수 있습니다. 이러한 경우 교체 연산의 결과 타입은 생략될 수 있습니다. 교체 중에는 결과 타입을 제외한 다른 구성 요소는 입력 연산으로부터 추론되지 않는다는 점에 유의하십시오.
rewrite 문¶// A simple pattern that replaces the root operation with its input value.
Pattern {
let root = op<my_dialect.foo>(input: Value);
rewrite root with {
...
replace root with input;
};
}
rewrite 문은 주어진 루트 연산을 중첩된 리라이터 블록으로 rewrite할 수 있게 해줍니다. 루트 연산은 암묵적으로 삭제되거나 교체되지 않으며, 이에 대한 모든 변환은 중첩된 rewrite 블록 안에서 표현되어야 합니다. 내부 본문은 다른 rewrite 문, 변수, 표현식을 얼마든지 포함할 수 있습니다.
추가 rewrite도 PDLL 안에서 정의할 수 있으며, 이를 통해 여러 다른 패턴에서 조합해 사용할 수 있는 rewrite 조각을 만들 수 있습니다. PDLL의 리라이터는 전통적인 프로그래밍 언어의 함수와 유사하게 정의됩니다. 이름, 입력 인수 집합, 결과 타입 집합, 본문을 포함합니다. rewrite의 결과는 return 문으로 반환합니다. 아래에 몇 가지 예를 보입니다.
// A rewrite that constructs and returns a new operation, given an input value.
Rewrite BuildFooOp(value: Value) -> Op {
return op<my_dialect.foo>(value);
}
Pattern {
// We invoke the rewrite in the same way as functions in traditional
// languages.
replace op<my_dialect.old_op>(input: Value) with BuildFooOp(input);
}
Rewrite는 값의 튜플을 반환함으로써 여러 결과를 반환할 수 있습니다. 여러 결과를 반환할 때, 각 결과에는 해당 튜플 요소를 인덱싱할 때 사용할 이름을 지정할 수도 있습니다. 튜플 요소는 인덱스 번호로 참조할 수 있고, 이름이 지정된 경우 이름으로도 참조할 수 있습니다.
// A rewrite that returns multiple results, with some of the results assigned
// a more readable name.
Rewrite CreateRewriteOps() -> (Op, result1: ValueRange) {
return (op<my_dialect.bar>, op<my_dialect.foo>);
}
Pattern {
rewrite root: Op<my_dialect.foo> with {
// Invoke the rewrite, which returns a tuple of values.
let result = CreateRewriteOps();
// Index the tuple elements by index, or by name.
replace root with (result.0, result.1, result.result1);
}
}
Rewrite 시그니처를 통해 결과를 명시적으로 지정하는 것 외에도, PDLL에서 정의된 rewrite는 return 문으로부터 결과 타입을 추론하는 것도 지원합니다. rewrite가 결과 제약 없이 정의되면 결과 타입 추론이 활성화됩니다.
// This rewrite returns a derived operation.
Rewrite ReturnSelf(op: Op<my_dialect.foo>) => op;
// This rewrite returns a tuple of two Values.
Rewrite ExtractMultipleResults(op: Op<my_dialect.foo>) {
return (result1 = op.result1, result2 = op.result2);
}
Pattern {
rewrite root: Op<my_dialect.foo> with {
let values = ExtractMultipleResults(op<my_dialect.foo>);
replace root with (values.result1, values.result2);
}
}
Rewrite는 일반적으로 아래와 같이 복합 문 블록으로 본문을 정의합니다.
Rewrite ReturnSelf(op: Op<my_dialect.foo>) {
return op;
}
Rewrite EraseOp(op: Op) {
erase op;
}
Rewrite도 단순한 단일 행 본문을 지정하기 위한 람다 유사 문법을 지원합니다. Rewrite의 람다 본문은 암묵적으로 반환되는 단일 표현식 또는 단일 operation rewrite statement를 기대합니다.
Rewrite ReturnSelf(op: Op<my_dialect.foo>) => op;
Rewrite EraseOp(op: Op) => erase op;
리라이터도 PDLL 외부에서 정의되어 C++ API 안에서 네이티브하게 등록될 수 있습니다.
외부에서 정의된 rewrite는 rewrite “선언”을 지정하여 PDLL로 가져올 수 있습니다. 이는 PDLL 형태의 rewrite 정의와 유사하지만 본문이 생략됩니다. 이 형태로 선언을 가져오면, PDLL은 예상되는 입력 및 출력 타입을 정적으로 알 수 있습니다.
// Import a single input native rewrite that returns a new operation. This
// rewrite must be registered by the consumer of the compiled PDL.
Rewrite BuildOp(value: Value) -> Op;
Pattern {
replace op<my_dialect.old_op>(input: Value) with BuildOp(input);
}
외부 rewrite는 C++ PDL API를 통해 RewritePatternSet에 명시적으로 등록되는 것들입니다. 예를 들어 위 rewrite는 다음과 같이 등록할 수 있습니다.
static Operation *buildOpImpl(PDLResultList &results, Value value) {
// insert special rewrite logic here.
Operation *resultOp = ...;
return resultOp;
}
void registerNativeRewrite(RewritePatternSet &patterns) {
patterns.getPDLPatterns().registerRewriteFunction("BuildOp", buildOpImpl);
}
네이티브 rewrite를 가져오는 것 외에도, PDLL은 C++용 AOT 컴파일 시 네이티브 rewrite를 직접 정의하는 것도 지원합니다. 이러한 rewrite는 rewrite 선언 뒤에 문자열 코드 블록을 지정하여 정의할 수 있습니다.
Rewrite BuildOp(value: Value) -> (foo: Op<my_dialect.foo>, bar: Op<my_dialect.bar>) [{
return {my_dialect::FooOp::create(rewriter, value), my_dialect::BarOp::create(rewriter)};
}];
Pattern {
let root = op<my_dialect.foo>(input: Value);
rewrite root with {
// Invoke the native rewrite and use the results when replacing the root.
let results = BuildOp(input);
replace root with (results.foo, results.bar);
}
}
rewrite의 인수는 동일한 이름으로 코드 블록 안에서 접근할 수 있습니다. PDLL 타입이 네이티브 타입으로 어떻게 변환되는지에 대한 자세한 내용은 아래의 “타입 변환”을 참조하십시오. PDLL 인수 외에도, 코드 블록은 rewriter를 통해 현재 PatternRewriter에 접근할 수 있습니다. 네이티브 함수의 결과 타입이 어떻게 결정되는지에 대한 자세한 내용은 “결과 변환” 절을 참조하십시오.
위에서 정의한 rewrite를 예로 들면, 이 함수는 대략 다음과 같이 변환됩니다.
std::tuple<my_dialect::FooOp, my_dialect::BarOp> BuildOp(Value value) {
return {my_dialect::FooOp::create(rewriter, value), my_dialect::BarOp::create(rewriter)};
}
인수 및 결과 변수의 타입은 일반적으로 사용된 제약조건의 해당 MLIR 타입으로 매핑됩니다. 네이티브 Rewrite 타입 변환 규칙은 네이티브 Constraint의 규칙과 동일하므로, 변수의 매핑 타입이 어떻게 결정되는지에 대한 자세한 설명은 해당 네이티브 Constraint 타입 변환 절을 참조하십시오.
네이티브 rewrite의 결과는 위에서 설명한 타입 변환 규칙을 사용하여 네이티브 함수의 결과로 직접 변환됩니다. 아래 절에서는 다양한 결과 변환 시나리오를 설명합니다.
Rewrite createOp() [{
my_dialect::FooOp::create(rewriter);
}];
네이티브 Rewrite에 결과가 없는 경우, 네이티브 함수는 void를 반환합니다.
void createOp(PatternRewriter &rewriter) {
my_dialect::FooOp::create(rewriter);
}
Rewrite createOp() -> Op<my_dialect.foo> [{
return my_dialect::FooOp::create(rewriter);
}];
네이티브 Rewrite에 단일 결과가 있는 경우, 네이티브 함수는 그 단일 결과에 해당하는 네이티브 타입을 반환합니다.
my_dialect::FooOp createOp(PatternRewriter &rewriter) {
return my_dialect::FooOp::create(rewriter);
}
Rewrite complexRewrite(value: Value) -> (Op<my_dialect.foo>, FunctionOpInterface) [{
...
}];
네이티브 Rewrite에 여러 결과가 있는 경우, 네이티브 함수는 각 결과에 대응하는 네이티브 타입을 담은 std::tuple<...>을 반환합니다.
std::tuple<my_dialect::FooOp, FunctionOpInterface>
complexRewrite(PatternRewriter &rewriter, Value value) {
...
}
전역 범위 외에도, PDLL 안에서 정의된 PDLL Rewrite 및 Native Rewrite는 어떤 중첩 수준에서든 인라인으로 지정할 수 있습니다. 이는 패턴, 다른 Rewrite 등 안에서 정의될 수 있음을 의미합니다.
Rewrite GlobalRewrite(inputValue: Value) {
Rewrite localRewrite(value: Value) {
...
};
Rewrite localNativeRewrite(value: Value) [{
...
}];
localRewrite(inputValue);
localNativeRewrite(inputValue);
}
인라인으로 정의된 Rewrite는 직접 사용할 때 이름도 생략할 수 있습니다.
Rewrite GlobalRewrite(inputValue: Value) {
Rewrite(value: Value) { ... }(inputValue);
Rewrite(value: Value) [{ ... }](inputValue);
}
인라인으로 정의할 때, PDLL rewrite는 이전에 정의된 어떤 변수든 참조할 수 있습니다.
Rewrite GlobalRewrite(op: Op<my_dialect.foo>) {
Rewrite localRewrite() {
let results = op.results;
};
}