MLIR is a unifying software framework for compiler development. MLIR can make optimal use of a variety of computing platforms such as GPUs, DPUs,TPUs, FPGAs, AI ASICS, and quantum computing systems (QPUs).
MLIR is a sub-project of the LLVM Compiler Infrastructure project and aims to build a ″reusable and extensible compiler infrastructure (..) and aid in connecting existing compilers together.″
Name
The name of the project stands for Multi-Level Intermediate Representation, in which the multi-level keyword refers to the possibility of defining multiple dialects and progressive conversions towards machine code. This capability enables MLIR to retain information at a higher level of abstraction and perform more accurate analyses and transformations, which otherwise would have to deal with lower level representations.
Dialects
Operations represent the core element around which dialects are built. They are identified by a name – that must be unique within the dialect they belong to – and have optional operands, results, attributes and regions. Operands and results adhere to the Static Single Assignment form. Each result also has an associated type. Attributes represent compile-time knowledge (e.g., constant values). Regions consist in a list of blocks, each of which may have input arguments and contain a list of operations. Despite the dialects being designed around the SSA form, PHI nodes are not part of this design and are instead replaced by the input arguments of blocks, in combination with operands of control-flow operations.
The general syntax for an operation is the following: %res:2 = "mydialect.morph"(%input#3) ({ ^bb0(%arg0: !mydialect<"custom_type"> loc("mysource.cc":10:8)): // nested operations }) { some.attribute = true, other_attribute = 1.5 } : (!mydialect<"custom_type">) -> (!mydialect<"other_type">, !mydialect<"other_type">) loc(callsite("foo" at "mysource.cc":10:8))The example shows an operation that is named morph, belongs to the mydialect dialect, takes one input operand and produces two results. The input argument has an associated type named custom_type and the results both have type other_type, with both the types belonging again to the mydialect dialect. The operation also has two associated attributes – named some.attribute and other_attribute – and a region containing one block. Finally, with keyword loc a locations are attached for debugging purposes.
The syntax of operations, types and attributes can also be customized according to the user preferences by implementing proper parsing and printing functions within the operation definition.
Core dialects
The MLIR dialects ecosystem is open and extensible, meaning that end-users are free to create new dialects capturing the semantics they need. Still, the codebase of MLIR already makes various kinds of dialects available to end-users. Each of them aims to address a specific aspect that often manifests within intermediate representations, but does it in a self-contained way. For example, the arith dialect holds simple mathematical operations on integer and floating point values, while the memref dialect holds the operations for memory management.
The following code defines a function that takes two floating point matrices and performs the sum between the values at the same positions: func.func @matrix_add(%arg0: memref<10x20xf32>, %arg1: memref<10x20xf32>) -> memref<10x20xf32> { %result = memref.alloc() : memref<10x20xf32>
affine.for %i = 0 to 10 {
affine.for %j = 0 to 20 {
%lhs = memref.load %arg0[%i, %j] : memref<10x20xf32>
%rhs = memref.load %arg1[%i, %j] : memref<10x20xf32>
%sum = arith.addf %lhs, %rhs : f32
memref.store %lhs, %result[%i, %j] : memref<10x20xf32>
}
}
func.return %result : memref<10x20xf32>
}Different dialects may be used to achieve the same result, and each of them may imply different levels of abstraction. In this example, the affine dialect has been chosen to reuse existing analyses and optimizations for polyhedral compilation.
One relevant core dialect is LLVM. Its purpose is to provide a one-to-one map of LLVM-IR – the intermediate representation used by LLVM – in order to enable the reuse all of its middle-end and backend transformations, including machine code generation.
Operation definition specification
The operations of a dialect can be defined using the C++ language, but also in a more convenient and robust way by using the Operation definition specification (ODS). By using TableGen, the C++ code for declarations and definitions can be then automatically generated.