Can't convert memref to llvm.ptr

I need to convert a memref to llvm.ptr because I want to call a C function using the memref:

LogicalResult matchAndRewrite(Operation *op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter) const final {
...
ArrayRef<Value> args({operands[0]}); // in operands[0] is where the memref is
rewriter.create<mlir::CallOp>(loc, function, results, args);

However, I get the error:

op operand type mismatch for operand 0: '!llvm.struct<(ptr<f64>, ptr<f64>, i64, array<2 x i64>, array<2 x i64>)>' != '!llvm.ptr<f64>'

Because my function expects a double * but operands[0] is a llvm.struct. Doc says (Conversion to the LLVM Dialect - MLIR) that when a memref has to be converted to LLVM, there are two ways:

  • default convention
  • bare pointer

Without further action, MLIR uses the default convention, but for my use case I need to use the bare pointer. In the doc says “The choice between conventions is specified at type converter construction time and is often exposed as an option by conversion passes”. That makes sense. In my code I have:

mlir::LowerToLLVMOptions opts(&getContext());
opts.useBarePtrCallConv = true;
LLVMTypeConverter typeConverter(&getContext(), opts);
...
populateStdToLLVMConversionPatterns(typeConverter, patterns);
...
patterns.add<DummyOpLowering>(&getContext());

So I set useBarePtrCallConv to true to say “hey, please use the bare pointer method”. However, in the DummyOpLowering (just my example) I still get the first form instead of the bare pointer I need.

What further actions do I need to perform to get the bare pointer to work? I have read everything on the documentation and the doxygen webpages but still no luck.

“Bare pointer” is a calling convention. It means that it will be used when converting call, indirect_call and func operations, all the other operations will use the normal descriptor. Before/after calls, the pointer will be extracted from/inserted into the descriptor. If you only want to call a function that expects a bare pointer, you can do so with the convention (assuming your memref has a static shape and the default layout) because it’s exactly what it was meant for. Otherwise, you would need to write the entire conversion differently, e.g., I don’t know how to implement the reshape operation that guarantees no-copy semantics when the memref is only a pointer.

So if I understand you well, you mean that this “bare pointer” convention only appiles to call operations. Then, I thought that I could solve the issue just converting my mlir::CallOp. I did this:

class ConvertDummyCallToBare : public OpConversionPattern<mlir::CallOp> {
public:
  using OpConversionPattern::OpConversionPattern;
  LogicalResult matchAndRewrite(mlir::CallOp op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter) const override {
    if(op.getNumResults() == 0)
      rewriter.replaceOpWithNewOp<mlir::CallOp>(op, TypeRange(), operands);
    else
      rewriter.replaceOpWithNewOp<mlir::CallOp>(op, op.getType(0), operands);
    return success();
  }
};

I successfully recieve the CallOp to be retwritten but unfortunately, the operands are still in the default convention…

(gdb) p operands[5].dump()
%23 = llvm.insertvalue %8, %22[4, 1] : !llvm.struct<(ptr<f64>, ptr<f64>, i64, array<2 x i64>, array<2 x i64>)>

What have I done wrong? Have I misinterpreted your post?

It’s the converter of the call operation that handles the convention, here specifically llvm-project/StandardToLLVM.cpp at 2182eda3062471e2e6994307c46ffcca7e39ecff · llvm/llvm-project · GitHub. If you need a custom converter, it has to do the same as the default one, e.g. call promoteOperands. The operands field contains the operands as given by the operation that produce them, and those have no knowledge of the calling convention (neither they should for scalability reasons).

1 Like

Thank you very much @ftynse for your help but unfortunately it is not working yet. I’ve added the promoteOperands to my converter:

class ConvertDummyCallToBare : public OpConversionPattern<mlir::CallOp> {
public:
  using OpConversionPattern::OpConversionPattern;
  LogicalResult matchAndRewrite(mlir::CallOp op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter) const override {
    LLVMTypeConverter* tpConv = static_cast<LLVMTypeConverter *>(getTypeConverter());
    auto promoted = tpConv->promoteOperands(op.getLoc(), /*opOperands=*/op.getOperands(), operands, rewriter);
    if(op.getNumResults() == 0)
      rewriter.replaceOpWithNewOp<mlir::CallOp>(op, TypeRange(), promoted);
    else
      rewriter.replaceOpWithNewOp<mlir::CallOp>(op, op.getType(0), promoted);
    return success();
  }
};

But the promoted var still contains the memref in the default convention. gdb says:

(gdb) 
273	    LLVMTypeConverter* tpConv = static_cast<LLVMTypeConverter *>(getTypeConverter());
(gdb) n
274	    auto promoted = tpConv->promoteOperands(op.getLoc(), /*opOperands=*/op.getOperands(), operands, rewriter);
(gdb) p tpConv->getOptions().useBarePtrCallConv
$6 = true
(gdb) p operands[5].dump()
%23 = llvm.insertvalue %8, %22[4, 1] : !llvm.struct<(ptr<f64>, ptr<f64>, i64, array<2 x i64>, array<2 x i64>)>
$7 = void
(gdb) n
275	    if(op.getNumResults() == 0)
(gdb) p promoted[5].dump()
%23 = llvm.insertvalue %8, %22[4, 1] : !llvm.struct<(ptr<f64>, ptr<f64>, i64, array<2 x i64>, array<2 x i64>)>
$8 = void
(gdb) 

promoted and operands remain the same.

I’ve debugged the promoteOperands function and found that the memref is not recognised in this line: llvm-project/StandardToLLVM.cpp at main · llvm/llvm-project · GitHub because the dyn_cast fails, so it does nothing with the memref.

This is strange. Did you try inspecting the value that is being dyn_casted? If it is a memref, but dyn_cast fails, this sounds like a linking/registration problem.

I’m not sure if a !llvm.struct<… is considered a memref or not, but it is what the promoteOperands receives:

4019	      if (auto memrefType = operand.getType().dyn_cast<MemRefType>()) {
(gdb) p operand.dump()
%23 = llvm.insertvalue %8, %22[4, 1] : !llvm.struct<(ptr<f64>, ptr<f64>, i64, array<2 x i64>, array<2 x i64>)>
$6 = void

Moreover, it may be worth mentioning that in the rewrite pattern that I showed in my first post, the operand that the matchAndRewrite receives is already a !llvm.struct<. Furtheremore, I also call populateStdToLLVMConversionPatterns(llvmtypeConverter, patterns); in my runOnOperation() if that matters. Any ideas?

No, only a memref is a considered a memref as its name indicates…

In this code, operand should contain the operand before any conversion happens and hence retain the original type. Since the value it points to is of LLVM type, it appears that something else has already converted the operation that defines this operand to the LLVM dialect, and did so in a different run of the conversion infra. You don’t provide sufficient information for me to tell you where exactly it could have gone wrong, but it is likely around the conversion setup.

I don’t understand what exactly you are trying to achieve and why you need a custom pattern just to call a function. The existing pattern would have worked just fine. Writing a trivial wrapper in C that has a signature matching the one MLIR emits for memref in default conversion and forwarding its arguments to the C function you actually want to call would have also worked (this is what we do in mlir-cpu-runner runtime).

I’m adding a more detailed view of this so it’s easier for you to tackle the problem.

First, the runOnOperation:

void FooLoweringPass::runOnOperation() {
  LLVMConversionTarget target(getContext());
  target.addLegalOp<ModuleOp>();

  mlir::LowerToLLVMOptions opts(&getContext());
  opts.useBarePtrCallConv = true;
  LLVMTypeConverter typeConverter(&getContext(), opts);

  RewritePatternSet patterns(&getContext());
  patterns.add<DummyOpLowering>(&getContext());
  populateAffineToStdConversionPatterns(patterns);
  populateLoopToStdConversionPatterns(patterns);
  populateStdToLLVMConversionPatterns(typeConverter, patterns);

  dummyConvertToBare(typeConverter, patterns, target);

  auto module = getOperation();
  if (failed(applyFullConversion(module, target, std::move(patterns))))
    signalPassFailure();
}

Then, my custom DummyOpLowering:

struct DummyOpLowering : public ConversionPattern {
  DummyOpLowering(MLIRContext *ctx) : ConversionPattern(foo::DummyOp::getOperationName(), 1, ctx) {}

  LogicalResult matchAndRewrite(Operation *op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter) const final {
  ...
  ArrayRef<Value> args({operands[0]}); // in operands[0] is where the memref is
  // here I add lot of values to args
  rewriter.create<mlir::CallOp>(loc, function, results, args);
  }
};

Esentially, this takes an op from my dialect and converts it to a C call. Finally, the call conversion you suggested:

void dummyConvertToBare(LLVMTypeConverter &typeConverter, RewritePatternSet &patterns, ConversionTarget &target) {
  patterns.add<ConvertDummyCallToBare>(typeConverter, patterns.getContext());
}
class ConvertDummyCallToBare : public OpConversionPattern<mlir::CallOp> {
public:
  using OpConversionPattern::OpConversionPattern;
  LogicalResult matchAndRewrite(mlir::CallOp op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter) const override {
    LLVMTypeConverter* tpConv = static_cast<LLVMTypeConverter *>(getTypeConverter());
    auto promoted = tpConv->promoteOperands(op.getLoc(), /*opOperands=*/op.getOperands(), operands, rewriter);
    if(op.getNumResults() == 0)
      rewriter.replaceOpWithNewOp<mlir::CallOp>(op, TypeRange(), promoted);
    else
      rewriter.replaceOpWithNewOp<mlir::CallOp>(op, op.getType(0), promoted);
    return success();
  }
};

Here is the issue. If I debug my DummyOpLowering, there is what I see:

(gdb) p op->dump()
%67 = foo.dummy(%24 : memref<2x3xf64>) to memref<3x2xf64>
$1 = void
(gdb) p operands[0].dump()
%23 = llvm.insertvalue %8, %22[4, 1] : !llvm.struct<(ptr<f64>, ptr<f64>, i64, array<2 x i64>, array<2 x i64>)>
$2 = void

The operation says that it recieves a memref, but if I look at operands, the memref has already been converted to !llvm.struct

One additional remark. The first thing that gets called is DummyOpLowering and then, ConvertDummyCallToBare. The problem is that in DummyOpLowering the memref has already been converted, so in ConvertDummyCallToBare it’s too late to tell MLIR: “convert this memref to bare ptr” because it is not a memref any more.

I need a custom pattern (DummyOpLowering) because my op receives one memref but the C function call expects 8 arguments, and I need to manually create them.

This is something I have thought of, but I honestly prefer to call the C function directly from MLIR.

Ok, I’ve solved the problem. Maybe this is not the most elegant solution, but it definitely works.

Instead of passing operands[0] as the Value to the function call, I get the operand from the op, which is not converted yet, and is still a memref:

struct DummyOpLowering : public ConversionPattern {
  DummyOpLowering(MLIRContext *ctx) : ConversionPattern(foo::DummyOp::getOperationName(), 1, ctx) {}

  LogicalResult matchAndRewrite(Operation *op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter) const final {
  ...
  Value memref_value = op->getOperand(0);
  ArrayRef<Value> args({memref_value});
  // here I add lot of values to args
  rewriter.create<mlir::CallOp>(loc, function, results, args);
  }
};

Then, probably because I already have opts.useBarePtrCallConv = true, even without the ConvertDummyCallToBare, the memref gets converted to llvm.ptr automatically. Zinenko, thank you very much for your support. Let me know if you find this a convenient solution or not. Even though the solution was a bit, let’s say, “naive”, I learned a lot in this process.

So, operands always contains the converted values, whereas op.getOperands() gives you the original values. This duplication is there precisely to allow the pattern to inspect the types of the original values in situations like this. You are not expected to use op.getOperands() when constructing new operations because op may be scheduled to get deleted after the conversion completes (the conversion doesn’t erase ops immediately because you may need to inspect their results later). This may lead to unintended consequences, i.e., the operation that produces the operands will persist in the IR alongside the converted operation, or the infrastructure will complain/assert when it will try actually erase the operation that defines the value for which the pattern created a new use. I think that your solution will likely work today, but may randomly stop working if we change the infrastructure.

The root issue is that you are creating the call operation in another pattern (DummyOpLowering) rather than accepting it in the input. In that pattern, the values of operands will have had their types converted, either because the operations producing those values have been converted or because of materialization. Then, another pattern ConvertDummyCallToBare looks at the newly created call operation and sees its operands which are exactly the values used in DummyOpLowering when constructing the call operation. Technically, your approach will work as long as ConvertDummyCallToBare always runs after DummyOpLowering and cleans up the call operation that keeps using the original operands, and that the operation producing those operands survives until that point (which is the case today because operations are only erased after pattern application has finished). A slightly more involved, but future-proof approach is as follows. In DummyOpLowering you have the information which operand used to be a memref. Instead of giving the original operand to call, take operands[0] (or whatever is the corresponding converted operand) and convert it back to memref using DialectCastOp. Then you can use the result of the cast as operand of the call and the following patterns will see it as memref. The infra is set up to clean up back-and-forth DialectCastOp so it will get deleted before the conversion ends.

On a tangential point, your pattern set has two patterns that both match CallOp and have the same benefit. While we the infra does apply patterns in a specific order, it is not recommended to rely on it.

More generally, I heavily discourage putting all these patterns together. This leads to spurious assumptions about the operations and overall poor composability. There are reasons why “affine-to-scf” and “loop-to-std” are separate passes. I would have implemented your conversion as a separate pass that only converts DummyOp to LLVM::CallOp with appropriate casts, and then just call the “std-to-llvm” conversion pass.

Also nit: if your ops are defined using ODS, there is a DummyOpAdaptor that you can construct from ArrayRef<Value> operands and that gives you a nicer way to access operands than indexing the flat list with magic numbers.