diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 9ba3fd1265c3..3c3abdaf3dce 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -49,20 +49,42 @@ class SabreLayout(AnalysisPass): `arXiv:1809.02573 `_ """ - def __init__(self, coupling_map, routing_pass=None, seed=None, max_iterations=3): + def __init__( + self, coupling_map, routing_pass=None, seed=None, max_iterations=3, swap_trials=None + ): """SabreLayout initializer. 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. 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 + :class:`~.SabreSwap` for each iteration. This is equivalent to + the ``trials`` argument on :class:`~.SabreSwap`. If this is not + specified (and ``routing_pass`` isn't set) by default the number + of physical CPUs on your local system will be used. For + reproducibility between environments it is best to set this + to an explicit number because the output will potentially depend + 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. + + Raises: + TranspilerError: If both ``routing_pass`` and ``swap_trials`` are + specified """ super().__init__() self.coupling_map = coupling_map + if routing_pass is not None and swap_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 def run(self, dag): """Run the SabreLayout pass on `dag`. @@ -86,7 +108,9 @@ def run(self, dag): 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) + 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 diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 275864ba7fba..0e37bc8b4b7d 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -23,6 +23,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.dagcircuit import DAGOpNode +from qiskit.tools.parallel import CPU_COUNT # pylint: disable=import-error from qiskit._accelerate.sabre_swap import ( @@ -61,6 +62,11 @@ class SabreSwap(TransformationPass): scored according to some heuristic cost function. The best SWAP is implemented and ``current_layout`` updated. + This transpiler pass adds onto the SABRE algorithm in that it will run + multiple trials of the algorithm with different seeds. The best output, + deteremined by the trial with the least amount of SWAPed inserted, will + be selected from the random trials. + **References:** [1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem @@ -68,13 +74,7 @@ class SabreSwap(TransformationPass): `arXiv:1809.02573 `_ """ - def __init__( - self, - coupling_map, - heuristic="basic", - seed=None, - fake_run=False, - ): + def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False, trials=None): r"""SabreSwap initializer. Args: @@ -84,6 +84,12 @@ def __init__( seed (int): random seed used to tie-break among candidate swaps. fake_run (bool): if true, it only pretend to do routing, i.e., no swap is effectively added. + trials (int): The number of seed trials to run sabre with. These will + be run in parallel (unless the PassManager is already running in + parallel). If not specified this defaults to the number of physical + CPUs on the local system. For reproducible results it is recommended + that you set this explicitly, as the output will be deterministic for + a fixed number of trials. Raises: TranspilerError: If the specified heuristic is not valid. @@ -158,6 +164,11 @@ def __init__( self.seed = np.random.default_rng(None).integers(0, ii32.max, dtype=int) else: self.seed = seed + if trials is None: + self.trials = CPU_COUNT + else: + self.trials = trials + self.fake_run = fake_run self._qubit_indices = None self._clbit_indices = None @@ -216,6 +227,7 @@ def run(self, dag): self.heuristic, self.seed, layout, + self.trials, ) layout_mapping = layout.layout_mapping() diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d1fc2b5e67c7..d3bce75e11bb 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -206,7 +206,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.initial_layout, ) if optimization_level == 0: - routing_pass = SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler) + routing_pass = SabreSwap( + coupling_map, heuristic="basic", seed=seed_transpiler, trials=5 + ) return common.generate_routing_passmanager( routing_pass, target, @@ -215,7 +217,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana use_barrier_before_measurement=True, ) if optimization_level == 1: - routing_pass = SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler) + routing_pass = SabreSwap( + coupling_map, heuristic="lookahead", seed=seed_transpiler, trials=5 + ) return common.generate_routing_passmanager( routing_pass, target, @@ -227,7 +231,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana use_barrier_before_measurement=True, ) if optimization_level == 2: - routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) + routing_pass = SabreSwap( + coupling_map, heuristic="decay", seed=seed_transpiler, trials=10 + ) return common.generate_routing_passmanager( routing_pass, target, @@ -238,7 +244,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana use_barrier_before_measurement=True, ) if optimization_level == 3: - routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) + routing_pass = SabreSwap( + coupling_map, heuristic="decay", seed=seed_transpiler, trials=20 + ) return common.generate_routing_passmanager( routing_pass, target, diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index c02c517a23e1..9779ed5dd0d4 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -86,7 +86,9 @@ def _choose_layout_condition(property_set): elif layout_method == "noise_adaptive": _choose_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _choose_layout = SabreLayout(coupling_map, max_iterations=1, seed=seed_transpiler) + _choose_layout = SabreLayout( + coupling_map, max_iterations=1, seed=seed_transpiler, swap_trials=5 + ) toqm_pass = False # Choose routing pass diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 05e7d56a736e..f6a32087ac95 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -144,7 +144,9 @@ def _vf2_match_not_found(property_set): elif layout_method == "noise_adaptive": _improve_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _improve_layout = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler) + _improve_layout = SabreLayout( + coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5 + ) toqm_pass = False routing_pm = None diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 5a8dcde691e8..7c7beb59f3fd 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -127,7 +127,9 @@ def _vf2_match_not_found(property_set): elif layout_method == "noise_adaptive": _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _choose_layout_1 = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler) + _choose_layout_1 = SabreLayout( + coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=10 + ) toqm_pass = False routing_pm = None diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 0d664f2172a0..7f64b7873e5a 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -138,7 +138,9 @@ def _vf2_match_not_found(property_set): elif layout_method == "noise_adaptive": _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _choose_layout_1 = SabreLayout(coupling_map, max_iterations=4, seed=seed_transpiler) + _choose_layout_1 = SabreLayout( + coupling_map, max_iterations=4, seed=seed_transpiler, swap_trials=20 + ) toqm_pass = False # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface diff --git a/releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml b/releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml new file mode 100644 index 000000000000..388ecc954ff1 --- /dev/null +++ b/releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + The :class:`~.SabreSwap` transpiler pass has a new keyword argument on its + constructor, ``trials``. The ``trials`` argument is used to specify the + number of random seed trials to attempt. The output from the + `SABRE algorithm `__ can differ greatly + based on the seed used for the random number. :class:`~.SabreSwap` will + now run the algorithm with ``trials`` number of random seeds and pick the + best (with the fewest swaps inserted). If ``trials`` is not specified the + pass will default to use the number of physical CPUs on the local system. + - | + The :class:`~.SabreLayout` transpiler pass has a new keyword argument on + its constructor, ``swap_trials``. The ``swap_trials`` argument is used + to specify how many random seed trials to run on the :class:`~.SabreSwap` + pass internally. It corresponds to the ``trials`` arugment on the + :class:`~.SabreSwap` pass. When set, each iteration of + :class:`~.SabreSwap` will be run internally ``swap_trials`` times. + If ``swap_trials`` is not specified the will default to use + the number of physical CPUs on the local system. diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index c038e13b673a..1798a0dd0e39 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -53,6 +53,12 @@ pub enum Heuristic { Decay, } +struct TrialResult { + out_map: HashMap>, + gate_order: Vec, + layout: NLayout, +} + /// Return a set of candidate swaps that affect qubits in front_layer. /// /// For each virtual qubit in front_layer, find its current location @@ -154,18 +160,88 @@ pub fn build_swap_map( heuristic: &Heuristic, seed: u64, layout: &mut NLayout, -) -> PyResult<(SwapMap, PyObject)> { - let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); + num_trials: usize, +) -> (SwapMap, PyObject) { let run_in_parallel = getenv_use_multiple_threads(); - let mut out_map: HashMap> = HashMap::new(); - let mut front_layer: Vec = dag.first_layer.clone(); + let dist = distance_matrix.as_array(); + let coupling_graph: DiGraph<(), ()> = cmap_from_neighor_table(neighbor_table); + let outer_rng = Pcg64Mcg::seed_from_u64(seed); + let seed_vec: Vec = outer_rng + .sample_iter(&rand::distributions::Standard) + .take(num_trials) + .collect(); + let result = if run_in_parallel { + seed_vec + .into_par_iter() + .enumerate() + .map(|(index, seed_trial)| { + ( + index, + swap_map_trial( + num_qubits, + dag, + neighbor_table, + &dist, + &coupling_graph, + heuristic, + seed_trial, + layout.clone(), + ), + ) + }) + .min_by_key(|(index, result)| { + [ + result.out_map.values().map(|x| x.len()).sum::(), + *index, + ] + }) + .unwrap() + .1 + } else { + seed_vec + .into_iter() + .map(|seed_trial| { + swap_map_trial( + num_qubits, + dag, + neighbor_table, + &dist, + &coupling_graph, + heuristic, + seed_trial, + layout.clone(), + ) + }) + .min_by_key(|result| result.out_map.values().map(|x| x.len()).sum::()) + .unwrap() + }; + *layout = result.layout; + ( + SwapMap { + map: result.out_map, + }, + result.gate_order.into_pyarray(py).into(), + ) +} + +fn swap_map_trial( + num_qubits: usize, + dag: &SabreDAG, + neighbor_table: &NeighborTable, + dist: &ArrayView2, + coupling_graph: &DiGraph<(), ()>, + heuristic: &Heuristic, + seed: u64, + mut layout: NLayout, +) -> TrialResult { let max_iterations_without_progress = 10 * neighbor_table.neighbors.len(); + let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); let mut ops_since_progress: Vec<[usize; 2]> = Vec::new(); + let mut out_map: HashMap> = HashMap::new(); + let mut front_layer: Vec = dag.first_layer.clone(); let mut required_predecessors: Vec = vec![0; dag.dag.node_count()]; let mut extended_set: Option> = None; let mut num_search_steps: u8 = 0; - let dist = distance_matrix.as_array(); - let coupling_graph: DiGraph<(), ()> = cmap_from_neighor_table(neighbor_table); let mut qubits_decay: Vec = vec![1.; num_qubits]; let mut rng = Pcg64Mcg::seed_from_u64(seed); @@ -245,7 +321,8 @@ pub fn build_swap_map( Some(NodeIndex::::new(v)), |_| Ok(1.), Some(&mut shortest_paths), - ) as PyResult>>)?; + ) as PyResult>>) + .unwrap(); let shortest_path: Vec = shortest_paths .get(&NodeIndex::new(v)) .unwrap() @@ -308,14 +385,13 @@ pub fn build_swap_map( let best_swap = sabre_score_heuristic( &first_layer, - layout, + &mut layout, neighbor_table, extended_set.as_ref().unwrap(), - &dist, + dist, &qubits_decay, heuristic, &mut rng, - run_in_parallel, ); num_search_steps += 1; if num_search_steps >= DECAY_RESET_INTERVAL { @@ -327,10 +403,14 @@ pub fn build_swap_map( } ops_since_progress.push(best_swap); } - Ok((SwapMap { map: out_map }, gate_order.into_pyarray(py).into())) + TrialResult { + out_map, + gate_order, + layout, + } } -pub fn sabre_score_heuristic( +fn sabre_score_heuristic( layer: &[[usize; 2]], layout: &mut NLayout, neighbor_table: &NeighborTable, @@ -339,7 +419,6 @@ pub fn sabre_score_heuristic( qubits_decay: &[f64], heuristic: &Heuristic, rng: &mut Pcg64Mcg, - run_in_parallel: bool, ) -> [usize; 2] { // Run in parallel only if we're not already in a multiprocessing context // unless force threads is set. @@ -366,11 +445,7 @@ pub fn sabre_score_heuristic( } layout.swap_logical(swap_qubits[0], swap_qubits[1]); } - if run_in_parallel { - best_swaps.par_sort_unstable(); - } else { - best_swaps.sort_unstable(); - } + best_swaps.sort_unstable(); let best_swap = *best_swaps.choose(rng).unwrap(); layout.swap_logical(best_swap[0], best_swap[1]); best_swap diff --git a/test/python/transpiler/test_mappers.py b/test/python/transpiler/test_mappers.py index ef061f7c7d28..6a3d0d1748e3 100644 --- a/test/python/transpiler/test_mappers.py +++ b/test/python/transpiler/test_mappers.py @@ -294,7 +294,7 @@ class TestsSabreSwap(SwapperCommonTestCases, QiskitTestCase): """Test SwapperCommonTestCases using SabreSwap.""" pass_class = SabreSwap - additional_args = {"seed": 4242} + additional_args = {"seed": 1242} if __name__ == "__main__": diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 2ce8ef1d0343..2186813bf6ef 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -698,25 +698,25 @@ def test_layout_tokyo_fully_connected_cx(self, level): } sabre_layout = { - 6: qr[0], - 11: qr[1], - 10: qr[2], - 5: qr[3], - 16: qr[4], + 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], - 7: ancilla[5], - 8: ancilla[6], - 9: ancilla[7], - 12: ancilla[8], - 13: ancilla[9], - 14: ancilla[10], - 15: ancilla[11], - 17: ancilla[12], - 18: ancilla[13], + 5: ancilla[5], + 7: ancilla[6], + 8: ancilla[7], + 9: ancilla[8], + 10: ancilla[9], + 12: ancilla[10], + 13: ancilla[11], + 14: ancilla[12], + 15: ancilla[13], 19: ancilla[14], } @@ -913,7 +913,7 @@ def test_2(self, level): optimization_level=level, basis_gates=basis, coupling_map=coupling_map, - seed_transpiler=42, + seed_transpiler=42123, ) 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 27205afe8a26..77aa5af050b3 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -55,14 +55,14 @@ 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) + pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0, swap_trials=32) pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr[0]], 11) - self.assertEqual(layout[qr[1]], 6) - self.assertEqual(layout[qr[2]], 12) - self.assertEqual(layout[qr[3]], 5) + 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) def test_6q_circuit_20q_coupling(self): @@ -95,12 +95,12 @@ def test_6q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr0[0]], 8) - self.assertEqual(layout[qr0[1]], 2) + self.assertEqual(layout[qr0[0]], 2) + self.assertEqual(layout[qr0[1]], 3) self.assertEqual(layout[qr0[2]], 10) - self.assertEqual(layout[qr1[0]], 3) - self.assertEqual(layout[qr1[1]], 12) - self.assertEqual(layout[qr1[2]], 11) + self.assertEqual(layout[qr1[0]], 1) + self.assertEqual(layout[qr1[1]], 7) + self.assertEqual(layout[qr1[2]], 5) def test_layout_with_classical_bits(self): """Test sabre layout with classical bits recreate from issue #8635.""" @@ -193,18 +193,18 @@ def test_layout_many_search_trials(self): ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout - self.assertEqual(layout[qc.qubits[0]], 11) + 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]], 12) + 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]], 16) + self.assertEqual(layout[qc.qubits[6]], 11) self.assertEqual(layout[qc.qubits[7]], 25) - self.assertEqual(layout[qc.qubits[8]], 19) + self.assertEqual(layout[qc.qubits[8]], 16) self.assertEqual(layout[qc.qubits[9]], 3) - self.assertEqual(layout[qc.qubits[10]], 14) - self.assertEqual(layout[qc.qubits[11]], 15) + 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)