Skip to content

Commit

Permalink
Add full path transpile() support for disjoint backends (#9840)
Browse files Browse the repository at this point in the history
* Add full path transpile() support for disjoint backends

This commit finalizes the last piece of the full path transpile()
support at all optimization levels. The primary piece to accomplish this
was adding DenseLayout support for targeting a disjoint CouplingMap.
With this piece added then all the default transpiler paths in the
preset pass managers are able to support running with a disjoint
CouplingMap. The biggest exception is the TrivialLayout pass which can
result in an invalid layout being selected (where 2q connectivity
crosses connected components). To handle this edge case a check is added
to each routing stage to ensure the selected layout is valid for the
coupling map and it is routable. This enables all the default paths at
all 4 optimization levels for transpile() to be usable with disjoint
connectivity. For optimization_level=0/TrivialLayout if the trivial
layout is invalid the routing pass will raise an error saying as much.

It's worth pointing out that NoiseAdaptiveLayout still doesn't
support disjoint connectivity. However, since it's performance is poor
and it's not used by default in any preset passmanager we can add this
at a later date, but all other combinations of layout and routing
methods should work (given the caveats with TrivialLayout) above.

* Fix test determinism

* Add future facing test for shared classical bits between components

* Enable shared control flow test

Now that #9802 supports shared classical bits correctly this commit
re-enables the tests disabled in the previous commit.

* Remove unused condition for faulty qubits backnedv1 path

* Remove opt level 1 variant of test_chained_data_dependency

* Use enumerate() in check_layout_isolated_to_component()

Co-authored-by: Kevin Hartman <[email protected]>

* Expand level 0 test coverage

* Update test/python/compiler/test_transpiler.py

Co-authored-by: Kevin Hartman <[email protected]>

* s/check_layout_isolated_to_component/require_layout_isolated_to_component/g

---------

Co-authored-by: Kevin Hartman <[email protected]>
  • Loading branch information
mtreinish and kevinhartman authored Apr 17, 2023
1 parent 51fdb3c commit 5dbac1e
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 58 deletions.
85 changes: 47 additions & 38 deletions qiskit/transpiler/passes/layout/dense_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from qiskit.transpiler.layout import Layout
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes.layout import disjoint_utils

from qiskit._accelerate.dense_layout import best_subset

Expand Down Expand Up @@ -47,21 +48,9 @@ def __init__(self, coupling_map=None, backend_prop=None, target=None):
self.coupling_map = coupling_map
self.backend_prop = backend_prop
self.target = target
num_qubits = 0
self.adjacency_matrix = None
if target is not None:
num_qubits = target.num_qubits
self.coupling_map = target.build_coupling_map()
if self.coupling_map is not None:
self.adjacency_matrix = rustworkx.adjacency_matrix(self.coupling_map.graph)
self.error_mat, self._use_error = _build_error_matrix(num_qubits, target=target)
else:
if self.coupling_map:
num_qubits = self.coupling_map.size()
self.adjacency_matrix = rustworkx.adjacency_matrix(self.coupling_map.graph)
self.error_mat, self._use_error = _build_error_matrix(
num_qubits, backend_prop=self.backend_prop, coupling_map=self.coupling_map
)

def run(self, dag):
"""Run the DenseLayout pass on `dag`.
Expand All @@ -79,13 +68,20 @@ def run(self, dag):
raise TranspilerError(
"A coupling_map or target with constrained qargs is necessary to run the pass."
)
if dag.num_qubits() > len(self.coupling_map.largest_connected_component()):
raise TranspilerError(
"Coupling Map is disjoint, this pass can't be used with a disconnected coupling "
"map for a circuit this wide."
)
layout_components = disjoint_utils.run_pass_over_connected_components(
dag, self.coupling_map, self._inner_run
)
layout_mapping = {}
for component in layout_components:
layout_mapping.update(component)
layout = Layout(layout_mapping)
for qreg in dag.qregs.values():
layout.add_register(qreg)
self.property_set["layout"] = layout

def _inner_run(self, dag, coupling_map):
num_dag_qubits = len(dag.qubits)
if num_dag_qubits > self.coupling_map.size():
if num_dag_qubits > coupling_map.size():
raise TranspilerError("Number of qubits greater than device.")
num_cx = 0
num_meas = 0
Expand All @@ -101,15 +97,13 @@ def run(self, dag):
if "measure" in ops.keys():
num_meas = ops["measure"]

best_sub = self._best_subset(num_dag_qubits, num_meas, num_cx)
layout = Layout()
for i, qubit in enumerate(dag.qubits):
layout.add(qubit, int(best_sub[i]))
for qreg in dag.qregs.values():
layout.add_register(qreg)
self.property_set["layout"] = layout
best_sub = self._best_subset(num_dag_qubits, num_meas, num_cx, coupling_map)
layout_mapping = {
qubit: coupling_map.graph[int(best_sub[i])] for i, qubit in enumerate(dag.qubits)
}
return layout_mapping

def _best_subset(self, num_qubits, num_meas, num_cx):
def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map):
"""Computes the qubit mapping with the best connectivity.
Args:
Expand All @@ -125,14 +119,25 @@ def _best_subset(self, num_qubits, num_meas, num_cx):
if num_qubits == 0:
return []

adjacency_matrix = rustworkx.adjacency_matrix(coupling_map.graph)
reverse_index_map = {v: k for k, v in enumerate(coupling_map.graph.nodes())}

error_mat, use_error = _build_error_matrix(
coupling_map.size(),
reverse_index_map,
backend_prop=self.backend_prop,
coupling_map=self.coupling_map,
target=self.target,
)

rows, cols, best_map = best_subset(
num_qubits,
self.adjacency_matrix,
adjacency_matrix,
num_meas,
num_cx,
self._use_error,
self.coupling_map.is_symmetric,
self.error_mat,
use_error,
coupling_map.is_symmetric,
error_mat,
)
data = [1] * len(rows)
sp_sub_graph = coo_matrix((data, (rows, cols)), shape=(num_qubits, num_qubits)).tocsr()
Expand All @@ -141,7 +146,7 @@ def _best_subset(self, num_qubits, num_meas, num_cx):
return best_map


def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop=None):
def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, backend_prop=None):
error_mat = np.zeros((num_qubits, num_qubits))
use_error = False
if target is not None and target.qargs is not None:
Expand All @@ -161,13 +166,15 @@ def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop
# the possible worst case.
error = max(error, props.error)
max_error = error
if any(qubit not in qubit_map for qubit in qargs):
continue
# TODO: Factor in T1 and T2 to error matrix after #7736
if len(qargs) == 1:
qubit = qargs[0]
qubit = qubit_map[qargs[0]]
error_mat[qubit][qubit] = max_error
use_error = True
elif len(qargs) == 2:
error_mat[qargs[0]][qargs[1]] = max_error
error_mat[qubit_map[qargs[0]]][qubit_map[qargs[1]]] = max_error
use_error = True
elif backend_prop and coupling_map:
error_dict = {
Expand All @@ -178,14 +185,16 @@ def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop
for edge in coupling_map.get_edges():
gate_error = error_dict.get(edge)
if gate_error is not None:
error_mat[edge[0]][edge[1]] = gate_error
if edge[0] not in qubit_map or edge[1] not in qubit_map:
continue
error_mat[qubit_map[edge[0]]][qubit_map[edge[1]]] = gate_error
use_error = True
for index, qubit_data in enumerate(backend_prop.qubits):
# Handle faulty qubits edge case
if index >= num_qubits:
break
if index not in qubit_map:
continue
for item in qubit_data:
if item.name == "readout_error":
error_mat[index][index] = item.value
mapped_index = qubit_map[index]
error_mat[mapped_index][mapped_index] = item.value
use_error = True
return error_mat, use_error
15 changes: 15 additions & 0 deletions qiskit/transpiler/passes/layout/disjoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):
node.op.label = None


