Skip to content

mlir: add an operation to EmitC for function template instantiation #100895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

rohany
Copy link
Contributor

@rohany rohany commented Jul 27, 2024

This commit adds an emitc.instantiate_function_template operation to allow for the expression of function template instantiation. Without this operation, there is no easy way to express a C++ program like:

auto x = ...;
auto y = ...;
const void* fptr = &f<decltype(x), decltype(y)>;

Doing so is necessary to generate code that interacts with some lower level APIs for launching parallel work into runtime systems.

@llvmbot
Copy link
Member

llvmbot commented Jul 27, 2024

@llvm/pr-subscribers-mlir-core
@llvm/pr-subscribers-mlir-emitc

@llvm/pr-subscribers-mlir

Author: Rohan Yadav (rohany)

Changes

This commit adds an emitc.instantiate_function_template operation to allow for the expression of function template instantiation. Without this operation, there is no easy way to express a C++ program like:

auto x = ...;
auto y = ...;
const void* fptr = &amp;f&lt;decltype(x), decltype(y)&gt;;

Doing so is necessary to generate code that interacts with some lower level APIs for launching parallel work into runtime systems.


Full diff: https://github.com/llvm/llvm-project/pull/100895.diff

3 Files Affected:

  • (modified) mlir/include/mlir/Dialect/EmitC/IR/EmitC.td (+15)
  • (modified) mlir/lib/Target/Cpp/TranslateToCpp.cpp (+26)
  • (modified) mlir/test/Dialect/EmitC/ops.mlir (+6)
diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index 452302c565139..83f2e14d9407d 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -1260,5 +1260,20 @@ def EmitC_SubscriptOp : EmitC_Op<"subscript", []> {
   let assemblyFormat = "$value `[` $indices `]` attr-dict `:` functional-type(operands, results)";
 }
 
+def EmitC_InstantiateFunctionTemplateOp : EmitC_Op<"instantiate_function_template", []> {
+  let summary = "Instantiate template operation";
+  let description = [{
+    Instantiate a function template with a given set of types
+    (given by the values as argument to this operation) to obtain
+    a function pointer.
+  }];
+  let arguments = (ins
+    Arg<StrAttr, "the C++ function to instantiate">:$callee,
+    Variadic<EmitCType>:$args
+  );
+  let results = (outs EmitC_PointerType);
+  let assemblyFormat = "$callee `(` $args `)` attr-dict `:` functional-type($args, results)";
+}
+
 
 #endif // MLIR_DIALECT_EMITC_IR_EMITC
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index 626638282efe1..60c5c7fb906fb 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -656,6 +656,30 @@ static LogicalResult printOperation(CppEmitter &emitter,
   return success();
 }
 
