Skip to content

Commit aca6d67

Browse files
mergify[bot]jakelishmanmtreinish
authored
Fix control-flow routing in StochasticSwap (#8880) (#8888)
* Fix control-flow routing in StochasticSwap The `StochasticSwap` pass has some fairly complex hand-offs between different parts of its API, including in recursive calls and the regular hand-off between Python and Rust. In the course of adding the control-flow support, some of these became muddled, and the mapping between different virtual/physical/integer representations got mixed up, resulting in invalid swaps being output in the final circuit. This commit simplifies much of the internal mapping, removing many superfluous `DAGCircuit` creations and compositions. This also removes instances where two layouts were "chained"; this was not well typed (the output of a "virtual -> physical" mapping can't be the input for another "virtual -> physical" mapping), and in general was being used to "undo" some of compositions that were about to be applied. This fixes a tacit assumption in the original code that the initial layout was a trivial layout in the hand-off between Rust and Python. This worked until the recursive call added the `initial_layout` option, making this assumption invalid. Previously, virtual qubit bit instances were converted to integers (to allow them to be passed to Rust) using their indices into the original DAG, but the integer outputs were then converted _back_ using the `initial_layout`. In the old form, this worked anyway, but wasn't logically correct and consequently broke when the assumptions about `initial_layout` changed. For the recursive calls, we now ensure that the inner passes are essentially created with the same internal structure as the outer pass; the passed in `DAGCircuit` uses the same bit instances and same meaning of the virtual qubits as the outer circuit, and the `initial_layout` ensures that the inner passes start with at the same layout as the outer pass. This makes the inner passes more like a logical continuation of the current operation, rather than a completely separate entity that needs to have its virtual qubits remapped. The changes to the tests are twofold: - move the `CheckMap` calls earlier and apply them directly to the `StochasticSwap` output rather than the expected circuit, to improve the quality of failure error messages - use the same physical qubits inside the expected control-flow blocks; the new simpler form of doing the circuit rewriting internally in the pass ensures that the same bit objects are used all the way through the control-flow stack now, rather than creating new instances. * Add tests for stochastic swap valid output This commit adds full path transpile() tests for running with stochastic swap that validates a full path transpilation outputs a valid physical circuit. These tests are purposefully high level to provide some validation that stochastic swap is not creating invalid output by inserting incorrect swaps. It's not meant as a test of valid unitary equivalent output of the full transpilation. Co-authored-by: Jake Lishman <jake.lishman@ibm.com> Co-authored-by: Matthew Treinish <mtreinish@kortar.org> (cherry picked from commit b3cf64f) Co-authored-by: Jake Lishman <jake.lishman@ibm.com> Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
1 parent a04df33 commit aca6d67

File tree

3 files changed

+268
-235
lines changed

3 files changed

+268
-235
lines changed

qiskit/transpiler/passes/routing/stochastic_swap.py

+45-83
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp
2727
from qiskit._accelerate import stochastic_swap as stochastic_swap_rs
2828

29-
from .utils import combine_permutations, get_swap_map_dag
29+
from .utils import get_swap_map_dag
3030

3131
logger = logging.getLogger(__name__)
3232

@@ -70,7 +70,8 @@ def __init__(self, coupling_map, trials=20, seed=None, fake_run=False, initial_l
7070
self.fake_run = fake_run
7171
self.qregs = None
7272
self.initial_layout = initial_layout
73-
self._qubit_indices = None
73+
self._qubit_to_int = None
74+
self._int_to_qubit = None
7475

7576
def run(self, dag):
7677
"""Run the StochasticSwap pass on `dag`.
@@ -97,7 +98,10 @@ def run(self, dag):
9798
canonical_register = dag.qregs["q"]
9899
if self.initial_layout is None:
99100
self.initial_layout = Layout.generate_trivial_layout(canonical_register)
100-
self._qubit_indices = {bit: idx for idx, bit in enumerate(dag.qubits)}
101+
# Qubit indices are used to assign an integer to each virtual qubit during the routing: it's
102+
# a mapping of {virtual: virtual}, for converting between Python and Rust forms.
103+
self._qubit_to_int = {bit: idx for idx, bit in enumerate(dag.qubits)}
104+
self._int_to_qubit = tuple(dag.qubits)
101105

102106
self.qregs = dag.qregs
103107
logger.debug("StochasticSwap rng seeded with seed=%s", self.seed)
@@ -174,18 +178,18 @@ def _layer_permutation(self, layer_partition, layout, qubit_subset, coupling, tr
174178

175179
cdist2 = coupling._dist_matrix**2
176180
int_qubit_subset = np.fromiter(
177-
(self._qubit_indices[bit] for bit in qubit_subset),
181+
(self._qubit_to_int[bit] for bit in qubit_subset),
178182
dtype=np.uintp,
179183
count=len(qubit_subset),
180184
)
181185

182186
int_gates = np.fromiter(
183-
(self._qubit_indices[bit] for gate in gates for bit in gate),
187+
(self._qubit_to_int[bit] for gate in gates for bit in gate),
184188
dtype=np.uintp,
185189
count=2 * len(gates),
186190
)
187191

188-
layout_mapping = {self._qubit_indices[k]: v for k, v in layout.get_virtual_bits().items()}
192+
layout_mapping = {self._qubit_to_int[k]: v for k, v in layout.get_virtual_bits().items()}
189193
int_layout = stochastic_swap_rs.NLayout(layout_mapping, num_qubits, coupling.size())
190194

191195
trial_circuit = DAGCircuit() # SWAP circuit for slice of swaps in this trial
@@ -204,16 +208,15 @@ def _layer_permutation(self, layer_partition, layout, qubit_subset, coupling, tr
204208
edges,
205209
seed=self.seed,
206210
)
207-
# If we have no best circuit for this layer, all of the
208-
# trials have failed
211+
# If we have no best circuit for this layer, all of the trials have failed
209212
if best_layout is None:
210213
logger.debug("layer_permutation: failed!")
211214
return False, None, None, None
212215

213216
edges = best_edges.edges()
214217
for idx in range(len(edges) // 2):
215-
swap_src = self.initial_layout._p2v[edges[2 * idx]]
216-
swap_tgt = self.initial_layout._p2v[edges[2 * idx + 1]]
218+
swap_src = self._int_to_qubit[edges[2 * idx]]
219+
swap_tgt = self._int_to_qubit[edges[2 * idx + 1]]
217220
trial_circuit.apply_operation_back(SwapGate(), [swap_src, swap_tgt], [])
218221
best_circuit = trial_circuit
219222

@@ -234,24 +237,17 @@ def _layer_update(self, dag, layer, best_layout, best_depth, best_circuit):
234237
best_depth (int): depth returned from _layer_permutation
235238
best_circuit (DAGCircuit): swap circuit returned from _layer_permutation
236239
"""
237-
layout = best_layout
238-
logger.debug("layer_update: layout = %s", layout)
240+
logger.debug("layer_update: layout = %s", best_layout)
239241
logger.debug("layer_update: self.initial_layout = %s", self.initial_layout)
240242

241243
# Output any swaps
242244
if best_depth > 0:
243245
logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth)
244-
dag.compose(best_circuit)
246+
dag.compose(best_circuit, qubits={bit: bit for bit in best_circuit.qubits})
245247
else:
246248
logger.debug("layer_update: there are no swaps in this layer")
247249
# Output this layer
248-
layer_circuit = layer["graph"]
249-
initial_v2p = self.initial_layout.get_virtual_bits()
250-
new_v2p = layout.get_virtual_bits()
251-
initial_order = [initial_v2p[qubit] for qubit in dag.qubits]
252-
new_order = [new_v2p[qubit] for qubit in dag.qubits]
253-
order = combine_permutations(initial_order, new_order)
254-
dag.compose(layer_circuit, qubits=order)
250+
dag.compose(layer["graph"], qubits=best_layout.reorder_bits(dag.qubits))
255251

256252
def _mapper(self, circuit_graph, coupling_graph, trials=20):
257253
"""Map a DAGCircuit onto a CouplingMap using swap gates.
@@ -376,29 +372,18 @@ def _controlflow_layer_update(self, dagcircuit_output, layer_dag, current_layout
376372
"""
377373
cf_opnode = layer_dag.op_nodes()[0]
378374
if isinstance(cf_opnode.op, IfElseOp):
379-
updated_ctrl_op, cf_layout, idle_qubits = self._route_control_flow_multiblock(
375+
new_op, new_qargs, new_layout = self._route_control_flow_multiblock(
380376
cf_opnode, current_layout, root_dag
381377
)
382378
elif isinstance(cf_opnode.op, (ForLoopOp, WhileLoopOp)):
383-
updated_ctrl_op, cf_layout, idle_qubits = self._route_control_flow_looping(
379+
new_op, new_qargs, new_layout = self._route_control_flow_looping(
384380
cf_opnode, current_layout, root_dag
385381
)
386382
else:
387383
raise TranspilerError(f"unsupported control flow operation: {cf_opnode}")
388-
if self.fake_run:
389-
return cf_layout
390-
391-
cf_layer_dag = DAGCircuit()
392-
cf_qubits = [qubit for qubit in root_dag.qubits if qubit not in idle_qubits]
393-
qreg = QuantumRegister(len(cf_qubits), "q")
394-
cf_layer_dag.add_qreg(qreg)
395-
for creg in layer_dag.cregs.values():
396-
cf_layer_dag.add_creg(creg)
397-
cf_layer_dag.apply_operation_back(updated_ctrl_op, cf_layer_dag.qubits, cf_opnode.cargs)
398-
target_qubits = [qubit for qubit in dagcircuit_output.qubits if qubit not in idle_qubits]
399-
order = current_layout.reorder_bits(target_qubits)
400-
dagcircuit_output.compose(cf_layer_dag, qubits=order)
401-
return cf_layout
384+
if not self.fake_run:
385+
dagcircuit_output.apply_operation_back(new_op, new_qargs, cf_opnode.cargs)
386+
return new_layout
402387

403388
def _new_seed(self):
404389
"""Get a seed for a new RNG instance."""
@@ -432,19 +417,19 @@ def _route_control_flow_multiblock(self, node, current_layout, root_dag):
432417
433418
Returns:
434419
ControlFlowOp: routed control flow operation.
435-
final_layout (Layout): layout after instruction.
436-
list(Qubit): list of idle qubits in controlflow layer.
420+
List[Qubit]: the new physical-qubit arguments that the output `ControlFlowOp` should be
421+
applied to. This might be wider than the input node if internal routing was needed.
422+
Layout: the new layout after the control-flow operation is applied.
437423
"""
438424
# For each block, expand it up be the full width of the containing DAG so we can be certain
439425
# that it is routable, then route it within that. When we recombine later, we'll reduce all
440426
# these blocks down to remove any qubits that are idle.
441427
block_dags = []
442428
block_layouts = []
443-
order = [self._qubit_indices[bit] for bit in node.qargs]
444429
for block in node.op.blocks:
445430
inner_pass = self._recursive_pass(current_layout)
446431
full_dag_block = root_dag.copy_empty_like()
447-
full_dag_block.compose(circuit_to_dag(block), qubits=order)
432+
full_dag_block.compose(circuit_to_dag(block), qubits=node.qargs)
448433
block_dags.append(inner_pass.run(full_dag_block))
449434
block_layouts.append(inner_pass.property_set["final_layout"].copy())
450435

@@ -455,42 +440,28 @@ def _route_control_flow_multiblock(self, node, current_layout, root_dag):
455440
deepest_index = np.argmax([block.depth(recurse=True) for block in block_dags])
456441
final_layout = block_layouts[deepest_index]
457442
if self.fake_run:
458-
return None, final_layout, None
459-
p2v = current_layout.get_physical_bits()
443+
return None, None, final_layout
460444
idle_qubits = set(root_dag.qubits)
461445
for i, updated_dag_block in enumerate(block_dags):
462446
if i != deepest_index:
463-
swap_circuit, swap_qubits = get_swap_map_dag(
447+
swap_dag, swap_qubits = get_swap_map_dag(
464448
root_dag,
465449
self.coupling_map,
466450
block_layouts[i],
467451
final_layout,
468452
seed=self._new_seed(),
469453
)
470-
if swap_circuit.depth():
471-
virtual_swap_dag = updated_dag_block.copy_empty_like()
472-
order = [p2v[virtual_swap_dag.qubits.index(qubit)] for qubit in swap_qubits]
473-
virtual_swap_dag.compose(swap_circuit, qubits=order)
474-
updated_dag_block.compose(virtual_swap_dag)
454+
if swap_dag.depth():
455+
updated_dag_block.compose(swap_dag, qubits=swap_qubits)
475456
idle_qubits &= set(updated_dag_block.idle_wires())
476457

477458
# Now for each block, expand it to be full width over all active wires (all blocks of a
478459
# control-flow operation need to have equal input wires), and convert it to circuit form.
479460
block_circuits = []
480461
for i, updated_dag_block in enumerate(block_dags):
481462
updated_dag_block.remove_qubits(*idle_qubits)
482-
new_dag_block = DAGCircuit()
483-
new_num_qubits = updated_dag_block.num_qubits()
484-
qreg = QuantumRegister(new_num_qubits, "q")
485-
new_dag_block.add_qreg(qreg)
486-
for creg in updated_dag_block.cregs.values():
487-
new_dag_block.add_creg(creg)
488-
for inner_node in updated_dag_block.op_nodes():
489-
new_qargs = [qreg[updated_dag_block.qubits.index(bit)] for bit in inner_node.qargs]
490-
new_dag_block.apply_operation_back(inner_node.op, new_qargs, inner_node.cargs)
491-
block_circuits.append(dag_to_circuit(new_dag_block))
492-
493-
return node.op.replace_blocks(block_circuits), final_layout, idle_qubits
463+
block_circuits.append(dag_to_circuit(updated_dag_block))
464+
return node.op.replace_blocks(block_circuits), block_circuits[0].qubits, final_layout
494465

495466
def _route_control_flow_looping(self, node, current_layout, root_dag):
496467
"""Route a control-flow operation that represents a loop, such as :class:`.ForOpLoop` or
@@ -505,45 +476,36 @@ def _route_control_flow_looping(self, node, current_layout, root_dag):
505476
506477
Returns:
507478
ControlFlowOp: routed control flow operation.
508-
Layout: layout after instruction (this will be the same as the input layout).
509-
list(Qubit): list of idle qubits in controlflow layer.
479+
List[Qubit]: the new physical-qubit arguments that the output `ControlFlowOp` should be
480+
applied to. This might be wider than the input node if internal routing was needed.
481+
Layout: the new layout after the control-flow operation is applied.
510482
"""
511483
if self.fake_run:
512-
return None, current_layout, None
484+
return None, None, current_layout
513485
# Temporarily expand to full width, and route within that.
514486
inner_pass = self._recursive_pass(current_layout)
515-
order = [self._qubit_indices[bit] for bit in node.qargs]
516487
full_dag_block = root_dag.copy_empty_like()
517-
full_dag_block.compose(circuit_to_dag(node.op.blocks[0]), qubits=order)
488+
full_dag_block.compose(circuit_to_dag(node.op.blocks[0]), qubits=node.qargs)
518489
updated_dag_block = inner_pass.run(full_dag_block)
519490

520491
# Ensure that the layout at the end of the block is returned to being the layout at the
521492
# start of the block again, so the loop works.
522-
swap_circuit, swap_qubits = get_swap_map_dag(
493+
swap_dag, swap_qubits = get_swap_map_dag(
523494
root_dag,
524495
self.coupling_map,
525496
inner_pass.property_set["final_layout"],
526497
current_layout,
527498
seed=self._new_seed(),
528499
)
529-
if swap_circuit.depth():
530-
p2v = current_layout.get_physical_bits()
531-
virtual_swap_dag = updated_dag_block.copy_empty_like()
532-
order = [p2v[virtual_swap_dag.qubits.index(qubit)] for qubit in swap_qubits]
533-
virtual_swap_dag.compose(swap_circuit, qubits=order)
534-
updated_dag_block.compose(virtual_swap_dag)
500+
if swap_dag.depth():
501+
updated_dag_block.compose(swap_dag, qubits=swap_qubits)
535502

536503
# Contract the routed block back down to only operate on the qubits that it actually needs.
537504
idle_qubits = set(root_dag.qubits) & set(updated_dag_block.idle_wires())
538505
updated_dag_block.remove_qubits(*idle_qubits)
539-
new_dag_block = DAGCircuit()
540-
new_num_qubits = updated_dag_block.num_qubits()
541-
qreg = QuantumRegister(new_num_qubits, "q")
542-
new_dag_block.add_qreg(qreg)
543-
for creg in updated_dag_block.cregs.values():
544-
new_dag_block.add_creg(creg)
545-
for inner_node in updated_dag_block.op_nodes():
546-
new_qargs = [qreg[updated_dag_block.qubits.index(bit)] for bit in inner_node.qargs]
547-
new_dag_block.apply_operation_back(inner_node.op, new_qargs, inner_node.cargs)
548-
updated_circ_block = dag_to_circuit(new_dag_block)
549-
return node.op.replace_blocks([updated_circ_block]), current_layout, idle_qubits
506+
updated_circ_block = dag_to_circuit(updated_dag_block)
507+
return (
508+
node.op.replace_blocks([updated_circ_block]),
509+
updated_dag_block.qubits,
510+
current_layout,
511+
)

qiskit/transpiler/passes/routing/utils.py

+9-34
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,24 @@
1212

1313
"""Utility functions for routing"""
1414

15-
from qiskit.transpiler import CouplingMap
1615
from qiskit.transpiler.exceptions import TranspilerError
1716
from .algorithms import ApproximateTokenSwapper
1817

1918

20-
def combine_permutations(*permutations):
21-
"""
22-
Chain a series of permutations.
23-
24-
Args:
25-
*permutations (list(int)): permutations to combine
26-
27-
Returns:
28-
list: combined permutation
29-
"""
30-
order = permutations[0]
31-
for this_order in permutations[1:]:
32-
order = [order[i] for i in this_order]
33-
return order
34-
35-
3619
def get_swap_map_dag(dag, coupling_map, from_layout, to_layout, seed, trials=4):
37-
"""Get the circuit of swaps to go from from_layout to to_layout, and the qubit ordering of the
38-
qubits in that circuit."""
39-
20+
"""Get the circuit of swaps to go from from_layout to to_layout, and the physical qubits
21+
(integers) that the swap circuit should be applied on."""
4022
if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None:
4123
raise TranspilerError("layout transformation runs on physical circuits only")
42-
4324
if len(dag.qubits) > len(coupling_map.physical_qubits):
4425
raise TranspilerError("The layout does not match the amount of qubits in the DAG")
45-
46-
if coupling_map:
47-
graph = coupling_map.graph.to_undirected()
48-
else:
49-
coupling_map = CouplingMap.from_full(len(to_layout))
50-
graph = coupling_map.graph.to_undirected()
51-
52-
token_swapper = ApproximateTokenSwapper(graph, seed)
26+
token_swapper = ApproximateTokenSwapper(coupling_map.graph.to_undirected(), seed)
5327
# Find the permutation between the initial physical qubits and final physical qubits.
5428
permutation = {
55-
pqubit: to_layout.get_virtual_bits()[vqubit]
56-
for vqubit, pqubit in from_layout.get_virtual_bits().items()
29+
pqubit: to_layout[vqubit] for vqubit, pqubit in from_layout.get_virtual_bits().items()
5730
}
58-
permutation_circ = token_swapper.permutation_circuit(permutation, trials)
59-
permutation_qubits = [dag.qubits[i] for i in sorted(permutation_circ.inputmap.keys())]
60-
return permutation_circ.circuit, permutation_qubits
31+
# The mapping produced here maps physical qubit indices of the outer dag to the bits used to
32+
# represent them in the inner map. For later composing, we actually want the opposite map.
33+
swap_circuit, phys_to_circuit_qubits = token_swapper.permutation_circuit(permutation, trials)
34+
circuit_to_phys = {inner: outer for outer, inner in phys_to_circuit_qubits.items()}
35+
return swap_circuit, [circuit_to_phys[bit] for bit in swap_circuit.qubits]

0 commit comments

Comments
 (0)