Skip to content

Commit

Permalink
Add support for disjoint coupling maps to SabreLayout (#9802)
Browse files Browse the repository at this point in the history
* 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 <kevin@hart.mn>

* 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 <kevin@hart.mn>

* Improve docstring for map_components

* Add comment explaining dag composition in run_pass_over_connected_components

---------

Co-authored-by: Kevin Hartman <kevin@hart.mn>
  • Loading branch information
mtreinish and kevinhartman committed Apr 13, 2023
1 parent d07c5cc commit 2534efb
Show file tree
Hide file tree
Showing 6 changed files with 858 additions and 44 deletions.
158 changes: 158 additions & 0 deletions qiskit/transpiler/passes/layout/disjoint_utils.py
Original file line number Diff line number Diff line change
@@ -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
134 changes: 94 additions & 40 deletions qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)}

Expand All @@ -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,
Expand All @@ -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
Expand Down
16 changes: 14 additions & 2 deletions qiskit/transpiler/passes/routing/sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit 2534efb

Please sign in to comment.