-
Notifications
You must be signed in to change notification settings - Fork 7
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
DRAFT: feat: Prototype dataflow analysis for static circuit extraction #1664
base: main
Are you sure you want to change the base?
Conversation
* DFContext reinstate fn hugr(), drop AsRef requirement (fixes StackOverflow) * test_tail_loop_iterates_twice: use tail_loop_builder_exts, fix from #1332(?) * Fix only-one-DataflowContext asserts using Arc::ptr_eq
…text interprets load_constant
return; | ||
} | ||
|
||
if ins.iter().any(|x| x == &PartialValue::Bottom) { |
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 this demonstrates exactly why I put in the test you comment on 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.
Yes, any bottom inputs => all bottom outputs.
That we have to check this at the linked callsite of interpret_leaf_op
and here is annoying. Perhaps interpret_leaf_op
should only be called when there are no Bottom inputs?
I think the 125 files changed is because I merged main. Annoying. |
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.
Ok... so my overall thought is this is not really a dataflow analysis. What you want is the part of dataflow analysis that does "join" at the exit of every conditional, and indeed on every control-flow merge; and possibly for TailLoops; but the things you want to join are circuits, not any kind of value that's per-qubit. (Because that join/merge logic currently only exists in dataflow analysis, "give a man a hammer and everything looks like a nail", but the other bits of the dataflow framework are really not helping here, and it'd be better to do something else - another datalog, probably, with just the bit you need. If you really want analysis like "only these cases are reachable" we should determine that using DF and then simplify the Hugr with this knowledge.)
Also, you need to do some much-more-complex factoring of classical values so that you can catch "quantum-part-of-circuit same, classical part different" which you clearly want to do. And there is the isomorphism concern....
So, overall, afraid I still think that the best approach is to say flat circuits (no nested control flow) are statically scheduled; and then we transform into that. That lets us deploy a wide range of different optimizations to get there, perhaps including this or something like it, but with flexibility to choose an optimization according to what works on a particular circuit. (I slightly favour a rewriting approach but don't mean to rule this approach out.)
hugr-passes/src/static_circuit.rs
Outdated
} | ||
} else { | ||
assert_eq!(qb_outs.len(), qb_ins.len(), "{e:?}"); | ||
let mb_in_qbs = qb_ins |
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 am finding this quite hard to follow and this is the critical part of the algorithm, so if I've got the wrong end of the stick, my conclusions may be....entirely wrong :). A few comments wouldn't hurt....
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 for looking, you've certainly identified the roughest area. Very much WIP.
hugr-passes/src/static_circuit.rs
Outdated
.collect::<Option<HashMap<_, _>>>(); | ||
let gating_qbs = mb_in_qbs | ||
.iter() | ||
.flat_map(|hash_map| hash_map.iter().map(|(_, qbh)| qbh.qb_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.
ooof. So is that equivalent to
mb_in_qbs.unwrap_or_default().values().map(QbHistory::qb_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.
So the "gating_nodes" of a new node is the ordered list of input gates, right? That could go in a comment on Gate::gating_nodes
.
I'm slightly concerned that Node
(rather than (Node, Port)
) is sufficiently accurate - the wire coming from one or another port sounds like an important difference - I think the counterargument is that if a different port were used, because of linearity other places would see different Node
instead, so we might be OK.
hugr-passes/src/static_circuit.rs
Outdated
.collect_vec(); | ||
let qb_out_to_in: HashMap<_, _> = zip_eq(qb_outs, qb_ins).collect(); | ||
for (i, v) in outs.iter_mut().enumerate() { | ||
let mb_join_val = (|| { |
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 no-args-closure-that-you-immediately-call is so that you can use ?
within it to exit just the closure, right?
Consider using continue
in the successful case, and then join
-ing with Top if you haven't continued
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.
Or even
let mut mb_join_val = PartialValue::Top;
if .... {
if .... {
....
mb_join_val = qbh.into();
}
}
v.join(mb_join_val);
hugr-passes/src/static_circuit.rs
Outdated
for (i, v) in outs.iter_mut().enumerate() { | ||
let mb_join_val = (|| { | ||
let in_p = qb_out_to_in.get(&OutgoingPort::from(i))?; | ||
let hash_map = mb_in_qbs.as_ref()?; |
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.
Note that if you did unwrap_or_default
when constructing mb_in_qbs
, so you had an empty map rather than None
, then the get(...)?
on the line below would do both ?
s in one go
hugr-passes/src/static_circuit.rs
Outdated
Some(qbh.into()) | ||
})(); | ||
|
||
v.join_mut(mb_join_val.unwrap_or(PartialValue::Top)); |
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.
Short comment - if the corresponding in-port has a QbHistory, push this gate (+ ordered list of its inputs) onto that QbHistory...
But, if this in-port is classical, join with Top. Doesn't this hit 3.py
where you have two different classical values?
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 intent is to always propagate TOP for classical outputs. In StaticCircuitPass
we only look at linear QB_T
wires, so I don't think it matters that classical wires are TOP.
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.
How does one proceed in that case (given a TOP)? Is the goal here just to identify cases which could be statically scheduled if we then did a separate transformation 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.
TOP only goes on the classical wires. If the circuit structure depends on those top values then we will fail to extract a static circuit.
Once we've extracted a static circuit, we should schedule it, then insert it at the "end" of the hugr(whatever that means). We'll need to wire up the angles. Then delete the original gate ops.
Does this answer your question?
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, "then wire up the angles" is the "separate transformation" i.e. where you add extra edges, Output node ports, etc. to get the angles from wherever they were computed.
I disagree. What is your criteria for being a dataflow analysis? Mine is: A well defined join and transfer function. I have pushed and the test cases are now working. I had to add handling for "arguments of public functions are top". I do not claim that my |
An abstract value per wire (perhaps only a subset of wires, i.e. only those of type Qubit). Here you want an abstract value per circuit (per sibling subgraph). I think the analysis framework you want is:
The use-case here then instantiates V == Circuit == I think this is necessary to handle qalloc/qfree which are not represented in the outputs (where you suggest "this might require Dense Analysis") |
Ok, so qalloc/qfree could be represented in the containing region's outputs mentioning (in their |
Data flows along wires, hence dataflow analysis attaches values (modelling data) to those wires. In order to split a circuit into per-wire "Vertical slices" we have to break up the definitive model of a circuit (i.e. a Hugr) into some per-wire variant. Your
One thing I like about the 'rewriting' approach is that it combines analysis with transformation, and I think generalizes fairly straightforwardly to handle both of those cases. But any analysis is likely to have shortcomings, which is why I prefer an input/ready-flattened format that is agnostic to analysis technique. That doesn't rule out the analysis you have here! |
I agree that qalloc and qfree are problematic, and that this approach only works when they are simply arranged with all qallocs at the beginning and all the qfrees at the end. I don't think this is unreasonable for extracting a static circuit, but perhaps I'm wrong. The reason I have inlining here is so that I get all the qallocs into main. The raw guppy calls functions that call call qalloc, which defeats the analysis for the same reasons as you describe above. We are surely in agreement that we should do transformations on the hugr first to make the analysis work better. |
Yeah ok so I think the things I would disagree with are not actually in this PR - they are the architecture, what analysis/transformation goes where in the pipeline, what the intermediates exchanged look like. That you can use dataflow analysis to extract (some) static circuits, I do not dispute :-) :-). Whether it's the best tool for the job - well that depends what else is in the toolbox, and right now the toolbox doesn't have a lot else in it and dataflow analysis is the most powerful tool in there.
Generally I would follow the school of thought that analysis enables transformation, rather than the other way around... |
Hey there and thank you for opening this pull request! 👋 We require pull request titles to follow the Conventional Commits specification Your title should look like this. The scope field is optional.
If the PR includes a breaking change, mark it with an exclamation mark:
and include a "BREAKING CHANGE:" footer in the body of the pull request. Details:
|
Based on #1603.
Includes an rough inliner and call graph.
See the new snapshots for current capabilities.