Skip to content

Commit

Permalink
Add MinimumPoint transpiler pass (Qiskit#9612)
Browse files Browse the repository at this point in the history
* Add MinimumPoint transpiler pass

This commit adds a new transpiler pass MinimumPoint which is used to
find a local minimum point from the property set between executions of
the pass. This is similar to the existing FixedPoint pass but will find
the minimum fixed point over the past n exeuctions as opposed to finding
a when two subsequent exectuions are at the same point. This is then
used in optimization level 3 because the 2q unitary synthesis
optimization that is part of the optimization loop can occasionally get
stuck oscillating between multiple different equivalent decompositions
which prevents the fixed point condition from ever being reached. By
checking that we've reached the minimum over the last 5 executions we
ensure we're reaching an exit condition in this situation.

Fixes Qiskit#5832
Fixes Qiskit#9177

(the underlying cause of the optimization loop's inability to converge
at optimization level 3 is not fixed here, there is still a root cause
of instability in UnitarySynthesis this just changes the loop exit
condition so we're never stuck in an infinte loop)

* Doc copy-paste error cleanups

* Rework logic to track state with a dataclass

This commit reworks the logic of the pass to track the state via a
dataclass instead of using separate property set fields. This cleans up
the code for dealing with checking the current state relative to earlier
iterations by making the access just attributes instead of secret
strings. At the same time the tests were updated to handle this new data
structure comments were added to better explain the logic flow being
tested.

* Fix doc typo

* Apply suggestions from code review

Co-authored-by: Jake Lishman <[email protected]>

* Update slots syntax for dataclass to be compatible with Python < 3.10

* Update property set check in tests

---------

Co-authored-by: Jake Lishman <[email protected]>
  • Loading branch information
2 people authored and king-p3nguin committed May 22, 2023
1 parent 6dd7b3b commit 6ac2b04
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 8 deletions.
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
RemoveFinalMeasurements
DAGFixedPoint
FixedPoint
MinimumPoint
ContainsInstruction
GatesInBasis
ConvertConditionsToIfOps
Expand Down Expand Up @@ -283,6 +284,7 @@
from .utils import MergeAdjacentBarriers
from .utils import DAGFixedPoint
from .utils import FixedPoint
from .utils import MinimumPoint
from .utils import Error
from .utils import RemoveBarriers
from .utils import ContainsInstruction
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .contains_instruction import ContainsInstruction
from .gates_basis import GatesInBasis
from .convert_conditions_to_if_ops import ConvertConditionsToIfOps
from .minimum_point import MinimumPoint

# Utility functions
from . import control_flow
118 changes: 118 additions & 0 deletions qiskit/transpiler/passes/utils/minimum_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023
#
# 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.

"""Check if the DAG has reached a relative semi-stable point over previous runs."""

from copy import deepcopy
from dataclasses import dataclass
import math
from typing import Tuple

from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.transpiler.basepasses import TransformationPass


class MinimumPoint(TransformationPass):
"""Check if the DAG has reached a relative semi-stable point over previous runs
This pass is similar to the :class:`~.FixedPoint` transpiler pass and is intended
primarily to be used to set a loop break condition in the property set.
However, unlike the :class:`~.FixedPoint` class which only sets the
condition if 2 consecutive runs have the same value property set value
this pass is designed to find a local minimum and use that instead. This
pass is designed for an optimization loop where a fixed point may never
get reached (for example if synthesis is used and there are multiple
equivalent outputs for some cases).
This pass will track the state of fields in the property set over its past
executions and set a boolean field when either a fixed point is reached
over the backtracking depth or selecting the minimum value found if the
backtracking depth is reached. To do this it stores a deep copy of the
current minimum DAG in the property set and when ``backtrack_depth`` number
of executions is reached since the last minimum the output dag is set to
that copy of the earlier minimum.
Fields used by this pass in the property set are (all relative to the ``prefix``
argument):
* ``{prefix}_minimum_point_state`` - Used to track the state of the minimpoint search
* ``{prefix}_minimum_point`` - This value gets set to ``True`` when either a fixed point
is reached over the ``backtrack_depth`` executions, or ``backtrack_depth`` was exceeded
and an earlier minimum is restored.
"""

def __init__(self, property_set_list, prefix, backtrack_depth=5):
"""Initialize an instance of this pass
Args:
property_set_list (list): A list of property set keys that will
be used to evaluate the local minimum. The values of these
property set keys will be used as a tuple for comparison
prefix (str): The prefix to use for the property set key that is used
for tracking previous evaluations
backtrack_depth (int): The maximum number of entries to store. If
this number is reached and the next iteration doesn't have
a decrease in the number of values the minimum of the previous
n will be set as the output dag and ``minimum_point`` will be set to
``True`` in the property set
"""
super().__init__()
self.property_set_list = property_set_list

self.backtrack_name = f"{prefix}_minimum_point_state"
self.minimum_reached = f"{prefix}_minimum_point"
self.backtrack_depth = backtrack_depth

def run(self, dag):
"""Run the MinimumPoint pass on `dag`."""
score = tuple(self.property_set[x] for x in self.property_set_list)
state = self.property_set[self.backtrack_name]

# The pass starts at None and the first iteration doesn't set a real
# score so the overall loop is treated as a do-while to ensure we have
# at least 2 iterations.
if state is None:
self.property_set[self.backtrack_name] = _MinimumPointState(
dag=None, score=(math.inf,) * len(self.property_set_list), since=0
)
# If the score of this execution is worse than the previous execution
# increment 'since' since we have not found a new minimum point
elif score > state.score:
state.since += 1
if state.since == self.backtrack_depth:
self.property_set[self.minimum_reached] = True
return self.property_set[self.backtrack_name].dag
# If the score has decreased (gotten better) then this iteration is
# better performing and this iteration should be the new minimum state.
# So update the state to be this iteration and reset counter
elif score < state.score:
state.since = 1
state.score = score
state.dag = deepcopy(dag)
# If the current execution is equal to the previous minimum value then
# we've reached an equivalent fixed point and we should use this iteration's
# dag as the output and set the property set flag that we've found a minimum
# point.
elif score == state.score:
self.property_set[self.minimum_reached] = True
return dag

return dag


@dataclass
class _MinimumPointState:
__slots__ = ("dag", "score", "since")

dag: DAGCircuit
score: Tuple[float, ...]
since: int
17 changes: 10 additions & 7 deletions qiskit/transpiler/preset_passmanagers/level3.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from qiskit.transpiler.passes import DenseLayout
from qiskit.transpiler.passes import NoiseAdaptiveLayout
from qiskit.transpiler.passes import SabreLayout
from qiskit.transpiler.passes import FixedPoint
from qiskit.transpiler.passes import MinimumPoint
from qiskit.transpiler.passes import Depth
from qiskit.transpiler.passes import Size
from qiskit.transpiler.passes import RemoveResetInZeroState
Expand Down Expand Up @@ -151,11 +151,14 @@ def _vf2_match_not_found(property_set):

# 8. Optimize iteratively until no more change in depth. Removes useless gates
# after reset and before measure, commutes gates and optimizes contiguous blocks.
_depth_check = [Depth(recurse=True), FixedPoint("depth")]
_size_check = [Size(recurse=True), FixedPoint("size")]
_minimum_point_check = [
Depth(recurse=True),
Size(recurse=True),
MinimumPoint(["depth", "size"], "optimization_loop"),
]

def _opt_control(property_set):
return (not property_set["depth_fixed_point"]) or (not property_set["size_fixed_point"])
return not property_set["optimization_loop_minimum_point"]

_opt = [
Collect2qBlocks(),
Expand Down Expand Up @@ -249,7 +252,7 @@ def _unroll_condition(property_set):
ConditionalController(unroll, condition=_unroll_condition),
]

optimization.append(_depth_check + _size_check)
optimization.append(_minimum_point_check)
if (coupling_map and not coupling_map.is_symmetric) or (
target is not None and target.get_non_global_operation_names(strict_direction=True)
):
Expand All @@ -261,13 +264,13 @@ def _unroll_condition(property_set):
]
if optimization is not None:
optimization.append(
_opt + _unroll_if_out_of_basis + _depth_check + _size_check,
_opt + _unroll_if_out_of_basis + _minimum_point_check,
do_while=_opt_control,
)
else:
pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True)
optimization.append(
_opt + _unroll_if_out_of_basis + _depth_check + _size_check, do_while=_opt_control
_opt + _unroll_if_out_of_basis + _minimum_point_check, do_while=_opt_control
)
else:
optimization = plugin_manager.get_passmanager_stage(
Expand Down
10 changes: 10 additions & 0 deletions releasenotes/notes/add-minimum-point-pass-09cf9a9eec86fd48.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
features:
- |
Added a new transpiler pass, :class:`~.MinimumPoint` which is used primarily as
a pass to check a loop condition in a :class:`~.PassManager`. This pass will
track the state of fields in the property set over its past executions and set
a boolean field when either a fixed point is reached over the backtracking depth
or selecting the minimum value found if the backtracking depth is reached. This
is an alternative to the :class:`~.FixedPoint` which simply checks for a fixed
value in a property set field between subsequent executions.
2 changes: 1 addition & 1 deletion test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1364,7 +1364,7 @@ def test_transpile_optional_registers(self, optimization_level):
out = transpile(qc, FakeBoeblingen(), optimization_level=optimization_level)

self.assertEqual(len(out.qubits), FakeBoeblingen().configuration().num_qubits)
self.assertEqual(out.clbits, clbits)
self.assertEqual(len(out.clbits), len(clbits))

@data(0, 1, 2, 3)
def test_translate_ecr_basis(self, optimization_level):
Expand Down
Loading

0 comments on commit 6ac2b04

Please sign in to comment.