-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Starting layout analysis pass for sabre #10829
Changes from all commits
9a5f01b
625d857
c244393
be118a6
6e9e113
7df9faf
3982e03
9ad9806
f006dcd
e02f859
64e89d1
c91fac1
7235ef7
986b8e6
668e2bd
e7e010e
96b1056
2728310
5e6d3a5
70754a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
# 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. | ||
|
||
"""Creating Sabre starting layouts.""" | ||
|
||
import itertools | ||
|
||
from qiskit.transpiler import CouplingMap, Target, AnalysisPass, TranspilerError | ||
from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout | ||
from qiskit._accelerate.error_map import ErrorMap | ||
|
||
|
||
class SabrePreLayout(AnalysisPass): | ||
"""Choose a starting layout to use for additional Sabre layout trials. | ||
|
||
Property Set Values Written | ||
--------------------------- | ||
|
||
``sabre_starting_layouts`` (``list[Layout]``) | ||
An optional list of :class:`~.Layout` objects to use for additional Sabre layout trials. | ||
|
||
""" | ||
|
||
def __init__( | ||
self, | ||
coupling_map, | ||
max_distance=2, | ||
error_rate=0.1, | ||
max_trials_vf2=100, | ||
call_limit_vf2=None, | ||
improve_layout=True, | ||
): | ||
"""SabrePreLayout initializer. | ||
|
||
The pass works by augmenting the coupling map with more and more "extra" edges | ||
until VF2 succeeds to find a perfect graph isomorphism. More precisely, the | ||
augmented coupling map contains edges between nodes that are within a given | ||
distance ``d`` in the original coupling map, and the value of ``d`` is increased | ||
until an isomorphism is found. | ||
|
||
Intuitively, a better layout involves fewer extra edges. The pass also optionally | ||
minimizes the number of extra edges involved in the layout until a local minimum | ||
is found. This involves removing extra edges and running VF2 to see if an | ||
isomorphism still exists. | ||
|
||
Args: | ||
coupling_map (Union[CouplingMap, Target]): directed graph representing the | ||
original coupling map or a target modelling the backend (including its | ||
connectivity). | ||
max_distance (int): the maximum distance to consider for augmented coupling maps. | ||
error_rate (float): the error rate to assign to the "extra" edges. A non-zero | ||
error rate prioritizes VF2 to choose original edges over extra edges. | ||
max_trials_vf2 (int): specifies the maximum number of VF2 trials. A larger number | ||
allows VF2 to explore more layouts, eventually choosing the one with the smallest | ||
error rate. | ||
call_limit_vf2 (int): limits each call to VF2 by bounding the number of VF2 state visits. | ||
improve_layout (bool): whether to improve the layout by minimizing the number of | ||
extra edges involved. This might be time-consuming as this requires additional | ||
VF2 calls. | ||
|
||
Raises: | ||
TranspilerError: At runtime, if neither ``coupling_map`` or ``target`` are provided. | ||
""" | ||
|
||
self.max_distance = max_distance | ||
self.error_rate = error_rate | ||
self.max_trials_vf2 = max_trials_vf2 | ||
self.call_limit_vf2 = call_limit_vf2 | ||
self.improve_layout = improve_layout | ||
|
||
if isinstance(coupling_map, Target): | ||
self.target = coupling_map | ||
self.coupling_map = self.target.build_coupling_map() | ||
else: | ||
self.target = None | ||
self.coupling_map = coupling_map | ||
|
||
super().__init__() | ||
|
||
def run(self, dag): | ||
"""Run the SabrePreLayout pass on `dag`. | ||
|
||
The discovered starting layout is written to the property set | ||
value ``sabre_starting_layouts``. | ||
|
||
Args: | ||
dag (DAGCircuit): DAG to create starting layout for. | ||
""" | ||
|
||
if self.coupling_map is None: | ||
raise TranspilerError( | ||
"SabrePreLayout requires coupling_map to be used with either" | ||
"CouplingMap or a Target." | ||
) | ||
|
||
starting_layout = None | ||
cur_distance = 1 | ||
while cur_distance <= self.max_distance: | ||
augmented_map, augmented_error_map = self._add_extra_edges(cur_distance) | ||
pass_ = VF2Layout( | ||
augmented_map, | ||
seed=0, | ||
max_trials=self.max_trials_vf2, | ||
call_limit=self.call_limit_vf2, | ||
) | ||
pass_.property_set["vf2_avg_error_map"] = augmented_error_map | ||
pass_.run(dag) | ||
|
||
if "layout" in pass_.property_set: | ||
starting_layout = pass_.property_set["layout"] | ||
break | ||
|
||
cur_distance += 1 | ||
|
||
if cur_distance > 1 and starting_layout is not None: | ||
# optionally improve starting layout | ||
if self.improve_layout: | ||
starting_layout = self._minimize_extra_edges(dag, starting_layout) | ||
# write discovered layout into the property set | ||
if "sabre_starting_layouts" not in self.property_set: | ||
self.property_set["sabre_starting_layouts"] = [starting_layout] | ||
else: | ||
self.property_set["sabre_starting_layouts"].append(starting_layout) | ||
|
||
def _add_extra_edges(self, distance): | ||
"""Augments the coupling map with extra edges that connect nodes ``distance`` | ||
apart in the original graph. The extra edges are assigned errors allowing VF2 | ||
to prioritize real edges over extra edges. | ||
""" | ||
nq = len(self.coupling_map.graph) | ||
augmented_coupling_map = CouplingMap() | ||
augmented_coupling_map.graph = self.coupling_map.graph.copy() | ||
augmented_error_map = ErrorMap(nq) | ||
|
||
for (x, y) in itertools.combinations(self.coupling_map.graph.node_indices(), 2): | ||
d = self.coupling_map.distance(x, y) | ||
if 1 < d <= distance: | ||
error_rate = 1 - ((1 - self.error_rate) ** d) | ||
augmented_coupling_map.add_edge(x, y) | ||
augmented_error_map.add_error((x, y), error_rate) | ||
augmented_coupling_map.add_edge(y, x) | ||
augmented_error_map.add_error((y, x), error_rate) | ||
|
||
return augmented_coupling_map, augmented_error_map | ||
|
||
def _get_extra_edges_used(self, dag, layout): | ||
"""Returns the set of extra edges involved in the layout.""" | ||
extra_edges_used = set() | ||
virtual_bits = layout.get_virtual_bits() | ||
for node in dag.two_qubit_ops(): | ||
p0 = virtual_bits[node.qargs[0]] | ||
p1 = virtual_bits[node.qargs[1]] | ||
if self.coupling_map.distance(p0, p1) > 1: | ||
extra_edge = (p0, p1) if p0 < p1 else (p1, p0) | ||
extra_edges_used.add(extra_edge) | ||
return extra_edges_used | ||
|
||
def _find_layout(self, dag, edges): | ||
"""Checks if there is a layout for a given set of edges.""" | ||
cm = CouplingMap(edges) | ||
pass_ = VF2Layout(cm, seed=0, max_trials=1, call_limit=self.call_limit_vf2) | ||
pass_.run(dag) | ||
return pass_.property_set.get("layout", None) | ||
|
||
def _minimize_extra_edges(self, dag, starting_layout): | ||
"""Minimizes the set of extra edges involved in the layout. This iteratively | ||
removes extra edges from the coupling map and uses VF2 to check if a layout | ||
still exists. This is reasonably efficiently as it only looks for a local | ||
minimum. | ||
""" | ||
# compute the set of edges in the original coupling map | ||
real_edges = [] | ||
for (x, y) in itertools.combinations(self.coupling_map.graph.node_indices(), 2): | ||
d = self.coupling_map.distance(x, y) | ||
if d == 1: | ||
real_edges.append((x, y)) | ||
|
||
best_layout = starting_layout | ||
|
||
# keeps the set of "necessary" extra edges: without a necessary edge | ||
# a layout no longer exists | ||
extra_edges_necessary = [] | ||
|
||
extra_edges_unprocessed_set = self._get_extra_edges_used(dag, starting_layout) | ||
|
||
while extra_edges_unprocessed_set: | ||
# choose some unprocessed edge | ||
edge_chosen = next(iter(extra_edges_unprocessed_set)) | ||
extra_edges_unprocessed_set.remove(edge_chosen) | ||
|
||
# check if a layout still exists without this edge | ||
layout = self._find_layout( | ||
dag, real_edges + extra_edges_necessary + list(extra_edges_unprocessed_set) | ||
) | ||
|
||
if layout is None: | ||
# without this edge the layout either does not exist or is too hard to find | ||
extra_edges_necessary.append(edge_chosen) | ||
|
||
else: | ||
# this edge is not necessary, furthermore we can trim the set of edges to examine based | ||
# in the edges involved in the layout. | ||
extra_edges_unprocessed_set = self._get_extra_edges_used(dag, layout).difference( | ||
set(extra_edges_necessary) | ||
) | ||
best_layout = layout | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we minimize the edge list we don't take into account noise really and just return the first edge found? Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good question. On the one hand, I believe that some part of the effort of looking for extra solutions at this stage would be wasted, as the top-level algorithm would proceed to removing more edges and calling this function again. On the other hand, it does seem a good idea to take the noise into account here as well. However, I do think that the effort here should be smaller than the effort in the main call. A possible solution would be to add yet another argument to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I'm not sure either way. In general for sabre I don't think noise awareness buys us very much because we have |
||
|
||
return best_layout |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
features: | ||
- | | ||
Added a new analysis :class:`.SabrePreLayout` pass that creates a starting | ||
layout for :class:`.SabreLayout`, writing the layout into the property set | ||
value ``sabre_starting_layouts``. | ||
|
||
The pass works by augmenting the coupling map with more and more "extra" edges | ||
until :class:`.VF2Layout` succeeds to find a perfect graph isomorphism. | ||
More precisely, the augmented coupling map contains edges between nodes that are | ||
within a given distance ``d`` in the original coupling map, and the value of ``d`` | ||
is increased until an isomorphism is found. The pass also optionally minimizes | ||
the number of extra edges involved in the layout until a local minimum is found. | ||
This involves removing extra edges and calling :class:`.VF2Layout` to check if | ||
an isomorphism still exists. | ||
|
||
Here is an example of calling the :class:`.SabrePreLayout` before :class:`.SabreLayout`:: | ||
|
||
import math | ||
from qiskit.transpiler import CouplingMap, PassManager | ||
from qiskit.circuit.library import EfficientSU2 | ||
from qiskit.transpiler.passes import SabrePreLayout, SabreLayout | ||
|
||
qc = EfficientSU2(16, entanglement='circular', reps=6, flatten=True) | ||
qc.assign_parameters([math.pi / 2] * len(qc.parameters), inplace=True) | ||
qc.measure_all() | ||
|
||
coupling_map = CouplingMap.from_heavy_hex(7) | ||
|
||
pm = PassManager( | ||
[ | ||
SabrePreLayout(coupling_map=coupling_map), | ||
SabreLayout(coupling_map), | ||
] | ||
) | ||
|
||
pm.run(qc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this is a bit heavy weight we're basically we just need to call
rustworkx.is_subgraph_isomorphic(cm_graph, im_graph, id_order=True, induced=False, call_limit=self.call_limit_vf2)
here without all the pass machinery to determine if a reduced edge list is valid or not.For a first implementation I think this is fine, because there is probably some larger refinement we'll want to do since we do need a layout with the minimized edge list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am leaving this as is for now, but sure this is something to rethink in the future.