RFC Emitting FuncOp as C++ function calls

122 views
Skip to first unread message

Jacques Pienaar

unread,
Nov 26, 2019, 7:02:11 PM11/26/19
to MLIR

Hey,


I wanted to propose adding a C++ function call emitter that takes as input a FuncOp and emits C++ function calls matching the names of the Ops inside the function. E.g., 


func @test_foo_print() {

  %0 = "foo.constant"() {value = dense<[0, 1]> : tensor<2xi32>} : () -> (i32)

  "foo.print"(%0) : (i32) -> ()

  return

}


func @test_single_return(%arg0 : i32) -> i32 {

  return %arg0 : i32

}


func @test_multiple_return() -> (i32, i32) {

  %0:2 = "foo.blah"() : () -> (i32, i32)

  return %0#0, %0#1 : i32, i32

}

And produces [modulo formatting and customization for emitting function/op names]


static void test_foo_print() {

  int32_t v1;

  v1 = foo::constant(/*value=*/{0, 1});

  foo::print(v1);

}


static int32_t test_single_return(int32_t v2) { return v2; }


static std::tuple<int32_t, int32_t> test_multiple_return() {

  int32_t v3;

  int32_t v4;

  std::tie(v3, v4) = foo::blah();

  return std::make_tuple(v3, v4);

}

This is a utility class parameterized on Ops, Types, attributes & function/op name emitting (e.g., dialect emitted as namespace or something else) so that the default lowering of an Op is a call to a similarly named function with first its operands, then a list of attributes (in alphabetical order), then regions (potentially emitted as lambdas). The above would the default behavior, but the intention is to make it parameterizable with the defaults, say ordering, a helper method with which to parametrize the output. Multiple result types modelled using std::tuple.


Goal

  • Create utility class/method to make producing C++-call like output emitters simpler


Non-goals

  • No verification from the function as to the semantics of the region (e.g., the emitter is just an emitter and doesn't understand that actual semantics of the ops or the regions, merely walks over the ops printing ops as calls);

  • No C++ function registrations known (e.g., whether foo::constant in the above example exists is unknown)

  • Not a C dialect (e.g., this does not attempt to model C or C++ as a dialect, just lowering to C++ calls to match a FuncOp);

    The emitter could be rebased on such a dialect, then that dialect could also be optimized on etc. Here the goal is more a textual output of a function that is amenable to export as simple calls, and so much smaller in scope than trying to model C.


The proposal is to have the base emitter and then also register a translation to C++ calls where compile time registered specializations (similar to how registered for translate functions) could be used.


Conceptually this is similar to AsmPrinter except focussed/specialized on generating calls. The initial use case I had was testing & shape functions, but I saw similar emitters being written and having some common base to make writing these simpler seemed good to have. Even if it is narrow in its claims.


-- Jacques


Ronan Keryell

unread,
Nov 26, 2019, 8:42:19 PM11/26/19
to Jacques Pienaar, MLIR
On 11/26/19 4:01 PM, 'Jacques Pienaar' via MLIR wrote:

> I wanted to propose adding a C++ function call emitter that takes as input a FuncOp and emits C++ function calls matching the names of the Ops inside the function.

That sounds like a good idea.

> static std::tuple<int32_t, int32_t> test_multiple_return() {
> int32_t v3;
> int32_t v4;
> std::tie(v3, v4) = foo::blah();
> return std::make_tuple(v3, v4);
> }

What about something simpler:

static std::tuple<int32_t, int32_t> test_multiple_return() {
auto [ v3, v4 ] = foo::blah();
return { v3, v4 };
}

Modern C++17 for a modern IR... :-)
--
Ronan KERYELL, Xilinx Research Labs / San José, California.

Mehdi AMINI

unread,
Nov 27, 2019, 12:29:28 AM11/27/19
to Ronan Keryell, Jacques Pienaar, MLIR


On Tue, Nov 26, 2019 at 4:02 PM 'Jacques Pienaar' via MLIR <ml...@tensorflow.org> wrote:

[...]

Conceptually this is similar to AsmPrinter except focussed/specialized on generating calls.


This makes me wonder if we could refactor the AsmPrinter and achieve this by injection, using something like our Interface concept.
You would anyway need to reuse some logic around SSA name uniquing for the variables right?

