Transform 다이어렉트에서 타입 제약, ApplyEach 트레이트, 사용자 정의 Transform 타입 및 메모리 이펙트 트레이트를 사용해 개별 페이로드 연산에 대한 변환을 일반화하고 재사용하는 방법을 설명한다.
각 페이로드 연산에 개별적으로 적용되며, 해당 연산이 특정 종류일 것을 요구하는 Transform 연산은 반복되는 패턴이다. Transform 다이어렉트 타입을 사용해 이러한 타입 전제 조건(precondition)을 명시할 수 있다. 구체적으로, 기존에 넓은 의미의 TransformHandleTypeInterface 였던 피연산자 타입을 더 좁은 Transform_ConcreteOp<"func.call"> 로 변경할 수 있다. 추가로, TransformEachOpTrait 트레이트를 사용하여 검증, 페이로드 반복, 결과 연결을 수행하는 apply 메서드의 골격 구현(skeleton implementation)을 제공할 수 있다. 개선된 ODS 연산 정의는 다음과 같다.
tablegen// In MyExtension.td. // 새로운 연산을 정의한다. 관례상, 이름 앞에 다이어렉트 익스텐션 이름인 // "my."를 붙인다. 전체 연산 이름은 여기에 다시 "transform."이 접두로 // 붙는다. def ChangeCallTargetOp : Op<Transform_Dialect, "my.change_call_target", // 이 연산이 필수 인터페이스인 TransformOpInterface 와 // MemoryEffectsOpInterface 를 구현함을 나타낸다. TransformEach 트레이트를 // 사용해 TransformOpInterface 의 구현을 제공한다. [TransformOpInterface, TransformEachOpTrait, DeclareOpInterfaceMethods<MemoryEffectsOpInterface>]> { // 간단 요약과 전체 설명을 제공한다. 전체 설명에는 피연산자에 대한 영향과 // 이 연산이 다양한 실패 모드를 어떻게 처리하는지도 기술하는 것이 좋다. let summary = "Changes the callee of a call operation to the specified one"; let description = [{ 핸들과 연관된 각 `func.call` 페이로드 연산에 대해, 이 연산에 속성으로 전달된 이름을 가진 심볼을 새로운 callee 로 설정한다. 피연산자가 `func.call` 이 아닌 페이로드 연산과 연관되어 있으면 silenceable failure 를 생성한다. 피연산자를 읽기만 한다. }]; // 인자는 페이로드 연산에 대한 핸들과 새 callee 를 지정하는 속성을 포함한다. // 핸들은 TransformHandleTypeInterface 를 구현해야 한다. 심볼은 transform IR // 에 존재하지 않을 수도 있으므로 검증이 실패할 수 있어 문자열 속성을 // 사용한다. let arguments = (ins Transform_ConcreteOpType<"func.call">:$call, StrAttr:$new_target); // 이 변환은 새로운 페이로드를 생성하지 않으므로 결과는 비어 있다. let results = (outs); // 보기 좋은 어셈블리 구문을 제공한다. let assemblyFormat = "$call `,` $new_target attr-dict `:` type($call)"; // 개별 페이로드 연산에 대한 인터페이스 구현 함수를 선언한다. let extraClassDeclaration = [{ ::mlir::DiagnosedSilenceableFailure applyToOne( ::mlir::transform::TransformRewriter &rewriter, ::mlir::func::CallOp call, ::mlir::transform::ApplyToEachResultList &results, ::mlir::transform::TransformState &state); }]; }
이제 루프를 포함한 apply 메서드를 정의하는 대신, 개별 페이로드 연산에 적용되는 함수를 하나만 정의하면 되고, 나머지는 트레이트가 처리해 준다.
c::mlir::DiagnosedSilenceableFailure ChangeCallTargetOp::applyToOne( ::mlir::transform::TransformRewriter &rewriter, ::mlir::func::CallOp call, ::mlir::transform::ApplyToEachResultList &results, ::mlir::transform::TransformState &state) { // 실제 변환 함수를 호출한다. updateCallee(call, getNewTarget()); // 성공을 나타낸다. return DiagnosedSilenceableFailure::success(); }
연산 외에도, Transform 다이어렉트는 익스텐션이 추가 속성과 타입을 정의하고 주입(inject)할 수 있도록 허용한다. 위에서 보았듯이, transform 타입은 페이로드 연산에 대한 제약을 명시하는 데 사용된다. 현재 우리의 호출 재작성 연산은 func.call 에만 적용된다. 이를 일반화하여 CallOpInterface 를 구현하는 모든 페이로드 연산에 적용하고자 할 수 있지만, Transform 다이어렉트에는 아직 해당 인터페이스를 구현하는 페이로드 연산인지 확인하는 타입이 없다. 이를 익스텐션 안에 정의해 보자.
타입 정의는 다시 ODS 로 다이어렉트 타입을 정의하는 것과 동일하다.
tablegen// Transform 다이어렉트는 추가 타입이 정의되어 주입되는 것을 허용한다. def CallOpInterfaceHandle : TypeDef<Transform_Dialect, "CallOpInterfaceHandle", // 이 타입은 `TransformHandleTypeInterface` 를 구현해야 한다. [DeclareTypeInterfaceMethods<TransformHandleTypeInterface>]> { // 타입의 일반적인 구성 요소(설명, mnemonic, 어셈블리 포맷 등)를 // 제공해야 한다. let summary = "handle to payload operations implementing CallOpInterface"; let mnemonic = "my.call_op_interface"; let assemblyFormat = ""; }
Tablegen 을 사용한 선언 및 정의 생성 과정은 일반적인 경우와 동일하므로 여기서는 생략한다.
Transform 타입 정의를 마무리하려면, 인터페이스 메서드를 구현해야 한다.
c// In MyExtension.cpp. // 인터페이스는 이 타입이 페이로드 연산에 대해 갖는 제약을 검증하기 위한 // 이 메서드를 선언한다. 반환 값은 이미 익숙한 3값(tri-state) 결과다. mlir::DiagnosedSilenceableFailure mlir::transform::CallOpInterfaceHandleType::checkPayload( // 진단 메시지를 출력할 위치. mlir::Location loc, // 이 타입을 가진 핸들과 곧 연관될(associate) 페이로드 연산 목록. llvm::ArrayRef<mlir::Operation *> payload) const { // 모든 페이로드 연산이 CallOpInterface 를 구현해야 하므로 이를 확인한다. for (Operation *op : payload) { if (llvm::isa<mlir::CallOpInterface>(op)) continue; // 관례상, 이러한 검증기는 전제 조건을 검사하므로 항상 silenceable // failure 를 발생시킨다. DiagnosedSilenceableFailure diag = emitSilenceableError(loc) << "expected the payload operation to implement CallOpInterface"; diag.attachNote(op->getLoc()) << "offending operation"; return diag; } // 모든 것이 문제 없으면 성공을 반환한다. return DiagnosedSilenceableFailure::success(); }
추가 속성과 타입은 연산 옆에서 익스텐션에 등록해야 한다.
c// In MyExtension.cpp. void MyExtension::init() { // ... registerTypes< #define GET_TYPEDEF_LIST #include "MyExtensionTypes.cpp.inc" >(); }
이 타입은 이제 Transform 다이어렉트 안에서 직접 사용 가능하며, 연산에서 사용할 수 있다. 이전 tablegen 정의에서는 $call 의 타입이 Transform_ConcreteOp<"func.call"> 여야 했다. $call 에 허용되는 타입으로 CallOpInterfaceHandle 을 추가하면, 해당 핸들은 이제 인터페이스를 구현하는 어떤 연산이라도 가리킬 수 있게 된다.
tablegendef ChangeCallTargetOp : ... { let arguments = (ins // 핸들이 구체적인 `func.call` 연산뿐 아니라 // `CallOpInterface` 를 구현하는 모든 연산을 가리킬 수 있도록 허용한다. AnyTypeOf<[Transform_ConcreteOpType<"func.call">, CallOpInterfaceHandle]>:$call, StrAttr:$new_target); }
이제 sequence.mlir 에 다음 코드를 추가하고 인터프리터로 실행할 수 있다.
mlir// 새 타입으로 캐스트한다. %casted = transform.cast %call : !transform.any_op to !transform.my.call_op_interface // 새 연산을 사용한다. transform.my.change_call_target %casted, "microkernel" : !transform.my.call_op_interface
연습 삼아, 재작성 연산이 피연산자를 소비하도록 수정해 보자. 예를 들어, 이 변환이 func.call 연산을 사용자 정의 연산 my.mm4 로 재작성하는 경우가 이에 해당한다. 이제 피연산자 핸들이 소비되므로, 이 연산은 새로 생성된 페이로드 연산에 대한 새 핸들을 결과로 반환할 수 있다. 그 외에는 transform 연산의 ODS 정의는 변경되지 않는다.
tablegen// In MyExtension.td. // 또 다른 transform 연산을 정의한다. def CallToOp : Op<Transform_Dialect, "my.call_to_op", // 이 연산이 필수 인터페이스인 TransformOpInterface 와 // MemoryEffectsOpInterface 를 구현함을 나타낸다. TransformEach 트레이트를 // 사용해 TransformOpInterface 의 구현을 제공한다. [TransformOpInterface, TransformEachOpTrait, DeclareOpInterfaceMethods<MemoryEffectsOpInterface>]> { // 요약과 설명은 간단히 하기 위해 생략한다. // 인자는 페이로드 연산에 대한 핸들이다. let arguments = (ins CallOpInterfaceHandle:$call); // 결과는 변환 과정에서 생성된 페이로드 연산에 대한 핸들이다. let results = (outs TransformHandleTypeInterface:$transformed); // 보기 좋은 어셈블리 구문을 제공한다. let assemblyFormat = "$call attr-dict `:` functional-type(operands, results)"; // 개별 페이로드 연산에 대한 인터페이스 구현 함수를 선언한다. let extraClassDeclaration = [{ ::mlir::DiagnosedSilenceableFailure applyToOne( ::mlir::transform::TransformRewriter &rewriter, ::mlir::CallOpInterface call, ::mlir::transform::ApplyToEachResultList &results, ::mlir::transform::TransformState &state); }]; }
이제 인터페이스 메서드 구현을 살펴보자.
c// In MyExtension.cpp. ::mlir::DiagnosedSilenceableFailure CallToOp::applyToOne( ::mlir::transform::TransformRewriter &rewriter, ::mlir::CallOpInterface call, ::mlir::transform::ApplyToEachResultList &results, ::mlir::transform::TransformState &state) { // 실제 재작성을 호출한다. Operation *rewritten = rewriteToOp(call); // 재작성기가 null 포인터를 반환하면 오류를 보고한다. 이 시점에서 페이로드 // IR 이 되돌릴 수 없게 수정되었을 수도 있으므로, definite failure 를 // 생성한다. if (!rewritten) { return emitDefiniteError() << "failed to rewrite call to operation"; } // 성공 시, 결과 연산을 결과 리스트에 추가한다. 리스트에는 결과 개수와 // 적용 횟수당 정확히 하나의 엔티티가 들어 있어야 한다. 이 핸들은 각각의 // 적용에서 생성된 값들의 리스트와 연관된다. results.push_back(rewritten); // 모든 것이 문제 없으면 성공을 반환한다. return DiagnosedSilenceableFailure::success(); } void CallToOp::getEffects( ::llvm::SmallVectorImpl<::mlir::MemoryEffects::EffectInstance> &effects) { // 피연산자 핸들이 소비되고, 결과 핸들이 생성됨을 사이드 이펙트로 나타낸다. consumesHandle(getCall(), effects); producesHandle(getTransformed(), effects); // 페이로드 IR 이 수정됨을 나타낸다. modifiesPayload(effects); }
전체적인 흐름은 이전 구현과 유사하다. 적용 시, 연산은 자신이 생성하는 핸들과 연관될 결과 엔티티도 지정해야 한다. 연산은 성공 시 모든 결과에 대해 연관할 엔티티를 지정해야 하는데, 리스트가 비어 있더라도 마찬가지다. 이 조건을 만족하지 않으면 assertion 이 발생한다. 실패한 경우, 인터프리터는 아직 정의되지 않은 모든 결과를 자동으로 빈 리스트와 연관시킨다.
또한 applyToOne 은 각 적용에서 각 결과 핸들에 대해 정확히 하나의 페이로드 엔티티가 연관되는 것을 항상 기대하므로, 피연산자 핸들이 비어 있지 않을 때 빈 리스트와 연관된 핸들을 반환하는 데에는 사용할 수 없다. 이러한 경우에는 apply 를 직접 사용해야 한다.
c::mlir::DiagnosedSilenceableFailure SomeOtherOp::apply( ::mlir::transform::TransformRewriter &rewriter, ::mlir::transform::TransformResults &results, ::mlir::transform::TransformState &state) { // ... // 결과 `transformed` 를 빈 페이로드 연산 리스트와 연관한다. results.set(cast<OpResult>(getTransformed()), {}); return DiagnosedSilenceableFailure::success(); }
일반적인 메모리 이펙트 패턴 일부는 트레이트로도 제공되어 보일러플레이트를 줄여 준다.
FunctionalStyleTransformOpTrait 는 모든 핸들-타입 피연산자가 소비되고, 모든 결과가 생성되며, 페이로드 IR 이 수정됨을 나타낸다.NavigationTransformOpTrait 는 모든 핸들-타입 피연산자가 읽기 전용이며, 모든 결과가 생성되고, 페이로드 IR 이 읽기 전용임을 나타낸다.이러한 트레이트를 사용하면 MemoryEffectsOpInterface 의 메서드를 선언하거나 정의할 필요가 없다.
tablegen// In MyExtension.td. // 또 다른 transform 연산을 정의한다. def CallToOp : Op<Transform_Dialect, "my.call_to_op", // 이 연산이 필수 인터페이스인 TransformOpInterface 를 구현함을 나타낸다. // TransformEach 트레이트를 사용해 이 인터페이스의 구현을 제공한다. [TransformOpInterface, TransformEachOpTrait, // 이 연산이 필수 인터페이스인 MemoryEffectsOpInterface 를 구현함을 // 나타낸다. FunctionalStyle 트레이트를 사용해 이 인터페이스의 구현을 // 제공한다. MemoryEffectsOpInterface, FunctionalStyleTransformOpTrait]> { // 요약과 설명은 간단히 하기 위해 생략한다. // 인자는 페이로드 연산에 대한 핸들이다. let arguments = (ins CallOpInterfaceHandle:$call); // 결과는 변환 과정에서 생성된 페이로드 연산에 대한 핸들이다. let results = (outs TransformHandleTypeInterface:$transformed); // 보기 좋은 어셈블리 구문을 제공한다. let assemblyFormat = "$call attr-dict `:` functional-type(operands, results)"; // 개별 페이로드 연산에 대한 인터페이스 구현 함수를 선언한다. let extraClassDeclaration = [{ ::mlir::DiagnosedSilenceableFailure applyToOne( ::mlir::transform::TransformRewriter &rewriter, ::mlir::CallOpInterface call, ::mlir::transform::ApplyToEachResultList &results, ::mlir::transform::TransformState &state); }]; }
transform.my.call_to_op (transform::CallToOp)구문:
mliroperation ::= `transform.my.call_to_op` $call attr-dict `:` functional-type(operands, results)
Traits: FunctionalStyleTransformOpTrait, TransformEachOpTrait
Interfaces: MemoryEffectsOpInterface, TransformOpInterface
| Operand | 설명 |
|---|---|
call | CallOpInterface 를 구현하는 페이로드 연산에 대한 핸들 |
| Result | 설명 |
|---|---|
transformed | TransformHandleTypeInterface 인스턴스 |
transform.my.change_call_target (transform::ChangeCallTargetOp)호출 연산의 callee 를 지정된 대상으로 변경한다
구문:
mliroperation ::= `transform.my.change_call_target` $call `,` $new_target attr-dict `:` qualified(type($call))
핸들과 연관된 각 func.call 페이로드 연산에 대해, 이 연산에 속성으로 제공된 이름을 가진 심볼을 callee 로 변경한다.
피연산자가 func.call 이 아닌 페이로드 연산과 연관된 경우 silenceable failure 를 생성한다. 피연산자는 읽기만 한다.
Traits: TransformEachOpTrait
Interfaces: MemoryEffectOpInterface, TransformOpInterface
| Attribute | MLIR 타입 | 설명 |
|---|---|---|
new_target | ::mlir::StringAttr | 문자열 속성 |
| Operand | 설명 |
|---|---|
call | func.call 연산에 대한 Transform IR 핸들이거나, CallOpInterface 를 구현하는 페이로드 연산에 대한 핸들 |