From 9a60a7424e4ca6b877a4786b97fd0ebc0454dc89 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 16 Nov 2022 16:59:46 -0500 Subject: [PATCH 1/3] Improve efficiency of vf2 pass search with free nodes This commit improves the efficiency of the VF2Layout and VF2PostLayout pass when there are qubits that only contain single qubit operations. Previously these passes would model these qubits as free nodes in the interaction graph. This would cause an explosion in the number of possible isomorphic mappings because vf2_mapping() will return a potential subgraph for every permutation of free nodes on the coupling graph. This means we end up spending a potentially huge amount of time scoring these permutations when we can more effectively place these free nodes as part of a dedicated step after placing the subgraph. This is only true for strict_direction=False because for strict_direction=True with a Target we need to rely on the subgraph isomorphism to ensure we're fully evaluating subgraphs comply with local operation availablility. When there are free nodes in an interaction graph this changes the interaction graph to strip those out of the graph before checking for isomorphic subgraphs. This greatly reduces the search space and should speed up that iterative scoring. After we've selected the best subgraph of nodes with 2q interactions before returning the final layout we evaluate the free nodes and pick an available qubit with lowest 1q error for each free node. --- qiskit/transpiler/passes/layout/vf2_layout.py | 9 +++- .../passes/layout/vf2_post_layout.py | 9 +++- qiskit/transpiler/passes/layout/vf2_utils.py | 43 ++++++++++++++++--- test/python/transpiler/test_vf2_layout.py | 6 ++- .../python/transpiler/test_vf2_post_layout.py | 18 -------- 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index b20ca0cf9a8a..bbc4bf492e8e 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -115,7 +115,7 @@ def run(self, dag): if result is None: self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.MORE_THAN_2Q return - im_graph, im_graph_node_map, reverse_im_graph_node_map = result + im_graph, im_graph_node_map, reverse_im_graph_node_map, free_nodes = result cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph( self.coupling_map, self.seed, self.strict_direction ) @@ -201,6 +201,13 @@ def run(self, dag): if chosen_layout is None: stop_reason = VF2LayoutStopReason.NO_SOLUTION_FOUND else: + chosen_layout = vf2_utils.map_free_qubits( + free_nodes, + chosen_layout, + cm_graph.num_nodes(), + reverse_im_graph_node_map, + self.avg_error_map, + ) self.property_set["layout"] = chosen_layout for reg in dag.qregs.values(): self.property_set["layout"].add_register(reg) diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index fb346d6e5639..0b04ed4cd174 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -138,7 +138,7 @@ def run(self, dag): if result is None: self.property_set["VF2PostLayout_stop_reason"] = VF2PostLayoutStopReason.MORE_THAN_2Q return - im_graph, im_graph_node_map, reverse_im_graph_node_map = result + im_graph, im_graph_node_map, reverse_im_graph_node_map, free_nodes = result if self.target is not None: # If qargs is None then target is global and ideal so no @@ -282,6 +282,13 @@ def run(self, dag): if chosen_layout is None: stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND else: + chosen_layout = vf2_utils.map_free_qubits( + free_nodes, + chosen_layout, + cm_graph.num_nodes(), + reverse_im_graph_node_map, + self.avg_error_map, + ) existing_layout = self.property_set["layout"] # If any ancillas in initial layout map them back to the final layout output if existing_layout is not None and len(existing_layout) > len(chosen_layout): diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 950b2447d936..9478b81e8ee9 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -16,7 +16,7 @@ import statistics import random -from retworkx import PyDiGraph, PyGraph +from retworkx import PyDiGraph, PyGraph, connected_components from qiskit.circuit import ControlFlowOp, ForLoopOp from qiskit.converters import circuit_to_dag @@ -75,18 +75,32 @@ def _visit(dag, weight, wire_map): _visit(dag, 1, {bit: bit for bit in dag.qubits}) except MultiQEncountered: return None - return im_graph, im_graph_node_map, reverse_im_graph_node_map + # Remove components with no 2q interactions from interaction graph + # these will be evaluated separately independently of scoring isomorphic + # mappings. This is not done for strict direction because for post layout + # we need to factor in local operation constraints when evaluating a graph + free_nodes = {} + if not strict_direction: + conn_comp = connected_components(im_graph) + for comp in conn_comp: + if len(comp) == 1: + index = comp.pop() + free_nodes[index] = im_graph[index] + im_graph.remove_node(index) + + return im_graph, im_graph_node_map, reverse_im_graph_node_map, free_nodes def score_layout(avg_error_map, layout, bit_map, reverse_bit_map, im_graph, strict_direction=False): """Score a layout given an average error map.""" bits = layout.get_virtual_bits() fidelity = 1 - for bit, node_index in bit_map.items(): - gate_count = sum(im_graph[node_index].values()) - error_rate = avg_error_map.get((bits[bit],)) - if error_rate is not None: - fidelity *= (1 - avg_error_map[(bits[bit],)]) ** gate_count + if strict_direction: + for bit, node_index in bit_map.items(): + gate_count = sum(im_graph[node_index].values()) + error_rate = avg_error_map.get((bits[bit],)) + if error_rate is not None: + fidelity *= (1 - avg_error_map[(bits[bit],)]) ** gate_count for edge in im_graph.edge_index_map().values(): gate_count = sum(edge[2].values()) qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]]) @@ -150,3 +164,18 @@ def shuffle_coupling_graph(coupling_map, seed, strict_direction=True): cm_nodes = [k for k, v in sorted(enumerate(cm_nodes), key=lambda item: item[1])] cm_graph = shuffled_cm_graph return cm_graph, cm_nodes + + +def map_free_qubits( + free_nodes, partial_layout, num_physical_qubits, reverse_bit_map, avg_error_map +): + """Add any free nodes to a layout.""" + if not free_nodes: + return partial_layout + qubits = set(range(num_physical_qubits)) + used_bits = set(partial_layout.get_physical_bits()) + for im_index in sorted(free_nodes, key=lambda x: sum(free_nodes[x].values())): + selected_qubit = min(qubits - used_bits, key=lambda bit: avg_error_map.get((bit,), 1.0)) + used_bits.add(selected_qubit) + partial_layout.add(reverse_bit_map[im_index], selected_qubit) + return partial_layout diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 15746e2d3684..8662ba59050c 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -543,6 +543,7 @@ def test_max_trials_exceeded(self): qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) + qc.cx(0, 1) qc.measure_all() cmap = CouplingMap(backend.configuration().coupling_map) properties = backend.properties() @@ -562,6 +563,7 @@ def test_time_limit_exceeded(self): qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) + qc.cx(0, 1) qc.measure_all() cmap = CouplingMap(backend.configuration().coupling_map) properties = backend.properties() @@ -583,7 +585,7 @@ def test_reasonable_limits_for_simple_layouts(self): """Test that the default trials is set to a reasonable number.""" backend = FakeManhattan() qc = QuantumCircuit(5) - qc.h(2) + qc.cx(2, 3) qc.cx(0, 1) cmap = CouplingMap(backend.configuration().coupling_map) properties = backend.properties() @@ -596,7 +598,7 @@ def test_reasonable_limits_for_simple_layouts(self): "DEBUG:qiskit.transpiler.passes.layout.vf2_layout:Trial 159 is >= configured max trials 159", cm.output, ) - self.assertEqual(set(property_set["layout"].get_physical_bits()), {49, 40, 58, 0, 1}) + self.assertEqual(set(property_set["layout"].get_physical_bits()), {49, 40, 33, 0, 34}) def test_no_limits_with_negative(self): """Test that we're not enforcing a trial limit if set to negative.""" diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index c89dbd8ca7fb..ee74c7f86e6d 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -385,24 +385,6 @@ def test_all_1q_score(self): score = vf2_pass._score_layout(layout, bit_map, reverse_bit_map, im_graph) self.assertAlmostEqual(0.002925, score, places=5) - def test_all_1q_avg_score(self): - """Test average scoring for all 1q input.""" - bit_map = {Qubit(): 0, Qubit(): 1} - reverse_bit_map = {v: k for k, v in bit_map.items()} - im_graph = retworkx.PyDiGraph() - im_graph.add_node({"sx": 1}) - im_graph.add_node({"sx": 1}) - backend = FakeYorktownV2() - vf2_pass = VF2PostLayout(target=backend.target) - vf2_pass.avg_error_map = vf2_utils.build_average_error_map( - vf2_pass.target, vf2_pass.properties, vf2_pass.coupling_map - ) - layout = Layout(bit_map) - score = vf2_utils.score_layout( - vf2_pass.avg_error_map, layout, bit_map, reverse_bit_map, im_graph - ) - self.assertAlmostEqual(0.02054, score, places=5) - class TestVF2PostLayoutUndirected(QiskitTestCase): """Tests the VF2Layout pass""" From 5f7bde3860dc99453e68484fbd77550b71dbefa4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 17 Nov 2022 05:30:50 -0500 Subject: [PATCH 2/3] Optimize performance of free node selection --- qiskit/transpiler/passes/layout/vf2_utils.py | 9 +++++---- test/python/transpiler/test_vf2_post_layout.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 9478b81e8ee9..dfcbb6dfb936 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -172,10 +172,11 @@ def map_free_qubits( """Add any free nodes to a layout.""" if not free_nodes: return partial_layout - qubits = set(range(num_physical_qubits)) - used_bits = set(partial_layout.get_physical_bits()) + free_qubits = sorted( + set(range(num_physical_qubits)) - partial_layout.get_physical_bits().keys(), + key=lambda bit: avg_error_map.get((bit,), 1.0), + ) for im_index in sorted(free_nodes, key=lambda x: sum(free_nodes[x].values())): - selected_qubit = min(qubits - used_bits, key=lambda bit: avg_error_map.get((bit,), 1.0)) - used_bits.add(selected_qubit) + selected_qubit = free_qubits.pop(0) partial_layout.add(reverse_bit_map[im_index], selected_qubit) return partial_layout diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index ee74c7f86e6d..eb763a05bec6 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -18,7 +18,6 @@ from qiskit.circuit import ControlFlowOp from qiskit.circuit.library import CXGate, XGate from qiskit.transpiler import CouplingMap, Layout, TranspilerError -from qiskit.transpiler.passes.layout import vf2_utils from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase From ae2908b2d0c503159c4897a313227e42888789e5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 21 Nov 2022 10:11:44 -0500 Subject: [PATCH 3/3] Update vf2 scoring usage after rebase with rust scoring function --- qiskit/transpiler/passes/layout/vf2_utils.py | 27 ++++++++++++++----- src/error_map.rs | 10 +++++++ src/vf2_layout.rs | 28 +++++++++++--------- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index e44c216e42b7..75557732fc8b 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -111,8 +111,9 @@ def score_layout( size = 0 nlayout = NLayout(layout_mapping, size + 1, size + 1) bit_list = np.zeros(len(im_graph), dtype=np.int32) - for node_index in bit_map.values(): - bit_list[node_index] = sum(im_graph[node_index].values()) + if strict_direction: + for node_index in bit_map.values(): + bit_list[node_index] = sum(im_graph[node_index].values()) edge_list = { (edge[0], edge[1]): sum(edge[2].values()) for edge in im_graph.edge_index_map().values() } @@ -175,7 +176,12 @@ def build_average_error_map(target, properties, coupling_map): continue avg_map.add_error(qargs, statistics.mean(v)) built = True - elif coupling_map is not None: + # if there are no error rates in the target we should fallback to using the degree heuristic + # used for a coupling map. To do this we can build the coupling map from the target before + # running the fallback heuristic + if not built and target is not None and coupling_map is None: + coupling_map = target.build_coupling_map() + if not built and coupling_map is not None: for qubit in range(num_qubits): avg_map.add_error( (qubit, qubit), @@ -215,10 +221,17 @@ def map_free_qubits( """Add any free nodes to a layout.""" if not free_nodes: return partial_layout - free_qubits = sorted( - set(range(num_physical_qubits)) - partial_layout.get_physical_bits().keys(), - key=lambda bit: avg_error_map.get((bit,), 1.0), - ) + if avg_error_map is not None: + free_qubits = sorted( + set(range(num_physical_qubits)) - partial_layout.get_physical_bits().keys(), + key=lambda bit: avg_error_map.get((bit, bit), 1.0), + ) + # If no error map is available this means there is no scoring heuristic available for this + # backend and we can just randomly pick a free qubit + else: + free_qubits = list( + set(range(num_physical_qubits)) - partial_layout.get_physical_bits().keys() + ) for im_index in sorted(free_nodes, key=lambda x: sum(free_nodes[x].values())): selected_qubit = free_qubits.pop(0) partial_layout.add(reverse_bit_map[im_index], selected_qubit) diff --git a/src/error_map.rs b/src/error_map.rs index bc56a3a6aacf..421510c38490 100644 --- a/src/error_map.rs +++ b/src/error_map.rs @@ -90,6 +90,16 @@ impl ErrorMap { fn __contains__(&self, key: [usize; 2]) -> PyResult { Ok(self.error_map.contains_key(&key)) } + + fn get(&self, py: Python, key: [usize; 2], default: Option) -> PyObject { + match self.error_map.get(&key).copied() { + Some(val) => val.to_object(py), + None => match default { + Some(val) => val, + None => py.None(), + }, + } + } } #[pymodule] diff --git a/src/vf2_layout.rs b/src/vf2_layout.rs index dbac7e4ab9b7..8ad65cfbcd1f 100644 --- a/src/vf2_layout.rs +++ b/src/vf2_layout.rs @@ -73,19 +73,21 @@ pub fn score_layout( } else { edge_list.par_iter().filter_map(edge_filter_map).product() }; - fidelity *= if bit_list.len() < PARALLEL_THRESHOLD || !run_in_parallel { - bit_counts - .iter() - .enumerate() - .filter_map(bit_filter_map) - .product::() - } else { - bit_counts - .par_iter() - .enumerate() - .filter_map(bit_filter_map) - .product() - }; + if strict_direction { + fidelity *= if bit_list.len() < PARALLEL_THRESHOLD || !run_in_parallel { + bit_counts + .iter() + .enumerate() + .filter_map(bit_filter_map) + .product::() + } else { + bit_counts + .par_iter() + .enumerate() + .filter_map(bit_filter_map) + .product() + }; + } Ok(1. - fidelity) }