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 VF2PostLayout pass #7862

Merged
merged 31 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d451d08
Add VF2PostLayout pass
mtreinish Apr 1, 2022
7b63d18
Fix matching callback function
mtreinish Apr 1, 2022
30355f1
Merge branch 'main' into post-layout-ala-mapomatic
mtreinish Apr 1, 2022
2c177c2
Fix lint
mtreinish Apr 1, 2022
7ffb187
Add back removed ancillas to layout
mtreinish Apr 1, 2022
bb7b3e4
Move VF2PostLayout before scheduling
mtreinish Apr 1, 2022
cc82c4a
Fix docs
mtreinish Apr 1, 2022
2a153cc
Drop max_trials and rely solely on time
mtreinish Apr 1, 2022
9368a96
Fix ancilla handling
mtreinish Apr 1, 2022
0ec350a
Restore original virtual bits and registers
mtreinish Apr 1, 2022
9d957c8
Only run post layout if no manual layout options are set
mtreinish Apr 1, 2022
93359c9
Only run VF2PostLayout if there are connectivity constraints in the t…
mtreinish Apr 1, 2022
3e456d7
Merge branch 'main' into post-layout-ala-mapomatic
mtreinish Apr 1, 2022
926680d
Adjust cost function to no be a sum of error rates
mtreinish Apr 2, 2022
8cb4ae9
Update tests that check exact layout
mtreinish Apr 2, 2022
f98b430
Fix lint
mtreinish Apr 2, 2022
9c95dd0
Fix typo in target node/edge match function
mtreinish Apr 4, 2022
9270a9b
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish Apr 4, 2022
411e835
Add release notes
mtreinish Apr 5, 2022
b7f478a
Add tests
mtreinish Apr 5, 2022
48a4e4f
Merge branch 'main' into post-layout-ala-mapomatic
mtreinish Apr 5, 2022
ad6ac58
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish Apr 21, 2022
356aad9
Update release notes
mtreinish Apr 21, 2022
3b8f208
Fix typos in release notes
mtreinish Apr 22, 2022
3b3c0a0
Add strict_direction flag to pass
mtreinish Apr 25, 2022
6873cf9
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish Apr 25, 2022
5e90148
Move VF2PostLayout to right after routing in preset passmanagers
mtreinish Apr 25, 2022
878c69a
Updated expected test layout for v2 failing test case
mtreinish Apr 25, 2022
c5d7070
Fix doc issues
mtreinish Apr 26, 2022
70e2bfd
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish Apr 26, 2022
29886f5
Merge branch 'main' into post-layout-ala-mapomatic
mergify[bot] Apr 26, 2022
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
9 changes: 9 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@
LinearFunctionsSynthesis
LinearFunctionsToPermutations

Post Layout (Post transpile qubit selection)
============================================

.. autosummary::
:toctree: ../stubs/

Vf2PostLayout

Additional Passes
=================

Expand Down Expand Up @@ -167,6 +175,7 @@
from .layout import SabreLayout
from .layout import CSPLayout
from .layout import VF2Layout
from .layout import VF2PostLayout
from .layout import ApplyLayout
from .layout import Layout2qDistance
from .layout import EnlargeWithAncilla
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 @@ -19,6 +19,7 @@
from .sabre_layout import SabreLayout
from .csp_layout import CSPLayout
from .vf2_layout import VF2Layout
from .vf2_post_layout import VF2PostLayout
from .apply_layout import ApplyLayout
from .layout_2q_distance import Layout2qDistance
from .enlarge_with_ancilla import EnlargeWithAncilla
Expand Down
305 changes: 305 additions & 0 deletions qiskit/transpiler/passes/layout/vf2_post_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# 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.

"""VF2PostLayout pass to find a layout after transpile using subgraph isomorphism"""
from enum import Enum
import logging
import random
import time
from collections import defaultdict

from retworkx import PyDiGraph, vf2_mapping

from qiskit.transpiler.layout import Layout
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.providers.exceptions import BackendPropertyError


logger = logging.getLogger(__name__)


class VF2PostLayoutStopReason(Enum):
"""Stop reasons for VF2PostLayout pass."""

