Skip to content

Commit

Permalink
Deduplicate and unify VF2 layout passes
Browse files Browse the repository at this point in the history
In Qiskit#7862 we recently added a new vf2 post layout pass which is designed
to run after routing to improve the layout once we know there is at
least one isomorphic subgraph in the coupling graph for the interactions
in the circuit. In that PR we still ran vf2 post layout even if vf2
layout pass found a match. That's because the heuristic scoring of
layouts used for the vf2 layout and vf2 post layout passes were
different. Originally this difference was due to the the vf2 post
layout pass being intedended to run after the optimization loop where we
could guarantee the gates were in the target and exactly score the error
for each potential layout. But since the vf2 post layout was updated to
score a layout based on the gate counts for each qubit and the average
1q and 2q instruction error rates we can leverage this better heuristic
scoring in the vf2 layout pass. This commit updates the vf2 layout pass
to use the same heuristic and deduplicates some of the code between the
passes at the same time. Additionally, since the scoring heuristics are
the same the preset pass managers are updated to only run vf2 post
layout if vf2 layout didn't find a match. If vf2 layout finds a match
it's going to be the same as what vf2 post layout finds so there is no
need to run the vf2 post layout pass anymore.
  • Loading branch information
mtreinish committed Apr 27, 2022
1 parent 18b5a04 commit 30bebdf
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 161 deletions.
75 changes: 33 additions & 42 deletions qiskit/transpiler/passes/layout/vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
"""VF2Layout pass to find a layout using subgraph isomorphism"""
from enum import Enum
import logging
import random
import time

