Skip to content

Commit

Permalink
Speed up synthesis passes when there is nothing to synthesize (#10788)
Browse files Browse the repository at this point in the history
* Speed up synthesis passes when there is nothing to synthesize

This commit makes a small optimization to the UnitarySynthesis and
HighLevelSynthesis synthesis passes when there are no gates to
synthesize. Previously these passes would iterate over the entire DAG
and check every gate to determine that None needed to be synthesized.
While this is relatively quick for very large circuits this can take
100s of ms. However, this was wasted work because the DAG carries a
dictionary of op names in the circuit so we can determine in constant
time whether any of the operations to synthesize are present up front
and skip the bulk of the iteration. To improve the impact here the
DAGCircuit.count_ops() method is updated to skip the recursion if no
control flow operations are present. Otherwise we'd still be iterating
over the DAG (in a faster manner) which would limit the improvement
from this commit. This isn't the best approach for that as it uses a
hardcoded list of control flow operations which doesn't seem super
general purpose, but as this hard coded set of instructions is
already used elsewhere in the DAGCircuit this isn't making the
situation any worse. If/when we expand recursive objects this can be
revisited (and we'll likely not be able to avoid the iteration in
count ops). With these changes the time these passes take when there are
no operations to synthesize only takes ~5-15 microseconds.

A similar optimization should be considered for UnrollCustomDefinitions
as this takes an even longer amount of time to iterate when there is
nothing to translate. However, it wasn't done as part of this PR as to
check the equivalence library has an entry we need to work with gate
objects (well actually name and number of qubits as that's all that's
used from the object for a lookup) and the name from the op count
dictionary is not sufficient for a lookup. So this will need to be
handled independently in a separate PR.

* Remove duplicated check in unitary synthesis

* Unify hardcoded sets of control flow op names
  • Loading branch information
mtreinish authored Sep 7, 2023
1 parent 3059193 commit a02e0a3
Show file tree
Hide file tree
Showing 5 changed files with 24 additions and 19 deletions.
3 changes: 3 additions & 0 deletions qiskit/circuit/controlflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
from .while_loop import WhileLoopOp
from .for_loop import ForLoopOp
from .switch_case import SwitchCaseOp, CASE_DEFAULT


CONTROL_FLOW_OP_NAMES = frozenset(("for_loop", "while_loop", "if_else", "switch_case"))
12 changes: 4 additions & 8 deletions qiskit/dagcircuit/dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
SwitchCaseOp,
_classical_resource_map,
)
from qiskit.circuit.controlflow import condition_resources, node_resources
from qiskit.circuit.controlflow import condition_resources, node_resources, CONTROL_FLOW_OP_NAMES
from qiskit.circuit.quantumregister import QuantumRegister, Qubit
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.circuit.gate import Gate
Expand Down Expand Up @@ -916,9 +916,7 @@ def size(self, *, recurse: bool = False):
"""
length = len(self._multi_graph) - 2 * len(self._wires)
if not recurse:
if any(
x in self._op_names for x in ("for_loop", "while_loop", "if_else", "switch_case")
):
if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES):
raise DAGCircuitError(
"Size with control flow is ambiguous."
" You may use `recurse=True` to get a result,"
Expand Down Expand Up @@ -980,9 +978,7 @@ def weight_fn(_source, target, _edge):
return node_lookup.get(target, 1)

else:
if any(
x in self._op_names for x in ("for_loop", "while_loop", "if_else", "switch_case")
):
if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES):
raise DAGCircuitError(
"Depth with control flow is ambiguous."
" You may use `recurse=True` to get a result,"
Expand Down Expand Up @@ -1978,7 +1974,7 @@ def count_ops(self, *, recurse: bool = True):
Returns:
Mapping[str, int]: a mapping of operation names to the number of times it appears.
"""
if not recurse:
if not recurse or not CONTROL_FLOW_OP_NAMES.intersection(self._op_names):
return self._op_names.copy()

