forked from Qiskit/qiskit
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR introduces a new transpilier architecture to support advance …
…functionality. (Qiskit#1060) The main goal of Qiskit Terra's transpiler is to provide an extensible infrastructure of pluggable passes that allows flexibility in customizing the compilation pipeline through the creation and combination of new passes. Includes: Pass manager. Dependency control. Pass scheduling. Control Flow modules. More information can be found on the README.md file inside qiskit/transpiler.
- Loading branch information
1 parent
f73912f
commit b6b351d
Showing
20 changed files
with
1,516 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
## Goals and elements | ||
The main goal of Qiskit Terra's transpiler is to provide an extensible infrastructure of pluggable passes that allows flexibility in customizing the compilation pipeline through the creation and combination of new passes. | ||
|
||
### Passes | ||
- Passes run with the implementation of the abstract method `run`, which takes and returns a DAG (directed acyclic graph) representation of the circuit. | ||
- Passes are instances of either `AnalysisPass` or `TransformationPass`. | ||
- Passes are described not just by their class, but also by their parameters (see Use cases: pass identity) | ||
- Analysis passes analyze the DAG and write conclusions to a common context, a `PropertySet` object. They cannot modify the DAG. | ||
- Transformation passes can alter the DAG, but have read-only access to the property set. | ||
|
||
### Pass Mananger | ||
- A `PassManager` instance determines the schedule for running registered passes. | ||
- The pass manager is in charge of deciding the next pass to run, not the pass itself. | ||
- Registering passes in the pass manager pipeline is done by the `add_passes` method. | ||
- While registering, you can specify basic control primitives over each pass (conditionals and loops). | ||
- Options to control the scheduler: | ||
- Passes can have arguments at init time that can affect their scheduling. If you want to set properties related to how the pass is run, you can do so by accessing these properties (e.g. pass_.max_iteration = 10). | ||
- Options set from the pass manager take more precedence over those set at the time of adding a pass set, and those take more precedence over the options of each individual pass. (see [tests](https://github.com/Qiskit/qiskit-terra/master/test/transpiler/test_pass_scheduler.py)) | ||
|
||
|
||
### Pass dependency control | ||
The transpiler architecture allows passes to declare two kinds of dependency control to the pass manager: | ||
- `requires` are passes that need to have been run before executing the current pass. | ||
- `preserves` are passes that are not invalidated by the current pass. | ||
- Analysis passes preserve all. | ||
- The `requires` and `preserves` lists contain concrete instances of other passes (i.e. with specific pass parameters). | ||
|
||
|
||
### Control Flow Plugins | ||
By default, there are two control flow plugins included in the default pass manager: `do_while` and `conditional` (see **Fixed Point** and **Conditional** sue cases). You might want to add more control flow plugins. For example, a for-loop can be implemented in the following way: | ||
|
||
```Python | ||
class DoXTimesController(FlowController): | ||
def __init__(self, passes, do_x_times, **_): | ||
self.do_x_times = do_x_times() | ||
super().__init__(passes) | ||
|
||
def __iter__(self): | ||
for _ in range(self.do_x_times): | ||
for pass_ in self.working_list: | ||
yield pass_ | ||
``` | ||
|
||
The plugin is added to the pass manager in this way: | ||
|
||
``` | ||
self.passmanager.add_flow_controller('do_x_times', DoXTimesController) | ||
``` | ||
|
||
This allows to use the parameter `do_x_times`, which needs to be a callable. In this case, this is used to parametrized the plugin, so it will for-loop 3 times. | ||
|
||
``` | ||
self.passmanager.add_passes([Pass()], do_x_times=lambda x : 3) | ||
``` | ||
|
||
|
||
## Use cases | ||
### A simple chain with dependencies: | ||
The `CxCancellation` requires and preserves `ToffoliDecompose`. Same for `RotationMerge`. The pass `Mapper` requires extra information for running (the `coupling_map`, in this case). | ||
|
||
``` | ||
pm = PassManager() | ||
pm.add_passes(CxCancellation()) # requires: ToffoliDecompose / preserves: ToffoliDecompose | ||
pm.add_passes(RotationMerge()) # requires: ToffoliDecompose / preserves: ToffoliDecompose | ||
pm.add_passes(Mapper(coupling_map=coupling_map)) # requires: [] / preserves: [] | ||
pm.add_passes(CxCancellation()) | ||
``` | ||
|
||
Given the above, the pass manager executes the following sequence of passes: | ||
|
||
1. `ToffoliDecompose`, because it is required by `CxCancellation`. | ||
2. `CxCancellation` | ||
3. `RotationMerge`, because, even when `RotationMerge` also requires `ToffoliDecompose`, the `CxCancellation` preserved it, so no need to run it again. | ||
4. `Mapper` | ||
5. `ToffoliDecompose`, because `Mapper` did not preserved `ToffoliDecompose` and is require by `CxCancellation` | ||
6. `CxCancellation` | ||
|
||
### Same pass with different parameters (pass identity) | ||
A pass behavior can be heavily influenced by its parameters. For example, unrolling using some basis gates is totally different than unrolling to different gates. And a PassManager might use both. | ||
|
||
``` | ||
pm.add_passes(Unroller(basis_gates=['id','u1','u2','u3','cx'])) | ||
pm.add_passes(...) | ||
pm.add_passes(Unroller(basis_gates=['U','CX'])) | ||
``` | ||
|
||
where (from `qelib1.inc`): | ||
|
||
``` | ||
gate id q { U(0,0,0) q; } | ||
gate u1(lambda) q { U(0,0,lambda) q; } | ||
gate u2(phi,lambda) q { U(pi/2,phi,lambda) q; } | ||
gate u3(theta,phi,lambda) q { U(theta,phi,lambda) q; } | ||
gate cx c,t { CX c,t; } | ||
``` | ||
|
||
For this reason, the identity of a pass is given by its name and parameters. | ||
|
||
### While loop up to fixed point | ||
There are cases when one or more passes have to be run repeatedly, until a condition is fulfilled. | ||
|
||
``` | ||
pm = PassManager() | ||
pm.add_passes([CxCancellation(), RotationMerge(), CalculateDepth()], | ||
do_while=lambda property_set: not property_set['fixed_point']['depth']) | ||
``` | ||
The control argument `do_while` will run these passes until the callable returns `False`. The callable always takes in one argument, the pass manager's property set. In this example, `CalculateDepth` is an analysis pass that updates the property `depth` in the property set. | ||
|
||
### Conditional | ||
The pass manager developer can avoid one or more passes by making them conditional (on a property in the property set): | ||
|
||
``` | ||
pm.add_passes(LayoutMapper(coupling_map)) | ||
pm.add_passes(CheckIfMapped(coupling_map)) | ||
pm.add_passes(SwapMapper(coupling_map), | ||
condition=lambda property_set: not property_set['is_mapped']) | ||
``` | ||
|
||
The `CheckIfMapped` is an analysis pass that updates the property `is_mapped`. If `LayoutMapper` could map the circuit to the coupling map, the `SwapMapper` is unnecessary. | ||
|
||
|
||
### Idempotent passes | ||
If a pass is idempotent, the transpiler can use that property to perform certain optimizations. | ||
A pass is idempotent if `pass.run(pass.run(dag)) == pass.run(dag)`. Analysis passes are idempotent by definition, since they do not modify the DAG. Transformation passes can declare themselves as idempotent by annotating as *self preserve* in the following way (`<-`): | ||
|
||
```Python | ||
class IdempotentPass(TransformationPass): | ||
def __init__(self): | ||
super().__init__() | ||
self.preserves.append(self) # <- | ||
``` | ||
|
||
### Misbehaving passes | ||
To help the pass developer discipline, if an analysis pass attempts to modify the DAG or if a transformation pass tries to set a property in the property set of the pass manager, a `TranspilerAccessError` raises. | ||
|
||
The enforcement of this does not attempt to be strict. | ||
|
||
## Next Steps | ||
|
||
* Support "provides". Different mappers provide the same feature. In this way, a pass can request `mapper` and any mapper that provides `mapper` can be used. | ||
* It might be handy to have property set items in the field `requires` and analysis passes that "provide" that field. | ||
* Move passes to this scheme and, on the way, well-define a DAG API. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright 2018, IBM. | ||
# | ||
# This source code is licensed under the Apache License, Version 2.0 found in | ||
# the LICENSE.txt file in the root directory of this source tree. | ||
|
||
"""This module implements the base pass.""" | ||
|
||
from abc import abstractmethod | ||
from collections import Hashable | ||
from inspect import signature | ||
|
||
|
||
class MetaPass(type): | ||
""" | ||
Enforces the creation of some fields in the pass | ||
while allowing passes to override __init__ | ||
""" | ||
|
||
def __call__(cls, *args, **kwargs): | ||
if '_pass_cache' not in cls.__dict__.keys(): | ||
cls._pass_cache = {} | ||
args, kwargs = cls.normalize_parameters(*args, **kwargs) | ||
hash_ = hash(MetaPass._freeze_init_parameters(cls.__init__, args, kwargs)) | ||
if hash_ not in cls._pass_cache: | ||
new_pass = type.__call__(cls, *args, **kwargs) | ||
cls._pass_cache[hash_] = new_pass | ||
return cls._pass_cache[hash_] | ||
|
||
@staticmethod | ||
def _freeze_init_parameters(init_method, args, kwargs): | ||
self_guard = object() | ||
init_signature = signature(init_method) | ||
bound_signature = init_signature.bind(self_guard, *args, **kwargs) | ||
arguments = [] | ||
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 BasePass(metaclass=MetaPass): | ||
"""Base class for transpiler passes.""" | ||
|
||
def __init__(self): | ||
self.requires = [] # List of passes that requires | ||
self.preserves = [] # List of passes that preserves | ||
self.property_set = {} # This pass's pointer to the pass manager's property set. | ||
|
||
@classmethod | ||
def normalize_parameters(cls, *args, **kwargs): | ||
""" | ||
Because passes with the same args/kwargs are considered the same, this method allows to | ||
modify the args/kargs to respect that identity. | ||
Args: | ||
*args: args to normalize | ||
**kwargs: kwargs to normalize | ||
Returns: | ||
tuple: normalized (list(args), dict(kwargs)) | ||
""" | ||
return args, kwargs | ||
|
||
def name(self): | ||
""" The name of the pass. """ | ||
return self.__class__.__name__ | ||
|
||
@abstractmethod | ||
def run(self, dag): | ||
""" | ||
Run a pass on the DAGCircuit. This is implemented by the pass developer. | ||
Args: | ||
dag (DAGCircuit): the dag on which the pass is run. | ||
Raises: | ||
NotImplementedError: when this is left unimplemented for a pass. | ||
""" | ||
raise NotImplementedError | ||
|
||
@property | ||
def is_transformation_pass(self): | ||
""" If the pass is a TransformationPass, that means that the pass can manipulate the DAG, | ||
but cannot modify the property set (but it can be read). """ | ||
return isinstance(self, TransformationPass) | ||
|
||
@property | ||
def is_analysis_pass(self): | ||
""" If the pass is an AnalysisPass, that means that the pass can analyze the DAG and write | ||
the results of that analysis in the property set. Modifications on the DAG are not allowed | ||
by this kind of pass. """ | ||
return isinstance(self, AnalysisPass) | ||
|
||
|
||
class AnalysisPass(BasePass): # pylint: disable=abstract-method | ||
""" An analysis pass: change property set, not DAG. """ | ||
pass | ||
|
||
|
||
class TransformationPass(BasePass): # pylint: disable=abstract-method | ||
""" A transformation pass: change DAG, not property set. """ | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright 2018, IBM. | ||
# | ||
# This source code is licensed under the Apache License, Version 2.0 found in | ||
# the LICENSE.txt file in the root directory of this source tree. | ||
|
||
""" Fenced objects are wraps for raising TranspilerAccessError when they are modified.""" | ||
|
||
from ._transpilererror import TranspilerAccessError | ||
|
||
|
||
class FencedObject(): | ||
""" Given an instance and a list of attributes to fence, raises a TranspilerAccessError when one | ||
of these attributes is accessed.""" | ||
|
||
def __init__(self, instance, attributes_to_fence): | ||
self._wrapped = instance | ||
self._attributes_to_fence = attributes_to_fence | ||
|
||
def __getattribute__(self, name): | ||
object.__getattribute__(self, '_check_if_fenced')(name) | ||
return getattr(object.__getattribute__(self, '_wrapped'), name) | ||
|
||
def __getitem__(self, key): | ||
object.__getattribute__(self, '_check_if_fenced')('__getitem__') | ||
return object.__getattribute__(self, '_wrapped')[key] | ||
|
||
def __setitem__(self, key, value): | ||
object.__getattribute__(self, '_check_if_fenced')('__setitem__') | ||
object.__getattribute__(self, '_wrapped')[key] = value | ||
|
||
def _check_if_fenced(self, name): | ||
""" | ||
Checks if the attribute name is in the list of attributes to protect. If so, raises | ||
TranpilerAccessError. | ||
Args: | ||
name (string): the attribute name to check | ||
Raises: | ||
TranspilerAccessError: when name is the list of attributes to protect. | ||
""" | ||
if name in object.__getattribute__(self, '_attributes_to_fence'): | ||
raise TranspilerAccessError("The fenced %s has the property %s protected" % | ||
(type(object.__getattribute__(self, '_wrapped')), name)) | ||
|
||
|
||
class FencedPropertySet(FencedObject): | ||
""" A property set that cannot be written (via __setitem__) """ | ||
def __init__(self, property_set_instance): | ||
super().__init__(property_set_instance, ['__setitem__']) | ||
|
||
|
||
class FencedDAGCircuit(FencedObject): | ||
""" A dag circuit that cannot be modified (via _remove_op_node) """ | ||
# FIXME: add more fenced methods of the dag after dagcircuit rewrite | ||
def __init__(self, dag_circuit_instance): | ||
super().__init__(dag_circuit_instance, ['_remove_op_node']) |
Oops, something went wrong.