MLIR에서 인터페이스는 변환과 분석을 특정 연산이나 다이얼렉트에 결합하지 않고 일반화해 표현하도록 하는 메커니즘이다. 이 문서는 다이얼렉트 인터페이스, 속성/연산/타입 인터페이스, 외부 모델과 Fallback, ODS를 활용한 선언적 정의 및 생성, 그리고 표준 연산 인터페이스들을 설명한다.
MLIR는 다양한 다이얼렉트가 고유한 속성(attribute), 연산(operation), 타입(type) 등을 갖고 공존할 수 있게 해 주는 범용적이고 확장 가능한 프레임워크입니다. MLIR 다이얼렉트는 매우 다양한 의미론과 추상화 수준의 연산을 표현할 수 있습니다. 단점은, MLIR 변환(transformations)과 분석(analyses)이 모든 연산의 의미론을 고려하거나, 그렇지 않으면 지나치게 보수적으로 동작해야 한다는 점입니다. 주의를 기울이지 않으면, 지원하는 각 연산 타입마다 특수 처리를 하는 코드가 양산될 수 있습니다. 이를 해결하기 위해, MLIR은 ‘인터페이스(interfaces)’라는 개념을 제공합니다.
인터페이스는 IR과 상호작용하는 일반적인 방식을 제공합니다. 목표는 특정 연산이나 다이얼렉트에 대한 구체적인 지식을 인코딩하지 않고도, 이 인터페이스들을 기준으로 변환·분석을 표현할 수 있게 하는 것입니다. 이렇게 하면 변환/분석 구현과 분리된 상태로 새로운 다이얼렉트와 연산을 추가할 수 있어 컴파일러의 확장성이 높아집니다.
다이얼렉트 인터페이스는 서로 다른 다이얼렉트에 정의된 것일 수 있는 속성/연산/타입의 집합을 일반적으로 다루고자 하는 변환 패스나 분석에 유용합니다. 이러한 인터페이스는 대체로 다이얼렉트 전체에 폭넓게 적용되며, 소수의 분석이나 변환에서만 사용됩니다. 이런 경우 인터페이스를 각 연산에 직접 등록하는 것은 과도하게 복잡하고 번거롭습니다. 인터페이스는 연산 자체의 핵심 요소가 아니라, 특정 변환에만 필요한 요소이기 때문입니다. 이런 유형의 인터페이스가 쓰이는 예로는 인라이닝(inlining)이 있습니다. 인라이닝은 일반적으로 한 다이얼렉트 내 연산들에 대한 고수준 정보(예: 비용 모델링, 합법성)를 조회하는데, 이는 종종 단일 연산에만 특화된 정보가 아닙니다.
다이얼렉트 인터페이스는 CRTP 기반 클래스 DialectInterfaceBase::Base<>를 상속하여 정의할 수 있습니다. 이 클래스는 나중에 참조할 수 있도록 다이얼렉트에 인터페이스를 등록하는 데 필요한 유틸리티를 제공합니다. 인터페이스가 정의되면, 각 다이얼렉트는 다이얼렉트 특화 정보를 사용하여 이를 재정의할 수 있습니다. 다이얼렉트가 정의한 인터페이스는 속성, 연산, 타입 등과 유사한 메커니즘인 addInterfaces<>로 등록됩니다.
/// 다이얼렉트가 인라이너에 옵트인(opt-in)할 수 있도록
/// 기본 인라이닝 인터페이스 클래스를 정의합니다.
class DialectInlinerInterface :
public DialectInterface::Base<DialectInlinerInterface> {
public:
/// 현재 다이얼렉트에 등록된 어떤 연산에 연결된 영역 'dest'로
/// 영역 'src'를 인라인할 수 있으면 true를 반환합니다.
/// 'valueMapping'에는 'src' 영역 내 값의 재매핑 정보가 들어있습니다.
/// 예를 들어, 이것을 사용하면 'src' 영역의 엔트리 인자들이
/// 어떤 값으로 대체될지 확인할 수 있습니다.
virtual bool isLegalToInline(Region *dest, Region *src,
IRMapping &valueMapping) const {
return false;
}
};
/// AffineDialect에 대한 인라이너 인터페이스를 재정의하여
/// affine 연산의 인라이닝을 가능하게 합니다.
struct AffineInlinerInterface : public DialectInlinerInterface {
/// Affine 구조에는 특정 인라이닝 제약이 있습니다.
bool isLegalToInline(Region *dest, Region *src,
IRMapping &valueMapping) const final {
...
}
};
/// 다이얼렉트에 인터페이스를 등록합니다.
AffineDialect::AffineDialect(MLIRContext *context) ... {
addInterfaces<AffineInlinerInterface>();
}
등록이 끝나면, 분석이나 변환은 특정 다이얼렉트의 서브클래스를 알 필요 없이 다이얼렉트에서 이 인터페이스를 조회할 수 있습니다:
Dialect *dialect = ...;
if (DialectInlinerInterface *interface = dyn_cast<DialectInlinerInterface>(dialect)) {
// 이 다이얼렉트는 해당 인터페이스의 구현을 제공합니다.
...
}
추가 유틸리티로 DialectInterfaceCollection이 제공됩니다. 이 클래스는 MLIRContext 인스턴스 내에서 특정 인터페이스를 등록한 모든 다이얼렉트를 수집할 수 있게 해 줍니다. 이는 등록된 다이얼렉트 인터페이스의 조회를 감추고 최적화하는 데 유용합니다.
class InlinerInterface : public
DialectInterfaceCollection<DialectInlinerInterface> {
/// 이 클래스의 훅(hook)들은 DialectInlinerInterface의 훅과 동일하며,
/// 주어진 다이얼렉트에 대한 인터페이스의 훅을 호출하는
/// 기본 구현을 제공합니다.
virtual bool isLegalToInline(Region *dest, Region *src,
IRMapping &valueMapping) const {
auto *handler = getInterfaceFor(dest->getContainingOp());
return handler ? handler->isLegalToInline(dest, src, valueMapping) : false;
}
};
MLIRContext *ctx = ...;
InlinerInterface interface(ctx);
if(!interface.isLegalToInline(...))
...
이름에서 알 수 있듯, 속성/연산/타입 인터페이스는 특정 속성/연산/타입 수준에서 등록되는 인터페이스입니다. 이들 인터페이스는 반드시 구현해야 하는 가상 인터페이스를 제공함으로써 파생 객체에 대한 접근을 제공합니다. 예를 들어, 많은 분석과 변환은 성능과 정확성을 높이기 위해 연산의 부작용(side effects)에 대해 추론하길 원합니다. 연산의 부작용은 일반적으로 특정 연산의 의미론에 결부되어 있습니다. 예를 들어 affine.load 연산은 이름에서 알 수 있듯 read 효과를 갖습니다.
이러한 인터페이스는 해당 IR 엔티티에 맞는 CRTP 클래스를 재정의하여 정의합니다. 각각 AttrInterface, OpInterface, TypeInterface입니다. 이 클래스들은 템플릿 매개변수로 Concept와 Model 클래스를 정의하는 Traits 클래스를 받습니다. 이 클래스들은 개념 기반 다형성(concept-based polymorphism)의 구현을 제공하며, 여기서 Concept는 가상 메서드 집합을 정의하고, Model은 구체 엔티티 타입을 템플릿으로 받아 이를 재정의합니다. 이 클래스들은 순수해야 하며, 비정적 데이터 멤버나 기타 변경 가능한 데이터를 포함하지 않아야 한다는 점이 중요합니다. 객체에 인터페이스를 부착하기 위해, 기본 인터페이스 클래스는 해당 객체의 트레이트 목록에 덧붙일 수 있는 Trait 클래스를 제공합니다.
struct ExampleOpInterfaceTraits {
/// 구현해야 할 가상 인터페이스를 지정하는 기본 개념(Concept) 클래스를 정의합니다.
struct Concept {
virtual ~Concept();
/// 연산에 대한 비정적(non-static) 훅의 예시입니다.
virtual unsigned exampleInterfaceHook(Operation *op) const = 0;
/// 연산에 대한 정적(static) 훅의 예시입니다. 정적 훅은
/// 연산의 구체 인스턴스를 필요로 하지 않습니다. 구현은
/// 비정적 경우와 마찬가지로 가상 훅입니다. 왜냐하면
/// 훅 자체의 구현은 여전히 간접 참조가 필요하기 때문입니다.
virtual unsigned exampleStaticInterfaceHook() const = 0;
};
/// 주어진 연산 타입에 대해 Concept를 특수화하는 모델 클래스를 정의합니다.
template <typename ConcreteOp>
struct Model : public Concept {
/// 메서드를 재정의하여 구체 연산으로 디스패치합니다.
unsigned exampleInterfaceHook(Operation *op) const final {
return llvm::cast<ConcreteOp>(op).exampleInterfaceHook();
}
/// 정적 메서드를 재정의하여 구체 연산 타입으로 디스패치합니다.
unsigned exampleStaticInterfaceHook() const final {
return ConcreteOp::exampleStaticInterfaceHook();
}
};
};
/// 분석과 변환이 상호작용할 메인 인터페이스 클래스를 정의합니다.
class ExampleOpInterface : public OpInterface<ExampleOpInterface,
ExampleOpInterfaceTraits> {
public:
/// LLVM 스타일 캐스팅을 지원하기 위해 기본 클래스 생성자를 상속합니다.
using OpInterface<ExampleOpInterface, ExampleOpInterfaceTraits>::OpInterface;
/// 인터페이스는 기본 `OpInterface` 클래스가 제공하는 'getImpl()'로 디스패치합니다.
/// 이 메서드는 Concept 인스턴스를 반환합니다.
unsigned exampleInterfaceHook() const {
return getImpl()->exampleInterfaceHook(getOperation());
}
unsigned exampleStaticInterfaceHook() const {
return getImpl()->exampleStaticInterfaceHook(getOperation()->getName());
}
};
인터페이스를 정의한 후에는, 앞서 언급한 제공된 트레이트 ExampleOpInterface::Trait를 추가하여 연산에 등록합니다. 이 인터페이스의 사용은 다른 파생 연산 타입을 사용하는 것과 동일합니다. 즉, 캐스팅을 사용합니다:
/// 연산을 정의할 때, 인터페이스는 'OpInterface<>' 기본 클래스가 제공하는
/// 중첩된 'Trait' 클래스를 통해 등록됩니다.
class MyOp : public Op<MyOp, ExampleOpInterface::Trait> {
public:
/// 파생 연산에서의 인터페이스 메서드 정의입니다.
unsigned exampleInterfaceHook() { return ...; }
static unsigned exampleStaticInterfaceHook() { return ...; }
};
/// 나중에, 특정 연산(예: 'MyOp')이 해당 인터페이스를 재정의하는지 질의할 수 있습니다.
Operation *op = ...;
if (ExampleOpInterface example = dyn_cast<ExampleOpInterface>(op))
llvm::errs() << "hook returned = " << example.exampleInterfaceHook() << "\n";
IR 객체의 정의를 수정하지 않고도 해당 객체에 대한 인터페이스 구현을 제공하고 싶은 경우가 있을 수 있습니다. 특히, 내장 타입과 같은, 이를 정의하는 다이얼렉트 외부에서 속성/연산/타입에 대한 인터페이스를 구현할 수 있게 해 줍니다.
이는 다음과 같이 Concept에서 파생된 두 개의 추가 클래스를 통해 개념 기반 다형성 모델을 확장함으로써 달성됩니다.
struct ExampleTypeInterfaceTraits {
struct Concept {
virtual unsigned exampleInterfaceHook(Type type) const = 0;
virtual unsigned exampleStaticInterfaceHook() const = 0;
};
template <typename ConcreteType>
struct Model : public Concept { /*...*/ };
/// `Model`과 달리, `FallbackModel`은 타입 객체를 훅에 그대로 전달하여
/// 메서드 본문에서 접근할 수 있게 합니다. 이는 메서드가 해당 클래스 내에
/// 정의되어 있지 않아 `this`에 접근할 수 없는 경우에도 유용합니다.
/// ODS는 모든 인터페이스에 대해 이 클래스를 자동으로 생성합니다.
template <typename ConcreteType>
struct FallbackModel : public Concept {
unsigned exampleInterfaceHook(Type type) const override {
getImpl()->exampleInterfaceHook(type);
}
unsigned exampleStaticInterfaceHook() const override {
ConcreteType::exampleStaticInterfaceHook();
}
};
/// `ExternalModel`은 인터페이스를 구현하는 모델 클래스와,
/// 인터페이스가 구현될 타입 클래스를 명시적으로 분리하여,
/// 인터페이스 메서드의 기본 구현을 제공할 공간을 마련합니다.
/// 기본 구현은 `cast<ConcreteType>`을 활용해 제네릭하게 정의할 수 있습니다.
/// `ConcreteType`이 기본 구현에 필요한 API를 제공하지 않는 경우,
/// 커스텀 구현이 기본 구현을 재정의하도록 `FallbackModel`을 직접 사용할 수 있습니다.
/// 이 클래스 템플릿은 인스턴스화되지 않으므로 컴파일 오류를 유발하지 않습니다.
/// ODS는 이 클래스를 자동으로 생성하고 기본 메서드 구현을 그 안에 둡니다.
template <typename ConcreteModel, typename ConcreteType>
struct ExternalModel : public FallbackModel<ConcreteModel> {
unsigned exampleInterfaceHook(Type type) const override {
// 여기에서 기본 구현을 제공할 수 있습니다.
return cast<ConcreteType>(type).callSomeTypeSpecificMethod();
}
};
};
외부 모델은 FallbackModel 또는 ExternalModel을 상속해 제공할 수 있으며, 특정 컨텍스트에서 관련 클래스에 모델 클래스를 등록하면 됩니다. 다른 컨텍스트에서는 등록되지 않는 한 해당 인터페이스를 볼 수 없습니다.
/// 구체 클래스에 대한 외부 인터페이스 구현 예시입니다.
/// 타입 클래스 정의 자체를 수정할 필요가 없습니다.
struct ExternalModelExample
: public ExampleTypeInterface::ExternalModel<ExternalModelExample,
IntegerType> {
static unsigned exampleStaticInterfaceHook() {
// 여기에서 구현을 제공합니다.
return IntegerType::someStaticMethod();
}
// `ExternalModel`에 기본 구현이 있으므로 `exampleInterfaceHook`를
// 정의할 필요가 없습니다. 원한다면 재정의할 수는 있습니다.
}
int main() {
MLIRContext context;
/* ... */;
// 사용하기 전에 주어진 컨텍스트에서 타입에 인터페이스 모델을 부착합니다.
// 이 시점에는 해당 타입을 포함한 다이얼렉트가 로드되어 있어야 합니다.
IntegerType::attachInterface<ExternalModelExample>(context);
}
참고: 외부에서 적용하는 인터페이스를 “소유”하고 있는 경우에만 이 메커니즘을 사용하는 것을 강력히 권장합니다. 이렇게 하면, 객체를 포함하는 다이얼렉트의 소유자와 인터페이스의 소유자 모두가 인터페이스 구현을 모르는 상황을 방지할 수 있으며, 이는 구현의 중복이나 분기를 초래할 수 있습니다.
외부 모델의 등록을 잊으면 추적하기 어려운 버그로 이어질 수 있습니다. declarePromisedInterface 함수를 사용하면 특정 연산에 대한 외부 모델 구현이 결국 제공되어야 함을 선언할 수 있습니다.
void MyDialect::initialize() {
declarePromisedInterface<SomeInterface, SomeOp>();
...
}
이제 인터페이스를(예: 캐스트에서) 사용하려 할 때, 외부 모델을 사전 등록하지 않았다면 다음과 유사한 런타임 오류가 발생합니다:
LLVM ERROR: checking for an interface (`SomeInterface`) that was promised by dialect 'mydialect' but never implemented. This is generally an indication that the dialect extension implementing the interface was never registered.
MLIR에서 제공하는 다이얼렉트와 인터페이스에 대해 이러한 오류가 발생한다면, register<Dialect><Interface>ExternalModels(DialectRegistry ®istry);와 같은 이름의 메서드를 찾아보십시오. git grep 'register.*SomeInterface.*Model' mlir로 검색해 보세요.
일부 다이얼렉트는 개방형 생태계를 갖고 있으며 가능한 모든 연산을 등록하지 않을 수 있습니다. 이런 경우에도 이러한 연산에 대해 OpInterface 구현을 지원할 수 있습니다. 연산이 등록되어 있지 않거나 특정 인터페이스의 구현을 제공하지 않으면, 질의는 다이얼렉트 자체로 폴백(fallback)됩니다.
이러한 경우를 위해 두 번째 모델이 사용되며, ODS(아래 참조)를 사용할 때 FallbackModel이라는 이름으로 자동 생성됩니다. 이 모델은 특정 다이얼렉트에 대해 구현할 수 있습니다:
// 이것은 `ExampleOpInterface`에 대한 다이얼렉트 폴백 구현입니다.
struct FallbackExampleOpInterface
: public ExampleOpInterface::FallbackModel<
FallbackExampleOpInterface> {
static bool classof(Operation *op) { return true; }
unsigned exampleInterfaceHook(Operation *op) const;
unsigned exampleStaticInterfaceHook() const;
};
다이얼렉트는 그런 다음 이 구현을 인스턴스화하고, getRegisteredInterfaceForOp 메서드를 재정의하여 특정 연산에 대해 이를 반환할 수 있습니다:
void *TestDialect::getRegisteredInterfaceForOp(TypeID typeID,
StringAttr opName) {
if (typeID == TypeID::get<ExampleOpInterface>()) {
if (isSupported(opName))
return fallbackExampleOpInterface;
return nullptr;
}
return nullptr;
}
참고: 이 섹션을 읽기 전에, 독자는 Operation Definition Specification 문서에 설명된 개념에 어느 정도 익숙해야 합니다.
앞서 설명한 바와 같이, 인터페이스는 호출자가 구체적인 파생 타입을 알 필요 없이 속성, 연산, 타입이 메서드 호출을 노출할 수 있게 합니다. 이 인프라의 단점은, 모든 조각을 연결하기 위해 약간의 보일러플레이트가 필요하다는 점입니다. MLIR은 ODS에서 인터페이스를 선언적으로 정의하고, C++ 정의를 자동 생성할 수 있는 메커니즘을 제공합니다.
예를 들어, ODS 프레임워크를 사용하면 위의 예시 인터페이스를 다음과 같이 정의할 수 있습니다:
def ExampleOpInterface : OpInterface<"ExampleOpInterface"> {
let description = [{
이것은 예시 인터페이스 정의입니다.
}];
let methods = [
InterfaceMethod<
"연산에 대한 비정적 훅의 예시입니다.",
"unsigned", "exampleInterfaceHook"
>,
StaticInterfaceMethod<
"연산에 대한 정적 훅의 예시입니다.",
"unsigned", "exampleStaticInterfaceHook"
>,
];
}
AttrInterface, OpInterface, TypeInterface 클래스의 정의를 제공하면, 인터페이스를 위한 C++ 클래스가 자동 생성됩니다. 인터페이스는 다음 구성 요소로 이루어져 있습니다:
C++ 클래스 이름(템플릿 매개변수로 제공)
인터페이스 기본 클래스들
설명(description)
C++ 네임스페이스(cppNamespace)
메서드(methods)
추가 클래스 선언(선택사항: extraClassDeclaration)
추가 공유 클래스 선언(선택사항: extraSharedClassDeclaration)
$_attr/$_op/$_type을 사용하여 IR 엔티티의 인스턴스를 참조할 수 있습니다. 인터페이스 선언에서는 인스턴스의 타입이 인터페이스 클래스이며, 트레이트 선언에서는 인스턴스의 타입이 구체 엔티티 클래스(예: IntegerAttr, FuncOp 등)입니다.추가 트레이트 클래스 선언(선택사항: extraTraitClassDeclaration)
OpInterface 클래스는 추가로 다음을 포함할 수 있습니다:
verify)
Trait::verifyTrait 메서드의 구조와 1:1로 대응됩니다.인터페이스에서 사용할 수 있는 메서드에는 InterfaceMethod와 StaticInterfaceMethod의 두 가지가 있습니다. 두 메서드는 핵심 구성 요소는 동일하지만, StaticInterfaceMethod는 파생 IR 객체의 정적 메서드를 모델링한다는 점이 다릅니다.
인터페이스 메서드는 다음 구성 요소로 이루어져 있습니다:
설명
반환 타입(ReturnType)
메서드 이름(MethodName)
인자(선택사항)
메서드 본문(MethodBody, 선택사항)
Model 트레이트 클래스에 정의된 메서드 내부에 배치되며, IR 엔티티에 부착되는 Trait 클래스에는 정의되지 않습니다. 보다 구체적으로, 이 본문은 인터페이스 클래스에서만 볼 수 있으며 파생 IR 엔티티에는 영향을 주지 않습니다.typename인 ConcreteAttr/ConcreteOp/ConcreteType을 사용할 수 있습니다.$_op와 $_self를 사용하여 파생 IR 엔티티의 인스턴스를 참조할 수 있습니다.기본 구현(DefaultImplementation, 선택사항)
Trait 클래스 내부에 배치되며, 어떤 인터페이스 클래스에도 직접적인 영향을 주지 않습니다. 따라서, 이 메서드는 다른 모든 Trait 메서드와 동일한 특성을 갖습니다.typename인 ConcreteAttr/ConcreteOp/ConcreteType을 사용할 수 있습니다.TestOpInterface::staticMethod()와 같이 완전 수식된 이름을 사용하여 인터페이스 클래스의 정적 필드를 참조할 수 있습니다.또한 ODS는 연산이 DeclareOpInterfaceMethods로 인터페이스를 지정하는 경우(InterfaceMethod들에 대한) 선언을 생성할 수 있습니다(아래 예 참조).
예시:
def MyInterface : OpInterface<"MyInterface"> {
let description = [{
이것은 인터페이스 설명입니다. 인터페이스의 의미와
컴파일러가 이를 어떻게 사용할 수 있는지에 대한 구체 정보를 제공합니다.
}];
let methods = [
InterfaceMethod<[{
이 메서드는 입력이 없고 반환 타입이 void인 간단한 비정적
인터페이스 메서드를 나타냅니다. 이 인터페이스를 구현하는 모든 연산은
이 메서드를 반드시 구현해야 합니다. 이 메서드는 대략 다음과 같은
연관 관계를 가집니다:
```c++
class ConcreteOp ... {
public:
void nonStaticMethod();
};
```
}], "void", "nonStaticMethod"
>,
InterfaceMethod<[{
이 메서드는 `unsigned` 입력 `i`를 갖고, 비-void 반환 값을 갖는
비정적 인터페이스 메서드를 나타냅니다. 이 인터페이스를 구현하는 모든
연산은 이 메서드를 반드시 구현해야 합니다. 이 메서드는 대략 다음과 같은
연관 관계를 가집니다:
```c++
class ConcreteOp ... {
public:
Value nonStaticMethod(unsigned i);
};
```
}], "Value", "nonStaticMethodWithParams", (ins "unsigned":$i)
>,
StaticInterfaceMethod<[{
이 메서드는 입력이 없고 반환 타입이 void인 정적 인터페이스 메서드를
나타냅니다. 이 인터페이스를 구현하는 모든 연산은 이 메서드를 반드시
구현해야 합니다. 이 메서드는 대략 다음과 같은 연관 관계를 가집니다:
```c++
class ConcreteOp ... {
public:
static void staticMethod();
};
```
}], "void", "staticMethod"
>,
StaticInterfaceMethod<[{
이 메서드는 메서드 본문의 명시적 구현을 갖는 정적 인터페이스 메서드에
해당합니다. 메서드 본문이 명시적으로 구현되었으므로, 이 메서드는 이를
구현하는 연산에서 정의되어서는 안 됩니다. 이 메서드는 이미 연산에
존재하는 속성(여기서는 `build` 메서드들)을 활용합니다. 이 메서드는
대략 인터페이스 `Model` 클래스에서 다음과 같은 코드에 해당합니다:
```c++
struct InterfaceTraits {
/// ... `Concept` 클래스는 생략 ...
template <typename ConcreteOp>
struct Model : public Concept {
Operation *create(OpBuilder &builder, Location loc) const override {
return ConcreteOp::create(builder, loc);
}
}
};
```
위에서 보듯, 이 메서드를 포함한 인터페이스를 구현하는 연산을
수정할 필요는 없습니다.
}],
"Operation *", "create", (ins "OpBuilder &":$builder, "Location":$loc),
/*methodBody=*/[{
return ConcreteOp::create(builder, loc);
}]>,
InterfaceMethod<[{
이 메서드는 메서드 본문의 명시적 구현을 갖는 비정적 메서드를
나타냅니다. 메서드 본문이 명시적으로 구현되었으므로, 이 메서드는
이를 구현하는 연산에서 정의되어서는 안 됩니다. 이 메서드는 이미
연산에 존재하는 속성(여기서는 `build` 메서드들)을 활용합니다. 이 메서드는
대략 인터페이스 `Model` 클래스에서 다음과 같은 코드에 해당합니다:
```c++
struct InterfaceTraits {
/// ... `Concept` 클래스는 생략 ...
template <typename ConcreteOp>
struct Model : public Concept {
unsigned getNumInputsAndOutputs(Operation *opaqueOp) const override {
ConcreteOp op = cast<ConcreteOp>(opaqueOp);
return op.getNumInputs() + op.getNumOutputs();
}
}
};
```
위에서 보듯, 이 메서드를 포함한 인터페이스를 구현하는 연산을
수정할 필요는 없습니다.
}],
"unsigned", "getNumInputsAndOutputs", (ins), /*methodBody=*/[{
return $_op.getNumInputs() + $_op.getNumOutputs();
}]>,
InterfaceMethod<[{
이 메서드는 메서드 본문의 기본 구현을 갖는 비정적 메서드를 나타냅니다.
여기 정의된 구현은 이 인터페이스를 구현하는 모든 연산에 부착되는
트레이트 클래스에 배치됩니다. 이는 생성되는 `Concept`와 `Model` 클래스에
영향을 주지 않습니다. 이 메서드는 대략 인터페이스 `Trait` 클래스에서
다음과 같은 코드에 해당합니다:
```c++
template <typename ConcreteOp>
class MyTrait : public OpTrait::TraitBase<ConcreteType, MyTrait> {
public:
bool isSafeToTransform() {
ConcreteOp op = cast<ConcreteOp>(this->getOperation());
return op.getProperties().hasFlag;
}
};
```
[Traits](Traits)에 상세히 설명된 바와 같이, 이 인터페이스를 구현하는
각 연산은 인터페이스 트레이트 또한 추가하므로, 이 인터페이스의 메서드는
파생 연산에 의해 상속됩니다. 이를 통해 인터페이스 클래스 자체를 변경하지 않고도,
이 인터페이스를 구현하는 각 연산에 이 메서드의 기본 구현을 주입할 수 있습니다.
어떤 연산이 이 기본 구현을 재정의하고 싶다면, 그저 메서드를 구현하면 되며
인터페이스 클래스는 파생 구현을 투명하게 사용합니다.
```c++
class ConcreteOp ... {
public:
bool isSafeToTransform() {
// 여기에서 트레이트가 제공한 훅의 기본 구현을 재정의할 수 있습니다.
}
};
```
}],
"bool", "isSafeToTransform", (ins), /*methodBody=*/[{}],
/*defaultImplementation=*/[{
return $_op.getProperties().hasFlag;
}]>,
];
}
// 연산 인터페이스는 선택적으로 `DeclareOpInterfaceMethods`로 감쌀 수 있습니다.
// 이렇게 하면 멤버 `foo`, `bar`, `fooStatic`에 대한 선언이 자동 생성됩니다.
// 본문이 있는 메서드는 연산 선언 내부에 선언되지 않고, 대신 연산 인터페이스
// 트레이트에서 직접 처리됩니다.
def OpWithInferTypeInterfaceOp : Op<...
[DeclareOpInterfaceMethods<MyInterface>]> { ... }
// 기본 구현을 갖는 메서드의 선언은 자동 생성되지 않습니다.
// 연산이 기본 동작을 재정의하길 원한다면, 재정의하고자 하는 메서드를 명시할 수 있습니다.
// 그러면 해당 메서드에 대한 선언 생성을 강제합니다.
def OpWithOverrideInferTypeInterfaceOp : Op<...
[DeclareOpInterfaceMethods<MyInterface, ["getNumWithDefault"]>]> { ... }
인터페이스는 제한적 형태의 상속을 지원하여, C++ 같은 프로그래밍 언어의 클래스 상속과 유사한 방식으로 기존 인터페이스 위에 구축할 수 있습니다. 이를 통해 많은 명시적 캐스팅의 고통 없이 더 쉽게 모듈식 인터페이스를 구축할 수 있습니다. 상속을 활성화하려면, 인터페이스가 그 정의에서 원하는 기본 클래스 집합을 제공하면 됩니다. 예:
def MyBaseInterface : OpInterface<"MyBaseInterface"> {
...
}
def MyInterface : OpInterface<"MyInterface", [MyBaseInterface]> {
...
}
이렇게 하면 MyInterface는 MyBaseInterface로부터, 특히 인터페이스 메서드와 추가 클래스 선언을 상속받습니다. 이러한 상속된 구성 요소는 불투명한 C++ 블랍으로 이루어져 있으므로, 이름을 적절히 샌드박싱할 수 없습니다. 따라서, 상속된 구성 요소가 이름 충돌을 일으키지 않도록 하는 것이 중요합니다. 그렇지 않으면 인터페이스 생성 중 오류가 발생합니다.
MyInterface는 또한 MyBaseInterface에 정의된 모든 기본 클래스들도 암묵적으로 상속합니다. 다만, 특정 속성/연산/타입에 대해 각 인터페이스는 항상 한 인스턴스만 존재한다는 점을 유의해야 합니다. 상속된 인터페이스 메서드는 단순히 기본 인터페이스 구현으로 포워딩됩니다. 이는 전체 시스템을 단순화하며, “다이아몬드 상속”과 관련된 잠재적 문제도 제거합니다. 속성/연산/타입에 부착된 인터페이스는 집합(set)으로 생각할 수 있으며, 각 인터페이스(기본 인터페이스 포함)는 이 집합 내에서 유일하며 필요 시 참조됩니다.
상속을 사용하는 인터페이스를 속성, 연산, 타입에 추가하면, 모든 기본 인터페이스도 암묵적으로 함께 추가됩니다. 사용자는 여전히 Declare<Attr|Op|Type>InterfaceMethods 헬퍼 클래스를 사용할 목적으로 기본 인터페이스를 수동으로 지정할 수 있습니다.
만약 인터페이스가 다음과 같이 지정되었다면:
def MyBaseInterface : OpInterface<"MyBaseInterface"> {
...
}
def MyOtherBaseInterface : OpInterface<MyOtherBaseInterface, [MyBaseInterface]> {
...
}
def MyInterface : OpInterface<"MyInterface", [MyBaseInterface, MyOtherBaseInterface]> {
...
}
MyInterface가 부착된 연산에는 다음 인터페이스들이 추가됩니다:
MyInterface와 MyOtherBaseInterface에 있는 MyBaseInterface의 메서드들은 해당 연산의 단일한 고유 구현으로 포워딩됩니다.
인터페이스를 정의한 후에는, mlir-tblgen에서 --gen-<attr|op|type>-interface-decls와 --gen-<attr|op|type>-interface-defs 옵션을 사용하여 C++ 헤더와 소스 파일을 생성할 수 있습니다. 인터페이스를 생성할 때, mlir-tblgen은 최상위 입력 .td 파일에 정의된 인터페이스만 생성한다는 점에 유의하십시오. 즉, 인클루드 파일들 내부에 정의된 인터페이스는 생성 고려 대상이 아닙니다.
참고: C++로 정의된 기존 연산 인터페이스는 ODS 프레임워크에서 OpInterfaceTrait 클래스를 통해 접근할 수 있습니다.
MLIR에는 다양한 연산에서 공통적으로 사용될 가능성이 높은 기능을 제공하는 표준 인터페이스가 포함되어 있습니다. 아래는 어떤 다이얼렉트에서도 직접 사용할 수 있는 핵심 인터페이스 목록입니다. 각 인터페이스 섹션의 헤더 형식은 다음과 같습니다:
인터페이스 클래스 이름
C++ 클래스 – ODS 클래스(해당되는 경우))CallOpInterface - ‘call’과 같은 연산을 표현하는 데 사용됩니다.
CallInterfaceCallable getCallableForCallee()void setCalleeFromCallable(CallInterfaceCallable)ArrayAttr getArgAttrsAttr()ArrayAttr getResAttrsAttr()void setArgAttrsAttr(ArrayAttr)void setResAttrsAttr(ArrayAttr)Attribute removeArgAttrsAttr()Attribute removeResAttrsAttr()CallableOpInterface - 호출의 대상 칼리(callee)를 표현하는 데 사용됩니다.
Region * getCallableRegion()ArrayRef<Type> getArgumentTypes()ArrayRef<Type> getResultTypes()ArrayAttr getArgAttrsAttr()ArrayAttr getResAttrsAttr()void setArgAttrsAttr(ArrayAttr)void setResAttrsAttr(ArrayAttr)Attribute removeArgAttrsAttr()Attribute removeResAttrsAttr()RegionKindInterface - 영역(region)의 추상적 의미론을 설명하는 데 사용됩니다.
RegionKind getRegionKind(unsigned index) - 이 연산 내부에서 주어진 인덱스를 갖는 영역의 종류를 반환합니다.
hasSSADominance(unsigned index) - 이 연산 내부에서 주어진 인덱스를 갖는 영역이 dominance를 요구하면 true를 반환합니다.
SymbolOpInterface - Symbol 연산을 표현하는 데 사용되며, 이는 SymbolTable을 정의하는 영역 바로 안에 존재합니다.
SymbolUserOpInterface - Symbol 연산을 참조하는 연산을 표현하는 데 사용됩니다. 이를 통해 심볼 사용의 안전하고 효율적인 검증 및 추가 기능을 제공할 수 있습니다.