From e653628fdd161c8d071e69bddba899716f09f0fa Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 09:55:36 -0400 Subject: [PATCH 01/23] Add to qnode --- pennylane/beta/tapes/qnode.py | 65 ++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 639191925a5..f2312ba7ec7 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -15,6 +15,7 @@ This module contains the QNode class and qnode decorator. """ from collections.abc import Sequence +from collections import OrderedDict from functools import lru_cache, update_wrapper import warnings @@ -97,6 +98,10 @@ class QNode: * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation arguments. + caching (int): number of device executions to store in a cache to speed up subsequent + executions. Caching does not take place by default. In caching mode, the quantum circuit + being executed must have a constant structure and only its parameters can varied. + Keyword Args: h=1e-7 (float): step size for the finite difference method order=1 (int): The order of the finite difference method to use. ``1`` corresponds @@ -115,7 +120,9 @@ class QNode: # pylint:disable=too-many-instance-attributes - def __init__(self, func, device, interface="autograd", diff_method="best", **diff_options): + def __init__( + self, func, device, interface="autograd", diff_method="best", caching=None, **diff_options + ): if interface is not None and interface not in self.INTERFACE_MAP: raise QuantumFunctionError( @@ -137,6 +144,25 @@ def __init__(self, func, device, interface="autograd", diff_method="best", **dif self.dtype = np.float64 self.max_expansion = 2 + self._caching = None + """float: number of device executions to store in a cache to speed up subsequent + executions. If set to zero, no caching occurs.""" + + if caching is not None: + warnings.warn( + "Caching mode activated. The quantum circuit being executed by the QNode must have " + "a fixed structure.", + ) + if self.diff_method is "backprop": + raise ValueError('Caching mode is incompatible with the "backprop" diff_method') + self._caching = caching + else: + self._caching = 0 + + self._cache_execute = OrderedDict() + """OrderedDict[int: Any]: A copy of the ``_cache_execute`` dictionary from the quantum + tape""" + @staticmethod def get_tape(device, interface, diff_method="best"): """Determine the best QuantumTape, differentiation method, and interface @@ -324,7 +350,7 @@ def _get_parameter_shift_method(device, interface): def construct(self, args, kwargs): """Call the quantum function with a tape context, ensuring the operations get queued.""" - self.qtape = self._tape() + self.qtape = self._tape(caching=self._caching) # apply the interface (if any) if self.interface is not None: @@ -370,8 +396,16 @@ def __call__(self, *args, **kwargs): # construct the tape self.construct(args, kwargs) + if self._caching: + self.qtape._cache_execute = self._cache_execute + # execute the tape - return self.qtape.execute(device=self.device) + res = self.qtape.execute(device=self.device) + + if self._caching: + self._cache_execute = self.qtape._cache_execute + + return res def to_tf(self, dtype=None): """Apply the TensorFlow interface to the internal quantum tape. @@ -443,10 +477,20 @@ def to_autograd(self): if self.qtape is not None: AutogradInterface.apply(self.qtape) + @property + def caching(self): + """float: number of device executions to store in a cache to speed up subsequent + executions. If set to zero, no caching occurs.""" + return self._caching + + @caching.setter + def caching(self, value): + self._caching = value + INTERFACE_MAP = {"autograd": to_autograd, "torch": to_torch, "tf": to_tf} -def qnode(device, interface="autograd", diff_method="best", **diff_options): +def qnode(device, interface="autograd", diff_method="best", caching=None, **diff_options): """Decorator for creating QNodes. This decorator is used to indicate to PennyLane that the decorated function contains a @@ -517,6 +561,10 @@ def qnode(device, interface="autograd", diff_method="best", **diff_options): * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation arguments. + caching (int): number of device executions to store in a cache to speed up subsequent + executions. Caching does not take place by default. In caching mode, the quantum circuit + being executed must have a constant structure and only its parameters can varied. + Keyword Args: h=1e-7 (float): Step size for the finite difference method. order=1 (int): The order of the finite difference method to use. ``1`` corresponds @@ -536,7 +584,14 @@ def qnode(device, interface="autograd", diff_method="best", **diff_options): @lru_cache() def qfunc_decorator(func): """The actual decorator""" - qn = QNode(func, device, interface=interface, diff_method=diff_method, **diff_options) + qn = QNode( + func, + device, + interface=interface, + diff_method=diff_method, + caching=caching, + **diff_options, + ) return update_wrapper(qn, func) return qfunc_decorator From 62f3582154569475bac636031674053b92aecc60 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 09:57:14 -0400 Subject: [PATCH 02/23] Add to tape --- pennylane/beta/tapes/tape.py | 93 +++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index 75b652ec30b..2c90c1df879 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -15,11 +15,13 @@ This module contains the base quantum tape. """ # pylint: disable=too-many-instance-attributes,protected-access,too-many-branches +from collections import OrderedDict import contextlib import numpy as np import pennylane as qml +from pennylane.utils import _hash_iterable from pennylane.beta.queuing import AnnotatedQueue, QueuingContext from pennylane.beta.queuing import mock_operations @@ -144,6 +146,11 @@ class QuantumTape(AnnotatedQueue): >>> from pennylane.beta.queuing import expval, var, sample, probs + Args: + name (str): a name given to the quantum tape + caching (int): number of device executions to store in a cache to speed up subsequent + executions. Caching does not take place by default. + **Example** .. code-block:: python @@ -215,7 +222,7 @@ class QuantumTape(AnnotatedQueue): [[-0.45478169]] """ - def __init__(self, name=None): + def __init__(self, name=None, caching=None): super().__init__() self.name = name @@ -247,6 +254,14 @@ def __init__(self, name=None): self._stack = None + self._caching = caching or 0 + """float: number of device executions to store in a cache to speed up subsequent + executions. If set to zero, no caching occurs.""" + + self._cache_execute = OrderedDict() + """OrderedDict[int: Any]: Mapping from hashes of the input parameters to results of + executing the device.""" + def __repr__(self): return f"<{self.__class__.__name__}: wires={self.wires.tolist()}, params={self.num_params}>" @@ -890,6 +905,60 @@ def execute(self, device, params=None): return self._execute(params, device=device) + def _get_all_parameters(self, params): + """Return all parameters by combining trainable parameters supplied by ``params`` with + existing non-trainable parameters. + + The returned parameters are provided in order of appearance + on the tape. + + Args: + params (list[Any]): The quantum tape operation parameters. + + **Example** + + .. code-block:: python + + from pennylane.beta.tapes import QuantumTape + from pennylane.beta.queuing import expval, var, sample, probs + + with QuantumTape() as tape: + qml.RX(0.432, wires=0) + qml.RY(0.543, wires=0) + qml.CNOT(wires=[0, 'a']) + qml.RX(0.133, wires='a') + expval(qml.PauliZ(wires=[0])) + + Suppose only parameters 0 and 2 are trainable: + + >>> tape.trainable_params = {0, 2} + + We can access all parameters using: + + >>> tape._get_all_parameters([0.1, 0.2]) + [0.1, 0.543, 0.2] + """ + num_all_parameters = len(self._par_info) # including non-trainable parameters + + if self.num_params == num_all_parameters: + return params + # Otherwise, we must combine the trainable parameters supplied by the params + # argument with the non-trainable parameters given by get_parameters() + + saved_all_parameters = self.get_parameters(trainable_only=False) + + all_parameters = [] + position = 0 + for i in range(num_all_parameters): + if i in self._trainable_params: + p = params[position] + position += 1 + else: + p = saved_all_parameters[i] + all_parameters.append(p) + + return all_parameters + def execute_device(self, params, device): """Execute the tape on a quantum device. @@ -904,6 +973,12 @@ def execute_device(self, params, device): params (list[Any]): The quantum tape operation parameters. If not provided, the current tape parameter values are used (via :meth:`~.get_parameters`). """ + if self._caching: + all_parameters = self._get_all_parameters(params) + hashed_params = _hash_iterable(all_parameters) + if hashed_params in self._cache_execute: + return self._cache_execute[hashed_params] + device.reset() # backup the current parameters @@ -936,6 +1011,12 @@ def execute_device(self, params, device): # restore original parameters self.set_parameters(saved_parameters) + + if self._caching and hashed_params not in self._cache_execute: + self._cache_execute[hashed_params] = res + if len(self._cache_execute) > self._caching: + self._cache_execute.popitem(last=False) + return res # interfaces can optionally override the _execute method @@ -1324,3 +1405,13 @@ def jacobian(self, device, params=None, **options): jac[:, idx] = g.flatten() return jac + + @property + def caching(self): + """float: number of device executions to store in a cache to speed up subsequent + executions. If set to zero, no caching occurs.""" + return self._caching + + @caching.setter + def caching(self, value): + self._caching = value From 96864476e8e49559e9266440c64d18744d6e5b5f Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 09:58:14 -0400 Subject: [PATCH 03/23] Add to utils --- pennylane/utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pennylane/utils.py b/pennylane/utils.py index af58d1f08f0..5f864464181 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -433,3 +433,22 @@ def expand_vector(vector, original_wires, expanded_wires): expanded_tensor = np.moveaxis(expanded_tensor, original_indices, wire_indices) return expanded_tensor.reshape(2 ** M) + + +def _hash_iterable(iterable): + """Returns a single hash of an input iterable. + + The iterable must be flat and can contain only numbers and NumPy arrays. + + Args: + iterable (Iterable): the iterable to generate a hash for + + Returns: + int: the resulting hash + """ + hashes = [] + for obj in iterable: + to_hash = (obj.tobytes(), obj.shape) if isinstance(obj, np.ndarray) else obj + obj_hash = hash(to_hash) + hashes.append(obj_hash) + return hash(tuple(hashes)) From 67c7f6126383adf91f3e25d3a8b163961c01c39f Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 09:59:21 -0400 Subject: [PATCH 04/23] Add caching test --- tests/beta/tapes/test_caching.py | 257 +++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/beta/tapes/test_caching.py diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py new file mode 100644 index 00000000000..d45bc84d95a --- /dev/null +++ b/tests/beta/tapes/test_caching.py @@ -0,0 +1,257 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for caching executions of the quantum tape and QNode.""" +import numpy as np +import pytest + +import pennylane as qml +import pennylane +from pennylane.beta.queuing import expval +from pennylane.beta.tapes import QuantumTape, qnode +from pennylane.devices import DefaultQubit +from pennylane.devices.default_qubit_autograd import DefaultQubitAutograd +from pennylane.utils import _hash_iterable + + +def get_tape(caching): + """Creates a simple quantum tape""" + with QuantumTape(caching=caching) as tape: + qml.QubitUnitary(np.eye(2), wires=0) + qml.RX(0.1, wires=0) + qml.RX(0.2, wires=1) + qml.CNOT(wires=[0, 1]) + expval(qml.PauliZ(wires=1)) + return tape + + +def get_qnode(caching, diff_method="finite-diff", interface="autograd"): + """Creates a simple QNode""" + dev = qml.device("default.qubit.autograd", wires=3) + + @qnode(dev, caching=caching, diff_method=diff_method, interface=interface) + def qfunc(x, y): + qml.RX(x, wires=0) + qml.RX(y, wires=1) + qml.CNOT(wires=[0, 1]) + return expval(qml.PauliZ(wires=1)) + + return qfunc + + +class TestTapeCaching: + """Tests for caching when using quantum tape""" + + def test_set_and_get(self): + """Test that the caching attribute can be set and accessed""" + tape = QuantumTape() + assert tape.caching == 0 + + tape = QuantumTape(caching=10) + assert tape.caching == 10 + + tape.caching = 20 + assert tape.caching == 20 + + def test_no_caching(self, mocker): + """Test that no caching occurs when the caching attribute is equal to zero""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(0) + + spy = mocker.spy(DefaultQubit, "execute") + tape.execute(device=dev) + tape.execute(device=dev) + + assert len(spy.call_args_list) == 2 + assert len(tape._cache_execute) == 0 + + def test_caching(self, mocker): + """Test that caching occurs when the caching attribute is above zero""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(10) + + tape.execute(device=dev) + spy = mocker.spy(DefaultQubit, "execute") + tape.execute(device=dev) + + spy.assert_not_called() + assert len(tape._cache_execute) == 1 + + def test_add_to_cache_execute(self): + """Test that the _cache_execute attribute is added to when the tape is executed""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(10) + + result = tape.execute(device=dev) + cache_execute = tape._cache_execute + params = tape.get_parameters() + hashed = _hash_iterable(params) + + assert len(cache_execute) == 1 + assert hashed in cache_execute + assert np.allclose(cache_execute[hashed], result) + + def test_get_all_parameters(self, mocker): + """Test that input params are correctly passed to the hash function when only a + subset of params are trainable""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(10) + + tape.trainable_params = {1} + spy = mocker.spy(pennylane.beta.tapes.tape, "_hash_iterable") + tape.execute(params=[-0.1], device=dev) + call = spy.call_args_list[0][0][0] + expected_call = [np.eye(2), -0.1, 0.2] + for arg, exp_arg in zip(call, expected_call): + assert np.allclose(arg, exp_arg) + + def test_fill_cache(self): + """Test that the cache is added to until it reaches its maximum size (in this case 10), + and then maintains that size upon subsequent additions.""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(10) + + tape.trainable_params = {1} + args = np.arange(20) + + for i, arg in enumerate(args[:10]): + tape.execute(params=[arg], device=dev) + assert len(tape._cache_execute) == i + 1 + + for arg in args[10:]: + tape.execute(params=[arg], device=dev) + assert len(tape._cache_execute) == 10 + + def test_drop_from_cache(self): + """Test that the first entry of the _cache_execute dictionary is the first to be dropped + from the dictionary once it becomes full""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(2) + + tape.trainable_params = {1} + tape.execute(device=dev) + first_hash = list(tape._cache_execute.keys())[0] + + tape.execute(device=dev, params=[0.2]) + assert first_hash in tape._cache_execute + tape.execute(device=dev, params=[0.3]) + assert first_hash not in tape._cache_execute + + def test_caching_multiple_values(self, mocker): + """Test that multiple device executions with different params are cached and accessed on + subsequent executions""" + dev = qml.device("default.qubit", wires=2) + tape = get_tape(10) + + tape.trainable_params = {1} + args = np.arange(10) + + for arg in args[:10]: + tape.execute(params=[arg], device=dev) + + spy = mocker.spy(DefaultQubit, "execute") + for arg in args[:10]: + tape.execute(params=[arg], device=dev) + + spy.assert_not_called() + + +@pytest.mark.filterwarnings("ignore:Caching mode activated") +class TestQNodeCaching: + """Tests for caching when using the QNode""" + + def test_set_and_get(self): + """Test that the caching attribute can be set and accessed""" + with pytest.warns(UserWarning, match="Caching mode activated."): + qnode = get_qnode(caching=0) + assert qnode.caching == 0 + + qnode = get_qnode(caching=10) + assert qnode.caching == 10 + + qnode.caching = 20 + assert qnode.caching == 20 + + def test_backprop_error(self): + """Test if an error is raised when caching is used with the backprop diff_method""" + with pytest.raises(ValueError, match="Caching mode is incompatible"): + get_qnode(caching=10, diff_method="backprop") + + def test_caching(self, mocker): + """Test that multiple device executions with different params are cached and accessed on + subsequent executions""" + qnode = get_qnode(caching=10) + args = np.arange(10) + + for arg in args[:10]: + qnode(arg, 0.2) + + assert qnode.qtape.caching == 10 + + spy = mocker.spy(DefaultQubitAutograd, "execute") + for arg in args[:10]: + qnode(arg, 0.2) + + spy.assert_not_called() + + def test_gradient_autograd(self, mocker): + """Test that caching works when calculating the gradient method using the autograd + interface""" + qnode = get_qnode(caching=10, interface="autograd") + d_qnode = qml.grad(qnode) + args = [0.1, 0.2] + + d_qnode(*args) + spy = mocker.spy(DefaultQubitAutograd, "execute") + d_qnode(*args) + spy.assert_not_called() + + @pytest.mark.usefixtures("skip_if_no_tf_support") + def test_gradient_tf(self, mocker): + """Test that caching works when calculating the gradient method using the TF interface""" + import tensorflow as tf + + qnode = get_qnode(caching=10, interface="tf") + args0 = tf.Variable(0.1) + args1 = tf.Variable(0.2) + + with tf.GradientTape() as tape: + res = qnode(args0, args1) + + grad = tape.gradient(res, args0) + assert grad is not None + + spy = mocker.spy(DefaultQubitAutograd, "execute") + with tf.GradientTape() as tape: + res = qnode(args0, args1) + + tape.gradient(res, args0) + spy.assert_not_called() + + @pytest.mark.usefixtures("skip_if_no_torch_support") + def test_gradient_torch(self, mocker): + """Test that caching works when calculating the gradient method using the Torch interface""" + import torch + + qnode = get_qnode(caching=10, interface="torch") + args0 = torch.tensor(0.1, requires_grad=True) + args1 = torch.tensor(0.2) + + res = qnode(args0, args1) + res.backward() + assert args0.grad is not None + + spy = mocker.spy(DefaultQubitAutograd, "execute") + res = qnode(args0, args1) + res.backward() + spy.assert_not_called() From 625e3b7b386dc75abfb82c9803ab40af304b72a4 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 10:00:39 -0400 Subject: [PATCH 05/23] Add to tape tests --- tests/beta/tapes/test_tape.py | 47 ++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/tests/beta/tapes/test_tape.py b/tests/beta/tapes/test_tape.py index 1f9e813cf85..1d447887bdf 100644 --- a/tests/beta/tapes/test_tape.py +++ b/tests/beta/tapes/test_tape.py @@ -515,8 +515,6 @@ def test_inverse(self): assert ops[2].inverse # check that parameter order has reversed - print(tape.get_parameters()) - print([init_state, p[1], p[2], p[3], p[0]]) assert tape.get_parameters() == [init_state, p[1], p[2], p[3], p[0]] def test_parameter_transforms(self): @@ -1320,8 +1318,6 @@ def test_numeric_unknown_order(self): qml.RZ(1, wires=[2]) qml.CNOT(wires=[0, 1]) expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2)) - for k, v in tape._queue.items(): - print(k, v) with pytest.raises(ValueError, match="Order must be 1 or 2"): tape.jacobian(dev, order=3) @@ -1507,3 +1503,46 @@ def test_trainable_measurement(self, tol): res = tape.jacobian(dev) expected = np.array([[-2 * a * np.sin(phi)]]) assert np.allclose(res, expected, atol=tol, rtol=0) + + +class TestGetAllParameters: + """Tests for the _get_all_parameters method""" + + def test_all_params_trainable(self): + """Test that the input params are simply returned when all params are trainable""" + with QuantumTape() as tape: + qml.RX(0.1, wires=0) + qml.RX(0.2, wires=1) + qml.CNOT(wires=[0, 1]) + expval(qml.PauliZ(wires=1)) + + new_params = tape._get_all_parameters([0.8, 0.9]) + assert new_params == [0.8, 0.9] + + def test_some_params_trainable(self): + """Test that the trainable params are combined with non-trainable params""" + with QuantumTape() as tape: + qml.RX(0.1, wires=0) + qml.RX(0.2, wires=1) + qml.CNOT(wires=[0, 1]) + expval(qml.PauliZ(wires=1)) + + tape.trainable_params = {0} + new_params = tape._get_all_parameters([0.8]) + assert new_params == [0.8, 0.2] + + tape.trainable_params = {1} + new_params = tape._get_all_parameters([0.9]) + assert new_params == [0.1, 0.9] + + def test_no_params_trainable(self): + """Test that the existing tape params are simply returned when no params are trainable""" + with QuantumTape() as tape: + qml.RX(0.1, wires=0) + qml.RX(0.2, wires=1) + qml.CNOT(wires=[0, 1]) + expval(qml.PauliZ(wires=1)) + + tape.trainable_params = {} + new_params = tape._get_all_parameters([]) + assert new_params == [0.1, 0.2] From 5fea812912fe47b08419a14b0de63c0156a9ee01 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 10:02:07 -0400 Subject: [PATCH 06/23] Add to utils tests --- tests/test_utils.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3f6f55cc790..fa66c44de00 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -25,6 +25,7 @@ import pennylane as qml import pennylane._queuing import pennylane.utils as pu +from pennylane.wires import Wires from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane.operation import Tensor @@ -825,3 +826,28 @@ def test_non_operations_in_list(self, arg): ValueError, match="The given operation_list does not only contain Operations" ): pu.inv(arg) + + +class TestHashIterable: + """Tests for the _hash_iterable function.""" + + iterables = [ + [1, 1.4, -1], + [], + [1, np.ones((4, 4)), 19.67], + [1, np.ones((2, 4)), np.ones((2, 4)), 19.67], + [1, np.zeros((4, 4)), 19.67], + ] + + @pytest.mark.parametrize("iterable", iterables) + def test_valid_hash(self, iterable): + """Test that a valid hash is generated and that it is the same when generated again""" + h = pu._hash_iterable(iterable) + h2 = pu._hash_iterable(iterable) + assert isinstance(h, int) + assert h == h2 + + @pytest.mark.parametrize("iterable1, iterable2", itertools.combinations(iterables, r=2)) + def test_different(self, iterable1, iterable2): + """Test that a different hash is given for each test iterable""" + assert pu._hash_iterable(iterable1) != pu._hash_iterable(iterable2) From f084d9d1209564f8998e44d1c9a9958c3d3cfa4e Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 10:13:42 -0400 Subject: [PATCH 07/23] Fix typo --- pennylane/beta/tapes/qnode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index f2312ba7ec7..76d1bcea181 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -100,7 +100,7 @@ class QNode: caching (int): number of device executions to store in a cache to speed up subsequent executions. Caching does not take place by default. In caching mode, the quantum circuit - being executed must have a constant structure and only its parameters can varied. + being executed must have a constant structure and only its parameters can be varied. Keyword Args: h=1e-7 (float): step size for the finite difference method @@ -563,7 +563,7 @@ def qnode(device, interface="autograd", diff_method="best", caching=None, **diff caching (int): number of device executions to store in a cache to speed up subsequent executions. Caching does not take place by default. In caching mode, the quantum circuit - being executed must have a constant structure and only its parameters can varied. + being executed must have a constant structure and only its parameters can be varied. Keyword Args: h=1e-7 (float): Step size for the finite difference method. From fe453d8b7bb42b8e098db3c14ac956c4d70175ea Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 10:19:34 -0400 Subject: [PATCH 08/23] Fix pylint --- pennylane/beta/tapes/tape.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index 2c90c1df879..c3e08ffc688 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -134,6 +134,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False): return new_tape +# pylint: disable=too-many-public-methods class QuantumTape(AnnotatedQueue): """A quantum tape recorder, that records, validates, executes, and differentiates variational quantum programs. From e6740ce312c1de00ac7f34b571f2e83dccf67a03 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 21 Sep 2020 10:28:08 -0400 Subject: [PATCH 09/23] Fix pylint --- pennylane/beta/tapes/qnode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 76d1bcea181..216aac4deea 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -118,7 +118,7 @@ class QNode: >>> qnode = QNode(circuit, dev) """ - # pylint:disable=too-many-instance-attributes + # pylint:disable=too-many-instance-attributes,too-many-arguments def __init__( self, func, device, interface="autograd", diff_method="best", caching=None, **diff_options @@ -153,14 +153,14 @@ def __init__( "Caching mode activated. The quantum circuit being executed by the QNode must have " "a fixed structure.", ) - if self.diff_method is "backprop": + if self.diff_method == "backprop": raise ValueError('Caching mode is incompatible with the "backprop" diff_method') self._caching = caching else: self._caching = 0 self._cache_execute = OrderedDict() - """OrderedDict[int: Any]: A copy of the ``_cache_execute`` dictionary from the quantum + """OrderedDict[int: Any]: A copy of the ``_cache_execute`` dictionary from the quantum tape""" @staticmethod From db3234121bcedc9cf6de07577415c858e9b2a9aa Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 07:29:12 -0400 Subject: [PATCH 10/23] Remove get_all_parameters --- pennylane/beta/interfaces/autograd.py | 2 + pennylane/beta/tapes/tape.py | 67 +++------------------------ tests/beta/tapes/test_tape.py | 43 ----------------- 3 files changed, 9 insertions(+), 103 deletions(-) diff --git a/pennylane/beta/interfaces/autograd.py b/pennylane/beta/interfaces/autograd.py index 9a3c92c733d..b86ce50e673 100644 --- a/pennylane/beta/interfaces/autograd.py +++ b/pennylane/beta/interfaces/autograd.py @@ -115,6 +115,8 @@ def get_parameters(self, trainable_only=True): # pylint: disable=missing-functi for idx, p in enumerate(self._all_parameter_values) if idx in self.trainable_params ] + else: + params = self._all_parameter_values return autograd.builtins.list(params) diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index c3e08ffc688..711db15e060 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -906,60 +906,6 @@ def execute(self, device, params=None): return self._execute(params, device=device) - def _get_all_parameters(self, params): - """Return all parameters by combining trainable parameters supplied by ``params`` with - existing non-trainable parameters. - - The returned parameters are provided in order of appearance - on the tape. - - Args: - params (list[Any]): The quantum tape operation parameters. - - **Example** - - .. code-block:: python - - from pennylane.beta.tapes import QuantumTape - from pennylane.beta.queuing import expval, var, sample, probs - - with QuantumTape() as tape: - qml.RX(0.432, wires=0) - qml.RY(0.543, wires=0) - qml.CNOT(wires=[0, 'a']) - qml.RX(0.133, wires='a') - expval(qml.PauliZ(wires=[0])) - - Suppose only parameters 0 and 2 are trainable: - - >>> tape.trainable_params = {0, 2} - - We can access all parameters using: - - >>> tape._get_all_parameters([0.1, 0.2]) - [0.1, 0.543, 0.2] - """ - num_all_parameters = len(self._par_info) # including non-trainable parameters - - if self.num_params == num_all_parameters: - return params - # Otherwise, we must combine the trainable parameters supplied by the params - # argument with the non-trainable parameters given by get_parameters() - - saved_all_parameters = self.get_parameters(trainable_only=False) - - all_parameters = [] - position = 0 - for i in range(num_all_parameters): - if i in self._trainable_params: - p = params[position] - position += 1 - else: - p = saved_all_parameters[i] - all_parameters.append(p) - - return all_parameters - def execute_device(self, params, device): """Execute the tape on a quantum device. @@ -974,12 +920,6 @@ def execute_device(self, params, device): params (list[Any]): The quantum tape operation parameters. If not provided, the current tape parameter values are used (via :meth:`~.get_parameters`). """ - if self._caching: - all_parameters = self._get_all_parameters(params) - hashed_params = _hash_iterable(all_parameters) - if hashed_params in self._cache_execute: - return self._cache_execute[hashed_params] - device.reset() # backup the current parameters @@ -988,6 +928,13 @@ def execute_device(self, params, device): # temporarily mutate the in-place parameters self.set_parameters(params) + if self._caching: + all_parameters = self.get_parameters(trainable_only=False) + hashed_params = _hash_iterable(all_parameters) + if hashed_params in self._cache_execute: + self.set_parameters(saved_parameters) + return self._cache_execute[hashed_params] + if isinstance(device, qml.QubitDevice): res = device.execute(self) else: diff --git a/tests/beta/tapes/test_tape.py b/tests/beta/tapes/test_tape.py index 1d447887bdf..61bfc53bd95 100644 --- a/tests/beta/tapes/test_tape.py +++ b/tests/beta/tapes/test_tape.py @@ -1503,46 +1503,3 @@ def test_trainable_measurement(self, tol): res = tape.jacobian(dev) expected = np.array([[-2 * a * np.sin(phi)]]) assert np.allclose(res, expected, atol=tol, rtol=0) - - -class TestGetAllParameters: - """Tests for the _get_all_parameters method""" - - def test_all_params_trainable(self): - """Test that the input params are simply returned when all params are trainable""" - with QuantumTape() as tape: - qml.RX(0.1, wires=0) - qml.RX(0.2, wires=1) - qml.CNOT(wires=[0, 1]) - expval(qml.PauliZ(wires=1)) - - new_params = tape._get_all_parameters([0.8, 0.9]) - assert new_params == [0.8, 0.9] - - def test_some_params_trainable(self): - """Test that the trainable params are combined with non-trainable params""" - with QuantumTape() as tape: - qml.RX(0.1, wires=0) - qml.RX(0.2, wires=1) - qml.CNOT(wires=[0, 1]) - expval(qml.PauliZ(wires=1)) - - tape.trainable_params = {0} - new_params = tape._get_all_parameters([0.8]) - assert new_params == [0.8, 0.2] - - tape.trainable_params = {1} - new_params = tape._get_all_parameters([0.9]) - assert new_params == [0.1, 0.9] - - def test_no_params_trainable(self): - """Test that the existing tape params are simply returned when no params are trainable""" - with QuantumTape() as tape: - qml.RX(0.1, wires=0) - qml.RX(0.2, wires=1) - qml.CNOT(wires=[0, 1]) - expval(qml.PauliZ(wires=1)) - - tape.trainable_params = {} - new_params = tape._get_all_parameters([]) - assert new_params == [0.1, 0.2] From b1160bfca82a7c474093c883e76b55257367b818 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 07:32:42 -0400 Subject: [PATCH 11/23] Add to test --- tests/beta/interfaces/test_tape_autograd.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/beta/interfaces/test_tape_autograd.py b/tests/beta/interfaces/test_tape_autograd.py index 7c7a5256394..75d9a6f22ad 100644 --- a/tests/beta/interfaces/test_tape_autograd.py +++ b/tests/beta/interfaces/test_tape_autograd.py @@ -34,8 +34,8 @@ def test_interface_str(self): assert isinstance(tape, AutogradInterface) def test_get_parameters(self): - """Test that the get_parameters function correctly sets and returns the - trainable parameters""" + """Test that the get_parameters function correctly gets the trainable parameters and all + parameters, depending on the trainable_only argument""" a = np.array(0.1, requires_grad=True) b = np.array(0.2, requires_grad=False) c = np.array(0.3, requires_grad=True) @@ -48,7 +48,8 @@ def test_get_parameters(self): expval(qml.PauliX(0)) assert tape.trainable_params == {0, 2} - assert np.all(tape.get_parameters() == [a, c]) + assert np.all(tape.get_parameters(trainable_only=True) == [a, c]) + assert np.all(tape.get_parameters(trainable_only=False) == [a, b, c, d]) def test_execution(self): """Test execution""" From 5bd1d37a87f0fc67f7f111bd0946ca15587ac028 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 07:47:42 -0400 Subject: [PATCH 12/23] Clarify filled cache behaviour --- pennylane/beta/tapes/qnode.py | 8 ++++++-- pennylane/beta/tapes/tape.py | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 41d9a92c708..89f3ddfdc75 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -99,7 +99,9 @@ class QNode: arguments. caching (int): number of device executions to store in a cache to speed up subsequent - executions. Caching does not take place by default. In caching mode, the quantum circuit + executions. A value of ``0`` indicates that no caching will take place. Once filled, + older elements of the cache are removed and replaced with the most recent device + executions to keep the cache up to date. In caching mode, the quantum circuit being executed must have a constant structure and only its parameters can be varied. Keyword Args: @@ -554,7 +556,9 @@ def qnode(device, interface="autograd", diff_method="best", caching=None, **diff operation arguments. caching (int): number of device executions to store in a cache to speed up subsequent - executions. Caching does not take place by default. In caching mode, the quantum circuit + executions. A value of ``0`` indicates that no caching will take place. Once filled, + older elements of the cache are removed and replaced with the most recent device + executions to keep the cache up to date. In caching mode, the quantum circuit being executed must have a constant structure and only its parameters can be varied. Keyword Args: diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index 711db15e060..d61cf0d4ecb 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -150,7 +150,9 @@ class QuantumTape(AnnotatedQueue): Args: name (str): a name given to the quantum tape caching (int): number of device executions to store in a cache to speed up subsequent - executions. Caching does not take place by default. + executions. A value of ``0`` indicates that no caching will take place. Once filled, + older elements of the cache are removed and replaced with the most recent device + executions to keep the cache up to date. **Example** @@ -223,7 +225,7 @@ class QuantumTape(AnnotatedQueue): [[-0.45478169]] """ - def __init__(self, name=None, caching=None): + def __init__(self, name=None, caching=0): super().__init__() self.name = name @@ -255,7 +257,7 @@ def __init__(self, name=None, caching=None): self._stack = None - self._caching = caching or 0 + self._caching = caching """float: number of device executions to store in a cache to speed up subsequent executions. If set to zero, no caching occurs.""" From f86adc7a057ef19a509c1ec5583414823727c8be Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 07:54:29 -0400 Subject: [PATCH 13/23] Make caching default 0 --- pennylane/beta/tapes/qnode.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 89f3ddfdc75..64404fd74e9 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -123,7 +123,7 @@ class QNode: # pylint:disable=too-many-instance-attributes,too-many-arguments def __init__( - self, func, device, interface="autograd", diff_method="best", caching=None, **diff_options + self, func, device, interface="autograd", diff_method="best", caching=0, **diff_options ): if interface is not None and interface not in self.INTERFACE_MAP: @@ -146,20 +146,17 @@ def __init__( self.dtype = np.float64 self.max_expansion = 2 - self._caching = None + self._caching = caching """float: number of device executions to store in a cache to speed up subsequent executions. If set to zero, no caching occurs.""" - if caching is not None: + if caching != 0: warnings.warn( "Caching mode activated. The quantum circuit being executed by the QNode must have " "a fixed structure.", ) if self.diff_method == "backprop": raise ValueError('Caching mode is incompatible with the "backprop" diff_method') - self._caching = caching - else: - self._caching = 0 self._cache_execute = OrderedDict() """OrderedDict[int: Any]: A copy of the ``_cache_execute`` dictionary from the quantum @@ -484,7 +481,7 @@ def caching(self, value): INTERFACE_MAP = {"autograd": to_autograd, "torch": to_torch, "tf": to_tf} -def qnode(device, interface="autograd", diff_method="best", caching=None, **diff_options): +def qnode(device, interface="autograd", diff_method="best", caching=0, **diff_options): """Decorator for creating QNodes. This decorator is used to indicate to PennyLane that the decorated function contains a From 4dbee7444cfa980dfef7592ee99500d69204b7fa Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 07:58:41 -0400 Subject: [PATCH 14/23] Improve warning test --- tests/beta/tapes/test_caching.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index d45bc84d95a..f5a48ccbffc 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -172,10 +172,13 @@ class TestQNodeCaching: def test_set_and_get(self): """Test that the caching attribute can be set and accessed""" - with pytest.warns(UserWarning, match="Caching mode activated."): + with pytest.warns(None) as warn: qnode = get_qnode(caching=0) assert qnode.caching == 0 + assert len(warn) is 0 # assert that no warning took place + + with pytest.warns(UserWarning, match="Caching mode activated."): qnode = get_qnode(caching=10) assert qnode.caching == 10 From 651e8024babec93b9298b7d5c29f5b8c6adac13c Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 09:28:06 -0400 Subject: [PATCH 15/23] Use circuit hash as hash --- pennylane/beta/tapes/tape.py | 16 +++++++--------- pennylane/utils.py | 19 ------------------- tests/beta/tapes/test_caching.py | 18 +----------------- tests/test_utils.py | 25 ------------------------- 4 files changed, 8 insertions(+), 70 deletions(-) diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index d61cf0d4ecb..fd128fb6c11 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -21,7 +21,6 @@ import numpy as np import pennylane as qml -from pennylane.utils import _hash_iterable from pennylane.beta.queuing import AnnotatedQueue, QueuingContext from pennylane.beta.queuing import mock_operations @@ -262,8 +261,8 @@ def __init__(self, name=None, caching=0): executions. If set to zero, no caching occurs.""" self._cache_execute = OrderedDict() - """OrderedDict[int: Any]: Mapping from hashes of the input parameters to results of - executing the device.""" + """OrderedDict[int: Any]: Mapping from hashes of the circuit to results of executing the + device.""" def __repr__(self): return f"<{self.__class__.__name__}: wires={self.wires.tolist()}, params={self.num_params}>" @@ -931,11 +930,10 @@ def execute_device(self, params, device): self.set_parameters(params) if self._caching: - all_parameters = self.get_parameters(trainable_only=False) - hashed_params = _hash_iterable(all_parameters) - if hashed_params in self._cache_execute: + circuit_hash = self.graph.hash + if circuit_hash in self._cache_execute: self.set_parameters(saved_parameters) - return self._cache_execute[hashed_params] + return self._cache_execute[circuit_hash] if isinstance(device, qml.QubitDevice): res = device.execute(self) @@ -962,8 +960,8 @@ def execute_device(self, params, device): # restore original parameters self.set_parameters(saved_parameters) - if self._caching and hashed_params not in self._cache_execute: - self._cache_execute[hashed_params] = res + if self._caching and circuit_hash not in self._cache_execute: + self._cache_execute[circuit_hash] = res if len(self._cache_execute) > self._caching: self._cache_execute.popitem(last=False) diff --git a/pennylane/utils.py b/pennylane/utils.py index 5f864464181..af58d1f08f0 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -433,22 +433,3 @@ def expand_vector(vector, original_wires, expanded_wires): expanded_tensor = np.moveaxis(expanded_tensor, original_indices, wire_indices) return expanded_tensor.reshape(2 ** M) - - -def _hash_iterable(iterable): - """Returns a single hash of an input iterable. - - The iterable must be flat and can contain only numbers and NumPy arrays. - - Args: - iterable (Iterable): the iterable to generate a hash for - - Returns: - int: the resulting hash - """ - hashes = [] - for obj in iterable: - to_hash = (obj.tobytes(), obj.shape) if isinstance(obj, np.ndarray) else obj - obj_hash = hash(to_hash) - hashes.append(obj_hash) - return hash(tuple(hashes)) diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index f5a48ccbffc..dd2d8372d9c 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -21,7 +21,6 @@ from pennylane.beta.tapes import QuantumTape, qnode from pennylane.devices import DefaultQubit from pennylane.devices.default_qubit_autograd import DefaultQubitAutograd -from pennylane.utils import _hash_iterable def get_tape(caching): @@ -94,27 +93,12 @@ def test_add_to_cache_execute(self): result = tape.execute(device=dev) cache_execute = tape._cache_execute - params = tape.get_parameters() - hashed = _hash_iterable(params) + hashed = tape.graph.hash assert len(cache_execute) == 1 assert hashed in cache_execute assert np.allclose(cache_execute[hashed], result) - def test_get_all_parameters(self, mocker): - """Test that input params are correctly passed to the hash function when only a - subset of params are trainable""" - dev = qml.device("default.qubit", wires=2) - tape = get_tape(10) - - tape.trainable_params = {1} - spy = mocker.spy(pennylane.beta.tapes.tape, "_hash_iterable") - tape.execute(params=[-0.1], device=dev) - call = spy.call_args_list[0][0][0] - expected_call = [np.eye(2), -0.1, 0.2] - for arg, exp_arg in zip(call, expected_call): - assert np.allclose(arg, exp_arg) - def test_fill_cache(self): """Test that the cache is added to until it reaches its maximum size (in this case 10), and then maintains that size upon subsequent additions.""" diff --git a/tests/test_utils.py b/tests/test_utils.py index fa66c44de00..a88a2251aa1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -826,28 +826,3 @@ def test_non_operations_in_list(self, arg): ValueError, match="The given operation_list does not only contain Operations" ): pu.inv(arg) - - -class TestHashIterable: - """Tests for the _hash_iterable function.""" - - iterables = [ - [1, 1.4, -1], - [], - [1, np.ones((4, 4)), 19.67], - [1, np.ones((2, 4)), np.ones((2, 4)), 19.67], - [1, np.zeros((4, 4)), 19.67], - ] - - @pytest.mark.parametrize("iterable", iterables) - def test_valid_hash(self, iterable): - """Test that a valid hash is generated and that it is the same when generated again""" - h = pu._hash_iterable(iterable) - h2 = pu._hash_iterable(iterable) - assert isinstance(h, int) - assert h == h2 - - @pytest.mark.parametrize("iterable1, iterable2", itertools.combinations(iterables, r=2)) - def test_different(self, iterable1, iterable2): - """Test that a different hash is given for each test iterable""" - assert pu._hash_iterable(iterable1) != pu._hash_iterable(iterable2) From b8909222079211bb567187292f84b98f9fc64053 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 09:42:17 -0400 Subject: [PATCH 16/23] Use circuit hash --- pennylane/beta/tapes/qnode.py | 15 ++++----------- tests/beta/tapes/test_caching.py | 17 ++++++----------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 64404fd74e9..78943927c4e 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -101,8 +101,7 @@ class QNode: caching (int): number of device executions to store in a cache to speed up subsequent executions. A value of ``0`` indicates that no caching will take place. Once filled, older elements of the cache are removed and replaced with the most recent device - executions to keep the cache up to date. In caching mode, the quantum circuit - being executed must have a constant structure and only its parameters can be varied. + executions to keep the cache up to date. Keyword Args: h=1e-7 (float): step size for the finite difference method @@ -150,13 +149,8 @@ def __init__( """float: number of device executions to store in a cache to speed up subsequent executions. If set to zero, no caching occurs.""" - if caching != 0: - warnings.warn( - "Caching mode activated. The quantum circuit being executed by the QNode must have " - "a fixed structure.", - ) - if self.diff_method == "backprop": - raise ValueError('Caching mode is incompatible with the "backprop" diff_method') + if caching != 0 and self.diff_method == "backprop": + raise ValueError('Caching mode is incompatible with the "backprop" diff_method') self._cache_execute = OrderedDict() """OrderedDict[int: Any]: A copy of the ``_cache_execute`` dictionary from the quantum @@ -555,8 +549,7 @@ def qnode(device, interface="autograd", diff_method="best", caching=0, **diff_op caching (int): number of device executions to store in a cache to speed up subsequent executions. A value of ``0`` indicates that no caching will take place. Once filled, older elements of the cache are removed and replaced with the most recent device - executions to keep the cache up to date. In caching mode, the quantum circuit - being executed must have a constant structure and only its parameters can be varied. + executions to keep the cache up to date. Keyword Args: h=1e-7 (float): Step size for the finite difference method. diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index dd2d8372d9c..342876fe55c 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -150,24 +150,19 @@ def test_caching_multiple_values(self, mocker): spy.assert_not_called() -@pytest.mark.filterwarnings("ignore:Caching mode activated") class TestQNodeCaching: """Tests for caching when using the QNode""" def test_set_and_get(self): """Test that the caching attribute can be set and accessed""" - with pytest.warns(None) as warn: - qnode = get_qnode(caching=0) - assert qnode.caching == 0 + qnode = get_qnode(caching=0) + assert qnode.caching == 0 - assert len(warn) is 0 # assert that no warning took place - - with pytest.warns(UserWarning, match="Caching mode activated."): - qnode = get_qnode(caching=10) - assert qnode.caching == 10 + qnode = get_qnode(caching=10) + assert qnode.caching == 10 - qnode.caching = 20 - assert qnode.caching == 20 + qnode.caching = 20 + assert qnode.caching == 20 def test_backprop_error(self): """Test if an error is raised when caching is used with the backprop diff_method""" From a640e26bcd08bef4b02ed83118aa4c06a348e0d4 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 11:10:15 -0400 Subject: [PATCH 17/23] Add to tests --- tests/beta/tapes/test_caching.py | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index 342876fe55c..b720e3d56a0 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -237,3 +237,76 @@ def test_gradient_torch(self, mocker): res = qnode(args0, args1) res.backward() spy.assert_not_called() + + def test_mutable_circuit(self, mocker): + """Test that caching is compatible with circuit mutability. Caching should take place if + the circuit and parameters are the same, and should not take place if the parameters are + the same but the circuit different.""" + dev = qml.device("default.qubit", wires=3) + + @qnode(dev, caching=10) + def qfunc(x, y, flag=1): + if flag == 1: + qml.RX(x, wires=0) + qml.RX(y, wires=1) + else: + qml.RX(x, wires=1) + qml.RX(y, wires=1) + qml.CNOT(wires=[0, 1]) + return expval(qml.PauliZ(wires=1)) + + spy = mocker.spy(DefaultQubit, "execute") + qfunc(0.1, 0.2) + qfunc(0.1, 0.2) + assert len(spy.call_args_list) == 1 + + qfunc(0.1, 0.2, flag=0) + assert len(spy.call_args_list) == 2 + + def test_classical_processing_in_circuit(self, mocker): + """Test if caching is compatible with QNodes that include classical processing""" + + dev = qml.device("default.qubit", wires=3) + + @qnode(dev, caching=10) + def qfunc(x, y): + qml.RX(x ** 2, wires=0) + qml.RX(x / y, wires=1) + qml.CNOT(wires=[0, 1]) + return expval(qml.PauliZ(wires=1)) + + spy = mocker.spy(DefaultQubit, "execute") + qfunc(0.1, 0.2) + qfunc(0.1, 0.2) + assert len(spy.call_args_list) == 1 + + qfunc(0.1, 0.3) + assert len(spy.call_args_list) == 2 + + def test_grad_classical_processing_in_circuit(self, mocker): + """Test that caching is compatible with calculating the gradient in QNodes which contain + classical processing""" + + dev = qml.device("default.qubit", wires=3) + + @qnode(dev, caching=10) + def qfunc(x, y): + qml.RX(x ** 2, wires=0) + qml.RX(x / y, wires=1) + qml.CNOT(wires=[0, 1]) + return expval(qml.PauliZ(wires=1)) + + d_qfunc = qml.grad(qfunc) + + spy = mocker.spy(DefaultQubit, "execute") + g = d_qfunc(0.1, 0.2) + calls1 = len(spy.call_args_list) + d_qfunc(0.1, 0.2) + calls2 = len(spy.call_args_list) + assert calls1 == calls2 + + d_qfunc(0.1, 0.3) + calls3 = len(spy.call_args_list) + assert calls3 == 2 * calls1 + + assert g is not None From 0be4262496d8c09df132f6bccff0a5c244aeefa3 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 22 Sep 2020 11:18:32 -0400 Subject: [PATCH 18/23] Tidy up imports --- tests/beta/tapes/test_caching.py | 1 - tests/test_utils.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index b720e3d56a0..a8eacd0f986 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -16,7 +16,6 @@ import pytest import pennylane as qml -import pennylane from pennylane.beta.queuing import expval from pennylane.beta.tapes import QuantumTape, qnode from pennylane.devices import DefaultQubit diff --git a/tests/test_utils.py b/tests/test_utils.py index a88a2251aa1..5922acd2eb8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,7 +17,6 @@ # pylint: disable=no-self-use,too-many-arguments,protected-access import functools import itertools -from unittest.mock import MagicMock import pytest import numpy as np @@ -25,7 +24,6 @@ import pennylane as qml import pennylane._queuing import pennylane.utils as pu -from pennylane.wires import Wires from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane.operation import Tensor From 950e8f48b6760ae7f4e377cda5995cb535654a77 Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Tue, 22 Sep 2020 14:08:06 -0400 Subject: [PATCH 19/23] Apply suggestions from code review Co-authored-by: Theodor --- pennylane/beta/tapes/qnode.py | 4 ++-- pennylane/beta/tapes/tape.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 78943927c4e..f14ee61dd81 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -98,7 +98,7 @@ class QNode: * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation arguments. - caching (int): number of device executions to store in a cache to speed up subsequent + caching (int): Number of device executions to store in a cache to speed up subsequent executions. A value of ``0`` indicates that no caching will take place. Once filled, older elements of the cache are removed and replaced with the most recent device executions to keep the cache up to date. @@ -546,7 +546,7 @@ def qnode(device, interface="autograd", diff_method="best", caching=0, **diff_op * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation arguments. - caching (int): number of device executions to store in a cache to speed up subsequent + caching (int): Number of device executions to store in a cache to speed up subsequent executions. A value of ``0`` indicates that no caching will take place. Once filled, older elements of the cache are removed and replaced with the most recent device executions to keep the cache up to date. diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index fd128fb6c11..3e2c91a388b 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -148,7 +148,7 @@ class QuantumTape(AnnotatedQueue): Args: name (str): a name given to the quantum tape - caching (int): number of device executions to store in a cache to speed up subsequent + caching (int): Number of device executions to store in a cache to speed up subsequent executions. A value of ``0`` indicates that no caching will take place. Once filled, older elements of the cache are removed and replaced with the most recent device executions to keep the cache up to date. From c3a56fc3660ce00fe15476ec7909cf31359ae705 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 23 Sep 2020 16:47:07 -0400 Subject: [PATCH 20/23] Add to changelog --- .github/CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2031bdcadb6..6afb6660a2d 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -27,6 +27,29 @@

