Repository: PennyLaneAI/pennylane
Pull request #5243 (2024-02-21 23:10:18)

Turn caching off by default when `max_diff==1`
**Context:**

Recent benchmarks (see https://github.com/PennyLaneAI/pennylane/pull/5211#issuecomment-1948619489) have shown that caching adds massive classical overheads, but often does not actually lead to a reduction in the the number of executions in normal workflows.

Because of this, we want to make smart choices about when to use caching. Higher-order derivatives often result in duplicate circuits, so we need to keep caching when calculating higher-order derivatives.  But, we can make caching opt-in for normal workflows. This will lead to reduced overheads in the vast majority of workflows.

**Description of the Change:**

The `QNode` keyword argument `cache` defaults to `None`.  This is interpreted as `True` if `max_diff > 1` and `False` otherwise.

**Benefits:**

Vastly reduced classical overheads in most cases.

**Possible Drawbacks:**

Increased number of executions in a few edge cases.  But these edge cases would be fairly convoluted. Somehow a transform would have to turn the starting tape into two identical tapes.

**Related GitHub Issues:**

**Performance Numbers:**

```
n_layers = 1

dev = qml.device('lightning.qubit', wires=n_wires)

shape = qml.StronglyEntanglingLayers.shape(n_wires=n_wires, n_layers=n_layers)
rng = qml.numpy.random.default_rng(seed=42)
params = rng.random(shape)

@qml.qnode(dev, cache=False)
def circuit(params):
    qml.StronglyEntanglingLayers(params, wires=range(n_wires))
    return qml.expval(qml.Z(0))

@qml.qnode(dev, cache=True)
def circuit_cache(params):
    qml.StronglyEntanglingLayers(params, wires=range(n_wires))
    return qml.expval(qml.Z(0))
```

For `n_wires = 20`
![Screenshot 2024-02-21 at 5 19 49 PM](https://github.com/PennyLaneAI/pennylane/assets/6364575/8b94647c-b0f3-4b0b-8fc7-62c26ac06d70)

But for `n_wires= 10`:
![Screenshot 2024-02-21 at 5 20 43 PM](https://github.com/PennyLaneAI/pennylane/assets/6364575/04248f9e-c949-49a3-a125-21025fd030d6)

For `n_wires=20 n_layers=5`, we have:
![Screenshot 2024-02-21 at 6 02 40 PM](https://github.com/PennyLaneAI/pennylane/assets/6364575/feeccd11-cc52-440d-8655-14a2d15ed93d)

While the cache version does seem to be faster here, that does seem to be statistical fluctuations.

For `n_wires=10 n_layers=20`:

![Screenshot 2024-02-21 at 6 09 04 PM](https://github.com/PennyLaneAI/pennylane/assets/6364575/64801dd4-a1ce-40c5-84ae-367ddf66aa26)



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5243?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`e254301`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/e2543019ef9673233f3c8eeb747b9d5990e41502?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`9a75f5f`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5243?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5243      +/-   ##
==========================================
- Coverage   99.68%   99.68%   -0.01%     
==========================================
  Files         399      399              
  Lines       36797    36516     -281     
==========================================
- Hits        36682    36400     -282     
- Misses        115      116       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5243?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

albi3ro: > Thanks @albi3ro!
> 
> Out of curiosity, I wonder if the caching affected any first-order gradient workflows that used `qml.jacobian()`? That is, any workflows where multiple identical tapes would be executed to build up the Jacobian?

We add an additional mandatory level of caching for autograd, so `cache_execute` was no influencing it.
timmysilv: i'm not sure what the current state of the benchmarking suite is, but perhaps this PR would be a good opportunity to test it out?
albi3ro: [sc-57404]
albi3ro: > i'm not sure what the current state of the benchmarking suite is, but perhaps this PR would be a good opportunity to test it out?

Current benchmarks are using a github runner and too noisy to extract reliable data.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 752cbbfc9..8440d2ed9 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -396,6 +396,10 @@
 
 <h3>Breaking changes 💔</h3>
 
+* Caching of executions is now turned off by default when `max_diff == 1`, as the classical overhead cost
+  outweighs the probability that duplicate circuits exists.
+  [(#5243)](https://github.com/PennyLaneAI/pennylane/pull/5243)
+
 * The entry point convention registering compilers with PennyLane has changed.
   [(#5140)](https://github.com/PennyLaneAI/pennylane/pull/5140)
 
diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py
index da26c4286..4c5ae78af 100644
--- a/pennylane/workflow/qnode.py
+++ b/pennylane/workflow/qnode.py
@@ -183,8 +183,9 @@ class QNode:
             Only applies if the device is queried for the gradient; gradient transform
             functions available in ``qml.gradients`` are only supported on the backward
             pass. The 'best' option chooses automatically between the two options and is default.
-        cache (bool or dict or Cache): Whether to cache evaluations. This can result in
-            a significant reduction in quantum evaluations during gradient computations.
+        cache="auto" (str or bool or dict or Cache): Whether to cache evalulations.
+            ``"auto"`` indicates to cache only when ``max_diff > 1``. This can result in
+            a reduction in quantum evaluations during higher order gradient computations.
             If ``True``, a cache with corresponding ``cachesize`` is created for each batch
             execution. If ``False``, no caching is used. You may also pass your own cache
             to be used; this can be any object that implements the special methods
@@ -415,7 +416,7 @@ class QNode:
         expansion_strategy="gradient",
         max_expansion=10,
         grad_on_execution="best",
-        cache=True,
+        cache="auto",
         cachesize=10000,
         max_diff=1,
         device_vjp=False,
@@ -483,6 +484,7 @@ class QNode:
         self.diff_method = diff_method
         self.expansion_strategy = expansion_strategy
         self.max_expansion = max_expansion
+        cache = (max_diff > 1) if cache == "auto" else cache
 
         # execution keyword arguments
         self.execute_kwargs = {


Test patch:
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
index ced345496..56d64f95d 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -77,6 +77,26 @@ def test_copy():
     assert copied_qn.expansion_strategy == qn.expansion_strategy
 
 
+class TestInitialization:
+    def test_cache_initialization_maxdiff_1(self):
+        """Test that when max_diff = 1, the cache initializes to false."""
+
+        @qml.qnode(qml.device("default.qubit"), max_diff=1)
+        def f():
+            return qml.state()
+
+        assert f.execute_kwargs["cache"] is False
+
+    def test_cache_initialization_maxdiff_2(self):
+        """Test that when max_diff = 2, the cache initialization to True."""
+
+        @qml.qnode(qml.device("default.qubit"), max_diff=2)
+        def f():
+            return qml.state()
+
+        assert f.execute_kwargs["cache"] is True
+
+
 # pylint: disable=too-many-public-methods
 class TestValidation:
     """Tests for QNode creation and validation"""