-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Add controlflow handling to stochastic swap pass #8418
Conversation
Thank you for opening a new pull request. Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient. While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone. One or more of the the following people are requested to review this:
|
Pull Request Test Coverage Report for Build 3200296423
💛 - Coveralls |
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've left a few comments throughout. I think the general strategy of treating the interior of each block separately, and inverting the swap map back to the starting state at the end is fine for now, and in the future we can consider an approach where we choose the "best" output permutation in order to reduce unnecessary swaps.
I think we need much more stringent tests of the actual layouts in the tests - using Aer to test that the simulation works isn't really sufficient to test that the layout is both valid, and has the properties we expect.
We agreed in the dynamic circuits meeting that since break
/continue
handling is difficult (especially in the absence of some sort of true CFG), so let's just error out on them for now, and remove all handling of them.
(A couple of my review comments are probably a little out-of-date since Kevin's comments on the basic swap routing too.
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 wrote quite a bit of text here, but then I realised that they're all symptoms of the same underlying thing: that the routing pass assumes all control-flow operations are full width of the circuit. I had thought we'd decided we didn't want to go that route? Certainly for our demo purposes, we don't want to go there - we want to be able to convert c_if
to IfElseOp
for single instructions (only 1q or 2q blocks), and have those scheduled at the same time. This form wouldn't permit that.
To have a working demo, we really need to have that working at not full width. I'm sorry I didn't see this sooner - I had thought it was already working with them.
Below is mostly what I wrote before I noticed this.
I had the same issues with this PR as the CheckMap
one when using control-flow bodies that don't have the exact same bits as the outer circuit. The following bits are a few cases where that came up.
I found that this threw an error when I used a control-flow builder and tried to route the circuit, such as:
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.passes import StochasticSwap
cm = CouplingMap.from_line(2)
qr = QuantumRegister(2, "q")
qc = QuantumCircuit(qr)
with qc.for_loop((0,)):
qc.cx(0, 1)
StochasticSwap(cm)(qc)
The same happened if I built a loop body as
loop_body = QuantumCircuit(2)
loop_body.cx(0, 1)
qc.for_loop((0,), None, loop_body, [0, 1], [])
It's possible that this isn't something we really need to care about because layout passes should always run before it, though if there's an easy way to have it work, I wouldn't be opposed to it. I don't know if it's the same issue, but I also tried this with a full layout pass, and got the same error message:
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.transpiler import CouplingMap, PassManager
from qiskit.transpiler.passes import (
StochasticSwap,
TrivialLayout,
FullAncillaAllocation,
EnlargeWithAncilla,
ApplyLayout,
)
cm = CouplingMap.from_line(2)
qr = QuantumRegister(2, "q")
qc = QuantumCircuit(qr)
with qc.for_loop((0,)):
qc.cx(0, 1)
pm = PassManager(
[
TrivialLayout(cm),
FullAncillaAllocation(cm),
EnlargeWithAncilla(),
ApplyLayout(),
StochasticSwap(cm),
]
)
pm.run(qc)
That might suggest we need some further handling in ApplyLayout
, perhaps?
Forcing through these two by using a QuantumRegister
for the loop body with exactly the right bits and the name q
throws a different error, which I think is related:
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.transpiler import CouplingMap, PassManager
from qiskit.transpiler.passes import (
StochasticSwap,
TrivialLayout,
FullAncillaAllocation,
EnlargeWithAncilla,
ApplyLayout,
)
cm = CouplingMap.from_line(5)
qc = QuantumCircuit(5)
qc.cx(0, 2) # Not physical
for_loop = QuantumCircuit(QuantumRegister(name="q", bits=qc.qubits[3:]))
for_loop.cx(0, 1)
qc.for_loop((0,), None, for_loop, [3, 4], []) # Physical
pm = PassManager(
[
TrivialLayout(cm),
FullAncillaAllocation(cm),
EnlargeWithAncilla(),
ApplyLayout(),
StochasticSwap(cm),
]
)
pm.run(qc)
This gives a key lookup error when using the layout in the internal routing of the control-flow operation.
A couple of issues I noticed while allowing control flow instructions to be less than full width:
|
For 1: when this method has been used in other transpiler passes, I've put in an explicit For 2: that seems a bit worrying. At a guess, perhaps it's if a clbit that's in the |
That's a possibility. I usually didn't intend to remove idle classical bits but it's possible they slipped through somehere. The pass only considers wires as idle as the intersection of idle wires of each block. |
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 form of removing idle wires really does look a lot better, thanks. My biggest user-facing concern is about the disappearing operations that come from the (imo incorrect) layer-based determination on whether something is "control-flow" or not.
Internally, though, I do think we could improve the code clarity throughout - the abbreviated variable names are quite hard to read in several places, and the utility functions are very dense with little explanation as to what's happening. I think with neater API boundaries, and perhaps a bit more unification of the two utility routing functions (and maybe one or two well done helper functions), it could really help. I've left some comments throughout with some suggestions.
@@ -43,7 +50,11 @@ class StochasticSwap(TransformationPass): | |||
the circuit. | |||
""" | |||
|
|||
def __init__(self, coupling_map, trials=20, seed=None, fake_run=False): | |||
_count = 0 # track number of instances of this class |
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 don't think this is necessary any more? (And if it is, I'm really not keen because it feels very magic, and the way it's used isn't thread-safe.)
(Also, it perhaps wouldn't be a bad idea to rebase this PR to get rid of the accidentally re-applied commits from |
06dfe40
to
2efddcb
Compare
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 the latest changes - it's getting easier to read, and the layering bug I had seen does appear to have gone. In the interests of timings now, since we're quite far beyond the deadline for rc1, I'm actioning the things I've suggested in this review myself, and Matthew's also going on bug patrol to try and find failing cases.
Right now, it's probably best if you don't make changes here - I can either (force) push mine directly onto this PR, or I can open a new PR, but it'll be easier to keep in sync if only one of us is doing stuff.
I tried to run this (after merging in main
) with:
import qiskit
from qiskit.transpiler import CouplingMap
cm = CouplingMap.from_line(6)
qc = qiskit.QuantumCircuit(6, 1)
qc.h(0)
with qc.for_loop((1,)):
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(0, 3)
qc.measure(0, 0)
with qc.if_test((0, True)) as else_:
qc.cx(1, 4)
qc.cx(1, 5)
with else_:
qc.cx(4, 5)
qc.cx(2, 3)
qiskit.transpile(
qc,
coupling_map=cm,
routing_method="stochastic",
basis_gates=["rz", "sx", "cx", "for_loop", "if_else", "while_loop"],
)
and go a later KeyError
in the BasisTranslator
:
[ ... snip transpile machinery ... ]
~/code/qiskit/terra/qiskit/transpiler/passes/basis/basis_translator.py in run(self, dag)
240 return dag_updated
241
--> 242 apply_translation(dag)
243 replace_end_time = time.time()
244 logger.info(
~/code/qiskit/terra/qiskit/transpiler/passes/basis/basis_translator.py in apply_translation(dag)
215 for block in node.op.blocks:
216 dag_block = circuit_to_dag(block)
--> 217 dag_updated = apply_translation(dag_block)
218 if dag_updated:
219 flow_circ_block = dag_to_circuit(dag_block)
~/code/qiskit/terra/qiskit/transpiler/passes/basis/basis_translator.py in apply_translation(dag)
208 dag_updated = False
209 for node in dag.op_nodes():
--> 210 node_qargs = tuple(qarg_indices[bit] for bit in node.qargs)
211 qubit_set = frozenset(node_qargs)
212 if node.name in target_basis:
~/code/qiskit/terra/qiskit/transpiler/passes/basis/basis_translator.py in <genexpr>(.0)
208 dag_updated = False
209 for node in dag.op_nodes():
--> 210 node_qargs = tuple(qarg_indices[bit] for bit in node.qargs)
211 qubit_set = frozenset(node_qargs)
212 if node.name in target_basis:
KeyError: Qubit(QuantumRegister(4, 'q'), 0)
It's not clear to me whether this pass produces an invalid circuit somewhere, or if it's a bug in the BasisTranslator
, but it's something to find.
The general procedure is that whenever we encounter a control-flow operation, we: - handle the entire layer serially (skipping the full-layer optimisation) - temporarily expand the operation to be full width on the outer DAG - recursively route each block of the control-flow at this full width - for each block, add swaps to the end to ensure that all the final layouts match, including any constraints (such as for looping blocks, the final layout needs to match the starting layout). - contract all the blocks to operate only on qubits that are non-idle in all the blocks - add this to the outer circuit, and continue. This approach currently does not handle `break` and `continue` statements; the control-flow structures of the DAG make it rather tricky to describe these correctly right now, and these are considered "advanced" uses that we're not supporting in our initial passes at control flow. This commit is the squashed content of Qiskitgh-8418 as of commit 8f583c9. Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
This improves the internal documentation of the utility routing passes to more accurately and clearly describe what is going on. A few variables are renamed to avoid abbreviations, and reduce unnecessary temporaries that added naming complexity.
Currently, the utility functions were attempting to be general in order to be used by multiple routing algorithms, but made some assumptions about the form of the input routing pass that essentially limited it to being `StochasticSwap`. Trying to be general also complicated the API boundary unnecessarily, so for now, we simply bring them into `StochasticSwap` itself. If we choose to make them more general in the future, they can potentially move back. Our premier routing algorithm is Sabre, however, which is principally implemented in Rust, which likely cannot make use of these utilities. The other Python-space routers are generally too slow and inefficient to get much use. This then further simplifies the APIs to avoid passing unnecessary state around and making things more complex.
8f583c9
to
5dc3e98
Compare
For `fake_run`, we just need the layout property to be set correctly, so we don't actually need to be rebuilding the output DAG at every stage.
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.
From my perspective we're at the point where we should merge this now, and get 0.22rc1 out the door. I've done quite a bit of coding on this PR, though, so it'd be good to get a second set of eyes on it to check for any issues I've become blind to.
Thanks Erick for all the work and all the changes!
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 did a quick pass through the code a couple of small nits inline but nothing worth blocking over (one maybe something we want to explore in 0.23.0). Thanks for all the hard work @ewinston @jakelishman
@@ -74,7 +74,7 @@ def semantic_eq(node1, node2, bit_indices1=None, bit_indices2=None): | |||
node2_cargs = [bit_indices2[carg] for carg in node2.cargs] | |||
|
|||
# For barriers, qarg order is not significant so compare as sets | |||
if "barrier" == node1.op.name == node2.op.name: | |||
if node1.op.name == node2.op.name and node1.name in {"barrier", "swap"}: |
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.
What about cz? But I feel like this should be an operation property that the qarg order doesn't matter because it comes up in other places too like GateDirection
(right now we hard code cz, but swap will have the same problem)
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.
cz
used to be in this list as well in an earlier iteration of this PR - I don't know why it got dropped. Was there a reason, @ewinston?
|
||
def _new_seed(self): | ||
"""Get a seed for a new RNG instance.""" | ||
return self.rng.integers(0x7FFF_FFFF_FFFF_FFFF) |
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 guess there's not constant we could use for signed 32bit int max. Maybe a comment to explain it would be nice, but it was obvious to me at least.
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.
oh yeah, I didn't really think. There's np.iinfo(np.int64).max
, I suppose?
Summary
Adds controlflow handling to StochasticSwap routing pass.
Depends on #8793
Details and comments