SOLUTION_FOUND = "solution found"
NO_SOLUTION_FOUND = "nonexistent solution"
MORE_THAN_2Q = ">2q gates in basis"


def _target_match(node_a, node_b):
if isinstance(node_a, set):
return node_a.issuperset(node_b.keys())
else:
return set(node_b).issubset(node_a)


class VF2PostLayout(AnalysisPass):
"""A pass for choosing a Layout after transpilation of a circuit onto a
Coupling graph, as a subgraph isomorphism problem, solved by VF2++.

Unlike the :class:`~.VF2PostLayout` transpiler pass which is designed to find an
initial layout for a circuit early in the transpilation pipeline this transpiler
pass is designed to try and find a better layout after transpilation is complete.
The initial layout phase of the transpiler doesn't have as much information available
as we do after transpilation. This pass is designed to be paired in a similar pipeline
as the layout passes. This pass will strip any idle wires from the circuit, use VF2
to find a subgraph in the coupling graph for the circuit to run on with better fidelity
and then update the circuit layout to use the new qubits.

If a solution is found that means there is a "perfect layout" and that no
further swap mapping or routing is needed. If a solution is found the layout
will be set in the property set as ``property_set['layout']``. However, if no
solution is found, no ``property_set['layout']`` is set. The stopping reason is
set in ``property_set['VF2PostLayout_stop_reason']`` in all the cases and will be
one of the values enumerated in ``VF2PostLayoutStopReason`` which has the
following values:

* ``"solution found"``: If a perfect layout was found.
* ``"nonexistent solution"``: If no perfect layout was found.
* ``">2q gates in basis"``: If VF2PostLayout can't work with basis

"""

def __init__(
self,
target=None,
coupling_map=None,
properties=None,
seed=None,
call_limit=None,
time_limit=None,
max_trials=None,
):
"""Initialize a ``VF2PostLayout`` pass instance

Args:
target (Target): A target representing the backend device to run ``VF2PostLayout`` on.
If specified it will supersede a set value for ``properties`` and
``coupling_map``.
coupling_map (CouplingMap): Directed graph representing a coupling map.
properties (BackendProperties): The backend properties for the backend. If
:meth:`~qiskit.providers.models.BackendProperties.readout_error` is available
it is used to score the layout.
seed (int): Sets the seed of the PRNG. -1 Means no node shuffling.
call_limit (int): The number of state visits to attempt in each execution of
VF2.
time_limit (float): The total time limit in seconds to run ``VF2PostLayout``
max_trials (int): The maximum number of trials to run VF2 to find
a layout. If this is not specified the number of trials will be limited
based on the number of edges in the interaction graph or the coupling graph
(whichever is larger). If set to a value <= 0 no limit on the number of trials
will be set.

Raises:
TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided.
"""
super().__init__()
self.target = target
self.coupling_map = coupling_map
self.properties = properties
self.call_limit = call_limit
self.time_limit = time_limit
self.max_trials = max_trials
self.seed = seed

def run(self, dag):
"""run the layout method"""
if self.target is None and (self.coupling_map is None or self.properties is None):
raise TranspilerError(
"A target must be specified or a coupling map and properties must be provided"
)
im_graph = PyDiGraph(multigraph=False)
im_graph_node_map = {}
reverse_im_graph_node_map = {}

for node in dag.op_nodes(include_directives=False):
len_args = len(node.qargs)
if len_args == 1:
if node.qargs[0] not in im_graph_node_map:
weight = defaultdict(int)
weight[node.name] += 1
im_graph_node_map[node.qargs[0]] = im_graph.add_node(weight)
reverse_im_graph_node_map[im_graph_node_map[node.qargs[0]]] = node.qargs[0]
else:
im_graph[im_graph_node_map[node.qargs[0]]][node.op.name] += 1
if len_args == 2:
if node.qargs[0] not in im_graph_node_map:
im_graph_node_map[node.qargs[0]] = im_graph.add_node(defaultdict(int))
reverse_im_graph_node_map[im_graph_node_map[node.qargs[0]]] = node.qargs[0]
if node.qargs[1] not in im_graph_node_map:
im_graph_node_map[node.qargs[1]] = im_graph.add_node(defaultdict(int))
reverse_im_graph_node_map[im_graph_node_map[node.qargs[1]]] = node.qargs[1]
edge = (im_graph_node_map[node.qargs[0]], im_graph_node_map[node.qargs[1]])
if im_graph.has_edge(*edge):
im_graph.get_edge_data(*edge)[node.name] += 1
else:
weight = defaultdict(int)
weight[node.name] += 1
im_graph.add_edge(*edge, weight)
if len_args >= 3:
self.property_set[
"VF2PostLayout_stop_reason"
] = VF2PostLayoutStopReason.MORE_THAN_2Q
return