from retworkx import PyGraph, PyDiGraph, vf2_mapping
from retworkx import 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
from qiskit.transpiler.passes.layout import vf2_utils


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -80,8 +80,8 @@ def __init__(
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.
(whichever is larger) if no other limits are set. If set to a value <= 0 no
limit on the number of trials will be set.
target (Target): A target representing the backend device to run ``VF2Layout`` on.
If specified it will supersede a set value for ``properties`` and
``coupling_map``.
Expand All @@ -101,50 +101,32 @@ def __init__(
self.call_limit = call_limit
self.time_limit = time_limit
self.max_trials = max_trials
self.avg_error_map = None

def run(self, dag):
"""run the layout method"""
if self.coupling_map is None:
raise TranspilerError("coupling_map or target must be specified.")

qubits = dag.qubits
qubit_indices = {qubit: index for index, qubit in enumerate(qubits)}

interactions = []
for node in dag.op_nodes(include_directives=False):
len_args = len(node.qargs)
if len_args == 2:
interactions.append((qubit_indices[node.qargs[0]], qubit_indices[node.qargs[1]]))
if len_args >= 3:
self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.MORE_THAN_2Q
return

if self.strict_direction:
cm_graph = self.coupling_map.graph
im_graph = PyDiGraph(multigraph=False)
else:
cm_graph = self.coupling_map.graph.to_undirected()
im_graph = PyGraph(multigraph=False)

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

im_graph.add_nodes_from(range(len(qubits)))
im_graph.add_edges_from_no_data(interactions)
if self.avg_error_map is None:
self.avg_error_map = vf2_utils.build_average_error_map(
self.target, self.properties, self.coupling_map
)

result = vf2_utils.build_interaction_graph(dag, self.strict_direction)
if result is None:
self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.MORE_THAN_2Q
return
im_graph, im_graph_node_map, reverse_im_graph_node_map = result
cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(
self.coupling_map, self.seed, self.strict_direction
)
# 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:
# mapping in the search space if no other limits are set
if self.max_trials is None and self.call_limit is None and self.time_limit is None:
im_graph_edge_count = len(im_graph.edge_list())
cm_graph_edge_count = len(cm_graph.edge_list())
cm_graph_edge_count = len(self.coupling_map.graph.edge_list())
self.max_trials = max(im_graph_edge_count, cm_graph_edge_count) + 15

logger.debug("Running VF2 to find mappings")
Expand All @@ -164,14 +146,23 @@ def run(self, dag):
trials += 1
logger.debug("Running trial: %s", trials)
stop_reason = VF2LayoutStopReason.SOLUTION_FOUND
layout = Layout({qubits[im_i]: cm_nodes[cm_i] for cm_i, im_i in mapping.items()})
layout = Layout(
{reverse_im_graph_node_map[im_i]: cm_nodes[cm_i] for cm_i, im_i in mapping.items()}
)
# If the graphs have the same number of nodes we don't need to score or do multiple
# trials as the score heuristic currently doesn't weigh nodes based on gates on a
# qubit so the scores will always all be the same
if len(cm_graph) == len(im_graph):
chosen_layout = layout
break
layout_score = self._score_layout(layout)
layout_score = vf2_utils.score_layout(
self.avg_error_map,
layout,
im_graph_node_map,
reverse_im_graph_node_map,
im_graph,
self.strict_direction,
)
logger.debug("Trial %s has score %s", trials, layout_score)
if chosen_layout is None:
chosen_layout = layout
Expand All @@ -186,7 +177,7 @@ def run(self, dag):
)
chosen_layout = layout
chosen_layout_score = layout_score
if self.max_trials > 0 and trials >= self.max_trials:
if self.max_trials is not None and 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
Expand Down
125 changes: 27 additions & 98 deletions qiskit/transpiler/passes/layout/vf2_post_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@
"""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
import statistics

from retworkx import PyDiGraph, vf2_mapping, PyGraph

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


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -129,44 +127,15 @@ def run(self, dag):
raise TranspilerError(
"A target must be specified or a coupling map and properties must be provided"
)
if self.strict_direction:
im_graph = PyDiGraph(multigraph=False)
else:
if self.avg_error_map is None:
self.avg_error_map = self._build_average_error_map()
im_graph = PyGraph(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 not self.strict_direction and self.avg_error_map is None:
self.avg_error_map = vf2_utils.build_average_error_map(
self.target, self.properties, self.coupling_map
)
result = vf2_utils.build_interaction_graph(dag, self.strict_direction)
if result is None:
self.property_set["VF2PostLayout_stop_reason"] = VF2PostLayoutStopReason.MORE_THAN_2Q
return
im_graph, im_graph_node_map, reverse_im_graph_node_map = result

if self.target is not None:
if self.strict_direction:
Expand All @@ -186,21 +155,9 @@ def run(self, dag):
)
cm_nodes = list(cm_graph.node_indexes())
else:
if self.strict_direction:
cm_graph = self.coupling_map.graph
else:
cm_graph = self.coupling_map.graph.to_undirected(multigraph=False)
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
cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(
self.coupling_map, self.seed, self.strict_direction
)

logger.debug("Running VF2 to find post transpile mappings")
if self.target and self.strict_direction:
Expand Down Expand Up @@ -231,8 +188,13 @@ def run(self, dag):
initial_layout, im_graph_node_map, reverse_im_graph_node_map, im_graph
)
else:
chosen_layout_score = self._approx_score_layout(
initial_layout, im_graph_node_map, reverse_im_graph_node_map, im_graph
chosen_layout_score = vf2_utils.score_layout(
self.avg_error_map,
initial_layout,
im_graph_node_map,
reverse_im_graph_node_map,
im_graph,
self.strict_direction,
)
# Circuit not in basis so we have nothing to compare against return here
except KeyError:
Expand All @@ -257,8 +219,13 @@ def run(self, dag):
layout, im_graph_node_map, reverse_im_graph_node_map, im_graph
)
else:
layout_score = self._approx_score_layout(
layout, im_graph_node_map, reverse_im_graph_node_map, im_graph
layout_score = vf2_utils.score_layout(
self.avg_error_map,
layout,
im_graph_node_map,
reverse_im_graph_node_map,
im_graph,
self.strict_direction,
)
logger.debug("Trial %s has score %s", trials, layout_score)
if layout_score < chosen_layout_score:
Expand Down Expand Up @@ -301,20 +268,6 @@ def run(self, dag):

self.property_set["VF2PostLayout_stop_reason"] = stop_reason

def _approx_score_layout(self, layout, bit_map, reverse_bit_map, im_graph):
bits = layout.get_virtual_bits()
fidelity = 1
for bit, node_index in bit_map.items():
gate_count = sum(im_graph[node_index].values())
fidelity *= (1 - self.avg_error_map[(bits[bit],)]) ** gate_count
for edge in im_graph.edge_index_map().values():
gate_count = sum(edge[2].values())
qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]])
if qargs not in self.avg_error_map:
qargs = (qargs[1], qargs[0])
fidelity *= (1 - self.avg_error_map[qargs]) ** gate_count
return 1 - fidelity

def _score_layout(self, layout, bit_map, reverse_bit_map, im_graph):
bits = layout.get_virtual_bits()
fidelity = 1
Expand Down Expand Up @@ -358,27 +311,3 @@ def _score_layout(self, layout, bit_map, reverse_bit_map, im_graph):
except BackendPropertyError:
pass
return 1 - fidelity

def _build_average_error_map(self):
avg_map = {}
if self.target is not None:
for qargs in self.target.qargs:
qarg_error = 0.0
count = 0
for op in self.target.operation_names_for_qargs(qargs):
inst_props = self.target[op].get(qargs, None)
if inst_props is not None and inst_props.error is not None:
count += 1
qarg_error += inst_props.error
avg_map[qargs] = qarg_error / count
else:
errors = defaultdict(list)
for qubit in range(len(self.properties.qubits)):
errors[(qubit,)].append(self.properties.readout_error(qubit))
for gate in self.properties.gates:
qubits = tuple(gate.qubits)
for param in gate.parameters:
if param.name == "gate_error":
errors[qubits].append(param.value)
avg_map = {k: statistics.mean(v) for k, v in errors.items()}
return avg_map
Loading

0 comments on commit 30bebdf

Please sign in to comment.