-
Notifications
You must be signed in to change notification settings - Fork 8
feat: Add a borrow_array
type replacement pass
#975
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
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #975 +/- ##
==========================================
+ Coverage 79.18% 80.29% +1.10%
==========================================
Files 120 121 +1
Lines 14147 15069 +922
Branches 13365 14287 +922
==========================================
+ Hits 11202 12099 +897
- Misses 2269 2288 +19
- Partials 676 682 +6
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.
Thanks, must have been quite tedious to write all that 😅
Looks good to me, mostly just a few nits and refactoring suggestions apart from the get
lowering comment and missing lowering for conversion ops.
// TODO uncomment once https://github.com/CQCL/hugr/issues/1234 is complete | ||
// ensure_no_nonlocal_edges(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.
This seems to be 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.
Not quite sure what's going on here but we "temporarily" added a guppy fix to avoid generating nonlocals, however now Hugr has LocalizeEdges pass, so can we remove the guppy workaround? Not for this PR, but do we have a guppy issue?
Here, I'm not sure it's worth adding an error variant for the existence of nonlocal edges, to me I think this is an assert (a bug in our code) if we have not run LocalizeEdges already (or if there is a bug in LocalizeEdges !).
// pass meant to be run on Guppy-generated HUGRs, which should not emit borrow array | ||
// constants that contain empty values. The borrow array extension however does not | ||
// guarantee this, so we should handle the case where the array contains empty | ||
// values on we add a more specific borrow array value that supports this. |
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.
// values on we add a more specific borrow array value that supports this. | |
// values once we add a more specific borrow array value that supports this. |
?
// Unwrap the result: expect that the element was present. | ||
let result_ty = vec![opt_elem_ty.clone().into(), arr_type.clone()]; | ||
let [opt_elem, out_arr] = dfb | ||
.build_unwrap_sum(1, either_type(result_ty.clone(), result_ty), result) |
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.
Would be nicer to have an unwrap with a custom panic message. E.g. in Guppy we say "Index out of bounds"
.add_array_set(opt_elem_ty.clone().into(), size, arr, idx, nothing) | ||
.unwrap(); | ||
|
||
// Unwrap the result: expect that the element was present. |
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.
// Unwrap the result: expect that the element was present. | |
// Unwrap the result: expect that the index was in bounds. |
.add_array_set(opt_elem_ty.clone().into(), size, arr, idx, opt_elem) | ||
.unwrap(); | ||
|
||
// Unwrap the result: expect that the slot was empty (none). |
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.
// Unwrap the result: expect that the slot was empty (none). | |
// Unwrap the result: expect that the index was in bounds. |
// Unpack the array to get the elements as wires | ||
let elems = dfb.add_array_unpack(elem_ty.clone(), size, arr).unwrap(); |
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 makes the replacement scale with the array size. Better to use a scan
op to wrap the elements into options
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 had issues using scan because when I try to define a function on the module builder I get from the DFG builder it doesn't seem to add the function so I can't load it in to use with scan
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.
Mhmm that's not ideal, pinging @acl-cqc
I guess it's ok to play around with for now but I'm sceptical about using this in production since it makes compile-time and binary size scale with arrays sizes...
// Unwrap all the options in the input array. | ||
let elems = dfb | ||
.add_array_unpack(opt_src_ty.clone().into(), size, inputs[0]) | ||
.unwrap(); |
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.
Same here: Better to unpack using a scan
}, | ||
); | ||
|
||
lw |
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.
You're missing the conversion ops from and to regular arrays
move |args| { | ||
let [size, elem_ty] = args else { | ||
unreachable!() | ||
}; | ||
Some(borrow_dest( | ||
size.as_nat().unwrap(), | ||
elem_ty.as_runtime().unwrap(), | ||
)) | ||
} |
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's a lot of duplication of this logic. You could consider commoning it up a bit:
for op_def in BArrayUnsafeOpDef::iter() {
w.replace_parametrized_op(
borrow_array::EXTENSION.get_op(&op_def.opdef_id()).unwrap(),
move |args| {
let [size, elem_ty] = args else {
unreachable!()
};
let size = size.as_nat().unwrap();
let elem_ty = elem_ty.as_runtime().unwrap();
Some(match op_def {
BArrayUnsafeOpDef::borrow => borrow_dest(size, elem_ty),
BArrayUnsafeOpDef::#return => return_dest(size, elem_ty),
...
})
},
);
}
} | ||
|
||
#[cfg(test)] | ||
mod tests { |
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 you also add a test for array constants?
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.
Thanks! Looks good, happy to merge this
Can you create an issue to use scan instead of array unpacking and link it in the relevant places?
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 feel that if this is passing tests then rather than benchmarking performance first, we should just get this in; and then work on adding benchmarks and improving/optimizing later. (e.g. from serialized Hugrs, so we are not waiting for guppy-main
to generate them; and measuring compilation time and size of compiled output, down to whatever level is easy to test - maybe the Hugr input to hugr-llvm, or the LLVM-IR output from hugr-llvm, without running LLVM itself.) @mark-koch what do you think?
tket-qsystem/src/replace_bools.rs
Outdated
vec![array_ty.clone(), array_ty], | ||
)) | ||
.unwrap(); | ||
// TODO: Find less hacky solution. |
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.
Have we lowered drops already? (If not, can we lower to a drop?)
tket-qsystem/src/replace_bools.rs
Outdated
let size = size.as_nat().unwrap(); | ||
let elem_ty = elem_ty.as_runtime().unwrap(); | ||
if !elem_ty.copyable() { | ||
Some(array_clone_dest(size, elem_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.
Consider (!elem_ty.copyable()).then(|| array_clone_dest(size,elem_ty))
instead of the if...else
@@ -18,6 +22,28 @@ impl<H: HugrMut<Node = Node>> ComposablePass<H> for LowerDropsPass { | |||
|
|||
fn run(&self, hugr: &mut H) -> Result<Self::Result, Self::Error> { | |||
let mut rt = ReplaceTypes::default(); | |||
|
|||
// future(bool) is not in the default linearizer handler so we add it here. |
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 think I favour commoning this up with the equivalent code in replace_bools.rs
by making a helper-method that makes a ReplaceTypes with future-bool linearizer already set-up.
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.
Do we still need this? If so, I stand by my previous comment; defining a private helper function is easy enough to do now
// TODO uncomment once https://github.com/CQCL/hugr/issues/1234 is complete | ||
// ensure_no_nonlocal_edges(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.
Not quite sure what's going on here but we "temporarily" added a guppy fix to avoid generating nonlocals, however now Hugr has LocalizeEdges pass, so can we remove the guppy workaround? Not for this PR, but do we have a guppy issue?
Here, I'm not sure it's worth adding an error variant for the existence of nonlocal edges, to me I think this is an assert (a bug in our code) if we have not run LocalizeEdges already (or if there is a bug in LocalizeEdges !).
Yeah imo ok to merge and make improvements in follow-up PRs |
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.
Thanks Tatiana, mammoth chunk of work, Rust hugr-building is still painful and you've had to do a lot of it here - well done! 👍 💯 🥇
@@ -44,6 +46,7 @@ pub struct QSystemPass { | |||
monomorphize: bool, | |||
force_order: bool, | |||
lazify: bool, | |||
lower_borrow_arrays: bool, |
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 could super-nit-pick and say that this should be replace_borrow_arrays
....or all should be called borrow_array_to_array
.....but I won't ;-)
@@ -18,6 +22,28 @@ impl<H: HugrMut<Node = Node>> ComposablePass<H> for LowerDropsPass { | |||
|
|||
fn run(&self, hugr: &mut H) -> Result<Self::Result, Self::Error> { | |||
let mut rt = ReplaceTypes::default(); | |||
|
|||
// future(bool) is not in the default linearizer handler so we add it here. |
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.
Do we still need this? If so, I stand by my previous comment; defining a private helper function is easy enough to do now
.unwrap(); | ||
|
||
// Unwrap the result: expect that the index was in bounds. | ||
let result_ty = vec![opt_elem_ty.clone().into(), arr_type.clone()]; |
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.
[Optional!] Could common up this bounds-check build_expect
with the one in return_dest
(helper method needs take only dfb
, arr_type
as the element type can be extracted from that, and result
)
|
||
// Check all elements are none and then discard array with now droppable elements. | ||
let scan_op = ArrayScan::new(opt_elem_ty.clone().into(), Type::UNIT, vec![], size); | ||
let unit_map_func = dfb.add_load_value(unit_map_func_value(elem_ty.clone())); |
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 isn't too bad if one forgets about needing unit_map_func_value
😁 ...ok good stuff 👍
Tag::new( | ||
0, | ||
vec![ | ||
vec![elem_ty.clone(), arr_type.clone()].into(), |
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.
Getting a bit hard to read - do let tys = vec![elem_ty.clone(), arr_type.clone()].into();
earlier and then here you can just vec![tys.clone(), tys]
here
Tag::new( | ||
1, | ||
vec![ | ||
vec![elem_ty.clone(), arr_type.clone()].into(), |
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 again using the same tys
as above
fn option_wrap_func_value(elem_ty: Type) -> Value { | ||
let func_hugr = { | ||
let mut dfb = DFGBuilder::new(inout_sig( | ||
vec![elem_ty.clone()], |
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: can skip vec!
if singleton
Draft because performance benchmarks with Guppy using this are needed first to see if this is suitable. There are probably a lot of ways that some of the more repetitive code in all the
dest
functions could be generalised but for now I prioritised getting something working.Closes #2397 (but should add a new issue for doing this properly in LLVM at some point)