Skip to content

Commit

Permalink
This PR introduces a new transpilier architecture to support advance …
Browse files Browse the repository at this point in the history
…functionality. (#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
delapuente authored Oct 9, 2018
1 parent 9c8f35e commit 017e456
Show file tree
Hide file tree
Showing 20 changed files with 1,516 additions and 61 deletions.
2 changes: 1 addition & 1 deletion examples/python/using_qiskit_terra_level_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
# 2. custom compile -- customize PassManager to run specific circuit transformations
from qiskit.transpiler.passes import CXCancellation
pm = transpiler.PassManager()
pm.add_pass(CXCancellation())
pm.add_passes(CXCancellation())
qobj_custom = transpiler.compile(circ, backend_device, pass_manager=pm)
[compiled_custom] = qobj_to_circuits(qobj_custom)
print(compiled_custom.qasm())
142 changes: 142 additions & 0 deletions qiskit/transpiler/README.md
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.
12 changes: 6 additions & 6 deletions qiskit/transpiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

"""Utils for transpiler."""
import os
from ._passmanager import PassManager
from ._transpilererror import TranspilerError

# pylint: disable=redefined-builtin
from ._transpiler import compile, transpile

from ._passmanager import PassManager, FlowController
from ._propertyset import PropertySet
from ._transpilererror import TranspilerError, TranspilerAccessError
from ._fencedobjs import FencedDAGCircuit, FencedPropertySet
from ._basepasses import AnalysisPass, TransformationPass
from ._transpiler import compile, transpile # pylint: disable=redefined-builtin
from ._parallel import parallel_map
from ._progressbar import TextProgressBar

Expand Down
20 changes: 0 additions & 20 deletions qiskit/transpiler/_basepass.py

This file was deleted.

105 changes: 105 additions & 0 deletions qiskit/transpiler/_basepasses.py
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
59 changes: 59 additions & 0 deletions qiskit/transpiler/_fencedobjs.py
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'])
Loading

0 comments on commit 017e456

Please sign in to comment.