Looking at some of the non-contentious (at least from observation) things from above, uploaded D118648 to move SelectOp to Arithmetic. Also sent out D118654 for removing the unit support from ConstantOp.
– River
Looking at some of the non-contentious (at least from observation) things from above, uploaded D118648 to move SelectOp to Arithmetic. Also sent out D118654 for removing the unit support from ConstantOp.
– River
Yes, so far I’ve been playing a bit of devil’s advocate to see what people’s perceptions were. It’s interesting to consider if we added FuncOp today, instead of it being a builtin thing from the beginning. On one hand, it’s a special construct, on the other it’s just fancy control flow. I think there are two main paths (which can be tweaked) that we can discuss and consider going down:
Basically mentioned above, we have some function-related operations that could be slotted into the CF dialect with the rest going into SCF.
Control Flow (new dialect):
SCF:
If we truly feel that Func is such a special construct to keep separately, we need to decide what kind of dialect could/should hold it. There are different ways we could do this:
We could just shove everything currently related to FuncOp in a new dialect.
Keying off of previous points, we could move some CallOp/CallIndirectOp (with some slight generalization) to a ControlFlow dialect; with the rest going to a new Func dialect.
Control Flow (new dialect):
New Func specific dialect:
I can see pros+cons to all of these as potential paths to take. There are definite benefits to a new dialect though. SCF in its current state is definitely not as general as the dialect name/description aspires to be (for better or worse), which kind of furthers the divide that FuncOp doesn’t really fit well. Also from a migration point-of-view, having a new dialect would definitely be easier in many respects. Realistically, this is a good opportunity to discuss where FuncOp really fits within the ecosystem (cough not builtin cough) and how it is positioned.
– River
Sorry for missing this thread, apparently gmail decided that llvm forums are spam, whodathought. I super endorse narrowing std.constant and moving select to arith. Both are great moves.
While I can see this, it doesn’t really seem right to me. “func” and related constructs are more of a default container than they are related to specific forms of control flow. The later should compose with other sorts of containers, e.g. a Fortran fir.procedure or whatever. Putting func into the scf dialect wouldn’t force the two to be used together, but I don’t see how they are conceptually aligned.
I’m not sure what the practical concerns are here, but from a theoretical perspective, this seems cleaner to me. I’d also trow in the builtin ModuleOp as well - it should eventually devolve into an artifact of convenience, not something that everyone uses.
The problem with these operators is that I’m not sure what they really are - they’re almost “convenience” operations that MLIR provides that are useful for bootstrapping things when you don’t care about nailing the semantics for containers down yet. That isn’t a bad thing btw, I do think there is utility in MLIR providing such a thing, but we’ve moved a long way in the MLIR design from when these were introduced - I don’t think they are nearly as primal as they used to be.
All “real” (whatever that means
compilers should define their own containers, because you want your own verification logic etc and to control your world.
Yes, I think this is also a very practical realization. I think there is a good argument for merging func and scf together into something (and rename it to not be called “scf”) in that scf is also mostly a convenience dialect at this point. It is useful for bootstrapping, research and experimentation, but has turned into more "kitchen sink"y than it might be.
-Chris
I’m fine with having a new dialect for FuncOp (and associated functions like Return, Call, …).
We should likely look at ModuleOp separately in another step than all this: it is a bit “special” right now and it’s worth more consideration.
I really don’t understand what warrants this comment right now: scf looks far from a “kitchen sink” to me. It has for, if, and while as the most basic block of structured control flow as far as I can understand it. The only things that might be seen as borderline there could be the parallel multi-loop construct (and the associated support for reductions) as well as the execute_region. It would be a bit of a stretch to create another dialect for these when they aren’t clearly not belonging to “structured control-flow” either.
I’m not really sure how different would be a scf dialect if we were designing it from scratch today.
Anyway, this is a bit off-topic for this thread ![]()
We should likely look at ModuleOp separately in another step than all this: it is a bit “special” right now and it’s worth more consideration.
Yeah, that one can wait until after we resolve the current situation. There are various special considerations there that I’d rather not pull into this discussion for now (killing Standard and moving FuncOp would already be monumental achievements).
– River
I really don’t understand what warrants this comment right now:
scflooks far from a “kitchen sink” to me. It hasfor,if, andwhileas the most basic block of structured control flow as far as I can understand it. The only things that might be seen as borderline there could be theparallelmulti-loop construct (and the associated support for reductions) as well as theexecute_region.
Kitchen sink is the wrong phrase, sorry I just meant it has been stretched, and /looks/ general but isn’t really a general solution. I don’t want to dig deep into this, but I just mean things like the scf.while loop follows the pattern of the tf+xla loop constructs which don’t allow the “condition” clause to have additional live outs that are visible on the body, forcing recomputation for many loop structures. The way scf.reduce and scf.parallel are associated is very particular to specific use cases, even that scf.if forces the use of i1 specifically for the condition, but allows arbitrary types for live ins and live outs, making it more annoying for use by dialects that have more specific condition types (like a bool type).
In any case, I agree they are useful! It is just hard to make a general thing that serves many audiences well, and the more general a construct is, the more difficult it is to analyze and transform.
I just mean things like the scf.while loop follows the pattern of the tf+xla loop constructs which don’t allow the “condition” clause to have additional live outs that are visible on the body, forcing recomputation for many loop structures.
Actually no: it does not follow the XLA while loop and allows for arbitrary flow of values from the condition to the body, exactly for the reason you mention ![]()
By the way: I recently implemented some canonicalization on the XLA while loop (around removing loop invariant value carried from iteration to iteration) and we had the same implemented in SCF, and the generality of the scf.while made the implementation much more complex! So there is definitely an interesting tradeoff here.
even that
scf.ifforces the use ofi1specifically for the condition, but allows arbitrary types for live ins and live outs, making it more annoying for use by dialects that have more specific condition types (like a bool type).
I think the same applies to cond_br for example right? It is hard to abstract entirely the type system, maybe with TypeInterface we could get there…
I wonder whether functions are so fundamental that we could also give them their own dialect. It would contain fairly few ops but functions in our pipelines outlive uses of
scf, which is a (maybe weak) indication that they are a thing of their own.
Another way to look at it as SCF constructs are almost always lowered to branches, functions are not. They persist until assembly levels. IMO, functions+calls can go to a separate dialect, or can live together with the rest of “assembly-level” control flow operations such as branches. The latter solution boils down to renaming whatever remains of “standard” to “control-flow”.
One other question that was left unanswered in the original splitting proposal is what happens with all the StandardToX and XToStandard conversions. I’m totally fine to ignore it here to reach convergence faster.
I think the same applies to
cond_brfor example right? It is hard to abstract entirely the type system, maybe with TypeInterface we could get there…
Just an additional reminder that SCF, as well as cond_br, largely perdate type interfaces and even custom types. We couldn’t have proposed a general solution to the problem we did not know existed. To me, the fact that SCF remained largely the same over the years means the exact opposite of it being “experimental”. I am happy to discuss this in a separate topic to avoid derailing the discussion here.
We discussed this a bit during the ODM today. I think the general semi-consensus is to have a new dialect containing the function constructs. From there, I think we need to fully decide what this dialect is supposed to represent (not just now, but into the future). There are a few options:
proc dialect)?Aside from that, there is still some desire to have CallOp/CallIndirectOp not be hardcoded on FuncOp, and be moved to (potentially) the Control Flow dialect.
I’d like to move the discussion towards the above (even though the concepts surrounding SCF are interesting), so that we can finish out this effort.
Thanks
– River
I’m sorry I missed the discussion today, River just poked me to suggest some names.
The design of ‘func’ has a bunch of assumptions in it: it allows a CFG, it has an imperative control flow nature to it (op execution is ordered by a program counter), etc. I’m still +1 on moving func out to its own dialect. If we do that, we should consider dialect names like “sw” (dual to “hw” in CIRCT) given that all these things are pretty generic to many different software systems, or maybe “imperative” given the model isn’t a dataflow graph.
I’m not as excited about the term “procedural” - I have old pascal hangups with that where procedural programming was against functional and higher order programming.
-Chris
If we do that, we should consider dialect names like “sw” (dual to “hw” in CIRCT) given that all these things are pretty generic to many different software systems, or maybe “imperative” given the model isn’t a dataflow graph .
If we’re getting to the name bike shed, we’re almost there ![]()
I’m not convinced by “sw”: first because it is overly generic as a name (like std was) but also because “dataflow” vs “imperative” isn’t about “hw” vs “sw”: TensorFlow, XLA, and many other software project have a dataflow model.
We also touched on the “procedural” term because some folks were asking about higher order programming and pure functional construct indeed: but my take was that the current FuncOp abstraction is indeed not really intended to target this kind of abstraction and I would think that someone addressing these would design a dedicated dialect.
Keeping the ball rolling, sent out a review(D118966) for an initial ControlFlow dialect while we discuss final steps for FuncOp and friends.
– River
The initial control flow dialect has landed, and we are quite literally nearing the finish line. The Standard dialect now only has 4 operations(!): CallOp, CallIndirectOp, ConstantOp, ReturnOp. We are now at the stage where we just need to rename the current standard dialect to whatever fits the FuncOp+friends dialect. Let’s discuss a destination for these and close off this chapter of MLIR!
There have been two main potential paths: a dialect focused around functional concepts; a dialect focused around more general high-level components (e.g. could conceptually contain Module).
If we intend to go the “function concepts” route, we could just go for functional. Such a dialect could conceptually grow modeling for things like “invoke”/exception handling, closures, etc.
(this is mostly a call for people who are better at naming than I am, which is a fairly low bar)
– River
Yay! Almost there!
From up-thread:
builtin.func is actually a fairly specific thing (as mentioned, CFG-based, aligned directly with what can be represented by LLVM’s concept of such things). When I think of “functional”, I don’t think of builtin.func – it is something lower level and more specific than that. So -1 on that.sw as a shorthand for “the last 40 years of CFG-based, low-level, C-influenced representations that LLVM also embraced but no one bothered to name”. But by opening up such a name, we take what is likely a simple naming discussion and make it in to an open ended taxonomy discussion.llvm interop and a lot of ambiguity that goes with that name, I probably would have just called it an “llvm function” and left other concepts of function to more specific cases.I’m trying to come up with something better…
In a functional dialect, I would expect to find lambdas and other combinators.
How about calling the dialect func? To have func.call, func.indirect_call, and func.constant. The function definition is a bit awkward as func.func (unless we keep the prefix-less format) though.
This looks the most reasonable to me. A tiny tweak:
func.func → func.function.
What about f? So we can have a UncOp what would read as f.unc ![]()
What about
f? So we can have aUncOpwhat would read asf.unc
I was going to to suggestion sticking with func but just calling it func.tion
This is cracking me up. If we don’t converge, I’m going to propose: ƑƲƝƇ ![]()
ConstantOp
What is this constant for?
Today’s std.func could just be func.constant (it’s constant of function type afterall if we consider functions values, so you can have ops producing functions and this is just a constant function value producing op
)
fn also a fun abbreviation (“fn.func not working” is a phrase that may have been uttered a few times before …). If seeming duplicate names is a concern, func.callabale to mirror call is an option (
)