-
Notifications
You must be signed in to change notification settings - Fork 129
[WIP] caching of transpiled circuit (continue of PR 815) #878
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
base: main
Are you sure you want to change the base?
Changes from all commits
3c48663
9bc659b
9119ff5
ba299f7
7497cb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -16,7 +16,8 @@ | |||||
from abc import ABC, abstractmethod | ||||||
import copy | ||||||
from collections import OrderedDict | ||||||
from typing import Sequence, Optional, Tuple, List, Dict, Union | ||||||
from typing import Sequence, Optional, Tuple, List, Dict, Union, Hashable | ||||||
from functools import wraps | ||||||
import warnings | ||||||
|
||||||
from qiskit import transpile, QuantumCircuit | ||||||
|
@@ -31,6 +32,55 @@ | |||||
from qiskit_experiments.framework.configs import ExperimentConfig | ||||||
|
||||||
|
||||||
def cached_method(method): | ||||||
"""Decorator to cache the return value of a BaseExperiment method. | ||||||
|
||||||
This stores the output of a method in the experiment object instance | ||||||
in a `_cache` dict attribute. Note that the value is cached only on | ||||||
the object instance method name, not any values of its arguments. | ||||||
|
||||||
The cache can be cleared by calling :meth:`.BaseExperiment.cache_clear`. | ||||||
""" | ||||||
|
||||||
@wraps(method) | ||||||
def wrapped_method(self, *args, **kwargs): | ||||||
name = f"{type(self).__name__}.{method.__name__}" | ||||||
|
||||||
# making a tuple from the options value. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
options_dict = vars(self.experiment_options) | ||||||
cache_key = tuple(options_dict.values()) + tuple([name]) | ||||||
for key, val in options_dict.items(): | ||||||
if isinstance(val, list): # pylint: disable=isinstance-second-argument-not-valid-type | ||||||
val = tuple(val) | ||||||
options_dict[key] = val | ||||||
cache_key = tuple(options_dict.values()) + tuple([name]) | ||||||
if isinstance( # pylint: disable=isinstance-second-argument-not-valid-type | ||||||
val, Hashable | ||||||
): | ||||||
continue | ||||||
# if one of the values in option isn't hashable, we raise a warning and we use the name as | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you've already made the effort of walking over the options one-by-one, you can just remove the non-hashable options, while keeping the hashable ones. |
||||||
# the key of the cached circuit | ||||||
warnings.warn( | ||||||
f"The value of the option {key!r} is not hashable. This can make the cached " | ||||||
f"transpiled circuit to not match the options." | ||||||
) | ||||||
cache_key = (name,) | ||||||
break | ||||||
|
||||||
# Check for cached value | ||||||
cached = self._cache.get(cache_key, None) | ||||||
if cached is not None: | ||||||
return cached | ||||||
|
||||||
# Call method and cache output | ||||||
cached = method(self, *args, **kwargs) | ||||||
self._cache[cache_key] = cached | ||||||
|
||||||
return cached | ||||||
|
||||||
return wrapped_method | ||||||
|
||||||
|
||||||
class BaseExperiment(ABC, StoreInitArgs): | ||||||
"""Abstract base class for experiments.""" | ||||||
|
||||||
|
@@ -55,6 +105,9 @@ def __init__( | |||||
# Experiment identification metadata | ||||||
self._type = experiment_type if experiment_type else type(self).__name__ | ||||||
|
||||||
# Initialize cache | ||||||
self._cache = {} | ||||||
|
||||||
# Circuit parameters | ||||||
self._num_qubits = len(qubits) | ||||||
self._physical_qubits = tuple(qubits) | ||||||
|
@@ -364,6 +417,7 @@ def circuits(self) -> List[QuantumCircuit]: | |||||
# values for any explicit experiment options that affect circuit | ||||||
# generation | ||||||
|
||||||
@cached_method | ||||||
def _transpiled_circuits(self) -> List[QuantumCircuit]: | ||||||
"""Return a list of experiment circuits, transpiled. | ||||||
|
||||||
|
@@ -382,7 +436,6 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: | |||||
DeprecationWarning, | ||||||
) | ||||||
self._postprocess_transpiled_circuits(transpiled) # pylint: disable=no-member | ||||||
|
||||||
return transpiled | ||||||
|
||||||
@classmethod | ||||||
|
@@ -409,6 +462,7 @@ def set_experiment_options(self, **fields): | |||||
Raises: | ||||||
AttributeError: If the field passed in is not a supported options | ||||||
""" | ||||||
self.cache_clear() | ||||||
for field in fields: | ||||||
if not hasattr(self._experiment_options, field): | ||||||
raise AttributeError( | ||||||
|
@@ -439,6 +493,7 @@ def set_transpile_options(self, **fields): | |||||
Raises: | ||||||
QiskitError: if `initial_layout` is one of the fields. | ||||||
""" | ||||||
self.cache_clear() | ||||||
if "initial_layout" in fields: | ||||||
raise QiskitError( | ||||||
"Initial layout cannot be specified as a transpile option" | ||||||
|
@@ -502,6 +557,10 @@ def set_analysis_options(self, **fields): | |||||
) | ||||||
self.analysis.options.update_options(**fields) | ||||||
|
||||||
def cache_clear(self): | ||||||
"""Clear all cached method outputs.""" | ||||||
self._cache = {} | ||||||
|
||||||
def _metadata(self) -> Dict[str, any]: | ||||||
"""Return experiment metadata for ExperimentData. | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
--- | ||
features: | ||
- | | ||
Adds caching of transpiled circuit generation to :class:`.BaseExperiment` | ||
so that repeated calls of :class:`~.BaseExperiment.run` will avoid | ||
repeated circuit generation and transpilation costs if no experiment options | ||
are changed between run calls. | ||
|
||
Changing experiment or transpilation options with the | ||
:meth:`~.BaseExperiment.set_experiment_options` or | ||
:meth:`~.BaseExperiment.set_transpilation_options` will clear the | ||
cached circuits. The cache can also be manually cleared by calling the | ||
:meth:`~.BaseExperiment.cache_clear` method. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -210,6 +210,30 @@ def test_return_same_circuit(self): | |
self.assertEqual(circs1[1].decompose(), circs2[1].decompose()) | ||
self.assertEqual(circs1[2].decompose(), circs2[2].decompose()) | ||
|
||
def test_experiment_cache(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason to test RB and IRB, and not any other experiment? |
||
"""Test experiment transpiled circuit cache""" | ||
exp0 = rb.StandardRB( | ||
qubits=(0, 1), | ||
lengths=[10, 20, 30], | ||
seed=123, | ||
backend=self.backend, | ||
) | ||
exp0.set_transpile_options(**self.transpiler_options) | ||
|
||
# calling a method with '@cached_method' decorator | ||
exp0_transpiled_circ = exp0._transpiled_circuits() | ||
|
||
# calling the method again returns cached circuit | ||
exp0_transpiled_cache = exp0._transpiled_circuits() | ||
|
||
self.assertEqual(exp0_transpiled_circ[0].decompose(), exp0_transpiled_cache[0].decompose()) | ||
self.assertEqual(exp0_transpiled_circ[1].decompose(), exp0_transpiled_cache[1].decompose()) | ||
self.assertEqual(exp0_transpiled_circ[2].decompose(), exp0_transpiled_cache[2].decompose()) | ||
Comment on lines
+229
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a |
||
|
||
# Checking that the cache is cleared when setting options | ||
exp0.set_experiment_options(lengths=[10, 20, 30, 40]) | ||
self.assertEqual(exp0._cache, {}) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
def test_full_sampling(self): | ||
"""Test if full sampling generates different circuits.""" | ||
exp1 = rb.StandardRB( | ||
|
@@ -357,6 +381,29 @@ def test_two_qubit(self): | |
epc_expected = 3 / 4 * self.p2q | ||
self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) | ||
|
||
def test_interleaved_cache(self): | ||
"""Test two qubit IRB.""" | ||
exp = rb.InterleavedRB( | ||
interleaved_element=CXGate(), | ||
qubits=(0, 1), | ||
lengths=list(range(1, 30, 3)), | ||
seed=123, | ||
backend=self.backend, | ||
) | ||
exp.set_transpile_options(**self.transpiler_options) | ||
|
||
# calling a method with '@cached_method' decorator | ||
exp_transpiled_circ = exp._transpiled_circuits() | ||
|
||
# calling the method again returns cached circuit | ||
exp_transpiled_cache = exp._transpiled_circuits() | ||
for circ, cached_circ in zip(exp_transpiled_circ, exp_transpiled_cache): | ||
self.assertEqual(circ.decompose(), cached_circ.decompose()) | ||
|
||
# Checking that the cache is cleared when setting options | ||
exp.set_experiment_options(lengths=[10, 20, 30, 40]) | ||
self.assertEqual(exp._cache, {}) | ||
|
||
def test_non_clifford_interleaved_element(self): | ||
"""Verifies trying to run interleaved RB with non Clifford element throws an exception""" | ||
qubits = 1 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cached on the method name and the options