forked from OSchip/llvm-project
Merge Ch3 of the Toy tutorial into chapter 2.
This effectively rewrites Ch.2 to introduce dialects, operations, and registration instead of deferring to Ch.3. This allows for introducing the best practices up front(using ODS, registering operations, etc.), and limits the opaque API to the chapter document instead of the code. PiperOrigin-RevId: 274724289
This commit is contained in:
parent
f29731d17f
commit
300112e135
|
@ -1,3 +1,9 @@
|
|||
|
||||
set(LLVM_TARGET_DEFINITIONS include/toy/Ops.td)
|
||||
mlir_tablegen(include/toy/Ops.h.inc -gen-op-decls)
|
||||
mlir_tablegen(include/toy/Ops.cpp.inc -gen-op-defs)
|
||||
add_public_tablegen_target(ToyCh2OpsIncGen)
|
||||
|
||||
set(LLVM_LINK_COMPONENTS
|
||||
Support
|
||||
)
|
||||
|
@ -6,8 +12,11 @@ add_toy_chapter(toyc-ch2
|
|||
toyc.cpp
|
||||
parser/AST.cpp
|
||||
mlir/MLIRGen.cpp
|
||||
mlir/Dialect.cpp
|
||||
)
|
||||
include_directories(include/)
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR}/include/)
|
||||
add_dependencies(toyc-ch2 ToyCh2OpsIncGen)
|
||||
target_link_libraries(toyc-ch2
|
||||
PRIVATE
|
||||
MLIRAnalysis
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
//===- Dialect.h - Dialect definition for the Toy IR ----------------------===//
|
||||
//
|
||||
// Copyright 2019 The MLIR Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// =============================================================================
|
||||
//
|
||||
// This file implements the IR Dialect for the Toy language.
|
||||
// See g3doc/Tutorials/Toy/Ch-2.md for more information.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef MLIR_TUTORIAL_TOY_DIALECT_H_
|
||||
#define MLIR_TUTORIAL_TOY_DIALECT_H_
|
||||
|
||||
#include "mlir/IR/Dialect.h"
|
||||
#include "mlir/IR/Function.h"
|
||||
|
||||
namespace mlir {
|
||||
namespace toy {
|
||||
|
||||
/// This is the definition of the Toy dialect. A dialect inherits from
|
||||
/// mlir::Dialect and registers custom attributes, operations, and types (in its
|
||||
/// constructor). It can also override some general behavior exposed via virtual
|
||||
/// methods.
|
||||
class ToyDialect : public mlir::Dialect {
|
||||
public:
|
||||
explicit ToyDialect(mlir::MLIRContext *ctx);
|
||||
|
||||
/// Provide a utility accessor to the dialect namespace. This is used by
|
||||
/// several utilities for casting between dialects.
|
||||
static llvm::StringRef getDialectNamespace() { return "toy"; }
|
||||
};
|
||||
|
||||
/// Include the auto-generated header file containing the declarations of the
|
||||
/// toy operations.
|
||||
#define GET_OP_CLASSES
|
||||
#include "toy/Ops.h.inc"
|
||||
|
||||
} // end namespace toy
|
||||
} // end namespace mlir
|
||||
|
||||
#endif // MLIR_TUTORIAL_TOY_DIALECT_H_
|
|
@ -0,0 +1,241 @@
|
|||
//===- Ops.td - Toy dialect operation definitions ----------*- tablegen -*-===//
|
||||
//
|
||||
// Copyright 2019 The MLIR Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// =============================================================================
|
||||
//
|
||||
// Defines the operations of the Toy dialect.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifdef TOY_OPS
|
||||
#else
|
||||
#define TOY_OPS
|
||||
|
||||
#ifdef OP_BASE
|
||||
#else
|
||||
include "mlir/IR/OpBase.td"
|
||||
#endif // OP_BASE
|
||||
|
||||
// Provide a definition of the 'toy' dialect in the ODS framework so that we
|
||||
// can define our operations.
|
||||
def Toy_Dialect : Dialect {
|
||||
let name = "toy";
|
||||
let cppNamespace = "toy";
|
||||
}
|
||||
|
||||
// Base class for toy dialect operations. This operation inherits from the base
|
||||
// `Op` class in OpBase.td, and provides:
|
||||
// * The parent dialect of the operation.
|
||||
// * The mnemonic for the operation, or the name without the dialect prefix.
|
||||
// * A list of traits for the operation.
|
||||
class Toy_Op<string mnemonic, list<OpTrait> traits = []> :
|
||||
Op<Toy_Dialect, mnemonic, traits>;
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Toy Operations
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
// We define a toy operation by inherting from our base 'Toy_Op' class above.
|
||||
// Here we provide the mnemonic and a list of traits for the operation. The
|
||||
// constant operation is marked as 'NoSideEffect' as it is a pure operation
|
||||
// and may be removed if dead.
|
||||
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
|
||||
// Provide a summary and description for this operation. This can be used to
|
||||
// auto-generate documenatation of the operations within our dialect.
|
||||
let summary = "constant";
|
||||
let description = [{
|
||||
Constant operation turns a literal into an SSA value. The data is attached
|
||||
to the operation as an attribute. For example:
|
||||
|
||||
```mlir
|
||||
%0 = "toy.constant"()
|
||||
{ value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }
|
||||
: () -> tensor<2x3xf64>
|
||||
```
|
||||
}];
|
||||
|
||||
// The constant operation takes an attribute as the only input.
|
||||
let arguments = (ins F64ElementsAttr:$value);
|
||||
|
||||
// The constant operation returns a single value of TensorType.
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Add custom build methods for the constant operation. These method populates
|
||||
// the `state` that MLIR uses to create operations, i.e. these are used when
|
||||
// using `builder.create<ConstantOp>(...)`.
|
||||
let builders = [
|
||||
// Build a constant with a given constant tensor value.
|
||||
OpBuilder<"Builder *builder, OperationState &result, "
|
||||
"DenseElementsAttr value", [{
|
||||
build(builder, result, value.getType(), value);
|
||||
}]>,
|
||||
|
||||
// Build a constant with a given constant floating-point value.
|
||||
OpBuilder<"Builder *builder, OperationState &result, double value", [{
|
||||
buildConstantOp(builder, result, value);
|
||||
}]>
|
||||
];
|
||||
|
||||
// Invoke a static verify method to verify this constant operation.
|
||||
let verifier = [{ return ::verify(*this); }];
|
||||
}
|
||||
|
||||
def AddOp : Toy_Op<"add", [NoSideEffect]> {
|
||||
let summary = "element-wise addition operation";
|
||||
let description = [{
|
||||
The "add" operation performs element-wise addition between two tensors.
|
||||
The shapes of the tensor operands are expected to match.
|
||||
}];
|
||||
|
||||
let arguments = (ins F64Tensor:$lhs, F64Tensor:$rhs);
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Allow building an AddOp with from the two input operands.
|
||||
let builders = [
|
||||
OpBuilder<"Builder *b, OperationState &result, Value *lhs, Value *rhs", [{
|
||||
buildAddOp(b, result, lhs, rhs);
|
||||
}]
|
||||
>];
|
||||
}
|
||||
|
||||
def GenericCallOp : Toy_Op<"generic_call"> {
|
||||
let summary = "generic call operation";
|
||||
let description = [{
|
||||
Generic calls represent calls to a user defined function that needs to
|
||||
be specialized for the shape of its arguments. The callee name is attached
|
||||
as a symbol reference via an attribute. The arguments list must match the
|
||||
arguments expected by the callee. For example:
|
||||
|
||||
```mlir
|
||||
%4 = "toy.generic_call"(%1, %3) {callee = @my_func}
|
||||
: (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64>
|
||||
```
|
||||
|
||||
This is only valid if a function named "my_func" exists and takes two
|
||||
arguments.
|
||||
}];
|
||||
|
||||
// The generic call operation takes a symbol reference attribute as the
|
||||
// callee, and inputs for the call.
|
||||
let arguments = (ins SymbolRefAttr:$callee, Variadic<F64Tensor>:$inputs);
|
||||
|
||||
// The generic call operation returns a single value of TensorType.
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Add custom build methods for the generic call operation.
|
||||
let builders = [
|
||||
// Build a constant with a given constant tensor value.
|
||||
OpBuilder<"Builder *builder, OperationState &result, "
|
||||
"StringRef callee, ArrayRef<Value *> arguments", [{
|
||||
buildGenericCallOp(builder, result, callee, arguments);
|
||||
}]>
|
||||
];
|
||||
}
|
||||
|
||||
def MulOp : Toy_Op<"mul", [NoSideEffect]> {
|
||||
let summary = "element-wise multiplication operation";
|
||||
let description = [{
|
||||
The "mul" operation performs element-wise multiplication between two
|
||||
tensors. The shapes of the tensor operands are expected to match.
|
||||
}];
|
||||
|
||||
let arguments = (ins F64Tensor:$lhs, F64Tensor:$rhs);
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Allow building a MulOp with from the two input operands.
|
||||
let builders = [
|
||||
OpBuilder<"Builder *b, OperationState &result, Value *lhs, Value *rhs", [{
|
||||
buildMulOp(b, result, lhs, rhs);
|
||||
}]
|
||||
>];
|
||||
}
|
||||
|
||||
def PrintOp : Toy_Op<"print"> {
|
||||
let summary = "print operation";
|
||||
let description = [{
|
||||
The "print" builtin operation prints a given input tensor, and produces
|
||||
no results.
|
||||
}];
|
||||
|
||||
// The print operation takes an input tensor to print.
|
||||
let arguments = (ins F64Tensor:$input);
|
||||
}
|
||||
|
||||
def ReshapeOp : Toy_Op<"reshape", [NoSideEffect]> {
|
||||
let summary = "tensor reshape operation";
|
||||
let description = [{
|
||||
Reshape operation is transforming its input tensor into a new tensor with
|
||||
the same number of elements but different shapes. For example:
|
||||
|
||||
```mlir
|
||||
%0 = "toy.reshape"(%arg1) : (tensor<10xf64>) -> tensor<5x2xf64>
|
||||
```
|
||||
}];
|
||||
|
||||
let arguments = (ins F64Tensor:$input);
|
||||
|
||||
// We expect that the reshape operation returns a statically shaped tensor.
|
||||
let results = (outs StaticShapeTensorOf<[F64]>);
|
||||
}
|
||||
|
||||
def ReturnOp : Toy_Op<"return", [Terminator, HasParent<"FuncOp">]> {
|
||||
let summary = "return operation";
|
||||
let description = [{
|
||||
The "return" operation represents a return operation within a function.
|
||||
The operation takes an optional tensor operand and produces no results.
|
||||
The operand type must match the signature of the function that contains
|
||||
the operation. For example:
|
||||
|
||||
```mlir
|
||||
func @foo() -> tensor<2xf64> {
|
||||
...
|
||||
toy.return %0 : tensor<2xf64>
|
||||
}
|
||||
```
|
||||
}];
|
||||
|
||||
// The return operation takes an optional input operand to return. This
|
||||
// value must match the return type of the enclosing function.
|
||||
let arguments = (ins Variadic<F64Tensor>:$input);
|
||||
|
||||
// Allow building a ReturnOp with no return operand.
|
||||
let builders = [OpBuilder<
|
||||
"Builder *b, OperationState &result", [{ build(b, result, llvm::None); }]
|
||||
>];
|
||||
|
||||
// Provide extra utility definitions on the c++ operation class definition.
|
||||
let extraClassDeclaration = [{
|
||||
bool hasOperand() { return getNumOperands() != 0; }
|
||||
}];
|
||||
|
||||
// Invoke a static verify method to verify this return operation.
|
||||
let verifier = [{ return ::verify(*this); }];
|
||||
}
|
||||
|
||||
def TransposeOp : Toy_Op<"transpose", [NoSideEffect]> {
|
||||
let summary = "transpose operation";
|
||||
|
||||
let arguments = (ins F64Tensor:$input);
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Allow building a TransposeOp with from the two input operands.
|
||||
let builders = [
|
||||
OpBuilder<"Builder *b, OperationState &result, Value *input", [{
|
||||
buildTransposeOp(b, result, input);
|
||||
}]
|
||||
>];
|
||||
}
|
||||
|
||||
#endif // TOY_OPS
|
|
@ -0,0 +1,156 @@
|
|||
//===- Dialect.cpp - Toy IR Dialect registration in MLIR ------------------===//
|
||||
//
|
||||
// Copyright 2019 The MLIR Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// =============================================================================
|
||||
//
|
||||
// This file implements the dialect for the Toy IR: custom type parsing and
|
||||
// operation verification.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "toy/Dialect.h"
|
||||
|
||||
#include "mlir/IR/Builders.h"
|
||||
#include "mlir/IR/StandardTypes.h"
|
||||
|
||||
using namespace mlir;
|
||||
using namespace mlir::toy;
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// ToyDialect
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Dialect creation, the instance will be owned by the context. This is the
|
||||
/// point of registration of custom types and operations for the dialect.
|
||||
ToyDialect::ToyDialect(mlir::MLIRContext *ctx) : mlir::Dialect("toy", ctx) {
|
||||
addOperations<
|
||||
#define GET_OP_LIST
|
||||
#include "toy/Ops.cpp.inc"
|
||||
>();
|
||||
}
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Toy Operations
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Build a constant operation.
|
||||
/// The builder is passed as an argument, so is the state that this method is
|
||||
/// expected to fill in order to build the operation.
|
||||
static void buildConstantOp(mlir::Builder *builder,
|
||||
mlir::OperationState &result, double value) {
|
||||
auto dataType = builder->getTensorType({}, builder->getF64Type());
|
||||
auto dataAttribute = DenseElementsAttr::get(dataType, value);
|
||||
ConstantOp::build(builder, result, dataType, dataAttribute);
|
||||
}
|
||||
|
||||
/// Verifier for the constant operation. This corresponds to the `::verify(...)`
|
||||
/// in the op definition.
|
||||
static mlir::LogicalResult verify(ConstantOp op) {
|
||||
// If the return type of the constant is not an unranked tensor, the shape
|
||||
// must match the shape of the attribute holding the data.
|
||||
auto resultType = op.getResult()->getType().cast<mlir::RankedTensorType>();
|
||||
if (!resultType)
|
||||
return success();
|
||||
|
||||
// Check that the rank of the attribute type matches the rank of the constant
|
||||
// result type.
|
||||
auto attrType = op.value().getType().cast<mlir::TensorType>();
|
||||
if (attrType.getRank() != resultType.getRank()) {
|
||||
return op.emitOpError(
|
||||
"return type must match the one of the attached value "
|
||||
"attribute: ")
|
||||
<< attrType.getRank() << " != " << resultType.getRank();
|
||||
}
|
||||
|
||||
// Check that each of the dimensions match between the two types.
|
||||
for (int dim = 0, dimE = attrType.getRank(); dim < dimE; ++dim) {
|
||||
if (attrType.getShape()[dim] != resultType.getShape()[dim]) {
|
||||
return op.emitOpError(
|
||||
"return type shape mismatches its attribute at dimension ")
|
||||
<< dim << ": " << attrType.getShape()[dim]
|
||||
<< " != " << resultType.getShape()[dim];
|
||||
}
|
||||
}
|
||||
return mlir::success();
|
||||
}
|
||||
|
||||
static void buildAddOp(mlir::Builder *builder, mlir::OperationState &result,
|
||||
mlir::Value *lhs, mlir::Value *rhs) {
|
||||
result.addTypes(builder->getTensorType(builder->getF64Type()));
|
||||
result.addOperands({lhs, rhs});
|
||||
}
|
||||
|
||||
static void buildGenericCallOp(mlir::Builder *builder,
|
||||
mlir::OperationState &result, StringRef callee,
|
||||
ArrayRef<mlir::Value *> arguments) {
|
||||
// Generic call always returns an unranked Tensor initially.
|
||||
result.addTypes(builder->getTensorType(builder->getF64Type()));
|
||||
result.addOperands(arguments);
|
||||
result.addAttribute("callee", builder->getSymbolRefAttr(callee));
|
||||
}
|
||||
|
||||
static void buildMulOp(mlir::Builder *builder, mlir::OperationState &result,
|
||||
mlir::Value *lhs, mlir::Value *rhs) {
|
||||
result.addTypes(builder->getTensorType(builder->getF64Type()));
|
||||
result.addOperands({lhs, rhs});
|
||||
}
|
||||
|
||||
static mlir::LogicalResult verify(ReturnOp op) {
|
||||
// We know that the parent operation is a function, because of the 'HasParent'
|
||||
// trait attached to the operation definition.
|
||||
auto function = cast<FuncOp>(op.getParentOp());
|
||||
|
||||
/// ReturnOps can only have a single optional operand.
|
||||
if (op.getNumOperands() > 1)
|
||||
return op.emitOpError() << "expects at most 1 return operand";
|
||||
|
||||
// The operand number and types must match the function signature.
|
||||
const auto &results = function.getType().getResults();
|
||||
if (op.getNumOperands() != results.size())
|
||||
return op.emitOpError()
|
||||
<< "does not return the same number of values ("
|
||||
<< op.getNumOperands() << ") as the enclosing function ("
|
||||
<< results.size() << ")";
|
||||
|
||||
// If the operation does not have an input, we are done.
|
||||
if (!op.hasOperand())
|
||||
return mlir::success();
|
||||
|
||||
auto inputType = *op.operand_type_begin();
|
||||
auto resultType = results.front();
|
||||
|
||||
// Check that the result type of the function matches the operand type.
|
||||
if (inputType == resultType || inputType.isa<mlir::UnrankedTensorType>() ||
|
||||
resultType.isa<mlir::UnrankedTensorType>())
|
||||
return mlir::success();
|
||||
|
||||
return op.emitError() << "type of return operand ("
|
||||
<< *op.operand_type_begin()
|
||||
<< ") doesn't match function result type ("
|
||||
<< results.front() << ")";
|
||||
}
|
||||
|
||||
static void buildTransposeOp(mlir::Builder *builder,
|
||||
mlir::OperationState &result, mlir::Value *value) {
|
||||
result.addTypes(builder->getTensorType(builder->getF64Type()));
|
||||
result.addOperands(value);
|
||||
}
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// TableGen'd op method definitions
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#define GET_OP_CLASSES
|
||||
#include "toy/Ops.cpp.inc"
|
|
@ -22,27 +22,29 @@
|
|||
|
||||
#include "toy/MLIRGen.h"
|
||||
#include "toy/AST.h"
|
||||
#include "toy/Dialect.h"
|
||||
|
||||
#include "mlir/Analysis/Verifier.h"
|
||||
#include "mlir/Dialect/StandardOps/Ops.h"
|
||||
#include "mlir/IR/Attributes.h"
|
||||
#include "mlir/IR/Builders.h"
|
||||
#include "mlir/IR/Function.h"
|
||||
#include "mlir/IR/Location.h"
|
||||
#include "mlir/IR/MLIRContext.h"
|
||||
#include "mlir/IR/Module.h"
|
||||
#include "mlir/IR/StandardTypes.h"
|
||||
#include "mlir/IR/Types.h"
|
||||
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
#include "llvm/ADT/ScopedHashTable.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
#include <numeric>
|
||||
|
||||
using namespace mlir::toy;
|
||||
using namespace toy;
|
||||
|
||||
using llvm::ArrayRef;
|
||||
using llvm::cast;
|
||||
using llvm::dyn_cast;
|
||||
using llvm::isa;
|
||||
using llvm::makeArrayRef;
|
||||
using llvm::ScopedHashTableScope;
|
||||
using llvm::SmallVector;
|
||||
using llvm::StringRef;
|
||||
|
@ -55,23 +57,16 @@ namespace {
|
|||
/// This will emit operations that are specific to the Toy language, preserving
|
||||
/// the semantics of the language and (hopefully) allow to perform accurate
|
||||
/// analysis and transformation based on these high level semantics.
|
||||
///
|
||||
/// At this point we take advantage of the "raw" MLIR APIs to create operations
|
||||
/// that haven't been registered in any way with MLIR. These operations are
|
||||
/// unknown to MLIR, custom passes could operate by string-matching the name of
|
||||
/// these operations, but no other type checking or semantics are associated
|
||||
/// with them natively by MLIR.
|
||||
class MLIRGenImpl {
|
||||
public:
|
||||
MLIRGenImpl(mlir::MLIRContext &context)
|
||||
: context(context), builder(&context) {}
|
||||
MLIRGenImpl(mlir::MLIRContext &context) : builder(&context) {}
|
||||
|
||||
/// Public API: convert the AST for a Toy module (source file) to an MLIR
|
||||
/// Module operation.
|
||||
mlir::ModuleOp mlirGen(ModuleAST &moduleAST) {
|
||||
// We create an empty MLIR module and codegen functions one at a time and
|
||||
// add them to the module.
|
||||
theModule = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context));
|
||||
theModule = mlir::ModuleOp::create(builder.getUnknownLoc());
|
||||
|
||||
for (FunctionAST &F : moduleAST) {
|
||||
auto func = mlirGen(F);
|
||||
|
@ -80,9 +75,9 @@ public:
|
|||
theModule.push_back(func);
|
||||
}
|
||||
|
||||
// FIXME: (in the next chapter...) without registering a dialect in MLIR,
|
||||
// this won't do much, but it should at least check some structural
|
||||
// properties of the generated MLIR module.
|
||||
// Verify the module after we have finished constructing it, this will check
|
||||
// the structural properties of the IR and invoke any specific verifiers we
|
||||
// have on the Toy operations.
|
||||
if (failed(mlir::verify(theModule))) {
|
||||
theModule.emitError("module verification error");
|
||||
return nullptr;
|
||||
|
@ -92,11 +87,6 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
/// In MLIR (like in LLVM) a "context" object holds the memory allocation and
|
||||
/// ownership of many internal structures of the IR and provides a level of
|
||||
/// "uniquing" across multiple modules (types for instance).
|
||||
mlir::MLIRContext &context;
|
||||
|
||||
/// A "module" matches a Toy source file: containing a list of functions.
|
||||
mlir::ModuleOp theModule;
|
||||
|
||||
|
@ -129,14 +119,14 @@ private:
|
|||
/// Create the prototype for an MLIR function with as many arguments as the
|
||||
/// provided Toy AST prototype.
|
||||
mlir::FuncOp mlirGen(PrototypeAST &proto) {
|
||||
auto location = loc(proto.loc());
|
||||
|
||||
// This is a generic function, the return type will be inferred later.
|
||||
llvm::SmallVector<mlir::Type, 4> ret_types;
|
||||
// Arguments type is uniformly a generic array.
|
||||
// Arguments type are uniformly unranked tensors.
|
||||
llvm::SmallVector<mlir::Type, 4> arg_types(proto.getArgs().size(),
|
||||
getType(VarType{}));
|
||||
auto func_type = builder.getFunctionType(arg_types, ret_types);
|
||||
auto function = mlir::FuncOp::create(loc(proto.loc()), proto.getName(),
|
||||
func_type, /* attrs = */ {});
|
||||
auto func_type = builder.getFunctionType(arg_types, llvm::None);
|
||||
auto function = mlir::FuncOp::create(location, proto.getName(), func_type);
|
||||
|
||||
// Mark the function as generic: it'll require type specialization for every
|
||||
// call site.
|
||||
|
@ -183,10 +173,16 @@ private:
|
|||
// Implicitly return void if no return statement was emitted.
|
||||
// FIXME: we may fix the parser instead to always return the last expression
|
||||
// (this would possibly help the REPL case later)
|
||||
if (function.getBody().back().back().getName().getStringRef() !=
|
||||
"toy.return") {
|
||||
ReturnExprAST fakeRet(funcAST.getProto()->loc(), llvm::None);
|
||||
mlirGen(fakeRet);
|
||||
ReturnOp returnOp;
|
||||
if (!entryBlock.empty())
|
||||
returnOp = dyn_cast<ReturnOp>(entryBlock.back());
|
||||
if (!returnOp) {
|
||||
builder.create<ReturnOp>(loc(funcAST.getProto()->loc()));
|
||||
} else if (returnOp.hasOperand()) {
|
||||
// Otherwise, if this return operation has an operand then add a result to
|
||||
// the function.
|
||||
function.setType(builder.getFunctionType(function.getType().getInputs(),
|
||||
getType(VarType{})));
|
||||
}
|
||||
|
||||
return function;
|
||||
|
@ -205,36 +201,25 @@ private:
|
|||
// and the result value is returned. If an error occurs we get a nullptr
|
||||
// and propagate.
|
||||
//
|
||||
mlir::Value *L = mlirGen(*binop.getLHS());
|
||||
if (!L)
|
||||
mlir::Value *lhs = mlirGen(*binop.getLHS());
|
||||
if (!lhs)
|
||||
return nullptr;
|
||||
mlir::Value *R = mlirGen(*binop.getRHS());
|
||||
if (!R)
|
||||
mlir::Value *rhs = mlirGen(*binop.getRHS());
|
||||
if (!rhs)
|
||||
return nullptr;
|
||||
auto location = loc(binop.loc());
|
||||
|
||||
// Derive the operation name from the binary operator. At the moment we only
|
||||
// support '+' and '*'.
|
||||
const char *op_name = nullptr;
|
||||
switch (binop.getOp()) {
|
||||
case '+':
|
||||
op_name = "toy.add";
|
||||
break;
|
||||
return builder.create<AddOp>(location, lhs, rhs);
|
||||
case '*':
|
||||
op_name = "toy.mul";
|
||||
break;
|
||||
default:
|
||||
emitError(location, "error: invalid binary operator '")
|
||||
<< binop.getOp() << "'";
|
||||
return nullptr;
|
||||
return builder.create<MulOp>(location, lhs, rhs);
|
||||
}
|
||||
|
||||
// Build the MLIR operation from the name and the two operands. The return
|
||||
// type is always a generic array for binary operators.
|
||||
mlir::OperationState result(location, op_name);
|
||||
result.addTypes(getType(VarType{}));
|
||||
result.addOperands({L, R});
|
||||
return builder.createOperation(result)->getResult(0);
|
||||
emitError(location, "invalid binary operator '") << binop.getOp() << "'";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// This is a reference to a variable in an expression. The variable is
|
||||
|
@ -251,17 +236,18 @@ private:
|
|||
|
||||
/// Emit a return operation. This will return failure if any generation fails.
|
||||
mlir::LogicalResult mlirGen(ReturnExprAST &ret) {
|
||||
mlir::OperationState result(loc(ret.loc()), "toy.return");
|
||||
auto location = loc(ret.loc());
|
||||
|
||||
// `return` takes an optional expression, we need to account for it here.
|
||||
// 'return' takes an optional expression, handle that case here.
|
||||
mlir::Value *expr = nullptr;
|
||||
if (ret.getExpr().hasValue()) {
|
||||
auto *expr = mlirGen(*ret.getExpr().getValue());
|
||||
if (!expr)
|
||||
if (!(expr = mlirGen(*ret.getExpr().getValue())))
|
||||
return mlir::failure();
|
||||
result.addOperands(expr);
|
||||
}
|
||||
|
||||
builder.createOperation(result);
|
||||
// Otherwise, this return operation has zero operands.
|
||||
builder.create<ReturnOp>(location, expr ? makeArrayRef(expr)
|
||||
: ArrayRef<mlir::Value *>());
|
||||
return mlir::success();
|
||||
}
|
||||
|
||||
|
@ -303,11 +289,9 @@ private:
|
|||
auto dataAttribute =
|
||||
mlir::DenseElementsAttr::get(dataType, llvm::makeArrayRef(data));
|
||||
|
||||
// Build the MLIR op `toy.constant`, only boilerplate below.
|
||||
mlir::OperationState result(loc(lit.loc()), "toy.constant");
|
||||
result.addTypes(type);
|
||||
result.addAttribute("value", dataAttribute);
|
||||
return builder.createOperation(result)->getResult(0);
|
||||
// Build the MLIR op `toy.constant`. This invokes the `ConstantOp::build`
|
||||
// method.
|
||||
return builder.create<ConstantOp>(loc(lit.loc()), type, dataAttribute);
|
||||
}
|
||||
|
||||
/// Recursive helper function to accumulate the data that compose an array
|
||||
|
@ -333,6 +317,7 @@ private:
|
|||
/// builtin. Other identifiers are assumed to be user-defined functions.
|
||||
mlir::Value *mlirGen(CallExprAST &call) {
|
||||
llvm::StringRef callee = call.getCallee();
|
||||
auto location = loc(call.loc());
|
||||
|
||||
// Codegen the operands first.
|
||||
SmallVector<mlir::Value *, 4> operands;
|
||||
|
@ -346,20 +331,18 @@ private:
|
|||
// Builting calls have their custom operation, meaning this is a
|
||||
// straightforward emission.
|
||||
if (callee == "transpose") {
|
||||
mlir::OperationState result(loc(call.loc()), "toy.transpose");
|
||||
result.addTypes(getType(VarType{}));
|
||||
result.operands = std::move(operands);
|
||||
return builder.createOperation(result)->getResult(0);
|
||||
if (call.getArgs().size() != 1) {
|
||||
emitError(location, "MLIR codegen encountered an error: toy.transpose "
|
||||
"does not accept multiple arguments");
|
||||
return nullptr;
|
||||
}
|
||||
return builder.create<TransposeOp>(location, operands[0]);
|
||||
}
|
||||
|
||||
// Otherwise this is a call to a user-defined function. Calls to
|
||||
// user-defined functions are mapped to a custom call that takes the callee
|
||||
// name as an attribute.
|
||||
mlir::OperationState result(loc(call.loc()), "toy.generic_call");
|
||||
result.addTypes(getType(VarType{}));
|
||||
result.operands = std::move(operands);
|
||||
result.addAttribute("callee", builder.getSymbolRefAttr(callee));
|
||||
return builder.createOperation(result)->getResult(0);
|
||||
// Otherwise this is a call to a user-defined function. Calls to ser-defined
|
||||
// functions are mapped to a custom call that takes the callee name as an
|
||||
// attribute.
|
||||
return builder.create<GenericCallOp>(location, callee, operands);
|
||||
}
|
||||
|
||||
/// Emit a print expression. It emits specific operations for two builtins:
|
||||
|
@ -369,19 +352,13 @@ private:
|
|||
if (!arg)
|
||||
return mlir::failure();
|
||||
|
||||
mlir::OperationState result(loc(call.loc()), "toy.print");
|
||||
result.addOperands(arg);
|
||||
builder.createOperation(result);
|
||||
builder.create<PrintOp>(loc(call.loc()), arg);
|
||||
return mlir::success();
|
||||
}
|
||||
|
||||
/// Emit a constant for a single number (FIXME: semantic? broadcast?)
|
||||
mlir::Value *mlirGen(NumberExprAST &num) {
|
||||
mlir::OperationState result(loc(num.loc()), "toy.constant");
|
||||
mlir::Type elementType = builder.getF64Type();
|
||||
result.addTypes(builder.getTensorType({}, elementType));
|
||||
result.addAttribute("value", builder.getF64FloatAttr(num.getValue()));
|
||||
return builder.createOperation(result)->getResult(0);
|
||||
return builder.create<ConstantOp>(loc(num.loc()), num.getValue());
|
||||
}
|
||||
|
||||
/// Dispatch codegen for the right expression subclass using RTTI.
|
||||
|
@ -425,13 +402,11 @@ private:
|
|||
// with specific shape, we emit a "reshape" operation. It will get
|
||||
// optimized out later as needed.
|
||||
if (!vardecl.getType().shape.empty()) {
|
||||
mlir::OperationState result(loc(vardecl.loc()), "toy.reshape");
|
||||
result.addTypes(getType(vardecl.getType()));
|
||||
result.addOperands(value);
|
||||
value = builder.createOperation(result)->getResult(0);
|
||||
value = builder.create<ReshapeOp>(loc(vardecl.loc()),
|
||||
getType(vardecl.getType()), value);
|
||||
}
|
||||
|
||||
// Register the value in the symbol table
|
||||
// Register the value in the symbol table.
|
||||
if (failed(declare(vardecl.getName(), value)))
|
||||
return nullptr;
|
||||
return value;
|
||||
|
@ -439,7 +414,7 @@ private:
|
|||
|
||||
/// Codegen a list of expression, return failure if one of them hit an error.
|
||||
mlir::LogicalResult mlirGen(ExprASTList &blockAST) {
|
||||
ScopedHashTableScope<llvm::StringRef, mlir::Value *> var_scope(symbolTable);
|
||||
ScopedHashTableScope<StringRef, mlir::Value *> var_scope(symbolTable);
|
||||
for (auto &expr : blockAST) {
|
||||
// Specific handling for variable declarations, return statement, and
|
||||
// print. These can only appear in block list and not in nested
|
||||
|
@ -465,7 +440,7 @@ private:
|
|||
}
|
||||
|
||||
/// Build a tensor type from a list of shape dimensions.
|
||||
mlir::Type getType(llvm::ArrayRef<int64_t> shape) {
|
||||
mlir::Type getType(ArrayRef<int64_t> shape) {
|
||||
// If the shape is empty, then this type is unranked.
|
||||
if (shape.empty())
|
||||
return builder.getTensorType(builder.getF64Type());
|
||||
|
@ -474,8 +449,8 @@ private:
|
|||
return builder.getTensorType(shape, builder.getF64Type());
|
||||
}
|
||||
|
||||
/// Build an MLIR type from a Toy AST variable type
|
||||
/// (forward to the generic getType(T) above).
|
||||
/// Build an MLIR type from a Toy AST variable type (forward to the generic
|
||||
/// getType above).
|
||||
mlir::Type getType(const VarType &type) { return getType(type.shape); }
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "toy/Dialect.h"
|
||||
#include "toy/MLIRGen.h"
|
||||
#include "toy/Parser.h"
|
||||
#include <memory>
|
||||
|
@ -75,6 +76,9 @@ std::unique_ptr<toy::ModuleAST> parseInputFile(llvm::StringRef filename) {
|
|||
}
|
||||
|
||||
int dumpMLIR() {
|
||||
// Register our Dialect with MLIR.
|
||||
mlir::registerDialect<mlir::toy::ToyDialect>();
|
||||
|
||||
mlir::MLIRContext context;
|
||||
|
||||
// Handle '.toy' input to the compiler.
|
||||
|
|
|
@ -23,45 +23,73 @@ addresses this issue by being designed for extensibility. As such, there are
|
|||
little to none pre-defined instructions (*operations* in MLIR terminology) or
|
||||
types.
|
||||
|
||||
## MLIR Dialects and Operations
|
||||
## Interfacing with MLIR
|
||||
|
||||
[Language reference](../../LangRef.md#dialects)
|
||||
|
||||
In MLIR, the core unit of abstraction and computation is an `Operation`, similar
|
||||
in many ways to LLVM instructions. Operations can be used to represent all of
|
||||
the core IR structures in LLVM: instructions, globals(like functions), modules,
|
||||
etc; however MLIR does not have a closed set of operations. Instead, the MLIR
|
||||
operation set is fully extensible and operations can have application-specific
|
||||
semantics.
|
||||
[Language reference](../../LangRef.md)
|
||||
|
||||
MLIR is designed to be a completely extensible infrastructure; there is no
|
||||
closed set of attributes (think always constant metadata), operations, or types.
|
||||
MLIR supports this extensibility with the concept of
|
||||
[Dialects](../../LangRef.md#dialects). Among other things, Dialects provide a
|
||||
grouping mechanism for operations under a unique `namespace`. Dialects will be
|
||||
discussed a bit more in the [next chapter](Ch-3.md).
|
||||
[Dialects](../../LangRef.md#dialects). Dialects provide a grouping mechanism for
|
||||
abstraction under a unique `namespace`.
|
||||
|
||||
In MLIR, [`Operations`](../../LangRef.md#operations) are the core unit of
|
||||
abstraction and computation, similar in many ways to LLVM instructions.
|
||||
Operations can have application-specific semantics and can be used to represent
|
||||
all of the core IR structures in LLVM: instructions, globals(like functions),
|
||||
modules, etc.
|
||||
|
||||
Here is the MLIR assembly for the Toy 'transpose' operations:
|
||||
|
||||
```MLIR(.mlir)
|
||||
%t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>
|
||||
```mlir
|
||||
%t_tensor = "toy.transpose"(%tensor) {inplace = true} : (tensor<2x3xf64>) -> tensor<3x2xf64>
|
||||
```
|
||||
|
||||
Let's look at the anatomy of this MLIR operation:
|
||||
Let's break down the anatomy of this MLIR operation:
|
||||
|
||||
- it is identified by its name, which is expected to be a unique string (e.g.
|
||||
`toy.transpose`).
|
||||
* the operation name is split in two parts: the dialect namespace prefix,
|
||||
and the specific op name. This can be read as the `transpose` operation
|
||||
in the `toy` dialect.
|
||||
- it takes as input zero or more operands (or arguments), which are SSA values
|
||||
defined by other operations or referring to block arguments (e.g.
|
||||
`%tensor`).
|
||||
- it produces zero or more results (we will limit ourselves to single result
|
||||
operations in the context of Toy), which are SSA values (e.g. `%t_tensor`).
|
||||
- it has zero or more attributes, which are special operands that are always
|
||||
constant (e.g. `inplace = true`).
|
||||
- lastly, the type of the operation appears at the end in a functional form,
|
||||
spelling the types of the arguments in parentheses and the type of the
|
||||
return values afterward.
|
||||
- `%t_tensor`
|
||||
|
||||
* The name given to the result defined by this operation. An operation may
|
||||
define zero or more results (we will limit ourselves to single result
|
||||
operations in the context of Toy), which are SSA values.
|
||||
|
||||
- `"toy.transpose"`
|
||||
|
||||
* The name of the operation. It is expected to be a unique string, with
|
||||
the namespace of the dialect prefixed before a "`.`". This can be read
|
||||
as the `transpose` operation in the `toy` dialect.
|
||||
|
||||
- `(%tensor)`
|
||||
|
||||
* A list of zero or more input operands (or arguments), which are SSA
|
||||
values defined by other operations or referring to block arguments.
|
||||
|
||||
- `{ inplace = true }`
|
||||
|
||||
* A dictionary of zero or more attributes, which are special operands that
|
||||
are always constant. Here we define a boolean attribute named 'inplace',
|
||||
that has a constant value of true.
|
||||
|
||||
- `(tensor<2x3xf64) -> tensor<3x2xf64>`
|
||||
|
||||
* This trailing portion refers to the type of the operation in a
|
||||
functional form, spelling the types of the arguments in parentheses and
|
||||
the type of the return values afterward.
|
||||
|
||||
Shown here is the general form of an operation. As described above, the set of
|
||||
operations in MLIR is extensible. This means that the infrastructure must be
|
||||
able to opaquely reason about the structure of an operation. This is done by
|
||||
boiling down the composition of an operation into discrete pieces:
|
||||
|
||||
- A name for the operation.
|
||||
- A source location for debugging purposes.
|
||||
- A list of SSA operand values.
|
||||
- A list of [types](../../LangRef.md#type-system) for result values.
|
||||
- A list of [attributes](../../LangRef.md#attributes).
|
||||
- A list of successors [blocks](../../LangRef.md#blocks) (for branches
|
||||
mostly).
|
||||
- A list of [regions](../../LangRef.md#regions) (for structural operations
|
||||
like functions).
|
||||
|
||||
Finally, in MLIR every operation has a mandatory source location associated with
|
||||
it. Contrary to LLVM where debug info locations are metadata and can be dropped,
|
||||
|
@ -69,93 +97,407 @@ in MLIR the location is a core requirement which translates in APIs manipulating
|
|||
operations requiring it. Dropping a location becomes an explicit choice and
|
||||
cannot happen by mistake.
|
||||
|
||||
## Opaque API
|
||||
### Opaque API
|
||||
|
||||
MLIR is designed to be a completely extensible system, as such the
|
||||
infrastructure has the capability to opaquely represent operations (as well as
|
||||
attributes, types, etc.) that have not been registered. This allows MLIR to
|
||||
parse, represent, and round-trip any valid IR. For example, the following can
|
||||
round-trip through *mlir-opt*:
|
||||
MLIR is designed to be a completely extensible system, and as such, the
|
||||
infrastructure has the capability to opaquely represent all of its core
|
||||
components: attributes, operations, types, etc. This allows MLIR to parse,
|
||||
represent, and round-trip any valid IR. For example, we could place our toy
|
||||
operation from above into an .mlir file and round-trip through *mlir-opt*
|
||||
without registering anything:
|
||||
|
||||
```mlir
|
||||
func @toy_func(%tensor: tensor<2x3xf64>) -> tensor<3x2xf64> {
|
||||
%t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>
|
||||
return %t_tensor : tensor<3x2xf64>
|
||||
}
|
||||
```
|
||||
|
||||
In the cases of unregistered attributes, operations, types, MLIR will enforce
|
||||
some structural constraints (SSA, block termination, etc.) but otherwise they
|
||||
are completely opaque. This can be useful for bootstrapping purposes, but it is
|
||||
generally advised against. Opaque operations must be treated conservatively by
|
||||
transformations and analyses, and are much harder to construct and manipulate.
|
||||
|
||||
This handling can be observed by crafting what should be an invalid IR for Toy
|
||||
and see it round-trip without tripping the verifier:
|
||||
|
||||
```MLIR(.mlir)
|
||||
func @some_func(%arg0: !random_dialect<"custom_type">) -> !another_dialect<"other_type"> {
|
||||
%result = "custom.operation"(%arg0) { attr = #random_dialect<"custom_attribute"> } : (!random_dialect<"custom_type">) -> !another_dialect<"other_type">
|
||||
return %result : !another_dialect<"other_type">
|
||||
// RUN: toyc %s -emit=mlir
|
||||
|
||||
func @main() {
|
||||
%0 = "toy.print"() : () -> tensor<2x3xf64>
|
||||
}
|
||||
```
|
||||
|
||||
Here MLIR will enforce some structural constraints (SSA, block termination,
|
||||
etc.) but otherwise the types and the `custom.operation` are completely opaque.
|
||||
There are multiple problems here: the `toy.print` operation is not a terminator,
|
||||
it should take an operand, and it shouldn't return any values. In the next
|
||||
section, we will register our dialect and operations with MLIR, plug into the
|
||||
verifier, and add nicer APIs to manipulate our operations.
|
||||
|
||||
We will take advantage of this facility for the initial emission of MLIR for Toy
|
||||
by traversing the AST. Our operation names will be prefixed `toy.` in
|
||||
preparation for a `toy` dialect, which we will introduce with more details in
|
||||
the [next chapter](Ch-3.md).
|
||||
## Defining a Toy Dialect
|
||||
|
||||
Programmatically creating an opaque operation, like the one above, involves
|
||||
using the `mlir::OperationState` structure which group all the basic elements
|
||||
needed to build an operation with an `mlir::OpBuilder`:
|
||||
|
||||
- The name of the operation.
|
||||
- A location for debugging purposes. It is mandatory, but can be explicitly
|
||||
set to `unknown`.
|
||||
- A list of operand values.
|
||||
- A list of types for result values.
|
||||
- A list of attributes.
|
||||
- A list of successors blocks (for branches mostly).
|
||||
- A list of regions (for structural operations like functions).
|
||||
|
||||
To build the `custom.operation` from the listing above, assuming you have a
|
||||
`Value *` handle to `%arg0`, is as simple as:
|
||||
To effectively interface with MLIR, we will define a new Toy dialect. This
|
||||
dialect will properly model the semantics of the Toy language, as well as
|
||||
provide an easy avenue for high-level analysis and transformation.
|
||||
|
||||
```c++
|
||||
// Creation of the state defining the operation:
|
||||
mlir::OperationState state(location, "custom.operation");
|
||||
state.addOperands(arg0);
|
||||
/// This is the definition of the Toy dialect. A dialect inherits from
|
||||
/// mlir::Dialect and registers custom attributes, operations, and types (in its
|
||||
/// constructor). It can also override some general behavior exposed via virtual
|
||||
/// methods, which will be demonstrated in later chapters of the tutorial.
|
||||
class ToyDialect : public mlir::Dialect {
|
||||
public:
|
||||
explicit ToyDialect(mlir::MLIRContext *ctx);
|
||||
|
||||
// The return type for the operation: `!another_dialect<"other_type">`
|
||||
auto anotherDialectPrefix = mlir::Identifier::get("another_dialect", &context);
|
||||
auto returnType = mlir::OpaqueType::get(another_dialect_prefix,
|
||||
"custom_type", &context);
|
||||
state.addTypes(returnType);
|
||||
|
||||
|
||||
// Using a builder to create the operation and insert it where the builder
|
||||
// insertion point is currently set.
|
||||
Operation *customOperation = builder.createOperation(state);
|
||||
|
||||
// An operation is not an SSA value (unlike LLVM), because it can return
|
||||
// multiple SSA values, the resulting value can be obtained:
|
||||
Value *result = customOperation->getResult(0);
|
||||
/// Provide a utility accessor to the dialect namespace. This is used by
|
||||
/// several utilities.
|
||||
static llvm::StringRef getDialectNamespace() { return "toy"; }
|
||||
};
|
||||
```
|
||||
|
||||
This approach is used in `Ch2/mlir/MLIRGen.cpp` to implement a naive MLIR
|
||||
generation through a simple depth-first search traversal of the Toy AST. Here is
|
||||
how we create a `toy.transpose` operation:
|
||||
The dialect can now be registered in the global registry:
|
||||
|
||||
```c++
|
||||
mlir::Operation *createTransposeOp(OpBuilder &builder,
|
||||
mlir::Value *input_tensor) {
|
||||
// Fill the `OperationState` with the required fields.
|
||||
mlir::OperationState result(location, "toy.transpose");
|
||||
result.addOperands(input_tensor);
|
||||
mlir::registerDialect<ToyDialect>();
|
||||
```
|
||||
|
||||
// We use the MLIR tensor type for 'toy' types.
|
||||
auto type = builder.getTensorType({2, 2}, builder.getF64Type());
|
||||
result.addTypes(type);
|
||||
Any new `MLIRContext` created from now on will contain an instance of the Toy
|
||||
dialect, and invoke specific hooks for things like parsing attributes and types.
|
||||
|
||||
// Create the transpose operation.
|
||||
Operation *newTransposeOp = builder->createOperation(result);
|
||||
return newTransposeOp;
|
||||
## Defining Toy Operations
|
||||
|
||||
Now that we have a `Toy` dialect, we can start registering operations. This will
|
||||
allow for providing semantic information that the rest of the system can hook
|
||||
into. Let's walk through the creation of the `toy.constant` operation:
|
||||
|
||||
```mlir
|
||||
%4 = "toy.constant"() {value = dense<1.0> : tensor<2x3xf64>} : () -> tensor<2x3xf64>
|
||||
```
|
||||
|
||||
This operation takes zero operands, a
|
||||
[dense elements](../../LangRef.md#dense-elements-attribute) attribute named
|
||||
`value`, and returns a single result of
|
||||
[TensorType](../../LangRef.md#tensor-type). An operation inherits from the
|
||||
[CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)
|
||||
`mlir::Op` class which also takes some optional *traits* to customize its
|
||||
behavior. These traits may provide additional accessors, verification, etc.
|
||||
|
||||
```c++
|
||||
class ConstantOp : public mlir::Op<ConstantOp,
|
||||
/// The ConstantOp takes zero inputs.
|
||||
mlir::OpTrait::ZeroOperands,
|
||||
/// The ConstantOp returns a single result.
|
||||
mlir::OpTrait::OneResult,
|
||||
/// The ConstantOp is pure and has no visible side-effects.
|
||||
mlir::OpTrait::HasNoSideEffect> {
|
||||
|
||||
public:
|
||||
/// Inherit the constructors from the base Op class.
|
||||
using Op::Op;
|
||||
|
||||
/// Provide the unique name for this operation. MLIR will use this to register
|
||||
/// the operation and uniquely identify it throughout the system.
|
||||
static llvm::StringRef getOperationName() { return "toy.constant"; }
|
||||
|
||||
/// Return the value of the constant by fetching it from the attribute.
|
||||
mlir::DenseElementsAttr getValue();
|
||||
|
||||
/// Operations can provide additional verification beyond the traits they
|
||||
/// define. Here we will ensure that the specific invariants of the constant
|
||||
/// operation are upheld, for example the result type must be of TensorType.
|
||||
LogicalResult verify();
|
||||
|
||||
/// Provide an interface to build this operation from a set of input values.
|
||||
/// This interface is used by the builder to allow for easily generating
|
||||
/// instances of this operation:
|
||||
/// mlir::OpBuilder::create<ConstantOp>(...)
|
||||
/// This method populates the given `state` that MLIR uses to create
|
||||
/// operations. This state is a collection of all of the discrete elements
|
||||
/// that an operation may contain.
|
||||
/// Build a constant with the given return type and `value` attribute.
|
||||
static void build(mlir::Builder *builder, mlir::OperationState &state,
|
||||
mlir::Type result, mlir::DenseElementsAttr value);
|
||||
/// Build a constant and reuse the type from the given 'value'.
|
||||
static void build(mlir::Builder *builder, mlir::OperationState &state,
|
||||
mlir::DenseElementsAttr value);
|
||||
/// Build a constant by broadcasting the given 'value'.
|
||||
static void build(mlir::Builder *builder, mlir::OperationState &state,
|
||||
double value);
|
||||
};
|
||||
```
|
||||
|
||||
and we register this operation in the `ToyDialect` constructor:
|
||||
|
||||
```c++
|
||||
ToyDialect::ToyDialect(mlir::MLIRContext *ctx)
|
||||
: mlir::Dialect(getDialectNamespace(), ctx) {
|
||||
addOperations<ConstantOp>();
|
||||
}
|
||||
```
|
||||
|
||||
### Op vs Operation: Using MLIR Operations
|
||||
|
||||
Now that we have defined an operation, we will want to access and transform it.
|
||||
In MLIR, there are two main classes related to operations: `Operation` and `Op`.
|
||||
Operation is the actual opaque instance of the operation, and represents the
|
||||
general API into an operation instance. An `Op` is the base class of a derived
|
||||
operation, like `ConstantOp`, and acts as smart pointer wrapper around a
|
||||
`Operation*`. This means that when we define our Toy operations, we are actually
|
||||
providing a clean interface for building and interfacing with the `Operation`
|
||||
class; this is why our `ConstantOp` defines no class fields. Therefore, we
|
||||
always pass these classes around by-value, instead of by reference or pointer
|
||||
(passing by-value is a common idiom and applies similarly to attributes, types,
|
||||
etc). We can always get an instance of our toy operation by using LLVM's casting
|
||||
infrastructure:
|
||||
|
||||
```c++
|
||||
void processConstantOp(mlir::Operation *op) {
|
||||
ConstantOp op = llvm::dyn_cast<ConstantOp>(op);
|
||||
|
||||
// This operation is not an instance of `ConstantOp`.
|
||||
if (!op)
|
||||
return;
|
||||
|
||||
// Get the internal operation instance back.
|
||||
mlir::Operation *internalOp = op.getOperation();
|
||||
assert(internalOp == op && "these operation instances are the same");
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Operation Definition Specification (ODS) Framework
|
||||
|
||||
In addition to specializing the `mlir::Op` C++ template, MLIR also supports
|
||||
defining operations in a declarative manner. This is achieved via the
|
||||
[Operation Definition Specification](../../OpDefinitions.md) framework. Facts
|
||||
regarding an operation are specified concisely into a TableGen record, which
|
||||
will be expanded into an equivalent `mlir::Op` C++ template specialization at
|
||||
compile time. Using the ODS framework is the desired way for defining operations
|
||||
in MLIR given the simplicity, conciseness, and general stability in the face of
|
||||
C++ API changes.
|
||||
|
||||
Lets see how to define the ODS equivalent of our ConstantOp:
|
||||
|
||||
The first thing to do is to define a link to the Toy dialect that we defined in
|
||||
c++. This is used to link all of the operations that we will define, to our
|
||||
dialect:
|
||||
|
||||
```tablegen
|
||||
// Provide a definition of the 'toy' dialect in the ODS framework so that we
|
||||
// can define our operations.
|
||||
def Toy_Dialect : Dialect {
|
||||
// The namespace of our dialect, this corresponds 1-1 with the string we
|
||||
// provided in `ToyDialect::getDialectNamespace`.
|
||||
let name = "toy";
|
||||
|
||||
// The c++ namespace that the dialect class definition resides in.
|
||||
let cppNamespace = "toy";
|
||||
}
|
||||
```
|
||||
|
||||
Now that we have defined a link to the toy dialect, we can start defining
|
||||
operations. Operations in ODS are defined by inheriting from the `Op` class. To
|
||||
simplify our operation definitions, we will define a base class for operations
|
||||
in the Toy dialect.
|
||||
|
||||
```tablegen
|
||||
// Base class for toy dialect operations. This operation inherits from the base
|
||||
// `Op` class in OpBase.td, and provides:
|
||||
// * The parent dialect of the operation.
|
||||
// * The mnemonic for the operation, or the name without the dialect prefix.
|
||||
// * A list of traits for the operation.
|
||||
class Toy_Op<string mnemonic, list<OpTrait> traits = []> :
|
||||
Op<Toy_Dialect, mnemonic, traits>;
|
||||
```
|
||||
|
||||
With all of the preliminary pieces defined, we can begin to define the constant
|
||||
operation:
|
||||
|
||||
We define a toy operation by inheriting from our base 'Toy_Op' class above. Here
|
||||
we provide the mnemonic and a list of traits for the operation. The
|
||||
[mnemonic](../../OpDefinitions.md#operation-name) here matches the one given in
|
||||
`ConstantOp::getOperationName` without the dialect prefix; `toy.`. The constant
|
||||
operation here is also marked as 'NoSideEffect'. This is an ODS trait, and
|
||||
matches one-to-one with the trait we providing when defining `ConstantOp`:
|
||||
`mlir::OpTrait::HasNoSideEffect`. Missing here from our c++ definition are the
|
||||
`ZeroOperands` and `OneResult` traits, these will be automatically inferred
|
||||
based upon the `arguments` and `results` fields we define later.
|
||||
|
||||
```tablegen
|
||||
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
|
||||
}
|
||||
```
|
||||
|
||||
#### Defining Arguments and Results
|
||||
|
||||
With the shell of the operation defined, we can now provide the
|
||||
[inputs](../../OpDefinitions.md#operation-arguments) and
|
||||
[outputs](../../OpDefinitions.md#operation-results) to our operation. The
|
||||
inputs, or arguments, to an operation may be attributes or types for SSA operand
|
||||
values. The results correspond to a set of types for the values produced by the
|
||||
operation:
|
||||
|
||||
```tablegen
|
||||
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
|
||||
// The constant operation takes an attribute as the only input.
|
||||
// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.
|
||||
let arguments = (ins F64ElementsAttr:$value);
|
||||
|
||||
// The generic call operation returns a single value of TensorType.
|
||||
// F64Tensor corresponds to a 64-bit floating-point TensorType.
|
||||
let results = (outs F64Tensor);
|
||||
}
|
||||
```
|
||||
|
||||
By providing a name to the arguments or results, e.g. `$value`, ODS will
|
||||
automatically generate a matching accessor: `DenseElementsAttr
|
||||
ConstantOp::value()`.
|
||||
|
||||
#### Adding Documentation
|
||||
|
||||
The next step after defining the operation, is to document it. Operations may
|
||||
provide
|
||||
[`summary` and `description`](../../OpDefinitions.md#operation-documentation)
|
||||
fields to describe the semantics of the operation. This information is useful
|
||||
for users of the dialect, and can even be used to auto-generate markdown
|
||||
documents.
|
||||
|
||||
```tablegen
|
||||
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
|
||||
// Provide a summary and description for this operation. This can be used to
|
||||
// auto-generate documenatation of the operations within our dialect.
|
||||
let summary = "constant operation";
|
||||
let description = [{
|
||||
Constant operation turns a literal into an SSA value. The data is attached
|
||||
to the operation as an attribute. For example:
|
||||
|
||||
%0 = "toy.constant"()
|
||||
{ value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }
|
||||
: () -> tensor<2x3xf64>
|
||||
}];
|
||||
|
||||
// The constant operation takes an attribute as the only input.
|
||||
// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.
|
||||
let arguments = (ins F64ElementsAttr:$value);
|
||||
|
||||
// The generic call operation returns a single value of TensorType.
|
||||
// F64Tensor corresponds to a 64-bit floating-point TensorType.
|
||||
let results = (outs F64Tensor);
|
||||
}
|
||||
```
|
||||
|
||||
#### Verifying Operation Semantics
|
||||
|
||||
At this point we've already covered a majority of the original c++ operation
|
||||
definition. The next piece to define is the verifier. Luckily, much like the
|
||||
named accessor, the ODS framework will automatically generate a lot of the
|
||||
necessary verification logic based upon the constraints we have given. This
|
||||
means that we don't need to verify the structure of the return type, or even the
|
||||
input attribute `value`. In many cases, additional verification is not even
|
||||
necessary for ODS operations. To add additional verification logic, an operation
|
||||
can override the [`verifier`](../../OpDefinitions.md#custom-verifier-code)
|
||||
field. The `verifier` field allows for defining a c++ code blob that will be run
|
||||
as part of ConstantOp::verify. This blob can assume that all of the other
|
||||
invariants of the operation have already been verified:
|
||||
|
||||
```tablegen
|
||||
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
|
||||
// Provide a summary and description for this operation. This can be used to
|
||||
// auto-generate documenatation of the operations within our dialect.
|
||||
let summary = "constant operation";
|
||||
let description = [{
|
||||
Constant operation turns a literal into an SSA value. The data is attached
|
||||
to the operation as an attribute. For example:
|
||||
|
||||
%0 = "toy.constant"()
|
||||
{ value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }
|
||||
: () -> tensor<2x3xf64>
|
||||
}];
|
||||
|
||||
// The constant operation takes an attribute as the only input.
|
||||
// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.
|
||||
let arguments = (ins F64ElementsAttr:$value);
|
||||
|
||||
// The generic call operation returns a single value of TensorType.
|
||||
// F64Tensor corresponds to a 64-bit floating-point TensorType.
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Add additional verification logic to the constant operation. Here we invoke
|
||||
// a static `verify` method in a c++ source file. This codeblock is executed
|
||||
// inside of ConstantOp::verify, so we can use `this` to refer to the current
|
||||
// operation instance.
|
||||
let verifier = [{ return ::verify(*this); }];
|
||||
}
|
||||
```
|
||||
|
||||
#### Attaching `build` Methods
|
||||
|
||||
The final missing component here from our original c++ example are the `build`
|
||||
methods. ODS can generate some simple build methods automatically, and in this
|
||||
case it will generate our first build method for us. For the rest, we define the
|
||||
[`builders`](../../OpDefinitions.md#custom-builder-methods) field. This field
|
||||
takes a list of `OpBuilder` objects that take a string corresponding to a list
|
||||
of c++ parameters, as well as a code block.
|
||||
|
||||
```tablegen
|
||||
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
|
||||
// Provide a summary and description for this operation. This can be used to
|
||||
// auto-generate documenatation of the operations within our dialect.
|
||||
let summary = "constant operation";
|
||||
let description = [{
|
||||
Constant operation turns a literal into an SSA value. The data is attached
|
||||
to the operation as an attribute. For example:
|
||||
|
||||
%0 = "toy.constant"()
|
||||
{ value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }
|
||||
: () -> tensor<2x3xf64>
|
||||
}];
|
||||
|
||||
// The constant operation takes an attribute as the only input.
|
||||
// `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr.
|
||||
let arguments = (ins F64ElementsAttr:$value);
|
||||
|
||||
// The generic call operation returns a single value of TensorType.
|
||||
// F64Tensor corresponds to a 64-bit floating-point TensorType.
|
||||
let results = (outs F64Tensor);
|
||||
|
||||
// Add additional verification logic to the constant operation. Here we invoke
|
||||
// a static `verify` method in a c++ source file. This codeblock is executed
|
||||
// inside of ConstantOp::verify, so we can use `this` to refer to the current
|
||||
// operation instance.
|
||||
let verifier = [{ return ::verify(*this); }];
|
||||
|
||||
// Add custom build methods for the constant operation. These method populates
|
||||
// the `state` that MLIR uses to create operations, i.e. these are used when
|
||||
// using `builder.create<ConstantOp>(...)`.
|
||||
let builders = [
|
||||
// Build a constant with a given constant tensor value.
|
||||
OpBuilder<"Builder *builder, OperationState &result, "
|
||||
"DenseElementsAttr value", [{
|
||||
build(builder, result, value.getType(), value);
|
||||
}]>,
|
||||
|
||||
// Build a constant with a given constant floating-point value. This builder
|
||||
// invokes a static `buildConstantOp` utility function in a c++ source file
|
||||
// to keep the tablegen c++ code blocks simple.
|
||||
OpBuilder<"Builder *builder, OperationState &result, double value", [{
|
||||
buildConstantOp(builder, result, value);
|
||||
}]>
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Above we introduce several of the concepts for defining operations in the ODS
|
||||
framework, but there are many more that we haven't had a chance to: regions,
|
||||
variadic operands, etc. Check out the
|
||||
[full specification](../../OpDefinitions.md) for more details.
|
||||
|
||||
## Complete Toy Example
|
||||
|
||||
At this point we can already generate our "Toy IR" without having registered
|
||||
anything with MLIR. A simplified version of the previous example:
|
||||
At this point we can generate our "Toy IR". A simplified version of the previous
|
||||
example:
|
||||
|
||||
```Toy {.toy}
|
||||
```.toy
|
||||
# User defined generic function that operates on unknown shaped arguments.
|
||||
def multiply_transpose(a, b) {
|
||||
return a * transpose(b);
|
||||
|
@ -172,9 +514,9 @@ def main() {
|
|||
|
||||
Results in the following IR:
|
||||
|
||||
```MLIR(.mlir)
|
||||
```mlir
|
||||
module {
|
||||
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
|
||||
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>) -> tensor<*xf64>
|
||||
attributes {toy.generic} {
|
||||
%0 = "toy.transpose"(%arg1) : (tensor<*xf64>) -> tensor<*xf64> loc("test/codegen.toy":3:14)
|
||||
%1 = "toy.mul"(%arg0, %0) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64> loc("test/codegen.toy":3:14)
|
||||
|
@ -198,25 +540,6 @@ You can build `toyc-ch2` and try yourself: `toyc-ch2 test/codegen.toy -emit=mlir
|
|||
test/codegen.toy -emit=mlir -mlir-print-debuginfo 2> codegen.mlir` followed by
|
||||
`toyc-ch2 codegen.mlir -emit=mlir`.
|
||||
|
||||
At this point MLIR does not know anything about Toy, so there are no semantics
|
||||
associated with the operations, everything is opaque and string-based. The only
|
||||
thing enforced by MLIR here is that the IR is in SSA form: values are defined
|
||||
once, and uses appear after their definition.
|
||||
|
||||
This can be observed by crafting what should be an invalid IR for Toy and see it
|
||||
round-trip without tripping the verifier:
|
||||
|
||||
```MLIR(.mlir)
|
||||
// RUN: toyc %s -emit=mlir
|
||||
|
||||
func @main() {
|
||||
%0 = "toy.print"() : () -> tensor<2x3xf64>
|
||||
}
|
||||
```
|
||||
|
||||
There are multiple problems here: the `toy.print` operation is not a terminator,
|
||||
it should take an operand, and it shouldn't return any values.
|
||||
|
||||
In the [next chapter](Ch-3.md) we will register our dialect and operations with
|
||||
MLIR, plug into the verifier, and add nicer APIs to manipulate our operations.
|
||||
|
||||
At this point MLIR knows about our Toy dialect and operations. In the
|
||||
[next chapter](Ch-3.md) we will leverage our new dialect to implement some
|
||||
high-level language-specific analyses and transformations for the Toy language.
|
||||
|
|
|
@ -967,6 +967,24 @@ class IntElementsAttr<int width> : ElementsAttrBase<
|
|||
def I32ElementsAttr : IntElementsAttr<32>;
|
||||
def I64ElementsAttr : IntElementsAttr<64>;
|
||||
|
||||
class FloatElementsAttr<int width> : ElementsAttrBase<
|
||||
CPred<"$_self.isa<DenseFPElementsAttr>() &&"
|
||||
"$_self.cast<DenseElementsAttr>().getType()."
|
||||
"getElementType().isF" # width # "()">,
|
||||
width # "-bit float elements attribute"> {
|
||||
|
||||
let storageType = [{ DenseElementsAttr }];
|
||||
let returnType = [{ DenseElementsAttr }];
|
||||
|
||||
// Note that this is only constructing scalar elements attribute.
|
||||
let constBuilderCall = "DenseElementsAttr::get("
|
||||
"$_builder.getTensorType({}, $_builder.getF" # width # "Type()),"
|
||||
"llvm::makeArrayRef($0))";
|
||||
let convertFromStorage = "$_self";
|
||||
}
|
||||
|
||||
def F64ElementsAttr : FloatElementsAttr<64>;
|
||||
|
||||
// A `width`-bit floating point elements attribute. The attribute should be
|
||||
// ranked and has a shape as specified in `dims`.
|
||||
class RankedFloatElementsAttr<int width, list<int> dims> : ElementsAttrBase<
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
// RUN: toyc-ch2 %s -emit=mlir 2>&1
|
||||
// RUN: not toyc-ch2 %s -emit=mlir 2>&1
|
||||
|
||||
|
||||
// This IR is not "valid":
|
||||
// The following IR is not "valid":
|
||||
// - toy.print should not return a value.
|
||||
// - toy.print should take an argument.
|
||||
// - There should be a block terminator.
|
||||
// This all round-trip since this is opaque for MLIR.
|
||||
func @main() {
|
||||
%0 = "toy.print"() : () -> !toy.array<2, 3>
|
||||
%0 = "toy.print"() : () -> tensor<2x3xf64>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# RUN: toyc-ch2 %s -emit=mlir 2>&1 | FileCheck %s
|
||||
|
||||
def main() {
|
||||
var a<2, 2> = 5.5;
|
||||
print(a);
|
||||
}
|
||||
|
||||
# CHECK-LABEL: func @main() {
|
||||
# CHECK-NEXT: %0 = "toy.constant"() {value = dense<5.500000e+00> : tensor<f64>} : () -> tensor<f64>
|
||||
# CHECK-NEXT: %1 = "toy.reshape"(%0) : (tensor<f64>) -> tensor<2x2xf64>
|
||||
# CHECK-NEXT: "toy.print"(%1) : (tensor<2x2xf64>) -> ()
|
||||
# CHECK-NEXT: "toy.return"() : () -> ()
|
||||
# CHECK-NEXT: }
|
||||
|
Loading…
Reference in New Issue