def require_layout_isolated_to_component(dag: DAGCircuit, coupling_map: CouplingMap) -> bool:
"""Check that the layout of the dag does not require connectivity across connected components
in the CouplingMap"""
qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
component_sets = [set(x.graph.nodes()) for x in coupling_map.connected_components()]
for inst in dag.two_qubit_ops():
component_index = None
for i, component_set in enumerate(component_sets):
if qubit_indices[inst.qargs[0]] in component_set:
component_index = i
break
if qubit_indices[inst.qargs[1]] not in component_sets[component_index]:
raise TranspilerError("Chosen layout is not valid for the target disjoint connectivity")


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
Expand Down
5 changes: 0 additions & 5 deletions qiskit/transpiler/passes/layout/trivial_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ def run(self, dag):
raise TranspilerError("Number of qubits greater than device.")
elif dag.num_qubits() > self.coupling_map.size():
raise TranspilerError("Number of qubits greater than device.")
if not self.coupling_map.is_connected():
raise TranspilerError(
"Coupling Map is disjoint, this pass can't be used with a disconnected coupling "
"map."
)
self.property_set["layout"] = Layout.generate_trivial_layout(
*(dag.qubits + list(dag.qregs.values()))
)
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/routing/basic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from qiskit.transpiler.layout import Layout
from qiskit.circuit.library.standard_gates import SwapGate
from qiskit.transpiler.target import Target
from qiskit.transpiler.passes.layout import disjoint_utils


