-
Notifications
You must be signed in to change notification settings - Fork 14
feat: Define pass application scopes #2772
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
base: main
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2772 +/- ##
==========================================
- Coverage 83.69% 83.55% -0.14%
==========================================
Files 265 263 -2
Lines 52893 53019 +126
Branches 47651 47480 -171
==========================================
+ Hits 44270 44302 +32
- Misses 6242 6328 +86
- Partials 2381 2389 +8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty good, I particularly like your implementation of "scope" :)
- However there are issues about addition of new nodes?
- Wrt. recursive and flat regions....so let's think about the two sorts of passes:
- those that operate on flat regions. These can additionally recurse on sub-containers, kinda separately, and this is roughly what you get by calling
regions. Note this means that when operating on a region, such passes must not change any container, as they're gonna recurse on it later. (I guess that also means making sure they continue to feed equivalent inputs to the container - so every container/subregion-parent is a black box.) Note thatrecursiveisn't really a question that such a pass would ask, it just looks atregions. - those that operate on subtrees. In which case these are gonna call
rootsand then consider all/some descendants. (Note special case whenrootsincludes a function and the entrypoint is inside it, I'm not sure what we can do about that.) This is the case whenrecursiveis required. Perhaps insteadrootsshould also return a bool ?
- those that operate on flat regions. These can additionally recurse on sub-containers, kinda separately, and this is roughly what you get by calling
Finally, with respect to the breakage....
- This is breaking now (we're adding a non-default method to the trait). Ok, this isn't so bad given we're about to make a breaking release, but it seems particularly galling that we still leave unsolved #2771. Well, fair, that is a bunch of work - so I we'll roll out "fixes" to individual passes as non-breaking releases, fine :). But then "breaking" 0.26.0 will just change the doc that passes "must" follow the trait method doc? So downstream crates will "upgrade" to a newer hugr-passes in order to assert that they themselves follow that doc?
This means that consumers get the pain now (and let's say they open issues to "properly implement with_scope before 0.26.0, just like we do here). Then at the point when implementing that method and solving the issue is actually required....there's no noise, just a change to a doc; we rely on them to track that from the first issue.
- The alternative, rather than adding 10 do-nothing methods, is a trait default method - so this is non-breaking. We could roll this out later (after 0.25.0), maybe even with the first batch of passes using it, say. Then breaking change (0.26.0) removes the default - at which point downstream crates will actually break if they haven't implemented
with_scopethemselves.
I guess downstream crates would get less warning that they need to do something, so it might be longer before they update to 0.26.0. But feels to me that (if we assume downstream crates follow our example) we'll get a stronger guarantee that they have actually done the right thing for 0.26.0.
- Or.... Leave ComposablePass as per 0.24.0, and define a new trait ComposablePassWithScope that's the same but with the new method. Blanket-implement CPWithScope for any ComposablePass, with the do-nothing method, and deprecate ComposablePass, for removal in 0.26.0. I guess that's "the right way"; if this is a big enough concern that we wanna (1) get the new method out there ASAP, (2) give a large window of opportunity for implementation, (3) have a breaking change when it becomes required....is it worth it?
| @@ -0,0 +1,384 @@ | |||
| //! Scope configuration for a pass. | |||
| //! | |||
| //! This defines the parts of the HUGR that a pass should be applied to, and | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reckon you could probably do the module doc with just a one-liner that says "see [PassScope]". I'm not sure whether "Scope configuration" really tells people anything (recognition of a term once they've seen it before), either, as we haven't really defined what "Scope" is, so you could drop that.
|
|
||
| /// Scope configuration for a pass. | ||
| /// | ||
| /// The scope of a pass defines which parts of a HUGR it is allowed to modify. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and the goals/targets of optimization? I think this is still short enough to be the one-line summary
| /// In `hugr 0.25.*`, this configuration is only a guidance, and may be | ||
| /// ignored by the pass. | ||
| /// | ||
| /// From `hugr >=0.26.0`, passes must respect the scope configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm really unsure about this, see main discussion/reply
hugr-passes/src/composable/scope.rs
Outdated
| /// The scope of a pass defines which parts of a HUGR it is allowed to modify. | ||
| /// | ||
| /// Each variant defines the following properties: | ||
| /// - `roots` is a set of **regions** in the HUGR that the pass should be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider "flat regions" rather than just "regions"?
Has the term "regions" been precisely defined somewhere else? If not, we should do so here. I believe the correct term from the spec is "Dataflow sibling graph"...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, some optimizations like nesting CFGs (perhaps even simpler ones like CFG normalization) cannot necessarily be expressed as operations on flat regions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given only EntrypointFlat really depends on the notion of regions, perhaps this should not be a main/defining property of a PassScope but just a helper method for passes that care.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, having written main PR comment, and noting you have both/separate fn regions and fn roots - I think the latter is the correct approach, whereas the doc here defines roots as being the regions.
hugr-passes/src/composable/scope.rs
Outdated
| /// - `roots` is a set of **regions** in the HUGR that the pass should be | ||
| /// applied to. | ||
| /// - The pass **MUST NOT** modify the container nodes of the regions defined | ||
| /// in `roots`. This includes the optype and ports of the node. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed on ports. I'm reasonably happy with optype, but there would seem to be cases where we can turn a CFG or TailLoop into a DFG, say, without affecting anything outside it. (Code expecting to manipulate the internals will be confused, of course.) Do we want to rule those out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I.e. is there some use case where it's particularly good to? My intuition is that the root node optype (as long as dataflow) is inside the subtree/region defined by the root node, whereas it's ports are not inside (they are the boundary)
hugr-passes/src/composable/scope.rs
Outdated
| ) -> impl Iterator<Item = H::Node> { | ||
| match self { | ||
| Self::EntrypointFlat | Self::EntrypointRecursive => { | ||
| // Include the entrypoint if it is not the module root. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to include like this for All and AllPublic too, no?
hugr-passes/src/composable/scope.rs
Outdated
| Either::Left(entrypoint) | ||
| } | ||
| Self::All | Self::AllPublic => { | ||
| // Decide which public functions to include. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Decide which public functions to include. | |
| // Decide which functions to include. |
hugr-passes/src/composable/scope.rs
Outdated
| let Some(fn_op) = hugr.get_optype(node).as_func_defn() else { | ||
| return false; | ||
| }; | ||
| let public = fn_op.visibility() == &Visibility::Public; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: either call this is_public or just inline at its single point of use
hugr-passes/src/composable/scope.rs
Outdated
| if node_parent != hugr.module_root() { | ||
| return true; | ||
| } | ||
| // For module children, only private functions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe "Additionally allow removing/modifying private functions" ?
hugr-passes/src/composable/scope.rs
Outdated
| /// - `scope` is a set of **nodes** in the HUGR that the pass **MAY** modify. | ||
| /// - This set is closed under descendants, meaning that all the descendants | ||
| /// of a node in `scope` are also in scope. | ||
| /// - Nodes that are not in `scope` **MUST** remain unchanged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both in terms of their signature/optype and their behaviour...or clarify scope is the nodes which may be modified both as ops and as the behaviour defined by their descendants
hugr-passes/src/composable/scope.rs
Outdated
| /// | ||
| /// Nodes in `root` are never in scope, but their children will be. | ||
| /// | ||
| /// If [PassScope::recursive] is `true`, then all descendants of nodes in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggests that if recursive is false, then descendants of nodes in root are not in scope, i.e. MUST NOT be modified? (EDIT: Yes, that is consistent with elsewhere, so ok.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. IIUC, you can't modify anything outside of roots. So if you run with AllPublic you can't modify the private functions?? EDIT sorry yes, the private functions are still in_scope even though not in roots.
EDIT2 So roots is really more about goals/targets than scope. How about renames...
scope -> may_modify and/or may_modify_children? (/may_modify_descendants - perhaps one might even make that false if the thing contains the entrypoint, but true for all non-entrypoint descendants, although "modify_children" might mean, "ok to add children")
roots -> target_roots
|
Urgh. Tried to use this for RemoveDeadFuncsPass....ok, that is an awkward one, maybe should have tried something simpler.
and got this error: which sounds a bit like it isn't using the call to You can see this in 6ed9ee6 |
|
Ok how about: |
I note this is only really one enum variant more than we have now, i.e. very similar! But I think this allows to summarize:
|
If the entrypoint is the module-root, there are a few possibilities for "preserve semantics of only the entrypoint"....
|
Defines
PassScopeconfigurations in bothhugr-passesandhugr-pythat let users define set the parts of the hugr that a pass should optimize.Since each pass must be carefully modified to support the new config, this PR only adds the definitions (and a
ComposablePass::with_scopemethod), and states that passes are not required to follow the configuration (for now).This will let us make the required changes incrementally without blocking the
hugr-rs 0.25.0release. I opened an issue to track the implementations. #2771Closes #2748. See discussion about pass properties there. We left the
NamedFunctionsvariant to be defined later.I'll add some python tests later.