There is also the question of being able to emit the function declaration before their use, which isn't a thing in the IR.

On Tue, Nov 26, 2019 at 5:42 PM Ronan Keryell <rker...@xilinx.com> wrote:
What about something simpler:

static std::tuple<int32_t, int32_t> test_multiple_return() {

No deduced type for the return type of the function? ;)
(ok, that can break when recursion is involved...)
 
   auto [ v3, v4 ] = foo::blah();
   return { v3, v4 };
}

Something we may not get with this form is catching inconsistencies in C++ types with respect to the IR types:

  auto [ v1 ] = producer();
  consumer(v1);

This would compile as long as there is an overload for consumer that can take the type v1, even if the C++ function for `producer` does not return the same type as (or a type convertible to) the one in the IR.
This may be a feature depending on your use case though :)

-- 
Mehdi

 

Ronan Keryell

unread,
Nov 27, 2019, 12:28:05 PM11/27/19
to Mehdi AMINI, Jacques Pienaar, MLIR
On 11/26/19 9:28 PM, Mehdi AMINI wrote:

> On Tue, Nov 26, 2019 at 5:42 PM Ronan Keryell <rker...@xilinx.com
> <mailto:rker...@xilinx.com>> wrote:
>
> What about something simpler:
>
> static std::tuple<int32_t, int32_t> test_multiple_return() {
>
>
> No deduced type for the return type of the function? ;)
> (ok, that can break when recursion is involved...)
>
>    auto [ v3, v4 ] = foo::blah();
>    return { v3, v4 };
> }

The problem in that case is that it is a std::initializer_list which
cannot be fully resolved.

https://godbolt.org/z/cCza8X
Perhaps the shortest example would be:

static constexpr auto test_multiple_return_1() {
auto [ v3, v4 ] = foo::blah();
return std::tuple { v3, v4 };
}

because you need to say somewhere you want a tuple and here we can
benefit from CTAD...

> Something we may not get with this form is catching inconsistencies in
> C++ types with respect to the IR types:
>
>   auto [ v1 ] = producer();
>   consumer(v1);
>
> This would compile as long as there is an overload for consumer that can
> take the type v1, even if the C++ function for `producer` does not
> return the same type as (or a type convertible to) the one in the IR.
> This may be a feature depending on your use case though :)

Actually it looks like there are 2 use cases for the C++ output:
- either you want the output to be typed exactly to mimic the MLIR and
thus detect errors if you use the C++ output in a different context. In
that case you want no type deduction;
- or you are using MLIR as a transformation framework to generate the
most generic C++ code you want to reuse in another context (but probably
with some caveat relative to the transformations applied). In that case
you want as many auto, CTAD and other type deductions as possible.

Adding an output flag to pick one of each style?

Jacques Pienaar

unread,
Nov 27, 2019, 1:49:05 PM11/27/19
to Ronan Keryell, Mehdi AMINI, MLIR
I'd prefer restricting it to C++14 to match LLVM's minimum language requirement. E.g., lets not generate code which cannot be used in places where MLIR should compile :)

Re: refactoring ASM printer to use op interfaces. This may indeed be interesting. My concern was that it would make AsmPrinter more complicated at the cost of one application (well expanding to also generate code in other languages too might be possible and so then more appealing to unify, but might require adding all kinds of different interfaces). And that these have different requirements as you mentioned (e.g., ordering of functions emitted). So complicating the AsmPrinter for the sake of this would seem unfortunate. But I haven't attempted that, so it may not be that bad. I think extracting reusable functions that could be shared between the two should be a goal. I created a small prototype and it is pretty simple/easy to read. While making the AsmPrinter parametrized enough to also support emitting C++ calls seems a bit more invasive and make the logic of each more complicated.

In the above example types one uses could be from some dialect, but it would be dependent on where the code is used as to the exact underlying classes used. E.g., if you have a `blah` type, then in one compilation `blah` could be backed by an integer while in another context a tree, the generated code in both cases would be the same (as long as the functions defined have matching types). This gives flexibility without needing too complicated type deductions & gives some extra error checking. So closer to (1) but I feel like we have some generality and reuse of (2) still.

-- Jacques
Reply all
Reply to author
Forward
0 new messages