if self.target is not None:
cm_graph = PyDiGraph(multigraph=False)
cm_graph.add_nodes_from(
[self.target.operation_names_for_qargs((i,)) for i in range(self.target.num_qubits)]
)
for qargs in self.target.qargs:
len_args = len(qargs)
# If qargs == 1 we already populated it and if qargs > 2 there are no instructions
# using those in the circuit because we'd have already returned by this point
if len_args == 2:
cm_graph.add_edge(
qargs[0], qargs[1], self.target.operation_names_for_qargs(qargs)
)
cm_nodes = list(cm_graph.node_indexes())
else:
cm_graph = self.coupling_map.graph
cm_nodes = list(cm_graph.node_indexes())
if self.seed != -1:
random.Random(self.seed).shuffle(cm_nodes)
shuffled_cm_graph = type(cm_graph)()
shuffled_cm_graph.add_nodes_from(cm_nodes)
new_edges = [
(cm_nodes[edge[0]], cm_nodes[edge[1]]) for edge in cm_graph.edge_list()
]
shuffled_cm_graph.add_edges_from_no_data(new_edges)
cm_nodes = [k for k, v in sorted(enumerate(cm_nodes), key=lambda item: item[1])]
cm_graph = shuffled_cm_graph

# To avoid trying to over optimize the result by default limit the number
# of trials based on the size of the graphs. For circuits with simple layouts
# like an all 1q circuit we don't want to sit forever trying every possible
# mapping in the search space
if self.max_trials is None:
im_graph_edge_count = len(im_graph.edge_list())
cm_graph_edge_count = len(cm_graph.edge_list())
self.max_trials = max(im_graph_edge_count, cm_graph_edge_count) + 15

logger.debug("Running VF2 to find post transpile mappings")
if self.target:
mappings = vf2_mapping(
cm_graph,
im_graph,
node_matcher=_target_match,
edge_matcher=_target_match,
subgraph=True,
id_order=False,
induced=False,
call_limit=self.call_limit,
)
Comment on lines +207 to +216
Copy link
Member

Choose a reason for hiding this comment

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

does this return ALL subgraphs and then we score them here? this should be pretty expensive right?

Also say we have a line of 5 qubits and we have chose subset 10 to 1., there are two possibilities based on the orientation of layout. It could be either 10-11-12-13-14 or 14-13-12-11-10. Does it return them both and score separately, or just one? For say a ring of 12 on heavy-hex, there would be 12 orientations.

Copy link
Member Author

@mtreinish mtreinish Apr 22, 2022

Choose a reason for hiding this comment

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

