forked from OSchip/llvm-project
192 lines
7.6 KiB
Markdown
192 lines
7.6 KiB
Markdown
# Introduction to MLIR Interfaces
|
|
|
|
MLIR is generic and very extensible, it allows for opaquely representing many
|
|
different dialects that have their own operations, attributes, types, etc. This
|
|
allows for dialects to be very expressive in their semantics, and allows for
|
|
MLIR to capture many different levels of abstraction. The downside to this is
|
|
that transformations and analyses must be extremely conservative about the
|
|
operations that they encounter, and must special case the different dialects
|
|
that they support. To combat this, MLIR provides the concept of `interfaces`.
|
|
|
|
## Motivation
|
|
|
|
Interfaces provide a generic way of interacting with the IR. The goal is to be
|
|
able to express transformations/analyses in terms of these interfaces without
|
|
encoding specific knowledge about the exact operation or dialect involved. This
|
|
will make the compiler more extensible by being able to add new dialects and
|
|
operations in a decoupled way with respect to the implementation of
|
|
transformations/analyses.
|
|
|
|
### Dialect Interfaces
|
|
|
|
Dialect interfaces are generally useful for transformation passes, or analyses,
|
|
that want to opaquely operate on operations, even *across* dialects. These
|
|
interfaces generally involve wide coverage over the entire dialect, and are only
|
|
used for a handful of transformations/analyses. In these cases, registering the
|
|
interface directly on each operation is overly complex and cumbersome. The
|
|
interface is not core to the operation, just to the specific transformation. An
|
|
example of where this type of interface would be used is inlining. Inlining
|
|
generally queries high level information about the operations within a dialect,
|
|
like legality and cost modeling, that often is not specific to one operation.
|
|
|
|
A dialect interface can be defined by inheriting from the CRTP base class
|
|
`DialectInterfaceBase::Base`. This class provides the necessary utilities for
|
|
registering an interface with the dialect so that it can be looked up later.
|
|
Once the interface has been defined, dialects can override it using dialect
|
|
specific information. The interfaces defined by a dialect are registered in a
|
|
similar mechanism to Attributes, Operations, Types, etc.
|
|
|
|
```c++
|
|
/// Define an Inlining interface to allow for dialects to opt-in.
|
|
class DialectInlinerInterface :
|
|
public DialectInterface::Base<DialectInlinerInterface> {
|
|
public:
|
|
/// Returns true if the given region 'src' can be inlined into the region
|
|
/// 'dest' that is attached to an operation registered to the current dialect.
|
|
/// 'valueMapping' contains any remapped values from within the 'src' region.
|
|
/// This can be used to examine what values will replace entry arguments into
|
|
/// the 'src' region for example.
|
|
virtual bool isLegalToInline(Region *dest, Region *src,
|
|
BlockAndValueMapping &valueMapping) const {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/// Override the inliner interface to add support for inlining affine
|
|
/// operations.
|
|
struct AffineInlinerInterface : public DialectInlinerInterface {
|
|
/// Affine structures have specific inlining constraints.
|
|
bool isLegalToInline(Region *dest, Region *src,
|
|
BlockAndValueMapping &valueMapping) const final {
|
|
...
|
|
}
|
|
};
|
|
|
|
/// Register the interface with the dialect.
|
|
AffineOpsDialect::AffineOpsDialect(MLIRContext *context) ... {
|
|
addInterfaces<AffineInlinerInterface>();
|
|
}
|
|
```
|
|
|
|
Once registered, these interfaces can be opaquely queried from the dialect by
|
|
the transformation/analysis that wants to use them:
|
|
|
|
```c++
|
|
Dialect *dialect = ...;
|
|
if (auto *interface = dialect->getInterface<DialectInlinerInterface>())
|
|
... // The dialect provides this interface.
|
|
```
|
|
|
|
#### DialectInterfaceCollections
|
|
|
|
An additional utility is provided via DialectInterfaceCollection. This CRTP
|
|
class allows for collecting all of the dialects that have registered a given
|
|
interface within the context.
|
|
|
|
```c++
|
|
class InlinerInterface : public
|
|
DialectInterfaceCollection<DialectInlinerInterface> {
|
|
/// The hooks for this class mirror the hooks for the DialectInlinerInterface,
|
|
/// with default implementations that call the hook on the interface for a
|
|
/// given dialect.
|
|
virtual bool isLegalToInline(Region *dest, Region *src,
|
|
BlockAndValueMapping &valueMapping) const {
|
|
auto *handler = getInterfaceFor(dest->getContainingOp());
|
|
return handler ? handler->isLegalToInline(dest, src, valueMapping) : false;
|
|
}
|
|
};
|
|
|
|
MLIRContext *ctx = ...;
|
|
InlinerInterface interface(ctx);
|
|
if(!interface.isLegalToInline(...))
|
|
...
|
|
```
|
|
|
|
### Operation Interfaces
|
|
|
|
Operation interfaces, as the name suggests, are those registered at the
|
|
Operation level. These interfaces provide an opaque view into derived
|
|
operations, by providing a virtual interface that must be implemented. As an
|
|
example, the `Linalg` dialect may implement an interface that provides general
|
|
queries about some of the dialects library operations. These queries may provide
|
|
things like: the number of parallel loops, the number of inputs and outputs,
|
|
etc.
|
|
|
|
Operation interfaces are defined by overriding the CRTP base class
|
|
`OpInterface`. This class takes as a template parameter, a `Traits` class that
|
|
defines a `Concept` and a `Model` class. These classes provide an implementation
|
|
of concept-based polymorphism, where the Concept defines a set of virtual
|
|
methods that are overridden by the Model that is templated on the concrete
|
|
operation type. It is important to note that these classes should be pure in
|
|
that they contain no non-static data members. Operations that wish to override
|
|
this interface should add the provided trait `OpInterface<..>::Trait` upon
|
|
registration.
|
|
|
|
```c++
|
|
struct ExampleOpInterfaceTraits {
|
|
/// Define a base concept class that defines the virtual interface that needs
|
|
/// to be overridden.
|
|
struct Concept {
|
|
virtual ~Concept();
|
|
virtual unsigned getNumInputs(Operation *op) = 0;
|
|
};
|
|
|
|
/// Define a model class that specializes a concept on a given operation type.
|
|
template <typename OpT>
|
|
struct Model {
|
|
/// Override the method to dispatch on the concrete operation.
|
|
unsigned getNumInputs(Operation *op) final {
|
|
return llvm::cast<OpT>(op).getNumInputs();
|
|
}
|
|
};
|
|
};
|
|
|
|
class ExampleOpInterface : public OpInterface<ExampleOpInterface,
|
|
ExampleOpInterfaceTraits> {
|
|
public:
|
|
/// The interface dispatches to 'getImpl()', an instance of the concept.
|
|
unsigned getNumInputs() {
|
|
return getImpl()->getNumInputs(getOperation());
|
|
}
|
|
};
|
|
|
|
```
|
|
|
|
Once the interface has been defined, it is registered to an operation by adding
|
|
the provided trait `ExampleOpInterface::Trait`. Using this interface is just
|
|
like using any other derived operation type, i.e. casting:
|
|
|
|
```c++
|
|
/// When defining the operation, the interface is registered via the nested
|
|
/// 'Trait' class provided by the 'OpInterface<>' base class.
|
|
class MyOp : public Op<MyOp, ExampleOpInterface::Trait> {
|
|
public:
|
|
/// The definition of the interface method on the derived operation.
|
|
unsigned getNumInputs() { return ...; }
|
|
};
|
|
|
|
/// Later, we can query if a specific operation(like 'MyOp') overrides the given
|
|
/// interface.
|
|
Operation *op = ...;
|
|
if (ExampleOpInterface example = dyn_cast<ExampleOpInterface>(op))
|
|
llvm::errs() << "num inputs = " << example.getNumInputs() << "\n";
|
|
```
|
|
|
|
#### Utilizing the ODS Framework
|
|
|
|
Operation interfaces require a bit of boiler plate to connect all of the pieces
|
|
together. The ODS(Operation Definition Specification) framework provides
|
|
simplified mechanisms for
|
|
[defining interfaces](OpDefinitions.md#operation-interfaces).
|
|
|
|
As an example, using the ODS framework would allow for defining the example
|
|
interface above as:
|
|
|
|
```tablegen
|
|
def ExampleOpInterface : OpInterface<"ExampleOpInterface"> {
|
|
let methods = [
|
|
InterfaceMethod<"unsigned", "getNumInputs">,
|
|
];
|
|
}
|
|
```
|