Repository: PennyLaneAI/pennylane
Pull request #2154 (2022-02-02 16:53:22)

Deprecate `QubitDevice`'s caching
**Context**

The `QubitDevice` class provides the ability to store device executions in a cache. With the latest versions of QNodes, caching is available on the QNode level too.

By default, QNode caching will only create the cache per QNode call/execution. So the default caching behaviour will cache all repeated circuit evaluations during a single call.

For example, a single call might cause multiple circuits internally for the parameter-shift rule; circuits that end up being identical will not be re-executed, simply the cached result will be retrieved.

This is deliberate, as users expect different results to be returned when shot noise is present (see discussion here https://github.com/PennyLaneAI/pennylane/issues/2150). However, it is possible to change this behaviour. If a user creates a cache manually and pass this to the QNode, the QNode will keep using this cache object for separate evaluations:

```python
dev = qml.device('default.qubit', wires=nwires)
​
cache = {}
​
@qml.qnode(dev, diff_method='parameter-shift', cache=cache)
def expval_circuit(params):
    qml.templates.BasicEntanglerLayers(params, wires=wires, rotation=qml.RX)
    return qml.expval(qml.Hermitian(hamiltonian, wires=wires))
​
@qml.qnode(dev, diff_method='parameter-shift', cache=cache)
def var_circuit(params):
    qml.templates.BasicEntanglerLayers(params, wires=wires, rotation=qml.RX)
    return qml.var(qml.Hermitian(hamiltonian, wires=wires))
​
>>> print(expval_circuit(params), var_circuit(params)))
-2.6224875809703287, 0.011605015096940896
>>> print(cache)
{-2104543859499562169: array([-2.62248758]), 3268478774057874589: array([0.01160502])}
>>> print(expval_circuit(params), var_circuit(params)))  # evaluate QNodes again
-2.6224875809703287, 0.011605015096940896
>>> dev.num_executions  # only the first two executions will register
2
```

![image (10)](https://user-images.githubusercontent.com/24476053/152220617-8869a754-a17d-4aa6-a35f-4f9ef2bf4ecd.png)

**Changes**

* Places a deprecation warning in the `QubitDevice` class mentioning that the caching ability of the class is being deprecated.

**Benefits**

Moving towards having a single way of solving the same issue.

**Note**

Likely the `dev.num_execution` has to be adjusted when using `diff_method="backprop"` because multiple executions are recorded whereas the cache itself is being populated and used:
![Screenshot from 2022-02-02 14-54-03](https://user-images.githubusercontent.com/24476053/152227059-5c7be907-132b-4ddc-a280-f35f608d60d3.png)

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
josh146: > Likely the dev.num_execution has to be adjusted when using diff_method="backprop" because multiple executions are recorded whereas the cache itself is being populated and used:

I wonder if [`qml.Tracker`](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.Tracker.html) is also affected?
antalszava: [sc-14402]
antalszava: Note: I'd like to merge this PR iff https://github.com/PennyLaneAI/pennylane/pull/2171 has been merged. That PR fixes a bug with QNode caching when recording the number of executions.
antalszava: > I wonder if [qml.Tracker](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.Tracker.html) is also affected?

I don't think so, as the issue was inside of `qml.QNode`. Tracking happens on the device level.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 4c34dcf01..71cd2ae23 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -524,6 +524,30 @@
   qml.grad(circuit, argnum=1)(0.5, x)
   ```
 
+<h3>Deprecations</h3>
+
+* Deprecates the caching ability provided by `QubitDevice`.
+  [(#2154)](https://github.com/PennyLaneAI/pennylane/pull/2154)
+
+  Going forward, the preferred way is to use the caching abilities of the
+  QNode:
+  ```python
+  dev = qml.device("default.qubit", wires=2)
+
+  cache = {}
+
+  @qml.qnode(dev, diff_method='parameter-shift', cache=cache)
+  def circuit():
+      qml.RY(0.345, wires=0)
+      return qml.expval(qml.PauliZ(0))
+  ```
+  ```pycon
+  >>> for _ in range(10):
+  ...    circuit()
+  >>> dev.num_executions
+  1
+  ```
+
 <h3>Bug fixes</h3>
 
 * Fixes a bug where an incorrect number of executions are recorded by
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index 7831b930c..72e2f85e8 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -128,6 +128,13 @@ class QubitDevice(Device):
         """None or array[int]: stores the samples generated by the device
         *after* rotation to diagonalize the observables."""
 
+        if cache > 0:
+            warnings.warn(
+                "The caching ability of QubitDevice is being deprecated, passing a "
+                "dictionary as the cache argument to the QNode is preferred.",
+                UserWarning,
+            )
+
         self._cache = cache
         """int: Number of device executions to store in a cache to speed up subsequent
         executions. If set to zero, no caching occurs."""


Test patch:
diff --git a/tests/devices/test_caching.py b/tests/devices/test_caching.py
index cc2b99f23..7ce56954f 100644
--- a/tests/devices/test_caching.py
+++ b/tests/devices/test_caching.py
@@ -29,6 +29,14 @@ def qfunc(x, y):
     return expval(qml.PauliZ(wires=1))
 
 
+def get_device_with_caching(wires, cache):
+    """Get a device that defines caching."""
+    with pytest.warns(UserWarning, match="deprecated"):
+        dev = qml.device("default.qubit", wires=wires, cache=cache)
+
+    return dev
+
+
 class TestCaching:
     """Tests for device caching"""
 
@@ -37,7 +45,7 @@ class TestCaching:
         dev = qml.device("default.qubit", wires=2)
         assert dev.cache == 0
 
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         assert dev.cache == 10
 
     def test_no_caching(self, mocker):
@@ -54,7 +62,7 @@ class TestCaching:
 
     def test_caching(self, mocker):
         """Test that caching occurs when the cache attribute is above zero"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         qn(0.1, 0.2)
@@ -66,7 +74,7 @@ class TestCaching:
 
     def test_add_to_cache_execute(self):
         """Test that the _cache_execute attribute is added to when the device is executed"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         result = qn(0.1, 0.2)
@@ -80,7 +88,7 @@ class TestCaching:
     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, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         args = np.arange(20)
@@ -96,7 +104,7 @@ class TestCaching:
     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, cache=2)
+        dev = get_device_with_caching(wires=2, cache=2)
         qn = QNode(qfunc, dev)
 
         qn(0.1, 0.2)
@@ -110,7 +118,7 @@ class TestCaching:
     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, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         args = np.arange(10)
@@ -126,7 +134,7 @@ class TestCaching:
 
     def test_backprop_error(self):
         """Test if an error is raised when caching is used with the backprop diff_method"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         with pytest.raises(
             qml.QuantumFunctionError, match="Device caching is incompatible with the backprop"
         ):
@@ -135,7 +143,7 @@ class TestCaching:
     def test_gradient_autograd(self, mocker):
         """Test that caching works when calculating the gradient using the autograd
         interface"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev, interface="autograd")
         d_qnode = qml.grad(qn)
         args = np.array([0.1, 0.2], requires_grad=True)
@@ -150,7 +158,7 @@ class TestCaching:
         """Test that caching works when calculating the gradient using the TF interface"""
         import tensorflow as tf
 
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev, interface="tf")
         args0 = tf.Variable(0.1)
         args1 = tf.Variable(0.2)
@@ -173,7 +181,7 @@ class TestCaching:
         """Test that caching works when calculating the gradient using the Torch interface"""
         import torch
 
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev, interface="torch")
         args0 = torch.tensor(0.1, requires_grad=True)
         args1 = torch.tensor(0.2)
@@ -191,7 +199,7 @@ class TestCaching:
         """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, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
 
         @qnode(dev)
         def qfunc(x, y, flag=1):
@@ -215,7 +223,7 @@ class TestCaching:
     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, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
 
         @qnode(dev)
         def qfunc(x, y):
@@ -236,7 +244,7 @@ class TestCaching:
         """Test that caching is compatible with calculating the gradient in QNodes which contain
         classical processing"""
 
-        dev = qml.device("default.qubit", wires=3, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
 
         @qnode(dev)
         def qfunc(x, y):
@@ -265,7 +273,7 @@ class TestCaching:
 
     devs = [
         (qml.device("default.qubit", wires=2)),
-        (qml.device("default.qubit", cache=1, wires=2)),
+        (get_device_with_caching(wires=2, cache=10)),
     ]
 
     @pytest.mark.parametrize("dev", devs)