Repository: PennyLaneAI/pennylane
Pull request #7050 (2025-03-06 21:29:08)

[Capture] Promote consts to normal args
**Context:**

After seeing how in catalyst that we can promote `constvars` to normal arguments, I realized this could make a lot our higher order primitives much simpler.  We would no longer need to slice and dice the consts and args. We could just treat them like the same thing.

**Description of the Change:**

Try promoting the `consts` from the qfunc jaxpr to being normal arguments.

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**

[sc-92591]


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7050?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 [(`bee1504`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/bee1504f7d88a092fee70b14bb1e2c6fc21b4f74?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`a6918c9`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/a6918c9bdc61df37ab5011c2c8edd8b7eb363c11?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    #7050      +/-   ##
==========================================
- Coverage   99.68%   99.68%   -0.01%     
==========================================
  Files         532      532              
  Lines       51894    51888       -6     
==========================================
- Hits        51730    51724       -6     
  Misses        164      164              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7050?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>
albi3ro: > Thanks, looks good. Will control flow, transforms, and grad/jacobian be addressed in a different PR?

Yes.  I found this to be a happy medium in size and complexity.  But yes, we should have a follow on.

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/capture/__init__.py b/pennylane/capture/__init__.py
index ae61f7be0..beb5d9c39 100644
--- a/pennylane/capture/__init__.py
+++ b/pennylane/capture/__init__.py
@@ -161,6 +161,7 @@ If needed, developers can also override the implementation method of the primiti
 from typing import Callable, Type
 
 from .switches import disable, enable, enabled, pause
+from .promote_consts import promote_consts
 from .capture_meta import CaptureMeta, ABCCaptureMeta
 from .flatfn import FlatFn
 from .make_plxpr import make_plxpr, run_autograph
diff --git a/pennylane/capture/base_interpreter.py b/pennylane/capture/base_interpreter.py
index d0c2307d9..47cfe3946 100644
--- a/pennylane/capture/base_interpreter.py
+++ b/pennylane/capture/base_interpreter.py
@@ -35,6 +35,7 @@ from .primitives import (
     qnode_prim,
     while_loop_prim,
 )
+from .promote_consts import promote_consts
 
 FlattenedHigherOrderPrimitives: dict["jax.extend.core.Primitive", Callable] = {}
 """
@@ -458,34 +459,27 @@ def _(self, *dyn_shape, dimension, dtype, shape, sharding):
 
 
 @PlxprInterpreter.register_primitive(adjoint_transform_prim)
-def handle_adjoint_transform(self, *invals, jaxpr, lazy, n_consts):
+def handle_adjoint_transform(self, *invals, jaxpr, lazy):
     """Interpret an adjoint transform primitive."""
-    consts = invals[:n_consts]
-    args = invals[n_consts:]
-    jaxpr = jaxpr_to_jaxpr(copy(self), jaxpr, consts, *args)
-
-    return adjoint_transform_prim.bind(
-        *jaxpr.consts, *args, jaxpr=jaxpr.jaxpr, lazy=lazy, n_consts=len(jaxpr.consts)
-    )
+    jaxpr = jaxpr_to_jaxpr(copy(self), jaxpr, [], *invals)
+    jaxpr, new_invals = promote_consts(jaxpr, invals)
+    return adjoint_transform_prim.bind(*new_invals, jaxpr=jaxpr, lazy=lazy)
 
 
 # pylint: disable=too-many-arguments
 @PlxprInterpreter.register_primitive(ctrl_transform_prim)
-def handle_ctrl_transform(self, *invals, n_control, jaxpr, control_values, work_wires, n_consts):
+def handle_ctrl_transform(self, *invals, n_control, jaxpr, control_values, work_wires):
     """Interpret a ctrl transform primitive."""
-    consts = invals[:n_consts]
-    args = invals[n_consts:-n_control]
-    jaxpr = jaxpr_to_jaxpr(copy(self), jaxpr, consts, *args)
+    args = invals[:-n_control]
+    jaxpr = jaxpr_to_jaxpr(copy(self), jaxpr, [], *args)
+    jaxpr, new_invals = promote_consts(jaxpr, invals)
 
     return ctrl_transform_prim.bind(
-        *jaxpr.consts,
-        *args,
-        *invals[-n_control:],
+        *new_invals,
         n_control=n_control,
-        jaxpr=jaxpr.jaxpr,
+        jaxpr=jaxpr,
         control_values=control_values,
         work_wires=work_wires,
-        n_consts=len(jaxpr.consts),
     )
 
 
@@ -587,22 +581,18 @@ def handle_while_loop(
 
 # pylint: disable=too-many-arguments
 @PlxprInterpreter.register_primitive(qnode_prim)
-def handle_qnode(self, *invals, shots, qnode, device, execution_config, qfunc_jaxpr, n_consts):
+def handle_qnode(self, *invals, shots, qnode, device, execution_config, qfunc_jaxpr):
     """Handle a qnode primitive."""
-    consts = invals[:n_consts]
-    args = invals[n_consts:]
-
-    new_qfunc_jaxpr = jaxpr_to_jaxpr(copy(self), qfunc_jaxpr, consts, *args)
 
+    qfunc_jaxpr = jaxpr_to_jaxpr(copy(self), qfunc_jaxpr, [], *invals)
+    jaxpr, new_invals = promote_consts(qfunc_jaxpr, invals)
     return qnode_prim.bind(
-        *new_qfunc_jaxpr.consts,
-        *args,
+        *new_invals,
         shots=shots,
         qnode=qnode,
         device=device,
         execution_config=execution_config,
-        qfunc_jaxpr=new_qfunc_jaxpr.jaxpr,
-        n_consts=len(new_qfunc_jaxpr.consts),
+        qfunc_jaxpr=jaxpr,
     )
 
 
diff --git a/pennylane/capture/promote_consts.py b/pennylane/capture/promote_consts.py
new file mode 100644
index 000000000..5443d94d3
--- /dev/null
+++ b/pennylane/capture/promote_consts.py
@@ -0,0 +1,49 @@
+# Copyright 2018-2025 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.
+# pylint: disable=protected-access
+"""
+This submodule contains the `promote_consts` util.
+"""
+
+from pennylane.typing import TensorLike
+
+
+def promote_consts(
+    jaxpr: "jax.extend.core.ClosedJaxpr", args: list[TensorLike] | tuple[TensorLike, ...]
+) -> tuple["jax.extend.core.Jaxpr", tuple[TensorLike]]:
+    """Convert a closed jaxpr into a jaxpr without constants and the constants prepended
+    to the arguments.
+
+    Args:
+        jaxpr (jax.extend.core.ClosedJaxpr): a closed jaxpr with bound constants
+        args (tuple[TensorLike]): the arguments we will add the constants to the start of
+
+    Returns:
+
+        (jax.extend.core.Jaxpr, tuple[TensorLike]): A Jaxpr and new arguments with the constants pre-pended.
+
+    >>> import jax
+    >>> def f(x):
+    ...     return jax.numpy.array([2.0]) + x
+    >>> args = (jax.numpy.array([0.5]), )
+    >>> jaxpr = jax.make_jaxpr(f)(*args)
+    >>> jaxpr
+    { lambda a:f32[1]; b:f32[1]. let c:f32[1] = add a b in (c,) }
+    >>> qml.capture.promote_consts(jaxpr, args)
+    ({ lambda ; a:f32[1] b:f32[1]. let c:f32[1] = add a b in (c,) },
+    (array([2.0], dtype=float32), Array([0.5], dtype=float32)))
+
+    """
+    new_args = tuple(jaxpr.consts) + tuple(args)
+    jaxpr = jaxpr.jaxpr.replace(constvars=(), invars=jaxpr.jaxpr.constvars + jaxpr.jaxpr.invars)
+    return jaxpr, new_args
diff --git a/pennylane/decomposition/collect_resource_ops.py b/pennylane/decomposition/collect_resource_ops.py
index d3a26dffb..19668dd5a 100644
--- a/pennylane/decomposition/collect_resource_ops.py
+++ b/pennylane/decomposition/collect_resource_ops.py
@@ -35,26 +35,23 @@ class CollectResourceOps(FlattenedInterpreter):
 
 
 @CollectResourceOps.register_primitive(adjoint_transform_prim)
-def _(self, *invals, jaxpr, lazy, n_consts):  # pylint: disable=unused-argument
+def _(self, *invals, jaxpr, lazy):  # pylint: disable=unused-argument
     """Collect all operations in the base plxpr and create adjoint resource ops with them."""
-    consts = invals[:n_consts]
-    args = invals[n_consts:]
     child = CollectResourceOps()
-    child.eval(jaxpr, consts, *args)
+    child.eval(jaxpr, [], *invals)
     for op in child.state["ops"]:
         self.state["ops"].add(adjoint_resource_rep(op.op_type, op.params))
     return []
 
 
 @CollectResourceOps.register_primitive(ctrl_transform_prim)
-def _(self, *invals, n_control, jaxpr, n_consts, **params):
+def _(self, *invals, n_control, jaxpr, **params):
     """Collect all operations in the target plxpr and create controlled resource ops with them."""
 
-    consts = invals[:n_consts]
-    args = invals[n_consts:-n_control]
+    args = invals[:-n_control]
     control = invals[-n_control:]
     child = CollectResourceOps()
-    child.eval(jaxpr, consts, *args)
+    child.eval(jaxpr, [], *args)
 
     # Extract the resource parameters of this control transform
     control_values = params.get("control_values")
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index 060b31574..30e8de7dd 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -1004,6 +1004,7 @@ class DefaultQubit(Device):
             key=key,
             execution_config=execution_config,
         )
+
         return interpreter.eval(jaxpr, consts, *args)
 
     def _backprop_jvp(self, jaxpr, args, tangents, execution_config=None):
diff --git a/pennylane/devices/qubit/dq_interpreter.py b/pennylane/devices/qubit/dq_interpreter.py
index 5755df63e..dc6d750f8 100644
--- a/pennylane/devices/qubit/dq_interpreter.py
+++ b/pennylane/devices/qubit/dq_interpreter.py
@@ -231,12 +231,9 @@ def _(self, *invals, reset, postselect):
 
 
 @DefaultQubitInterpreter.register_primitive(adjoint_transform_prim)
-def _(self, *invals, jaxpr, n_consts, lazy=True):
-    # TODO: requires jaxpr -> list of ops first
-    consts = invals[:n_consts]
-    args = invals[n_consts:]
+def _(self, *invals, jaxpr, lazy=True):
     recorder = CollectOpsandMeas()
-    recorder.eval(jaxpr, consts, *args)
+    recorder.eval(jaxpr, [], *invals)
 
     ops = recorder.state["ops"]
     with pause():
@@ -252,13 +249,12 @@ def _(self, *invals, jaxpr, n_consts, lazy=True):
 
 # pylint: disable=too-many-arguments
 @DefaultQubitInterpreter.register_primitive(ctrl_transform_prim)
-def _(self, *invals, n_control, jaxpr, control_values, work_wires, n_consts):
+def _(self, *invals, n_control, jaxpr, control_values, work_wires):
     # TODO: requires jaxpr -> list of ops first
-    consts = invals[:n_consts]
     control_wires = invals[-n_control:]
-    args = invals[n_consts:-n_control]
+    args = invals[:-n_control]
     recorder = CollectOpsandMeas()
-    recorder.eval(jaxpr, consts, *args)
+    recorder.eval(jaxpr, [], *args)
 
     ops = recorder.state["ops"]
     with pause():
diff --git a/pennylane/ops/op_math/adjoint.py b/pennylane/ops/op_math/adjoint.py
index f0eead95b..1af97d1c7 100644
--- a/pennylane/ops/op_math/adjoint.py
+++ b/pennylane/ops/op_math/adjoint.py
@@ -198,13 +198,11 @@ def _get_adjoint_qfunc_prim():
     adjoint_prim.prim_type = "higher_order"
 
     @adjoint_prim.def_impl
-    def _(*args, jaxpr, lazy, n_consts):
+    def _(*args, jaxpr, lazy):
         from pennylane.tape.plxpr_conversion import CollectOpsandMeas
 
-        consts = args[:n_consts]
-        args = args[n_consts:]
         collector = CollectOpsandMeas()
-        collector.eval(jaxpr, consts, *args)
+        collector.eval(jaxpr, [], *args)
         for op in reversed(collector.state["ops"]):
             adjoint(op, lazy=lazy)
         return []
@@ -228,13 +226,13 @@ def _capture_adjoint_transform(qfunc: Callable, lazy=True) -> Callable:
         abstracted_axes, abstract_shapes = qml.capture.determine_abstracted_axes(args)
         jaxpr = jax.make_jaxpr(partial(qfunc, **kwargs), abstracted_axes=abstracted_axes)(*args)
         flat_args = jax.tree_util.tree_leaves(args)
+        jaxpr, new_args = qml.capture.promote_consts(
+            jaxpr, tuple(abstract_shapes) + tuple(flat_args)
+        )
         adjoint_prim.bind(
-            *jaxpr.consts,
-            *abstract_shapes,
-            *flat_args,
-            jaxpr=jaxpr.jaxpr,
+            *new_args,
+            jaxpr=jaxpr,
             lazy=lazy,
-            n_consts=len(jaxpr.consts),
         )
 
     return new_qfunc
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
index 537ead1b2..468005178 100644
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -288,16 +288,15 @@ def _get_ctrl_qfunc_prim():
     ctrl_prim.prim_type = "higher_order"
 
     @ctrl_prim.def_impl
-    def _(*args, n_control, jaxpr, control_values, work_wires, n_consts):
+    def _(*args, n_control, jaxpr, control_values, work_wires):
         from pennylane.tape.plxpr_conversion import CollectOpsandMeas
 
-        consts = args[:n_consts]
         control_wires = args[-n_control:]
-        args = args[n_consts:-n_control]
+        args = args[:-n_control]
 
         collector = CollectOpsandMeas()
         with qml.QueuingManager.stop_recording():
-            collector.eval(jaxpr, consts, *args)
+            collector.eval(jaxpr, [], *args)
 
         for op in collector.state["ops"]:
             ctrl(op, control_wires, control_values, work_wires)
@@ -324,17 +323,18 @@ def _capture_ctrl_transform(qfunc: Callable, control, control_values, work_wires
             *args
         )
         flat_args = jax.tree_util.tree_leaves(args)
+        jaxpr, new_args = qml.capture.promote_consts(
+            jaxpr, tuple(abstract_shapes) + tuple(flat_args)
+        )
+
         control_wires = qml.wires.Wires(control)  # make sure is iterable
         ctrl_prim.bind(
-            *jaxpr.consts,
-            *abstract_shapes,
-            *flat_args,
+            *new_args,
             *control_wires,
-            jaxpr=jaxpr.jaxpr,
+            jaxpr=jaxpr,
             n_control=len(control_wires),
             control_values=control_values,
             work_wires=work_wires,
-            n_consts=len(jaxpr.consts),
         )
 
     return new_qfunc
diff --git a/pennylane/tape/plxpr_conversion.py b/pennylane/tape/plxpr_conversion.py
index 590eeaa4c..f9cf7eb3d 100644
--- a/pennylane/tape/plxpr_conversion.py
+++ b/pennylane/tape/plxpr_conversion.py
@@ -97,13 +97,11 @@ class CollectOpsandMeas(FlattenedInterpreter):
 
 
 @CollectOpsandMeas.register_primitive(adjoint_transform_prim)
-def _(self, *invals, jaxpr, lazy, n_consts):
+def _(self, *invals, jaxpr, lazy):
     """Handle an adjoint transform primitive by collecting the operations in the jaxpr, and
     then applying their adjoint in reverse order."""
-    consts = invals[:n_consts]
-    args = invals[n_consts:]
     child = CollectOpsandMeas()
-    child.eval(jaxpr, consts, *args)
+    child.eval(jaxpr, [], *invals)
     assert child.state
 
     for op in reversed(child.state["ops"]):
@@ -113,16 +111,15 @@ def _(self, *invals, jaxpr, lazy, n_consts):
 
 
 @CollectOpsandMeas.register_primitive(ctrl_transform_prim)
-def _(self, *invals, n_control, jaxpr, n_consts, **params):
+def _(self, *invals, n_control, jaxpr, **params):
     """Handle a control transform primitive by collecting the operations in the jaxpr,
     and then applying their controlled versions.
     """
-    consts = invals[:n_consts]
-    args = invals[n_consts:-n_control]
+    args = invals[:-n_control]
     control = invals[-n_control:]
 
     child = CollectOpsandMeas()
-    child.eval(jaxpr, consts, *args)
+    child.eval(jaxpr, [], *args)
     assert child.state
 
     for op in child.state["ops"]:
@@ -190,13 +187,10 @@ def _(self, *invals, jaxpr, n_consts, **params):
 
 @CollectOpsandMeas.register_primitive(qnode_prim)
 def _(
-    self, *invals, shots, qnode, device, execution_config, qfunc_jaxpr, n_consts
+    self, *invals, shots, qnode, device, execution_config, qfunc_jaxpr
 ):  # pylint: disable=too-many-arguments
-    consts = invals[:n_consts]
-    args = invals[n_consts:]
-
     child = CollectOpsandMeas()
-    out = child.eval(qfunc_jaxpr, consts, *args)
+    out = child.eval(qfunc_jaxpr, [], *invals)
     assert child.state
     self.state["ops"].extend(child.state["ops"])
     self.state["measurements"].extend(child.state["measurements"])
diff --git a/pennylane/transforms/decompose.py b/pennylane/transforms/decompose.py
index 8e0f9f986..7757276d2 100644
--- a/pennylane/transforms/decompose.py
+++ b/pennylane/transforms/decompose.py
@@ -331,9 +331,8 @@ def _get_plxpr_decompose():  # pylint: disable=missing-docstring, too-many-state
 
     # pylint: disable=too-many-arguments
     @DecomposeInterpreter.register_primitive(ctrl_transform_prim)
-    def _(self, *invals, n_control, jaxpr, control_values, work_wires, n_consts):
-        consts = invals[:n_consts]
-        args = invals[n_consts:-n_control]
+    def _(self, *invals, n_control, jaxpr, control_values, work_wires):
+        args = invals[:-n_control]
         control_wires = invals[-n_control:]
 
         unroller = ControlTransformInterpreter(
@@ -341,7 +340,7 @@ def _get_plxpr_decompose():  # pylint: disable=missing-docstring, too-many-state
         )
 
         def wrapper(*inner_args):
-            return unroller.eval(jaxpr, consts, *inner_args)
+            return unroller.eval(jaxpr, [], *inner_args)
 
         jaxpr = jax.make_jaxpr(wrapper)(*args)
         return self.eval(jaxpr.jaxpr, jaxpr.consts, *args)
diff --git a/pennylane/transforms/defer_measurements.py b/pennylane/transforms/defer_measurements.py
index 3f1a4eb2e..086e29c54 100644
--- a/pennylane/transforms/defer_measurements.py
+++ b/pennylane/transforms/defer_measurements.py
@@ -419,6 +419,7 @@ def _get_plxpr_defer_measurements():
                 for branch, value in condition.items():
                     # When reduce_postselected is True, some branches can be ()
                     cur_consts = invals[consts_slices[i]]
+                    jaxpr = jaxpr.replace(constvars=(), invars=jaxpr.constvars + jaxpr.invars)
                     qml.cond(value, ctrl_transform_prim.bind)(
                         *cur_consts,
                         *args,
@@ -427,7 +428,6 @@ def _get_plxpr_defer_measurements():
                         n_control=len(control_wires),
                         control_values=branch,
                         work_wires=None,
-                        n_consts=len(cur_consts),
                     )
 
         return [None] * len(jaxpr_branches[0].outvars)
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/_capture_qnode.py b/pennylane/workflow/_capture_qnode.py
index 6cab0575e..85e55ec74 100644
--- a/pennylane/workflow/_capture_qnode.py
+++ b/pennylane/workflow/_capture_qnode.py
@@ -116,7 +116,7 @@ import jax
 from jax.interpreters import ad, batching, mlir
 
 import pennylane as qml
-from pennylane.capture import FlatFn, QmlPrimitive
+from pennylane.capture import FlatFn, QmlPrimitive, promote_consts
 from pennylane.exceptions import CaptureError
 from pennylane.logging import debug_logger
 from pennylane.typing import TensorLike
@@ -192,15 +192,12 @@ qnode_prim.prim_type = "higher_order"
 # pylint: disable=too-many-arguments
 @debug_logger
 @qnode_prim.def_impl
-def _(*args, qnode, shots, device, execution_config, qfunc_jaxpr, n_consts, batch_dims=None):
+def _(*args, qnode, shots, device, execution_config, qfunc_jaxpr, batch_dims=None):
     if shots != device.shots:
         raise NotImplementedError(
             "Overriding shots is not yet supported with the program capture execution."
         )
 
-    consts = args[:n_consts]
-    non_const_args = args[n_consts:]
-
     device_program = device.preprocess_transforms(execution_config)
     if batch_dims is not None:
         temp_all_args = []
@@ -211,56 +208,48 @@ def _(*args, qnode, shots, device, execution_config, qfunc_jaxpr, n_consts, batc
                 temp_all_args.append(a[tuple(slices)])
             else:
                 temp_all_args.append(a)
-        temp_consts = temp_all_args[:n_consts]
-        temp_args = temp_all_args[n_consts:]
     else:
-        temp_consts = consts
-        temp_args = non_const_args
+        temp_all_args = args
 
     # Expand user transforms applied to the qfunc
+    accumulated_consts = ()
     if getattr(qfunc_jaxpr.eqns[0].primitive, "prim_type", "") == "transform":
         transformed_func = qml.capture.expand_plxpr_transforms(
-            partial(qml.capture.eval_jaxpr, qfunc_jaxpr, temp_consts)
+            partial(qml.capture.eval_jaxpr, qfunc_jaxpr, [])
         )
 
-        qfunc_jaxpr = jax.make_jaxpr(transformed_func)(*temp_args)
-        temp_consts = qfunc_jaxpr.consts
-        qfunc_jaxpr = qfunc_jaxpr.jaxpr
+        qfunc_jaxpr = jax.make_jaxpr(transformed_func)(*temp_all_args)
+        qfunc_jaxpr, accumulated_consts = promote_consts(qfunc_jaxpr, accumulated_consts)
 
     # Expand user transforms applied to the qnode
-    qfunc_jaxpr = qnode.transform_program(qfunc_jaxpr, temp_consts, *temp_args)
-    temp_consts = qfunc_jaxpr.consts
-    qfunc_jaxpr = qfunc_jaxpr.jaxpr
+    qfunc_jaxpr = qnode.transform_program(qfunc_jaxpr, [], *accumulated_consts, *temp_all_args)
+    qfunc_jaxpr, accumulated_consts = promote_consts(qfunc_jaxpr, accumulated_consts)
 
     # Apply device preprocessing transforms
     graph_enabled = qml.decomposition.enabled_graph()
     try:
         qml.decomposition.disable_graph()
-        qfunc_jaxpr = device_program(qfunc_jaxpr, temp_consts, *temp_args)
+        qfunc_jaxpr = device_program(qfunc_jaxpr, [], *temp_all_args)
+        qfunc_jaxpr, accumulated_consts = promote_consts(qfunc_jaxpr, accumulated_consts)
     finally:
         if graph_enabled:
             qml.decomposition.enable_graph()
-    consts = qfunc_jaxpr.consts
-    qfunc_jaxpr = qfunc_jaxpr.jaxpr
 
-    partial_eval = partial(
-        device.eval_jaxpr, qfunc_jaxpr, consts, execution_config=execution_config
-    )
+    partial_eval = partial(device.eval_jaxpr, qfunc_jaxpr, [], execution_config=execution_config)
     if batch_dims is None:
-        return partial_eval(*non_const_args)
-    return jax.vmap(partial_eval, batch_dims[n_consts:])(*non_const_args)
+        return partial_eval(*accumulated_consts, *args)
+
+    return jax.vmap(partial_eval, batch_dims)(*accumulated_consts, *args)
 
 
 # pylint: disable=unused-argument
 @debug_logger
 @qnode_prim.def_abstract_eval
-def _(*args, qnode, shots, device, execution_config, qfunc_jaxpr, n_consts, batch_dims=None):
+def _(*args, qnode, shots, device, execution_config, qfunc_jaxpr, batch_dims=None):
 
     mps = qfunc_jaxpr.outvars
 
-    batch_shape = (
-        _get_batch_shape(args[n_consts:], batch_dims[n_consts:]) if batch_dims is not None else ()
-    )
+    batch_shape = _get_batch_shape(args, batch_dims) if batch_dims is not None else ()
 
     return _get_shapes_for(
         *mps, shots=shots, num_device_wires=len(device.wires), batch_shape=batch_shape
@@ -277,7 +266,6 @@ def _qnode_batching_rule(
     device,
     execution_config,
     qfunc_jaxpr,
-    n_consts,
 ):
     """
     Batching rule for the ``qnode`` primitive.
@@ -286,25 +274,10 @@ def _qnode_batching_rule(
     """
 
     for idx, (arg, batch_dim) in enumerate(zip(batched_args, batch_dims)):
-
-        if _is_scalar_tensor(arg):
-            continue
-
-        # Regardless of their shape, jax.vmap automatically inserts `None` as the batch dimension for constants.
-        # However, if the constant is not a standard JAX type, the batch dimension is not inserted at all.
-        # How to handle this case is still an open question. For now, we raise a warning and give the user full flexibility.
-        if idx < n_consts:
-            warn(
-                f"Constant argument at index {idx} is not scalar. "
-                "This may lead to unintended behavior or wrong results if the argument is provided "
-                "using parameter broadcasting to a quantum operation that supports batching.",
-                UserWarning,
-            )
-
         # To resolve this ambiguity, we might add more properties to the AbstractOperator
         # class to indicate which operators support batching and check them here.
         # As above, at this stage we raise a warning and give the user full flexibility.
-        elif arg.size > 1 and batch_dim is None:
+        if not _is_scalar_tensor(arg) and arg.size > 1 and batch_dim is None:
             warn(
                 f"Argument at index {idx} has size > 1 but its batch dimension is None. "
                 "This may lead to unintended behavior or wrong results if the argument is provided "
@@ -319,7 +292,6 @@ def _qnode_batching_rule(
         device=device,
         execution_config=execution_config,
         qfunc_jaxpr=qfunc_jaxpr,
-        n_consts=n_consts,
         batch_dims=batch_dims,
     )
 
@@ -476,8 +448,10 @@ def _extract_qfunc_jaxpr(qnode, abstracted_axes, *args, **kwargs):
             "flow functions like for_loop, while_loop, etc."
         ) from exc
 
+    qfunc_jaxpr, consts = promote_consts(qfunc_jaxpr, ())
+
     assert flat_fn.out_tree is not None, "out_tree should be set by call to flat_fn"
-    return qfunc_jaxpr, flat_fn.out_tree
+    return qfunc_jaxpr, flat_fn.out_tree, consts
 
 
 def capture_qnode(qnode: "qml.QNode", *args, **kwargs) -> "qml.typing.Result":
@@ -569,7 +543,7 @@ def capture_qnode(qnode: "qml.QNode", *args, **kwargs) -> "qml.typing.Result":
     cache_key = _get_jaxpr_cache_key(flat_dynamic_args, flat_static_args, kwargs, abstracted_axes)
 
     if cached_value := qnode.capture_cache.get(cache_key, None):
-        qfunc_jaxpr, config, out_tree = cached_value
+        qfunc_jaxpr, config, out_tree, consts = cached_value
     else:
         config = construct_execution_config(
             qnode, resolve=False
@@ -581,20 +555,21 @@ def capture_qnode(qnode: "qml.QNode", *args, **kwargs) -> "qml.typing.Result":
             # as the original dynamic arguments
             abstracted_axes = jax.tree_util.tree_unflatten(dynamic_args_struct, abstracted_axes)
 
-        qfunc_jaxpr, out_tree = _extract_qfunc_jaxpr(qnode, abstracted_axes, *args, **kwargs)
+        qfunc_jaxpr, out_tree, consts = _extract_qfunc_jaxpr(
+            qnode, abstracted_axes, *args, **kwargs
+        )
 
-        qnode.capture_cache[cache_key] = (qfunc_jaxpr, config, out_tree)
+        qnode.capture_cache[cache_key] = (qfunc_jaxpr, config, out_tree, consts)
 
     res = qnode_prim.bind(
-        *qfunc_jaxpr.consts,
         *abstract_shapes,
+        *consts,
         *flat_dynamic_args,
         shots=shots,
         qnode=qnode,
         device=qnode.device,
         execution_config=config,
-        qfunc_jaxpr=qfunc_jaxpr.jaxpr,
-        n_consts=len(qfunc_jaxpr.consts),
+        qfunc_jaxpr=qfunc_jaxpr,
     )
 
     return jax.tree_util.tree_unflatten(out_tree, res)
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/capture/test_base_interpreter.py b/tests/capture/test_base_interpreter.py
index 3a39e58b0..d429aad90 100644
--- a/tests/capture/test_base_interpreter.py
+++ b/tests/capture/test_base_interpreter.py
@@ -402,7 +402,7 @@ class TestHigherOrderPrimitiveRegistrations:
 
         jaxpr2 = jax.make_jaxpr(ConstAdder()(f))(0.5)
         assert jaxpr2.consts == [scalar]
-        assert len(jaxpr2.eqns[0].params["jaxpr"].constvars) == 1
+        assert len(jaxpr2.eqns[0].params["jaxpr"].constvars) == 0
 
     def test_ctrl_transform(self):
         """Test the higher order ctrl transform."""
@@ -439,12 +439,14 @@ class TestHigherOrderPrimitiveRegistrations:
             qml.ctrl(g, control)(x)
 
         jaxpr = jax.make_jaxpr(f)(0.5, 1)
+
         assert len(jaxpr.consts) == 0
         assert len(jaxpr.eqns[0].params["jaxpr"].constvars) == 0
+        assert len(jaxpr.eqns[0].params["jaxpr"].invars) == 1
 
         jaxpr2 = jax.make_jaxpr(ConstAdder()(f))(0.5, 1)
         assert jaxpr2.consts == [scalar]
-        assert len(jaxpr2.eqns[0].params["jaxpr"].constvars) == 1
+        assert len(jaxpr2.eqns[0].params["jaxpr"].constvars) == 0
 
     def test_cond(self):
         """Test the cond higher order primitive."""
@@ -697,7 +699,7 @@ class TestHigherOrderPrimitiveRegistrations:
 
         jaxpr2 = jax.make_jaxpr(ConstAdder()(f))()
         assert jaxpr2.consts == [scalar]
-        assert len(jaxpr2.eqns[0].params["qfunc_jaxpr"].constvars) == 1
+        assert len(jaxpr2.eqns[0].params["qfunc_jaxpr"].constvars) == 0
 
     @pytest.mark.parametrize("grad_f", (qml.grad, qml.jacobian))
     def test_grad_and_jac(self, grad_f):
diff --git a/tests/capture/test_nested_plxpr.py b/tests/capture/test_nested_plxpr.py
index de2e975c9..392b1885a 100644
--- a/tests/capture/test_nested_plxpr.py
+++ b/tests/capture/test_nested_plxpr.py
@@ -146,7 +146,6 @@ class TestAdjointQfunc:
         jaxpr = jax.make_jaxpr(workflow)(0.5)
 
         assert jaxpr.eqns[0].primitive == adjoint_transform_prim
-        assert jaxpr.eqns[0].params["n_consts"] == 1
         assert len(jaxpr.eqns[0].invars) == 2  # one const, one arg
 
         with qml.queuing.AnnotatedQueue() as q:
@@ -170,9 +169,8 @@ class TestAdjointQfunc:
         assert len(plxpr.eqns) == 1
         grad_eqn = plxpr.eqns[0]
         assert grad_eqn.primitive == grad_prim
-        assert set(grad_eqn.params.keys()) == {"argnum", "n_consts", "jaxpr", "method", "h"}
+        assert set(grad_eqn.params.keys()) == {"argnum", "jaxpr", "n_consts", "method", "h"}
         assert grad_eqn.params["argnum"] == [0]
-        assert grad_eqn.params["n_consts"] == 0
         assert grad_eqn.params["method"] is None
         assert grad_eqn.params["h"] is None
         assert len(grad_eqn.params["jaxpr"].eqns) == 1
@@ -296,7 +294,6 @@ class TestCtrlQfunc:
         assert plxpr.eqns[0].params["control_values"] == [True]
         assert plxpr.eqns[0].params["n_control"] == 1
         assert plxpr.eqns[0].params["work_wires"] is None
-        assert plxpr.eqns[0].params["n_consts"] == 0
 
     def test_dynamic_control_wires(self):
         """Test that control wires can be dynamic."""
@@ -366,7 +363,6 @@ class TestCtrlQfunc:
         plxpr = jax.make_jaxpr(f)(-0.5, 1, 2)
 
         # First equation of plxpr is the multiplication of x by 2
-        assert plxpr.eqns[1].params["n_consts"] == 1  # w1 is a const for the outer `ctrl`
         assert (
             plxpr.eqns[1].invars[0] is plxpr.jaxpr.invars[1]
         )  # first input is first control wire, const
@@ -409,7 +405,6 @@ class TestCtrlQfunc:
 
         eqn = jaxpr.eqns[0]
         assert eqn.params["control_values"] == [True]
-        assert eqn.params["n_consts"] == 0
         assert eqn.params["n_control"] == 1
         assert eqn.params["work_wires"] is None
 
diff --git a/tests/capture/test_promote_consts.py b/tests/capture/test_promote_consts.py
new file mode 100644
index 000000000..9ba364967
--- /dev/null
+++ b/tests/capture/test_promote_consts.py
@@ -0,0 +1,44 @@
+# Copyright 2018-2025 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.
+# pylint: disable=protected-access
+"""Unit tests for qml.capture.promote_consts."""
+
+import pytest
+
+from pennylane.capture import promote_consts
+
+jax = pytest.importorskip("jax")
+jnp = pytest.importorskip("jax.numpy")
+
+
+@pytest.mark.jax
+def test_promote_consts():
+    """Test that promote consts works as expected."""
+
+    const = jnp.array([2.0])
+
+    def f(x):
+        return const + x
+
+    args = (jnp.array([0.5]),)
+
+    jaxpr = jax.make_jaxpr(f)(*args)
+
+    new_jaxpr, new_args = promote_consts(jaxpr, args)
+
+    assert isinstance(new_jaxpr, jax.extend.core.Jaxpr)
+    assert new_jaxpr.constvars == []
+
+    assert len(new_args) == 2
+    assert jnp.allclose(new_args[0], const)
+    assert jnp.allclose(new_args[1], args[0])
diff --git a/tests/capture/transforms/test_capture_defer_measurements.py b/tests/capture/transforms/test_capture_defer_measurements.py
index 7ca44c736..c7d8d01f9 100644
--- a/tests/capture/transforms/test_capture_defer_measurements.py
+++ b/tests/capture/transforms/test_capture_defer_measurements.py
@@ -675,7 +675,7 @@ class TestDeferMeasurementsHigherOrderPrimitives:
 
         inner_jaxpr = jaxpr.eqns[0].params["qfunc_jaxpr"]
         collector = CollectOpsandMeas()
-        collector.eval(inner_jaxpr, jaxpr.consts, x)
+        collector.eval(inner_jaxpr, [], *jaxpr.consts, x)
 
         ops = collector.state["ops"]
         expected_ops = [
@@ -727,7 +727,10 @@ class TestDeferMeasurementsHigherOrderPrimitives:
         qfunc_jaxpr = inner_jaxpr.eqns[0].params["qfunc_jaxpr"]
 
         collector = CollectOpsandMeas()
-        collector.eval(qfunc_jaxpr, jaxpr.consts, x)
+        if postselect is None:
+            collector.eval(qfunc_jaxpr, [], x)
+        else:
+            collector.eval(qfunc_jaxpr, [], qml.math.array([postselect]), x)
 
         ops = collector.state["ops"]
         expected_ops = [qml.RX(x, 0), qml.CNOT([0, 3]), qml.CRX(x, [3, 0])]
diff --git a/tests/capture/transforms/test_capture_merge_amplitude_embedding.py b/tests/capture/transforms/test_capture_merge_amplitude_embedding.py
index 691d9363e..2f8ae87ef 100644
--- a/tests/capture/transforms/test_capture_merge_amplitude_embedding.py
+++ b/tests/capture/transforms/test_capture_merge_amplitude_embedding.py
@@ -622,9 +622,10 @@ class TestHigherOrderPrimitiveIntegration:
         assert jaxpr.eqns[2].primitive == qml.RY._primitive
 
         inner_jaxpr = jaxpr.eqns[1].params["jaxpr"]
-        assert inner_jaxpr.eqns[0].primitive == qml.AmplitudeEmbedding._primitive
-        assert qml.math.allclose(inner_jaxpr.eqns[0].params["n_wires"], 2)
-        assert inner_jaxpr.eqns[1].primitive == qml.X._primitive
+
+        assert inner_jaxpr.eqns[-2].primitive == qml.AmplitudeEmbedding._primitive
+        assert qml.math.allclose(inner_jaxpr.eqns[-2].params["n_wires"], 2)
+        assert inner_jaxpr.eqns[-1].primitive == qml.X._primitive
 
     @pytest.mark.parametrize("lazy", [True, False])
     def test_adjoint_transform_prim(self, lazy):
@@ -645,10 +646,11 @@ class TestHigherOrderPrimitiveIntegration:
         assert jaxpr.eqns[0].params["lazy"] == lazy
 
         inner_jaxpr = jaxpr.eqns[0].params["jaxpr"]
-        assert len(inner_jaxpr.eqns) == 2
-        assert inner_jaxpr.eqns[0].primitive == qml.AmplitudeEmbedding._primitive
-        assert qml.math.allclose(inner_jaxpr.eqns[0].params["n_wires"], 2)
-        assert inner_jaxpr.eqns[1].primitive == qml.X._primitive
+
+        assert len(inner_jaxpr.eqns) == 9
+        assert inner_jaxpr.eqns[-2].primitive == qml.AmplitudeEmbedding._primitive
+        assert qml.math.allclose(inner_jaxpr.eqns[-2].params["n_wires"], 2)
+        assert inner_jaxpr.eqns[-1].primitive == qml.X._primitive
 
     def test_cond_prim_only_true_branch(self):
         """Test that the transform works correctly when applied with cond_prim with just a true branch."""
@@ -814,12 +816,12 @@ class TestHigherOrderPrimitiveIntegration:
 
         assert jaxpr.eqns[0].primitive == qnode_prim
         qfunc_jaxpr = jaxpr.eqns[0].params["qfunc_jaxpr"]
-        assert len(qfunc_jaxpr.eqns) == 4
-        assert qfunc_jaxpr.eqns[0].primitive == qml.AmplitudeEmbedding._primitive
-        assert qml.math.allclose(qfunc_jaxpr.eqns[0].params["n_wires"], 2)
-        assert qfunc_jaxpr.eqns[1].primitive == qml.Hadamard._primitive
-        assert qfunc_jaxpr.eqns[2].primitive == qml.PauliZ._primitive
-        assert qfunc_jaxpr.eqns[3].primitive == qml.measurements.ExpectationMP._obs_primitive
+        assert len(qfunc_jaxpr.eqns) == 11
+        assert qfunc_jaxpr.eqns[-4].primitive == qml.AmplitudeEmbedding._primitive
+        assert qml.math.allclose(qfunc_jaxpr.eqns[-4].params["n_wires"], 2)
+        assert qfunc_jaxpr.eqns[-3].primitive == qml.Hadamard._primitive
+        assert qfunc_jaxpr.eqns[-2].primitive == qml.PauliZ._primitive
+        assert qfunc_jaxpr.eqns[-1].primitive == qml.measurements.ExpectationMP._obs_primitive
 
     def test_grad_prim(self):
         """Test that the transform works correctly when applied with grad_prim."""
diff --git a/tests/capture/workflow/test_capture_qnode.py b/tests/capture/workflow/test_capture_qnode.py
index d86d24b26..7641a3d89 100644
--- a/tests/capture/workflow/test_capture_qnode.py
+++ b/tests/capture/workflow/test_capture_qnode.py
@@ -318,7 +318,6 @@ def test_qnode_closure_variables():
 
     jaxpr = jax.make_jaxpr(circuit)(1)
     assert len(jaxpr.eqns[0].invars) == 2  # one closure variable, one arg
-    assert jaxpr.eqns[0].params["n_consts"] == 1
 
     out = jax.core.eval_jaxpr(jaxpr.jaxpr, [jnp.array(0.5)], 0)
     assert qml.math.allclose(out, jnp.cos(0.5))
@@ -630,6 +629,89 @@ class TestDevicePreprocessing:
         assert all(sample == 0 for sample in results) or all(sample == 1 for sample in results)
 
 
+def _interpret1(jaxpr, consts, *args):
+    args = [jnp.array([2]) * a for a in args]
+    return qml.capture.PlxprInterpreter().eval(jaxpr, consts, *args)
+
+
+# pylint: disable=unused-argument
+def _dummy_transform1(jaxpr, consts, targs, tkwargs, *args):
+    return jax.make_jaxpr(partial(_interpret1, jaxpr, consts))(*args)
+
+
+@partial(qml.transform, plxpr_transform=_dummy_transform1)
+def transform1(tape):
+    raise NotImplementedError
+
+
+def _interpret2(jaxpr, consts, *args):
+    args = [jnp.array([3.0, 3.0]) * a for a in args]
+    return qml.capture.PlxprInterpreter().eval(jaxpr, consts, *args)
+
+
+# pylint: disable=unused-argument
+def _dummy_transform2(jaxpr, consts, targs, tkwargs, *args):
+    return jax.make_jaxpr(partial(_interpret2, jaxpr, consts))(*args)
+
+
+@partial(qml.transform, plxpr_transform=_dummy_transform2)
+def transform2(tape):
+    raise NotImplementedError
+
+
+class JaxprDevice(qml.devices.Device):
+
+    def __init__(self, wires, shots=None):
+        super().__init__(wires=wires, shots=shots)
+        self.jaxpr_history = []
+        self.consts_history = []
+        self.args_history = []
+
+    def preprocess_transforms(self, execution_config=None):
+        program = qml.transforms.core.TransformProgram()
+        program.add_transform(transform1)
+        program.add_transform(transform2)
+        return program
+
+    def execute(self, circuits, execution_config=None):
+        raise NotImplementedError
+
+    def eval_jaxpr(self, jaxpr, consts, *args, execution_config=None):
+        self.jaxpr_history.append(jaxpr)
+        self.consts_history.append(consts)
+        self.args_history.append(args)
+        return qml.device("default.qubit", wires=self.wires).eval_jaxpr(
+            jaxpr, consts, *args, execution_config=execution_config
+        )
+
+
+def test_device_preprocessing_consts():
+    """Test that device preprocessing can add consts to the jaxpr multiple times."""
+
+    dev = JaxprDevice(wires=2)
+
+    @qml.qnode(dev)
+    def c(x):
+        qml.RX(x, 0)
+        return qml.expval(qml.Z(0))
+
+    _ = c(jnp.array(0.5))
+
+    jaxpr = dev.jaxpr_history[-1]
+    assert len(jaxpr.constvars) == 0
+    assert len(jaxpr.invars) == 3
+
+    assert jaxpr.invars[0].aval.shape == (2,)  # [3.0, 3.0]
+    assert jaxpr.invars[1].aval.shape == (1,)  # [2]
+
+    assert len(dev.consts_history[-1]) == 0
+    assert len(dev.args_history[-1]) == 3
+
+    assert jnp.allclose(dev.args_history[-1][0], jnp.array([3.0, 3.0]))
+    assert jnp.allclose(dev.args_history[-1][1], jnp.array([2.0]))
+    assert jnp.allclose(dev.args_history[-1][2], jnp.array(0.5))
+
+
 class TestDifferentiation:
 
     def test_error_backprop_unsupported(self):
@@ -901,7 +983,7 @@ class TestQNodeVmapIntegration:
             qml.RX(const, wires=0)
             return qml.expval(qml.PauliZ(0))
 
-        with pytest.warns(UserWarning, match="Constant argument at index 0 is not scalar. "):
+        with pytest.warns(UserWarning, match="Argument at index 0 has size > 1 "):
             jax.make_jaxpr(jax.vmap(circuit))(jnp.array([0.1, 0.2]))
 
     def test_vmap_overriding_shots(self):
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!"):