From 2534efb7023179253cad13e3be933a006c09e446 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 13 Apr 2023 15:22:56 -0400 Subject: [PATCH] Add support for disjoint coupling maps to SabreLayout (#9802) * Add support for disjoint coupling maps to SabreLayout This commit adds support to the SabreLayout pass for targeting disjoint CouplingMap objects. The SABRE algorithm is written assuming a connected graph, so to enable targeting a disjoint coupling graph this commit decomposes the circuit into it's weakly connected components and then maps those to the connected components of the coupling map and runs the rust portion of the sabre code on each connected component of the coupling graph. The results for each subgraph of the target backend is then combined the pass is building the output dag. In general the biggest potential issue for output quality right now is the mapping function does not take into account the relative connectivity of the dag component and the coupling map component. The mapping is just done in order and it tries every component until it finds one that has enough qubits. In the future we should use a heuristic to try and pack the components based on what we expect will require the least amount of swaps, but we can attempt that in a future PR. * Fix handling of barriers across separating components This commit fixes the handling of the barrier directive as the input DAG is split across multiple components. It is entirely valid for an input dag to have a multi-qubit barrier that spans multiple connected components. However, such barriers shouldn't be treated as multi-qubit operation for computing the connected components (as this result in the components being treated larger than they otherwise would be). To handle this edge case the logic introduced into this commit is prior to computing the connected components in the input dag we split each multi-qubit barrier into n single qubit barriers and assign a UUID label to each of the single qubit barriers. Then after we create each subgraph DAGCircuit object for each connected component we find all the barriers with a UUID and recombine them into a multi qubit barrier. This will retain the barriers characteristics for each connected component (as if it were in the original disjoint dag). Then in `SabreLayout` after we've built the output mapped dagcircuit we run the combination function one more time to combine the multiqubit dags across the different components, and in that process the UUID labels are removed. It is worth pointing out the downside with this approach is that it precludes preserving barriers with labels through transpile(). * Fix compatibility with Python < 3.9 * Adjust sorting order for mapping components * Add full path transpile() tests * Tag six component test as a slow test * Fix handling of splitting dags with shared classical bits This commit fixes the handling of the separate_dag() function when the input DAGCircuit has shared classical bits between quantum connected components. In previous commits on this branch these would incorrectly be treated as a single connected component because from the perspective of rustworkx's weakly_connected_components() function there is no difference between the type of wires when computing the connected components of the graph. This commit fixes this by instead of computing the connected components of the DAG itself we create an interaction graph of just the quantum component and use that to find the connected components of qubits. From that we build subgraphs of the DAGCircuit for each connected component DAGCircuit. This ends up being less efficient, but produces the correct result by ignoring the classical component for computing the connected components. * Add more test coverage and release notes * Don't route in SabreLayout with > 1 layout component with shared clbits When we run SabreLayout normally we're running both the layout and routing stage together. This is done for performance because the rust component is running routing internally so we avoid multiple back and forths between Python and Rust this way and it can lead to noticeable runtime improvements when doing this. However, in the case of > 1 circuit component that have any shared clbits we can not safetly route from the split dag components because the data dependency is lost for the classical component of the circuit is lost between the split components. To do routing without potentially reordering the data dependencies for the classical bits we need to run it on the combined dag all at once. For SabreLayout the easiest way to do that is to just have it work as a normal layout pass and let `SabreSwap` do the routing as it will have the full context and won't cause things to reorder incorrectly. This commit makes the change by checking if we have more than one component and any shared clbits between any components. If we do then we skip routing in SabreLayout and only return layout information. * Apply suggestions from code review Co-authored-by: Kevin Hartman * Fix return type hint for map_components() * Cleanup variables and set usage in separate_dag * Remove duplicate lines in SabreLayout * Update error message text for routing_pass arg and disjoint cmap Co-authored-by: Kevin Hartman * Improve docstring for map_components * Add comment explaining dag composition in run_pass_over_connected_components --------- Co-authored-by: Kevin Hartman --- .../passes/layout/disjoint_utils.py | 158 +++++++ .../transpiler/passes/layout/sabre_layout.py | 134 ++++-- .../transpiler/passes/routing/sabre_swap.py | 16 +- ...-disjoint-cmap-sabre-551ae4295131a449.yaml | 11 + test/python/compiler/test_transpiler.py | 444 +++++++++++++++++- test/python/transpiler/test_sabre_layout.py | 139 ++++++ 6 files changed, 858 insertions(+), 44 deletions(-) create mode 100644 qiskit/transpiler/passes/layout/disjoint_utils.py create mode 100644 releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py new file mode 100644 index 000000000000..f4f55a352e1e --- /dev/null +++ b/qiskit/transpiler/passes/layout/disjoint_utils.py @@ -0,0 +1,158 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. + +"""This module contains common utils for disjoint coupling maps.""" + +from collections import defaultdict +from typing import List, Callable, TypeVar, Dict +import uuid + +import rustworkx as rx + +from qiskit.circuit import Qubit, Barrier, Clbit +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOutNode +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.layout import vf2_utils + +T = TypeVar("T") + + +def run_pass_over_connected_components( + dag: DAGCircuit, + coupling_map: CouplingMap, + run_func: Callable[[DAGCircuit, CouplingMap], T], +) -> List[T]: + """Run a transpiler pass inner function over mapped components.""" + cmap_components = coupling_map.connected_components() + # If graph is connected we only need to run the pass once + if len(cmap_components) == 1: + return [run_func(dag, cmap_components[0])] + dag_components = separate_dag(dag) + mapped_components = map_components(dag_components, cmap_components) + out_component_pairs = [] + for cmap_index, dags in mapped_components.items(): + # Take the first dag from the mapped dag components and then + # compose it with any other dag components that are operating on the + # same coupling map connected component. This results in a subcircuit + # of possibly disjoint circuit components which we will run the layout + # pass on. + out_dag = dag_components[dags.pop()] + for dag_index in dags: + dag = dag_components[dag_index] + out_dag.add_qubits(dag.qubits) + out_dag.add_clbits(dag.clbits) + for qreg in dag.qregs: + out_dag.add_qreg(qreg) + for creg in dag.cregs: + out_dag.add_cregs(creg) + out_dag.compose(dag, qubits=dag.qubits, clbits=dag.clbits) + out_component_pairs.append((out_dag, cmap_components[cmap_index])) + res = [run_func(out_dag, cmap) for out_dag, cmap in out_component_pairs] + return res + + +def map_components( + dag_components: List[DAGCircuit], cmap_components: List[CouplingMap] +) -> Dict[int, List[int]]: + """Returns a map where the key is the index of each connected component in cmap_components and + the value is a list of indices from dag_components which should be placed onto it.""" + free_qubits = {index: len(cmap.graph) for index, cmap in enumerate(cmap_components)} + out_mapping = defaultdict(list) + + for dag_index, dag in sorted( + enumerate(dag_components), key=lambda x: x[1].num_qubits(), reverse=True + ): + for cmap_index in sorted( + range(len(cmap_components)), key=lambda index: free_qubits[index], reverse=True + ): + # TODO: Improve heuristic to involve connectivity and estimate + # swap cost + if dag.num_qubits() <= free_qubits[cmap_index]: + out_mapping[cmap_index].append(dag_index) + free_qubits[cmap_index] -= dag.num_qubits() + break + else: + raise TranspilerError( + "A connected component of the DAGCircuit is too large for any of the connected " + "components in the coupling map." + ) + return out_mapping + + +def split_barriers(dag: DAGCircuit): + """Mutate an input dag to split barriers into single qubit barriers.""" + for node in dag.op_nodes(Barrier): + num_qubits = len(node.qargs) + if num_qubits == 1: + continue + barrier_uuid = uuid.uuid4() + split_dag = DAGCircuit() + split_dag.add_qubits([Qubit() for _ in range(num_qubits)]) + for i in range(num_qubits): + split_dag.apply_operation_back( + Barrier(1, label=barrier_uuid), qargs=[split_dag.qubits[i]] + ) + dag.substitute_node_with_dag(node, split_dag) + + +def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True): + """Mutate input dag to combine barriers with UUID labels into a single barrier.""" + qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + uuid_map = {} + for node in dag.op_nodes(Barrier): + if isinstance(node.op.label, uuid.UUID): + barrier_uuid = node.op.label + if barrier_uuid in uuid_map: + other_node = uuid_map[node.op.label] + num_qubits = len(other_node.qargs) + len(node.qargs) + new_op = Barrier(num_qubits, label=barrier_uuid) + new_node = dag.replace_block_with_op([node, other_node], new_op, qubit_indices) + uuid_map[barrier_uuid] = new_node + else: + uuid_map[barrier_uuid] = node + if not retain_uuid: + for node in dag.op_nodes(Barrier): + if isinstance(node.op.label, uuid.UUID): + node.op.label = None + + +def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]: + """Separate a dag circuit into it's connected components.""" + # Split barriers into single qubit barriers before splitting connected components + split_barriers(dag) + im_graph, _, qubit_map, __ = vf2_utils.build_interaction_graph(dag) + connected_components = rx.weakly_connected_components(im_graph) + component_qubits = [] + for component in connected_components: + component_qubits.append(set(qubit_map[x] for x in component)) + + qubits = set(dag.qubits) + + decomposed_dags = [] + for dag_qubits in component_qubits: + new_dag = dag.copy_empty_like() + new_dag.remove_qubits(*qubits - dag_qubits) + new_dag.global_phase = 0 + for node in dag.topological_op_nodes(): + if dag_qubits.issuperset(node.qargs): + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + idle_clbits = [] + for bit, node in new_dag.input_map.items(): + succ_node = next(new_dag.successors(node)) + if isinstance(succ_node, DAGOutNode) and isinstance(succ_node.wire, Clbit): + idle_clbits.append(bit) + new_dag.remove_clbits(*idle_clbits) + combine_barriers(new_dag) + decomposed_dags.append(new_dag) + return decomposed_dags diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 26db9ddecb43..749751fdba5e 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -23,6 +23,7 @@ from qiskit.transpiler.passes.layout.full_ancilla_allocation import FullAncillaAllocation from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout +from qiskit.transpiler.passes.layout import disjoint_utils from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import TransformationPass @@ -171,13 +172,13 @@ def run(self, dag): """ if len(dag.qubits) > self.coupling_map.size(): raise TranspilerError("More virtual qubits exist than physical.") - if not self.coupling_map.is_connected(): - raise TranspilerError( - "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " - "map." - ) + # Choose a random initial_layout. if self.routing_pass is not None: + if not self.coupling_map.is_connected(): + raise TranspilerError( + "The routing_pass argument cannot be used with disjoint coupling maps." + ) if self.seed is None: seed = np.random.randint(0, np.iinfo(np.int32).max) else: @@ -215,7 +216,90 @@ def run(self, dag): self.property_set["layout"] = initial_layout self.routing_pass.fake_run = False return dag - dist_matrix = self.coupling_map.distance_matrix + # Combined + layout_components = disjoint_utils.run_pass_over_connected_components( + dag, self.coupling_map, self._inner_run + ) + initial_layout_dict = {} + final_layout_dict = {} + shared_clbits = False + seen_clbits = set() + for ( + layout_dict, + final_dict, + component_map, + _gate_order, + _swap_map, + local_dag, + ) in layout_components: + initial_layout_dict.update({k: component_map[v] for k, v in layout_dict.items()}) + final_layout_dict.update({component_map[k]: component_map[v] for k, v in final_dict}) + if not shared_clbits: + for clbit in local_dag.clbits: + if clbit in seen_clbits: + shared_clbits = True + break + seen_clbits.add(clbit) + self.property_set["layout"] = Layout(initial_layout_dict) + # If skip_routing is set then return the layout in the property set + # and throwaway the extra work we did to compute the swap map. + # We also skip routing here if the input circuit is split over multiple + # connected components and there is a shared clbit between any + # components. We can only reliably route the full dag if there is any + # shared classical data. + if self.skip_routing or shared_clbits: + return dag + # After this point the pass is no longer an analysis pass and the + # output circuit returned is transformed with the layout applied + # and swaps inserted + dag = self._apply_layout_no_pass_manager(dag) + mapped_dag = dag.copy_empty_like() + self.property_set["final_layout"] = Layout( + {dag.qubits[k]: v for (k, v) in final_layout_dict.items()} + ) + canonical_register = dag.qregs["q"] + qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + original_layout = NLayout.generate_trivial_layout(self.coupling_map.size()) + for ( + _layout_dict, + _final_layout_dict, + component_map, + gate_order, + swap_map, + local_dag, + ) in layout_components: + for node_id in gate_order: + node = local_dag._multi_graph[node_id] + process_swaps( + swap_map, + node, + mapped_dag, + original_layout, + canonical_register, + False, + qubit_indices, + component_map, + ) + apply_gate( + mapped_dag, + node, + original_layout, + canonical_register, + False, + initial_layout_dict, + ) + disjoint_utils.combine_barriers(mapped_dag, retain_uuid=False) + return mapped_dag + + def _inner_run(self, dag, coupling_map): + if not coupling_map.is_symmetric: + # deepcopy is needed here to avoid modifications updating + # shared references in passes which require directional + # constraints + coupling_map = copy.deepcopy(coupling_map) + coupling_map.make_symmetric() + neighbor_table = NeighborTable(rx.adjacency_matrix(coupling_map.graph)) + dist_matrix = coupling_map.distance_matrix original_qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} original_clbit_indices = {bit: index for index, bit in enumerate(dag.clbits)} @@ -236,7 +320,7 @@ def run(self, dag): ((initial_layout, final_layout), swap_map, gate_order) = sabre_layout_and_routing( len(dag.clbits), dag_list, - self._neighbor_table, + neighbor_table, dist_matrix, Heuristic.Decay, self.max_iterations, @@ -245,44 +329,14 @@ def run(self, dag): self.seed, ) # Apply initial layout selected. - original_dag = dag layout_dict = {} num_qubits = len(dag.qubits) for k, v in initial_layout.layout_mapping(): if k < num_qubits: layout_dict[dag.qubits[k]] = v - initital_layout = Layout(layout_dict) - self.property_set["layout"] = initital_layout - # If skip_routing is set then return the layout in the property set - # and throwaway the extra work we did to compute the swap map - if self.skip_routing: - return dag - # After this point the pass is no longer an analysis pass and the - # output circuit returned is transformed with the layout applied - # and swaps inserted - dag = self._apply_layout_no_pass_manager(dag) - # Apply sabre swap ontop of circuit with sabre layout - final_layout_mapping = final_layout.layout_mapping() - self.property_set["final_layout"] = Layout( - {dag.qubits[k]: v for (k, v) in final_layout_mapping} - ) - mapped_dag = dag.copy_empty_like() - canonical_register = dag.qregs["q"] - qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} - original_layout = NLayout.generate_trivial_layout(self.coupling_map.size()) - for node_id in gate_order: - node = original_dag._multi_graph[node_id] - process_swaps( - swap_map, - node, - mapped_dag, - original_layout, - canonical_register, - False, - qubit_indices, - ) - apply_gate(mapped_dag, node, original_layout, canonical_register, False, layout_dict) - return mapped_dag + final_layout_dict = final_layout.layout_mapping() + component_mapping = {x: coupling_map.graph[x] for x in coupling_map.graph.node_indices()} + return layout_dict, final_layout_dict, component_mapping, gate_order, swap_map, dag def _apply_layout_no_pass_manager(self, dag): """Apply and embed a layout into a dagcircuit without using a ``PassManager`` to diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 04e15cbce94b..3fef57f16f66 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -291,11 +291,18 @@ def process_swaps( canonical_register, fake_run, qubit_indices, + swap_qubit_mapping=None, ): """Process swaps from SwapMap.""" if node._node_id in swap_map: for swap in swap_map[node._node_id]: - swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] + if swap_qubit_mapping: + swap_qargs = [ + canonical_register[swap_qubit_mapping[swap[0]]], + canonical_register[swap_qubit_mapping[swap[1]]], + ] + else: + swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] apply_gate( mapped_dag, DAGOpNode(op=SwapGate(), qargs=swap_qargs), @@ -304,7 +311,12 @@ def process_swaps( fake_run, qubit_indices, ) - current_layout.swap_logical(*swap) + if swap_qubit_mapping: + current_layout.swap_logical( + swap_qubit_mapping[swap[0]], swap_qubit_mapping[swap[1]] + ) + else: + current_layout.swap_logical(*swap) def apply_gate(mapped_dag, node, current_layout, canonical_register, fake_run, qubit_indices): diff --git a/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml b/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml new file mode 100644 index 000000000000..806a4e7a12ac --- /dev/null +++ b/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The :class:`~.SabreLayout` pass now supports running against a target + with a disjoint :class:`~.CouplingMap`. When targeting a disjoint coupling + the input :class:`.DAGCircuit` is split into its connected components of + virtual qubits, each component is mapped to the connected components + of the :class:`~.CouplingMap`, layout is run on each connected + component in isolation, and then all layouts are combined and returned. + Note when the ``routing_pass`` argument is set the pass doesn't + support running with disjoint connectivity. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a2efb497ec83..0dcc5ba9ba81 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -24,13 +24,14 @@ from test import combine # pylint: disable=wrong-import-order import numpy as np +import rustworkx as rx from qiskit.exceptions import QiskitError from qiskit import BasicAer from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse, qpy, qasm3 from qiskit.circuit import Parameter, Gate, Qubit, Clbit from qiskit.compiler import transpile -from qiskit.dagcircuit import DAGOutNode +from qiskit.dagcircuit import DAGOutNode, DAGOpNode from qiskit.converters import circuit_to_dag from qiskit.circuit.library import ( CXGate, @@ -42,9 +43,12 @@ RZGate, UGate, CZGate, + XGate, + SXGate, ) from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp from qiskit.circuit.measure import Measure +from qiskit.circuit.delay import Delay from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( FakeMelbourne, @@ -55,7 +59,7 @@ ) from qiskit.transpiler import Layout, CouplingMap from qiskit.transpiler import PassManager, TransformationPass -from qiskit.transpiler.target import Target +from qiskit.transpiler.target import Target, InstructionProperties from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection from qiskit.quantum_info import Operator, random_unitary @@ -63,6 +67,9 @@ from qiskit.transpiler.preset_passmanagers import level_0_pass_manager from qiskit.tools import parallel from qiskit.pulse import InstructionScheduleMap +from qiskit.providers.backend import BackendV2 +from qiskit.providers.options import Options +from qiskit.test import slow_test class CustomCX(Gate): @@ -2026,3 +2033,436 @@ def test_backend_and_custom_gate(self, opt_level): self.assertEqual(tqc.data[0].operation, newgate) qubits = tuple(tqc.find_bit(x).index for x in tqc.data[0].qubits) self.assertIn(qubits, backend.target.qargs) + + +@ddt +class TestTranspileMultiChipTarget(QiskitTestCase): + """Test transpile() with a disjoint coupling map.""" + + def setUp(self): + super().setUp() + + class FakeMultiChip(BackendV2): + """Fake multi chip backend.""" + + def __init__(self): + super().__init__() + graph = rx.generators.directed_heavy_hex_graph(3) + num_qubits = len(graph) * 3 + rng = np.random.default_rng(seed=12345678942) + rz_props = {} + x_props = {} + sx_props = {} + measure_props = {} + delay_props = {} + self._target = Target("Fake multi-chip backend", num_qubits=num_qubits) + for i in range(num_qubits): + qarg = (i,) + rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0) + x_props[qarg] = InstructionProperties( + error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7) + ) + sx_props[qarg] = InstructionProperties( + error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7) + ) + measure_props[qarg] = InstructionProperties( + error=rng.uniform(1e-3, 1e-1), duration=rng.uniform(1e-8, 9e-7) + ) + delay_props[qarg] = None + self._target.add_instruction(XGate(), x_props) + self._target.add_instruction(SXGate(), sx_props) + self._target.add_instruction(RZGate(Parameter("theta")), rz_props) + self._target.add_instruction(Measure(), measure_props) + self._target.add_instruction(Delay(Parameter("t")), delay_props) + cz_props = {} + for i in range(3): + for root_edge in graph.edge_list(): + offset = i * len(graph) + edge = (root_edge[0] + offset, root_edge[1] + offset) + cz_props[edge] = InstructionProperties( + error=rng.uniform(1e-5, 5e-3), duration=rng.uniform(1e-8, 9e-7) + ) + self._target.add_instruction(CZGate(), cz_props) + + @property + def target(self): + return self._target + + @property + def max_circuits(self): + return None + + @classmethod + def _default_options(cls): + return Options(shots=1024) + + def run(self, circuit, **kwargs): + raise NotImplementedError + + self.backend = FakeMultiChip() + + # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps + @data(2, 3) + def test_basic_connected_circuit(self, opt_level): + """Test basic connected circuit on disjoint backend""" + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps + @data(2, 3) + def test_triple_circuit(self, opt_level): + """Test a split circuit with one circuit component per chip.""" + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps + # Tagged as slow until #9834 is fixed + @slow_test + @data(2, 3) + def test_six_component_circuit(self, opt_level): + """Test input circuit with more than 1 component per backend component.""" + qc = QuantumCircuit(42) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.h(30) + qc.cx(30, 31) + qc.cx(30, 32) + qc.cx(30, 33) + qc.h(34) + qc.cx(34, 35) + qc.cx(34, 36) + qc.cx(34, 37) + qc.h(38) + qc.cx(38, 39) + qc.cx(39, 40) + qc.cx(39, 41) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + @data(2, 3) + def test_shared_classical_between_components_condition(self, opt_level): + """Test a condition sharing classical bits between components.""" + creg = ClassicalRegister(19) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(0) + for i in range(18): + qc.cx(0, i + 1) + for i in range(18): + qc.measure(i, creg[i]) + + qc.ecr(20, 21).c_if(creg, 0) + tqc = transpile(qc, self.backend, optimization_level=opt_level) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + + @data(2, 3) + def test_shared_classical_between_components_condition_large_to_small(self, opt_level): + """Test a condition sharing classical bits between components.""" + creg = ClassicalRegister(2) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(24) + qc.cx(24, 23) + qc.measure(24, creg[0]) + qc.measure(23, creg[1]) + qc.h(0).c_if(creg, 0) + for i in range(18): + qc.ecr(0, i + 1).c_if(creg, 0) + tqc = transpile(qc, self.backend, optimization_level=opt_level) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + # Check clbits are in order + # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # go in order between the components. This is a sanity check to ensure that routing + # doesn't reorder a classical data dependency between components. Inside a component + # we have the dag ordering so nothing should be out of order within a component. + initial_layout = tqc.layout.initial_layout + first_component = {qc.qubits[23], qc.qubits[24]} + second_component = {qc.qubits[i] for i in range(19)} + tqc_dag = circuit_to_dag(tqc) + qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} + input_node = tqc_dag.input_map[tqc_dag.clbits[0]] + first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + # The first node should be a measurement + self.assertIsInstance(first_meas_node.op, Measure) + # This shoulde be in the first ocmponent + self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while isinstance(op_node, DAGOpNode): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + + @data(2, 3) + def test_shared_classical_between_components_condition_large_to_small_reverse_index( + self, opt_level + ): + """Test a condition sharing classical bits between components.""" + creg = ClassicalRegister(2) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(0) + qc.cx(0, 1) + qc.measure(0, creg[0]) + qc.measure(1, creg[1]) + qc.h(24).c_if(creg, 0) + for i in range(23, 5, -1): + qc.ecr(24, i).c_if(creg, 0) + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=2023) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + # Check clbits are in order + # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # go in order between the components. This is a sanity check to ensure that routing + # doesn't reorder a classical data dependency between components. Inside a component + # we have the dag ordering so nothing should be out of order within a component. + initial_layout = tqc.layout.initial_layout + first_component = {qc.qubits[i] for i in range(2)} + second_component = {qc.qubits[i] for i in range(6, 25)} + tqc_dag = circuit_to_dag(tqc) + qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} + input_node = tqc_dag.input_map[tqc_dag.clbits[0]] + first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + # The first node should be a measurement + self.assertIsInstance(first_meas_node.op, Measure) + # This shoulde be in the first ocmponent + self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while isinstance(op_node, DAGOpNode): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + + @data(2, 3) + def test_chained_data_dependency(self, opt_level): + """Test 3 component circuit with shared clbits between each component.""" + creg = ClassicalRegister(1) + qc = QuantumCircuit(30) + qc.add_register(creg) + # Component 1 + qc.h(0) + for i in range(9): + qc.cx(0, i + 1) + measure_op = Measure() + qc.append(measure_op, [9], [creg[0]]) + # Component 2 + qc.h(10).c_if(creg, 0) + for i in range(11, 20): + qc.ecr(10, i).c_if(creg, 0) + measure_op = Measure() + qc.append(measure_op, [19], [creg[0]]) + # Component 3 + qc.h(20).c_if(creg, 0) + for i in range(21, 30): + qc.cz(20, i).c_if(creg, 0) + measure_op = Measure() + qc.append(measure_op, [29], [creg[0]]) + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=2023) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + # Check clbits are in order + # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # go in order between the components. This is a sanity check to ensure that routing + # doesn't reorder a classical data dependency between components. Inside a component + # we have the dag ordering so nothing should be incompatible there. + + initial_layout = tqc.layout.initial_layout + first_component = {qc.qubits[i] for i in range(10)} + second_component = {qc.qubits[i] for i in range(10, 20)} + third_component = {qc.qubits[i] for i in range(20, 30)} + tqc_dag = circuit_to_dag(tqc) + qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} + input_node = tqc_dag.input_map[tqc_dag.clbits[0]] + first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + self.assertIsInstance(first_meas_node.op, Measure) + self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while not isinstance(op_node.op, Measure): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while not isinstance(op_node.op, Measure): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7a0ebbfba692..2dc639439308 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -17,6 +17,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.transpiler import CouplingMap from qiskit.transpiler.passes import SabreLayout +from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.compiler.transpiler import transpile @@ -217,5 +218,143 @@ def test_layout_many_search_trials(self): ) +class TestDisjointDeviceSabreLayout(QiskitTestCase): + """Test SabreLayout with a disjoint coupling map.""" + + def setUp(self): + super().setUp() + self.dual_grid_cmap = CouplingMap( + [[0, 1], [0, 2], [1, 3], [2, 3], [4, 5], [4, 6], [5, 7], [5, 8]] + ) + + def test_dual_ghz(self): + """Test a basic example with 2 circuit components and 2 cmap components.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_dual_ghz_with_wide_barrier(self): + """Test a basic example with 2 circuit components and 2 cmap components.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_dual_ghz_with_intermediate_barriers(self): + """Test dual ghz circuit with intermediate barriers local to each componennt.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.barrier(0, 1, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.barrier(4, 5, 6) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_dual_ghz_with_intermediate_spanning_barriers(self): + """Test dual ghz circuit with barrier in the middle across components.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.barrier(0, 1, 2, 4, 5) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_too_large_components(self): + """Assert trying to run a circuit with too large a connected component raises.""" + qc = QuantumCircuit(8) + qc.h(0) + for i in range(1, 6): + qc.cx(0, i) + qc.h(7) + qc.cx(7, 6) + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + with self.assertRaises(TranspilerError): + layout_routing_pass(qc) + + if __name__ == "__main__": unittest.main()