class BasicSwap(TransformationPass):
Expand Down Expand Up @@ -71,6 +72,7 @@ def run(self, dag):

if len(dag.qubits) > len(self.coupling_map.physical_qubits):
raise TranspilerError("The layout does not match the amount of qubits in the DAG")
disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map)

canonical_register = dag.qregs["q"]
trivial_layout = Layout.generate_trivial_layout(canonical_register)
Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/routing/bip_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from qiskit.transpiler.passes.routing.algorithms.bip_model import BIPMappingModel
from qiskit.transpiler.target import target_to_backend_properties, Target
from qiskit.utils.deprecation import deprecate_func
from qiskit.transpiler.passes.layout import disjoint_utils

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -166,6 +167,7 @@ def run(self, dag):
"BIPMapping requires the number of virtual and physical qubits to be the same. "
"Supply 'qubit_subset' to specify physical qubits to use."
)
disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map)

original_dag = dag

Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/routing/lookahead_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from qiskit.transpiler.layout import Layout
from qiskit.dagcircuit import DAGOpNode
from qiskit.transpiler.target import Target
from qiskit.transpiler.passes.layout import disjoint_utils

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -129,6 +130,7 @@ def run(self, dag):
f"The number of DAG qubits ({len(dag.qubits)}) is greater than the number of "
f"available device qubits ({number_of_available_qubits})."
)
disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map)

register = dag.qregs["q"]
current_state = _SystemState(
Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/routing/sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout
from qiskit.transpiler.target import Target
from qiskit.transpiler.passes.layout import disjoint_utils
from qiskit.dagcircuit import DAGOpNode
from qiskit.tools.parallel import CPU_COUNT

Expand Down Expand Up @@ -212,6 +213,7 @@ def run(self, dag):
heuristic = Heuristic.Decay
else:
raise TranspilerError("Heuristic %s not recognized." % self.heuristic)
disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map)

self.dist_matrix = self.coupling_map.distance_matrix

Expand Down
3 changes: 3 additions & 0 deletions qiskit/transpiler/passes/routing/stochastic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp, Instruction
from qiskit._accelerate import stochastic_swap as stochastic_swap_rs
from qiskit._accelerate import nlayout
from qiskit.transpiler.passes.layout import disjoint_utils

from .utils import get_swap_map_dag

Expand Down Expand Up @@ -104,6 +105,8 @@ def run(self, dag):
if len(dag.qubits) > len(self.coupling_map.physical_qubits):
raise TranspilerError("The layout does not match the amount of qubits in the DAG")

disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map)

self.rng = np.random.default_rng(self.seed)

canonical_register = dag.qregs["q"]
Expand Down
Loading

0 comments on commit 5dbac1e

Please sign in to comment.