Repository: PennyLaneAI/pennylane
Pull request #7644 (2025-06-10 15:04:34)

Caching defaults to auto with execute
**Context:**

Caching can get expensive when we don't actually have duplicate tapes, so it's not worth it when `max_diff=1` the vast majority of the time. Also, it can cause confusion when it eliminates the expected randomness.

**Description of the Change:**

Defaults `cache="auto"` in `qml.execute` matching the behavior of `QNode`.

*Always* warn about caching with finite shots.

**Benefits:**

Increased performance and reduced confusion.

**Possible Drawbacks:**

**Related GitHub Issues:**

Fixes #7643  [sc-92964]

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7644?dropdown=coverage&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:
> Project coverage is 99.68%. Comparing base [(`362d9e3`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/362d9e3a7da2e55a423fed6f54c883d643225bb3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`3c66f22`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/3c66f220e56c830210e3bdbcb42dbaba7434bb22?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

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


```diff
@@            Coverage Diff             @@
##           master    #7644      +/-   ##
==========================================
- Coverage   99.68%   99.68%   -0.01%     
==========================================
  Files         530      530              
  Lines       52148    52150       +2     
==========================================
+ Hits        51985    51986       +1     
- Misses        163      164       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7644?dropdown=coverage&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).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 8b62cd681..02dcf14c2 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -364,6 +364,13 @@
 
 <h3>Improvements 🛠</h3>
 
+* Caching with finite shots now always warns about the lack of expected noise.
+  [(#7644)](https://github.com/PennyLaneAI/pennylane/pull/7644)
+
+* `cache` now defaults to `"auto"` with `qml.execute`, matching the behavior of `QNode` and reducing the 
+  performance cost of using `qml.execute` for standard executions.
+  [(#7644)](https://github.com/PennyLaneAI/pennylane/pull/7644)
+
 * `qml.grad` and `qml.jacobian` can now handle inputs with dynamic shapes being captured into plxpr.
   [(#7544)](https://github.com/PennyLaneAI/pennylane/pull/7544/)
 
@@ -547,6 +554,10 @@
 
 <h3>Breaking changes 💔</h3>
 
+* The default value of `cache` is now `"auto"` with `qml.execute`. Like `QNode`, `"auto"` only turns on caching
+  when `max_diff > 1`.
+  [(#7644)](https://github.com/PennyLaneAI/pennylane/pull/7644)
+
 * A new decomposition for two-qubit unitaries was implemented in `two_qubit_decomposition`.
   It ensures the correctness of the decomposition in some edge cases but uses 3 CNOT gates
   even if 2 CNOTs would suffice theoretically.
diff --git a/pennylane/workflow/_cache_transform.py b/pennylane/workflow/_cache_transform.py
index 03dc85640..70262b5cf 100644
--- a/pennylane/workflow/_cache_transform.py
+++ b/pennylane/workflow/_cache_transform.py
@@ -43,7 +43,7 @@ def _cache_transform(tape: QuantumScript, cache: MutableMapping):
     def cache_hit_postprocessing(_results: ResultBatch) -> Result:
         result = cache[tape.hash]
         if result is not None:
-            if tape.shots and getattr(cache, "_persistent_cache", True):
+            if tape.shots:
                 warnings.warn(_CACHED_EXECUTION_WITH_FINITE_SHOTS_WARNINGS, UserWarning)
             return result
 
diff --git a/pennylane/workflow/_setup_transform_program.py b/pennylane/workflow/_setup_transform_program.py
index fd1ba0c3c..325e5e0be 100644
--- a/pennylane/workflow/_setup_transform_program.py
+++ b/pennylane/workflow/_setup_transform_program.py
@@ -90,9 +90,13 @@ def _setup_transform_program(
     _prune_dynamic_transform(outer_transform_program, inner_transform_program)
 
     # If caching is desired but an explicit cache is not provided, use an ``LRUCache``.
+    if cache == "auto":
+        if resolved_execution_config.derivative_order == 1:
+            cache = None
+        else:
+            cache = True
     if cache is True:
         cache = LRUCache(maxsize=cachesize)
-        setattr(cache, "_persistent_cache", False)
 
     # Ensure that ``cache`` is not a Boolean to simplify downstream code.
     elif cache is False:
diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py
index d42450d56..97aa4dbc0 100644
--- a/pennylane/workflow/execution.py
+++ b/pennylane/workflow/execution.py
@@ -47,7 +47,7 @@ def execute(
     *,
     transform_program: TransformProgram = None,
     grad_on_execution: Literal[True, False, "best"] = "best",
-    cache: Union[None, bool, dict, Cache] = True,
+    cache: Union[None, bool, dict, Cache, Literal["auto"]] = "auto",
     cachesize: int = 10000,
     max_diff: int = 1,
     device_vjp: Union[bool, None] = False,
@@ -75,8 +75,13 @@ def execute(
             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 (None, bool, dict, 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
+            ``__getitem__()``, ``__setitem__()``, and ``__delitem__()``, such as a dictionary.
         cachesize (int): the size of the cache.
         max_diff (int): If ``diff_method`` is a gradient transform, this option specifies
             the maximum number of derivatives to support. Increasing this value allows
diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py
index 7563bb9dc..47bbb6979 100644
--- a/pennylane/workflow/qnode.py
+++ b/pennylane/workflow/qnode.py
@@ -613,7 +613,6 @@ class QNode:
             _validate_jax_version()
         self.diff_method = diff_method
         _validate_diff_method(self.device, self.diff_method)
-        cache = (max_diff > 1) if cache == "auto" else cache
 
         self.capture_cache = LRUCache(maxsize=1000)
         if isinstance(static_argnums, int):


Test patch:
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
index ceb261d00..d801fc771 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -229,7 +229,7 @@ class TestInitialization:
         def f():
             return qml.state()
 
-        assert f.execute_kwargs["cache"] is False
+        assert f.execute_kwargs["cache"] == "auto"
 
     def test_cache_initialization_maxdiff_2(self):
         """Test that when max_diff = 2, the cache initialization to True."""
@@ -238,7 +238,7 @@ class TestInitialization:
         def f():
             return qml.state()
 
-        assert f.execute_kwargs["cache"] is True
+        assert f.execute_kwargs["cache"] == "auto"
 
 
 # pylint: disable=too-many-public-methods
@@ -1394,6 +1394,20 @@ class TestShots:
         with pytest.warns(UserWarning, match="Cached execution with finite shots detected"):
             qml.execute([tape], dev, None, cache=cache)
 
+        cache2 = {}
+        with pytest.warns(UserWarning, match="Cached execution with finite shots detected"):
+            qml.execute([tape, tape], dev, cache=cache2)
+
+    def test_no_caching_by_default(self):
+        """Test that caching is turned off by default."""
+
+        dev = qml.device("default.qubit", wires=1)
+
+        tape = qml.tape.QuantumScript([qml.H(0)], [qml.sample(wires=0)], shots=1000)
+
+        res1, res2 = qml.execute([tape, tape], dev)
+        assert not np.allclose(res1, res2)
+
     def test_no_warning_infinite_shots(self):
         """Tests that no warning is raised when caching is used with infinite shots."""
         dev = qml.device("default.qubit", wires=1)
diff --git a/tests/workflow/interfaces/execute/test_jax_jit.py b/tests/workflow/interfaces/execute/test_jax_jit.py
index bb3285a87..c8aa45fc3 100644
--- a/tests/workflow/interfaces/execute/test_jax_jit.py
+++ b/tests/workflow/interfaces/execute/test_jax_jit.py
@@ -195,6 +195,7 @@ class TestCaching:
                 [tape],
                 dev,
                 diff_method=param_shift,
+                cache=True,
                 cachesize=cachesize,
             )[0]
 
diff --git a/tests/workflow/interfaces/test_transform_program_integration.py b/tests/workflow/interfaces/test_transform_program_integration.py
index c346ea868..bf0dc9b5c 100644
--- a/tests/workflow/interfaces/test_transform_program_integration.py
+++ b/tests/workflow/interfaces/test_transform_program_integration.py
@@ -156,7 +156,7 @@ class TestTransformProgram:
         )
         with dev.tracker:
             results = qml.execute(
-                (tape1, tape2, tape3), dev, interface=interface, transform_program=prog
+                (tape1, tape2, tape3), dev, interface=interface, transform_program=prog, cache=True
             )
 
         assert qml.math.allclose(results[0], np.cos(1.2))
diff --git a/tests/workflow/test_cache_transform.py b/tests/workflow/test_cache_transform.py
index a270387f1..41087a40e 100644
--- a/tests/workflow/test_cache_transform.py
+++ b/tests/workflow/test_cache_transform.py
@@ -101,13 +101,13 @@ def test_batch_of_identical_tapes(cache):
     assert batch_fns((result,)) == (result, result, result)
 
 
-def test_finite_shots_with_persistent_cache_warning(cache):
+def test_finite_shots_with_persistent_cache_warning():
     """Tests that a UserWarning is emitted if a cache hit occurs for a tape with
-    finite shots that uses a persistent cache.
+    finite shots that uses a cache.
     """
     tape = QuantumScript([], [qml.expval(qml.Z(0))], shots=1)
 
-    batch_tapes, batch_fns = _cache_transform([tape, tape], cache=cache)
+    batch_tapes, batch_fns = _cache_transform([tape, tape], cache={})
     assert len(batch_tapes) == 1
 
     with pytest.warns(UserWarning, match=r"Cached execution with finite shots detected!"):