-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Add VF2PostLayout pass #7862
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
d451d08
Add VF2PostLayout pass
mtreinish 7b63d18
Fix matching callback function
mtreinish 30355f1
Merge branch 'main' into post-layout-ala-mapomatic
mtreinish 2c177c2
Fix lint
mtreinish 7ffb187
Add back removed ancillas to layout
mtreinish bb7b3e4
Move VF2PostLayout before scheduling
mtreinish cc82c4a
Fix docs
mtreinish 2a153cc
Drop max_trials and rely solely on time
mtreinish 9368a96
Fix ancilla handling
mtreinish 0ec350a
Restore original virtual bits and registers
mtreinish 9d957c8
Only run post layout if no manual layout options are set
mtreinish 93359c9
Only run VF2PostLayout if there are connectivity constraints in the t…
mtreinish 3e456d7
Merge branch 'main' into post-layout-ala-mapomatic
mtreinish 926680d
Adjust cost function to no be a sum of error rates
mtreinish 8cb4ae9
Update tests that check exact layout
mtreinish f98b430
Fix lint
mtreinish 9c95dd0
Fix typo in target node/edge match function
mtreinish 9270a9b
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish 411e835
Add release notes
mtreinish b7f478a
Add tests
mtreinish 48a4e4f
Merge branch 'main' into post-layout-ala-mapomatic
mtreinish ad6ac58
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish 356aad9
Update release notes
mtreinish 3b8f208
Fix typos in release notes
mtreinish 3b3c0a0
Add strict_direction flag to pass
mtreinish 6873cf9
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish 5e90148
Move VF2PostLayout to right after routing in preset passmanagers
mtreinish 878c69a
Updated expected test layout for v2 failing test case
mtreinish c5d7070
Fix doc issues
mtreinish 70e2bfd
Merge remote-tracking branch 'origin/main' into post-layout-ala-mapom…
mtreinish 29886f5
Merge branch 'main' into post-layout-ala-mapomatic
mergify[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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
or14-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.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.
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 setcall_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.
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.
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: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.
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.
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 doGateDirection
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 addVF2PostLayout
andApplyLayout
to end of the optimization loop with that flag setFalse
.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 withBackendV2
where the gates are defined per qubit (like in your example if0 -> 1 -> 2
was all in cx but3 -> 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.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.
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:
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)
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'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.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'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.
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 added the undirected mode in 3b3c0a0 and started using that in the preset pass managers after routing in 5e90148