From 42ebe34d922800814935f484278bccc860ff914f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 8 Nov 2022 09:40:37 -0500 Subject: [PATCH 01/24] Oxidize SabreLayout pass This commit modifies the SabreLayout pass when run without the routing_pass argument to run primarily in Rust. This builds on top of the rust version of SabreSwap previously added in #7977, #8388, and #8572. Internally, when the routing_pass argument is not set SabreLayout will perform the full sabre algorithm both layout selection and final swap mapping in rust and return the selected initial layout, the final layout, the toplogical sorting used to traverse the circuit, and a SwapMap for any swaps inserted. This is then used to build the output circuit in place of running separate layout and routing passes. The preset pass managers are updated to handle the new combined layout and routing mode of operation for SabreLayout. The routing stage to the preset pass managers remains intact, it will just operate as if a perfect layout was selected and skip SabreSwap because the circuit is already matching the connectivity constraints. Besides just operating more quickly because the heavy lifting of the algorithm operates more efficiently in a compiled language, doing this in rust also lets change our parallelization model for running multiple seed in Sabre. Just as in #8572 we added support for SabreSwap to run multiple parallel trials with different seeds this commit adds a layout_trials argument to SabreLayout to try multiple seeds in parallel. When this is used it parallelizes at the outer layer for each layout/routing combination and the total minimal swap count seed is used. So for example if you set swap_trials=5 and layout_trails=5 that will run 5 tasks in the threadpool with 5 different seeds for the outer layout run. Inside that every time sabre swap is run (which will be multiple times as part of layout plus the final routing run) it tries 5 different seeds for each execution serially inside that parallel task. This should hopefully further improve the quality of the transpiler output and better match expectations for users who were previously calling transpile() multiple times to emulate this behavior. Implements #9090 --- qiskit/__init__.py | 1 + .../transpiler/passes/layout/sabre_layout.py | 184 ++++++++++--- .../transpiler/passes/routing/sabre_swap.py | 88 ++++--- .../transpiler/preset_passmanagers/level0.py | 9 +- .../transpiler/preset_passmanagers/level1.py | 9 +- .../transpiler/preset_passmanagers/level2.py | 9 +- .../transpiler/preset_passmanagers/level3.py | 9 +- src/lib.rs | 2 + src/sabre_layout.rs | 241 ++++++++++++++++++ src/sabre_swap/mod.rs | 37 ++- tox.ini | 2 +- 11 files changed, 516 insertions(+), 75 deletions(-) create mode 100644 src/sabre_layout.rs diff --git a/qiskit/__init__.py b/qiskit/__init__.py index e272ec304d3d..2b6d68fdd709 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -37,6 +37,7 @@ sys.modules["qiskit._accelerate.nlayout"] = qiskit._accelerate.nlayout sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap sys.modules["qiskit._accelerate.sabre_swap"] = qiskit._accelerate.sabre_swap +sys.modules["qiskit._accelerate.sabre_layout"] = qiskit._accelerate.sabre_layout sys.modules["qiskit._accelerate.pauli_expval"] = qiskit._accelerate.pauli_expval sys.modules["qiskit._accelerate.dense_layout"] = qiskit._accelerate.dense_layout sys.modules["qiskit._accelerate.sparse_pauli_op"] = qiskit._accelerate.sparse_pauli_op diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 3c3abdaf3dce..f4328f1d93ca 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -13,24 +13,33 @@ """Layout selection using the SABRE bidirectional search approach from Li et al. """ +import copy import logging import numpy as np +import retworkx from qiskit.converters import dag_to_circuit from qiskit.transpiler.passes.layout.set_layout import SetLayout from qiskit.transpiler.passes.layout.full_ancilla_allocation import FullAncillaAllocation from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout -from qiskit.transpiler.passes.routing import SabreSwap from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.layout import Layout -from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit._accelerate.nlayout import NLayout +from qiskit._accelerate.sabre_layout import sabre_layout_and_routing +from qiskit._accelerate.sabre_swap import ( + Heuristic, + NeighborTable, +) +from qiskit.transpiler.passes.routing.sabre_swap import process_swaps, apply_gate +from qiskit.tools.parallel import CPU_COUNT logger = logging.getLogger(__name__) -class SabreLayout(AnalysisPass): +class SabreLayout(TransformationPass): """Choose a Layout via iterative bidirectional routing of the input circuit. Starting with a random initial `Layout`, the algorithm does a full routing @@ -50,7 +59,13 @@ class SabreLayout(AnalysisPass): """ def __init__( - self, coupling_map, routing_pass=None, seed=None, max_iterations=3, swap_trials=None + self, + coupling_map, + routing_pass=None, + seed=None, + max_iterations=3, + swap_trials=None, + layout_trials=None, ): """SabreLayout initializer. @@ -71,6 +86,8 @@ def __init__( on the number of trials run. This option is mutually exclusive with the ``routing_pass`` argument and an error will be raised if both are used. + layout_trials (int): The number of random seed trials to run + layout with. Raises: TranspilerError: If both ``routing_pass`` and ``swap_trials`` are @@ -78,13 +95,27 @@ def __init__( """ super().__init__() self.coupling_map = coupling_map - if routing_pass is not None and swap_trials is not None: + self._neighbor_table = None + if self.coupling_map is not None: + if not self.coupling_map.is_symmetric: + self.coupling_map = copy.copy(self.coupling_map) + self.coupling_map.make_symmetric() + self._neighbor_table = NeighborTable(retworkx.adjacency_matrix(self.coupling_map.graph)) + + if routing_pass is not None and (swap_trials is not None or layout_trials is not None): raise TranspilerError("Both routing_pass and swap_trials can't be set at the same time") self.routing_pass = routing_pass self.seed = seed self.max_iterations = max_iterations self.trials = swap_trials - self.swap_trials = swap_trials + if swap_trials is None: + self.swap_trials = CPU_COUNT + else: + self.swap_trials = swap_trials + if layout_trials is None: + self.layout_trials = CPU_COUNT + else: + self.layout_trials = layout_trials def run(self, dag): """Run the SabreLayout pass on `dag`. @@ -92,6 +123,10 @@ def run(self, dag): Args: dag (DAGCircuit): DAG to find layout for. + Returns: + DAGCircuit: The output dag if swap mapping was run + (otherwise the input dag is returned unmodified). + Raises: TranspilerError: if dag wider than self.coupling_map """ @@ -101,44 +136,119 @@ def run(self, dag): # Choose a random initial_layout. if self.seed is None: self.seed = np.random.randint(0, np.iinfo(np.int32).max) - rng = np.random.default_rng(self.seed) - physical_qubits = rng.choice(self.coupling_map.size(), len(dag.qubits), replace=False) - physical_qubits = rng.permutation(physical_qubits) - initial_layout = Layout({q: dag.qubits[i] for i, q in enumerate(physical_qubits)}) + if self.routing_pass is not None: + rng = np.random.default_rng(self.seed) + + physical_qubits = rng.choice(self.coupling_map.size(), len(dag.qubits), replace=False) + physical_qubits = rng.permutation(physical_qubits) + initial_layout = Layout({q: dag.qubits[i] for i, q in enumerate(physical_qubits)}) - if self.routing_pass is None: - self.routing_pass = SabreSwap( - self.coupling_map, "decay", seed=self.seed, fake_run=True, trials=self.swap_trials - ) - else: self.routing_pass.fake_run = True - # Do forward-backward iterations. - circ = dag_to_circuit(dag) - rev_circ = circ.reverse_ops() - for _ in range(self.max_iterations): - for _ in ("forward", "backward"): - pm = self._layout_and_route_passmanager(initial_layout) - new_circ = pm.run(circ) - - # Update initial layout and reverse the unmapped circuit. - pass_final_layout = pm.property_set["final_layout"] - final_layout = self._compose_layouts( - initial_layout, pass_final_layout, new_circ.qregs - ) - initial_layout = final_layout - circ, rev_circ = rev_circ, circ + # Do forward-backward iterations. + circ = dag_to_circuit(dag) + rev_circ = circ.reverse_ops() + for _ in range(self.max_iterations): + for _ in ("forward", "backward"): + pm = self._layout_and_route_passmanager(initial_layout) + new_circ = pm.run(circ) - # Diagnostics - logger.info("new initial layout") - logger.info(initial_layout) + # Update initial layout and reverse the unmapped circuit. + pass_final_layout = pm.property_set["final_layout"] + final_layout = self._compose_layouts( + initial_layout, pass_final_layout, new_circ.qregs + ) + initial_layout = final_layout + circ, rev_circ = rev_circ, circ - for qreg in dag.qregs.values(): - initial_layout.add_register(qreg) + # Diagnostics + logger.info("new initial layout") + logger.info(initial_layout) - self.property_set["layout"] = initial_layout - self.routing_pass.fake_run = False + for qreg in dag.qregs.values(): + initial_layout.add_register(qreg) + self.property_set["layout"] = initial_layout + self.routing_pass.fake_run = False + return dag + else: + dist_matrix = self.coupling_map.distance_matrix + original_qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + original_clbit_indices = {bit: index for index, bit in enumerate(dag.clbits)} + + dag_list = [] + for node in dag.topological_op_nodes(): + cargs = {original_clbit_indices[x] for x in node.cargs} + if node.op.condition is not None: + for clbit in dag._bits_in_condition(node.op.condition): + cargs.add(original_clbit_indices[clbit]) + + dag_list.append( + ( + node._node_id, + [original_qubit_indices[x] for x in node.qargs], + cargs, + ) + ) + ((initial_layout, final_layout), swap_map, gate_order) = sabre_layout_and_routing( + len(dag.clbits), + dag_list, + self._neighbor_table, + dist_matrix, + Heuristic.Decay, + self.seed, + self.max_iterations, + self.swap_trials, + self.layout_trials, + ) + # Apply initial layout selected. + # this is a pseudo-pass manager to avoid the repeated round trip between + # dag and circuit and just use a dag + original_dag = dag + layout_dict = {} + num_qubits = len(dag.qubits) + for k, v in initial_layout.layout_mapping(): + if k < num_qubits: + layout_dict[dag.qubits[k]] = v + initital_layout = Layout(layout_dict) + self.property_set["layout"] = initital_layout + ancilla_pass = FullAncillaAllocation(self.coupling_map) + ancilla_pass.property_set = self.property_set + dag = ancilla_pass.run(dag) + enlarge_pass = EnlargeWithAncilla() + enlarge_pass.property_set = ancilla_pass.property_set + dag = enlarge_pass.run(dag) + apply_pass = ApplyLayout() + apply_pass.property_set = enlarge_pass.property_set + dag = apply_pass.run(dag) + # Apply sabre swap ontop of circuit with sabre layout + final_layout_mapping = final_layout.layout_mapping() + self.property_set["final_layout"] = Layout( + {dag.qubits[k]: v for (k, v) in final_layout_mapping} + ) + mapped_dag = dag.copy_empty_like() + canonical_register = dag.qregs["q"] + current_layout = Layout.generate_trivial_layout(canonical_register) + qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + layout_mapping = { + qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() + } + original_layout = NLayout(layout_mapping, len(dag.qubits), self.coupling_map.size()) + for node_id in gate_order: + node = original_dag._multi_graph[node_id] + process_swaps( + swap_map, + node, + mapped_dag, + original_layout, + canonical_register, + False, + qubit_indices, + ) + apply_gate( + mapped_dag, node, original_layout, canonical_register, False, layout_dict + ) + return mapped_dag def _layout_and_route_passmanager(self, initial_layout): """Return a passmanager for a full layout and routing. diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 8f1bdf4b676b..b89e41cd8b5a 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -13,7 +13,7 @@ """Routing via SWAP insertion using the SABRE method from Li et al.""" import logging -from copy import copy, deepcopy +from copy import copy import numpy as np import retworkx @@ -143,7 +143,7 @@ def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False, t if coupling_map is None or coupling_map.is_symmetric: self.coupling_map = coupling_map else: - self.coupling_map = deepcopy(coupling_map) + self.coupling_map = copy(coupling_map) self.coupling_map.make_symmetric() self._neighbor_table = None if coupling_map is not None: @@ -241,33 +241,63 @@ def run(self, dag): if not self.fake_run: for node_id in gate_order: node = dag._multi_graph[node_id] - self._process_swaps(swap_map, node, mapped_dag, original_layout, canonical_register) - self._apply_gate(mapped_dag, node, original_layout, canonical_register) - return mapped_dag - return dag - - def _process_swaps(self, swap_map, node, mapped_dag, current_layout, canonical_register): - if node._node_id in swap_map: - for swap in swap_map[node._node_id]: - swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] - self._apply_gate( + process_swaps( + swap_map, + node, mapped_dag, - DAGOpNode(op=SwapGate(), qargs=swap_qargs), - current_layout, + original_layout, canonical_register, + self.fake_run, + self._qubit_indices, ) - current_layout.swap_logical(*swap) - - def _apply_gate(self, mapped_dag, node, current_layout, canonical_register): - new_node = self._transform_gate_for_layout(node, current_layout, canonical_register) - if self.fake_run: - return new_node - return mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs) - - def _transform_gate_for_layout(self, op_node, layout, device_qreg): - """Return node implementing a virtual op on given layout.""" - mapped_op_node = copy(op_node) - mapped_op_node.qargs = tuple( - device_qreg[layout.logical_to_physical(self._qubit_indices[x])] for x in op_node.qargs - ) - return mapped_op_node + apply_gate( + mapped_dag, + node, + original_layout, + canonical_register, + self.fake_run, + self._qubit_indices, + ) + return mapped_dag + return dag + + +def process_swaps( + swap_map, + node, + mapped_dag, + current_layout, + canonical_register, + fake_run, + qubit_indices, +): + """Process swaps from SwapMap.""" + if node._node_id in swap_map: + for swap in swap_map[node._node_id]: + swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] + apply_gate( + mapped_dag, + DAGOpNode(op=SwapGate(), qargs=swap_qargs), + current_layout, + canonical_register, + fake_run, + qubit_indices, + ) + current_layout.swap_logical(*swap) + + +def apply_gate(mapped_dag, node, current_layout, canonical_register, fake_run, qubit_indices): + """Apply gate given the current layout.""" + new_node = transform_gate_for_layout(node, current_layout, canonical_register, qubit_indices) + if fake_run: + return new_node + return mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs) + + +def transform_gate_for_layout(op_node, layout, device_qreg, qubit_indices): + """Return node implementing a virtual op on given layout.""" + mapped_op_node = copy(op_node) + mapped_op_node.qargs = tuple( + device_qreg[layout.logical_to_physical(qubit_indices[x])] for x in op_node.qargs + ) + return mapped_op_node diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 78cffd88f24a..546c5c05589e 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -109,10 +109,17 @@ def _choose_layout_condition(property_set): "layout", layout_method, pass_manager_config, optimization_level=0 ) else: + + def _swap_mapped(property_set): + return property_set["final_layout"] is None + layout = PassManager() layout.append(_given_layout) layout.append(_choose_layout, condition=_choose_layout_condition) - layout += common.generate_embed_passmanager(coupling_map) + embed = common.generate_embed_passmanager(coupling_map) + layout.append( + [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped + ) routing = routing_pm else: layout = None diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index b3a58fe703d8..7b4ae1774e39 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -196,12 +196,19 @@ def _opt_control(property_set): "layout", layout_method, pass_manager_config, optimization_level=1 ) else: + + def _swap_mapped(property_set): + return property_set["final_layout"] is None + layout = PassManager() layout.append(_given_layout) layout.append(_choose_layout_0, condition=_choose_layout_condition) layout.append(_choose_layout_1, condition=_layout_not_perfect) layout.append(_improve_layout, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) + embed = common.generate_embed_passmanager(coupling_map) + layout.append( + [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped + ) routing = routing_pm diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 1edd917c1160..6b767511b21e 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -165,11 +165,18 @@ def _opt_control(property_set): "layout", layout_method, pass_manager_config, optimization_level=2 ) else: + + def _swap_mapped(property_set): + return property_set["final_layout"] is None + layout = PassManager() layout.append(_given_layout) layout.append(_choose_layout_0, condition=_choose_layout_condition) layout.append(_choose_layout_1, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) + embed = common.generate_embed_passmanager(coupling_map) + layout.append( + [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped + ) routing = routing_pm else: layout = None diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 46cc03e58917..fd930303fad6 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -192,11 +192,18 @@ def _opt_control(property_set): "layout", layout_method, pass_manager_config, optimization_level=3 ) else: + + def _swap_mapped(property_set): + return property_set["final_layout"] is None + layout = PassManager() layout.append(_given_layout) layout.append(_choose_layout_0, condition=_choose_layout_condition) layout.append(_choose_layout_1, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) + embed = common.generate_embed_passmanager(coupling_map) + layout.append( + [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped + ) routing = routing_pm else: layout = None diff --git a/src/lib.rs b/src/lib.rs index 828f444188a9..9159d487e8b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ mod nlayout; mod optimize_1q_gates; mod pauli_exp_val; mod results; +mod sabre_layout; mod sabre_swap; mod sampled_exp_val; mod sparse_pauli_op; @@ -51,5 +52,6 @@ fn _accelerate(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(results::results))?; m.add_wrapped(wrap_pymodule!(optimize_1q_gates::optimize_1q_gates))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val::sampled_exp_val))?; + m.add_wrapped(wrap_pymodule!(sabre_layout::sabre_layout))?; Ok(()) } diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs new file mode 100644 index 000000000000..5b38399f5cdc --- /dev/null +++ b/src/sabre_layout.rs @@ -0,0 +1,241 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. +#![allow(clippy::too_many_arguments)] + +use hashbrown::HashSet; +use ndarray::prelude::*; +use numpy::IntoPyArray; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; +use rayon::prelude::*; +use retworkx_core::petgraph::prelude::*; + +use crate::getenv_use_multiple_threads; +use crate::nlayout::NLayout; +use crate::sabre_swap::neighbor_table::NeighborTable; +use crate::sabre_swap::sabre_dag::SabreDAG; +use crate::sabre_swap::swap_map::SwapMap; +use crate::sabre_swap::{build_swap_map_inner, Heuristic}; + +#[pyfunction] +pub fn sabre_layout_and_routing( + py: Python, + num_clbits: usize, + dag_nodes: Vec<(usize, Vec, HashSet)>, + neighbor_table: &NeighborTable, + distance_matrix: PyReadonlyArray2, + heuristic: &Heuristic, + seed: u64, + max_iterations: usize, + num_swap_trials: usize, + num_layout_trials: usize, +) -> ([NLayout; 2], SwapMap, PyObject) { + let run_in_parallel = getenv_use_multiple_threads(); + let outer_rng = Pcg64Mcg::seed_from_u64(seed); + let seed_vec: Vec = outer_rng + .sample_iter(&rand::distributions::Standard) + .take(num_layout_trials) + .collect(); + let dist = distance_matrix.as_array(); + let result = if run_in_parallel { + seed_vec + .into_par_iter() + .enumerate() + .map(|(index, seed_trial)| { + ( + index, + layout_trial( + num_clbits, + dag_nodes.clone(), + neighbor_table, + &dist, + heuristic, + seed_trial, + max_iterations, + num_swap_trials, + ), + ) + }) + .min_by_key(|(index, result)| { + ( + result.1.map.values().map(|x| x.len()).sum::(), + *index, + ) + }) + .unwrap() + .1 + } else { + seed_vec + .into_iter() + .map(|seed_trial| { + layout_trial( + num_clbits, + dag_nodes.clone(), + neighbor_table, + &dist, + heuristic, + seed_trial, + max_iterations, + num_swap_trials, + ) + }) + .min_by_key(|result| result.1.map.values().map(|x| x.len()).sum::()) + .unwrap() + }; + (result.0, result.1, result.2.into_pyarray(py).into()) +} + +fn layout_trial( + num_clbits: usize, + mut dag_nodes: Vec<(usize, Vec, HashSet)>, + neighbor_table: &NeighborTable, + distance_matrix: &ArrayView2, + heuristic: &Heuristic, + seed: u64, + max_iterations: usize, + num_swap_trials: usize, +) -> ([NLayout; 2], SwapMap, Vec) { + // Pick a random initial layout and fully populate ancillas in that layout too + let num_physical_qubits = distance_matrix.shape()[0]; + let mut rng = Pcg64Mcg::seed_from_u64(seed); + let mut physical_qubits: Vec = (0..num_physical_qubits).collect(); + physical_qubits.shuffle(&mut rng); + let mut phys_to_logic = vec![0; num_physical_qubits]; + physical_qubits + .iter() + .enumerate() + .for_each(|(logic, phys)| phys_to_logic[*phys] = logic); + let mut initial_layout = NLayout { + logic_to_phys: physical_qubits, + phys_to_logic, + }; + let mut rev_dag_nodes: Vec<(usize, Vec, HashSet)> = + dag_nodes.iter().rev().cloned().collect(); + for _iter in 0..max_iterations { + // forward and reverse + for _direction in 0..2 { + let dag = apply_layout(&dag_nodes, &initial_layout, num_physical_qubits, num_clbits); + let mut pass_final_layout = NLayout { + logic_to_phys: (0..num_physical_qubits).collect(), + phys_to_logic: (0..num_physical_qubits).collect(), + }; + build_swap_map_inner( + num_physical_qubits, + &dag, + neighbor_table, + distance_matrix, + heuristic, + seed, + &mut pass_final_layout, + num_swap_trials, + Some(false), + ); + let final_layout = compose_layout(&initial_layout, &pass_final_layout); + initial_layout = final_layout; + std::mem::swap(&mut dag_nodes, &mut rev_dag_nodes); + } + } + let layout_dag = apply_layout(&dag_nodes, &initial_layout, num_physical_qubits, num_clbits); + let mut final_layout = initial_layout.clone(); + let (swap_map, gate_order) = build_swap_map_inner( + num_physical_qubits, + &layout_dag, + neighbor_table, + distance_matrix, + heuristic, + seed, + &mut final_layout, + num_swap_trials, + Some(false), + ); + ([initial_layout, final_layout], swap_map, gate_order) +} + +fn apply_layout( + dag_nodes: &[(usize, Vec, HashSet)], + layout: &NLayout, + num_qubits: usize, + num_clbits: usize, +) -> SabreDAG { + let layout_dag_nodes: Vec<(usize, Vec, HashSet)> = dag_nodes + .iter() + .map(|(node_index, qargs, cargs)| { + let new_qargs: Vec = qargs.iter().map(|n| layout.logic_to_phys[*n]).collect(); + (*node_index, new_qargs, cargs.clone()) + }) + .collect(); + build_sabre_dag(layout_dag_nodes, num_qubits, num_clbits) +} + +fn build_sabre_dag( + layout_dag_nodes: Vec<(usize, Vec, HashSet)>, + num_qubits: usize, + num_clbits: usize, +) -> SabreDAG { + let mut dag: DiGraph<(usize, Vec), ()> = + Graph::with_capacity(layout_dag_nodes.len(), 2 * layout_dag_nodes.len()); + let mut first_layer = Vec::::new(); + let mut qubit_pos: Vec> = vec![None; num_qubits]; + let mut clbit_pos: Vec> = vec![None; num_clbits]; + for node in &layout_dag_nodes { + let qargs = &node.1; + let cargs = &node.2; + let gate_index = dag.add_node((node.0, qargs.clone())); + let mut is_front = true; + for x in qargs { + if let Some(predecessor) = qubit_pos[*x] { + is_front = false; + dag.add_edge(predecessor, gate_index, ()); + } + qubit_pos[*x] = Some(gate_index); + } + for x in cargs { + if let Some(predecessor) = clbit_pos[*x] { + is_front = false; + dag.add_edge(predecessor, gate_index, ()); + } + clbit_pos[*x] = Some(gate_index); + } + if is_front { + first_layer.push(gate_index); + } + } + SabreDAG { dag, first_layer } +} + +fn compose_layout(initial_layout: &NLayout, final_layout: &NLayout) -> NLayout { + let logic_to_phys = initial_layout + .logic_to_phys + .iter() + .map(|n| final_layout.logic_to_phys[*n]) + .collect(); + let phys_to_logic = final_layout + .phys_to_logic + .iter() + .map(|n| initial_layout.phys_to_logic[*n]) + .collect(); + + NLayout { + logic_to_phys, + phys_to_logic, + } +} + +#[pymodule] +pub fn sabre_layout(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(sabre_layout_and_routing))?; + Ok(()) +} diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index f25e7acb06a6..dbc4a00c2344 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -153,9 +153,38 @@ pub fn build_swap_map( seed: u64, layout: &mut NLayout, num_trials: usize, + run_in_parallel: Option, ) -> (SwapMap, PyObject) { - let run_in_parallel = getenv_use_multiple_threads() && num_trials > 1; let dist = distance_matrix.as_array(); + let (swap_map, gate_order) = build_swap_map_inner( + num_qubits, + dag, + neighbor_table, + &dist, + heuristic, + seed, + layout, + num_trials, + run_in_parallel, + ); + (swap_map, gate_order.into_pyarray(py).into()) +} + +pub fn build_swap_map_inner( + num_qubits: usize, + dag: &SabreDAG, + neighbor_table: &NeighborTable, + dist: &ArrayView2, + heuristic: &Heuristic, + seed: u64, + layout: &mut NLayout, + num_trials: usize, + run_in_parallel: Option, +) -> (SwapMap, Vec) { + let run_in_parallel = match run_in_parallel { + Some(run_in_parallel) => run_in_parallel, + None => getenv_use_multiple_threads() && num_trials > 1, + }; let coupling_graph: DiGraph<(), ()> = cmap_from_neighor_table(neighbor_table); let outer_rng = Pcg64Mcg::seed_from_u64(seed); let seed_vec: Vec = outer_rng @@ -173,7 +202,7 @@ pub fn build_swap_map( num_qubits, dag, neighbor_table, - &dist, + dist, &coupling_graph, heuristic, seed_trial, @@ -197,7 +226,7 @@ pub fn build_swap_map( num_qubits, dag, neighbor_table, - &dist, + dist, &coupling_graph, heuristic, seed_trial, @@ -212,7 +241,7 @@ pub fn build_swap_map( SwapMap { map: result.out_map, }, - result.gate_order.into_pyarray(py).into(), + result.gate_order, ) } diff --git a/tox.ini b/tox.ini index 59cc9f6748b6..8dbd5b2d32c2 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ setenv = QISKIT_SUPRESS_PACKAGING_WARNINGS=Y QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE -passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL RUST_BACKTRACE SETUPTOOLS_ENABLE_FEATURES +passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL RUST_BACKTRACE SETUPTOOLS_ENABLE_FEATURES QISKIT_IN_PARALLEL deps = setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt From 6e4d7210ebbc0e7b5c9668c44da775ae45c34bb6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 10 Nov 2022 14:20:25 -0500 Subject: [PATCH 02/24] Use deepcopy for coupling map copy Previously this PR was using copy() to copy the coupling map before we mutated it to be symmetric (a requirement for the sabre algorithm). However, this modification of the object was leaking out causing test failures. This commit switches it to a deepcopy to ensure there are no shared references (and a comment added to explain it's needed). --- qiskit/transpiler/passes/layout/sabre_layout.py | 5 ++++- qiskit/transpiler/passes/routing/sabre_swap.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index f4328f1d93ca..d3bbfcfdab56 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -98,7 +98,10 @@ def __init__( self._neighbor_table = None if self.coupling_map is not None: if not self.coupling_map.is_symmetric: - self.coupling_map = copy.copy(self.coupling_map) + # deepcopy is needed here to avoid modifications updating + # shared references in passes which require directional + # constraints + self.coupling_map = copy.deepcopy(self.coupling_map) self.coupling_map.make_symmetric() self._neighbor_table = NeighborTable(retworkx.adjacency_matrix(self.coupling_map.graph)) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index b89e41cd8b5a..dcded7900be2 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -13,7 +13,7 @@ """Routing via SWAP insertion using the SABRE method from Li et al.""" import logging -from copy import copy +from copy import copy, deepcopy import numpy as np import retworkx @@ -143,7 +143,10 @@ def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False, t if coupling_map is None or coupling_map.is_symmetric: self.coupling_map = coupling_map else: - self.coupling_map = copy(coupling_map) + # A deepcopy is needed here to avoid modifications updating + # shared references in passes which require directional + # constraints + self.coupling_map = deepcopy(coupling_map) self.coupling_map.make_symmetric() self._neighbor_table = None if coupling_map is not None: From 64cc68ef709d182d336e7f6bb343961b60ceb227 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 07:53:37 -0500 Subject: [PATCH 03/24] Fix failing unitary synthesis tests This PR branch modifies the default behavior of the SabreLayout pass so it is now a transformation pass that computes a layout, applies it, and then performs routing. This means when using sabre layout in a custom pass manager we no longer need to embed a layout after computing the layout. The failing unitary synthesis tests were using a custom pass manager and trying to apply the layout again after SabreLayout already did. This commit just removes this now unecessary steps from the test code. --- test/python/transpiler/test_unitary_synthesis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index da4e87dcd85b..f3dfcacf21c3 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -486,7 +486,6 @@ def _repeat_condition(property_set): seed = 2 _map = [SabreLayout(coupling_map, max_iterations=2, seed=seed)] - _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] _unroll3q = Unroll3qOrMore() _swap_check = CheckMap(coupling_map) _swap = [ @@ -509,7 +508,6 @@ def _repeat_condition(property_set): pm = PassManager() pm.append(_map) # map to hardware by inserting swaps - pm.append(_embed) pm.append(_unroll3q) pm.append(_swap_check) pm.append(_swap) From 1e2e318991709ab19598d68d928df83d04fe5869 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 08:55:05 -0500 Subject: [PATCH 04/24] Add release note --- .../rusty-sabre-layout-2e1ca05d1902dcb5.yaml | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml diff --git a/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml b/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml new file mode 100644 index 000000000000..faeed98a8af9 --- /dev/null +++ b/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml @@ -0,0 +1,97 @@ +--- +features: + - | + The :class:`~.SabreLayout` transpiler pass has greatly improved performance + as it has been re-written in Rust. As part of this rewrite the pass has been + transformed from an analysis pass to a transformation pass that will run both + layout and routing. This was done to not only improve the runtime performance + but also improve the quality of the results. The previous functionality of the + pass as an analysis pass can be retained by manually setting the ``routing_pass`` + argument. + - | + The :class:`~.SabreLayout` transpiler pass has a new constructor argument + ``layout_trials``. This argument is used to control how many random number + generator seeds will be attempted to run :class:`~.SabreLayout` with. When + set the SABRE layout algorithm is run ``layout_trials`` number of times and + the best quality output (measured in the lowest number of swap gates added) + is selected. These seed trials are executed in parallel using multithreading + to minimize the potential performance overhead of running layout multiple + times. By default if this is not specified the :class:`~.SabreLayout` + pass will default to using the number of physical CPUs are available on the + local system. This multiple seed +upgrade: + - | + The default behavior of the :class:`~.SabreLayout` compiler pass has + changed. The pass is no longer an :class:`~.AnalysisPass` and by default + will compute the initital layout, apply it to the circuit, and will + also run :class:`~.SabreSwap` internally and apply the swap mapping + and set the ``final_layout`` property set with the permutation caused + by swap insertions. This means for users running :class:`~.SabreLayout` + as part of a custom :class:`~.PassManager` will need to adjust the pass + manager to account for this (unless they were setting the ``routing_pass`` + argument for :class:`~.SabreLayout`). This change was made in the interest + of improving the quality output, the layout and routing quality are highly + coupled and :class:`~.SabreLayout` will now run multiple parallel seed + trials and to calculate which seed provides the best results it needs to + perform both the layout and routing together. There are two ways you can + adjust the usage in your custom pass manager. The first is to avoid using + embedding in your preset pass manager. If you were previously running something + like:: + + from qiskit.transpiler import PassManager + from qiskit.transpiler.preset_passmanagers import common + from qiskit.transpiler.passes.SabreLayout + + pm = PassManager() + pm.append(SabreLayout(coupling_map) + pm += common.generate_embed_passmanager(coupling_map) + + to compute the layout and then apply it (which was typically followed by routing) + you can adjust the usage to just simply be:: + + + from qiskit.transpiler import PassManager + from qiskit.transpiler.preset_passmanagers import common + from qiskit.transpiler.passes.SabreLayout + + pm = PassManager() + pm.append(SabreLayout(coupling_map) + + as :class:`~.SabreLayout` will apply the layout and you no longer need the embedding + stage. Alternatively, you can specify the ``routing_pass`` argument which will revert + :class:`~.SabreLayout` to its previous behavior. For example, if you want to run + :class:`~.SabreLayout` as it was run in previous releases you can do something like:: + + from qiskit.transpiler.passes import SabreSwap, SabreLayout + routing_pass = SabreSwap( + coupling_map, "decay", seed=seed, fake_run=True + ) + layout_pass = SabreLayout(coupling_map, routing_pass=routing_pass, seed=seed) + + which will have :class:`~.SabreLayout` run as an analysis pass and just set + the ``layout`` property set. + - | + The layouts computed by the :class:`~.SabreLayout` pass (when run without + the ``routing_pass`` argument) with a fixed seed value may change from + previous releases. This is caused by a new random number generator being + used as part of the rewrite of the :class:`~.SabreLayout` pass in Rust which + significantly improved the performance. If you rely on having consistent + output you can run the pass in an earlier version of Qiskit and leverage + :mod:`qiskit.qpy` to save the circuit and then load it using the current + version. Alternatively you can explicitly set the ``routing_pass`` argument + to an instance of :class:`~.SabreSwap` to mirror the previous behavior + of :class:`~.SabreLayout`:: + + from qiskit.transpiler.passes import SabreSwap, SabreLayout + + + routing_pass = SabreSwap( + coupling_map, "decay", seed=seed, fake_run=True + ) + layout_pass = SabreLayout(coupling_map, routing_pass=routing_pass, seed=seed) + + which will mirror the behavior of the pass in the previous release. Note, that if you + were using the ``swap_trials`` argument on :class:`~.SabreLayout` in previous releases + when adjusting the usage to this form that you will need to set ``trials`` argument + on the :class:`~.SabreSwap` constructor if you want to retain the previous output with + a fixed seed. From c6ac421cd6483f0a44bc090c2829cc93f278ac53 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 09:00:59 -0500 Subject: [PATCH 05/24] Run BarrierBeforeMeasurement before new SabreLayout Now that the routing stage is integrated into the SabreLayout pass we should be running the BarrierBeforeMeasurement pass prior to layout in the preset pass managers instead of before routing. The goal of the pass is to prevent the routing algorithms for accidentally reusing a qubit after a final measurement which would be invalid by inserting a barrier before the measurements to ensure all qubits are swap mapped prior to adding the measurements during routing. While this might not strictly be necessary (it didn't affect any test output) it feels like best practice to ensure we're doing this prior to potentially routing to prevent issues. --- qiskit/transpiler/preset_passmanagers/level1.py | 5 ++++- qiskit/transpiler/preset_passmanagers/level2.py | 5 ++++- qiskit/transpiler/preset_passmanagers/level3.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 7b4ae1774e39..bf79015dd812 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -34,6 +34,7 @@ from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import CheckMap from qiskit.transpiler.passes import GatesInBasis +from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason @@ -204,7 +205,9 @@ def _swap_mapped(property_set): layout.append(_given_layout) layout.append(_choose_layout_0, condition=_choose_layout_condition) layout.append(_choose_layout_1, condition=_layout_not_perfect) - layout.append(_improve_layout, condition=_vf2_match_not_found) + layout.append( + [BarrierBeforeFinalMeasurements(), _improve_layout], condition=_vf2_match_not_found + ) embed = common.generate_embed_passmanager(coupling_map) layout.append( [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 6b767511b21e..e72638b26662 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -34,6 +34,7 @@ from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import CommutativeCancellation from qiskit.transpiler.passes import GatesInBasis +from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason @@ -172,7 +173,9 @@ def _swap_mapped(property_set): layout = PassManager() layout.append(_given_layout) layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout.append( + [BarrierBeforeFinalMeasurements(), _choose_layout_1], condition=_vf2_match_not_found + ) embed = common.generate_embed_passmanager(coupling_map) layout.append( [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index fd930303fad6..58d059111787 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -40,6 +40,7 @@ from qiskit.transpiler.passes import ConsolidateBlocks from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes import GatesInBasis +from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements from qiskit.transpiler.runningpassmanager import ConditionalController from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason @@ -199,7 +200,9 @@ def _swap_mapped(property_set): layout = PassManager() layout.append(_given_layout) layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout.append( + [BarrierBeforeFinalMeasurements(), _choose_layout_1], condition=_vf2_match_not_found + ) embed = common.generate_embed_passmanager(coupling_map) layout.append( [pass_ for x in embed.passes() for pass_ in x["passes"]], condition=_swap_mapped From 3171a6f929bc21b92db967f735bbdea22e9ba44b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 09:23:06 -0500 Subject: [PATCH 06/24] Improve docstrings --- .../transpiler/passes/layout/sabre_layout.py | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index d3bbfcfdab56..555f92c3fbda 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -51,6 +51,21 @@ class SabreLayout(TransformationPass): This method exploits the reversibility of quantum circuits, and tries to include global circuit information in the choice of initial_layout. + By default this pass will run both layout and routing and will transform the + circuit so that the layout is applied to the input dag (meaning that the output + circuit will have ancilla qubits allocated for unused qubits on the coupling map + and the qubits will be reordered to match the mapped physical qubits) and then + routing will be applied (inserting :class:`~.SwapGate`s to account for limited + connectivity). This is unlike most other layout passes which are :class:`~.AnalysisPass` + objects and just find an initial layout and set that on the property set. This is + done because by default the pass will run parallel seed trials with different random + seeds for selecting the random initial layout and then selecting the routed output + which results in the least number of swap gates needed. + + You can use the ``routing_pass`` argument to have this pass operate as a typical + layout pass. When specified this will use the specified routing pass to select an + initial layout only and will not run multiple seed trials. + **References:** [1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem @@ -72,8 +87,12 @@ def __init__( Args: coupling_map (Coupling): directed graph representing a coupling map. routing_pass (BasePass): the routing pass to use while iterating. - This is mutually exclusive with the ``swap_trials`` argument and - if both are set an error will be raised. + If specified this pass operates as an :class:`~.AnalysisPass` and + will only populate the ``layout`` field in the property set and + the input dag is returned unmodified. This argument is mutually + exclusive with the ``swap_trials`` and the ``layout_trials`` + arguments and if this is specified at the same time as either + argument an error will be raised. seed (int): seed for setting a random first trial layout. max_iterations (int): number of forward-backward iterations. swap_trials (int): The number of trials to run of @@ -87,11 +106,16 @@ def __init__( with the ``routing_pass`` argument and an error will be raised if both are used. layout_trials (int): The number of random seed trials to run - layout with. + layout with. When > 1 the trial that resuls in the output with + the fewest swap gates will be selected. If this is not specified + (and ``routing_pass`` is not set) then the number of local + physical CPUs will be used as the default value. This option is + mutually exclusive with the ``routing_pass`` argument and an error + will be raised if both are used. Raises: - TranspilerError: If both ``routing_pass`` and ``swap_trials`` are - specified + TranspilerError: If both ``routing_pass`` and ``swap_trials`` or + both ``routing_pass`` and ``layout_trials`` are specified """ super().__init__() self.coupling_map = coupling_map From d5ce6c46802f6287f85315d5b2184d2cb5eed3ab Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 09:27:12 -0500 Subject: [PATCH 07/24] Set a fixed number of layout trials in preset pass managers For reproducible results with a fixed seed this commit sets a fixed number of layout_trials for the SabreLayout pass in the preset pass managers. If we did not set a fixed value than the output of the transpiler with a fixed seed will vary based on the number of physical cores that is running the compilation. To start optimization levels 0 and 1 use 5, level 2 uses 10, and level 3 uses 20 which matches the swap_trials argument we used. This is just a starting point, we can adjust these values later if needed. --- qiskit/transpiler/preset_passmanagers/level0.py | 6 +++++- qiskit/transpiler/preset_passmanagers/level1.py | 6 ++++-- qiskit/transpiler/preset_passmanagers/level2.py | 6 +++++- qiskit/transpiler/preset_passmanagers/level3.py | 6 +++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 546c5c05589e..613f6eb8535b 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -85,7 +85,11 @@ def _choose_layout_condition(property_set): _choose_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _choose_layout = SabreLayout( - coupling_map, max_iterations=1, seed=seed_transpiler, swap_trials=5 + coupling_map, + max_iterations=1, + seed=seed_transpiler, + swap_trials=5, + layout_trials=5, ) # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index bf79015dd812..45adc60e4058 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -139,12 +139,14 @@ def _vf2_match_not_found(property_set): _improve_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _improve_layout = SabreLayout( - coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5 + coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5, layout_trials=5 ) elif layout_method is None: _improve_layout = common.if_has_control_flow_else( DenseLayout(coupling_map, backend_properties, target=target), - SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5), + SabreLayout( + coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5, layout_trials=5 + ), ).to_flow_controller() # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index e72638b26662..36229b21b89e 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -129,7 +129,11 @@ def _vf2_match_not_found(property_set): _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _choose_layout_1 = SabreLayout( - coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=10 + coupling_map, + max_iterations=2, + seed=seed_transpiler, + swap_trials=10, + layout_trials=10, ) # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 58d059111787..4e1de9707b3a 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -135,7 +135,11 @@ def _vf2_match_not_found(property_set): _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _choose_layout_1 = SabreLayout( - coupling_map, max_iterations=4, seed=seed_transpiler, swap_trials=20 + coupling_map, + max_iterations=4, + seed=seed_transpiler, + swap_trials=20, + layout_trials=20, ) # Choose routing pass From 5c2cef4ab7cd213f1c2b3ad878967222f4c50af5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 10:09:04 -0500 Subject: [PATCH 08/24] Update tests for layout changes This commit updates the tests which are checking exact layouts with a fixed seed when running SabreLayout. The changes to SabreLayout breaks exact seed reproducibility from the earlier version of the pass. So we need to update these tests for their new layout assignment from the improved pass. One exception is a test which was trying to assert that transpile() preserves a swap if it's in the basis set. However, the new layout and routing output from SabreLayout for that test was resulting in all the swaps getting optimized away at optimization level 3 (resulting in 13 cx gates instead of ~4 cx gates and 5 swaps before, which would be more efficient on real hardware). So the test was removed and only run at lower optimziation levels. --- .../transpiler/test_preset_passmanagers.py | 91 ++++++++++++++----- test/python/transpiler/test_sabre_layout.py | 64 ++++++------- .../transpiler/test_unitary_synthesis.py | 3 - 3 files changed, 102 insertions(+), 56 deletions(-) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 148488edabf4..3b4c8090bb73 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -716,32 +716,78 @@ def test_layout_tokyo_fully_connected_cx(self, level): } sabre_layout = { + 0: ancilla[12], + 1: ancilla[10], + 2: ancilla[0], + 3: ancilla[1], + 4: ancilla[2], + 5: qr[1], + 6: qr[0], + 7: ancilla[3], + 8: ancilla[4], + 9: ancilla[5], + 10: qr[2], + 11: qr[4], + 12: ancilla[6], + 13: ancilla[7], + 14: ancilla[8], + 15: ancilla[13], + 16: qr[3], + 17: ancilla[14], + 18: ancilla[9], + 19: ancilla[11], + } + + sabre_layout_lvl_2 = { + 0: ancilla[10], + 1: ancilla[0], + 2: ancilla[1], + 3: ancilla[2], + 4: ancilla[3], + 5: qr[4], + 6: qr[1], + 7: ancilla[4], + 8: ancilla[5], + 9: ancilla[6], + 10: qr[2], 11: qr[0], - 17: qr[1], - 16: qr[2], - 6: qr[3], - 18: qr[4], - 0: ancilla[0], - 1: ancilla[1], - 2: ancilla[2], - 3: ancilla[3], - 4: ancilla[4], - 5: ancilla[5], - 7: ancilla[6], - 8: ancilla[7], - 9: ancilla[8], - 10: ancilla[9], - 12: ancilla[10], - 13: ancilla[11], - 14: ancilla[12], + 12: ancilla[7], + 13: ancilla[8], + 14: ancilla[9], 15: ancilla[13], + 16: qr[3], + 17: ancilla[14], + 18: ancilla[11], + 19: ancilla[12], + } + + sabre_layout_lvl_3 = { + 6: qr[0], + 11: ancilla[8], + 10: qr[1], + 5: qr[4], + 7: qr[2], + 1: qr[3], + 16: ancilla[7], + 17: ancilla[6], + 0: ancilla[0], + 2: ancilla[1], + 3: ancilla[2], + 4: ancilla[3], + 8: ancilla[4], + 9: ancilla[5], + 12: ancilla[9], + 13: ancilla[10], + 14: ancilla[11], + 15: ancilla[12], + 18: ancilla[13], 19: ancilla[14], } expected_layout_level0 = trivial_layout expected_layout_level1 = sabre_layout - expected_layout_level2 = sabre_layout - expected_layout_level3 = sabre_layout + expected_layout_level2 = sabre_layout_lvl_2 + expected_layout_level3 = sabre_layout_lvl_3 expected_layouts = [ expected_layout_level0, @@ -910,8 +956,11 @@ def test_1(self, circuit, level): resulting_basis = {node.name for node in circuit_to_dag(result).op_nodes()} self.assertIn("swap", resulting_basis) + # Skipping optimization level 3 because the swap gates get absorbed into + # a unitary block as part of the KAK decompostion optimization passes and + # optimized away. @combine( - level=[0, 1, 2, 3], + level=[0, 1, 2], dsc="If swap in basis, do not decompose it. level: {level}", name="level{level}", ) @@ -931,7 +980,7 @@ def test_2(self, level): optimization_level=level, basis_gates=basis, coupling_map=coupling_map, - seed_transpiler=42123, + seed_transpiler=421234242, ) self.assertIsInstance(result, QuantumCircuit) resulting_basis = {node.name for node in circuit_to_dag(result).op_nodes()} diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7aabc5ef1a83..7d28a66a4eeb 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -59,11 +59,11 @@ def test_5q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr[0]], 10) - self.assertEqual(layout[qr[1]], 12) - self.assertEqual(layout[qr[2]], 7) - self.assertEqual(layout[qr[3]], 11) - self.assertEqual(layout[qr[4]], 13) + self.assertEqual(layout[qr[0]], 16) + self.assertEqual(layout[qr[1]], 17) + self.assertEqual(layout[qr[2]], 11) + self.assertEqual(layout[qr[3]], 18) + self.assertEqual(layout[qr[4]], 10) def test_6q_circuit_20q_coupling(self): """Test finds layout for 6q circuit on 20q device.""" @@ -95,11 +95,11 @@ def test_6q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr0[0]], 2) - self.assertEqual(layout[qr0[1]], 3) - self.assertEqual(layout[qr0[2]], 10) - self.assertEqual(layout[qr1[0]], 1) - self.assertEqual(layout[qr1[1]], 7) + self.assertEqual(layout[qr0[0]], 11) + self.assertEqual(layout[qr0[1]], 10) + self.assertEqual(layout[qr0[2]], 12) + self.assertEqual(layout[qr1[0]], 16) + self.assertEqual(layout[qr1[1]], 17) self.assertEqual(layout[qr1[2]], 5) def test_layout_with_classical_bits(self): @@ -132,14 +132,14 @@ def test_layout_with_classical_bits(self): res = transpile(qc, FakeKolkata(), layout_method="sabre", seed_transpiler=1234) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout - self.assertEqual(layout[qc.qubits[0]], 14) - self.assertEqual(layout[qc.qubits[1]], 19) - self.assertEqual(layout[qc.qubits[2]], 7) - self.assertEqual(layout[qc.qubits[3]], 13) - self.assertEqual(layout[qc.qubits[4]], 6) - self.assertEqual(layout[qc.qubits[5]], 16) - self.assertEqual(layout[qc.qubits[6]], 18) - self.assertEqual(layout[qc.qubits[7]], 26) + self.assertEqual(layout[qc.qubits[0]], 11) + self.assertEqual(layout[qc.qubits[1]], 22) + self.assertEqual(layout[qc.qubits[2]], 21) + self.assertEqual(layout[qc.qubits[3]], 19) + self.assertEqual(layout[qc.qubits[4]], 26) + self.assertEqual(layout[qc.qubits[5]], 8) + self.assertEqual(layout[qc.qubits[6]], 17) + self.assertEqual(layout[qc.qubits[7]], 1) # pylint: disable=line-too-long def test_layout_many_search_trials(self): @@ -193,20 +193,20 @@ def test_layout_many_search_trials(self): ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout - self.assertEqual(layout[qc.qubits[0]], 19) - self.assertEqual(layout[qc.qubits[1]], 22) - self.assertEqual(layout[qc.qubits[2]], 17) - self.assertEqual(layout[qc.qubits[3]], 14) - self.assertEqual(layout[qc.qubits[4]], 18) - self.assertEqual(layout[qc.qubits[5]], 9) - self.assertEqual(layout[qc.qubits[6]], 11) - self.assertEqual(layout[qc.qubits[7]], 25) - self.assertEqual(layout[qc.qubits[8]], 16) - self.assertEqual(layout[qc.qubits[9]], 3) - self.assertEqual(layout[qc.qubits[10]], 12) - self.assertEqual(layout[qc.qubits[11]], 13) - self.assertEqual(layout[qc.qubits[12]], 20) - self.assertEqual(layout[qc.qubits[13]], 8) + self.assertEqual(layout[qc.qubits[0]], 11) + self.assertEqual(layout[qc.qubits[1]], 6) + self.assertEqual(layout[qc.qubits[2]], 14) + self.assertEqual(layout[qc.qubits[3]], 4) + self.assertEqual(layout[qc.qubits[4]], 16) + self.assertEqual(layout[qc.qubits[5]], 20) + self.assertEqual(layout[qc.qubits[6]], 12) + self.assertEqual(layout[qc.qubits[7]], 7) + self.assertEqual(layout[qc.qubits[8]], 15) + self.assertEqual(layout[qc.qubits[9]], 21) + self.assertEqual(layout[qc.qubits[10]], 10) + self.assertEqual(layout[qc.qubits[11]], 1) + self.assertEqual(layout[qc.qubits[12]], 2) + self.assertEqual(layout[qc.qubits[13]], 0) if __name__ == "__main__": diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index f3dfcacf21c3..89af400e63cb 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -42,9 +42,6 @@ SabreLayout, Depth, FixedPoint, - FullAncillaAllocation, - EnlargeWithAncilla, - ApplyLayout, Unroll3qOrMore, CheckMap, BarrierBeforeFinalMeasurements, From 784a9b51ec452c7c90d48770d43d160a2d0a4c85 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 12:19:56 -0500 Subject: [PATCH 09/24] Set a fixed number of layout trials in SabreLayout tests The dedicated tests for SabreLayout were not running a fixed number of trials. This was causing a different layout to be returned in tests when run across multiple systems as the number of trials defaults to the number of physical CPUs. This commit fixes the trial count to the number of cores on the local system where the layout was updated. This should fix the non-determinism in the tests causing failures in CI and on different local systems. --- test/python/transpiler/test_sabre_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7d28a66a4eeb..1e7540f7107d 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -55,7 +55,7 @@ def test_5q_circuit_20q_coupling(self): circuit.cx(qr[1], qr[2]) dag = circuit_to_dag(circuit) - pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0, swap_trials=32) + pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0, swap_trials=32, layout_trials=32) pass_.run(dag) layout = pass_.property_set["layout"] @@ -91,7 +91,7 @@ def test_6q_circuit_20q_coupling(self): circuit.cx(qr1[1], qr0[0]) dag = circuit_to_dag(circuit) - pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0) + pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0, swap_trials=32, layout_trials=32) pass_.run(dag) layout = pass_.property_set["layout"] From bda04eae85bcb8ce8686678a8879e8f25bafa03a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 16:15:00 -0500 Subject: [PATCH 10/24] Run SabreSwap in parallel if only a single layout trial If there is only a single layout trial being run we don't have to worry about trying to do too much work in parallel at once by parallelizing the inner sabre swap execution. This commit updates the threading logic to enable running the inner sabre swap trials in parallel if there is only a single layout trial. --- src/sabre_layout.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 5b38399f5cdc..45dd2ebdbcb3 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -50,7 +50,7 @@ pub fn sabre_layout_and_routing( .take(num_layout_trials) .collect(); let dist = distance_matrix.as_array(); - let result = if run_in_parallel { + let result = if run_in_parallel && num_layout_trials > 1 { seed_vec .into_par_iter() .enumerate() @@ -66,6 +66,7 @@ pub fn sabre_layout_and_routing( seed_trial, max_iterations, num_swap_trials, + false, ), ) }) @@ -90,6 +91,7 @@ pub fn sabre_layout_and_routing( seed_trial, max_iterations, num_swap_trials, + run_in_parallel, ) }) .min_by_key(|result| result.1.map.values().map(|x| x.len()).sum::()) @@ -107,6 +109,7 @@ fn layout_trial( seed: u64, max_iterations: usize, num_swap_trials: usize, + run_swap_in_parallel: bool, ) -> ([NLayout; 2], SwapMap, Vec) { // Pick a random initial layout and fully populate ancillas in that layout too let num_physical_qubits = distance_matrix.shape()[0]; @@ -141,7 +144,7 @@ fn layout_trial( seed, &mut pass_final_layout, num_swap_trials, - Some(false), + Some(run_swap_in_parallel), ); let final_layout = compose_layout(&initial_layout, &pass_final_layout); initial_layout = final_layout; @@ -159,7 +162,7 @@ fn layout_trial( seed, &mut final_layout, num_swap_trials, - Some(false), + Some(run_swap_in_parallel), ); ([initial_layout, final_layout], swap_map, gate_order) } From 5dbd76be5a15bfe73264790a103b691c0acf59e0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 11 Nov 2022 16:54:35 -0500 Subject: [PATCH 11/24] Remove duplicated SabreDAG creation --- src/sabre_layout.rs | 39 +-------------------------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 45dd2ebdbcb3..eaf8e4a08761 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -21,7 +21,6 @@ use pyo3::Python; use rand::prelude::*; use rand_pcg::Pcg64Mcg; use rayon::prelude::*; -use retworkx_core::petgraph::prelude::*; use crate::getenv_use_multiple_threads; use crate::nlayout::NLayout; @@ -180,43 +179,7 @@ fn apply_layout( (*node_index, new_qargs, cargs.clone()) }) .collect(); - build_sabre_dag(layout_dag_nodes, num_qubits, num_clbits) -} - -fn build_sabre_dag( - layout_dag_nodes: Vec<(usize, Vec, HashSet)>, - num_qubits: usize, - num_clbits: usize, -) -> SabreDAG { - let mut dag: DiGraph<(usize, Vec), ()> = - Graph::with_capacity(layout_dag_nodes.len(), 2 * layout_dag_nodes.len()); - let mut first_layer = Vec::::new(); - let mut qubit_pos: Vec> = vec![None; num_qubits]; - let mut clbit_pos: Vec> = vec![None; num_clbits]; - for node in &layout_dag_nodes { - let qargs = &node.1; - let cargs = &node.2; - let gate_index = dag.add_node((node.0, qargs.clone())); - let mut is_front = true; - for x in qargs { - if let Some(predecessor) = qubit_pos[*x] { - is_front = false; - dag.add_edge(predecessor, gate_index, ()); - } - qubit_pos[*x] = Some(gate_index); - } - for x in cargs { - if let Some(predecessor) = clbit_pos[*x] { - is_front = false; - dag.add_edge(predecessor, gate_index, ()); - } - clbit_pos[*x] = Some(gate_index); - } - if is_front { - first_layer.push(gate_index); - } - } - SabreDAG { dag, first_layer } + SabreDAG::new(num_qubits, num_clbits, layout_dag_nodes).unwrap() } fn compose_layout(initial_layout: &NLayout, final_layout: &NLayout) -> NLayout { From d98ef6c4a14ebbf0281e5ef2784200f93896cbd0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 12:56:09 -0500 Subject: [PATCH 12/24] Correctly apply selected layout on dag nodes This commit corrects a bug in the PR branch that was caused by applying the selected initial layout in a trial to the swapped order node list. This was causing unexpected results when applying the circuit because the intent was to apply it only to the original input not the reversed input. --- src/sabre_layout.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index eaf8e4a08761..a44cadd76104 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -150,7 +150,11 @@ fn layout_trial( std::mem::swap(&mut dag_nodes, &mut rev_dag_nodes); } } - let layout_dag = apply_layout(&dag_nodes, &initial_layout, num_physical_qubits, num_clbits); + // Reverse the circuit back to avoid replaying the circuit in reverse order. + // the output of the loop above will always have the reverse order nodes set + // to dag_nodes. So one more swap is needed to restore the original order + std::mem::swap(dag_nodes, &mut rev_dag_nodes); + let layout_dag = apply_layout(dag_nodes, &initial_layout, num_physical_qubits, num_clbits); let mut final_layout = initial_layout.clone(); let (swap_map, gate_order) = build_swap_map_inner( num_physical_qubits, From 1a6a43d6ae1e4eb4a8286218446dd5a668f3edbd Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 12:57:25 -0500 Subject: [PATCH 13/24] Remove unnecessary clone from serial layout trials In the case we're evaluating the layout trials serially instead of in a parallel iterator we don't need to clone the dag nodes list. This is because nothing will be modifying it in parallel, so we don't need a thread local copy. Each call to layout_trial() will keep the dag nodes vector intact (see previous commit for fixing this) so it can just be passed by reference if there are no parallel threads involved. --- src/sabre_layout.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index a44cadd76104..580afb16a368 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -33,7 +33,7 @@ use crate::sabre_swap::{build_swap_map_inner, Heuristic}; pub fn sabre_layout_and_routing( py: Python, num_clbits: usize, - dag_nodes: Vec<(usize, Vec, HashSet)>, + mut dag_nodes: Vec<(usize, Vec, HashSet)>, neighbor_table: &NeighborTable, distance_matrix: PyReadonlyArray2, heuristic: &Heuristic, @@ -58,7 +58,7 @@ pub fn sabre_layout_and_routing( index, layout_trial( num_clbits, - dag_nodes.clone(), + &mut dag_nodes.clone(), neighbor_table, &dist, heuristic, @@ -83,7 +83,7 @@ pub fn sabre_layout_and_routing( .map(|seed_trial| { layout_trial( num_clbits, - dag_nodes.clone(), + &mut dag_nodes, neighbor_table, &dist, heuristic, @@ -101,7 +101,7 @@ pub fn sabre_layout_and_routing( fn layout_trial( num_clbits: usize, - mut dag_nodes: Vec<(usize, Vec, HashSet)>, + dag_nodes: &mut Vec<(usize, Vec, HashSet)>, neighbor_table: &NeighborTable, distance_matrix: &ArrayView2, heuristic: &Heuristic, @@ -129,7 +129,7 @@ fn layout_trial( for _iter in 0..max_iterations { // forward and reverse for _direction in 0..2 { - let dag = apply_layout(&dag_nodes, &initial_layout, num_physical_qubits, num_clbits); + let dag = apply_layout(dag_nodes, &initial_layout, num_physical_qubits, num_clbits); let mut pass_final_layout = NLayout { logic_to_phys: (0..num_physical_qubits).collect(), phys_to_logic: (0..num_physical_qubits).collect(), @@ -147,7 +147,7 @@ fn layout_trial( ); let final_layout = compose_layout(&initial_layout, &pass_final_layout); initial_layout = final_layout; - std::mem::swap(&mut dag_nodes, &mut rev_dag_nodes); + std::mem::swap(dag_nodes, &mut rev_dag_nodes); } } // Reverse the circuit back to avoid replaying the circuit in reverse order. From 19c3182fbbea485d0a9cc8ba9f55010f5a6353e8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 13:15:39 -0500 Subject: [PATCH 14/24] Fix seed setup when no user seed specified This commit fixes an issue prevent seed randomization when no seed is specified. On subsequent uses of a pass SabreLayout would not randomize the seed between runs because it was setting the seed to instance state. This commit fixes this issue by relying on initializing the RNG from entropy each time run() is called if no user specified seed is provided. --- qiskit/transpiler/passes/layout/sabre_layout.py | 9 +++++---- src/sabre_layout.rs | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 555f92c3fbda..33b6f74b54c3 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -161,11 +161,12 @@ def run(self, dag): raise TranspilerError("More virtual qubits exist than physical.") # Choose a random initial_layout. - if self.seed is None: - self.seed = np.random.randint(0, np.iinfo(np.int32).max) - if self.routing_pass is not None: - rng = np.random.default_rng(self.seed) + if self.seed is None: + seed = np.random.randint(0, np.iinfo(np.int32).max) + else: + seed = self.seed + rng = np.random.default_rng(seed) physical_qubits = rng.choice(self.coupling_map.size(), len(dag.qubits), replace=False) physical_qubits = rng.permutation(physical_qubits) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 580afb16a368..773dd1958230 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -37,13 +37,16 @@ pub fn sabre_layout_and_routing( neighbor_table: &NeighborTable, distance_matrix: PyReadonlyArray2, heuristic: &Heuristic, - seed: u64, + seed: Option, max_iterations: usize, num_swap_trials: usize, num_layout_trials: usize, ) -> ([NLayout; 2], SwapMap, PyObject) { let run_in_parallel = getenv_use_multiple_threads(); - let outer_rng = Pcg64Mcg::seed_from_u64(seed); + let outer_rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; let seed_vec: Vec = outer_rng .sample_iter(&rand::distributions::Standard) .take(num_layout_trials) From 7a6cb41191d72ed42e23b954eeeebc7683b52708 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 15:01:35 -0500 Subject: [PATCH 15/24] Start from trivial layout for routing stage This commit fixes the routing run to run from a trivial layout instead of the initial layout. By the time we do final routing for a trial we've already applied the selected initial layout to the SabreDAG. So the correct layout to use for running final swap mapping is a trivial layout where logical bit 0 is at physical bit 0. Using initial layout twice means we end up mapping more than is needed resulting in incorrect results. --- src/sabre_layout.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 773dd1958230..ec0493d2a604 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -158,7 +158,10 @@ fn layout_trial( // to dag_nodes. So one more swap is needed to restore the original order std::mem::swap(dag_nodes, &mut rev_dag_nodes); let layout_dag = apply_layout(dag_nodes, &initial_layout, num_physical_qubits, num_clbits); - let mut final_layout = initial_layout.clone(); + let mut final_layout = NLayout { + logic_to_phys: (0..num_physical_qubits).collect(), + phys_to_logic: (0..num_physical_qubits).collect(), + }; let (swap_map, gate_order) = build_swap_map_inner( num_physical_qubits, &layout_dag, From 606171685c4ea2119143964ac090cb9300f378f2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 15:04:13 -0500 Subject: [PATCH 16/24] Revert "Correctly apply selected layout on dag nodes" This change was incorrect, the output was already in the correct order and this was causing the behavior it strived to fix. This commit reverts the addition of the extra mem::swap() call to fix things. This reverts commit d98ef6c4a14ebbf0281e5ef2784200f93896cbd0. --- src/sabre_layout.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index ec0493d2a604..858aaae00094 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -153,10 +153,6 @@ fn layout_trial( std::mem::swap(dag_nodes, &mut rev_dag_nodes); } } - // Reverse the circuit back to avoid replaying the circuit in reverse order. - // the output of the loop above will always have the reverse order nodes set - // to dag_nodes. So one more swap is needed to restore the original order - std::mem::swap(dag_nodes, &mut rev_dag_nodes); let layout_dag = apply_layout(dag_nodes, &initial_layout, num_physical_qubits, num_clbits); let mut final_layout = NLayout { logic_to_phys: (0..num_physical_qubits).collect(), From 7c69de5a0ca2ffc2291c3f27b021efea3b87f5c3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 15:24:00 -0500 Subject: [PATCH 17/24] Deduplicate NLayout trivial layout creation This commit deduplicates the trivial layout generation for the NLayout class. Previously there were a few places both in rust and python that sabre layout was manually generating a trivial NLayout object. THis commit adds a static method to the NLayout class that allows both Python and Rust to easily create a new trivial NLayout object instead of manually creating the object. --- qiskit/transpiler/passes/layout/sabre_layout.py | 6 +----- src/nlayout.rs | 8 ++++++++ src/sabre_layout.rs | 10 ++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 33b6f74b54c3..e0d5a290fc7a 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -256,12 +256,8 @@ def run(self, dag): ) mapped_dag = dag.copy_empty_like() canonical_register = dag.qregs["q"] - current_layout = Layout.generate_trivial_layout(canonical_register) qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} - layout_mapping = { - qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() - } - original_layout = NLayout(layout_mapping, len(dag.qubits), self.coupling_map.size()) + original_layout = NLayout.generate_trivial_layout(self.coupling_map.size()) for node_id in gate_order: node = original_dag._multi_graph[node_id] process_swaps( diff --git a/src/nlayout.rs b/src/nlayout.rs index ae5fe007a7e6..70e2767aaed5 100644 --- a/src/nlayout.rs +++ b/src/nlayout.rs @@ -116,6 +116,14 @@ impl NLayout { pub fn copy(&self) -> NLayout { self.clone() } + + #[staticmethod] + pub fn generate_trivial_layout(num_qubits: usize) -> Self { + NLayout { + logic_to_phys: (0..num_qubits).collect(), + phys_to_logic: (0..num_qubits).collect(), + } + } } #[pymodule] diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 858aaae00094..c3bcc3440000 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -133,10 +133,7 @@ fn layout_trial( // forward and reverse for _direction in 0..2 { let dag = apply_layout(dag_nodes, &initial_layout, num_physical_qubits, num_clbits); - let mut pass_final_layout = NLayout { - logic_to_phys: (0..num_physical_qubits).collect(), - phys_to_logic: (0..num_physical_qubits).collect(), - }; + let mut pass_final_layout = NLayout::generate_trivial_layout(num_physical_qubits); build_swap_map_inner( num_physical_qubits, &dag, @@ -154,10 +151,7 @@ fn layout_trial( } } let layout_dag = apply_layout(dag_nodes, &initial_layout, num_physical_qubits, num_clbits); - let mut final_layout = NLayout { - logic_to_phys: (0..num_physical_qubits).collect(), - phys_to_logic: (0..num_physical_qubits).collect(), - }; + let mut final_layout = NLayout::generate_trivial_layout(num_physical_qubits); let (swap_map, gate_order) = build_swap_map_inner( num_physical_qubits, &layout_dag, From f16d0a3291f92fb345ff6ed4a62b6e8709e617d3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 15:44:08 -0500 Subject: [PATCH 18/24] Fix fixed layout tests after updates Since more recent commits fixed a few bugs in the behavior of the SabreLayout pass, the previously updated fixed layout tests were no longer correct. This commit updates the tests which were now failing because the layout changed again after fixing bugs in the new pass code. --- .../transpiler/test_preset_passmanagers.py | 86 +++++++++---------- test/python/transpiler/test_sabre_layout.py | 46 +++++----- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 3b4c8090bb73..9184bd996583 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -716,70 +716,70 @@ def test_layout_tokyo_fully_connected_cx(self, level): } sabre_layout = { - 0: ancilla[12], - 1: ancilla[10], - 2: ancilla[0], - 3: ancilla[1], - 4: ancilla[2], + 0: ancilla[0], + 1: ancilla[1], + 2: ancilla[2], + 3: ancilla[3], + 4: ancilla[4], 5: qr[1], - 6: qr[0], - 7: ancilla[3], - 8: ancilla[4], - 9: ancilla[5], - 10: qr[2], - 11: qr[4], - 12: ancilla[6], - 13: ancilla[7], - 14: ancilla[8], - 15: ancilla[13], - 16: qr[3], - 17: ancilla[14], - 18: ancilla[9], - 19: ancilla[11], + 6: qr[2], + 7: ancilla[5], + 8: ancilla[6], + 9: ancilla[7], + 10: qr[3], + 11: qr[0], + 12: qr[4], + 13: ancilla[8], + 14: ancilla[9], + 15: ancilla[10], + 16: ancilla[11], + 17: ancilla[12], + 18: ancilla[13], + 19: ancilla[14], } sabre_layout_lvl_2 = { - 0: ancilla[10], - 1: ancilla[0], + 0: ancilla[0], + 1: qr[4], 2: ancilla[1], 3: ancilla[2], 4: ancilla[3], - 5: qr[4], - 6: qr[1], + 5: qr[1], + 6: qr[0], 7: ancilla[4], 8: ancilla[5], 9: ancilla[6], 10: qr[2], - 11: qr[0], + 11: qr[3], 12: ancilla[7], 13: ancilla[8], 14: ancilla[9], - 15: ancilla[13], - 16: qr[3], - 17: ancilla[14], - 18: ancilla[11], - 19: ancilla[12], + 15: ancilla[10], + 16: ancilla[11], + 17: ancilla[12], + 18: ancilla[13], + 19: ancilla[14], } sabre_layout_lvl_3 = { - 6: qr[0], - 11: ancilla[8], - 10: qr[1], - 5: qr[4], - 7: qr[2], - 1: qr[3], - 16: ancilla[7], - 17: ancilla[6], 0: ancilla[0], + 1: qr[4], 2: ancilla[1], 3: ancilla[2], 4: ancilla[3], - 8: ancilla[4], - 9: ancilla[5], - 12: ancilla[9], - 13: ancilla[10], - 14: ancilla[11], - 15: ancilla[12], + 5: qr[1], + 6: qr[0], + 7: ancilla[4], + 8: ancilla[5], + 9: ancilla[6], + 10: qr[2], + 11: qr[3], + 12: ancilla[7], + 13: ancilla[8], + 14: ancilla[9], + 15: ancilla[10], + 16: ancilla[11], + 17: ancilla[12], 18: ancilla[13], 19: ancilla[14], } diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 1e7540f7107d..f1757a955f56 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -59,11 +59,11 @@ def test_5q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr[0]], 16) - self.assertEqual(layout[qr[1]], 17) - self.assertEqual(layout[qr[2]], 11) - self.assertEqual(layout[qr[3]], 18) - self.assertEqual(layout[qr[4]], 10) + self.assertEqual(layout[qr[0]], 18) + self.assertEqual(layout[qr[1]], 11) + self.assertEqual(layout[qr[2]], 13) + self.assertEqual(layout[qr[3]], 12) + self.assertEqual(layout[qr[4]], 14) def test_6q_circuit_20q_coupling(self): """Test finds layout for 6q circuit on 20q device.""" @@ -95,12 +95,12 @@ def test_6q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr0[0]], 11) - self.assertEqual(layout[qr0[1]], 10) - self.assertEqual(layout[qr0[2]], 12) - self.assertEqual(layout[qr1[0]], 16) - self.assertEqual(layout[qr1[1]], 17) - self.assertEqual(layout[qr1[2]], 5) + self.assertEqual(layout[qr0[0]], 12) + self.assertEqual(layout[qr0[1]], 7) + self.assertEqual(layout[qr0[2]], 14) + self.assertEqual(layout[qr1[0]], 11) + self.assertEqual(layout[qr1[1]], 18) + self.assertEqual(layout[qr1[2]], 13) def test_layout_with_classical_bits(self): """Test sabre layout with classical bits recreate from issue #8635.""" @@ -193,20 +193,20 @@ def test_layout_many_search_trials(self): ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout - self.assertEqual(layout[qc.qubits[0]], 11) - self.assertEqual(layout[qc.qubits[1]], 6) - self.assertEqual(layout[qc.qubits[2]], 14) - self.assertEqual(layout[qc.qubits[3]], 4) - self.assertEqual(layout[qc.qubits[4]], 16) - self.assertEqual(layout[qc.qubits[5]], 20) - self.assertEqual(layout[qc.qubits[6]], 12) + self.assertEqual(layout[qc.qubits[0]], 19) + self.assertEqual(layout[qc.qubits[1]], 10) + self.assertEqual(layout[qc.qubits[2]], 1) + self.assertEqual(layout[qc.qubits[3]], 14) + self.assertEqual(layout[qc.qubits[4]], 4) + self.assertEqual(layout[qc.qubits[5]], 11) + self.assertEqual(layout[qc.qubits[6]], 8) self.assertEqual(layout[qc.qubits[7]], 7) - self.assertEqual(layout[qc.qubits[8]], 15) - self.assertEqual(layout[qc.qubits[9]], 21) - self.assertEqual(layout[qc.qubits[10]], 10) - self.assertEqual(layout[qc.qubits[11]], 1) + self.assertEqual(layout[qc.qubits[8]], 9) + self.assertEqual(layout[qc.qubits[9]], 0) + self.assertEqual(layout[qc.qubits[10]], 5) + self.assertEqual(layout[qc.qubits[11]], 13) self.assertEqual(layout[qc.qubits[12]], 2) - self.assertEqual(layout[qc.qubits[13]], 0) + self.assertEqual(layout[qc.qubits[13]], 3) if __name__ == "__main__": From b206473d9f086e2ff0c7a288a3696e7600cb5394 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 14 Nov 2022 17:43:10 -0500 Subject: [PATCH 19/24] Try nesting parallelism in the sabres Looking at profiles for running the new SabreLayout pass, as expected the runtime of the rust SabreSwap routines is dominating. This is because we've basically serialized the sabre swap routines and are running multiple seed trials. As an experiment this commit sets the inner SabreSwap routines to run in parallel too. Since the rayon algorithm uses a work stealing algorithm this hopefully shouldn't cause too much extra overhead, especially because the layout trials are quite fast. This ideally means we're just scheduling each sabre swap trial in a big parallel work queue and rayon does the rest of the magic to figure out how to execute things. Initial testing is showing an improvement for large circuits and a more modest improvement for more modest circuits. --- src/sabre_layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index c3bcc3440000..0a0838482bf1 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -68,7 +68,7 @@ pub fn sabre_layout_and_routing( seed_trial, max_iterations, num_swap_trials, - false, + run_in_parallel, ), ) }) From 1467e87fdc98960415ea3c478a240bdf9de83a78 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 15 Nov 2022 07:03:10 -0500 Subject: [PATCH 20/24] Add skip_routing argument to preserve custom user provided routing This commit adds a new argument, skip_routing, to the SabreLayout constructor. The intent of this new option is to enable mixing custom routing_method user arguments with SabreLayout in it's new accelerated mode of operation. In the earlier commits no matter what users specified the preset pass manager construction would use sabreswap for routing as it was run internally as part of layout. This meant doing something like: transpile(qc, backend, routing_method='stochastic') would really run SabreSwap which is clearly not the user intent. To provide the layout benefits with multiple seed trials this new argument allows disabling the application of the routing found. This comes with a runtime penalty because effectively we end up running routing twice and only using one of the results. But for custom user provided methods or plugins this seems like a reasonable tradeoff. --- qiskit/transpiler/passes/layout/sabre_layout.py | 11 +++++++++++ qiskit/transpiler/preset_passmanagers/level0.py | 2 ++ qiskit/transpiler/preset_passmanagers/level1.py | 16 ++++++++++++++-- qiskit/transpiler/preset_passmanagers/level2.py | 2 ++ qiskit/transpiler/preset_passmanagers/level3.py | 2 ++ .../rusty-sabre-layout-2e1ca05d1902dcb5.yaml | 13 +++++++++---- 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index e0d5a290fc7a..560373517391 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -81,6 +81,7 @@ def __init__( max_iterations=3, swap_trials=None, layout_trials=None, + skip_routing=False, ): """SabreLayout initializer. @@ -112,6 +113,11 @@ def __init__( physical CPUs will be used as the default value. This option is mutually exclusive with the ``routing_pass`` argument and an error will be raised if both are used. + skip_routing (bool): If this is set ``True`` and ``routing_pass` is not used + then routing will not be applied to the output circuit. Only the layout + will be returned in the property set. This is a tradeoff to run custom + routing with multiple layout trials, as using this option will cause + SabreLayout to run the routing stage internally but not use that result. Raises: TranspilerError: If both ``routing_pass`` and ``swap_trials`` or @@ -143,6 +149,7 @@ def __init__( self.layout_trials = CPU_COUNT else: self.layout_trials = layout_trials + self.skip_routing = skip_routing def run(self, dag): """Run the SabreLayout pass on `dag`. @@ -240,6 +247,10 @@ def run(self, dag): layout_dict[dag.qubits[k]] = v initital_layout = Layout(layout_dict) self.property_set["layout"] = initital_layout + # If skip_routing is set then return the layout in the property set + # and throwaway the extra work we did to compute the swap map + if self.skip_routing: + return dag ancilla_pass = FullAncillaAllocation(self.coupling_map) ancilla_pass.property_set = self.property_set dag = ancilla_pass.run(dag) diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 613f6eb8535b..6ead4237a113 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -84,12 +84,14 @@ def _choose_layout_condition(property_set): elif layout_method == "noise_adaptive": _choose_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": + skip_routing = pass_manager_config.routing_method is not None and routing_method != "sabre" _choose_layout = SabreLayout( coupling_map, max_iterations=1, seed=seed_transpiler, swap_trials=5, layout_trials=5, + skip_routing=skip_routing, ) # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 45adc60e4058..445cbe89de58 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -139,13 +139,25 @@ def _vf2_match_not_found(property_set): _improve_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _improve_layout = SabreLayout( - coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5, layout_trials=5 + coupling_map, + max_iterations=2, + seed=seed_transpiler, + swap_trials=5, + layout_trials=5, + skip_routing=pass_manager_config.routing_method is not None + and routing_method != "sabre", ) elif layout_method is None: _improve_layout = common.if_has_control_flow_else( DenseLayout(coupling_map, backend_properties, target=target), SabreLayout( - coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5, layout_trials=5 + coupling_map, + max_iterations=2, + seed=seed_transpiler, + swap_trials=5, + layout_trials=5, + skip_routing=pass_manager_config.routing_method is not None + and routing_method != "sabre", ), ).to_flow_controller() diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 36229b21b89e..0a15c919bc36 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -134,6 +134,8 @@ def _vf2_match_not_found(property_set): seed=seed_transpiler, swap_trials=10, layout_trials=10, + skip_routing=pass_manager_config.routing_method is not None + and routing_method != "sabre", ) # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 4e1de9707b3a..7d01594f67ec 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -140,6 +140,8 @@ def _vf2_match_not_found(property_set): seed=seed_transpiler, swap_trials=20, layout_trials=20, + skip_routing=pass_manager_config.routing_method is not None + and routing_method != "sabre", ) # Choose routing pass diff --git a/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml b/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml index faeed98a8af9..5a835d1e00bb 100644 --- a/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml +++ b/releasenotes/notes/rusty-sabre-layout-2e1ca05d1902dcb5.yaml @@ -7,7 +7,7 @@ features: layout and routing. This was done to not only improve the runtime performance but also improve the quality of the results. The previous functionality of the pass as an analysis pass can be retained by manually setting the ``routing_pass`` - argument. + argument or using the new ``skip_routing`` argument. - | The :class:`~.SabreLayout` transpiler pass has a new constructor argument ``layout_trials``. This argument is used to control how many random number @@ -18,7 +18,7 @@ features: to minimize the potential performance overhead of running layout multiple times. By default if this is not specified the :class:`~.SabreLayout` pass will default to using the number of physical CPUs are available on the - local system. This multiple seed + local system. upgrade: - | The default behavior of the :class:`~.SabreLayout` compiler pass has @@ -33,7 +33,7 @@ upgrade: of improving the quality output, the layout and routing quality are highly coupled and :class:`~.SabreLayout` will now run multiple parallel seed trials and to calculate which seed provides the best results it needs to - perform both the layout and routing together. There are two ways you can + perform both the layout and routing together. There are three ways you can adjust the usage in your custom pass manager. The first is to avoid using embedding in your preset pass manager. If you were previously running something like:: @@ -69,7 +69,12 @@ upgrade: layout_pass = SabreLayout(coupling_map, routing_pass=routing_pass, seed=seed) which will have :class:`~.SabreLayout` run as an analysis pass and just set - the ``layout`` property set. + the ``layout`` property set. The final approach is to leverage the ``skip_routing`` + argument on :class:`~SabreLayout`, when this argument is set to ``True`` it will + skip applying the found layout and inserting the swap gates from routing. However, + doing this has a runtime penalty as :class:`~SabreLayout` will still be computing + the routing and just does not use this data. The first two approaches outlined do + not have additional overhead associated with them. - | The layouts computed by the :class:`~.SabreLayout` pass (when run without the ``routing_pass`` argument) with a fixed seed value may change from From c09a03a73274d1f4e0417fddcb8cb2a7c85031cb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 15 Nov 2022 08:31:53 -0500 Subject: [PATCH 21/24] Fix typo in docstring --- qiskit/transpiler/passes/layout/sabre_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 560373517391..5b1fb7bb738f 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -113,7 +113,7 @@ def __init__( physical CPUs will be used as the default value. This option is mutually exclusive with the ``routing_pass`` argument and an error will be raised if both are used. - skip_routing (bool): If this is set ``True`` and ``routing_pass` is not used + skip_routing (bool): If this is set ``True`` and ``routing_pass`` is not used then routing will not be applied to the output circuit. Only the layout will be returned in the property set. This is a tradeoff to run custom routing with multiple layout trials, as using this option will cause From 87f7a57d727f8c4ed61f48cd6434d1612e905b76 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 15 Nov 2022 11:46:29 -0500 Subject: [PATCH 22/24] Update random seed usage in rust code In #9132 we updated the random seed parameters in the rust code for sabre swap to make the seed optional and default to initializing from entropy if it's not specified. This commit updates the usage to account for this change on main. --- src/sabre_layout.rs | 4 ++-- src/sabre_swap/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 0a0838482bf1..7065ef388b39 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -140,7 +140,7 @@ fn layout_trial( neighbor_table, distance_matrix, heuristic, - seed, + Some(seed), &mut pass_final_layout, num_swap_trials, Some(run_swap_in_parallel), @@ -158,7 +158,7 @@ fn layout_trial( neighbor_table, distance_matrix, heuristic, - seed, + Some(seed), &mut final_layout, num_swap_trials, Some(run_swap_in_parallel), diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index 8e4d46485034..1b49103ab100 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -178,7 +178,7 @@ pub fn build_swap_map_inner( neighbor_table: &NeighborTable, dist: &ArrayView2, heuristic: &Heuristic, - seed: u64, + seed: Option, layout: &mut NLayout, num_trials: usize, run_in_parallel: Option, From fa61681331d79a5a35ccbe3fdd8bdf6e449799d7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 21 Nov 2022 11:01:05 -0500 Subject: [PATCH 23/24] s/retworkx/rustworkx/g --- qiskit/transpiler/passes/layout/sabre_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 5b1fb7bb738f..c7bb6b2fa5b0 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -16,7 +16,7 @@ import copy import logging import numpy as np -import retworkx +import rustworkx as rx from qiskit.converters import dag_to_circuit from qiskit.transpiler.passes.layout.set_layout import SetLayout @@ -133,7 +133,7 @@ def __init__( # constraints self.coupling_map = copy.deepcopy(self.coupling_map) self.coupling_map.make_symmetric() - self._neighbor_table = NeighborTable(retworkx.adjacency_matrix(self.coupling_map.graph)) + self._neighbor_table = NeighborTable(rx.adjacency_matrix(self.coupling_map.graph)) if routing_pass is not None and (swap_trials is not None or layout_trials is not None): raise TranspilerError("Both routing_pass and swap_trials can't be set at the same time") From be74de6a6b884135f98ec2fe2d1a34043fba330d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 21 Nov 2022 14:41:23 -0500 Subject: [PATCH 24/24] Use VF2 to find a partial layout for seeding SabreLayout 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 the initial guess starts 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. --- .../transpiler/passes/layout/sabre_layout.py | 240 +++++++++++++++++- qiskit/transpiler/passes/layout/vf2_utils.py | 5 +- .../transpiler/preset_passmanagers/level0.py | 1 + .../transpiler/preset_passmanagers/level1.py | 3 + .../transpiler/preset_passmanagers/level2.py | 2 + .../transpiler/preset_passmanagers/level3.py | 2 + src/sabre_layout.rs | 32 ++- 7 files changed, 267 insertions(+), 18 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index c7bb6b2fa5b0..30c5f67a8861 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -13,8 +13,11 @@ """Layout selection using the SABRE bidirectional search approach from Li et al. """ +from collections import defaultdict import copy import logging +import time + import numpy as np import rustworkx as rx @@ -23,6 +26,7 @@ from qiskit.transpiler.passes.layout.full_ancilla_allocation import FullAncillaAllocation from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout +from qiskit.transpiler.passes.layout import vf2_utils from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import TransformationPass @@ -35,6 +39,8 @@ ) from qiskit.transpiler.passes.routing.sabre_swap import process_swaps, apply_gate from qiskit.tools.parallel import CPU_COUNT +from qiskit.circuit.controlflow import ControlFlowOp, ForLoopOp +from qiskit.converters import circuit_to_dag logger = logging.getLogger(__name__) @@ -42,11 +48,16 @@ class SabreLayout(TransformationPass): """Choose a Layout via iterative bidirectional routing of the input circuit. - Starting with a random initial `Layout`, the algorithm does a full routing - of the circuit (via the `routing_pass` method) to end up with a - `final_layout`. This final_layout is then used as the initial_layout for - routing the reverse circuit. The algorithm iterates a number of times until - it finds an initial_layout that reduces full routing cost. + The algorithm does a full routing of the circuit (via the `routing_pass` + method) to end up with a `final_layout`. This final_layout is then used as + the initial_layout for routing the reverse circuit. The algorithm iterates a + number of times until it finds an initial_layout that reduces full routing cost. + + Prior to running the SABRE algorithm this transpiler pass will try to find the layout + for deepest layer that is has an isomorphic subgraph in the coupling graph. This is + done by progressively using the algorithm from :class:`~.VF2Layout` on the circuit + until a mapping is not found. This partial layout is then used to seed the SABRE algorithm + and then random physical bits are selected for the remaining elements in the mapping. This method exploits the reversibility of quantum circuits, and tries to include global circuit information in the choice of initial_layout. @@ -82,6 +93,11 @@ def __init__( swap_trials=None, layout_trials=None, skip_routing=False, + target=None, + vf2_partial_layout=True, + vf2_call_limit=None, + vf2_time_limit=None, + vf2_max_trials=None, ): """SabreLayout initializer. @@ -118,6 +134,18 @@ def __init__( will be returned in the property set. This is a tradeoff to run custom routing with multiple layout trials, as using this option will cause SabreLayout to run the routing stage internally but not use that result. + target (Target): A target representing the backend device to run ``SabreLayout`` on. + If specified it will supersede a set value for ``coupling_map``. + vf2_partial_layout (bool): Run vf2 partial layout + vf2_call_limit (int): The number of state visits to attempt in each execution of + VF2 to attempt to find a partial layout. + vf2_time_limit (float): The total time limit in seconds to run VF2 to find a partial + layout + vf2_max_trials (int): The maximum number of trials to run VF2 to find + a partial layout. If this is not specified the number of trials will be limited + based on the number of edges in the interaction graph or the coupling graph + (whichever is larger) if no other limits are set. If set to a value <= 0 no + limit on the number of trials will be set. Raises: TranspilerError: If both ``routing_pass`` and ``swap_trials`` or @@ -126,15 +154,6 @@ def __init__( super().__init__() self.coupling_map = coupling_map self._neighbor_table = None - if self.coupling_map is not None: - if not self.coupling_map.is_symmetric: - # deepcopy is needed here to avoid modifications updating - # shared references in passes which require directional - # constraints - self.coupling_map = copy.deepcopy(self.coupling_map) - self.coupling_map.make_symmetric() - self._neighbor_table = NeighborTable(rx.adjacency_matrix(self.coupling_map.graph)) - if routing_pass is not None and (swap_trials is not None or layout_trials is not None): raise TranspilerError("Both routing_pass and swap_trials can't be set at the same time") self.routing_pass = routing_pass @@ -150,6 +169,22 @@ def __init__( else: self.layout_trials = layout_trials self.skip_routing = skip_routing + self.target = target + if target is not None: + self.coupling_map = target.build_coupling_map() + self.avg_error_map = None + self.vf2_partial_layout = vf2_partial_layout + self.call_limit = vf2_call_limit + self.time_limit = vf2_time_limit + self.max_trials = vf2_max_trials + if self.coupling_map is not None: + if not self.coupling_map.is_symmetric: + # deepcopy is needed here to avoid modifications updating + # shared references in passes which require directional + # constraints + self.coupling_map = copy.deepcopy(self.coupling_map) + self.coupling_map.make_symmetric() + self._neighbor_table = NeighborTable(rx.adjacency_matrix(self.coupling_map.graph)) def run(self, dag): """Run the SabreLayout pass on `dag`. @@ -210,6 +245,10 @@ def run(self, dag): dist_matrix = self.coupling_map.distance_matrix original_qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} original_clbit_indices = {bit: index for index, bit in enumerate(dag.clbits)} + partial_layout = None + if self.vf2_partial_layout: + partial_layout_virtual_bits = self._vf2_partial_layout(dag).get_virtual_bits() + partial_layout = [partial_layout_virtual_bits.get(i, None) for i in dag.qubits] dag_list = [] for node in dag.topological_op_nodes(): @@ -235,6 +274,7 @@ def run(self, dag): self.max_iterations, self.swap_trials, self.layout_trials, + partial_layout, ) # Apply initial layout selected. # this is a pseudo-pass manager to avoid the repeated round trip between @@ -313,3 +353,175 @@ def _compose_layouts(self, initial_layout, pass_final_layout, qregs): qubit_map = Layout.combine_into_edge_map(initial_layout, trivial_layout) final_layout = {v: pass_final_layout._v2p[qubit_map[v]] for v in initial_layout._v2p} return Layout(final_layout) + + # TODO: Migrate this to rust as part of sabre_layout.rs after + # https://github.com/Qiskit/rustworkx/issues/741 is implemented and released + def _vf2_partial_layout(self, dag): + """Find a partial layout using vf2 on the deepest subgraph that is isomorphic to + the coupling graph.""" + im_graph_node_map = {} + reverse_im_graph_node_map = {} + im_graph = rx.PyGraph(multigraph=False) + logger.debug("Buidling interaction graphs") + largest_im_graph = None + best_mapping = None + first_mapping = None + if self.avg_error_map is None: + self.avg_error_map = vf2_utils.build_average_error_map( + self.target, None, self.coupling_map + ) + + cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(self.coupling_map, self.seed, False) + # To avoid trying to over optimize the result by default limit the number + # of trials based on the size of the graphs. For circuits with simple layouts + # like an all 1q circuit we don't want to sit forever trying every possible + # mapping in the search space if no other limits are set + if self.max_trials is None and self.call_limit is None and self.time_limit is None: + im_graph_edge_count = len(im_graph.edge_list()) + cm_graph_edge_count = len(self.coupling_map.graph.edge_list()) + self.max_trials = max(im_graph_edge_count, cm_graph_edge_count) + 15 + + start_time = time.time() + + # A more efficient search pattern would be to do a binary search + # and find, but to conserve memory and avoid a large number of + # unecessary graphs this searchs from the beginning and continues + # until there is no vf2 match + def _visit(dag, weight, wire_map): + for node in dag.topological_op_nodes(): + nonlocal largest_im_graph + largest_im_graph = im_graph.copy() + if getattr(node.op, "_directive", False): + continue + if isinstance(node.op, ControlFlowOp): + if isinstance(node.op, ForLoopOp): + inner_weight = len(node.op.params[0]) * weight + else: + inner_weight = weight + for block in node.op.blocks: + inner_wire_map = { + inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) + } + _visit(circuit_to_dag(block), inner_weight, inner_wire_map) + continue + len_args = len(node.qargs) + qargs = [wire_map[q] for q in node.qargs] + if len_args == 1: + if qargs[0] not in im_graph_node_map: + weights = defaultdict(int) + weights[node.name] += weight + im_graph_node_map[qargs[0]] = im_graph.add_node(weights) + reverse_im_graph_node_map[im_graph_node_map[qargs[0]]] = qargs[0] + else: + im_graph[im_graph_node_map[qargs[0]]][node.op.name] += weight + if len_args == 2: + if qargs[0] not in im_graph_node_map: + im_graph_node_map[qargs[0]] = im_graph.add_node(defaultdict(int)) + reverse_im_graph_node_map[im_graph_node_map[qargs[0]]] = qargs[0] + if qargs[1] not in im_graph_node_map: + im_graph_node_map[qargs[1]] = im_graph.add_node(defaultdict(int)) + reverse_im_graph_node_map[im_graph_node_map[qargs[1]]] = qargs[1] + edge = (im_graph_node_map[qargs[0]], im_graph_node_map[qargs[1]]) + if im_graph.has_edge(*edge): + im_graph.get_edge_data(*edge)[node.name] += weight + else: + weights = defaultdict(int) + weights[node.name] += weight + im_graph.add_edge(*edge, weights) + if len_args > 2: + raise TranspilerError( + "Encountered an instruction operating on more than 2 qubits, this pass " + "only functions with 1 or 2 qubit operations." + ) + vf2_mapping = rx.vf2_mapping( + cm_graph, + im_graph, + subgraph=True, + id_order=False, + induced=False, + call_limit=self.call_limit, + ) + try: + nonlocal first_mapping + first_mapping = next(vf2_mapping) + except StopIteration: + break + nonlocal best_mapping + best_mapping = vf2_mapping + elapsed_time = time.time() - start_time + if ( + self.time_limit is not None + and best_mapping is not None + and elapsed_time >= self.time_limit + ): + logger.debug( + "SabreLayout VF2 heuristic has taken %s which exceeds configured max time: %s", + elapsed_time, + self.time_limit, + ) + break + + _visit(dag, 1, {bit: bit for bit in dag.qubits}) + logger.debug("Finding best mappings of largest partial subgraph") + im_graph = largest_im_graph + + def mapping_to_layout(layout_mapping): + return Layout({reverse_im_graph_node_map[k]: v for k, v in layout_mapping.items()}) + + layout_mapping = {im_i: cm_nodes[cm_i] for cm_i, im_i in first_mapping.items()} + chosen_layout = mapping_to_layout(layout_mapping) + chosen_layout_score = vf2_utils.score_layout( + self.avg_error_map, + layout_mapping, + im_graph_node_map, + reverse_im_graph_node_map, + im_graph, + False, + ) + trials = 1 + for mapping in best_mapping: # pylint: disable=not-an-iterable + trials += 1 + logger.debug("Running trial: %s", trials) + layout_mapping = {im_i: cm_nodes[cm_i] for cm_i, im_i in mapping.items()} + # If the graphs have the same number of nodes we don't need to score or do multiple + # trials as the score heuristic currently doesn't weigh nodes based on gates on a + # qubit so the scores will always all be the same + if len(cm_graph) == len(im_graph): + break + layout_score = vf2_utils.score_layout( + self.avg_error_map, + layout_mapping, + im_graph_node_map, + reverse_im_graph_node_map, + im_graph, + False, + ) + logger.debug("Trial %s has score %s", trials, layout_score) + if chosen_layout is None: + chosen_layout = mapping_to_layout(layout_mapping) + chosen_layout_score = layout_score + elif layout_score < chosen_layout_score: + layout = mapping_to_layout(layout_mapping) + logger.debug( + "Found layout %s has a lower score (%s) than previous best %s (%s)", + layout, + layout_score, + chosen_layout, + chosen_layout_score, + ) + chosen_layout = layout + chosen_layout_score = layout_score + if self.max_trials and trials >= self.max_trials: + logger.debug("Trial %s is >= configured max trials %s", trials, self.max_trials) + break + elapsed_time = time.time() - start_time + if self.time_limit is not None and elapsed_time >= self.time_limit: + logger.debug( + "VF2Layout has taken %s which exceeds configured max time: %s", + elapsed_time, + self.time_limit, + ) + break + for reg in dag.qregs.values(): + chosen_layout.add_register(reg) + return chosen_layout diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 1cb868d1d247..80a09338355b 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -98,8 +98,9 @@ def score_layout( size = 0 nlayout = NLayout(layout_mapping, size + 1, size + 1) bit_list = np.zeros(len(im_graph), dtype=np.int32) - for node_index in bit_map.values(): - bit_list[node_index] = sum(im_graph[node_index].values()) + for qubit, node_index in bit_map.items(): + if qubit in layout_mapping: + bit_list[node_index] = sum(im_graph[node_index].values()) edge_list = { (edge[0], edge[1]): sum(edge[2].values()) for edge in im_graph.edge_index_map().values() } diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 6ead4237a113..e74e91307753 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -92,6 +92,7 @@ def _choose_layout_condition(property_set): swap_trials=5, layout_trials=5, skip_routing=skip_routing, + target=target, ) # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 2180fc9e19ba..f98b7b3dcdde 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -146,6 +146,8 @@ def _vf2_match_not_found(property_set): layout_trials=5, skip_routing=pass_manager_config.routing_method is not None and routing_method != "sabre", + target=target, + vf2_call_limit=int(5e4), ) elif layout_method is None: _improve_layout = common.if_has_control_flow_else( @@ -158,6 +160,7 @@ def _vf2_match_not_found(property_set): layout_trials=5, skip_routing=pass_manager_config.routing_method is not None and routing_method != "sabre", + target=target, ), ).to_flow_controller() diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index b37c8d1a4d67..4dc43db32dce 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -136,6 +136,8 @@ def _vf2_match_not_found(property_set): layout_trials=10, skip_routing=pass_manager_config.routing_method is not None and routing_method != "sabre", + target=target, + vf2_call_limit=int(5e6), ) # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index f0bb68538178..5f786c5d69ca 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -142,6 +142,8 @@ def _vf2_match_not_found(property_set): layout_trials=20, skip_routing=pass_manager_config.routing_method is not None and routing_method != "sabre", + target=target, + vf2_call_limit=int(3e7), ) # Choose routing pass diff --git a/src/sabre_layout.rs b/src/sabre_layout.rs index 7065ef388b39..9824b3072d3f 100644 --- a/src/sabre_layout.rs +++ b/src/sabre_layout.rs @@ -41,6 +41,7 @@ pub fn sabre_layout_and_routing( max_iterations: usize, num_swap_trials: usize, num_layout_trials: usize, + partial_layout: Option>>, ) -> ([NLayout; 2], SwapMap, PyObject) { let run_in_parallel = getenv_use_multiple_threads(); let outer_rng = match seed { @@ -69,6 +70,7 @@ pub fn sabre_layout_and_routing( max_iterations, num_swap_trials, run_in_parallel, + partial_layout.clone(), ), ) }) @@ -94,6 +96,7 @@ pub fn sabre_layout_and_routing( max_iterations, num_swap_trials, run_in_parallel, + partial_layout.clone(), ) }) .min_by_key(|result| result.1.map.values().map(|x| x.len()).sum::()) @@ -112,17 +115,42 @@ fn layout_trial( max_iterations: usize, num_swap_trials: usize, run_swap_in_parallel: bool, + partial_layout: Option>>, ) -> ([NLayout; 2], SwapMap, Vec) { // Pick a random initial layout and fully populate ancillas in that layout too let num_physical_qubits = distance_matrix.shape()[0]; let mut rng = Pcg64Mcg::seed_from_u64(seed); - let mut physical_qubits: Vec = (0..num_physical_qubits).collect(); - physical_qubits.shuffle(&mut rng); + let mut physical_qubits: Vec; + match partial_layout { + Some(partial_layout_bits) => { + let used_bits: HashSet = partial_layout_bits + .iter() + .filter_map(|x| x.as_ref()) + .copied() + .collect(); + let mut free_bits: Vec = (0..num_physical_qubits) + .filter(|x| !used_bits.contains(x)) + .collect(); + free_bits.shuffle(&mut rng); + physical_qubits = partial_layout_bits + .iter() + .map(|x| match x { + Some(phys) => *phys, + None => free_bits.pop().unwrap(), + }) + .collect(); + } + None => { + physical_qubits = (0..num_physical_qubits).collect(); + physical_qubits.shuffle(&mut rng); + } + }; let mut phys_to_logic = vec![0; num_physical_qubits]; physical_qubits .iter() .enumerate() .for_each(|(logic, phys)| phys_to_logic[*phys] = logic); + let mut initial_layout = NLayout { logic_to_phys: physical_qubits, phys_to_logic,