It does mappings here is an iterator and each step will compute a new isomorphic mapping. It can be slow and that is why we set call_limit which is the number of internal state visits the function will try so we limit the amount of time we try waiting. In the preset passmanagers we set this at increasing large number so that we spend at most ~100ms for level1, ~10sec for level 2, and ~60 sec for level 3 (we also check on each iteration that we haven't gone over a timeout parameter and break if we have).

As for the orientation it will try both because it is a directed graph and we're using strict edges. This is actually necessary especially for the backendv2/target path because in that path we might not have all gates available in both directions (which is what the matcher functions here are checking). Also the scores can be different because we'd potentially end up with different gate counts on each qubit and 2q link which would change the score between each orientation.

Copy link
Member

Choose a reason for hiding this comment

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

oh i see, it's looking for directed subgraphs. I think this can be problematic. Suppose I have a circuit that has been laid out on qubits 0 ---> 1 ---> 2 in this direction. So the circuit would be:

----*------
    |
----X--*---
       |
-------X---

Now if these qubits are very noisy but qubits 3 --> 4 <--- 5 are very good, they will not be chosen because they don't have the right direction. But actually fixing direction is trivial. I suggest that choosing layouts should be on undirected graphs (only find good subsets). Then apply a post-post-layout direction fixing pass.

For the case I brought up which is when there are multiple orientations (2 for a line, 12 for a ring of 12), I think this can be an interesting follow-up of choosing the best among those. But already choosing the best subset among many will go a long way.

Copy link
Member Author

@mtreinish mtreinish Apr 22, 2022

Choose a reason for hiding this comment

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

Yeah I considered using undirected edges and using GateDirection when I wrote this, the problem with it is to do an undirected subgraph search we'd basically need to rerun the transpiler after the routing phase again. We can easily do GateDirection after applying the layout to fix this, but then we've potentially injected a bunch of hadamards into the circuit so we need the basis translator to convert that to the native basis set. This will then require the optimization passes to run because the basis translator output can likely be simplified. We basically end up rerunning most of the transpiler at that point. Typing all of this now though has made met think of a potentially interesting follow on we can play with where we could add a strict direction flag to this pass and then add VF2PostLayout and ApplyLayout to end of the optimization loop with that flag set False.

The way I was viewing this pass was given the hard constraints on the backend can we find a better qubit selection with lower noise and if not we don't do anything. So we do miss the opportunity for 3 -> 4 < - 5 if there are no compatible 2q gates on that direction but it is just a heuristic and that seemed ok . Especially with BackendV2 where the gates are defined per qubit (like in your example if 0 -> 1 -> 2 was all in cx but 3 -> 4 <- 5 was only ecr). This seemed the better path to start since in all my tests it was able to find better layouts. At least for all the current backends with connectivity constraints this won't come up since they all currently define bidirectional edges with the same error rates.

Copy link
Member

Choose a reason for hiding this comment

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

We basically end up rerunning most of the transpiler at that point.

If we order the passes right I don't think there will be a duplication. In my mind the order should be something like this:

  1. high-level synthesis (toffolis, cliffords, unitaries, etc.) to reduce the circuit to 1 & 2 qubit gates
  2. layout + routing + post-layout
  3. other parts including 1-& 2-qubit synthesis, optimization, scheduling. Since these just make local changes on 1 or 2 qubits at a time, they don't alter the mapping.

So I was thinking that this PostLayout pass can be done in stage 2 (basically to improve the layout). But if the scoring mechanism relies on the gates exactly being in the Target, then it wouldn't work.
For it to work we would need a more relaxed scoring that can approximate. e.g. if there's a 2-qubit unitary it can assign a score to it based on looking at the 2-qubit error rates on that link. It wouldn't be exact, but I think it would be good enough. Since the whole scoring is approximate anyway. (I think soon the devices will report the native cx direction only btw)

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'll give it a try, I'm curious to see the difference between running it at different spots in the pass manager. I'm also wondering if there value in doing post layout > 1 time in a pipeline, like if we did it with looser constraints at the end of 2 and after 3 with the stricter constraints.

FWIW, I did this after 3 because I thought it would be better because we have the complete circuit so we can see how many gates get run on each of the qubits and get the full error rates with each layout. Especially since DenseLayout is already noise aware so it should be picking similar qubits already. But it's definitely worth testing and checking to see what makes a bigger impact on result quality.

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've been testing this locally running on hardware and most of the time the layouts are the same. But there has been 1 time so far where the layouts were significantly different and the undirected case doing it right after routing was significantly better. So I'm going to adjust the preset pass managers to do it this way in the PR.

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 added the undirected mode in 3b3c0a0 and started using that in the preset pass managers after routing in 5e90148

else:
mappings = vf2_mapping(
cm_graph,
im_graph,
subgraph=True,
id_order=False,
induced=False,
call_limit=self.call_limit,
)
chosen_layout = None
initial_layout = Layout(dict(enumerate(dag.qubits)))
chosen_layout_score = self._score_layout(
initial_layout, im_graph_node_map, reverse_im_graph_node_map, im_graph
)
logger.debug("Initial layout has score %s", chosen_layout_score)

start_time = time.time()
trials = 0
for mapping in mappings:
trials += 1
logger.debug("Running trial: %s", trials)
stop_reason = VF2PostLayoutStopReason.SOLUTION_FOUND
layout = Layout(
{reverse_im_graph_node_map[im_i]: cm_nodes[cm_i] for cm_i, im_i in mapping.items()}
)
layout_score = self._score_layout(
layout, im_graph_node_map, reverse_im_graph_node_map, im_graph
)
logger.debug("Trial %s has score %s", trials, layout_score)
if layout_score < chosen_layout_score:
logger.debug(
"Found layout %s has a lower score (%s) than previous best %s (%s)",
layout,
layout_score,
chosen_layout,
chosen_layout_score,
)
chosen_layout = layout
chosen_layout_score = layout_score
if self.max_trials > 0 and trials >= self.max_trials:
logger.debug("Trial %s is >= configured max trials %s", trials, self.max_trials)
break
elapsed_time = time.time() - start_time
if self.time_limit is not None and elapsed_time >= self.time_limit:
logger.debug(
"VFPostLayout has taken %s which exceeds configured max time: %s",
elapsed_time,
self.time_limit,
)
break
if chosen_layout is None:
stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND
else:
self.property_set["layout"] = chosen_layout
for reg in dag.qregs.values():
self.property_set["layout"].add_register(reg)

self.property_set["VF2PostLayout_stop_reason"] = stop_reason

def _score_layout(self, layout, bit_map, reverse_bit_map, im_graph):
bits = layout.get_virtual_bits()
score = 0
if self.target is not None:
for bit, node_index in bit_map.items():
gate_counts = im_graph[node_index]
for gate, count in gate_counts.items():
if self.target[gate] is not None and None not in self.target[gate]:
props = self.target[gate][(bits[bit],)]
if props is not None and props.error is not None:
score += props.error * count

for edge in im_graph.edge_index_map().values():
qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]])
gate_counts = edge[2]
for gate, count in gate_counts.items():
if self.target[gate] is not None and None not in self.target[gate]:
props = self.target[gate][qargs]
if props is not None and props.error is not None:
score += props.error * count
else:
for bit, node_index in bit_map.items():
gate_counts = im_graph[node_index]
for gate, count in gate_counts.items():
if gate == "measure":
try:
score += self.properties.readout_error(bits[bit]) * count
except BackendPropertyError:
pass
else:
try:
score += self.properties.gate_error(gate, bits[bit]) * count
except BackendPropertyError:
pass
for edge in im_graph.edge_index_map().values():
qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]])
gate_counts = edge[2]
for gate, count in gate_counts.items():
try:
score += self.properties.gate_error(gate, qargs) * count
except BackendPropertyError:
pass
return score
27 changes: 27 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level1.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from qiskit.transpiler.passes import GateDirection
from qiskit.transpiler.passes import SetLayout
from qiskit.transpiler.passes import VF2Layout
from qiskit.transpiler.passes import VF2PostLayout
from qiskit.transpiler.passes import TrivialLayout
from qiskit.transpiler.passes import DenseLayout
from qiskit.transpiler.passes import NoiseAdaptiveLayout
Expand Down Expand Up @@ -61,6 +62,7 @@
from qiskit.transpiler.passes import Error
from qiskit.transpiler.passes import ContainsInstruction
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason
from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason

from qiskit.transpiler import TranspilerError

Expand Down Expand Up @@ -368,4 +370,29 @@ def _require_alignment(property_set):
# Call padding pass if circuit is scheduled
pm1.append(PadDelay())

if target is not None or (coupling_map and backend_properties):

def _post_layout_condition(property_set):
# if VF2 layout stopped for any reason other than solution found we need
# to run layout since VF2 didn't converge.
if (
property_set["VF2PostLayout_stop_reason"] is not None
and property_set["VF2PostLayout_stop_reason"]
is VF2PostLayoutStopReason.SOLUTION_FOUND
):
return True
return False

pm1.append(
VF2PostLayout(
target,
coupling_map,
backend_properties,
seed_transpiler,
call_limit=int(5e4), # Set call limit to ~100ms with retworkx 0.10.2
time_limit=0.1,
)
)
pm1.append(ApplyLayout(), condition=_post_layout_condition)
kdk marked this conversation as resolved.
Show resolved Hide resolved

return pm1
Loading