Hi folks,
Summary
We would like to start a discussion on the semantics of the Arithmetic dialect. Currently, there are many ops that we expect to be undefined for some inputs, yet this is not covered by the dialect’s documentation. This makes it difficult to lower to arith
without having to rely on the subsequent lowering to the next dialect (e.g., LLVM, SPIR-V), or reason about the correctness of transforms within the arith
dialect. We propose to gradually start filling in the gaps in the semantics.
Background
Let’s consider two examples that illustrate the difficulties we run into:
-
The
arith.muli
op does not specify the overflow semantics. It is unclear if one should expect wrapping semantics (result modulo 2^N), or an undefined result. When emittingarith.muli
, one has to either insert some runtime checks to avoid triggering a potential UB, or check the lowering paths to LLVM/SPIR-V/etc. and confirm thatarith.muli
will be converted to an op with defined overflow behavior. -
The
arith.shrui
op does not specify what happens with shift amount >= the type bitwidth. LLVM defines the result of such shifts aspoison
. When handling shifts in the wide integer emulation pass, the LLVM spec allows us to emit selects to stoppoison
propagation. If we had a (hypothetical) lowering fromarith.shrui
to C’s shift operator>>
, these selects wouldn’t have been sufficient as this might trigger an immediate UB.
In the current state of things, all of the following: lowering to arith
, lowering from arith
, transforming within arith
, require confirming the exact semantics with *all* of the lowering targets: LLVM, SPIR-V, and potentially more. We argue it would be better for arith
to define the exact semantics, so that there’s only a single place to check in these scenarios.
Solution Space
We propose to make arith
more specific wrt to potential undefined behavior of each op. We could execute that in a number of ways.
Source IR → arith → Destination IR
One high level consideration is how much ‘defined’ we want the ops to be:
- If we make an op undefined, it becomes difficult to emit it. If the source IR is more defined, it may require runtime checks at the source.
- If we make an op overdefined, it becomes difficult to lower it. If the target IR is less defined, it may require runtime checks at the destination.
Because the two main existing destination IRs, LLVM and SPIR-V, have similar semantics, it seems reasonable to use the LLVM semantics as the starting point.
Another thing to consider is how to implement the changes to the semantics:
-
Tie
arith
semantics to the LLVM Language Reference Manual. This probably requires the least amount of changes, but does not allow for independent evolution of both IRs. For example, it seems unreasonable to require LLVM contributors take the impact onarith
and the rest of MLIR into consideration every LLVM IR change they propose. Also,arith
is not simply a subset of LLVM: it supports more types, e.g., n-D vectors, tensors, which may have more semantic options. -
‘Copy’ the current LLVM semantics in one sweep, but allow for future divergence if it makes sense for particular ops.
-
Consider the semantics on the op-by-op basis, default to the LLVM semantics unless there is a reason not to. This follows the spirit of incremental development and is expected to make code reviews easier, and we can also audit existing lowerings to make sure they are proper wrt the defined semantics step by step.
Proposal
We propose to move forward with the implementation strategy 3. This allows us to not disrupt the status quo by following what seems to be the existing undocumented expectation, and fill in the gaps through a series of incremental changes developed and reviewed independently.
-Jakub (@kuhar), with input from @jpienaar, @ftynse, and @antiagainst