diff --git a/Cargo.lock b/Cargo.lock index 9283953439c7..0f821d94ffdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", @@ -90,9 +90,15 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "getrandom" @@ -105,6 +111,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", + "rayon", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -131,27 +147,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "rayon", ] [[package]] name = "indoc" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" +checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "lock_api" @@ -260,9 +276,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "parking_lot" @@ -287,6 +303,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "petgraph" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -295,9 +321,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -309,7 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.12.3", "indoc", "libc", "num-bigint", @@ -369,7 +395,7 @@ name = "qiskit-terra" version = "0.22.0" dependencies = [ "ahash 0.8.0", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "ndarray", "num-bigint", @@ -380,13 +406,14 @@ dependencies = [ "rand_distr", "rand_pcg", "rayon", + "retworkx-core", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -472,13 +499,26 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] +[[package]] +name = "retworkx-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353bcdcdab6c754ea32bce39ee7a763c8a3c16c91a8dd648befd14fbcb0d5b68" +dependencies = [ + "ahash 0.7.6", + "hashbrown 0.11.2", + "indexmap", + "petgraph", + "rayon", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -493,9 +533,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", @@ -510,15 +550,15 @@ checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unindent" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" +checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 881f7bd09a42..d0d3882db611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ rand_distr = "0.4.3" ahash = "0.8.0" num-complex = "0.4" num-bigint = "0.4" +retworkx-core = "0.11" [dependencies.pyo3] version = "0.16.5" diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 3f83d83b04dd..275864ba7fba 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -13,7 +13,6 @@ """Routing via SWAP insertion using the SABRE method from Li et al.""" import logging -from collections import defaultdict from copy import copy, deepcopy import numpy as np @@ -27,22 +26,15 @@ # pylint: disable=import-error from qiskit._accelerate.sabre_swap import ( - sabre_score_heuristic, + build_swap_map, Heuristic, - EdgeList, - QubitsDecay, NeighborTable, - SabreRng, + SabreDAG, ) from qiskit._accelerate.stochastic_swap import NLayout # pylint: disable=import-error logger = logging.getLogger(__name__) -EXTENDED_SET_SIZE = 20 # Size of lookahead window. TODO: set dynamically to len(current_layout) - -DECAY_RATE = 0.001 # Decay coefficient for penalizing serial swaps. -DECAY_RESET_INTERVAL = 5 # How often to reset all decay rates to 1. - class SabreSwap(TransformationPass): r"""Map input circuit onto a backend topology via insertion of SWAPs. @@ -167,9 +159,8 @@ def __init__( else: self.seed = seed self.fake_run = fake_run - self.required_predecessors = None - self.qubits_decay = None - self._bit_indices = None + self._qubit_indices = None + self._clbit_indices = None self.dist_matrix = None def run(self, dag): @@ -189,18 +180,8 @@ def run(self, dag): if len(dag.qubits) > self.coupling_map.size(): raise TranspilerError("More virtual qubits exist than physical.") - max_iterations_without_progress = 10 * len(dag.qubits) # Arbitrary. - ops_since_progress = [] - extended_set = None - - # Normally this isn't necessary, but here we want to log some objects that have some - # non-trivial cost to create. - do_expensive_logging = logger.isEnabledFor(logging.DEBUG) - self.dist_matrix = self.coupling_map.distance_matrix - rng = SabreRng(self.seed) - # Preserve input DAG's name, regs, wire_map, etc. but replace the graph. mapped_dag = None if not self.fake_run: @@ -208,244 +189,68 @@ def run(self, dag): canonical_register = dag.qregs["q"] current_layout = Layout.generate_trivial_layout(canonical_register) - self._bit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + self._qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + self._clbit_indices = {bit: idx for idx, bit in enumerate(dag.clbits)} layout_mapping = { - self._bit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() + self._qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() } layout = NLayout(layout_mapping, len(dag.qubits), self.coupling_map.size()) - - # A decay factor for each qubit used to heuristically penalize recently - # used qubits (to encourage parallelism). - self.qubits_decay = QubitsDecay(len(dag.qubits)) - - # Start algorithm from the front layer and iterate until all gates done. - self.required_predecessors = self._build_required_predecessors(dag) - num_search_steps = 0 - front_layer = dag.front_layer() - - while front_layer: - execute_gate_list = [] - - # Remove as many immediately applicable gates as possible - new_front_layer = [] - for node in front_layer: - if len(node.qargs) == 2: - v0 = self._bit_indices[node.qargs[0]] - v1 = self._bit_indices[node.qargs[1]] - if self.coupling_map.graph.has_edge( - layout.logical_to_physical(v0), layout.logical_to_physical(v1) - ): - execute_gate_list.append(node) - else: - new_front_layer.append(node) - else: # Single-qubit gates as well as barriers are free - execute_gate_list.append(node) - front_layer = new_front_layer - - if not execute_gate_list and len(ops_since_progress) > max_iterations_without_progress: - # Backtrack to the last time we made progress, then greedily insert swaps to route - # the gate with the smallest distance between its arguments. This is a release - # valve for the algorithm to avoid infinite loops only, and should generally not - # come into play for most circuits. - self._undo_operations(ops_since_progress, mapped_dag, layout) - self._add_greedy_swaps(front_layer, mapped_dag, layout, canonical_register) - continue - - if execute_gate_list: - for node in execute_gate_list: - self._apply_gate(mapped_dag, node, layout, canonical_register) - for successor in self._successors(node, dag): - self.required_predecessors[successor] -= 1 - if self._is_resolved(successor): - front_layer.append(successor) - - if node.qargs: - self.qubits_decay.reset() - - # Diagnostics - if do_expensive_logging: - logger.debug( - "free! %s", - [ - (n.name if isinstance(n, DAGOpNode) else None, n.qargs) - for n in execute_gate_list - ], - ) - logger.debug( - "front_layer: %s", - [ - (n.name if isinstance(n, DAGOpNode) else None, n.qargs) - for n in front_layer - ], - ) - - ops_since_progress = [] - extended_set = None - continue - - # After all free gates are exhausted, heuristically find - # the best swap and insert it. When two or more swaps tie - # for best score, pick one randomly. - - if extended_set is None: - extended_set = self._obtain_extended_set(dag, front_layer) - extended_set_list = EdgeList(len(extended_set)) - for x in extended_set: - extended_set_list.append( - self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] - ) - - front_layer_list = EdgeList(len(front_layer)) - for x in front_layer: - front_layer_list.append( - self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] + original_layout = layout.copy() + + dag_list = [] + for node in dag.topological_op_nodes(): + dag_list.append( + ( + node._node_id, + [self._qubit_indices[x] for x in node.qargs], + [self._clbit_indices[x] for x in node.cargs], ) - best_swap = sabre_score_heuristic( - front_layer_list, - layout, - self._neighbor_table, - extended_set_list, - self.dist_matrix, - self.qubits_decay, - self.heuristic, - rng, ) - best_swap_qargs = [canonical_register[best_swap[0]], canonical_register[best_swap[1]]] - swap_node = self._apply_gate( - mapped_dag, - DAGOpNode(op=SwapGate(), qargs=best_swap_qargs), - layout, - canonical_register, - ) - layout.swap_logical(*best_swap) - ops_since_progress.append(swap_node) - - num_search_steps += 1 - if num_search_steps % DECAY_RESET_INTERVAL == 0: - self.qubits_decay.reset() - else: - self.qubits_decay[best_swap[0]] += DECAY_RATE - self.qubits_decay[best_swap[1]] += DECAY_RATE - - # Diagnostics - if do_expensive_logging: - logger.debug("SWAP Selection...") - logger.debug("extended_set: %s", [(n.name, n.qargs) for n in extended_set]) - logger.debug("best swap: %s", best_swap) - logger.debug("qubits decay: %s", self.qubits_decay) + front_layer = np.asarray([x._node_id for x in dag.front_layer()], dtype=np.uintp) + sabre_dag = SabreDAG(len(dag.qubits), len(dag.clbits), dag_list, front_layer) + swap_map, gate_order = build_swap_map( + len(dag.qubits), + sabre_dag, + self._neighbor_table, + self.dist_matrix, + self.heuristic, + self.seed, + layout, + ) + layout_mapping = layout.layout_mapping() output_layout = Layout({dag.qubits[k]: v for (k, v) in layout_mapping}) self.property_set["final_layout"] = output_layout 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( + mapped_dag, + DAGOpNode(op=SwapGate(), qargs=swap_qargs), + current_layout, + canonical_register, + ) + 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 _build_required_predecessors(self, dag): - out = defaultdict(int) - # We don't need to count in- or out-wires: outs can never be predecessors, and all input - # wires are automatically satisfied at the start. - for node in dag.op_nodes(): - for successor in self._successors(node, dag): - out[successor] += 1 - return out - - def _successors(self, node, dag): - """Return an iterable of the successors along each wire from the given node. - - This yields the same successor multiple times if there are parallel wires (e.g. two adjacent - operations that have one clbit and qubit in common), which is important in the swapping - algorithm for detecting if each wire has been accounted for.""" - for _, successor, _ in dag.edges(node): - if isinstance(successor, DAGOpNode): - yield successor - - def _is_resolved(self, node): - """Return True if all of a node's predecessors in dag are applied.""" - return self.required_predecessors[node] == 0 - - def _obtain_extended_set(self, dag, front_layer): - """Populate extended_set by looking ahead a fixed number of gates. - For each existing element add a successor until reaching limit. - """ - extended_set = [] - decremented = [] - tmp_front_layer = front_layer - done = False - while tmp_front_layer and not done: - new_tmp_front_layer = [] - for node in tmp_front_layer: - for successor in self._successors(node, dag): - decremented.append(successor) - self.required_predecessors[successor] -= 1 - if self._is_resolved(successor): - new_tmp_front_layer.append(successor) - if len(successor.qargs) == 2: - extended_set.append(successor) - if len(extended_set) >= EXTENDED_SET_SIZE: - done = True - break - tmp_front_layer = new_tmp_front_layer - for node in decremented: - self.required_predecessors[node] += 1 - return extended_set - - def _add_greedy_swaps(self, front_layer, dag, layout, qubits): - """Mutate ``dag`` and ``layout`` by applying greedy swaps to ensure that at least one gate - can be routed.""" - target_node = min( - front_layer, - key=lambda node: self.dist_matrix[ - layout.logical_to_physical(self._bit_indices[node.qargs[0]]), - layout.logical_to_physical(self._bit_indices[node.qargs[1]]), - ], - ) - for pair in _shortest_swap_path( - tuple(target_node.qargs), self.coupling_map, layout, qubits - ): - self._apply_gate(dag, DAGOpNode(op=SwapGate(), qargs=pair), layout, qubits) - layout.swap_logical(*[self._bit_indices[x] for x in pair]) - - def _undo_operations(self, operations, dag, layout): - """Mutate ``dag`` and ``layout`` by undoing the swap gates listed in ``operations``.""" - if dag is None: - for operation in reversed(operations): - layout.swap_logical(*[self._bit_indices[x] for x in operation.qargs]) - else: - for operation in reversed(operations): - dag.remove_op_node(operation) - p0 = self._bit_indices[operation.qargs[0]] - p1 = self._bit_indices[operation.qargs[1]] - layout.swap_logical(p0, p1) - 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._bit_indices[x])] for x in op_node.qargs + device_qreg[layout.logical_to_physical(self._qubit_indices[x])] for x in op_node.qargs ) return mapped_op_node - - -def _shortest_swap_path(target_qubits, coupling_map, layout, qreg): - """Return an iterator that yields the swaps between virtual qubits needed to bring the two - virtual qubits in ``target_qubits`` together in the coupling map.""" - v_start, v_goal = target_qubits - start, goal = layout.logical_to_physical(qreg.index(v_start)), layout.logical_to_physical( - qreg.index(v_goal) - ) - # TODO: remove the list call once using retworkx 0.12, as the return value can be sliced. - path = list(retworkx.dijkstra_shortest_paths(coupling_map.graph, start, target=goal)[goal]) - # Swap both qubits towards the "centre" (as opposed to applying the same swaps to one) to - # parallelise and reduce depth. - split = len(path) // 2 - forwards, backwards = path[1:split], reversed(path[split:-1]) - for swap in forwards: - yield v_start, qreg[layout.physical_to_logical(swap)] - for swap in backwards: - yield v_goal, qreg[layout.physical_to_logical(swap)] diff --git a/src/nlayout.rs b/src/nlayout.rs index e4ca1223b33d..2d4ff5a29880 100644 --- a/src/nlayout.rs +++ b/src/nlayout.rs @@ -112,4 +112,8 @@ impl NLayout { pub fn swap_physical(&mut self, bit_a: usize, bit_b: usize) { self.swap(bit_a, bit_b) } + + pub fn copy(&self) -> NLayout { + self.clone() + } } diff --git a/src/sabre_swap/edge_list.rs b/src/sabre_swap/edge_list.rs deleted file mode 100644 index a1dbf0fb55e7..000000000000 --- a/src/sabre_swap/edge_list.rs +++ /dev/null @@ -1,101 +0,0 @@ -// 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. - -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; - -/// A simple container that contains a vector representing edges in the -/// coupling map that are found to be optimal by the swap mapper. -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] -#[derive(Clone, Debug)] -pub struct EdgeList { - pub edges: Vec<[usize; 2]>, -} - -impl Default for EdgeList { - fn default() -> Self { - Self::new(None) - } -} - -#[pymethods] -impl EdgeList { - #[new] - pub fn new(capacity: Option) -> Self { - match capacity { - Some(size) => EdgeList { - edges: Vec::with_capacity(size), - }, - None => EdgeList { edges: Vec::new() }, - } - } - - /// Append an edge to the list. - /// - /// Args: - /// edge_start (int): The start qubit of the edge. - /// edge_end (int): The end qubit of the edge. - #[pyo3(text_signature = "(self, edge_start, edge_end, /)")] - pub fn append(&mut self, edge_start: usize, edge_end: usize) { - self.edges.push([edge_start, edge_end]); - } - - pub fn __iter__(slf: PyRef) -> PyResult> { - let iter = EdgeListIter { - inner: slf.edges.clone().into_iter(), - }; - Py::new(slf.py(), iter) - } - - pub fn __len__(&self) -> usize { - self.edges.len() - } - - pub fn __contains__(&self, object: [usize; 2]) -> bool { - self.edges.contains(&object) - } - - pub fn __getitem__(&self, object: usize) -> PyResult<[usize; 2]> { - if object >= self.edges.len() { - return Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))); - } - Ok(self.edges[object]) - } - - fn __getstate__(&self) -> Vec<[usize; 2]> { - self.edges.clone() - } - - fn __setstate__(&mut self, state: Vec<[usize; 2]>) { - self.edges = state - } -} - -#[pyclass] -pub struct EdgeListIter { - inner: std::vec::IntoIter<[usize; 2]>, -} - -#[pymethods] -impl EdgeListIter { - fn __iter__(slf: PyRef) -> PyRef { - slf - } - - fn __next__(mut slf: PyRefMut) -> Option<[usize; 2]> { - slf.inner.next() - } -} diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index 73323cd446d4..a2301c56e31f 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -12,29 +12,38 @@ #![allow(clippy::too_many_arguments)] -pub mod edge_list; pub mod neighbor_table; -pub mod qubits_decay; -pub mod sabre_rng; +pub mod sabre_dag; +pub mod swap_map; +use std::cmp::Ordering; + +use hashbrown::{HashMap, HashSet}; use ndarray::prelude::*; +use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; use pyo3::wrap_pyfunction; use pyo3::Python; - -use hashbrown::HashSet; use rand::prelude::SliceRandom; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; use rayon::prelude::*; +use retworkx_core::dictmap::*; +use retworkx_core::petgraph::prelude::*; +use retworkx_core::petgraph::visit::EdgeRef; +use retworkx_core::shortest_path::dijkstra; use crate::getenv_use_multiple_threads; use crate::nlayout::NLayout; -use edge_list::EdgeList; use neighbor_table::NeighborTable; -use qubits_decay::QubitsDecay; -use sabre_rng::SabreRng; +use sabre_dag::SabreDAG; +use swap_map::SwapMap; +const EXTENDED_SET_SIZE: usize = 20; // Size of lookahead window. +const DECAY_RATE: f64 = 0.001; // Decay coefficient for penalizing serial swaps. +const DECAY_RESET_INTERVAL: u8 = 5; // How often to reset all decay rates to 1. const EXTENDED_SET_WEIGHT: f64 = 0.5; // Weight of lookahead window compared to front_layer. #[pyclass] @@ -53,16 +62,15 @@ pub enum Heuristic { /// /// Candidate swaps are sorted so SWAP(i,j) and SWAP(j,i) are not duplicated. fn obtain_swaps( - front_layer: &EdgeList, + front_layer: &[[usize; 2]], neighbors: &NeighborTable, layout: &NLayout, ) -> HashSet<[usize; 2]> { // This will likely under allocate as it's a function of the number of // neighbors for the qubits in the layer too, but this is basically a // minimum allocation assuming each qubit has only 1 unique neighbor - let mut candidate_swaps: HashSet<[usize; 2]> = - HashSet::with_capacity(2 * front_layer.edges.len()); - for node in &front_layer.edges { + let mut candidate_swaps: HashSet<[usize; 2]> = HashSet::with_capacity(2 * front_layer.len()); + for node in front_layer { for v in node { let physical = layout.logic_to_phys[*v]; for neighbor in &neighbors.neighbors[physical] { @@ -79,49 +87,274 @@ fn obtain_swaps( candidate_swaps } -/// Run the sabre heuristic scoring +fn obtain_extended_set( + dag: &SabreDAG, + front_layer: &[NodeIndex], + required_predecessors: &mut [u32], +) -> Vec<[usize; 2]> { + let mut extended_set: Vec<[usize; 2]> = Vec::new(); + let mut decremented: Vec = Vec::new(); + let mut tmp_front_layer: Vec = front_layer.to_vec(); + let mut done: bool = false; + while !tmp_front_layer.is_empty() && !done { + let mut new_tmp_front_layer = Vec::new(); + for node in tmp_front_layer { + for edge in dag.dag.edges(node) { + let successor_index = edge.target(); + let successor = successor_index.index(); + decremented.push(successor); + required_predecessors[successor] -= 1; + if required_predecessors[successor] == 0 { + new_tmp_front_layer.push(successor_index); + let node_weight = dag.dag.node_weight(successor_index).unwrap(); + let qargs = &node_weight.1; + if qargs.len() == 2 { + let extended_set_edges: [usize; 2] = [qargs[0], qargs[1]]; + extended_set.push(extended_set_edges); + } + } + } + if extended_set.len() >= EXTENDED_SET_SIZE { + done = true; + break; + } + } + tmp_front_layer = new_tmp_front_layer; + } + for node in decremented { + required_predecessors[node] += 1; + } + extended_set +} + +fn cmap_from_neighor_table(neighbor_table: &NeighborTable) -> DiGraph<(), ()> { + DiGraph::<(), ()>::from_edges(neighbor_table.neighbors.iter().enumerate().flat_map( + |(u, targets)| { + targets + .iter() + .map(move |v| (NodeIndex::new(u), NodeIndex::new(*v))) + }, + )) +} + +/// Run sabre swap on a circuit /// -/// Args: -/// layers (EdgeList): The input layer edge list to score and find the -/// best swaps -/// layout (NLayout): The current layout -/// neighbor_table (NeighborTable): The table of neighbors for each node -/// in the coupling graph -/// extended_set (EdgeList): The extended set -/// distance_matrix (ndarray): The 2D array distance matrix for the coupling -/// graph -/// qubits_decay (QubitsDecay): The current qubit decay factors for -/// heuristic (Heuristic): The chosen heuristic method to use /// Returns: -/// ndarray: A 2d array of the best swap candidates all with the minimum score +/// (SwapMap, gate_order): A tuple where the first element is a mapping of +/// DAGCircuit node ids to a list of virtual qubit swaps that should be +/// added before that operation. The second element is a numpy array of +/// node ids that represents the traversal order used by sabre. #[pyfunction] +pub fn build_swap_map( + py: Python, + num_qubits: usize, + dag: &SabreDAG, + neighbor_table: &NeighborTable, + distance_matrix: PyReadonlyArray2, + heuristic: &Heuristic, + seed: u64, + layout: &mut NLayout, +) -> PyResult<(SwapMap, PyObject)> { + let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); + 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 max_iterations_without_progress = 10 * neighbor_table.neighbors.len(); + let mut ops_since_progress: Vec<[usize; 2]> = Vec::new(); + 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); + + for node in dag.dag.node_indices() { + for edge in dag.dag.edges(node) { + required_predecessors[edge.target().index()] += 1; + } + } + while !front_layer.is_empty() { + let mut execute_gate_list: Vec = Vec::new(); + // Remove as many immediately applicable gates as possible + let mut new_front_layer: Vec = Vec::new(); + for node in front_layer { + let node_weight = dag.dag.node_weight(node).unwrap(); + let qargs = &node_weight.1; + if qargs.len() == 2 { + let physical_qargs: [usize; 2] = [ + layout.logic_to_phys[qargs[0]], + layout.logic_to_phys[qargs[1]], + ]; + if coupling_graph + .find_edge( + NodeIndex::new(physical_qargs[0]), + NodeIndex::new(physical_qargs[1]), + ) + .is_none() + { + new_front_layer.push(node); + } else { + execute_gate_list.push(node); + } + } else { + execute_gate_list.push(node); + } + } + front_layer = new_front_layer.clone(); + + // Backtrack to the last time we made progress, then greedily insert swaps to route + // the gate with the smallest distance between its arguments. This is f block a release + // valve for the algorithm to avoid infinite loops only, and should generally not + // come into play for most circuits. + if execute_gate_list.is_empty() + && ops_since_progress.len() > max_iterations_without_progress + { + // If we're stuck in a loop without making progress first undo swaps: + ops_since_progress + .drain(..) + .rev() + .for_each(|swap| layout.swap_logical(swap[0], swap[1])); + // Then pick the closest pair in the current layer + let target_qubits = front_layer + .iter() + .map(|n| { + let node_weight = dag.dag.node_weight(*n).unwrap(); + let qargs = &node_weight.1; + [qargs[0], qargs[1]] + }) + .min_by(|qargs_a, qargs_b| { + let dist_a = dist[[ + layout.logic_to_phys[qargs_a[0]], + layout.logic_to_phys[qargs_a[1]], + ]]; + let dist_b = dist[[ + layout.logic_to_phys[qargs_b[0]], + layout.logic_to_phys[qargs_b[1]], + ]]; + dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal) + }) + .unwrap(); + // find Shortest path between target qubits + let mut shortest_paths: DictMap> = DictMap::new(); + let u = layout.logic_to_phys[target_qubits[0]]; + let v = layout.logic_to_phys[target_qubits[1]]; + (dijkstra( + &coupling_graph, + NodeIndex::::new(u), + Some(NodeIndex::::new(v)), + |_| Ok(1.), + Some(&mut shortest_paths), + ) as PyResult>>)?; + let shortest_path: Vec = shortest_paths + .get(&NodeIndex::new(v)) + .unwrap() + .iter() + .map(|n| n.index()) + .collect(); + // Insert greedy swaps along that shortest path + let split: usize = shortest_path.len() / 2; + let forwards = &shortest_path[1..split]; + let backwards = &shortest_path[split..shortest_path.len() - 1]; + let mut greedy_swaps: Vec<[usize; 2]> = Vec::with_capacity(split); + for swap in forwards { + let logical_swap_bit = layout.phys_to_logic[*swap]; + greedy_swaps.push([target_qubits[0], logical_swap_bit]); + layout.swap_logical(target_qubits[0], logical_swap_bit); + } + backwards.iter().rev().for_each(|swap| { + let logical_swap_bit = layout.phys_to_logic[*swap]; + greedy_swaps.push([target_qubits[1], logical_swap_bit]); + layout.swap_logical(target_qubits[1], logical_swap_bit); + }); + ops_since_progress = greedy_swaps; + continue; + } + if !execute_gate_list.is_empty() { + for node in execute_gate_list { + let node_weight = dag.dag.node_weight(node).unwrap(); + gate_order.push(node_weight.0); + let out_swaps: Vec<[usize; 2]> = ops_since_progress.drain(..).collect(); + if !out_swaps.is_empty() { + out_map.insert(dag.dag.node_weight(node).unwrap().0, out_swaps); + } + for edge in dag.dag.edges(node) { + let successor = edge.target().index(); + required_predecessors[successor] -= 1; + if required_predecessors[successor] == 0 { + front_layer.push(edge.target()); + } + } + } + qubits_decay.fill_with(|| 1.); + extended_set = None; + continue; + } + let first_layer: Vec<[usize; 2]> = front_layer + .iter() + .map(|n| { + let node_weight = dag.dag.node_weight(*n).unwrap(); + let qargs = &node_weight.1; + [qargs[0], qargs[1]] + }) + .collect(); + if extended_set.is_none() { + extended_set = Some(obtain_extended_set( + dag, + &front_layer, + &mut required_predecessors, + )); + } + + let best_swap = sabre_score_heuristic( + &first_layer, + layout, + neighbor_table, + extended_set.as_ref().unwrap(), + &dist, + &qubits_decay, + heuristic, + &mut rng, + run_in_parallel, + ); + num_search_steps += 1; + if num_search_steps % DECAY_RESET_INTERVAL == 0 { + qubits_decay.fill_with(|| 1.); + } else { + qubits_decay[best_swap[0]] += DECAY_RATE; + qubits_decay[best_swap[1]] += DECAY_RATE; + } + ops_since_progress.push(best_swap); + } + Ok((SwapMap { map: out_map }, gate_order.into_pyarray(py).into())) +} + pub fn sabre_score_heuristic( - layer: EdgeList, + layer: &[[usize; 2]], layout: &mut NLayout, neighbor_table: &NeighborTable, - extended_set: EdgeList, - distance_matrix: PyReadonlyArray2, - qubits_decay: QubitsDecay, + extended_set: &[[usize; 2]], + dist: &ArrayView2, + qubits_decay: &[f64], heuristic: &Heuristic, - rng: &mut SabreRng, + 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. - let run_in_parallel = getenv_use_multiple_threads(); - let dist = distance_matrix.as_array(); - let candidate_swaps = obtain_swaps(&layer, neighbor_table, layout); + let candidate_swaps = obtain_swaps(layer, neighbor_table, layout); let mut min_score = f64::MAX; let mut best_swaps: Vec<[usize; 2]> = Vec::new(); for swap_qubits in candidate_swaps { layout.swap_logical(swap_qubits[0], swap_qubits[1]); let score = score_heuristic( heuristic, - &layer.edges, - &extended_set.edges, + layer, + extended_set, layout, &swap_qubits, - &dist, - &qubits_decay.decay, + dist, + qubits_decay, ); if score < min_score { min_score = score; @@ -137,7 +370,9 @@ pub fn sabre_score_heuristic( } else { best_swaps.sort_unstable(); } - *best_swaps.choose(&mut rng.rng).unwrap() + let best_swap = *best_swaps.choose(rng).unwrap(); + layout.swap_logical(best_swap[0], best_swap[1]); + best_swap } #[inline] @@ -196,11 +431,10 @@ fn score_heuristic( #[pymodule] pub fn sabre_swap(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(sabre_score_heuristic))?; + m.add_wrapped(wrap_pyfunction!(build_swap_map))?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/sabre_swap/qubits_decay.rs b/src/sabre_swap/qubits_decay.rs deleted file mode 100644 index 0a5899af1bc5..000000000000 --- a/src/sabre_swap/qubits_decay.rs +++ /dev/null @@ -1,85 +0,0 @@ -// 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. - -use numpy::IntoPyArray; -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; -use pyo3::Python; - -/// A container for qubit decay values for each qubit -/// -/// This class tracks the qubit decay for the sabre heuristic. When initialized -/// all qubits are set to a value of ``1.``. This class implements the sequence -/// protocol and can be modified in place like any python sequence. -/// -/// Args: -/// qubit_count (int): The number of qubits -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(qubit_indices, logical_qubits, physical_qubits, /)")] -#[derive(Clone, Debug)] -pub struct QubitsDecay { - pub decay: Vec, -} - -#[pymethods] -impl QubitsDecay { - #[new] - pub fn new(qubit_count: usize) -> Self { - QubitsDecay { - decay: vec![1.; qubit_count], - } - } - - // Mapping Protocol - pub fn __len__(&self) -> usize { - self.decay.len() - } - - pub fn __contains__(&self, object: f64) -> bool { - self.decay.contains(&object) - } - - pub fn __getitem__(&self, object: usize) -> PyResult { - match self.decay.get(object) { - Some(val) => Ok(*val), - None => Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))), - } - } - - pub fn __setitem__(mut slf: PyRefMut, object: usize, value: f64) -> PyResult<()> { - if object >= slf.decay.len() { - return Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))); - } - slf.decay[object] = value; - Ok(()) - } - - pub fn __array__(&self, py: Python) -> PyObject { - self.decay.clone().into_pyarray(py).into() - } - - pub fn __str__(&self) -> PyResult { - Ok(format!("{:?}", self.decay)) - } - - /// Reset decay for all qubits back to default ``1.`` - #[pyo3(text_signature = "(self, /)")] - pub fn reset(mut slf: PyRefMut) { - slf.decay.fill_with(|| 1.); - } -} diff --git a/src/sabre_swap/sabre_dag.rs b/src/sabre_swap/sabre_dag.rs new file mode 100644 index 000000000000..bb60b990b2f9 --- /dev/null +++ b/src/sabre_swap/sabre_dag.rs @@ -0,0 +1,69 @@ +// 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. + +use hashbrown::HashMap; +use numpy::PyReadonlyArray1; +use pyo3::prelude::*; +use retworkx_core::petgraph::prelude::*; + +/// A DAG object used to represent the data interactions from a DAGCircuit +/// to run the the sabre algorithm. This is structurally identical to the input +/// DAGCircuit, but the contents of the node are a tuple of DAGCircuit node ids, +/// a list of qargs and a list of cargs +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(num_qubits, num_clbits, nodes, front_layer, /)")] +#[derive(Clone, Debug)] +pub struct SabreDAG { + pub dag: DiGraph<(usize, Vec, Vec), ()>, + pub first_layer: Vec, +} + +#[pymethods] +impl SabreDAG { + #[new] + pub fn new( + num_qubits: usize, + num_clbits: usize, + nodes: Vec<(usize, Vec, Vec)>, + front_layer: PyReadonlyArray1, + ) -> PyResult { + let mut qubit_pos: Vec = vec![usize::MAX; num_qubits]; + let mut clbit_pos: Vec = vec![usize::MAX; num_clbits]; + let mut reverse_index_map: HashMap = HashMap::with_capacity(nodes.len()); + let mut dag: DiGraph<(usize, Vec, Vec), ()> = + Graph::with_capacity(nodes.len(), 2 * nodes.len()); + for node in &nodes { + let qargs = &node.1; + let cargs = &node.2; + let gate_index = dag.add_node(node.clone()); + reverse_index_map.insert(node.0, gate_index); + for x in qargs { + if qubit_pos[*x] != usize::MAX { + dag.add_edge(NodeIndex::new(qubit_pos[*x]), gate_index, ()); + } + qubit_pos[*x] = gate_index.index(); + } + for x in cargs { + if clbit_pos[*x] != usize::MAX { + dag.add_edge(NodeIndex::new(qubit_pos[*x]), gate_index, ()); + } + clbit_pos[*x] = gate_index.index(); + } + } + let first_layer = front_layer + .as_slice()? + .iter() + .map(|x| reverse_index_map[x]) + .collect(); + Ok(SabreDAG { dag, first_layer }) + } +} diff --git a/src/sabre_swap/sabre_rng.rs b/src/sabre_swap/sabre_rng.rs deleted file mode 100644 index 79a4a70acb13..000000000000 --- a/src/sabre_swap/sabre_rng.rs +++ /dev/null @@ -1,35 +0,0 @@ -// 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. - -use pyo3::prelude::*; -use rand::prelude::*; -use rand_pcg::Pcg64Mcg; - -/// A rng container that shares an rng state between python and sabre's rust -/// code. It should be initialized once and passed to -/// ``sabre_score_heuristic`` to avoid recreating a rng on the inner loop -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] -#[derive(Clone, Debug)] -pub struct SabreRng { - pub rng: Pcg64Mcg, -} - -#[pymethods] -impl SabreRng { - #[new] - pub fn new(seed: u64) -> Self { - SabreRng { - rng: Pcg64Mcg::seed_from_u64(seed), - } - } -} diff --git a/src/sabre_swap/swap_map.rs b/src/sabre_swap/swap_map.rs new file mode 100644 index 000000000000..b14d9c72ecdc --- /dev/null +++ b/src/sabre_swap/swap_map.rs @@ -0,0 +1,48 @@ +// 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. + +use hashbrown::HashMap; +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; + +/// A container for required swaps before a gate qubit +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[derive(Clone, Debug)] +pub struct SwapMap { + pub map: HashMap>, +} + +#[pymethods] +impl SwapMap { + // Mapping Protocol + pub fn __len__(&self) -> usize { + self.map.len() + } + + pub fn __contains__(&self, object: usize) -> bool { + self.map.contains_key(&object) + } + + pub fn __getitem__(&self, object: usize) -> PyResult> { + match self.map.get(&object) { + Some(val) => Ok(val.clone()), + None => Err(PyIndexError::new_err(format!( + "Node index {} not in swap mapping", + object + ))), + } + } + + pub fn __str__(&self) -> PyResult { + Ok(format!("{:?}", self.map)) + } +} diff --git a/tox.ini b/tox.ini index 0561b10732a7..d2bf7ad9f7a5 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 SETUPTOOLS_ENABLE_FEATURES +passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL RUST_BACKTRACE SETUPTOOLS_ENABLE_FEATURES deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt commands =