MLIR의 패스 인프라스트럭처 개요와 사용 방법을 설명합니다.
패스는 변환과 최적화를 위한 기본 인프라스트럭처를 나타냅니다. 이 문서는 MLIR의 패스 인프라스트럭처 개요와 사용 방법을 제공합니다.
MLIR 및 핵심 개념(예: IR 구조와 연산)에 대한 더 많은 정보는 MLIR 명세를 참조하십시오.
MLIR에서 그래프 리라이팅을 빠르게 시작하려면 MLIR Rewrites를 참조하십시오. 변환이 연산 DAG의 패턴 매칭을 포함하는 경우 매우 좋은 출발점입니다.
MLIR에서 추상화와 변환의 주요 단위는 연산(operation)입니다. 따라서 패스 매니저는 서로 다른 중첩 수준의 연산 인스턴스에서 동작하도록 설계되었습니다. 이하에서 패스가 동작하는 연산을 “현재 연산(current operation)”이라 부릅니다.
패스 매니저의 구조와 중첩 개념은 아래에서 자세히 설명합니다. MLIR의 모든 패스는 OperationPass에서 파생되며 다음 제약을 준수해야 합니다. 이를 위반하면 멀티스레딩 및 기타 고급 시나리오에서 문제가 발생할 수 있습니다.
현재 연산의 형제(sibling) 연산의 상태를 검사해서는 안 됩니다. 또한 그 형제 밑에 중첩된 연산에도 접근해서는 안 됩니다.
현재 연산 밑에 중첩된 연산 이외의 연산 상태를 수정해서는 안 됩니다. 이는 조상/부모 블록에서 다른 연산을 추가/수정/삭제하는 것을 포함합니다.
runOnOperation 호출 간에 변경 가능한 패스 상태를 유지해서는 안 됩니다. 패스는 실행 순서 보장 없이 여러 다른 연산에서 실행될 수 있습니다.
전역 변경 가능한 상태(예: 소스 파일의 static 변수)를 유지해서는 안 됩니다. 모든 변경 가능한 상태는 패스 인스턴스가 유지해야 합니다.
복사 생성 가능해야 합니다.
기본적으로 연산 패스는 op-agnostic이며, 이는 그 패스를 추가한 패스 매니저의 연산 타입에서 동작함을 의미합니다. 즉, 하나의 패스가 다양한 연산 타입에서 동작할 수 있습니다. 이런 비특정 패스는 자신이 실행되는 연산에 대해 가정을 두지 않도록 작성해야 합니다. 이 유형의 예로는 정규화(Canonicalization)와 공통 부분식 제거(CSE)가 있습니다.
비특정 연산 패스를 만들려면 파생 클래스가 다음을 따라야 합니다.
OperationPass를 상속합니다.void runOnOperation()을 오버라이드합니다.단순한 패스 예시는 다음과 같습니다:
/// 여기서는 CRTP `PassWrapper` 유틸리티 클래스를 사용해 필요한
/// 유틸리티 훅을 제공합니다. 이는 C++에서 직접 정의된 패스에만 필요합니다.
/// 선언적으로 정의된 패스는 이러한 유틸리티를 더 깔끔한 방식으로 제공합니다.
struct MyOperationPass : public PassWrapper<MyOperationPass, OperationPass<>> {
void runOnOperation() override {
// 현재 처리 중인 연산을 가져옵니다.
Operation *op = getOperation();
...
}
};
패스가 특정 타입이나 클래스의 연산으로 실행을 제한할 필요가 있다면, 추가 필터링을 적용할 수 있습니다. 이는 한때 agnostic이던 패스를 특정 컨텍스트에 더 특화된 패스로 변환합니다. 패스 실행을 필터링하는 방법은 여러 가지가 있으며, 필터링이 적용되는 컨텍스트도 다양합니다.
정적 필터링은 패스가 스케줄될 수 있는 연산 타입에 추가 제약을 가할 수 있게 해줍니다. 이 유형의 필터링은 일반적으로 필요한 제약을 만족하는 연산에서만 스케줄될 수 있는 더 제한된 패스를 구축할 수 있게 합니다. 예를 들어, 특정 연산 타입의 모든 인스턴스에 적용되는 인터페이스나 트레이트 또는 기타 제약을 제공하는 연산에서만 실행되는 패스를 지정할 수 있습니다. 아래는 FunctionOpInterface를 구현한 연산에서만 스케줄을 허용하는 패스의 예입니다:
struct MyFunctionPass : ... {
/// 이 메서드는 추가 정적 필터링을 제공하는 데 사용되며,
/// 주어진 연산 타입에서 패스를 스케줄할 수 있는지를 반환합니다.
bool canScheduleOn(RegisteredOperationName opInfo) const override {
return opInfo.hasInterface<FunctionOpInterface>();
}
void runOnOperation() {
// `canScheduleOn`이 해당 인터페이스를 구현하는 연산에서만 패스가 실행됨을 보장하므로
// 여기서 자유롭게 FunctionOpInterface로 캐스팅할 수 있습니다.
FunctionOpInterface op = cast<FunctionOpInterface>(getOperation());
}
};
정적 필터링을 가진 패스를 op-specific 패스 매니저에 추가하면, 패스 매니저의 연산 타입이 패스의 정적 제약을 만족하는지 단언합니다. 이를 op-agnostic 패스 매니저에 추가하면, 그 패스 매니저와 그 안에 포함된 모든 패스는 해당 패스의 정적 제약을 상속받습니다. 예를 들어 위의 MyFunctionPass처럼 FunctionOpInterface로 필터링하는 패스가 있으면, 패스 매니저 내에서 실행되는 어떤 패스든 FunctionOpInterface를 구현하는 연산에서만 고려됩니다. 이 불변식은 중요합니다. op-agnostic 패스 매니저에 추가되는 각 패스는 해당 패스 매니저에서 스케줄 가능한 연산을 더 제한하기 때문입니다. 다음 예를 생각해 보십시오:
func.func @foo() {
// ...
return
}
module @someModule {
// ...
}
위 MLIR 스니펫에 any(cse,my-function-pass)라는 op-agnostic 파이프라인을 적용한다면, 이는 foo 함수 연산에서만 실행될 것입니다. 그 이유는 my-function-pass가 FunctionOpInterface를 구현하는 연산에만 스케줄되도록 정적 필터링 제약을 갖기 때문입니다. 이 제약은 전체 패스 매니저가 상속하므로, cse처럼 일반적으로 어떤 연산에도 스케줄될 수 있는 패스라도 someModule은 전혀 고려되지 않습니다.
위 섹션에서는 패스가 스케줄될 수 있는 연산 타입을 정적으로 필터링하는 일반적인 메커니즘을 설명했습니다. 단일 연산 타입에서만 스케줄되도록 제한된 패스를 더 쉽게 정의하기 위한 편의 문법도 제공됩니다. 이 경우, 패스는 OperationPass 기본 클래스에 연산 타입만 제공하면 됩니다. 그러면 해당 연산 타입에 대한 필터링이 자동으로 적용됩니다:
/// 여기서는 CRTP `PassWrapper` 유틸리티 클래스를 사용해 필요한
/// 유틸리티 훅을 제공합니다. 이는 C++에서 직접 정의된 패스에만 필요합니다.
/// 선언적으로 정의된 패스는 이러한 유틸리티를 더 깔끔한 방식으로 제공합니다.
struct MyFunctionPass : public PassWrapper<MyOperationPass, OperationPass<func::FuncOp>> {
void runOnOperation() {
// 현재 처리 중인 연산을 가져옵니다.
func::FuncOp op = getOperation();
}
};
위 섹션에서는 패스가 스케줄될 수 있는 연산 타입을 정적으로 필터링하는 일반적인 메커니즘을 설명했습니다. 특정 연산 인터페이스에서만 스케줄되도록 제한된 패스를 더 쉽게 정의하기 위한 편의 문법도 제공합니다. 이 경우, 패스는 InterfacePass 기본 클래스를 상속하기만 하면 됩니다. 이 클래스는 OperationPass와 유사하지만 동작할 인터페이스 타입을 기대합니다. 그러면 해당 인터페이스 타입에 대한 필터링이 자동으로 적용됩니다:
/// 여기서는 CRTP `PassWrapper` 유틸리티 클래스를 사용해 필요한
/// 유틸리티 훅을 제공합니다. 이는 C++에서 직접 정의된 패스에만 필요합니다.
/// 선언적으로 정의된 패스는 이러한 유틸리티를 더 깔끔한 방식으로 제공합니다.
struct MyFunctionPass : public PassWrapper<MyOperationPass, InterfacePass<FunctionOpInterface>> {
void runOnOperation() {
// 현재 처리 중인 연산을 가져옵니다.
FunctionOpInterface op = getOperation();
}
};
다이얼렉트의 엔티티(연산, 타입, 속성 등)를 생성하려면 먼저 MLIRContext에 해당 다이얼렉트가 로드되어 있어야 합니다. 또한 멀티스레드 패스 파이프라인 실행을 시작하기 전에 다이얼렉트가 로드되어 있어야 합니다. 이를 위해, 아직 로드되어 있다고 보장되지 않은 다이얼렉트의 엔티티를 생성할 수 있는 패스는 getDependentDialects()를 오버라이드하여 해당 다이얼렉트 목록을 명시적으로 선언해야 합니다. TableGen 명세의 dependentDialects 필드도 참고하십시오.
특정 상황에서는 패스가 동적으로 구성되는 상태를 포함할 수 있으며, 이는 패스를 연속 실행할 때 재계산 비용이 클 수 있습니다. 한 예로 런타임에 바이트코드로 컴파일되는 PDL 기반패턴을 사용하는 경우가 있습니다. 이런 상황에서는 다음 훅을 오버라이드하여 무거운 상태를 초기화할 수 있습니다:
LogicalResult initialize(MLIRContext *context)이 훅은 전체 패스 파이프라인 실행당 한 번 실행되며, 따라서 runOnOperation 호출 중에 사용할 수 있는 상태에는 접근할 수 없습니다. 보다 구체적으로, 모든 MLIRContext 접근은 제공된 context 매개변수를 통해 이루어져야 하며, getContext/getOperation/getAnalysis 등과 같은 “실행당(per-run)” 상태를 사용하는 메서드는 사용해서는 안 됩니다. 초기화 중 오류가 발생하면, 패스는 오류 진단을 출력하고 failure()를 반환하여 패스 파이프라인 실행을 중단해야 합니다.
변환 패스와 더불어 중요한 개념이 분석입니다. 분석은 특정 연산에 대한 정보를 계산하되, 해당 연산을 수정하지 않는다는 점을 제외하면 개념적으로 변환 패스와 유사합니다. MLIR에서 분석은 패스가 아니라 독립형 클래스이며, 필요할 때 지연 계산되고 캐시되어 불필요한 재계산을 피합니다. MLIR의 분석은 다음을 준수해야 합니다.
Operation* 또는 Operation*와 AnalysisManager &를 받는 유효한 생성자를 제공합니다.
AnalysisManager &는 필요한 분석 의존성을 조회하는 데 사용해야 합니다.주어진 연산을 수정해서는 안 됩니다.
또한 분석은 다양한 동작을 제어하기 위한 추가 훅을 제공할 수 있습니다:
bool isInvalidated(const AnalysisManager::PreservedAnalyses &)보존된 분석 집합이 주어지면, 분석이 실제로 무효화되어야 하는지를 true/false로 반환합니다. 이는 분석이 명시적으로 보존되지는 않았지만, 분석 집합 등의 다른 속성에 따라 보존(또는 무효화)될 수 있는 경우에 더 미세한 무효화를 가능하게 합니다. 분석이 다른 분석을 의존성으로 사용하는 경우, 해당 의존성이 무효화되었는지도 확인해야 합니다.
기본 OperationPass 클래스는 현재 처리 중인 연산에 대한 분석을 조회하고 보존하는 유틸리티를 제공합니다.
getAnalysis<>
getCachedAnalysis<>
getCachedParentAnalysis<>
getCachedChildAnalysis<>
getChildAnalysis<>
위에서 정의한 예시 패스를 사용하여 몇 가지 예를 보겠습니다:
/// 흥미로운 분석.
struct MyOperationAnalysis {
// 제공된 연산으로 이 분석을 계산합니다.
MyOperationAnalysis(Operation *op);
};
struct MyOperationAnalysisWithDependency {
MyOperationAnalysisWithDependency(Operation *op, AnalysisManager &am) {
// 의존성으로 다른 분석을 요청합니다.
MyOperationAnalysis &otherAnalysis = am.getAnalysis<MyOperationAnalysis>();
...
}
bool isInvalidated(const AnalysisManager::PreservedAnalyses &pa) {
// 분석 또는 그 의존성이 무효화되었는지 확인합니다.
return !pa.isPreserved<MyOperationAnalysisWithDependency>() ||
!pa.isPreserved<MyOperationAnalysis>();
}
};
void MyOperationPass::runOnOperation() {
// 현재 연산에 대한 MyOperationAnalysis를 조회합니다.
MyOperationAnalysis &myAnalysis = getAnalysis<MyOperationAnalysis>();
// 현재 연산에 대한 MyOperationAnalysis의 캐시된 인스턴스를 조회합니다.
// 존재하지 않으면 계산되지 않습니다.
auto optionalAnalysis = getCachedAnalysis<MyOperationAnalysis>();
if (optionalAnalysis)
...
// 현재 연산의 부모 연산에 대한 MyOperationAnalysis의 캐시된 인스턴스를 조회합니다.
// 존재하지 않으면 계산되지 않습니다.
auto optionalAnalysis = getCachedParentAnalysis<MyOperationAnalysis>();
if (optionalAnalysis)
...
}
패스에 의해 조회된 후 구성된 분석은 나중에 다시 요청될 때 불필요한 계산을 피하기 위해 캐시됩니다. 오래된(stale) 분석을 피하기 위해, 기본적으로 모든 분석은 패스에 의해 무효화된 것으로 간주됩니다. 무효화를 피하려면, 패스는 보존된 것으로 알려진 분석을 명시적으로 표시해야 합니다.
markAllAnalysesPreservedmarkAnalysesPreserved<>void MyOperationPass::runOnOperation() {
// 모든 분석을 보존으로 표시합니다. 패스가 어떤 변환도 수행하지 않았음을
// 보장할 수 있는 경우 유용합니다.
markAllAnalysesPreserved();
// 특정 분석만 보존으로 표시합니다. 일부 변환이 수행되었더라도,
// 일부 분석이 영향을 받지 않았거나 명시적으로 보존된 경우에 사용합니다.
markAnalysesPreserved<MyAnalysis, MyAnalyses...>();
}
MLIR의 패스는 우아하게 실패하도록 허용됩니다. 이는 패스의 불변식이 깨져 IR이 잘못된 상태가 될 수 있는 경우 발생합니다. 이런 상황이 발생하면, 패스는 signalPassFailure 메서드를 통해 패스 매니저에 직접 실패를 신호할 수 있습니다. 실행 중 패스가 실패를 신호하면, 파이프라인의 다른 패스는 실행되지 않으며 최상위 PassManager::run 호출은 failure를 반환합니다.
void MyOperationPass::runOnOperation() {
// 깨진 불변식에 대해 실패를 신호합니다.
if (some_broken_invariant)
return signalPassFailure();
}
위 섹션에서는 다양한 유형의 패스와 그 불변식을 소개했습니다. 본 섹션에서는 PassManager 개념과 이를 사용해 패스 파이프라인을 구성하고 스케줄하는 방법을 소개합니다. 패스 관리와 관련된 주요 클래스는 PassManager와 OpPassManager 두 가지입니다. PassManager는 최상위 진입점 역할을 하며 전체 패스 파이프라인에 사용되는 다양한 구성을 포함합니다. OpPassManager는 특정 중첩 수준에서 실행할 패스를 스케줄하는 데 사용됩니다. 최상위 PassManager 또한 OpPassManager로 동작합니다.
OpPassManager는 본질적으로 주어진 중첩 수준의 연산에서 실행되도록 고정(앵커)된 패스 모음입니다. 패스 매니저는 특정 연산 타입에 고정된 op-specific일 수도 있고, 특정 연산에 제한되지 않고 실행 가능한 어떤 연산 타입에서도 실행되는 op-agnostic일 수도 있습니다. 패스 매니저를 고정하는 연산 타입은 다음 요건을 만족해야 합니다:
등록되어 있으며 IsolatedFromAbove로 표시되어야 합니다.
패스는 addPass를 통해 패스 매니저에 추가할 수 있습니다.
OpPassManager는 일반적으로 기존 OpPassManager 내에 nest<OpT> 또는 nestAny 메서드로 파이프라인을 명시적으로 중첩해 생성합니다. 전자는 중첩된 패스 매니저가 동작할 연산 타입을 인수로 받습니다. 후자는 어떤 실행 가능한 연산 타입에서도 동작할 수 있는 op-agnostic 패스 매니저를 중첩합니다. 이러한 중첩은 IR의 Region 내 구조적 중첩에 대응됩니다.
예를 들어, 다음 .mlir:
module {
spirv.module "Logical" "GLSL450" {
func @foo() {
...
}
}
}
의 중첩 구조는 다음과 같습니다:
`builtin.module`
`spirv.module`
`spirv.func`
아래는 위 구조에서 동작하는 파이프라인을 구성하는 예입니다:
// 최상위 `PassManager` 클래스를 생성합니다.
auto pm = PassManager::on<ModuleOp>(ctx);
// 최상위 module 연산에 패스를 추가합니다.
pm.addPass(std::make_unique<MyModulePass>());
// 최상위 module 바로 아래에 중첩된 `spirv.module` 연산에서 동작하는
// 패스 매니저를 중첩합니다.
OpPassManager &nestedModulePM = pm.nest<spirv::ModuleOp>();
nestedModulePM.addPass(std::make_unique<MySPIRVModulePass>());
// 중첩된 SPIRV module 내의 함수에서 동작하는 패스 매니저를 중첩합니다.
OpPassManager &nestedFunctionPM = nestedModulePM.nest<func::FuncOp>();
nestedFunctionPM.addPass(std::make_unique<MyFunctionPass>());
// op-agnostic 패스 매니저를 중첩합니다. 이는 실행 가능한 모든 연산에서 동작합니다.
// 예: func.func, spirv.func, spirv.module, builtin.module 등.
OpPassManager &nestedAnyPM = nestedModulePM.nestAny();
nestedAnyPM.addPass(createCanonicalizePass());
nestedAnyPM.addPass(createCSEPass());
// 최상위 모듈에 대해 패스 매니저를 실행합니다.
ModuleOp m = ...;
if (failed(pm.run(m)))
... // 패스 중 하나가 실패를 신호했습니다.
위 패스 매니저는 다음과 같은 파이프라인 구조를 포함합니다:
OpPassManager<ModuleOp>
MyModulePass
OpPassManager<spirv::ModuleOp>
MySPIRVModulePass
OpPassManager<func::FuncOp>
MyFunctionPass
OpPassManager<>
Canonicalizer
CSE
이러한 파이프라인은 한 번에 하나의 연산에 대해 실행됩니다. 예를 들어, func::FuncOp에 대한 연속적인 패스 시리즈가 주어지면, 첫 번째 함수에 대해 모두 실행한 뒤 두 번째 함수에 대해 모두 실행하는 식으로 프로그램 전체가 패스를 통과할 때까지 반복됩니다. 이는 몇 가지 이점을 제공합니다:
일부 상황에서는 다른 패스 내에서 패스 파이프라인을 실행하여, 현재 처리 중인 연산의 불변식에 따라 구성하거나 필터링하는 것이 유용할 수 있습니다. 예를 들어 인라이너 패스는 더 나은 비용 모델을 만들고 더 최적의 인라이닝을 제공하기 위해, 인라이닝 도중에 함수 내부 단순화 패스를 실행하고 싶을 수 있습니다. 이를 위해, 패스는 LogicalResult Pass::runPipeline(OpPassManager &, Operation *) 메서드를 통해 현재 연산 또는 현재 연산에 중첩된 어떤 연산에 대해서도 임의의 OpPassManager를 실행할 수 있습니다. 이 메서드는 최상위 PassManager::run 결과와 마찬가지로 동적 파이프라인의 성공/실패 여부를 반환합니다. 간단한 예는 아래와 같습니다:
void MyModulePass::runOnOperation() {
ModuleOp module = getOperation();
if (hasSomeSpecificProperty(module)) {
OpPassManager dynamicPM("builtin.module");
...; // 동적 파이프라인을 구성합니다.
if (failed(runPipeline(dynamicPM, module)))
return signalPassFailure();
}
}
참고: 위에서는 동적 파이프라인을 runOnOperation 메서드 내에서 구성했지만, 반드시 그럴 필요는 없으며, OpPassManager 클래스는 안전하게 복사 생성할 수 있으므로 가능하면 파이프라인을 캐시해야 합니다.
이 섹션에서 설명한 메커니즘은 중첩 방식으로 패스 파이프라인을 실행해야 할 때(즉, 중첩 파이프라인을 주 파이프라인과 함께 정적으로 스케줄할 수 없을 때) 사용해야 합니다. 보다 구체적으로, 일반적으로 PassManager를 Pass 내부에서 생성할 필요는 없습니다. runPipeline을 사용하면 모든 분석, 인스트루멘테이션, 및 기타 패스 매니저 관련 구성 요소가 실행 중인 동적 파이프라인과 통합되는 것도 보장됩니다.
MLIR은 패스가 동작을 구성하도록 옵션을 지정할 수 있는 내장 메커니즘을 제공합니다. 이러한 옵션은 패스의 각 인스턴스에 대해, 패스 생성 시점에 파싱됩니다. 옵션은 Option<>과 ListOption<> 클래스를 사용해 정의하며, 일반적으로 LLVM 명령줄 플래그 정의 규칙을 따릅니다. LLVM 명령줄 기능과의 한 가지 큰 차이는 모든 ListOption이 쉼표로 구분되며, 리스트의 개별 요소 내에 구분된 서브 범위는 쉼표를 포함할 수 있지만 최상위 리스트의 구분자로 처리되지 않는다는 점입니다.
struct MyPass ... {
/// 옵션이 올바르게 초기화되도록 기본 생성자와 복사 생성자를
/// 유효하게 유지합니다.
MyPass() = default;
MyPass(const MyPass& pass) {}
/// 설명 이후의 파라미터는 각각 llvm::cl::list와 llvm::cl::opt로 전달됩니다.
Option<int> exampleOption{*this, "flag-name", llvm::cl::desc("...")};
ListOption<int> exampleListOption{*this, "list-flag-name", llvm::cl::desc("...")};
};
패스 파이프라인의 경우, PassPipelineRegistration 템플릿은 선택적 Option 구조체 정의를 위한 추가 템플릿 파라미터를 받습니다. 이 구조체는 mlir::PassPipelineOptions를 상속하고 원하는 파이프라인 옵션을 포함해야 합니다. PassPipelineRegistration을 사용할 때, 생성자는 이제 void (OpPassManager &pm, const MyPipelineOptions&) 시그니처의 함수를 받으며, 이 함수는 옵션에서 패스를 구성하고 pm에 추가해야 합니다:
struct MyPipelineOptions : public PassPipelineOptions {
// 옵션의 구조는 패스 옵션과 동일합니다.
Option<int> exampleOption{*this, "flag-name", llvm::cl::desc("...")};
ListOption<int> exampleListOption{*this, "list-flag-name",
llvm::cl::desc("...")};
};
void registerMyPasses() {
PassPipelineRegistration<MyPipelineOptions>(
"example-pipeline", "Run an example pipeline.",
[](OpPassManager &pm, const MyPipelineOptions &pipelineOptions) {
// 패스 매니저를 초기화합니다.
});
}
통계는 컴파일러가 무엇을 하고 있는지, 그리고 다양한 변환이 얼마나 효과적인지 추적하는 방법입니다. 특정 입력에서 특정 변환이 어떤 영향을 미치고 얼마나 자주 트리거되는지 확인하는 것은 종종 유용합니다. 패스 통계는 각 패스 인스턴스에 특화되어 있어, 특정 변환을 패스 파이프라인의 특정 위치에 배치했을 때의 효과를 볼 수 있습니다. 예를 들어 “여기서 CSE를 한 번 더 실행하면 무엇이 달라지지?”와 같은 질문에 답하는 데 도움이 됩니다.
통계는 ‘Pass::Statistic’ 클래스를 사용하여 패스에 추가할 수 있습니다. 이 클래스는 생성자로 부모 패스, 이름, 설명을 받습니다. 이 클래스는 원자적 부호 없는 정수처럼 동작하며, 증가 및 갱신할 수 있습니다. 이러한 통계는 llvm::Statistic과 동일한 인프라를 사용하므로 유사한 사용 제약을 가집니다. 수집된 통계는 패스 매니저를 통해 프로그램적으로 PassManager::enableStatistics로 덤프하거나, 커맨드라인에서 -mlir-pass-statistics와 -mlir-pass-statistics-display로 덤프할 수 있습니다.
예시는 아래와 같습니다:
struct MyPass ... {
/// 옵션이 올바르게 초기화되도록 기본 생성자와 복사 생성자를
/// 유효하게 유지합니다.
MyPass() = default;
MyPass(const MyPass& pass) {}
StringRef getArgument() const final {
// 이는 텍스트 형식(예: 명령줄)에서 패스를 지칭할 때 사용하는 인자입니다.
return "argument";
}
StringRef getDescription() const final {
// 패스에 대한 간단한 설명입니다.
return "description";
}
/// MyPass 실행 동안 추적할 통계를 정의합니다.
Statistic exampleStat{this, "exampleStat", "An example statistic"};
void runOnOperation() {
...
// 어떤 불변식이 충족된 후 통계를 갱신합니다.
++exampleStat;
...
}
};
수집된 통계는 두 가지 유형의 보기로 집계될 수 있습니다:
파이프라인 보기는 패스 매니저의 구조를 모델링하며, 기본 보기입니다:
$ mlir-opt -pass-pipeline='any(func.func(my-pass,my-pass))' foo.mlir -mlir-pass-statistics
===-------------------------------------------------------------------------===
... Pass statistics report ...
===-------------------------------------------------------------------------===
'func.func' Pipeline
MyPass
(S) 15 exampleStat - An example statistic
VerifierPass
MyPass
(S) 6 exampleStat - An example statistic
VerifierPass
VerifierPass
리스트 보기는 특정 패스의 모든 인스턴스 통계를 함께 집계합니다:
$ mlir-opt -pass-pipeline='any(func.func(my-pass,my-pass))' foo.mlir -mlir-pass-statistics -mlir-pass-statistics-display=list
===-------------------------------------------------------------------------===
... Pass statistics report ...
===-------------------------------------------------------------------------===
MyPass
(S) 21 exampleStat - An example statistic
다양한 패스 유형의 예시 정의에서 간략히 보았듯이 PassRegistration 클래스가 있습니다. 이 메커니즘은 패스 클래스를 등록하여 텍스트 패스 파이프라인 설명 내에서 생성할 수 있게 해줍니다. 등록 예시는 아래와 같습니다:
void registerMyPass() {
PassRegistration<MyPass>();
}
MyPass는 파생 패스 클래스의 이름입니다.getArgument() 메서드는 패스를 지칭하는 데 사용될 식별자를 가져오는 데 사용됩니다.getDescription() 메서드는 패스를 설명하는 간단한 요약을 제공합니다.기본 생성할 수 없는 패스의 경우, PassRegistration은 패스를 생성하는 콜백을 받는 선택적 인수를 허용합니다:
void registerMyPass() {
PassRegistration<MyParametricPass>(
[]() -> std::unique_ptr<Pass> {
std::unique_ptr<Pass> p = std::make_unique<MyParametricPass>(/*options*/);
/*... 패스를 구성하기 위한 비자명한 로직 ...*/;
return p;
});
}
이 등록 변형은 예를 들어, 명령줄 인수에서 패스 구성을 받아 패스 생성자에 전달하는 데 사용할 수 있습니다.
참고: 패스 매니저가 병렬 실행을 위해 패스의 복사본을 생성할 수 있으므로, 패스가 데이터를 공유하지 않는 방식으로 복사 생성 가능하도록 하십시오.
위에서 설명한 것은 특정 파생 패스 클래스를 등록하는 메커니즘입니다. 그 위에 MLIR은 유사한 방식으로 사용자 정의 패스 파이프라인을 등록할 수 있게 합니다. 이를 통해 사용자 정의 파이프라인을 mlir-opt와 같은 도구에서 패스와 동일한 방식으로 사용할 수 있으며, “-O1”과 같은 일반적인 파이프라인을 캡슐화하는 데 유용합니다. 파이프라인은 PassPipelineRegistration 형태로 패스와 유사한 메커니즘을 통해 등록됩니다. PassRegistration과 비교하여, 이 클래스는 제공된 OpPassManager를 수정하는 파이프라인 빌더를 추가 파라미터로 받습니다.
void pipelineBuilder(OpPassManager &pm) {
pm.addPass(std::make_unique<MyPass>());
pm.addPass(std::make_unique<MyOtherPass>());
}
void registerMyPasses() {
// 기존 파이프라인 빌더 함수를 등록합니다.
PassPipelineRegistration<>(
"argument", "description", pipelineBuilder);
// 인라인 파이프라인 빌더를 등록합니다.
PassPipelineRegistration<>(
"argument", "description", [](OpPassManager &pm) {
pm.addPass(std::make_unique<MyPass>());
pm.addPass(std::make_unique<MyOtherPass>());
});
}
이전 섹션에서는 특정 인자와 설명으로 패스와 패스 파이프라인을 등록하는 방법을 설명했습니다. 등록이 완료되면, 문자열 설명으로 패스 매니저를 구성하는 데 사용할 수 있습니다. 이는 명령줄에서 패스 매니저를 구성하는 mlir-opt와 같은 도구나, 동적 패스 파이프라인을 활용하는 패스의 옵션으로 특히 유용합니다.
패스 파이프라인의 전체 구조를 기술하는 기능을 지원하기 위해, MLIR은 사용자 정의 텍스트 설명을 지원합니다. 텍스트 설명에는 중첩 구조, 실행할 패스와 패스 파이프라인의 인자, 해당 패스와 파이프라인의 옵션이 포함됩니다. 텍스트 파이프라인은 일련의 이름으로 정의되며, 각 이름은 그 자체로 재귀적으로 중첩된 파이프라인 설명을 포함할 수 있습니다. 명세의 문법은 다음과 같습니다:
pipeline ::= op-anchor `(` pipeline-element (`,` pipeline-element)* `)`
pipeline-element ::= pipeline | (pass-name | pass-pipeline-name) options?
options ::= '{' (key ('=' value)?)+ '}'
op-anchor
func.func 또는 builtin.module)이거나, 실행 가능한 어떤 연산(즉, 패스 매니저의 기준점으로 사용할 수 있는 연산)에서도 실행되는 op-agnostic 패스 매니저의 경우 any입니다.pass-name | pass-pipeline-name
cse 또는 canonicalize).options
예를 들어 다음 파이프라인은:
$ mlir-opt foo.mlir -cse -canonicalize -convert-func-to-llvm='use-bare-ptr-memref-call-conv=1'
다음과 같이 지정할 수도 있습니다(-pass-pipeline 플래그 사용):
# `func.func` 연산에 cse와 canonicalize 패스를 고정(앵커)합니다.
$ mlir-opt foo.mlir -pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-func-to-llvm{use-bare-ptr-memref-call-conv=1})'
# "any" 실행 가능한 최상위 연산에 cse와 canonicalize 패스를 고정합니다.
$ mlir-opt foo.mlir -pass-pipeline='builtin.module(any(cse,canonicalize),convert-func-to-llvm{use-bare-ptr-memref-call-conv=1})'
OpPassManager::printAsTextualPipeline(raw_ostream&)를 사용해 패스를 텍스트 표현으로 라운드트립하려면, 패스 등록 시 사용된 인자를 지정하도록 StringRef Pass::getArgument()를 오버라이드하십시오.
패스의 일부 측면은 연산과 유사한 선언적 형태로 지정할 수 있습니다. 이 명세는 패스를 정의할 때 사용되는 여러 메커니즘을 단순화합니다. 패스 등록 호출 생성, 보일러플레이트 패스 유틸리티 정의, 패스 문서 생성에 사용할 수 있습니다.
다음과 같이 C++로 지정된 패스를 생각해 봅시다:
struct MyPass : PassWrapper<MyPass, OperationPass<ModuleOp>> {
MyPass() = default;
MyPass(const MyPass &) {}
...
// 옵션을 지정합니다.
Option<bool> option{
*this, "example-option",
llvm::cl::desc("An example option"), llvm::cl::init(true)};
ListOption<int64_t> listOption{
*this, "example-list",
llvm::cl::desc("An example list option")};
// 통계를 지정합니다.
Statistic statistic{this, "example-statistic", "An example statistic"};
};
/// 이 패스를 외부에 노출합니다.
std::unique_ptr<Pass> foo::createMyPass() {
return std::make_unique<MyPass>();
}
/// 이 패스를 등록합니다.
void foo::registerMyPass() {
PassRegistration<MyPass>();
}
이 패스는 다음과 같이 선언적으로 지정할 수 있습니다:
def MyPass : Pass<"my-pass", "ModuleOp"> {
let summary = "My Pass Summary";
let description = [{
여기에서 이제 `MyPass`에 대한 더 긴 설명을 제공할 수 있으며,
각종 제약과 동작을 모두 포함할 수 있습니다.
}];
// 옵션을 지정합니다.
let options = [
Option<"option", "example-option", "bool", /*default=*/"true",
"An example option">,
ListOption<"listOption", "example-list", "int64_t",
"An example list option">
];
// 통계를 지정합니다.
let statistics = [
Statistic<"statistic", "example-statistic", "An example statistic">
];
}
gen-pass-decls 제너레이터를 사용하면 위 보일러플레이트 대부분을 자동으로 생성할 수 있습니다. 이 제너레이터는 입력으로 생성되는 패스 그룹에 대한 태그를 제공하는 -name 파라미터를 받습니다. 이 제너레이터는 여러 목적의 코드를 생성합니다:
첫 번째는 선언된 패스를 전역 레지스트리에 등록하는 것입니다. 각 패스에 대해, 테이블젠에서 지정된 정의 이름 PassName에 대해 registerPassName을 생성합니다. 또한 입력 파라미터 -name으로 제공된 태그 Group에 대해, 존재하는 모든 패스를 등록하는 registerGroupPasses도 생성합니다.
// Tablegen options: -gen-pass-decls -name="Example"
// Passes.h
namespace foo {
#define GEN_PASS_REGISTRATION
#include "Passes.h.inc"
} // namespace foo
void registerMyPasses() {
// 모든 패스를 등록합니다.
foo::registerExamplePasses();
// 또는
// `MyPass`만 구체적으로 등록합니다.
foo::registerMyPass();
}
두 번째는 패스 옵션을 구성하는 방법을 제공하는 것입니다. 이러한 클래스는 MyPassOptions 형태로 명명되며, 여기서 MyPass는 테이블젠의 패스 정의 이름입니다. 구성 가능한 파라미터는 테이블젠 파일에 선언된 옵션을 반영합니다. 이러한 선언은 전체 패스 그룹에 대해 GEN_PASS_DECL 매크로를 정의하여 활성화할 수 있으며, 또는 PASSNAME이 테이블젠에서 지정한 이름의 대문자 버전인 GEN_PASS_DECL_PASSNAME을 정의하여 패스별로 활성화할 수 있습니다.
// .h.inc
#ifdef GEN_PASS_DECL_MYPASS
struct MyPassOptions {
bool option = true;
::llvm::ArrayRef<int64_t> listOption;
};
#undef GEN_PASS_DECL_MYPASS
#endif // GEN_PASS_DECL_MYPASS
자동 생성된 파일에는 기본 생성자의 선언도 포함됩니다.
// .h.inc
#ifdef GEN_PASS_DECL_MYPASS
...
std::unique_ptr<::mlir::Pass> createMyPass();
std::unique_ptr<::mlir::Pass> createMyPass(const MyPassOptions &options);
#undef GEN_PASS_DECL_MYPASS
#endif // GEN_PASS_DECL_MYPASS
이 제너레이터의 마지막 목적은 패스 정의와 관련된 보일러플레이트 대부분을 포함하는 각 패스의 기본 클래스를 내보내는 것입니다. 이러한 클래스는 impl 네임스페이스 안의 MyPassBase 형태로 명명되며, 여기서 MyPass는 테이블젠의 패스 정의 이름입니다. 원래 C++ 패스 정의는 다음과 같이 업데이트할 수 있습니다:
// MyPass.cpp
/// 생성된 기본 패스 클래스 정의를 포함합니다.
namespace foo {
#define GEN_PASS_DEF_MYPASS
#include "Passes.h.inc"
}
/// 실제 클래스는 생성된 기본 클래스로부터 파생합니다.
struct MyPass : foo::impl::MyPassBase<MyPass> {
using MyPassBase::MyPassBase;
/// 옵션 및 통계 정의는 이제 기본 클래스에서 생성되지만,
/// 동일한 방식으로 접근할 수 있습니다.
};
이러한 정의는 테이블젠의 패스 정의 이름의 대문자 버전과 동일한 GEN_PASS_DEF_PASSNAME 전처리기 매크로를 정의하여 패스별로 활성화할 수 있습니다. 기본 생성자도 정의되며, 실제 패스 클래스의 이름이 테이블젠에 정의된 이름과 동일하다고 가정합니다.
gen-pass-doc 제너레이터를 사용하면 각 패스에 대한 마크다운 문서를 생성할 수 있습니다. 실제 MLIR 패스의 예시 출력은 Passes.md를 참조하십시오.
Pass 클래스는 새로운 패스 정의를 시작하는 데 사용됩니다. 이 클래스는 인자로 패스에 부여할 레지스트리 인자와, 패스가 동작하는 연산 타입에 해당하는 선택적 문자열을 받습니다. 이 클래스는 다음 필드를 포함합니다:
summary
description
dependentDialects
Dialect 클래스를 나타내는 문자열 리스트입니다.options
statistics
constructor
옵션은 Option과 ListOption 클래스로 지정할 수 있습니다. Option 클래스는 다음 템플릿 파라미터를 받습니다:
C++ 변수명
argument
type
default value
description
additional option flags
def MyPass : Pass<"my-pass"> {
let options = [
Option<"option", "example-option", "bool", /*default=*/"true",
"An example option">,
];
}
ListOption 클래스는 다음 필드를 받습니다:
C++ 변수명
argument
element type
description
additional option flags
def MyPass : Pass<"my-pass"> {
let options = [
ListOption<"listOption", "example-list", "int64_t",
"An example list option">
];
}
통계는 Statistic으로 지정할 수 있으며, 다음 템플릿 파라미터를 받습니다:
C++ 변수명
display name
description
def MyPass : Pass<"my-pass"> {
let statistics = [
Statistic<"statistic", "example-statistic", "An example statistic">
];
}
MLIR은 PassInstrumentation 클래스를 통해 패스 실행과 분석 계산을 계측(instrument)하기 위한 커스터마이즈 가능한 프레임워크를 제공합니다. 이 클래스는 다양한 이벤트를 관찰하는 패스 매니저의 훅을 제공합니다:
runBeforePipeline
runAfterPipeline
runBeforePass
runAfterPass
runAfterPassFailed는 실행되지 않습니다.runAfterPassFailed
runAfterPass는 실행되지 않습니다.runBeforeAnalysis
runBeforeAnalysis/runAfterAnalysis 쌍은 현재의 runBeforeAnalysis/runAfterAnalysis 쌍 내부에서 호출될 수 있습니다.runAfterAnalysis
PassInstrumentation 인스턴스는 addInstrumentation 메서드를 통해 패스 매니저 인스턴스에 직접 등록할 수 있습니다. 패스 매니저에 추가된 인스트루멘테이션은 스택 방식으로 실행됩니다. 즉, 마지막으로 runBefore* 훅을 실행한 인스트루멘테이션이 해당하는 runAfter* 훅을 가장 먼저 실행합니다. PassInstrumentation 클래스의 훅은 스레드 안전하게 실행됨이 보장되므로 추가 동기화는 필요하지 않습니다. 아래는 DominanceInfo 분석이 계산된 횟수를 세는 예시 인스트루멘테이션입니다:
struct DominanceCounterInstrumentation : public PassInstrumentation {
/// dominance가 계산된 누적 횟수입니다.
unsigned &count;
DominanceCounterInstrumentation(unsigned &count) : count(count) {}
void runAfterAnalysis(llvm::StringRef, TypeID id, Operation *) override {
if (id == TypeID::get<DominanceInfo>())
++count;
}
};
MLIRContext *ctx = ...;
PassManager pm(ctx);
// 패스 매니저에 인스트루멘테이션을 추가합니다.
unsigned domInfoCount;
pm.addInstrumentation(
std::make_unique<DominanceCounterInstrumentation>(domInfoCount));
// 모듈 연산에 대해 패스 매니저를 실행합니다.
ModuleOp m = ...;
if (failed(pm.run(m)))
...
llvm::errs() << "DominanceInfo was computed " << domInfoCount << " times!\n";
MLIR은 패스 인스트루멘테이션 프레임워크를 활용해 몇 가지 유용한 개발자 도구와 유틸리티를 제공합니다. 각 인스트루멘테이션은 MLIR 패스 프레임워크의 모든 사용자에게 직접 제공됩니다.
PassTiming 인스트루멘테이션은 패스 실행과 분석 계산에 대한 타이밍 정보를 제공합니다. 이는 어떤 패스가 실행하는 데 가장 많은 시간을 소비하는지, 그리고 파이프라인 전체 실행 시간에 패스가 어느 정도 영향을 미치는지 빠르게 파악할 수 있게 합니다. 사용자는 enableTiming을 통해 패스 매니저에서 이 인스트루멘테이션을 직접 활성화할 수 있습니다. 이 인스트루멘테이션은 mlir-opt에서도 -mlir-timing 플래그로 제공됩니다. PassTiming 인스트루멘테이션은 타이밍 결과에 대해 여러 표시 모드를 제공하며, 각 모드는 아래에 설명되어 있습니다:
이 모드에서는 결과가 총 시간 기준으로 정렬된 리스트로 표시되며, 각 패스/분석 인스턴스는 하나의 고유 결과로 집계됩니다. 이 보기는 파이프라인에서 어떤 분석/패스가 가장 많은 시간을 소비하는지 개략적으로 파악하는 데 유용합니다. 이 표시 모드는 mlir-opt에서 -mlir-timing-display=list로 사용할 수 있습니다.
$ mlir-opt foo.mlir -mlir-disable-threading -pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-func-to-llvm)' -mlir-timing -mlir-timing-display=list
===-------------------------------------------------------------------------===
... Execution time report ...
===-------------------------------------------------------------------------===
Total Execution Time: 0.0135 seconds
----Wall Time---- ----Name----
0.0135 (100.0%) root
0.0041 ( 30.1%) Parser
0.0018 ( 13.3%) ConvertFuncToLLVMPass
0.0011 ( 8.2%) Output
0.0007 ( 5.2%) Pipeline Collection : ['func.func']
0.0006 ( 4.6%) 'func.func' Pipeline
0.0005 ( 3.5%) Canonicalizer
0.0001 ( 0.9%) CSE
0.0001 ( 0.5%) (A) DataLayoutAnalysis
0.0000 ( 0.1%) (A) DominanceInfo
0.0058 ( 43.2%) Rest
0.0135 (100.0%) Total
JSON 형식으로도 -mlir-output-format=json을 통해 표시할 수 있습니다.
$ mlir-opt foo.mlir -mlir-disable-threading -pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-func-to-llvm)' -mlir-timing -mlir-timing-display=list -mlir-output-format=json
[
{"wall": {"duration": 0.0135, "percentage": 100.0}, "name": "root"},
{"wall": {"duration": 0.0041, "percentage": 30.1}, "name": "Parser"},
{"wall": {"duration": 0.0018, "percentage": 13.3}, "name": "ConvertFuncToLLVMPass"},
{"wall": {"duration": 0.0011, "percentage": 8.2}, "name": "Output"},
{"wall": {"duration": 0.0007, "percentage": 5.2}, "name": "Pipeline Collection : ['func.func']"},
{"wall": {"duration": 0.0006, "percentage": 4.6}, "name": "'func.func' Pipeline"},
{"wall": {"duration": 0.0005, "percentage": 3.5}, "name": "Canonicalizer"},
{"wall": {"duration": 0.0001, "percentage": 0.9}, "name": "CSE"},
{"wall": {"duration": 0.0001, "percentage": 0.5}, "name": "(A) DataLayoutAnalysis"},
{"wall": {"duration": 0.0000, "percentage": 0.1}, "name": "(A) DominanceInfo"},
{"wall": {"duration": 0.0058, "percentage": 43.2}, "name": "Rest"},
{"wall": {"duration": 0.0135, "percentage": 100.0}, "name": "Total"}
]
이 모드에서는 결과가 패스 매니저에서 실행되는 내부 패스 파이프라인을 반영하는 중첩 파이프라인 보기로 표시됩니다. 이 보기는 파이프라인의 어떤 부분이 가장 많은 시간을 소모하는지 이해하는 데 유용하며, 분석이 언제 무효화되어 재계산되는지도 파악할 수 있습니다. 기본 표시 모드입니다.
$ mlir-opt foo.mlir -mlir-disable-threading -pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-func-to-llvm)' -mlir-timing
===-------------------------------------------------------------------------===
... Execution time report ...
===-------------------------------------------------------------------------===
Total Execution Time: 0.0127 seconds
----Wall Time---- ----Name----
0.0038 ( 30.2%) Parser
0.0006 ( 4.8%) 'func.func' Pipeline
0.0001 ( 0.9%) CSE
0.0000 ( 0.1%) (A) DominanceInfo
0.0005 ( 3.7%) Canonicalizer
0.0017 ( 13.7%) ConvertFuncToLLVMPass
0.0001 ( 0.6%) (A) DataLayoutAnalysis
0.0010 ( 8.2%) Output
0.0054 ( 42.5%) Rest
0.0127 (100.0%) Total
JSON 형식으로도 -mlir-output-format=json을 통해 표시할 수 있습니다.
$ mlir-opt foo.mlir -mlir-disable-threading -pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-func-to-llvm)' -mlir-timing -mlir-output-format=json
[
{"wall": {"duration": 0.0038, "percentage": 30.2}, "name": "Parser", "passes": [
{}]},
{"wall": {"duration": 0.0006, "percentage": 4.8}, "name": "'func.func' Pipeline", "passes": [
{"wall": {"duration": 0.0001, "percentage": 0.9}, "name": "CSE", "passes": [
{"wall": {"duration": 0.0000, "percentage": 0.1}, "name": "(A) DominanceInfo", "passes": [
{}]},
{}]},
{"wall": {"duration": 0.0005, "percentage": 3.7}, "name": "Canonicalizer", "passes": [
{}]},
{}]},
{"wall": {"duration": 0.0017, "percentage": 13.7}, "name": "ConvertFuncToLLVMPass", "passes": [
{"wall": {"duration": 0.0001, "percentage": 0.6}, "name": "(A) DataLayoutAnalysis", "passes": [
{}]},
{}]},
{"wall": {"duration": 0.0010, "percentage": 8.2}, "name": "Output", "passes": [
{}]},
{"wall": {"duration": 0.0054, "percentage": 42.5}, "name": "Rest"},
{"wall": {"duration": 0.0127, "percentage": 100.0}, "name": "Total"}
]
패스 매니저에서 멀티스레딩이 활성화되면 표시 의미가 약간 바뀝니다. 먼저 User Time이라는 새로운 타이밍 열이 추가되며, 모든 스레드에서 소비된 총 시간을 보여줍니다. 둘째로 Wall Time 열은 모든 스레드 중 가장 긴 개별 시간을 표시합니다. 이는 Wall Time 열이 인지된 시간(벽시계 시간)을 계속 나타내는 반면, User Time은 총 CPU 시간을 표시함을 의미합니다.
$ mlir-opt foo.mlir -pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-func-to-llvm)' -mlir-timing
===-------------------------------------------------------------------------===
... Pass execution timing report ...
===-------------------------------------------------------------------------===
Total Execution Time: 0.0078 seconds
---User Time--- ---Wall Time--- --- Name ---
0.0177 ( 88.5%) 0.0057 ( 71.3%) 'func.func' Pipeline
0.0044 ( 22.0%) 0.0015 ( 18.9%) CSE
0.0029 ( 14.5%) 0.0012 ( 15.2%) (A) DominanceInfo
0.0038 ( 18.9%) 0.0015 ( 18.7%) VerifierPass
0.0089 ( 44.6%) 0.0025 ( 31.1%) Canonicalizer
0.0006 ( 3.0%) 0.0002 ( 2.6%) VerifierPass
0.0004 ( 2.2%) 0.0004 ( 5.4%) VerifierPass
0.0013 ( 6.5%) 0.0013 ( 16.3%) LLVMLoweringPass
0.0006 ( 2.8%) 0.0006 ( 7.0%) VerifierPass
0.0200 (100.0%) 0.0081 (100.0%) Total
디버깅할 때는 패스 파이프라인의 여러 단계에서 IR을 덤프하는 것이 종종 유용합니다. IR 출력 인스트루멘테이션은 이를 위해 존재합니다. 이 인스트루멘테이션을 사용하면, 실행 중인 패스에 대해 선택적으로 필터링하여 패스 실행 전후에 IR을 조건부로 출력할 수 있습니다. 이 인스트루멘테이션은 enableIRPrinting 메서드를 통해 패스 매니저에 추가할 수 있습니다. mlir-opt는 이 인스트루멘테이션을 활용하기 위한 몇 가지 유용한 플래그를 제공합니다:
mlir-print-ir-before=(comma-separated-pass-list)
mlir-print-ir-before-all
$ mlir-opt foo.mlir -pass-pipeline='func.func(cse)' -mlir-print-ir-before=cse
*** IR Dump Before CSE ***
func.func @simple_constant() -> (i32, i32) {
%c1_i32 = arith.constant 1 : i32
%c1_i32_0 = arith.constant 1 : i32
return %c1_i32, %c1_i32_0 : i32, i32
}
mlir-print-ir-after=(comma-separated-pass-list)
mlir-print-ir-after-all
$ mlir-opt foo.mlir -pass-pipeline='func.func(cse)' -mlir-print-ir-after=cse
*** IR Dump After CSE ***
func.func @simple_constant() -> (i32, i32) {
%c1_i32 = arith.constant 1 : i32
return %c1_i32, %c1_i32 : i32, i32
}
mlir-print-ir-after-change
$ mlir-opt foo.mlir -pass-pipeline='func.func(cse,cse)' -mlir-print-ir-after=cse -mlir-print-ir-after-change
*** IR Dump After CSE ***
func.func @simple_constant() -> (i32, i32) {
%c1_i32 = arith.constant 1 : i32
return %c1_i32, %c1_i32 : i32, i32
}
mlir-print-ir-after-failure
mlir-print-ir-after 플래그와 함께 사용해서는 안 됩니다.$ mlir-opt foo.mlir -pass-pipeline='func.func(cse,bad-pass)' -mlir-print-ir-after-failure
*** IR Dump After BadPass Failed ***
func.func @simple_constant() -> (i32, i32) {
%c1_i32 = arith.constant 1 : i32
return %c1_i32, %c1_i32 : i32, i32
}
mlir-print-ir-module-scope
-mlir-disable-threading).$ mlir-opt foo.mlir -mlir-disable-threading -pass-pipeline='func.func(cse)' -mlir-print-ir-after=cse -mlir-print-ir-module-scope
*** IR Dump After CSE *** ('func.func' operation: @bar)
func.func @bar(%arg0: f32, %arg1: f32) -> f32 {
...
}
func.func @simple_constant() -> (i32, i32) {
%c1_i32 = arith.constant 1 : i32
%c1_i32_0 = arith.constant 1 : i32
return %c1_i32, %c1_i32_0 : i32, i32
}
*** IR Dump After CSE *** ('func.func' operation: @simple_constant)
func.func @bar(%arg0: f32, %arg1: f32) -> f32 {
...
}
func.func @simple_constant() -> (i32, i32) {
%c1_i32 = arith.constant 1 : i32
return %c1_i32, %c1_i32 : i32, i32
}
mlir-print-ir-tree-dir=(directory path)
stderr에 출력됩니다. 이 옵션으로 디렉터리를 제공하면, 디렉터리 트리(루트는 (directory path))에 각 패스에 해당하는 출력이 파일로 기록됩니다. 각 패스에 대해 생성되는 경로는 IR과 패스 파이프라인의 중첩 구조를 반영합니다.builtin.module op 안에 두 개의 func.func가 있는 IR에 대해 패스 파이프라인을 실행해 생성된 파일 트리를 보여줍니다.1_1_pass4.mlir 파일의 경우, 첫 번째 1은 부모 op의 카운터, 두 번째 1은 해당 함수의 카운터를 뜻합니다.$ pipeline="builtin.module(pass1,pass2,func.func(pass3,pass4),pass5)"
$ mlir-opt foo.mlir -pass-pipeline="$pipeline" -mlir-print-ir-tree-dir=/tmp/pipeline_output
$ tree /tmp/pipeline_output
/tmp/pass_output
├── builtin_module_the_symbol_name
│ ├── 0_pass1.mlir
│ ├── 1_pass2.mlir
│ ├── 2_pass5.mlir
│ ├── func_func_my_func_name
│ │ ├── 1_0_pass3.mlir
│ │ ├── 1_1_pass4.mlir
│ ├── func_func_my_other_func_name
│ │ ├── 1_0_pass3.mlir
│ │ ├── 1_1_pass4.mlir
mlir-use-nameloc-as-prefix
loc("named_location"))가 있다면 이 플래그를 사용하면 해당 이름(named_location)을 대응하는 SSA 식별자의 접두사로 사용합니다:%1 = memref.load %0[] : memref<i32> loc("alice")
%2 = memref.load %0[] : memref<i32> loc("bob")
%3 = memref.load %0[] : memref<i32> loc("bob")
이 경우 출력은 다음과 같습니다
%alice = memref.load %0[] : memref<i32>
%bob = memref.load %0[] : memref<i32>
%bob_0 = memref.load %0[] : memref<i32>
적절한 위치를 사용하면 이러한 이름은 새로 생성된 연산에도 패스를 거치며 보존됩니다.
MLIR의 패스 매니저는 크래시 또는 패스 실패 발생 시 재현물을 생성하는 내장 메커니즘을 포함합니다. 이 기능은 PassManager::enableCrashReproducerGeneration 또는 커맨드라인 플래그 mlir-pass-pipeline-crash-reproducer로 활성화할 수 있습니다. 두 경우 모두 재현물을 기록할 출력 .mlir 파일 이름에 해당하는 인자를 제공합니다. 재현물에는 실행 중이던 패스 매니저의 구성과 어떤 패스도 실행되기 전의 초기 IR이 포함됩니다. 리프로듀서는 어셈블리 포맷 내 외부 리소스로 저장됩니다. 가능한 재현물은 다음과 같을 수 있습니다:
module {
func.func @foo() {
...
}
}
{-#
external_resources: {
mlir_reproducer: {
pipeline: "builtin.module(func.func(cse,canonicalize),inline)",
disable_threading: true,
verify_each: true
}
}
#-}
덤프된 구성은 -run-reproducer 플래그를 지정하여 mlir-opt에 전달할 수 있습니다. 그러면 리프로듀서의 구성을 파싱하여 패스 매니저, 컨텍스트 등 필요한 opt 상태를 조정합니다.
파일 이름을 지정하는 것 외에도, 크래시 시 호출되어 리프로듀서를 해당 스트림에 기록할 ReproducerStreamFactory 함수를 등록할 수도 있습니다.
PassManager::enableCrashReproducerGeneration에 추가 플래그를 전달하거나, 커맨드라인에서 mlir-pass-pipeline-local-reproducer를 지정하여, 패스 매니저가 “로컬” 리프로듀서를 생성하도록 지시할 수 있습니다. 이는 실패한 패스 바로 직전의 IR을 포함하는 리프로듀서를 생성하려고 시도합니다. 이는 크래시가 특정 패스 내에 있다고 알려진 경우나, 원래 입력이 항상 사용 가능하지 않을 수 있는 구성 요소(다이얼렉트나 패스 등)에 의존하는 경우에 유용합니다.
참고: 로컬 리프로듀서 생성을 위해서는 멀티스레딩이 비활성화되어 있어야 합니다(-mlir-disable-threading).
예를 들어, 이전 예시에서 실패가 canonicalize 패스에서 발생했다면, 다음 리프로듀서가 생성됩니다:
module {
func.func @foo() {
...
}
}
{-#
external_resources: {
mlir_reproducer: {
pipeline: "builtin.module(func.func(canonicalize))",
disable_threading: true,
verify_each: true
}
}
#-}