# pylint: disable=cyclic-import
Expand Down
4 changes: 4 additions & 0 deletions qiskit/transpiler/passes/synthesis/high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
Raises:
TranspilerError: when the specified synthesis method is not available.
"""
# If there aren't any high level operations to synthesize return fast
hls_names = set(self.hls_plugin_manager.plugins_by_op)
if not hls_names.intersection(dag.count_ops()):
return dag
for node in dag.op_nodes():
if node.name in self.hls_config.methods.keys():
# the operation's name appears in the user-provided config,
Expand Down
8 changes: 5 additions & 3 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,12 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
"""
if self.method != "default" and self.method not in self.plugins.ext_plugins:
raise TranspilerError("Specified method: %s not found in plugin list" % self.method)
# Return fast if we have no synth gates (ie user specified an empty
# list or the synth gates are all in the basis
if not self._synth_gates:

# If there aren't any gates to synthesize in the circuit we can skip all the iteration
# and just return.
if not set(self._synth_gates).intersection(dag.count_ops()):
return dag

if self.plugins:
plugin_method = self.plugins.ext_plugins[self.method].obj
else:
Expand Down
16 changes: 8 additions & 8 deletions qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Optional

from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel
from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES
from qiskit.utils.deprecation import deprecate_func

from qiskit.transpiler.passmanager import PassManager
Expand Down Expand Up @@ -53,7 +54,6 @@
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout

_CONTROL_FLOW_OP_NAMES = {"for_loop", "if_else", "while_loop", "switch_case"}

_ControlFlowState = collections.namedtuple("_ControlFlowState", ("working", "not_working"))

Expand All @@ -76,22 +76,22 @@


def _has_control_flow(property_set):
return any(property_set[f"contains_{x}"] for x in _CONTROL_FLOW_OP_NAMES)
return any(property_set[f"contains_{x}"] for x in CONTROL_FLOW_OP_NAMES)


def _without_control_flow(property_set):
return not any(property_set[f"contains_{x}"] for x in _CONTROL_FLOW_OP_NAMES)
return not any(property_set[f"contains_{x}"] for x in CONTROL_FLOW_OP_NAMES)


class _InvalidControlFlowForBackend:
# Explicitly stateful closure to allow pickling.

def __init__(self, basis_gates=(), target=None):
if target is not None:
self.unsupported = [op for op in _CONTROL_FLOW_OP_NAMES if op not in target]
self.unsupported = [op for op in CONTROL_FLOW_OP_NAMES if op not in target]
else:
basis_gates = set(basis_gates) if basis_gates is not None else set()
self.unsupported = [op for op in _CONTROL_FLOW_OP_NAMES if op not in basis_gates]
self.unsupported = [op for op in CONTROL_FLOW_OP_NAMES if op not in basis_gates]

def message(self, property_set):
"""Create an error message for the given property set."""
Expand Down Expand Up @@ -147,7 +147,7 @@ def generate_control_flow_options_check(
)
bad_options.append(option)
out = PassManager()
out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False))
out.append(ContainsInstruction(CONTROL_FLOW_OP_NAMES, recurse=False))
if bad_options:
out.append(Error(message), condition=_has_control_flow)
backend_control = _InvalidControlFlowForBackend(basis_gates, target)
Expand All @@ -159,7 +159,7 @@ def generate_error_on_control_flow(message):
"""Get a pass manager that always raises an error if control flow is present in a given
circuit."""
out = PassManager()
out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False))
out.append(ContainsInstruction(CONTROL_FLOW_OP_NAMES, recurse=False))
out.append(Error(message), condition=_has_control_flow)
return out

Expand All @@ -172,7 +172,7 @@ def if_has_control_flow_else(if_present, if_absent):
if isinstance(if_absent, PassManager):
if_absent = if_absent.to_flow_controller()
out = PassManager()
out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False))
out.append(ContainsInstruction(CONTROL_FLOW_OP_NAMES, recurse=False))
out.append(if_present, condition=_has_control_flow)
out.append(if_absent, condition=_without_control_flow)
return out
Expand Down

0 comments on commit a02e0a3

Please sign in to comment.