MLIR Beginner: MLIR backend for custom accelerator

Hello! I am trying to get accustomed to MLIR in order to be able to use it to generate code for a custom accelerator.

Currently I am running over Chapter 2: Emitting Basic MLIR - MLIR and figuring out the code.

While the tutorial covers well the toy dialect construction, ops definitions and tablegen, I am trying to learn the MLIR API , i.e. the way the methods build(), parse() and verify() are defined in Dialect.cpp and the generation of IR code from MLIRGen.cpp.

Basically I am looking for a method to learn the MLIR API, what each class does and where to use it.

My scope would be to define a dialect from MLIR then to lower it into binary representation. How one can ‘learn’ the MLIR, is there are systematic approach method?

I would like to know what would be the right approach to accomplish my goal, given that I am completely new to MLIR development.

TIA!

The code for the tutorial is in the repo, did you build it, modify it, extend it?
There should enough there to cover a quite significant part of the API surface I think.

Yes, I’ve built it, I’m debugging it to understand how it works, so the next step would be to then a new operator to the toy language?

You mentioned you’re running over Chapter 2, so I would say the next step is chapter 3 :wink:
Probably worth completing a few of the chapters before trying to modify the code too heavily. Instead of adding operator, doing some change is an easier start probably.

Hi again, thanks for the feedback so far.

Currently I have defined several traversal procedures more or less like the example in https://mlir.llvm.org/docs/Tutorials/UnderstandingTheIRStructure/ , on the MLIR structure.

At an Operation I have defined an iterator over the results, and by calling “llvm::outs() << op->getResult(idx)” I am getting something like

“%0 = onnx.Constant dense<[[[[0.0632363558, 0.16975446, …]]]]>”

Being able to get the attribute using attr.dyn_cast(), my question now is how to get the naming of the result, that is the “%0” in the stringified state of the Operation above?

And, in general, what’s the best way to get the results, attributes, operands for a concrete operator, given its MLIR interface pointer (as in the tutorial)? I.e. how to determine whether is in fact a specific implementation of an operation, and access its content?

This is a bit of a complicated thing but values are technically not named in MLIR (unless you give them a name hint? But I’m not sure, and in any case this is rarely done). The name in the text format is picked at the very end by the printer. Why are you interested in getting it?

When you have an instance of an operation interface, I believe it also has the methods of the Operation type, so you can just use them directly. There is also getOperation() that will return a generic Operation* for any given specific instance of an operation.

If you have an Operation *op, you have access to the results op->getResults(), the discardable attributes op->getDiscardableAttributes(), the inherent attributes (the attributes defined in TableGen): op->getInherentAttributes(), the operands op->getOperands().
Look at the class for the complete list of accessors (and mutators).

isa/cast/dyn_cast: ODS generates a C++ class for the operation you define, assuming you have a OnnxConstantOp C++ class you can do:

OnnxConstantOp onnxConstant = dyn_cast<OnnxConstantOp>(op);
if (!onnxConstant) {
  // `op` was an instance of `OnnxConstantOp `, and `onnxConstant` allows direct access through the `OnnxConstantOp` class methods.
} else {
   // `op` isn't what you're looking for...
}

Thank you.

I am working on a PatternReplacer now and I would need some kind of reference guide on how to create a tensor within an Attribute; I understand that this is a kind of DenseElementsAttr, but it would help much to have a proper way of working with these kind of attributes. Currently I’m doing something like:

template <typename T>
static std::unique_ptr<OnnxTensor<T>> getTensorFromAttribute(mlir::Attribute const& attribute) {
  auto disposableElemsAttr = attribute.dyn_cast<DisposableElementsAttr>();
  if (!disposableElemsAttr)
    throw std::runtime_error("Attribute is not a DisposableElementsAttr");

  auto denseElemsAttr = disposableElemsAttr.toDenseElementsAttr();
  if (!denseElemsAttr)
    throw std::runtime_error("Cannot convert DisposableElementsAttr to DenseElementsAttr");

  auto intOrFpElementsAttr = llvm::dyn_cast<DenseIntOrFPElementsAttr>(denseElemsAttr);
  if (!intOrFpElementsAttr)
    throw std::runtime_error("DenseElementsAttr is not a DenseIntOrFPElementsAttr");

  // create the tensor
  std::unique_ptr<OnnxTensor<T>> tensor = std::unique_ptr<OnnxTensor<T>>(new OnnxTensor<T>());

  // get the shape
  mlir::ShapedType shapedType = intOrFpElementsAttr.getType();
  getShapeFromShapedType(shapedType, tensor->shape);

  // get the tensor values
  auto valueIt = intOrFpElementsAttr.value_begin<APFloat>();
  int64_t numElements = shapedType.getNumElements();

  for (unsigned idx = 0; idx < numElements; ++idx) {
    if (std::is_same<T, std::float_t>::value)
      tensor->value.push_back((*(valueIt + idx)).convertToFloat());
    else if (std::is_same<T, std::double_t>::value)
      tensor->value.push_back((*(valueIt + idx)).convertToDouble());
    else
      throw std::runtime_error("Type not supported in reading from APFloat array.");
  }

  return tensor;
}

