-
Notifications
You must be signed in to change notification settings - Fork 11
feat: BorrowSquashPass to elide redundant borrow/return ops #1159
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 #1159 +/- ##
==========================================
+ Coverage 79.02% 79.14% +0.11%
==========================================
Files 160 161 +1
Lines 20421 20678 +257
Branches 19489 19746 +257
==========================================
+ Hits 16138 16365 +227
- Misses 3294 3320 +26
- Partials 989 993 +4
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:
|
b30d6a6 to
2724321
Compare
tket/src/passes/squash_borrow.rs
Outdated
| } | ||
| } | ||
|
|
||
| /// Reasons we may fail to determine whether a node is a borrow/return. |
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.
TBH I do kinda think that these might be better all made into panics. As per comment on is_borrow_return, Ok(None) means "can't figure out this op, leave it be, continue only in areas excluding this op"; and it's quite hard to define over what (wider) scope an error would inhibit optimization (potentially including bits of the Hugr with no access to the same value but which by chance would have been optimized after the error was thrown, so were never run).
tket/src/passes/squash_borrow.rs
Outdated
| @@ -0,0 +1,708 @@ | |||
| //! Reorder and squash pairs of borrow and return nodes where possible. | |||
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.
| //! Reorder and squash pairs of borrow and return nodes where possible. | |
| //! Reorder and squash pairs of borrow and return operations on [`BorrowArray`] where possible. |
tket/src/passes/squash_borrow.rs
Outdated
| /// * `None` if the array's size is not statically known. | ||
| /// | ||
| /// Otherwise (if the type is not such an array), return `None`. | ||
| fn get_array_size(&self, ty: &Type) -> Option<Option<u64>>; |
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.
The nested option is a bit annoying for readability, could be worth a dedicated enum
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.
Now just a bool, the assumption that in-range won't panic is not good if we have dynamic indexing among nested arrays 😢
tket/src/passes/squash_borrow.rs
Outdated
| /// A pass for eliding (e.g. `BorrowArray`) reborrows of elements (with constant indices) | ||
| /// along with the preceding return. |
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.
| /// A pass for eliding (e.g. `BorrowArray`) reborrows of elements (with constant indices) | |
| /// along with the preceding return. | |
| /// A pass for eliding return followed by borrow on [`BorrowArray`] with matching constant index. |
tket/src/passes/squash_borrow.rs
Outdated
| impl<BR> BorrowSquashPass<BR> { | ||
| /// Add regions (subgraphs) in which to perform the pass. | ||
| /// | ||
| /// If no regions are specified, the pass is performed on all dataflow 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.
Maybe Option<Vec<Node>> is a safer encapsulation of this? If for whatever reason the region list becomes empty (maybe I am popping and pushing to it somewhere else), I would assume nothing happens rather than everything.
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.
The only way for the region list to become empty would be for that empty list to be passed in here...but done
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.
yes I am talking about expected behaviour when called by external code (in some other composite pass for example)
| .as_extension_op() | ||
| .is_some_and(|eop| eop.qualified_id() == "tket.quantum.CX")); | ||
| } | ||
| assert!(h.output_neighbours(cx1).all(|n| n == cx2)); |
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.
could be nice to have this as an explicit assert-not-any before the pass
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.
does assert that the output neighbours of both CX's were returns?
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 fair enough, just preferred something more explicit but not important
tket/src/passes/squash_borrow.rs
Outdated
| matches!(op, OpType::LoadConstant(..)) | ||
| || op | ||
| .as_extension_op() | ||
| .is_some_and(|op| ConvertOpDef::from_extension_op(op) == Ok(ConvertOpDef::itousize)) |
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 feels flaky - future conversion ops wouldn't be covered. Would it not be better to rely on constant folding eliminating these beforehand?
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.
Yes, done. Rather slows down the big-hugr test but it works...
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 the test need not be so big?
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.
let's wait for Agustin's integration tests and then maybe we can just drop some of these
| Ok(Some((node, idx, is_br.action, is_br.borrow_from))) | ||
| } | ||
|
|
||
| fn wire_type(h: &impl HugrView<Node = Node>, w: Wire) -> Type { |
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.
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.
That's on a builder::Dataflow, not a hugr ?!
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.
ah woops that's annoying, still the implementation is on a hugr and saves iteration to find the port https://docs.rs/hugr-core/0.24.0/src/hugr_core/builder/build_traits.rs.html#555
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.
Ah yes thanks! Done
tket/src/passes/squash_borrow.rs
Outdated
| let out_ty = wire_type(hugr, Wire::new(node, is_br.borrow_from.out)); | ||
| if *array_ty != out_ty { | ||
| let array_ty = array_ty.clone(); | ||
| return Err(BorrowAnalysisError::InconsistentArrayType { array_ty, out_ty }); |
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.
should this be prevented by validation or could the rewrites in this pass induce this failure?
| { | ||
| array = Wire::new(node, borrow_from.out); // for next iteration | ||
|
|
||
| match (action, borrowed.entry(index)) { |
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 match is very readable 👍
3f5cf42 to
539aa15
Compare
684f85c to
b264751
Compare
bdc00d9 to
e555e88
Compare
|
This PR contains breaking changes to the public Rust API. cargo-semver-checks summary |
closes #1072, closes #1074
The main questions here are about
unsafe_get/unsafe_put(CQCL/guppylang#1165), and to some extentnew_all_borrowed. At the moment the PR is in an awkward midway state towards supporting these but not really:new_all_borroweds are generated by guppy (i.e. we know that they are immediately followed byreturns into every element), then we can fixassume_all_presentto true and get more optimization. Clobber is then essentially pointless (guppy does not generate borrow/return around any op that Clobbers).assume_all_presentto false, which loses some optimization.TODO:
Other ways of regaining safety in the presence of unsafe_put/get, that are not as good dataflow analysis but potentially less work:
matchblock would have remember+to handle cases of initial-Return whereas currently it only tracks the first op being a Borrow.Return-Borrow-Return-> just the final return (not requiring that the returned value is the borrowed value)? This would preserve that the program panics, but might change the order of panics (among ops whose order is forced by a linear array value being fed between them, heh). So a question of what guarantees do we give about visible semantics.get(copies element) andset(swaps external element in), 'coz we might now have return/borrow 'pairs' with set/get inbetween (and we can elide the pair if the index is definitely different). Indeed we might want two variants of "clobber" - giving the state of the element after the op (definitely borrowed, definitely not)