Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for disjoint coupling maps to SabreLayout #9802

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f0fb38e
Add support for disjoint coupling maps to SabreLayout
mtreinish Mar 15, 2023
b5f0017
Fix handling of barriers across separating components
mtreinish Mar 16, 2023
4611e08
Fix compatibility with Python < 3.9
mtreinish Mar 16, 2023
2801978
Adjust sorting order for mapping components
mtreinish Mar 21, 2023
e637d19
Add full path transpile() tests
mtreinish Mar 21, 2023
35fdc84
Tag six component test as a slow test
mtreinish Mar 22, 2023
3092e68
Fix handling of splitting dags with shared classical bits
mtreinish Mar 23, 2023
1ddcb03
Add more test coverage and release notes
mtreinish Mar 24, 2023
19d9570
Merge branch 'main' into layout_and_route_disjoint_coupling_maps
mtreinish Mar 27, 2023
b3443d4
Merge branch 'main' into layout_and_route_disjoint_coupling_maps
mtreinish Apr 6, 2023
0fb0d4f
Merge remote-tracking branch 'origin/main' into layout_and_route_disj…
mtreinish Apr 6, 2023
1575b8a
Don't route in SabreLayout with > 1 layout component with shared clbits
mtreinish Apr 12, 2023
3b078b8
Merge remote-tracking branch 'origin/main' into layout_and_route_disj…
mtreinish Apr 12, 2023
0019460
Apply suggestions from code review
mtreinish Apr 13, 2023
01690e5
Fix return type hint for map_components()
mtreinish Apr 13, 2023
7085288
Cleanup variables and set usage in separate_dag
mtreinish Apr 13, 2023
fe54898
Remove duplicate lines in SabreLayout
mtreinish Apr 13, 2023
1fc880e
Update error message text for routing_pass arg and disjoint cmap
mtreinish Apr 13, 2023
a093283
Merge remote-tracking branch 'origin/main' into layout_and_route_disj…
mtreinish Apr 13, 2023
117223c
Merge remote-tracking branch 'origin/main' into layout_and_route_disj…
mtreinish Apr 13, 2023
08afa5d
Improve docstring for map_components
mtreinish Apr 13, 2023
5df53a7
Add comment explaining dag composition in run_pass_over_connected_com…
mtreinish Apr 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()]
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am potentially worried about this potentially introducing a cycle when combining a barrier across qubits. I think it's possible this could cause that which is why I left the cycle check enabled for this. But we should try to confirm this before merging.

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))
Comment on lines +135 to +138
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A potential future optimization that I'm thinking of doing after this and #9840 merge is to skip the intermediate dag creation here, and go from connected qubits straight to a list of SabreDAGs. This should reduce the number of dag iterations we do and speed things up. But for the first iteration of this I'd like to keep everything unified on using full dags.


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:
Comment on lines +263 to +270
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talking with @kevinhartman offline about this and writing extra test cases to confirm there is a potential bug with classical bits here that will result in a reordering of the data dependency between components. Basically the component which is first in layout_components will end up being first on the classical bits regardless of what the actual data dependency is (the order here is from largest component to smallest).

I think the best fix for this is to just skip routing if layout_components > 1 and just return the layout as metadata. The problem with trying to do routing across components like this is getting the topological order exactly right between components manually is very tricky, especially after the routing pass has done 3 different topological iterations over the subcircuits. It's just easier to run routing again even at a small performance penalty.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this in: 1575b8a so we don't run routing as part of SabreLayout if there is a potential data dependency between components for classical bits.

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