Skip to content

Commit ca4c3bd

Browse files
committed
Use VF2 to find a partial layout for seeding a SabreLayout trial
This commit builds on the VF2PartialLayout pass which was an experiment available as an external plugin here: https://github.com/mtreinish/vf2_partial_layout That pass used the vf2 algorithm in rustworkx to find the deepest partial interaction graph of a circuit which is isomorphic with the coupling graph and uses that mapping to apply an initial layout. The issue with the performance of that pass was the selection of the qubits outside the partial interaction graph. Selecting the mapping for those qubits is similar to the same heuristic layout that SabreLayout is trying to solve, just for a subset of qubits. In VF2PartialLayout a simple nearest neighbor based approach was used for selecting qubits from the coupling graph for any virtual qubits outside the partial layout. In practice this ended up performing worse than SabreLayout. To address the shortcomings of that pass this commit combines the partial layout selection from that external plugin with SabreLayout. The sabre layout algorithm starts by randomly selecting a layout and then progressively working forwards and backwards across the circuit and swap mapping it to find the permutation caused by inserted swaps. Those permutations are then used to modify the random layout and eventual an initial layout that minimizes the number of swaps needed is selected. With this commit instead of using a completely random layout for all the initial guesses this starts a single trial with the partial layout found in the same way as VF2PartialLayout. Then the remaining qubits are selected at random and the Sabrelayout algorithm is run in the same manner as before. This hopefully should improve the quality of the results because we're starting from a partial layout that doesn't require swaps for those qubits. A similar (almost identical approach) was tried in #9174 except instead of seeding a single trial with the partial layout it used the partial layout for all the the trials. In that case the results were not generally better and the results were mixed. At the time my guess was that using the partial layout constrained the search space too much and was inducing more swaps to be needed. However, looking at the details in issue #10160 this adapts #9174 to see if doing the partial layout in a more limited manner has any impact there.
1 parent 5013fe2 commit ca4c3bd

File tree

5 files changed

+260
-9
lines changed

5 files changed

+260
-9
lines changed

crates/accelerate/src/sabre_layout.rs

