MLIR allows for a truly open ecosystem, as any dialect may define attributes, operations, and types that suit a specific level of abstraction. Traits
are a mechanism which abstracts implementation details and properties that are common across many different attributes/operations/types/etc.. Traits
may be used to specify special properties and constraints of the object, including whether an operation has side effects or that its output has the same type as the input. Some examples of operation traits are Commutative
, SingleResult
, Terminator
, etc. See the more comprehensive list of operation traits below for more examples of what is possible.
Traits may be defined in C++ by inheriting from the TraitBase<ConcreteType, TraitType>
class for the specific IR type. For attributes, this is AttributeTrait::TraitBase
. For operations, this is OpTrait::TraitBase
. For types, this is TypeTrait::TraitBase
. This base class takes as template parameters:
Curiously Recurring Template Pattern
.A derived trait class is expected to take a single template that corresponds to the ConcreteType
. An example trait definition is shown below:
template <typename ConcreteType> class MyTrait : public TraitBase<ConcreteType, MyTrait> { };
Operation traits may also provide a verifyTrait
hook, that is called when verifying the concrete operation. The trait verifiers will currently always be invoked before the main Op::verify
.
template <typename ConcreteType> class MyTrait : public OpTrait::TraitBase<ConcreteType, MyTrait> { public: /// Override the 'verifyTrait' hook to add additional verification on the /// concrete operation. static LogicalResult verifyTrait(Operation *op) { // ... } };
Note: It is generally good practice to define the implementation of the verifyTrait
hook out-of-line as a free function when possible to avoid instantiating the implementation for every concrete operation type.
Operation traits may also provide a foldTrait
hook that is called when folding the concrete operation. The trait folders will only be invoked if the concrete operation fold is either not implemented, fails, or performs an in-place fold.
The following signature of fold will be called if it is implemented and the op has a single result.
template <typename ConcreteType> class MyTrait : public OpTrait::TraitBase<ConcreteType, MyTrait> { public: /// Override the 'foldTrait' hook to support trait based folding on the /// concrete operation. static OpFoldResult foldTrait(Operation *op, ArrayRef<Attribute> operands) { { // ... } };
Otherwise, if the operation has a single result and the above signature is not implemented, or the operation has multiple results, then the following signature will be used (if implemented):
template <typename ConcreteType> class MyTrait : public OpTrait::TraitBase<ConcreteType, MyTrait> { public: /// Override the 'foldTrait' hook to support trait based folding on the /// concrete operation. static LogicalResult foldTrait(Operation *op, ArrayRef<Attribute> operands, SmallVectorImpl<OpFoldResult> &results) { { // ... } };
Note: It is generally good practice to define the implementation of the foldTrait
hook out-of-line as a free function when possible to avoid instantiating the implementation for every concrete operation type.
The above demonstrates the definition of a simple self-contained trait. It is also often useful to provide some static parameters to the trait to control its behavior. Given that the definition of the trait class is rigid, i.e. we must have a single template argument for the concrete object, the templates for the parameters will need to be split out. An example is shown below:
template <int Parameter> class MyParametricTrait { public: template <typename ConcreteType> class Impl : public TraitBase<ConcreteType, Impl> { // Inside of 'Impl' we have full access to the template parameters // specified above. }; };
Traits may be used when defining a derived object type, by simply appending the name of the trait class to the end of the base object class operation type:
/// Here we define 'MyAttr' along with the 'MyTrait' and `MyParametric trait /// classes we defined previously. class MyAttr : public Attribute::AttrBase<MyAttr, ..., MyTrait, MyParametricTrait<10>::Impl> {}; /// Here we define 'MyOp' along with the 'MyTrait' and `MyParametric trait /// classes we defined previously. class MyOp : public Op<MyOp, MyTrait, MyParametricTrait<10>::Impl> {}; /// Here we define 'MyType' along with the 'MyTrait' and `MyParametric trait /// classes we defined previously. class MyType : public Type::TypeBase<MyType, ..., MyTrait, MyParametricTrait<10>::Impl> {};
To use an operation trait in the ODS framework, we need to provide a definition of the trait class. This can be done using the NativeOpTrait
and ParamNativeOpTrait
classes. ParamNativeOpTrait
provides a mechanism in which to specify arguments to a parametric trait class with an internal Impl
.
// The argument is the c++ trait class name. def MyTrait : NativeOpTrait<"MyTrait">; // The first argument is the parent c++ class name. The second argument is a // string containing the parameter list. class MyParametricTrait<int prop> : NativeOpTrait<"MyParametricTrait", !cast<string>(!head(parameters))>;
These can then be used in the traits
list of an op definition:
def OpWithInferTypeInterfaceOp : Op<...[MyTrait, MyParametricTrait<10>]> { ... }
See the documentation on operation definitions for more details.
Traits may be used to provide additional methods, static fields, or other information directly on the concrete object. Traits
internally become Base
classes of the concrete operation, so all of these are directly accessible. To expose this information opaquely to transformations and analyses, interfaces
may be used.
To query if a specific object contains a specific trait, the hasTrait<>
method may be used. This takes as a template parameter the trait class, which is the same as the one passed when attaching the trait to an operation.
Operation *op = ..; if (op->hasTrait<MyTrait>() || op->hasTrait<MyParametricTrait<10>::Impl>()) ...;
MLIR provides a suite of traits that provide various functionalities that are common across many different operations. Below is a list of some key traits that may be used directly by any dialect. The format of the header for each trait section goes as follows:
Header
C++ class
-- ODS class
(if applicable))OpTrait::AffineScope
-- AffineScope
This trait is carried by region holding operations that define a new scope for the purposes of polyhedral optimization and the affine dialect in particular. Any SSA values of ‘index’ type that either dominate such operations, or are defined at the top-level of such operations, or appear as region arguments for such operations automatically become valid symbols for the polyhedral scope defined by that operation. As a result, such SSA values could be used as the operands or index operands of various affine dialect operations like affine.for, affine.load, and affine.store. The polyhedral scope defined by an operation with this trait includes all operations in its region excluding operations that are nested inside of other operations that themselves have this trait.
OpTrait::AutomaticAllocationScope
-- AutomaticAllocationScope
This trait is carried by region holding operations that define a new scope for automatic allocation. Such allocations are automatically freed when control is transferred back from the regions of such operations. As an example, allocations performed by memref.alloca
are automatically freed when control leaves the region of its closest surrounding op that has the trait AutomaticAllocationScope.
OpTrait::ResultsBroadcastableShape
-- ResultsBroadcastableShape
This trait adds the property that the operation is known to have broadcast-compatible operands and its result types' shape is the broadcast compatible with the shape of the broadcasted operands. Specifically, starting from the most varying dimension, each dimension pair of the two operands' shapes should either be the same or one of them is one. Also, the result shape should have the corresponding dimension equal to the larger one, if known. Shapes are checked partially if ranks or dimensions are not known. For example, an op with tensor<?x2xf32>
and tensor<2xf32>
as operand types and tensor<3x2xf32>
as the result type is broadcast-compatible.
This trait requires that the operands are either vector or tensor types.
OpTrait::IsCommutative
-- Commutative
This trait adds the property that the operation is commutative, i.e. X op Y == Y op X
OpTrait::ElementwiseMappable
-- ElementwiseMappable
This trait tags scalar ops that also can be applied to vectors/tensors, with their semantics on vectors/tensors being elementwise application. This trait establishes a set of properties that allow reasoning about / converting between scalar/vector/tensor code. These same properties allow blanket implementations of various analyses/transformations for all ElementwiseMappable
ops.
Note: Not all ops that are “elementwise” in some abstract sense satisfy this trait. In particular, broadcasting behavior is not allowed. See the comments on OpTrait::ElementwiseMappable
for the precise requirements.
OpTrait::FunctionLike
This trait provides APIs for operations that behave like functions. In particular:
Symbol
trait;This trait provides limited type support for the declared or defined functions. The convenience function getTypeAttrName()
returns the name of an attribute that can be used to store the function type. In addition, this trait provides getType
and setType
helpers to store a FunctionType
in the attribute named by getTypeAttrName()
.
In general, this trait assumes concrete ops use FunctionType
under the hood. If this is not the case, in order to use the function type support, concrete ops must define the following methods, using the same name, to hide the ones defined for FunctionType
: addBodyBlock
, getType
, getTypeWithoutArgsAndResults
and setType
.
OpTrait::HasParent<typename ParentOpType>
-- HasParent<string op>
or ParentOneOf<list<string> opList>
This trait provides APIs and verifiers for operations that can only be nested within regions that are attached to operations of ParentOpType
.
OpTrait::IsIsolatedFromAbove
-- IsolatedFromAbove
This trait signals that the regions of an operations are known to be isolated from above. This trait asserts that the regions of an operation will not capture, or reference, SSA values defined above the region scope. This means that the following is invalid if foo.region_op
is defined as IsolatedFromAbove
:
%result = arith.constant 10 : i32 foo.region_op { foo.yield %result : i32 }
This trait is an important structural property of the IR, and enables operations to have passes scheduled under them.
OpTrait::MemRefsNormalizable
-- MemRefsNormalizable
This trait is used to flag operations that consume or produce values of MemRef
type where those references can be ‘normalized’. In cases where an associated MemRef
has a non-identity memory-layout specification, such normalizable operations can be modified so that the MemRef
has an identity layout specification. This can be implemented by associating the operation with its own index expression that can express the equivalent of the memory-layout specification of the MemRef type. See [the -normalize-memrefs pass]. (https://mlir.llvm.org/docs/Passes/#-normalize-memrefs-normalize-memrefs)
OpTrait::SingleBlock
-- SingleBlock
This trait provides APIs and verifiers for operations with regions that have a single block.
OpTrait::SingleBlockImplicitTerminator<typename TerminatorOpType>
-- SingleBlockImplicitTerminator<string op>
This trait implies the SingleBlock
above, but adds the additional requirement that the single block must terminate with TerminatorOpType
.
OpTrait::SymbolTable
-- SymbolTable
This trait is used for operations that define a SymbolTable
.
OpTrait::IsTerminator
-- Terminator
This trait provides verification and functionality for operations that are known to be terminators.
OpTrait::NoTerminator
-- NoTerminator
This trait removes the requirement on regions held by an operation to have terminator operations at the end of a block. This requires that these regions have a single block. An example of operation using this trait is the top-level ModuleOp
.