Thank you for attending the round table on early exit from MLIR regions. We concluded that early exit is generally useful. Stakeholders include Clang IR, Flang, Mojo, CUDA Tile. Use cases include mapping C++ / Python to MLIR and exception handling.
We started the discussion by looking at Mehdi’s prototype (Option 1: Numbers), only focusing on the dialect design (not how it it’s implemented). The per-op opt-in mechanism, i.e., ops must implement PropagateControlFlowBreak / HasBreakingControlFlowOpInterface to signal that they support breaking control flow, seems solid.
We also talked about scf.loop and whether it’s going to be a replacement for scf.while. We concluded that scf.loop is a good first step to develop + land early exit in a non-intrusive way. (Without having to update all transformations on scf.while, etc.)
We then discussed the following 5 options for dialect design. Using SSA values to identify operations/regions seems to be a good middle ground because it allows us to reuse SSA dominance for verification.
Option 1: Numbers
%r = scf.loop -> i32 {
scf.if %cond {
scf.break 2, %value
}
scf.continue 1
}
Downside: Magic numbers may require renumbering during transformations.
Option 2: Region / Operation Pointer
Like Option 1, but instead of an integer, store an Operation *. (We discussed storing a Region *, but I think it has to be an Operation *.)
Downside: Storage size, requires introduction of a new MLIR “concept” which affects bytecode, in-memory representation, …
Option 3: Labels
%r = scf.loop @my_loop -> i32 {
scf.if %cond {
scf.break @my_loop, %value
}
scf.continue @my_loop
}
Note: This is how Mojo implements early exit. Checking whether a loop has early exit requires walking the body of the loop op, but experience from Mojo seems to suggest that it’s not a problem in practice.
Option 4: Break Scope with Token
%r0 = scf.break_scope -> i32 {
^bb0(%token1: !token) {
%r1 = scf.loop -> i32 {
%r2 = scf.break_scope {
^bb1(%token2: !token) {
scf.if %cond {
scf.break %token1, %value
}
scf.break %token2
}
}
}
Downside: IR bloat, extra work/op needed for yielding values from break_scope (?)
Option 5: Loop with Token
%r = scf.loop -> i32 {
^bb0(%token: !token)
scf.if %cond {
scf.break %token, %value
}
scf.continue %token
}
Note: The token must be an entry block argument. In case of multi-region ops, each region has its own entry block token, but those tokens identify the same operation.