Skip to content

Commit

Permalink
Pass manager refactoring: add passmanager module (#10124)
Browse files Browse the repository at this point in the history
* Add passmanager module and reorganize transpiler module

* Documentation update

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* Readd qiskit.transpiler.propertyset

* Documentation update

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com>

* BasePass -> GenericPass to avoid name overlap

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 23, 2023
1 parent 924702e commit fbd64d9
Show file tree
Hide file tree
Showing 16 changed files with 1,154 additions and 480 deletions.
6 changes: 6 additions & 0 deletions docs/apidocs/passmanager.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. _qiskit-passmanager:

.. automodule:: qiskit.passmanager
:no-members:
:no-inherited-members:
:no-special-members:
1 change: 1 addition & 0 deletions docs/apidocs/terra.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Qiskit Terra API Reference
assembler
dagcircuit
extensions
passmanager
providers_basicaer
providers
providers_fake_provider
Expand Down
106 changes: 106 additions & 0 deletions qiskit/passmanager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# 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.

"""
=======================================
Passmanager (:mod:`qiskit.passmanager`)
=======================================
.. currentmodule:: qiskit.passmanager
Overview
========
Qiskit pass manager is somewhat inspired by the `LLVM compiler <https://llvm.org/>`_,
but it is designed to take Qiskit object as an input instead of plain source code.
The pass manager converts the input object into an intermediate representation (IR),
and it can be optimized and get lowered with a variety of transformations over multiple passes.
This representation must be preserved throughout the transformation.
The passes may consume the hardware constraints that Qiskit backend may provide.
Finally, the IR is converted back to some Qiskit object.
Note that the input type and output type don't need to match.
Execution of passes is managed by the :class:`.FlowController`,
which is initialized with a set of transform and analysis passes and provides an iterator of them.
This iterator can be conditioned on the :class:`.PropertySet`, which is a namespace
storing the intermediate data necessary for the transformation.
A pass has read and write access to the property set namespace,
and the stored data is shared among scheduled passes.
The :class:`BasePassManager` provides a user interface to build and execute transform passes.
It internally spawns a :class:`BasePassRunner` instance to apply transforms to
the input object. In this sense, the pass manager itself is unaware of the
underlying IR, but it is indirectly tied to a particular IR through the pass runner class.
The responsibilities of the pass runner are the following:
* Defining the input type / pass manager IR / output type.
* Converting an input object to a particular pass manager IR.
* Preparing own property set namespace.
* Running scheduled flow controllers to apply a series of transformations to the IR.
* Converting the IR back to an output object.
A single pass runner always takes a single input object and returns a single output object.
Parallelism for multiple input objects is supported by the :class:`BasePassManager` by
broadcasting the pass runner via the :mod:`qiskit.tools.parallel_map` function.
The base class :class:`BasePassRunner` doesn't define any associated type by itself,
and a developer needs to implement a subclass for a particular object type to optimize.
This `veil of ignorance` allows us to choose the most efficient data representation
for a particular optimization task, while we can reuse the pass flow control machinery
for different input and output types.
Base classes
------------
.. autosummary::
:toctree: ../stubs/
BasePassRunner
BasePassManager
Flow controllers
----------------
.. autosummary::
:toctree: ../stubs/
FlowController
ConditionalController
DoWhileController
PropertySet
-----------
.. autosummary::
:toctree: ../stubs/
PropertySet
Exceptions
----------
.. autosummary::
:toctree: ../stubs/
PassManagerError
"""

from .passrunner import BasePassRunner
from .passmanager import BasePassManager
from .flow_controllers import FlowController, ConditionalController, DoWhileController
from .base_pass import GenericPass
from .propertyset import PropertySet
from .exceptions import PassManagerError
80 changes: 80 additions & 0 deletions qiskit/passmanager/base_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.

"""Metaclass of Qiskit pass manager pass."""

from abc import abstractmethod
from collections.abc import Hashable
from inspect import signature
from typing import Any

from .propertyset import PropertySet


class MetaPass(type):
"""Metaclass for transpiler passes.
Enforces the creation of some fields in the pass while allowing passes to
override ``__init__``.
"""

def __call__(cls, *args, **kwargs):
pass_instance = type.__call__(cls, *args, **kwargs)
pass_instance._hash = hash(MetaPass._freeze_init_parameters(cls, args, kwargs))
return pass_instance

@staticmethod
def _freeze_init_parameters(class_, args, kwargs):
self_guard = object()
init_signature = signature(class_.__init__)
bound_signature = init_signature.bind(self_guard, *args, **kwargs)
arguments = [("class_.__name__", class_.__name__)]
for name, value in bound_signature.arguments.items():
if value == self_guard:
continue
if isinstance(value, Hashable):
arguments.append((name, type(value), value))
else:
arguments.append((name, type(value), repr(value)))
return frozenset(arguments)


class GenericPass(metaclass=MetaPass):
"""Generic pass class with IR-agnostic minimal functionality."""

def __init__(self):
self.requires = [] # List of passes that requires
self.preserves = [] # List of passes that preserves
self.property_set = PropertySet() # This pass's pointer to the pass manager's property set.
self._hash = hash(None)

def __hash__(self):
return self._hash

def __eq__(self, other):
return hash(self) == hash(other)

def name(self):
"""Return the name of the pass."""
return self.__class__.__name__

@abstractmethod
def run(self, passmanager_ir: Any):
"""Run a pass on the pass manager IR. This is implemented by the pass developer.
Args:
passmanager_ir: the dag on which the pass is run.
Raises:
NotImplementedError: when this is left unimplemented for a pass.
"""
raise NotImplementedError
19 changes: 19 additions & 0 deletions qiskit/passmanager/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

"""Qiskit pass manager exceptions."""

from qiskit.exceptions import QiskitError


class PassManagerError(QiskitError):
"""Pass manager error."""
158 changes: 158 additions & 0 deletions qiskit/passmanager/flow_controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2019, 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.

"""Pass flow controllers to provide pass iterator conditioned on the property set."""
from __future__ import annotations
from collections import OrderedDict
from collections.abc import Sequence
from typing import Union, List
import logging

from .base_pass import GenericPass
from .exceptions import PassManagerError

logger = logging.getLogger(__name__)


class FlowController:
"""Base class for multiple types of working list.
This class is a base class for multiple types of working list. When you iterate on it, it
returns the next pass to run.
"""

registered_controllers = OrderedDict()

def __init__(self, passes, options, **partial_controller):
self._passes = passes
self.passes = FlowController.controller_factory(passes, options, **partial_controller)
self.options = options

def __iter__(self):
yield from self.passes

def dump_passes(self):
"""Fetches the passes added to this flow controller.
Returns:
dict: {'options': self.options, 'passes': [passes], 'type': type(self)}
"""
# TODO remove
ret = {"options": self.options, "passes": [], "type": type(self)}
for pass_ in self._passes:
if isinstance(pass_, FlowController):
ret["passes"].append(pass_.dump_passes())
else:
ret["passes"].append(pass_)
return ret

@classmethod
def add_flow_controller(cls, name, controller):
"""Adds a flow controller.
Args:
name (string): Name of the controller to add.
controller (type(FlowController)): The class implementing a flow controller.
"""
cls.registered_controllers[name] = controller

@classmethod
def remove_flow_controller(cls, name):
"""Removes a flow controller.
Args:
name (string): Name of the controller to remove.
Raises:
KeyError: If the controller to remove was not registered.
"""
if name not in cls.registered_controllers:
raise KeyError("Flow controller not found: %s" % name)
del cls.registered_controllers[name]

@classmethod
def controller_factory(
cls,
passes: Sequence[GenericPass | "FlowController"],
options: dict,
**partial_controller,
):
"""Constructs a flow controller based on the partially evaluated controller arguments.
Args:
passes: passes to add to the flow controller.
options: PassManager options.
**partial_controller: Partially evaluated controller arguments in the form `{name:partial}`
Raises:
PassManagerError: When partial_controller is not well-formed.
Returns:
FlowController: A FlowController instance.
"""
if None in partial_controller.values():
raise PassManagerError("The controller needs a condition.")

if partial_controller:
for registered_controller in cls.registered_controllers.keys():
if registered_controller in partial_controller:
return cls.registered_controllers[registered_controller](
passes, options, **partial_controller
)
raise PassManagerError("The controllers for %s are not registered" % partial_controller)

return FlowControllerLinear(passes, options)


class FlowControllerLinear(FlowController):
"""The basic controller runs the passes one after the other."""

def __init__(self, passes, options): # pylint: disable=super-init-not-called
self.passes = self._passes = passes
self.options = options


class DoWhileController(FlowController):
"""Implements a set of passes in a do-while loop."""

def __init__(self, passes, options=None, do_while=None, **partial_controller):
self.do_while = do_while
self.max_iteration = options["max_iteration"] if options else 1000
super().__init__(passes, options, **partial_controller)

def __iter__(self):
for _ in range(self.max_iteration):
yield from self.passes

if not self.do_while():
return

raise PassManagerError("Maximum iteration reached. max_iteration=%i" % self.max_iteration)


class ConditionalController(FlowController):
"""Implements a set of passes under a certain condition."""

def __init__(self, passes, options=None, condition=None, **partial_controller):
self.condition = condition
super().__init__(passes, options, **partial_controller)

def __iter__(self):
if self.condition():
yield from self.passes


# Alias to a sequence of all kind of pass elements
PassSequence = Union[Union[GenericPass, FlowController], List[Union[GenericPass, FlowController]]]

# Default controllers
FlowController.add_flow_controller("condition", ConditionalController)
FlowController.add_flow_controller("do_while", DoWhileController)

0 comments on commit fbd64d9

Please sign in to comment.