+static LogicalResult
+printOperation(CppEmitter &emitter,
+               emitc::InstantiateFunctionTemplateOp instOp) {
+
+  raw_ostream &os = emitter.ostream();
+  Operation &op = *instOp.getOperation();
+
+  if (failed(emitter.emitAssignPrefix(op)))
+    return failure();
+  os << "&" << instOp.getCallee() << "<";
+
+  auto emitArgs = [&](mlir::Value val) -> LogicalResult {
+    os << "decltype(";
+    if (failed(emitter.emitOperand(val)))
+      return failure();
+    os << ")";
+    return success();
+  };
+  if (failed(interleaveCommaWithError(instOp.getArgs(), os, emitArgs)))
+    return failure();
+  os << ">";
+  return success();
+}
+
 static LogicalResult printOperation(CppEmitter &emitter,
                                     emitc::ApplyOp applyOp) {
   raw_ostream &os = emitter.ostream();
@@ -1508,6 +1532,8 @@ LogicalResult CppEmitter::emitOperation(Operation &op, bool trailingSemicolon) {
           .Case<func::CallOp, func::FuncOp, func::ReturnOp>(
               [&](auto op) { return printOperation(*this, op); })
           .Case<emitc::LiteralOp>([&](auto op) { return success(); })
+          .Case<emitc::InstantiateFunctionTemplateOp>(
+              [&](auto op) { return printOperation(*this, op); })
           .Default([&](Operation *) {
             return op.emitOpError("unable to find printer for op");
           });
diff --git a/mlir/test/Dialect/EmitC/ops.mlir b/mlir/test/Dialect/EmitC/ops.mlir
index 51c484a633eec..9ddcb7b539a48 100644
--- a/mlir/test/Dialect/EmitC/ops.mlir
+++ b/mlir/test/Dialect/EmitC/ops.mlir
@@ -224,6 +224,12 @@ func.func @test_subscript(%arg0 : !emitc.array<2x3xf32>, %arg1 : !emitc.ptr<i32>
   return
 }
 
+func.func @test_instantiate_template() {
+  %c1 = "emitc.constant"() <{value = 7 : i32}> : () -> i32
+  %0 = emitc.instantiate_function_template "func_template"(%c1) : (i32) -> !emitc.ptr<!emitc.opaque<"void">>
+  return
+}
+
 emitc.verbatim "#ifdef __cplusplus"
 emitc.verbatim "extern \"C\" {"
 emitc.verbatim "#endif  // __cplusplus"

@rohany
Copy link
Contributor Author

rohany commented Jul 27, 2024

Requesting a review from @simon-camp @aniragil, folks who have touched emitc recently.

cc @christopherbate

def EmitC_InstantiateFunctionTemplateOp : EmitC_Op<"instantiate_function_template", []> {
let summary = "Instantiate template operation";
let description = [{
Instantiate a function template with a given set of types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please give an example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

a function pointer.
}];
let arguments = (ins
Arg<StrAttr, "the C++ function to instantiate">:$callee,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to derive the type from an SSA value? Why not instead use an ArrayAttr (of TypeAttr) directly here? Is there really a use case where it is difficult to infer that information? When you create this operation, you would could just have a builder with an API like b.create<InstatiateFunctionTemplateOp>(loc, TypeRange(args)) in order to create an ArrayAttr containing all the types.

Copy link
Contributor Author

@rohany rohany Jul 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where the types are known inside of emitc, this operation is not actually needed (use of verbatim and string formatting with a TypeRange would suffice). In the case that I'm considering, we need to derive the types from the SSA values themselves. For example, consider generating code using the CuTe C++ library. After a few layout transformations, only the C++ compiler knows the exact types of a CuTe object. In such a case, a decltype is needed to correctly instantiate the function template.

Variadic<EmitCType>:$args
);
let results = (outs EmitC_PointerType);
let assemblyFormat = "$callee `(` $args `)` attr-dict `:` functional-type($args, results)";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there would be an issue with instead using < ... > instead; did you already try that? This format makes the operation look like a "call" instead of an instantiation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -656,6 +656,30 @@ static LogicalResult printOperation(CppEmitter &emitter,
return success();
}

static LogicalResult
printOperation(CppEmitter &emitter,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing a test for this translation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do those tests live? I grepped around but couldnt find where the others are.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests are under llvm-project/mlir/test/Target/Cpp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, thanks

@rohany rohany force-pushed the emitc-instantiate-function-templates branch from 2372a35 to 4994322 Compare July 29, 2024 16:50
@llvmbot llvmbot added the mlir:core MLIR Core Infrastructure label Jul 29, 2024
This commit adds an `emitc.instantiate_function_template` operation to
allow for the expression of function template instantiation. Without
this operation, there is no easy way to express a C++ program like:
```
auto x = ...;
auto y = ...;
const void* fptr = &f<decltype(x), decltype(y)>;
```
Doing so is necessary to generate code that interacts with some lower
level APIs for launching parallel work into runtime systems.
@rohany rohany force-pushed the emitc-instantiate-function-templates branch from 4994322 to 4300427 Compare July 29, 2024 16:51
@aniragil
Copy link
Contributor

I wonder how this op relates to the existing call_opaque op, which already contains extensive support for calling template instantiations, including specifying SSA values as template arguments. It currently doesn't allow specifying the type of its SSA operands using decltype, but could probably be extended to do so. If so, we could perhaps treat template instantiation as a special case of call_opaque. If this is too much of a semantic stretch (as it is after all not a call), then it would be good if the new op uses the same format as call_opaque to the relevant extent.

Translates to the C++:
```c++
int32_t v1 = 7;
void* v2 = &func_template<decltype(v1)>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we separate the application of template instantiation from taking its address (which emitc has an op for)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but right now there isn't a way to use the returned value except taking a pointer of it, as call excepts a SymbolRefAttr.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add an call_indirect op, similar to the func dialect. But we would need to also add support for function types in the emitter (or add an generic auto type, but that might have problems with wrongly inferred types if used in other places).

OTOH, is it even legal to cast function pointers to void *?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH, is it even legal to cast function pointers to void *?

I think it's technically undefined because some kinds of hardware cannot support such a cast. However, the runtime library (CUTLASS) I'm trying to interact with only accepts void* function pointers for a particular function, so its not invalid either.

@rohany
Copy link
Contributor Author

rohany commented Jul 30, 2024

Unless we want to add a mode to call_opaque where no actual call is done, i think some separation is probably good. There isn't an easy way to disambiguate between the two uses based on the arguments only, as an empty list of arguments is reasonable for a function call.

@aniragil
Copy link
Contributor

aniragil commented Jul 31, 2024

Unless we want to add a mode to call_opaque where no actual call is done, i think some separation is probably good. There isn't an easy way to disambiguate between the two uses based on the arguments only, as an empty list of arguments is reasonable for a function call.

We could use something like an 'instantiate_only' unit attribute to disambiguate, it's the no-call semantic stretch that makes me somewhat uncomfortable with raising this option. OTOH, the new op would probably replicate eventually the (quite complex) capabilities of call_opaque, as call_opaque could also benefit from the ability to use decltype and the new op could benefit from providing literal types. This could lead to two very similar and quite complex ops separated mostly by their name. So I guess the more general question would be: can/should these ops share functionality or do we need to replicate? Unifying them into a single op is one way to do it, but there may be others.
@marbre, @simon-camp, @mgehre-amd what do you think?

@simon-camp
Copy link
Contributor

I agree that the ops should be modeled the same, but I don't know if unifying these into one op is a good design. As I understand the result type should be something callable (i.e. function type to start with) if only instantiation is desired.

@aniragil
Copy link
Contributor

aniragil commented Aug 2, 2024

I agree that the ops should be modeled the same, but I don't know if unifying these into one op is a good design. As I understand the result type should be something callable (i.e. function type to start with) if only instantiation is desired.

How about we go the other way around? We could separate the template instantiation part out of call_opaque into its own op and replace call_opaque ability to take attributes as arguments using emitc.constants and emitc.literals. IIUC, without these complex behaviors call_opaque is a simple call op and can be made obsolete by adding @simon-camp's suggested call_indirect op which takes pointers and callable values as its callee and a call_lib op which takes a string as its callee.

@rohany
Copy link
Contributor Author

rohany commented Aug 2, 2024

  1. Are we OK with a backwards breaking change to the dialect? @aniragil's suggestion would result in every emitc user of call_opaque having to change their code.
  2. Unifying the operations might save some code inside emitc, but it will likely require some duplication of code in any user of emitc using template arguments. They'll probably have to pass around their list of template function arguments to both the instantiate call and the call_indirect op, versus doing it in a single place like it is now.

@aniragil
Copy link
Contributor

aniragil commented Aug 2, 2024

  1. Are we OK with a backwards breaking change to the dialect? @aniragil's suggestion would result in every emitc user of call_opaque having to change their code.

Sorry, should have been more clear: I'm not suggesting to modify call_opaque but to declare it deprecated as-is and remove it after a (long as needed) grace period for downstream users to use the new ops. A similar process is planned for other emitc ops following the lvalue discussions. It's difficult to make progress otherwise.

  1. Unifying the operations might save some code inside emitc, but it will likely require some duplication of code in any user of emitc using template arguments. They'll probably have to pass around their list of template function arguments to both the instantiate call and the call_indirect op, versus doing it in a single place like it is now.

I'm not sure why that would be a problem, as the change means replacing each call_opaque with an instantiate_template immediately followed by a call_indirect, so any shared argument should be readily available.
Anyway, I'm actually less concerned about code duplication within EmitC (usually resolved using common functions) and more about functionality duplication within the dialect, especially complex ones. If unavoidable, we should make a (constant) effort to keep them as similar as possible to avoid confusing the users, but best is to avoid duplication where possible.

Another aspect is separating concerns: EmitC is used for various C variants, with templated-C++ being only one of. The call_indirect and call_lib ops would also serve C, non-templated C++, and hopefully other variants later on (e.g. OpenCL-C, CUDA). The current call_opaque therefore mixes C++-only template-instantiation aspects with more general function-call aspects, which separating them seems to resolve.

Another advantage of separating instantiation from the call is the ability to reuse the same template instance for multiple calls. This may be less useful for decltype-based instantiation but literal type instantiations may benefit from it.

@rohany
Copy link
Contributor Author

rohany commented Aug 5, 2024

So is the consensus here to

  1. add function types / function pointers
  2. add a call_indirect op that accepts a value of a function type
  3. add a call_lib op that accepts a string as the callee (I'm not sure how this differs much from the current call_opaque)
  4. add a instantiate_template op that has a similar set of arguments as the current call_opaque that returns a value of a function type. instantiate_template would accept a string as the callee
  5. deprecate call_opaque

I could embark on this, but I want to make sure that everyone is on board before starting any of the work.

@marbre
Copy link
Member

marbre commented Aug 6, 2024

So is the consensus here to

  1. add function types / function pointers
  2. add a call_indirect op that accepts a value of a function type
  3. add a call_lib op that accepts a string as the callee (I'm not sure how this differs much from the current call_opaque)
  4. add a instantiate_template op that has a similar set of arguments as the current call_opaque that returns a value of a function type. instantiate_template would accept a string as the callee
  5. deprecate call_opaque

I could embark on this, but I want to make sure that everyone is on board before starting any of the work.

Sorry for not following up earlier. Isn't call_lib essentially the same as call_opaque with the lack of support for template arguments? In that case I think I would choose a different name instead of call_lib or even just keep the current name call_opaque. The disadvantage is that it is not as clear that the behavior will change as it would be with having a differently named op, but I favor this over introducing ops with not that clear names.

Generally speaking, yes we try to keep the dialect as stable as possible and therefore do not make us the decision to change it easy. With other words we try to avoid breaking changes. However, if evolvement is strictly required and we reached consensus we can go forward also with breaking changes.

@aniragil
Copy link
Contributor

aniragil commented Aug 6, 2024

Sorry for not following up earlier. Isn't call_lib essentially the same as call_opaque with the lack of support for template arguments? In that case I think I would choose a different name instead of call_lib or even just keep the current name call_opaque. The disadvantage is that it is not as clear that the behavior will change as it would be with having a differently named op, but I favor this over introducing ops with not that clear names.

Then how about we extend emitc.call to accept string attrs, callable values and function pointers as callees in addition to symbols? Would that be enough to drop emitc.call_opaque?

@marbre
Copy link
Member

marbre commented Aug 6, 2024

Sorry for not following up earlier. Isn't call_lib essentially the same as call_opaque with the lack of support for template arguments? In that case I think I would choose a different name instead of call_lib or even just keep the current name call_opaque. The disadvantage is that it is not as clear that the behavior will change as it would be with having a differently named op, but I favor this over introducing ops with not that clear names.

Then how about we extend emitc.call to accept string attrs, callable values and function pointers as callees in addition to symbols? Would that be enough to drop emitc.call_opaque?

I think we don't should go the way to extend options to much, which hasn't worked that well in the past. I think we should rather try to add new functionality with adding new ops. Therefore, I am in favor of adding an emitc.call_indriect similar to func.call_indirect and not add this to emitc.call.
My complains were more about not being happy about the naming of a new emitc.call_lib. For me it would fine if we keep the name emitc.call_opaque but move (and extend!) the template args / instantiation to emitc.instantiate_template. I am aware that we get a emitc.call_opaque with changed functionality behind but we had a similar situation with renaming the former emitc.call op to emitc.call_opaque and introducing a entirely new emitc.call op. If we really look for a new name emitc.opaque_call could work but I think I am still in favor of keeping an op named emitc.call_opaque. Hope this makes it clearer :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
mlir:core MLIR Core Infrastructure mlir:emitc mlir
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants