Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9a5f01b
adding SabreStartingLayoutUsingVF2 analysis pass
alexanderivrii Sep 13, 2023
625d857
fixing imports
alexanderivrii Sep 13, 2023
c244393
bug fix
alexanderivrii Sep 13, 2023
be118a6
fixing test after changing some of the options
alexanderivrii Sep 13, 2023
6e9e113
renaming
alexanderivrii Sep 15, 2023
7df9faf
adding target; more renaming; tests
alexanderivrii Sep 15, 2023
3982e03
release notes
alexanderivrii Sep 15, 2023
9ad9806
Merge branch 'main' into initial-layout-for-sabre
alexanderivrii Sep 20, 2023
f006dcd
applying suggestions from code review
alexanderivrii Sep 20, 2023
e02f859
removing debug print
alexanderivrii Sep 20, 2023
64e89d1
Update qiskit/transpiler/passes/layout/sabre_pre_layout.py
alexanderivrii Sep 29, 2023
c91fac1
adding missing :
alexanderivrii Sep 29, 2023
7235ef7
collecting edges into a set
alexanderivrii Sep 29, 2023
986b8e6
Merge branch 'main' into initial-layout-for-sabre
alexanderivrii Oct 1, 2023
668e2bd
adjusting error_rate with respect to distance
alexanderivrii Oct 12, 2023
e7e010e
Merge branch 'initial-layout-for-sabre' of github.com:alexanderivrii/…
alexanderivrii Oct 12, 2023
96b1056
letting coupling_map be either coupling map or target, for consistenc…
alexanderivrii Oct 12, 2023
2728310
Merge branch 'main' into initial-layout-for-sabre
alexanderivrii Oct 16, 2023
5e6d3a5
apply suggestions from code review
alexanderivrii Oct 16, 2023
70754a5
Update qiskit/transpiler/passes/layout/sabre_pre_layout.py
alexanderivrii Oct 16, 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
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
Layout2qDistance
EnlargeWithAncilla
FullAncillaAllocation
SabrePreLayout

Routing
=======
Expand Down Expand Up @@ -191,6 +192,7 @@
from .layout import Layout2qDistance
from .layout import EnlargeWithAncilla
from .layout import FullAncillaAllocation
from .layout import SabrePreLayout

# routing
from .routing import BasicSwap
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
from .layout_2q_distance import Layout2qDistance
from .enlarge_with_ancilla import EnlargeWithAncilla
from .full_ancilla_allocation import FullAncillaAllocation
from .sabre_pre_layout import SabrePreLayout
217 changes: 217 additions & 0 deletions qiskit/transpiler/passes/layout/sabre_pre_layout.py
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:
target (Target): A target representing the backend device. If specified, it will
supersede a set value for ``coupling_map``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
target (Target): A target representing the backend device. If specified, it will
supersede a set value for ``coupling_map``.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oops. Fixed in 5e6d3a5.

coupling_map (Union[CouplingMap, Target]): directed graph representing the
original coupling map.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
coupling_map (Union[CouplingMap, Target]): directed graph representing the
original coupling map.
coupling_map (Union[CouplingMap, Target]): directed graph representing the
original coupling map or a target modelling the backend (including its
connectivity).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 5e6d3a5.

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 either target or coupling_map to be provided."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
"SabrePreLayout requires either target or coupling_map to be provided."
"SabrePreLayout requires coupling_map to be used with either a "
"CouplingMap or Target."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 5e6d3a5.

)

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.node_indices())
Comment thread
alexanderivrii marked this conversation as resolved.
Outdated
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)

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member Author

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.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 _find_layout sets max_trials=1. Is there value in running multiple trials on the output of minimization to take into account error rates?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 run function, something like improve_layout_max_trials_vf2 (and, while we are at this, also improve_layout_call_limit_vf2). Please tell me if you are agree with this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 VF2PostLayout which factors in the whole circuit after routing. The place where this is different is to try and avoid the extra edges we've added to the coupling map. But I think for right now this is probably fine, we can always refine the pass more and add extra options in a follow up.


return best_layout
37 changes: 37 additions & 0 deletions releasenotes/notes/add-sabre-starting-layout-7e151b7abb8a6c13.yaml
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)
31 changes: 31 additions & 0 deletions test/python/transpiler/test_sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

import unittest

import math

from qiskit import QuantumRegister, QuantumCircuit
from qiskit.circuit.library import EfficientSU2
from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager
from qiskit.transpiler.passes import SabreLayout, DenseLayout
from qiskit.transpiler.exceptions import TranspilerError
Expand All @@ -24,6 +27,7 @@
from qiskit.providers.fake_provider import FakeAlmaden, FakeAlmadenV2
from qiskit.providers.fake_provider import FakeKolkata
from qiskit.providers.fake_provider import FakeMontreal
from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout


class TestSabreLayout(QiskitTestCase):
Expand Down Expand Up @@ -389,5 +393,32 @@ def test_with_partial_layout(self):
self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8])


class TestSabrePreLayout(QiskitTestCase):
"""Tests the SabreLayout pass with starting layout created by SabrePreLayout."""

def setUp(self):
super().setUp()
circuit = EfficientSU2(16, entanglement="circular", reps=6, flatten=True)
circuit.assign_parameters([math.pi / 2] * len(circuit.parameters), inplace=True)
circuit.measure_all()
self.circuit = circuit
self.coupling_map = CouplingMap.from_heavy_hex(7)

def test_starting_layout(self):
"""Test that a starting layout is created and looks as expected."""
pm = PassManager(
[
SabrePreLayout(coupling_map=self.coupling_map),
SabreLayout(self.coupling_map, seed=123456, swap_trials=1, layout_trials=1),
]
)
pm.run(self.circuit)
layout = pm.property_set["layout"]
self.assertEqual(
[layout[q] for q in self.circuit.qubits],
[30, 98, 104, 36, 103, 35, 65, 28, 61, 91, 22, 92, 23, 93, 62, 99],
)


if __name__ == "__main__":
unittest.main()
Loading