diff --git a/doc/releases/changelog-0.43.0.md b/doc/releases/changelog-0.43.0.md index a1acf41a964..e24d254d2ac 100644 --- a/doc/releases/changelog-0.43.0.md +++ b/doc/releases/changelog-0.43.0.md @@ -801,6 +801,7 @@ qubit allocation. [(#7678)](https://github.com/PennyLaneAI/pennylane/pull/7678) [(#8184)](https://github.com/PennyLaneAI/pennylane/pull/8184) + [(#8406)](https://github.com/PennyLaneAI/pennylane/pull/8406) * The :func:`qml.workflow.set_shots` transform can now be directly applied to a QNode without the need for `functools.partial`, providing a more user-friendly syntax and negating having to import the `functools` package. [(#7876)](https://github.com/PennyLaneAI/pennylane/pull/7876) @@ -1178,6 +1179,9 @@

Internal changes ⚙️

+* Removes excess copies in `QuantumScript.copy`, and some other performance improvements to `resolve_dynamic_wires`. + [(#8406)](https://github.com/PennyLaneAI/pennylane/pull/8406) + * GitHub actions and workflows (`interface-unit-tests.yml`, `tests-labs.yml`, `unit-test.yml`, `upload-nightly-release.yml` and `upload.yml`) have been updated to use `ubuntu-24.04` runners. [(#8371)](https://github.com/PennyLaneAI/pennylane/pull/8371) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 5cab17a461d..c3da4dac897 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -878,8 +878,12 @@ def copy(self, copy_operations: bool = False, **update) -> "QuantumScript": # Perform a shallow copy of all operations in the operation and measurement # queues. The operations will continue to share data with the original script operations # unless modified. - _ops = update.get("operations", [copy.copy(op) for op in self.operations]) - _measurements = update.get("measurements", [copy.copy(op) for op in self.measurements]) + _ops = update.get("operations") + _measurements = update.get("measurements") + if _ops is None: + _ops = (copy.copy(op) for op in self.operations) + if _measurements is None: + _measurements = (copy.copy(mp) for mp in self.measurements) else: # Perform a shallow copy of the operation and measurement queues. The # operations within the queues will be references to the original script operations; @@ -891,7 +895,7 @@ def copy(self, copy_operations: bool = False, **update) -> "QuantumScript": update_trainable_params = "operations" in update or "measurements" in update # passing trainable_params=None will re-calculate trainable_params - default_trainable_params = None if update_trainable_params else self.trainable_params + default_trainable_params = None if update_trainable_params else self._trainable_params new_qscript = self.__class__( ops=_ops, diff --git a/pennylane/transforms/resolve_dynamic_wires.py b/pennylane/transforms/resolve_dynamic_wires.py index 2da8ed8439c..8e96bef40ce 100644 --- a/pennylane/transforms/resolve_dynamic_wires.py +++ b/pennylane/transforms/resolve_dynamic_wires.py @@ -16,7 +16,7 @@ """ from collections.abc import Hashable, Sequence -from pennylane.allocation import Allocate, AllocateState, Deallocate +from pennylane.allocation import AllocateState from pennylane.exceptions import AllocationError from pennylane.measurements import measure from pennylane.tape import QuantumScript, QuantumScriptBatch @@ -91,6 +91,28 @@ def null_postprocessing(results: ResultBatch) -> Result: return results[0] +def _new_ops(operations, manager, wire_map, deallocated): + for op in operations: + # check name faster than isinstance + if op.name == "Allocate": + for w in op.wires: + wire, ops = manager.get_wire(**op.hyperparameters) + yield from ops + wire_map[w] = wire + elif op.name == "Deallocate": + for w in op.wires: + deallocated.add(w) + manager.return_wire(wire_map.pop(w)) + else: + if wire_map: + op = op.map_wires(wire_map) + if deallocated and (intersection := deallocated.intersection(set(op.wires))): + raise AllocationError( + f"Encountered deallocated wires {intersection} in {op}. Dynamic wires cannot be used after deallocation." + ) + yield op + + @transform def resolve_dynamic_wires( tape: QuantumScript, @@ -234,25 +256,8 @@ def multiple_allocations(): wire_map = {} deallocated = set() - new_ops = [] - for op in tape.operations: - if isinstance(op, Allocate): - for w in op.wires: - wire, ops = manager.get_wire(**op.hyperparameters) - new_ops += ops - wire_map[w] = wire - elif isinstance(op, Deallocate): - for w in op.wires: - deallocated.add(w) - manager.return_wire(wire_map.pop(w)) - else: - if wire_map: - op = op.map_wires(wire_map) - if intersection := deallocated.intersection(set(op.wires)): - raise AllocationError( - f"Encountered deallocated wires {intersection} in {op}. Dynamic wires cannot be used after deallocation." - ) - new_ops.append(op) + # note that manager, wire_map, and deallocated updated in place + new_ops = list(_new_ops(tape.operations, manager, wire_map, deallocated)) if wire_map: mps = [mp.map_wires(wire_map) for mp in tape.measurements] @@ -263,6 +268,11 @@ def multiple_allocations(): raise AllocationError( f"Encountered deallocated wires {intersection} in {mp}. Dynamic wires cannot be used after deallocation." ) + + if not wire_map and not deallocated: + return (tape,), null_postprocessing + # use private trainable params to avoid calculating them if they haven't already been set + # pylint: disable=protected-access return ( - tape.copy(ops=new_ops, measurements=mps, trainable_params=tape.trainable_params), + tape.copy(ops=new_ops, measurements=mps, trainable_params=tape._trainable_params), ), null_postprocessing