From 8ce6e0a2566c4c045d8053687503b87643b46dae Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 22 Aug 2022 11:46:23 -0400 Subject: [PATCH] Further oxidize sabre (#8388) * Further oxidize sabre In #7977 we started the process of oxidizing SabreSwap by replacing the inner-most scoring heuristic loop with a rust routine. This greatly improved the overall performance and scaling of the transpiler pass. Continuing from where that started this commit migrates more of the pass into the Rust domain so that almost all the pass's operations are done inside a rust module and all that is returned is a list of swaps to run prior to each 2q gate. This should further improve the runtime performance of the pass and scaling because the only steps performed in Python are generating the input data structures and then replaying the circuit with SWAPs inserted at the appropriate points. While we could have stuck with #7977 as the performance of the pass was more than sufficient after it. What this commit really enables by moving most of the pass to the rust domain is to expand with improvments and expansion of the sabre algorithm which will require multithreaded to be efficiently implemented. So while this will have some modest performance improvements this is more about setting the stage for introducing variants of SabreSwap that do more thorough analysis in the future (which were previously preculded by the parallelism limitations of python). * Fix most test failures This commit fixes a small typo/logic error in the algorithm implementation that was preventing sabre from making forward progress because it wasn't correctly identifying successors for the next layer. By fixing this all the hard errors in the SabreSwap tests are fixed. The only failures left seem to be related to a different layout which hopefully is not a correctness issue but just caused by different ordering. * Rework circuit reconstruction to use layer order In some tests there were subtle differences in the relative positioning of the 1q gates relative to inserted swaps (i.e. a 1q gate which was before the swap previously could move to after it). This was caused by different topological ordering being used between the hybrid python sabre implementation and the mostly rust sabre implementations. To ensure a consistent ordering fater moving mostly to rust this changes the swap insertion loop to iterate over the circuit layers which mirrors how the old sabre implementation worked. * Differentiate between empty extended_set and none * Simplify arguments passing to remove adjacency matrix storage * Only check env variables once in rust * Rust side NLayout.copy() * Preserve SabreSwap execution order This commit fixes an issue where in some cases the topological order the DAGCircuit is traversed is different from the topological order that sabre uses internally. The build_swap_map sabre swap function is only valid if the 2q gates are replayed in the same exact order when rebuilding the DAGCircuit. If a 2q gate gets replayed in a different order the layout mapping will cause the circuit to diverge and potentially be invalid. This commit updates the replay logic in the python side to detect when the topological order over the dagcircuit differs from the sabre traversal order and attempts to correct it. * Rework SabreDAG to include full DAGCircuit structure Previously we attempted to just have the rust component of sabre deal solely with the 2q component of the input circuit. However, while this works for ~80% of the cases it fails to account ordering and interactions between non-2q gates or instructions with classical bits. To address this the sabre dag structure is modified to contain all isntructions in the input circuit and structurally match the DAGCircuit's edges. This fixes most of the issues related to gate ordering the previous implementation was encountering. It also simplifies the swap insertion/replay of the circuit in the python side as we now get an exact application order from the rust code. * Switch back to topological_op_nodes() for SabreDAG creation * Fix lint * Fix extended set construction * Fix typo in application of decay rate * Remove unused QubitsDecay class * Remove unused EdgeList class * Remove unnecessary SabreRNG class * Cleanup SabreDAG docstring and comments * Remove unused edge weights from SabreDAG The edge weights in the SabreDAG struct were set to the qubit indices from the input DAGCircuit because the edges represent the flow of data on the qubit. However, we never actually inspect the edge weights and all having them present does is use extra memory. This commit changes SabreDAG to just not set any weight for edges as all we need is the source and target nodes for the algorithm to work. * s/_bit_indices/_qubit_indices/g * Fix sabre rust class signatures --- Cargo.lock | 106 ++++-- Cargo.toml | 1 + .../transpiler/passes/routing/sabre_swap.py | 285 +++------------- src/nlayout.rs | 4 + src/sabre_swap/edge_list.rs | 101 ------ src/sabre_swap/mod.rs | 318 +++++++++++++++--- src/sabre_swap/qubits_decay.rs | 85 ----- src/sabre_swap/sabre_dag.rs | 69 ++++ src/sabre_swap/sabre_rng.rs | 35 -- src/sabre_swap/swap_map.rs | 48 +++ tox.ini | 2 +- 11 files changed, 517 insertions(+), 537 deletions(-) delete mode 100644 src/sabre_swap/edge_list.rs delete mode 100644 src/sabre_swap/qubits_decay.rs create mode 100644 src/sabre_swap/sabre_dag.rs delete mode 100644 src/sabre_swap/sabre_rng.rs create mode 100644 src/sabre_swap/swap_map.rs 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 =