+43-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub fn sabre_layout_and_routing(
4141
num_swap_trials: usize,
4242
num_layout_trials: usize,
4343
seed: Option<u64>,
44+
partial_layout: Option<Vec<Option<usize>>>,
4445
) -> ([NLayout; 2], SwapMap, PyObject) {
4546
let run_in_parallel = getenv_use_multiple_threads();
4647
let outer_rng = match seed {
@@ -57,6 +58,7 @@ pub fn sabre_layout_and_routing(
5758
.into_par_iter()
5859
.enumerate()
5960
.map(|(index, seed_trial)| {
61+
let partial = if index > 0 { &partial_layout } else { &None };
6062
(
6163
index,
6264
layout_trial(
@@ -69,6 +71,7 @@ pub fn sabre_layout_and_routing(
6971
max_iterations,
7072
num_swap_trials,
7173
run_in_parallel,
74+
partial.clone(),
7275
),
7376
)
7477
})
@@ -83,7 +86,9 @@ pub fn sabre_layout_and_routing(
8386
} else {
8487
seed_vec
8588
.into_iter()
86-
.map(|seed_trial| {
89+
.enumerate()
90+
.map(|(index, seed_trial)| {
91+
let partial = if index > 0 { &partial_layout } else { &None };
8792
layout_trial(
8893
num_clbits,
8994
&mut dag_nodes,
@@ -94,6 +99,7 @@ pub fn sabre_layout_and_routing(
9499
max_iterations,
95100
num_swap_trials,
96101
run_in_parallel,
102+
partial.clone(),
97103
)
98104
})
99105
.min_by_key(|result| result.1.map.values().map(|x| x.len()).sum::<usize>())
@@ -112,13 +118,46 @@ fn layout_trial(
112118
max_iterations: usize,
113119
num_swap_trials: usize,
114120
run_swap_in_parallel: bool,
121+
partial_layout: Option<Vec<Option<usize>>>,
115122
) -> ([NLayout; 2], SwapMap, Vec<usize>) {
116123
// Pick a random initial layout and fully populate ancillas in that layout too
117124
let num_physical_qubits = distance_matrix.shape()[0];
118125
let mut rng = Pcg64Mcg::seed_from_u64(seed);
119-
let mut physical_qubits: Vec<usize> = (0..num_physical_qubits).collect();
120-
physical_qubits.shuffle(&mut rng);
121-
let mut initial_layout = NLayout::from_logical_to_physical(physical_qubits);
126+
let mut physical_qubits: Vec<usize>;
127+
match partial_layout {
128+
Some(partial_layout_bits) => {
129+
let used_bits: HashSet<usize> = partial_layout_bits
130+
.iter()
131+
.filter_map(|x| x.as_ref())
132+
.copied()
133+
.collect();
134+
let mut free_bits: Vec<usize> = (0..num_physical_qubits)
135+
.filter(|x| !used_bits.contains(x))
136+
.collect();
137+
free_bits.shuffle(&mut rng);
138+
physical_qubits = partial_layout_bits
139+
.iter()
140+
.map(|x| match x {
141+
Some(phys) => *phys,
142+
None => free_bits.pop().unwrap(),
143+
})
144+
.collect();
145+
}
146+
None => {
147+
physical_qubits = (0..num_physical_qubits).collect();
148+
physical_qubits.shuffle(&mut rng);
149+
}
150+
};
151+
let mut phys_to_logic = vec![0; num_physical_qubits];
152+
physical_qubits
153+
.iter()
154+
.enumerate()
155+
.for_each(|(logic, phys)| phys_to_logic[*phys] = logic);
156+
157+
let mut initial_layout = NLayout {
158+
logic_to_phys: physical_qubits,
159+
phys_to_logic,
160+
};
122161
let mut rev_dag_nodes: Vec<(usize, Vec<usize>, HashSet<usize>)> =
123162
dag_nodes.iter().rev().cloned().collect();
124163
for _iter in 0..max_iterations {

qiskit/transpiler/passes/layout/sabre_layout.py

+213-5
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
"""Layout selection using the SABRE bidirectional search approach from Li et al.
1414
"""
1515

16+
from collections import defaultdict
1617
import copy
1718
import logging
19+
import time
20+
1821
import numpy as np
1922
import rustworkx as rx
2023

@@ -24,6 +27,7 @@
2427
from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla
2528
from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout
2629
from qiskit.transpiler.passes.layout import disjoint_utils
30+
from qiskit.transpiler.passes.layout import vf2_utils
2731
from qiskit.transpiler.passmanager import PassManager
2832
from qiskit.transpiler.layout import Layout
2933
from qiskit.transpiler.basepasses import TransformationPass
@@ -38,18 +42,25 @@
3842
from qiskit.transpiler.target import Target
3943
from qiskit.transpiler.coupling import CouplingMap
4044
from qiskit.tools.parallel import CPU_COUNT
45+
from qiskit.circuit.controlflow import ControlFlowOp, ForLoopOp
46+
from qiskit.converters import circuit_to_dag
4147

4248
logger = logging.getLogger(__name__)
4349

4450

4551
class SabreLayout(TransformationPass):
4652
"""Choose a Layout via iterative bidirectional routing of the input circuit.
4753
48-
Starting with a random initial `Layout`, the algorithm does a full routing
49-
of the circuit (via the `routing_pass` method) to end up with a
50-
`final_layout`. This final_layout is then used as the initial_layout for
51-
routing the reverse circuit. The algorithm iterates a number of times until
52-
it finds an initial_layout that reduces full routing cost.
54+
The algorithm does a full routing of the circuit (via the `routing_pass`
55+
method) to end up with a `final_layout`. This final_layout is then used as
56+
the initial_layout for routing the reverse circuit. The algorithm iterates a
57+
number of times until it finds an initial_layout that reduces full routing cost.
58+
59+
Prior to running the SABRE algorithm this transpiler pass will try to find the layout
60+
for deepest layer that is has an isomorphic subgraph in the coupling graph. This is
61+
done by progressively using the algorithm from :class:`~.VF2Layout` on the circuit
62+
until a mapping is not found. This partial layout is then used to seed the SABRE algorithm
63+
and then random physical bits are selected for the remaining elements in the mapping.
5364
5465
This method exploits the reversibility of quantum circuits, and tries to
5566
include global circuit information in the choice of initial_layout.
@@ -85,6 +96,10 @@ def __init__(
8596
swap_trials=None,
8697
layout_trials=None,
8798
skip_routing=False,
99+
vf2_partial_layout=True,
100+
vf2_call_limit=None,
101+
vf2_time_limit=None,
102+
vf2_max_trials=None,
88103
):
89104
"""SabreLayout initializer.
90105
@@ -121,6 +136,16 @@ def __init__(
121136
will be returned in the property set. This is a tradeoff to run custom
122137
routing with multiple layout trials, as using this option will cause
123138
SabreLayout to run the routing stage internally but not use that result.
139+
vf2_partial_layout (bool): Run vf2 partial layout
140+
vf2_call_limit (int): The number of state visits to attempt in each execution of
141+
VF2 to attempt to find a partial layout.
142+
vf2_time_limit (float): The total time limit in seconds to run VF2 to find a partial
143+
layout
144+
vf2_max_trials (int): The maximum number of trials to run VF2 to find
145+
a partial layout. If this is not specified the number of trials will be limited
146+
based on the number of edges in the interaction graph or the coupling graph
147+
(whichever is larger) if no other limits are set. If set to a value <= 0 no
148+
limit on the number of trials will be set.
124149
125150
Raises:
126151
TranspilerError: If both ``routing_pass`` and ``swap_trials`` or
@@ -158,6 +183,11 @@ def __init__(
158183
self.coupling_map = copy.deepcopy(self.coupling_map)
159184
self.coupling_map.make_symmetric()
160185
self._neighbor_table = NeighborTable(rx.adjacency_matrix(self.coupling_map.graph))
186+
self.avg_error_map = None
187+
self.vf2_partial_layout = vf2_partial_layout
188+
self.call_limit = vf2_call_limit
189+
self.time_limit = vf2_time_limit
190+
self.max_trials = vf2_max_trials
161191

162192
def run(self, dag):
163193
"""Run the SabreLayout pass on `dag`.
@@ -321,6 +351,13 @@ def _inner_run(self, dag, coupling_map):
321351
cargs,
322352
)
323353
)
354+
partial_layout = None
355+
if self.vf2_partial_layout:
356+
partial_layout_virtual_bits = self._vf2_partial_layout(
357+
dag, coupling_map
358+
).get_virtual_bits()
359+
partial_layout = [partial_layout_virtual_bits.get(i, None) for i in dag.qubits]
360+
324361
((initial_layout, final_layout), swap_map, gate_order) = sabre_layout_and_routing(
325362
len(dag.clbits),
326363
dag_list,
@@ -331,6 +368,7 @@ def _inner_run(self, dag, coupling_map):
331368
self.swap_trials,
332369
self.layout_trials,
333370
self.seed,
371+
partial_layout,
334372
)
335373
# Apply initial layout selected.
336374
layout_dict = {}
@@ -385,3 +423,173 @@ def _compose_layouts(self, initial_layout, pass_final_layout, qregs):
385423
qubit_map = Layout.combine_into_edge_map(initial_layout, trivial_layout)
386424
final_layout = {v: pass_final_layout._v2p[qubit_map[v]] for v in initial_layout._v2p}
387425
return Layout(final_layout)
426+
427+
# TODO: Migrate this to rust as part of sabre_layout.rs after
428+
# https://github.com/Qiskit/rustworkx/issues/741 is implemented and released
429+
def _vf2_partial_layout(self, dag, coupling_map):
430+
"""Find a partial layout using vf2 on the deepest subgraph that is isomorphic to
431+
the coupling graph."""
432+
im_graph_node_map = {}
433+
reverse_im_graph_node_map = {}
434+
im_graph = rx.PyGraph(multigraph=False)
435+
logger.debug("Buidling interaction graphs")
436+
largest_im_graph = None
437+
best_mapping = None
438+
first_mapping = None
439+
if self.avg_error_map is None:
440+
self.avg_error_map = vf2_utils.build_average_error_map(self.target, None, coupling_map)
441+
442+
cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(coupling_map, self.seed, False)
443+
# To avoid trying to over optimize the result by default limit the number
444+
# of trials based on the size of the graphs. For circuits with simple layouts
445+
# like an all 1q circuit we don't want to sit forever trying every possible
446+
# mapping in the search space if no other limits are set
447+
if self.max_trials is None and self.call_limit is None and self.time_limit is None:
448+
im_graph_edge_count = len(im_graph.edge_list())
449+
cm_graph_edge_count = len(coupling_map.graph.edge_list())
450+
self.max_trials = max(im_graph_edge_count, cm_graph_edge_count) + 15
451+
452+
start_time = time.time()
453+
454+
# A more efficient search pattern would be to do a binary search
455+
# and find, but to conserve memory and avoid a large number of
456+
# unecessary graphs this searchs from the beginning and continues
457+
# until there is no vf2 match
458+
def _visit(dag, weight, wire_map):
459+
for node in dag.topological_op_nodes():
460+
nonlocal largest_im_graph
461+
largest_im_graph = im_graph.copy()
462+
if getattr(node.op, "_directive", False):
463+
continue
464+
if isinstance(node.op, ControlFlowOp):
465+
if isinstance(node.op, ForLoopOp):
466+
inner_weight = len(node.op.params[0]) * weight
467+
else:
468+
inner_weight = weight
469+
for block in node.op.blocks:
470+
inner_wire_map = {
471+
inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits)
472+
}
473+
_visit(circuit_to_dag(block), inner_weight, inner_wire_map)
474+
continue
475+
len_args = len(node.qargs)
476+
qargs = [wire_map[q] for q in node.qargs]
477+
if len_args == 1:
478+
if qargs[0] not in im_graph_node_map:
479+
weights = defaultdict(int)
480+
weights[node.name] += weight
481+
im_graph_node_map[qargs[0]] = im_graph.add_node(weights)
482+
reverse_im_graph_node_map[im_graph_node_map[qargs[0]]] = qargs[0]
483+
else:
484+
im_graph[im_graph_node_map[qargs[0]]][node.op.name] += weight
485+
if len_args == 2:
486+
if qargs[0] not in im_graph_node_map:
487+
im_graph_node_map[qargs[0]] = im_graph.add_node(defaultdict(int))
488+
reverse_im_graph_node_map[im_graph_node_map[qargs[0]]] = qargs[0]
489+
if qargs[1] not in im_graph_node_map:
490+
im_graph_node_map[qargs[1]] = im_graph.add_node(defaultdict(int))
491+
reverse_im_graph_node_map[im_graph_node_map[qargs[1]]] = qargs[1]
492+
edge = (im_graph_node_map[qargs[0]], im_graph_node_map[qargs[1]])
493+
if im_graph.has_edge(*edge):
494+
im_graph.get_edge_data(*edge)[node.name] += weight
495+
else:
496+
weights = defaultdict(int)
497+
weights[node.name] += weight
498+
im_graph.add_edge(*edge, weights)
499+
if len_args > 2:
500+
raise TranspilerError(
501+
"Encountered an instruction operating on more than 2 qubits, this pass "
502+
"only functions with 1 or 2 qubit operations."
503+
)
504+
vf2_mapping = rx.vf2_mapping(
505+
cm_graph,
506+
im_graph,
507+
subgraph=True,
508+
id_order=False,
509+
induced=False,
510+
call_limit=self.call_limit,
511+
)
512+
try:
513+
nonlocal first_mapping
514+
first_mapping = next(vf2_mapping)
515+
except StopIteration:
516+
break
517+
nonlocal best_mapping
518+
best_mapping = vf2_mapping
519+
elapsed_time = time.time() - start_time
520+
if (
521+
self.time_limit is not None
522+
and best_mapping is not None
523+
and elapsed_time >= self.time_limit
524+
):
525+
logger.debug(
526+
"SabreLayout VF2 heuristic has taken %s which exceeds configured max time: %s",
527+
elapsed_time,
528+
self.time_limit,
529+
)
530+
break
531+
532+
_visit(dag, 1, {bit: bit for bit in dag.qubits})
533+
logger.debug("Finding best mappings of largest partial subgraph")
534+
im_graph = largest_im_graph
535+
536+
def mapping_to_layout(layout_mapping):
537+
return Layout({reverse_im_graph_node_map[k]: v for k, v in layout_mapping.items()})
538+
539+
layout_mapping = {im_i: cm_nodes[cm_i] for cm_i, im_i in first_mapping.items()}
540+
chosen_layout = mapping_to_layout(layout_mapping)
541+
chosen_layout_score = vf2_utils.score_layout(
542+
self.avg_error_map,
543+
layout_mapping,
544+
im_graph_node_map,
545+
reverse_im_graph_node_map,
546+
im_graph,
547+
False,
548+
)
549+
trials = 1
550+
for mapping in best_mapping: # pylint: disable=not-an-iterable
551+
trials += 1
552+
logger.debug("Running trial: %s", trials)
553+
layout_mapping = {im_i: cm_nodes[cm_i] for cm_i, im_i in mapping.items()}
554+
# If the graphs have the same number of nodes we don't need to score or do multiple
555+
# trials as the score heuristic currently doesn't weigh nodes based on gates on a
556+
# qubit so the scores will always all be the same
557+
if len(cm_graph) == len(im_graph):
558+
break
559+
layout_score = vf2_utils.score_layout(
560+
self.avg_error_map,
561+
layout_mapping,
562+
im_graph_node_map,
563+
reverse_im_graph_node_map,
564+
im_graph,
565+
False,
566+
)
567+
logger.debug("Trial %s has score %s", trials, layout_score)
568+
if chosen_layout is None:
569+
chosen_layout = mapping_to_layout(layout_mapping)
570+
chosen_layout_score = layout_score
571+
elif layout_score < chosen_layout_score:
572+
layout = mapping_to_layout(layout_mapping)
573+
logger.debug(
574+
"Found layout %s has a lower score (%s) than previous best %s (%s)",
575+
layout,
576+
layout_score,
577+
chosen_layout,
578+
chosen_layout_score,
579+
)
580+
chosen_layout = layout
581+
chosen_layout_score = layout_score
582+
if self.max_trials and trials >= self.max_trials:
583+
logger.debug("Trial %s is >= configured max trials %s", trials, self.max_trials)
584+
break
585+
elapsed_time = time.time() - start_time
586+
if self.time_limit is not None and elapsed_time >= self.time_limit:
587+
logger.debug(
588+
"VF2Layout has taken %s which exceeds configured max time: %s",
589+
elapsed_time,
590+
self.time_limit,
591+
)
592+
break
593+
for reg in dag.qregs.values():
594+
chosen_layout.add_register(reg)
595+
return chosen_layout

qiskit/transpiler/preset_passmanagers/level1.py

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def _vf2_match_not_found(property_set):
156156
layout_trials=5,
157157
skip_routing=pass_manager_config.routing_method is not None
158158
and routing_method != "sabre",
159+
vf2_call_limit=int(5e4),
159160
)
160161
elif layout_method is None:
161162
_improve_layout = common.if_has_control_flow_else(
@@ -168,6 +169,7 @@ def _vf2_match_not_found(property_set):
168169
layout_trials=5,
169170
skip_routing=pass_manager_config.routing_method is not None
170171
and routing_method != "sabre",
172+
vf2_call_limit=int(5e4),
171173
),
172174
).to_flow_controller()
173175

0 commit comments

Comments
 (0)