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

Modify preset pass managers for control-flow support #8830

Merged
merged 12 commits into from
Oct 7, 2022
Merged
21 changes: 19 additions & 2 deletions qiskit/transpiler/passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,12 @@ def _normalize_passes(
passes = [passes]
for pass_ in passes:
if isinstance(pass_, FlowController):
# Normalize passes in nested FlowController
PassManager._normalize_passes(pass_.passes)
# Normalize passes in nested FlowController.
# TODO: Internal renormalisation should be the responsibility of the
# `FlowController`, but the separation between `FlowController`,
# `RunningPassManager` and `PassManager` is so muddled right now, it would be better
# to do this as part of more top-down refactoring. ---Jake, 2022-10-03.
pass_.passes = PassManager._normalize_passes(pass_.passes)
elif not isinstance(pass_, BasePass):
raise TranspilerError(
"%s is not a BasePass or FlowController instance " % pass_.__class__
Expand Down Expand Up @@ -322,6 +326,19 @@ def passes(self) -> List[Dict[str, BasePass]]:
ret.append(item)
return ret

def to_flow_controller(self) -> FlowController:
"""Linearize this manager into a single :class:`.FlowController`, so that it can be nested
inside another :class:`.PassManager`."""
return FlowController.controller_factory(
[
FlowController.controller_factory(
pass_set["passes"], None, **pass_set["flow_controllers"]
)
for pass_set in self._pass_sets
],
None,
)


class StagedPassManager(PassManager):
"""A Pass manager pipeline built up of individual stages
Expand Down
115 changes: 106 additions & 9 deletions qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

"""Common preset passmanager generators."""

import collections
from typing import Optional

from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel

from qiskit.transpiler.passmanager import PassManager
from qiskit.transpiler.passes import Error
from qiskit.transpiler.passes import Unroller
from qiskit.transpiler.passes import BasisTranslator
from qiskit.transpiler.passes import UnrollCustomDefinitions
Expand Down Expand Up @@ -51,6 +53,105 @@
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout

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

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

# Any method neither known good nor known bad (i.e. not a Terra-internal pass) is passed through
# without error, since it is being supplied by a plugin and we don't have any knowledge of these.
_CONTROL_FLOW_STATES = {
"layout_method": _ControlFlowState(
working={"trivial", "dense"}, not_working={"sabre", "noise_adaptive"}
),
"routing_method": _ControlFlowState(
working={"none", "stochastic"}, not_working={"sabre", "lookahead", "basic", "toqm"}
),
# 'synthesis' is not a supported translation method because of the block-collection passes
# involved; we currently don't have a neat way to pass the information about nested blocks - the
# `UnitarySynthesis` pass itself is control-flow aware.
"translation_method": _ControlFlowState(
working={"translator", "unroller"}, not_working={"synthesis"}
),
"optimization_method": _ControlFlowState(working=set(), not_working=set()),
"scheduling_method": _ControlFlowState(working=set(), not_working={"alap", "asap"}),
}


def _has_control_flow(property_set):
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)


def generate_control_flow_options_check(
layout_method=None,
routing_method=None,
translation_method=None,
optimization_method=None,
scheduling_method=None,
):
"""Generate a pass manager that, when run on a DAG that contains control flow, fails with an
error message explaining the invalid options, and what could be used instead.

Returns:
PassManager: a pass manager that populates the ``contains_x`` properties for each of the
control-flow operations, and raises an error if any of the given options do not support
control flow, but a circuit with control flow is given.
"""

bad_options = []
message = "Some options cannot be used with control flow."
for stage, given in [
("layout", layout_method),
("routing", routing_method),
("translation", translation_method),
("optimization", optimization_method),
("scheduling", scheduling_method),
]:
option = stage + "_method"
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't we just use "layout_method" etc for L99-L103? Besides the _CONTROL_FLOW_STATES lookup this is just used for the error message and *_method works fine for the error message too.

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 think I did this just because one of the possible error messages is "the entire {stage} stage is not supported", and that's weird with scheduling_method.

method_states = _CONTROL_FLOW_STATES[option]
if given is not None and given in method_states.not_working:
if method_states.working:
message += (
f" Got {option}='{given}', but valid values are {list(method_states.working)}."
)
else:
message += (
f" Got {option}='{given}', but the entire {stage} stage is not supported."
)
bad_options.append(option)
out = PassManager()
out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False))
if not bad_options:
return out
out.append(Error(message), condition=_has_control_flow)
return out


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(Error(message), condition=_has_control_flow)
return out


def if_has_control_flow_else(if_present, if_absent):
"""Generate a pass manager that will run the passes in ``if_present`` if the given circuit
has control-flow operations in it, and those in ``if_absent`` if it doesn't."""
if isinstance(if_present, PassManager):
if_present = if_present.to_flow_controller()
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(if_present, condition=_has_control_flow)
out.append(if_absent, condition=_without_control_flow)
return out


def generate_unroll_3q(
target,
Expand Down Expand Up @@ -111,14 +212,10 @@ def generate_embed_passmanager(coupling_map):
return PassManager([FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()])


def _trivial_not_perfect(property_set):
# Verify that a trivial layout is perfect. If trivial_layout_score > 0
# the layout is not perfect. The layout is unconditionally set by trivial
# layout so we need to clear it before contuing.
return (
property_set["trivial_layout_score"] is not None
and property_set["trivial_layout_score"] != 0
)
def _layout_not_perfect(property_set):
"""Return ``True`` if the first attempt at layout has been checked and found to be imperfect.
In this case, perfection means "does not require any swap routing"."""
return property_set["is_swap_mapped"] is not None and not property_set["is_swap_mapped"]


def _apply_post_layout_condition(property_set):
Expand Down Expand Up @@ -170,7 +267,7 @@ def generate_routing_passmanager(
def _run_post_layout_condition(property_set):
# If we check trivial layout and the found trivial layout was not perfect also
# ensure VF2 initial layout was not used before running vf2 post layout
if not check_trivial or _trivial_not_perfect(property_set):
if not check_trivial or _layout_not_perfect(property_set):
vf2_stop_reason = property_set["VF2Layout_stop_reason"]
if vf2_stop_reason is None or vf2_stop_reason != VF2LayoutStopReason.SOLUTION_FOUND:
return True
Expand Down
13 changes: 10 additions & 3 deletions qiskit/transpiler/preset_passmanagers/level0.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,19 @@ def _choose_layout_condition(property_set):
sched = plugin_manager.get_passmanager_stage(
"scheduling", scheduling_method, pass_manager_config, optimization_level=0
)
init = common.generate_control_flow_options_check(
layout_method=layout_method,
routing_method=routing_method,
translation_method=translation_method,
optimization_method=optimization_method,
scheduling_method=scheduling_method,
)
if init_method is not None:
init = plugin_manager.get_passmanager_stage(
init += plugin_manager.get_passmanager_stage(
"init", init_method, pass_manager_config, optimization_level=0
)
else:
init = unroll_3q
elif unroll_3q is not None:
init += unroll_3q
optimization = None
if optimization_method is not None:
optimization = plugin_manager.get_passmanager_stage(
Expand Down
63 changes: 41 additions & 22 deletions qiskit/transpiler/preset_passmanagers/level1.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from qiskit.transpiler.passes import Depth
from qiskit.transpiler.passes import Size
from qiskit.transpiler.passes import Optimize1qGatesDecomposition
from qiskit.transpiler.passes import Layout2qDistance
from qiskit.transpiler.passes import CheckMap
from qiskit.transpiler.passes import GatesInBasis
from qiskit.transpiler.preset_passmanagers import common
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason
Expand Down Expand Up @@ -72,8 +72,10 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa
coupling_map = pass_manager_config.coupling_map
initial_layout = pass_manager_config.initial_layout
init_method = pass_manager_config.init_method
layout_method = pass_manager_config.layout_method or "sabre"
routing_method = pass_manager_config.routing_method or "sabre"
# Unlike other presets, the layout and routing defaults aren't set here because they change
# based on whether the input circuit has control flow.
layout_method = pass_manager_config.layout_method
routing_method = pass_manager_config.routing_method
translation_method = pass_manager_config.translation_method or "translator"
optimization_method = pass_manager_config.optimization_method
scheduling_method = pass_manager_config.scheduling_method
Expand All @@ -93,16 +95,10 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa
def _choose_layout_condition(property_set):
return not property_set["layout"]

def _trivial_not_perfect(property_set):
# Verify that a trivial layout is perfect. If trivial_layout_score > 0
# the layout is not perfect. The layout is unconditionally set by trivial
# layout so we need to clear it before contuing.
if (
property_set["trivial_layout_score"] is not None
and property_set["trivial_layout_score"] != 0
):
return True
return False
def _layout_not_perfect(property_set):
"""Return ``True`` if the first attempt at layout has been checked and found to be
imperfect. In this case, perfection means "does not require any swap routing"."""
return property_set["is_swap_mapped"] is not None and not property_set["is_swap_mapped"]
jakelishman marked this conversation as resolved.
Show resolved Hide resolved

# Use a better layout on densely connected qubits, if circuit needs swaps
def _vf2_match_not_found(property_set):
Expand All @@ -122,10 +118,7 @@ def _vf2_match_not_found(property_set):
_choose_layout_0 = (
[]
if pass_manager_config.layout_method
else [
TrivialLayout(coupling_map),
Layout2qDistance(coupling_map, property_name="trivial_layout_score"),
]
else [TrivialLayout(coupling_map), CheckMap(coupling_map)]
)

_choose_layout_1 = (
Expand All @@ -150,6 +143,11 @@ def _vf2_match_not_found(property_set):
_improve_layout = SabreLayout(
coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5
)
elif layout_method is None:
_improve_layout = common.if_has_control_flow_else(
DenseLayout(coupling_map, backend_properties, target=target),
SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5),
).to_flow_controller()

toqm_pass = False
routing_pm = None
Expand Down Expand Up @@ -185,6 +183,20 @@ def _vf2_match_not_found(property_set):
check_trivial=True,
use_barrier_before_measurement=not toqm_pass,
)
elif routing_method is None:
_stochastic_routing = plugin_manager.get_passmanager_stage(
"routing",
"stochastic",
pass_manager_config,
optimization_level=1,
)
_sabre_routing = plugin_manager.get_passmanager_stage(
"routing",
"sabre",
pass_manager_config,
optimization_level=1,
)
routing_pm = common.if_has_control_flow_else(_stochastic_routing, _sabre_routing)
else:
routing_pm = plugin_manager.get_passmanager_stage(
"routing",
Expand Down Expand Up @@ -214,15 +226,15 @@ def _opt_control(property_set):
unitary_synthesis_plugin_config,
hls_config,
)
if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}:
if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre", None}:
layout = plugin_manager.get_passmanager_stage(
"layout", layout_method, pass_manager_config, optimization_level=1
)
else:
layout = PassManager()
layout.append(_given_layout)
layout.append(_choose_layout_0, condition=_choose_layout_condition)
layout.append(_choose_layout_1, condition=_trivial_not_perfect)
layout.append(_choose_layout_1, condition=_layout_not_perfect)
layout.append(_improve_layout, condition=_vf2_match_not_found)
layout += common.generate_embed_passmanager(coupling_map)

Expand Down Expand Up @@ -289,12 +301,19 @@ def _unroll_condition(property_set):
sched = plugin_manager.get_passmanager_stage(
"scheduling", scheduling_method, pass_manager_config, optimization_level=1
)
init = common.generate_control_flow_options_check(
layout_method=layout_method,
routing_method=routing_method,
translation_method=translation_method,
optimization_method=optimization_method,
scheduling_method=scheduling_method,
)
if init_method is not None:
init = plugin_manager.get_passmanager_stage(
init += plugin_manager.get_passmanager_stage(
"init", init_method, pass_manager_config, optimization_level=1
)
else:
init = unroll_3q
elif unroll_3q is not None:
init += unroll_3q

return StagedPassManager(
init=init,
Expand Down
9 changes: 6 additions & 3 deletions qiskit/transpiler/preset_passmanagers/level2.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,15 @@ def _unroll_condition(property_set):
sched = plugin_manager.get_passmanager_stage(
"scheduling", scheduling_method, pass_manager_config, optimization_level=2
)
init = common.generate_error_on_control_flow(
"The optimizations in optimization_level=2 do not yet support control flow."
)
Comment on lines +265 to +267
Copy link
Member

Choose a reason for hiding this comment

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

We technically could get a level2 pm that given the assumptions made in common.py is runnable with control flow. But it'd basically be layout_method=dense, routing_method=stochastic, optimization=plugin. It seems to be fairly unlikely in practice so this is fine I think.

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 was treating us as "supporting" an optimisation level if the defaults will work - for O2, you have to override everything that actually makes it O2 back to what's in O1, so I think it's better to reject optimization_level=2 out-of-hand.

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 guess at O1, now that routing and layout are both Sabre by default there's an argument that we're kind of just doing that internally too, I suppose. I was still thinking in terms of the old O1 that had dense/stochastic as its default.

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine with this for now, to a certain degree if people want to get more complicated maybe they should just be using custom pass managers. We just will have to document clearly in the release note that optimization_level 2 and 3 will error regardless of options with a control flow circuit.

if init_method is not None:
init = plugin_manager.get_passmanager_stage(
init += plugin_manager.get_passmanager_stage(
"init", init_method, pass_manager_config, optimization_level=2
)
else:
init = unroll_3q
elif unroll_3q is not None:
init += unroll_3q

return StagedPassManager(
init=init,
Expand Down
9 changes: 6 additions & 3 deletions qiskit/transpiler/preset_passmanagers/level3.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,15 @@ def _opt_control(property_set):
]

# Build pass manager
init = common.generate_error_on_control_flow(
"The optimizations in optimization_level=3 do not yet support control flow."
)
if init_method is not None:
init = plugin_manager.get_passmanager_stage(
"init", init_method, pass_manager_config, optimization_level=3
init += plugin_manager.get_passmanager_stage(
"init", init_method, pass_manager_config, optimization_level=2
)
else:
init = common.generate_unroll_3q(
init += common.generate_unroll_3q(
target,
basis_gates,
approximation_degree,
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/runningpassmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def _do_pass(self, pass_, dag, options):
pass_.do_while = partial(pass_.do_while, self.fenced_property_set)

for _pass in pass_:
self._do_pass(_pass, dag, pass_.options)
dag = self._do_pass(_pass, dag, pass_.options)
else:
raise TranspilerError(
"Expecting type BasePass or FlowController, got %s." % type(pass_)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
fixes:
- |
Nesting a :class:`.FlowController` inside another in a :class:`.PassManager`
could previously cause some transpiler passes to become "forgotten" during
transpilation, if the passes returned a new :class:`.DAGCircuit` rather than
mutating their input. Nested :class:`.FlowController`\ s will now affect
the transpilation correctly.
Loading