Skip to content

Modify resource tracking to write to distinct filenames #7392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ Here's a list of deprecations made this release. For a more detailed breakdown o
* `null.qubit` can now support an optional `track_resources` argument which allows it to record which gates are executed.
[(#7226)](https://github.com/PennyLaneAI/pennylane/pull/7226)
[(#7372)](https://github.com/PennyLaneAI/pennylane/pull/7372)
[(#7392)](https://github.com/PennyLaneAI/pennylane/pull/7392)

* A new internal module, `qml.concurrency`, is added to support internal use of multiprocess and multithreaded execution of workloads. This also migrates the use of `concurrent.futures` in `default.qubit` to this new design.
[(#7303)](https://github.com/PennyLaneAI/pennylane/pull/7303)
Expand Down
31 changes: 18 additions & 13 deletions pennylane/devices/null_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import inspect
import json
import logging
import time
from collections import defaultdict
from dataclasses import replace
from functools import lru_cache, singledispatch
Expand Down Expand Up @@ -51,6 +52,8 @@
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

RESOURCES_FNAME_PREFIX = "__pennylane_resources_data_"


@singledispatch
def zero_measurement(
Expand Down Expand Up @@ -132,7 +135,7 @@ def _interface(config: ExecutionConfig):
return config.interface.get_like() if config.gradient_method == "backprop" else "numpy"


def _simulate_resource_use(circuit, resources_fname="__pennylane_resources_data.json"):
def _simulate_resource_use(circuit, outfile):
num_wires = len(circuit.wires)
gate_types = defaultdict(int)

Expand Down Expand Up @@ -163,17 +166,16 @@ def _simulate_resource_use(circuit, resources_fname="__pennylane_resources_data.
name = f"{controls if controls > 1 else ''}C({name})"

gate_types[name] += 1
# NOTE: For now, this information is being printed to match the behavior of catalyst resource tracking.
# NOTE: For now, this information is being printed to match the behaviour of catalyst resource tracking.
# In the future it may be better to return this information in a more structured way.
with open(resources_fname, "w") as f:
json.dump(
{
"num_wires": num_wires,
"num_gates": sum(gate_types.values()),
"gate_types": gate_types,
},
f,
)
json.dump(
{
"num_wires": num_wires,
"num_gates": sum(gate_types.values()),
"gate_types": gate_types,
},
outfile,
)


@simulator_tracking
Expand All @@ -188,7 +190,7 @@ class NullQubit(Device):
(``['aux_wire', 'q1', 'q2']``). Default ``None`` if not specified.
shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots
to use in executions involving this device.
track_resources (bool): If ``True``, track the number of resources used by the device. This argument is experimental and subject to change.
track_resources (bool): If True, track the number of resources used by the device and save them to a JSON file. This argument is experimental and subject to change.
**Example:**

.. code-block:: python
Expand Down Expand Up @@ -285,7 +287,10 @@ def _simulate(self, circuit, interface):
results = []

if self._track_resources:
_simulate_resource_use(circuit)
timestamp = int(time.time() * 1e9) # nanoseconds since epoch
resources_fname = f"{RESOURCES_FNAME_PREFIX}{timestamp}.json"
with open(resources_fname, "x", encoding="utf-8") as f:
_simulate_resource_use(circuit, f)

for s in circuit.shots or [None]:
r = tuple(
Expand Down
73 changes: 45 additions & 28 deletions tests/devices/test_null_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ def test_resource_tracking_attribute():
"""Test NullQubit track_resources attribute"""
# pylint: disable=protected-access
assert NullQubit()._track_resources is False
assert NullQubit(track_resources=True)._track_resources is True

dev = NullQubit(track_resources=True)
assert dev._track_resources is True

def small_circ(params):
qml.X(0)
Expand All @@ -93,41 +92,59 @@ def small_circ(params):
qml.RX(params[0], wires=0)
qml.RX(params[0] * 2, wires=1)

qml.QubitUnitary([[1, 0], [0, 1]], wires=0)
qml.ControlledQubitUnitary([[1, 0], [0, 1]], wires=[0, 1, 2], control_values=[1, 1])

return qml.expval(qml.PauliZ(0))

qnode = qml.QNode(small_circ, dev, diff_method="backprop")

inputs = qml.numpy.array([0.5])

def check_outputs():
written_files = list(
filter(
lambda fname: fname.startswith(qml.devices.null_qubit.RESOURCES_FNAME_PREFIX),
os.listdir(os.getcwd()),
)
)

assert len(written_files) == 1
resources_fname = written_files[0]

assert os.path.exists(resources_fname)

with open(resources_fname, "r", encoding="utf-8") as f:
stats = f.read()

os.remove(resources_fname)

assert stats == json.dumps(
{
"num_wires": 3,
"num_gates": 11,
"gate_types": {
"PauliX": 1,
"Hadamard": 1,
"C(Adj(T))": 1,
"2C(S)": 1,
"CNOT": 1,
"C(IsingXX)": 1,
"Adj(S)": 1,
"RX": 2,
"QubitUnitary": 1,
"ControlledQubitUnitary": 1,
},
}
)

# Check ordinary forward computation
qnode(inputs)
check_outputs()

# Check that resource tracking doesn't interfere with backprop
# Check backpropagation
assert qml.grad(qnode)(inputs) == 0

RESOURCES_FNAME = "__pennylane_resources_data.json"
assert os.path.exists(RESOURCES_FNAME)

with open(RESOURCES_FNAME, "r") as f:
stats = f.read()

os.remove(RESOURCES_FNAME)

assert stats == json.dumps(
{
"num_wires": 3,
"num_gates": 9,
"gate_types": {
"PauliX": 1,
"Hadamard": 1,
"C(Adj(T))": 1,
"2C(S)": 1,
"CNOT": 1,
"C(IsingXX)": 1,
"Adj(S)": 1,
"RX": 2,
},
}
)
check_outputs()


@pytest.mark.parametrize("shots", (None, 10))
Expand Down