Improvements

+* QNode caching has been introduced, allowing the QNode to keep track of the results of previous + device executions and reuse those results in subsequent calls. + [(#817)](https://github.com/PennyLaneAI/pennylane/pull/817) + + Caching is available by passing a ``caching`` argument to the QNode: + + ```python + from pennylane.beta.tapes import qnode + from pennylane.beta.queuing import expval + + dev = qml.device("default.qubit", wires=2) + + @qnode(dev, caching=10) # cache up to 10 evaluations + def qfunc(x): + qml.RX(x, wires=0) + qml.RX(0.3, wires=1) + qml.CNOT(wires=[0, 1]) + return expval(qml.PauliZ(1)) + + qfunc(0.1) # first evaluation executes on the device + qfunc(0.1) # second evaluation accesses the cached result + ``` + * Sped up the application of certain gates in `default.qubit` by using array/tensor manipulation tricks. The following gates are affected: `PauliX`, `PauliY`, `PauliZ`, `Hadamard`, `SWAP`, `S`, `T`, `CNOT`, `CZ`. From c0e10b3f504b1ec1f732acf5aad6ae391da6162b Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 25 Sep 2020 10:30:17 -0400 Subject: [PATCH 21/23] Apply suggestions --- pennylane/beta/interfaces/autograd.py | 2 -- pennylane/beta/tapes/qnode.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pennylane/beta/interfaces/autograd.py b/pennylane/beta/interfaces/autograd.py index 979118ca46b..4d9534ec3a2 100644 --- a/pennylane/beta/interfaces/autograd.py +++ b/pennylane/beta/interfaces/autograd.py @@ -117,8 +117,6 @@ def get_parameters(self, trainable_only=True): # pylint: disable=missing-functi for idx, p in enumerate(self._all_parameter_values) if idx in self.trainable_params ] - else: - params = self._all_parameter_values return autograd.builtins.list(params) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index aec8142a710..716f7a9b4dd 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -426,6 +426,9 @@ def __call__(self, *args, **kwargs): self.construct(args, kwargs) if self._caching: + # Every time the QNode is called, it creates a new tape. We want the tape cache to + # persist over multiple tapes, so hence keep track of it as a QNode attribute and + # load it into the new tape self.qtape._cache_execute = self._cache_execute # execute the tape From c438d5a2d173f6f63e5bc60c09f8c455a856c37f Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 25 Sep 2020 10:31:55 -0400 Subject: [PATCH 22/23] Remove caching setter --- pennylane/beta/tapes/qnode.py | 4 ---- pennylane/beta/tapes/tape.py | 4 ---- tests/beta/tapes/test_caching.py | 6 ------ 3 files changed, 14 deletions(-) diff --git a/pennylane/beta/tapes/qnode.py b/pennylane/beta/tapes/qnode.py index 716f7a9b4dd..b51694c8dd9 100644 --- a/pennylane/beta/tapes/qnode.py +++ b/pennylane/beta/tapes/qnode.py @@ -529,10 +529,6 @@ def caching(self): executions. If set to zero, no caching occurs.""" return self._caching - @caching.setter - def caching(self, value): - self._caching = value - INTERFACE_MAP = {"autograd": to_autograd, "torch": to_torch, "tf": to_tf} diff --git a/pennylane/beta/tapes/tape.py b/pennylane/beta/tapes/tape.py index 0c5583edcef..6eb158773a2 100644 --- a/pennylane/beta/tapes/tape.py +++ b/pennylane/beta/tapes/tape.py @@ -1379,7 +1379,3 @@ def caching(self): """float: number of device executions to store in a cache to speed up subsequent executions. If set to zero, no caching occurs.""" return self._caching - - @caching.setter - def caching(self, value): - self._caching = value diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index a8eacd0f986..550ca64909b 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -58,9 +58,6 @@ def test_set_and_get(self): tape = QuantumTape(caching=10) assert tape.caching == 10 - tape.caching = 20 - assert tape.caching == 20 - def test_no_caching(self, mocker): """Test that no caching occurs when the caching attribute is equal to zero""" dev = qml.device("default.qubit", wires=2) @@ -160,9 +157,6 @@ def test_set_and_get(self): qnode = get_qnode(caching=10) assert qnode.caching == 10 - qnode.caching = 20 - assert qnode.caching == 20 - def test_backprop_error(self): """Test if an error is raised when caching is used with the backprop diff_method""" with pytest.raises(ValueError, match="Caching mode is incompatible"): From 0b0d9d4453315413a5ee6aced192fb65481cfe8e Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 25 Sep 2020 10:40:02 -0400 Subject: [PATCH 23/23] Apply suggestion --- tests/beta/tapes/test_caching.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/beta/tapes/test_caching.py b/tests/beta/tapes/test_caching.py index 550ca64909b..f1a8b509d4a 100644 --- a/tests/beta/tapes/test_caching.py +++ b/tests/beta/tapes/test_caching.py @@ -168,13 +168,13 @@ def test_caching(self, mocker): qnode = get_qnode(caching=10) args = np.arange(10) - for arg in args[:10]: + for arg in args: qnode(arg, 0.2) assert qnode.qtape.caching == 10 spy = mocker.spy(DefaultQubitAutograd, "execute") - for arg in args[:10]: + for arg in args: qnode(arg, 0.2) spy.assert_not_called()