-
Notifications
You must be signed in to change notification settings - Fork 31
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
Strategy dialect for precise specification of complex rewrite rules #149
Comments
Nice! apply/apply_dynamic seem analogous to call/call_indirect, right? I think I see recursion being useful in cases where we write super generic stuff that targets an abstract machine model. My inclination is to start without recursion and see where this brings us, as we can likely start scaling in a more naive fashion on fixed HW models. Generalization can come later. I have a somewhat similar thinking for failures: I wonder how much "can fail" is a property of application of a complex pattern to "too low-level IR objects". Experience with the current system seems to show that our very progressive lowering approach from high-level IR allows us to not worry about rollbacks. There seems to be a connection to more static vs more dynamic programs; i.e. can you hoist all your conditions upfront or not. I imagine we'll encounter plenty of use cases for failures as we scale up but similarly, to get off the ground, I think we can do without most of the infra complexity. How about a proto ? :) |
Correct!
Recursion is mostly useful for expressing rewrites that need to traverse down arbitrary unknown operations, and we certainly could start without it, or only with some special forms (e.g. exhaustive greedy application like in the default driver).
I think that you're right in that proper IR design can let you avoid thinking about failures in many cases, but I don't think it's always possible. Linalg might be a neat enough use-case and design such that you can get away without it, but you might run into trouble trying to scale it up to arbitrary MLIR transforms. And IMO things like loop-interchange should be also expressible as rewrites, even though this is out of scope for linalg.
Sounds good to me! :) |
What happens if the strategy mutates the operation, or replaces it entirely? E.g. strategy.apply @canonicalize(%op) // %op is canonicalized away/replaced
strategy.apply @myStrategy(%op) // %op is now invalid? I will also propose that "strategies" can return IR handles, i.e. %tiledOp = strategy.apply @tile(%op, %tileSize) : (!pdl.operation, i64) -> (!pdl.operation)
strategy.apply @canonicalize(%tiledOp) : (!pdl.operation) -> () |
Extending strategies to have additional return values is definitely possible (in functional terms those would run in the Either monad, which roughly means that every strategy either succeeds with a value or fails with an error). The question of what happens if an op is destroyed is a good one and it does tie to the difficulties around mutability. I think there are multiple ways we can potentially handling that. First, we could consider this to be a programmer error and rooting a strategy at a deleted operation could cause the overall strategy to fail. Alternatively, we could make it so that patterns cannot delete operations and they are expected to be later cleaned up by DCE if they end up being unnecessary. |
I would add a In general, do you think we can survive with non-parametric strategies (i.e. ones that only take a Plus the usual concerns about complexity of the rewrite engine and action-at-a-distance on the IR that make debugging hard should something go wrong. I suppose these cannot be answered without a prototype. |
I think adding parameters to patterns is relatively straightforward (attributes on
This sounds to me like too strict of a restriction. We can instead have the pattern explicitly return the operation handles that can be chained. |
@ntv recently brought to my mind that together with @ftynse (and now @Mogball) they started working on a dialect for driving pattern (and pass) applications. This is meant to be an improvement over the standard greedy driver provided by MLIR which does seem to be very constrained. I think this is all a super cool domain, and @ntv prompted me to dump some of my thoughts on this in an issue, so here we go!
Note that most of it is heavily inspired by the functional pearl paper that describes ELEVATE, only translated to MLIR. Finally, I’m far from being an MLIR expert, so parts of this proposal might be based in some significant misconceptions. Please point those out whenever you see them!
The
strategy
dialectThis is an outline of the operations I'd suggest to include in the new dialect:
Types:
!strategy.strategy
: Roughly represents a function with signature(!pdl.operation) -> ()
. Note that an application might fail which is represented as a side effect and not as a return value.Operations:
strategy.try
: has a single-block region and one boolean result. If the execution of the region fails, it doesn't propagate the failure upwards but returnsfalse
. If the execution of the region succeeds returnstrue
.strategy.apply
: applies an external strategy to an operation (its single operand). The external strategies are identified by symbols that will be resolved to real passes and rewrite patterns during interpretation (e.g.@canonicalize
could be backed by the canonicalization pass).strategy.reify
: turns a single-block MLIR region that takes an operation as an argument into a!strategy.strategy
value. Basically a lambda expression.strategy.apply_dynamic
: applies a dynamic strategy to an operation (i.e. it takes two operands). If the dynamic strategy fails, the execution of this operation fails.Many other useful types and operations can be inherited from PDL (
!pdl.operation
,!pdl.attribute
, etc.).The programs expressed in this dialect are meant to be interpreted sequentially, as usual in MLIR, and encode a sequence of rewrite applications, potentially orchestrated by control flow (e.g. using the
scf
dialect) and recursion.Example program:
How to use it?
Initially it would make sense to start with a simple interpreter, but I imagine that eventually we could go much further. For example, we could try partially evaluating all of the ops from the
strategy
dialect and lower them all to a PDL program (assuming PDL is expressive enough).What are patterns?
Patterns can be applied to a particular "root" operation and either fail to match, or mutate the IR according to their definition. Operation passes work similarly, except they always succeed (at least as far as I understand them). In the new dialect both passes and patterns defined in C++ would be treated as externally defined strategies, which are functions that apply to an operation handle and can throw as a side effect.
The text was updated successfully, but these errors were encountered: