MLIR의 단일 정규화 패스와 전역 적용 규칙, RewritePattern 및 fold 메서드를 통한 정규화 정의 방법과 사용 기준을 설명합니다.
정규화는 컴파일러 IR 설계에서 중요한 요소입니다. 이는 신뢰할 수 있는 컴파일러 변환을 더 쉽게 구현하고, 코드에서 무엇이 더 좋거나 나쁜지에 대해 추론하기 쉽게 하며, 특정 IR 단계의 목표에 대해 흥미로운 논의를 촉발합니다. Dan Gohman이 이러한 이슈를 탐구한 글을 썼으니, 이 개념에 익숙하지 않다면 읽어볼 가치가 있습니다.
대부분의 컴파일러에는 정규화 패스가 있으며, 때로는 서로 다른 정규화 패스(예: LLVM의 instcombine, dag combine 등)가 여러 개 있습니다. MLIR는 다단계 IR이기 때문에, 하나의 정규화 인프라를 제공하고 그것을 다양한 IR에서 재사용할 수 있습니다. 이 문서는 일반적인 접근 방식과 전역 정규화 규칙을 설명하고, 참고를 위해 IR별 규칙을 수록하는 섹션을 제공합니다.
MLIR에는 하나의 정규화 패스가 있으며, 로드된 모든 방언(dialect)의 정규화 패턴을 탐욕적(greedy) 방식으로 반복 적용합니다. 정규화는 최선의 노력을 하는 것이며, 전체 IR을 보장된 정규 형태로 만드는 것은 아닙니다. 패턴은 고정점에 도달하거나(더는 적용할 패턴이 없거나) 최대 반복/재기록(패스 옵션으로 지정됨) 횟수를 소진할 때까지 적용됩니다. 이는 효율성을 위한 것이며, 결함 있는 패턴이 무한 루프를 유발하지 않도록 하기 위함입니다.
정규화 패턴은 연산 자체에 등록되며, 이를 통해 각 방언이 자신만의 연산과 정규화를 함께 정의할 수 있습니다.
정규화 패턴과 관련하여 고려할 중요한 사항:
정규화의 목표는 후속 분석과 최적화를 더 효과적으로 만드는 것입니다. 따라서 정규화 자체에서의 성능 향상은 필수적이지 않습니다.
패스 파이프라인은 올바름을 위해 정규화 패스에 의존해서는 안 됩니다. 정규화 패스가 모두 제거되더라도 올바르게 동작해야 합니다.
패턴의 반복 적용은 수렴해야 합니다. 불안정하거나 순환적인 재기록은 버그로 간주됩니다. 이는 정규화 패스를 덜 예측 가능하고 덜 효과적으로 만들 수 있으며(즉, 일부 패턴이 적용되지 않을 수 있음), 수렴을 방해할 수 있습니다.
피연산자가 중복되어 값의 사용이 늘어나는 경우에는, 가능한 한 해당 값의 사용 수가 적은 형태로 정규화하는 편이 일반적으로 더 좋습니다. 일부 패턴은 값의 단일 사용자(single user)일 때만 매치되기 때문입니다. 예를 들어, 일반적으로 “x + x”를 “x * 2”로 정규화하는 것이 좋은데, 이는 x의 사용 횟수를 하나 줄여 주기 때문입니다.
가능한 경우 연산을 완전히 제거하는 것이 항상 좋습니다(예: “x + 0 = x” 같은 항등식 폴딩).
실행 시간이 비싼(예: O(n) 복잡도를 갖는) 패턴이나 복잡한 비용 모델을 가진 패턴은 정규화에 적합하지 않습니다. 알고리즘은 고정점에 도달할 때까지 반복적으로 실행되므로, 특히 매칭 단계가 빠르게 수행되는 패턴이 필요합니다.
정규화는 원래 연산의 의미(semantic)를 잃어서는 안 됩니다. 변환된 IR에서 언제나 원래 정보를 복구할 수 있어야 합니다.
예를 들어, 다음을 변환하는 패턴은
%transpose = linalg.transpose
ins(%input : tensor<1x2x3xf32>)
outs(%init1 : tensor<2x1x3xf32>)
dimensions = [1, 0, 2]
%out = linalg.transpose
ins(%transpose: tensor<2x1x3xf32>)
outs(%init2 : tensor<3x1x2xf32>)
permutation = [2, 1, 0]
다음과 같이 만드는 것입니다:
%out= linalg.transpose
ins(%input : tensor<1x2x3xf32>)
outs(%init2: tensor<3x1x2xf32>)
permutation = [2, 0, 1]
이는 중복 연산을 제거하여 다른 분석과 최적화를 더 효율적으로 만들기 때문에 좋은 정규화 패턴입니다.
다음 변환은 모든 수준의 IR에 적용됩니다.
부작용이 없고 사용처가 없는 연산의 제거.
상수 폴딩 — 예: “(addi 1, 2)”를 “3”으로. 상수 폴딩 훅은 연산이 지정합니다.
교환 법칙이 성립하는 연산자에서 상수 피연산자를 오른쪽으로 이동 — 예: “(addi 4, x)”를 “(addi x, 4)”로.
constant-like 연산은 유일화(uniqued)되고, 첫 번째 상위 배리어 영역의 엔트리 블록으로 호이스트(hoist)됩니다. 배리어 영역이란 위쪽으로부터 격리된 영역(예: 함수의 엔트리 블록) 또는 DialectFoldInterface의 shouldMaterializeInto 메서드로 배리어로 표시된 영역을 말합니다.
정규화를 정의하는 데에는 두 가지 메커니즘이 있습니다. 일반적인 RewritePattern과 fold 메서드입니다.
RewritePattern으로 정규화¶이 메커니즘은 정규화를 RewritePattern들의 집합으로 제공할 수 있게 합니다. 이는 C++로 명령형으로 정의하거나, 선언적 재작성 규칙(Declarative Rewrite Rules)로 선언적으로 정의할 수 있습니다. 패턴 재작성 인프라는 매우 다양한 종류의 정규화를 표현할 수 있습니다. 이러한 변환은 곱셈을 시프트로 바꾸는 것처럼 단순할 수도 있고, 조건 분기를 무조건 분기로 대체하는 것처럼 더 과감할 수도 있습니다.
ODS에서, 연산은 hasCanonicalizer 비트 또는 hasCanonicalizeMethod 비트를 설정하여 getCanonicalizationPatterns 메서드의 선언을 생성할 수 있습니다:
def MyOp : ... {
// 이 연산에 대해 완전히 일반적인 패턴 집합을 정의하고 싶습니다.
let hasCanonicalizer = 1;
}
def OtherOp : ... {
// 단일 "matchAndRewrite" 스타일의 RewritePattern을 메서드로 구현하면 충분합니다.
let hasCanonicalizeMethod = 1;
}
그런 다음 소스 파일에서 정규화 패턴을 제공할 수 있습니다:
void MyOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
MLIRContext *context) {
patterns.add<...>(...);
}
LogicalResult OtherOp::canonicalize(OtherOp op, PatternRewriter &rewriter) {
// 패턴과 재기록을 여기서 수행합니다.
return failure();
}
연산 재작성 정의에 대한 정보는 퀵스타트 가이드를 참고하세요.
fold 메서드로 정규화¶fold 메커니즘은 의도적으로 제한적이지만 강력하여, 컴파일러 전반의 많은 위치에서 정규화를 적용할 수 있게 합니다. 예를 들어, 정규화 패스 외부에서도 fold는 방언 변환 인프라에서 합법화 메커니즘으로 사용되며, 어디서든 OpBuilder::createOrFold가 있는 곳에서 직접 호출될 수 있습니다.
fold에는 새로운 연산을 생성할 수 없고 루트 연산만 대체할 수 있으며(삭제는 불가), 대신 제자리(in-place)에서 연산을 갱신하거나 기존 값(또는 속성) 집합을 반환하여 연산을 대체할 수 있다는 제약이 있습니다. 이를 통해 fold 메서드는 진정한 “로컬” 변환이 되며, 패턴 리라이터 없이도 호출할 수 있습니다.
ODS에서, 연산은 hasFolder 비트를 설정하여 fold 메서드의 선언을 생성할 수 있습니다. 이 메서드는 연산의 구조에 따라 다른 형태를 가집니다.
def MyOp : ... {
let hasFolder = 1;
}
연산이 단일 결과를 가지면 다음이 생성됩니다:
/// 이 훅의 구현은 연산에 대해 다음 변경만 수행할 수 있습니다:
///
/// 1. IR을 변경하지 않고 연산을 그대로 두고 nullptr을 반환할 수 있습니다.
/// 2. IR의 다른 어떤 것도 변경하지 않고 연산을 제자리에서 변경(mutate)할 수 있습니다.
/// 이 경우, 연산 자기 자신을 반환합니다.
/// 3. 연산 대신 사용할 수 있는 기존 값 또는 속성을 반환할 수 있습니다.
/// 호출자는 연산을 제거하고 그 결과를 대신 사용합니다.
///
OpFoldResult MyOp::fold(FoldAdaptor adaptor) {
...
}
그렇지 않으면, 다음이 생성됩니다:
/// 이 훅의 구현은 연산에 대해 다음 변경만 수행할 수 있습니다:
///
/// 1. IR을 변경하지 않고 연산을 그대로 두고 failure를 반환할 수 있습니다.
/// 2. IR의 다른 어떤 것도 변경하지 않고 연산을 제자리에서 변경(mutate)할 수 있습니다.
/// 이 경우, success를 반환합니다.
/// 3. 연산 대신 사용할 수 있는 기존 값 또는 속성의 목록을 반환할 수 있습니다.
/// 이 경우, 결과 리스트를 채우고 success를 반환합니다. 결과 리스트는 연산의
/// 결과와 1:1로 대응해야 하며, 부분 폴딩은 지원되지 않습니다. 호출자는 연산을
/// 제거하고 해당 결과들을 대신 사용합니다.
///
/// 이 메커니즘은 결과가 0개인 연산을 제거하는 데 사용할 수 없음을 유의하세요.
LogicalResult MyOp::fold(FoldAdaptor adaptor,
SmallVectorImpl<OpFoldResult> &results) {
...
}
위에서 각 메서드에는 각 피연산자에 대한 getter를 제공하는 FoldAdaptor가 제공되며, 해당 상수 속성(attribute)을 반환합니다. 이러한 피연산자는 ConstantLike 특성을 구현하는 것들입니다. 피연산자 중 상수가 아닌 것이 있으면 null Attribute 값이 대신 제공됩니다. 예를 들어, MyOp가 세 개의 피연산자 [a, b, c]를 제공하는데 b만 상수라면, adaptor는 getA()와 getC()에 대해 Attribute()를 반환하고, getB()에는 b 값의 속성을 반환합니다.
또한 위에는 OpFoldResult의 사용이 있습니다. 이 클래스는 연산 결과의 폴딩 결과를 나타내며, SSA Value 또는 상수 결과의 경우 Attribute가 될 수 있습니다. SSA Value를 제공하는 경우, 그것은 반드시 기존 값에 해당해야 합니다. fold 메서드는 새로운 Value를 생성할 수 없습니다. 반환되는 Attribute 값의 형태에 대한 특별한 제약은 없지만, 특정 Type에 대한 Attribute 표현이 일관되도록 하는 것이 중요합니다.
연산의 fold 훅이 성공하지 못하는 경우, 방언은 DialectFoldInterface를 구현하고 fold 훅을 재정의하여 폴백을 제공할 수 있습니다.
fold 메서드가 결과로 Attribute를 반환하면, 이는 해당 결과가 “상수”임을 의미합니다. Attribute는 그 값의 상수 표현입니다. 정규화 패스와 같은 fold 사용자는 이러한 Attribute를 받아 IR에서 그것을 표현하는 상수 연산을 구체화(materialize)합니다. 이러한 구체화를 가능하게 하려면, 연산의 방언이 materializeConstant 훅을 구현해야 합니다. 이 훅은 일반적으로 fold가 반환한 Attribute 값을 입력으로 받아, 그 값을 구체화하는 “constant-like” 연산을 생성합니다.
ODS에서, 방언은 hasConstantMaterializer 비트를 설정하여 materializeConstant 메서드의 선언을 생성할 수 있습니다.
def MyDialect : ... {
let hasConstantMaterializer = 1;
}
그런 다음 소스 파일에서 상수를 구체화할 수 있습니다:
/// 주어진 속성 값과 원하는 결과 타입으로 단일 상수 연산을 구체화하는 훅입니다.
/// 이 메서드는 제공된 빌더를 사용하되, 삽입 위치를 변경하지 않고 연산을 생성해야 합니다.
/// 생성된 연산은 constant-like이어야 합니다. 성공 시, 상수 값을 나타내기 위해
/// 생성된 값을 반환해야 합니다. 실패 시에는 nullptr을 반환해야 합니다.
Operation *MyDialect::materializeConstant(OpBuilder &builder, Attribute value,
Type type, Location loc) {
...
}
fold 메서드와 RewriterPattern 중 무엇을 사용할지¶가능하다면 항상 fold 메서드로 정규화를 구현하고, 그렇지 못한 경우 RewritePattern으로 구현해야 합니다.