It works, but I’m not sure it is the proper way …

It would help me to understand how to find out all types that can be used with dynamic_cast, i.e. from where can I figure out that an Attribute can actually be a DisposableElementsAttr (but not only).

All MLIR types and attributes can be used with dyn_cast. Best option is to check what you support and then error. If the question is more what types are allowed there ONNX side, then that’s a question the onnx-mlir folks would be best placed to answer.

Ok, thanks, I was thinking there is a designing pattern (i.e. look in the TableGen files, I guess).

Also, currently I have defined a walker. Now, let’s say I am traversing an Operation *op, and I’m getting that operand, as in Value operand = op->getOperand(/* idx */ 1). Is it possible to inspect that operand again, as an operation.

Currently I am getting its name from its location, operand.getLoc(), but I would like to get an Operation * pointer to that operand, to be able to search for its specific attributes.

Is it possible?

In MLIR operation operands may be either block arguments of results of operations.

You can use operand.getDefiningOp() to get the defining operation if it exists, otherwise, your operand is a block argument and you will get nullptr.

1 Like

Thanks! It works! I should have also looked more carefully into Chapter 3: High-level Language-Specific Analysis and Transformation - MLIR

Hi folks,

Thanks for the help so far.

I have another question, rather a design one: is it possible to mix dialects? I want to generate an intermediate MLIR representation that would use two dialects - some high-level operators will be replaced with lower-level ones from the new dialect, but some others would stay the same.

Would you please recommed such an example, if possible, for me to better understand how this should be done? I would prefer a simple example first.

Dialects are almost always mixed. If you go through the tutorial this should be visible.

Starting with the Func dialect that provides the ability to create a function and call functions. But also the SCF for things like loop structures and conditionals.
You can’t even write IR using the SCF dialect without using other dialects as well, so it’s not that you “can” mix them, you must!

The tests are usually good examples of all this, here for example: llvm-project/mlir/test/Dialect/SCF/loop-pipelining.mlir at main · llvm/llvm-project · GitHub

Currently I work on the ONNX MLIR and it looks painfully complicated to insert a dialect.

What I got stuck with is a simple problem: remove an operator by match and rewite. The code I have so far is:

LogicalResult matchAndRewrite(ONNXPadOp padOp, PatternRewriter& rewriter) const override {
        auto previous = padOp->getOperand(0);

        // get successor operations
        SmallVector<Operation *, 4> successors;
        for (Value result : padOp->getResults())
            for (Operation *user : result.getUsers())
                successors.push_back(user);

        rewriter.eraseOp(padOp);

        if (successors.empty())
            return success();

        for (Operation *successor : successors) {
            // redirect the output of the PadOp to the successor
            unsigned operandIndex = 0;
            for (unsigned i = 0; i < successor->getNumOperands(); i++) {
                if (successor->getOperand(i) == padOp->getResult(0)) {
                    operandIndex = i;
                    break;
                }
            }
            rewriter.updateRootInPlace(successor, [&]() {
                successor->setOperand(operandIndex, previous);
            });
        }

        return success();
    }

and it crashes within rewriter.eraseOp().

No matter where I put this rewriter.eraseOp(), beginning / end, it still throws a SIGSEGV signal.

( as a side note, ONNX MLIR looks unreasonably complex… )

It can be achived with single line:

rewriter.replaceOp(padOp, padOp->getOperand(0));

In your original code there is an UB - it uses padOp after the operation is erased (use-after-free).

Indeed, how obvious … thanks!