Skip to content

Commit 3e9b0d4

Browse files
jzaia18maliasadidime10
authored
Add resource tracking to NullQubit (#1619)
**Context:** For benchmarking, we need a method of precise resource tracking. This method has to be compatible with code run directly from PennyLane and should be able to track precise resources after various transformation passes have already occurred. See also: PennyLaneAI/pennylane-benchmarks#76 and PennyLaneAI/pennylane#7226 **Description of the Change:** Adds 2 new class attributes to `NullQubit`, as well as an optional argument to its constructor. When this option is toggled on, `NullQubit` will track precise gate counts while "executing" a circuit and save them to a file when all qubits are released. **Benefits:** It is now possible to track precise gate counts (with or without gate decomposition or other transformation passes) for measuring circuit complexity. **Possible Drawbacks:** This implementation currently write the resources JSON to a file on disk as opposed to neatly returning an object back through to the Python caller in PennyLane. In the future, a more robust solution may be desired. **Related GitHub Issues:** [sc-91366] --------- Co-authored-by: Ali Asadi <[email protected]> Co-authored-by: David Ittah <[email protected]>
1 parent aea14fc commit 3e9b0d4

File tree

8 files changed

+256
-6
lines changed

8 files changed

+256
-6
lines changed

.dep-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ enzyme=v0.0.149
88

99
# For a custom PL version, update the package version here and at
1010
# 'doc/requirements.txt
11-
pennylane=0.42.0-dev27
11+
pennylane=0.42.0-dev29
1212

1313
# For a custom LQ/LK version, update the package version here and at
1414
# 'doc/requirements.txt'

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pennylane_catalyst.egg-info
99
# Testing files and directories
1010
.lit
1111
.pytest_cache
12+
__pennylane_resources_data_*.json
1213

1314
# Packaging directories
1415
dist

doc/releases/changelog-dev.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@
154154

155155
<h3>Internal changes ⚙️</h3>
156156

157+
* `null.qubit` can now support an optional `track_resources` argument which allows it to record which gates are executed.
158+
[(#1619)](https://github.com/PennyLaneAI/catalyst/pull/1619)
159+
157160
* Add an xDSL MLIR plugin to denote whether we will be using xDSL to execute some passes.
158161
This changelog entry may be moved to new features once all branches are merged together.
159162
[(#1707)](https://github.com/PennyLaneAI/catalyst/pull/1707)
@@ -239,4 +242,5 @@ Mehrdad Malekmohammadi,
239242
Anton Naim Ibrahim,
240243
Erick Ochoa Lopez,
241244
Ritu Thombre,
242-
Paul Haochen Wang.
245+
Paul Haochen Wang,
246+
Jake Zaia.

doc/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ lxml_html_clean
3333
--extra-index-url https://test.pypi.org/simple/
3434
pennylane-lightning-kokkos==0.42.0-dev16
3535
pennylane-lightning==0.42.0-dev16
36-
pennylane==0.42.0-dev27
36+
pennylane==0.42.0-dev29

frontend/test/pytest/test_device_api.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,16 @@ def circuit():
137137
assert circuit.mlir
138138

139139

140+
def test_track_resources():
141+
"""Test that resource tracking settings get passed to the device."""
142+
dev = NullQubit(wires=2)
143+
assert "track_resources" in QJITDevice.extract_backend_info(dev, dev.capabilities).kwargs
144+
assert QJITDevice.extract_backend_info(dev, dev.capabilities).kwargs["track_resources"] is False
145+
146+
dev = NullQubit(wires=2, track_resources=True)
147+
assert "track_resources" in QJITDevice.extract_backend_info(dev, dev.capabilities).kwargs
148+
assert QJITDevice.extract_backend_info(dev, dev.capabilities).kwargs["track_resources"] is True
149+
150+
140151
if __name__ == "__main__":
141152
pytest.main(["-x", __file__])

runtime/lib/backend/common/Utils.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,25 @@ static inline auto parse_kwargs(std::string kwargs) -> std::unordered_map<std::s
126126
return map;
127127
}
128128

129+
template <class K, class V>
130+
void pretty_print_dict(const std::unordered_map<K, V> &map, size_t leadingSpaces = 0,
131+
std::ostream &out = std::cout)
132+
{
133+
const std::string indent(leadingSpaces, ' ');
134+
const std::string innerIndent = indent + " ";
135+
136+
out << indent << "{\n";
137+
auto it = map.begin();
138+
while (it != map.end()) {
139+
out << innerIndent << "\"" << it->first << "\": " << it->second;
140+
if (++it != map.end()) {
141+
out << ",";
142+
}
143+
out << "\n";
144+
}
145+
out << indent << "}";
146+
}
147+
129148
enum class MeasurementsT : uint8_t {
130149
None, // = 0
131150
Expval,

runtime/lib/backend/null_qubit/NullQubit.hpp

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@
1515
#pragma once
1616

1717
#include <algorithm> // generate_n
18+
#include <chrono>
1819
#include <complex>
20+
#include <cstdio>
21+
#include <fstream>
1922
#include <memory>
2023
#include <optional>
2124
#include <random>
25+
#include <unordered_map>
2226
#include <vector>
2327

2428
#include "DataView.hpp"
2529
#include "QuantumDevice.hpp"
2630
#include "QubitManager.hpp"
2731
#include "Types.h"
32+
#include "Utils.hpp"
2833

2934
namespace Catalyst::Runtime::Devices {
3035

@@ -40,14 +45,48 @@ namespace Catalyst::Runtime::Devices {
4045
* of the device; these are used to implement Quantum Instruction Set (QIS) instructions.
4146
*/
4247
struct NullQubit final : public Catalyst::Runtime::QuantumDevice {
43-
NullQubit(const std::string &kwargs = "{}") {}
44-
~NullQubit() = default; // LCOV_EXCL_LINE
48+
NullQubit(const std::string &kwargs = "{}")
49+
{
50+
auto device_kwargs = Catalyst::Runtime::parse_kwargs(kwargs);
51+
if (device_kwargs.find("track_resources") != device_kwargs.end()) {
52+
track_resources_ = device_kwargs["track_resources"] == "True";
53+
}
54+
}
55+
~NullQubit() {} // LCOV_EXCL_LINE
4556

4657
NullQubit &operator=(const NullQubit &) = delete;
4758
NullQubit(const NullQubit &) = delete;
4859
NullQubit(NullQubit &&) = delete;
4960
NullQubit &operator=(NullQubit &&) = delete;
5061

62+
/**
63+
* @brief Prints resources that would be used to execute this circuit as a JSON
64+
*/
65+
void PrintResourceUsage(FILE *resources_file)
66+
{
67+
// Store the 2 special variables and clear them from the map to make
68+
// pretty-printing easier
69+
const size_t num_qubits = resource_data_["num_qubits"];
70+
const size_t num_gates = resource_data_["num_gates"];
71+
resource_data_.erase("num_gates");
72+
resource_data_.erase("num_qubits");
73+
74+
std::stringstream resources;
75+
76+
resources << "{\n";
77+
resources << " \"num_qubits\": " << num_qubits << ",\n";
78+
resources << " \"num_gates\": " << num_gates << ",\n";
79+
resources << " \"gate_types\": ";
80+
pretty_print_dict(resource_data_, 2, resources);
81+
resources << "\n}" << std::endl;
82+
83+
fwrite(resources.str().c_str(), 1, resources.str().size(), resources_file);
84+
85+
// Restore 2 special variables
86+
resource_data_["num_qubits"] = num_qubits;
87+
resource_data_["num_gates"] = num_gates;
88+
}
89+
5190
/**
5291
* @brief Allocate a "null" qubit.
5392
*
@@ -56,6 +95,10 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice {
5695
auto AllocateQubit() -> QubitIdType
5796
{
5897
num_qubits_++; // next_id
98+
if (this->track_resources_) {
99+
// Store the highest number of qubits allocated at any time since device creation
100+
resource_data_["num_qubits"] = std::max(num_qubits_, resource_data_["num_qubits"]);
101+
}
59102
return this->qubit_manager.Allocate(num_qubits_);
60103
}
61104

@@ -94,6 +137,27 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice {
94137
{
95138
num_qubits_ = 0;
96139
this->qubit_manager.ReleaseAll();
140+
if (this->track_resources_) {
141+
auto time = std::chrono::high_resolution_clock::now();
142+
auto timestamp =
143+
std::chrono::duration_cast<std::chrono::nanoseconds>(time.time_since_epoch())
144+
.count();
145+
std::stringstream resources_fname;
146+
resources_fname << "__pennylane_resources_data_" << timestamp << ".json";
147+
148+
// Need to use FILE* instead of ofstream since ofstream has no way to atomically open a
149+
// file only if it does not already exist
150+
FILE *resources_file = fopen(resources_fname.str().c_str(), "wx");
151+
if (resources_file == nullptr) {
152+
std::string err_msg = "Error opening file '" + resources_fname.str() + "'.";
153+
RT_FAIL(err_msg.c_str());
154+
}
155+
else {
156+
PrintResourceUsage(resources_file);
157+
fclose(resources_file);
158+
}
159+
this->resource_data_.clear();
160+
}
97161
}
98162

99163
/**
@@ -170,17 +234,47 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice {
170234
const std::vector<QubitIdType> &controlled_wires = {},
171235
const std::vector<bool> &controlled_values = {})
172236
{
237+
if (this->track_resources_) {
238+
std::string prefix = "";
239+
std::string suffix = "";
240+
if (!controlled_wires.empty()) {
241+
if (controlled_wires.size() > 1) {
242+
prefix += std::to_string(controlled_wires.size());
243+
}
244+
prefix += "C(";
245+
suffix += ")";
246+
}
247+
if (inverse) {
248+
prefix += "Adj(";
249+
suffix += ")";
250+
}
251+
resource_data_["num_gates"]++;
252+
resource_data_[prefix + name + suffix]++;
253+
}
173254
}
174255

175256
/**
176257
* @brief Doesn't apply a given matrix directly to the state vector of a device.
177258
*
178259
*/
179260
void MatrixOperation(const std::vector<std::complex<double>> &,
180-
const std::vector<QubitIdType> &, bool,
261+
const std::vector<QubitIdType> &, bool inverse,
181262
const std::vector<QubitIdType> &controlled_wires = {},
182263
const std::vector<bool> &controlled_values = {})
183264
{
265+
if (this->track_resources_) {
266+
resource_data_["num_gates"]++;
267+
268+
std::string op_name = "QubitUnitary";
269+
270+
if (!controlled_wires.empty()) {
271+
op_name = "Controlled" + op_name;
272+
}
273+
if (inverse) {
274+
op_name = "Adj(" + op_name + ")";
275+
}
276+
resource_data_[op_name]++;
277+
}
184278
}
185279

186280
/**
@@ -360,9 +454,28 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice {
360454
return {0, 0, 0, {}, {}};
361455
}
362456

457+
/**
458+
* @brief Returns the number of gates used since the last time all qubits were released. Only
459+
* works if resource tracking is enabled
460+
*/
461+
auto ResourcesGetNumGates() -> std::size_t { return resource_data_["num_gates"]; }
462+
463+
/**
464+
* @brief Returns the maximum number of qubits used since the last time all qubits were
465+
* released. Only works if resource tracking is enabled
466+
*/
467+
auto ResourcesGetNumQubits() -> std::size_t { return resource_data_["num_qubits"]; }
468+
469+
/**
470+
* @brief Returns whether the device is tracking resources or not.
471+
*/
472+
auto IsTrackingResources() const -> bool { return track_resources_; }
473+
363474
private:
475+
bool track_resources_{false};
364476
std::size_t num_qubits_{0};
365477
std::size_t device_shots_{0};
478+
std::unordered_map<std::string, std::size_t> resource_data_;
366479
Catalyst::Runtime::QubitManager<QubitIdType, size_t> qubit_manager{};
367480

368481
// static constants for RESULT values

runtime/tests/Test_NullQubit.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <catch2/catch_approx.hpp>
1717
#include <catch2/catch_test_macros.hpp>
1818
#include <catch2/matchers/catch_matchers_string.hpp>
19+
#include <cstdio>
1920

2021
#include "ExecutionContext.hpp"
2122
#include "QuantumDevice.hpp"
@@ -598,3 +599,104 @@ TEST_CASE("Test NullQubit device shots methods", "[NullQubit]")
598599
CHECK(sim->GetDeviceShots() == i);
599600
}
600601
}
602+
603+
TEST_CASE("Test NullQubit device resource tracking", "[NullQubit]")
604+
{
605+
// The name of the file where the resource usage data is stored
606+
constexpr char RESOURCES_FNAME[] = "__pennylane_resources_data.json";
607+
608+
// Open a file for writing the resources JSON
609+
FILE *resource_file_w = fopen(RESOURCES_FNAME, "wx");
610+
if (resource_file_w == nullptr) { // LCOV_EXCL_LINE
611+
FAIL("Failed to open resource usage file for writing."); // LCOV_EXCL_LINE
612+
}
613+
614+
std::unique_ptr<NullQubit> dummy = std::make_unique<NullQubit>();
615+
CHECK(dummy->IsTrackingResources() == false);
616+
617+
std::unique_ptr<NullQubit> sim = std::make_unique<NullQubit>("{'track_resources':True}");
618+
CHECK(sim->IsTrackingResources() == true);
619+
CHECK(sim->ResourcesGetNumGates() == 0);
620+
CHECK(sim->ResourcesGetNumQubits() == 0);
621+
622+
std::vector<QubitIdType> Qs = sim->AllocateQubits(4);
623+
624+
CHECK(sim->ResourcesGetNumGates() == 0);
625+
CHECK(sim->ResourcesGetNumQubits() == 4);
626+
627+
// Apply named gates to test all possible name modifiers
628+
sim->NamedOperation("PauliX", {}, {Qs[0]}, false);
629+
sim->NamedOperation("T", {}, {Qs[0]}, true);
630+
sim->NamedOperation("S", {}, {Qs[0]}, false, {Qs[2]});
631+
sim->NamedOperation("S", {}, {Qs[0]}, false, {Qs[1], Qs[2]});
632+
sim->NamedOperation("T", {}, {Qs[0]}, true, {Qs[2]});
633+
sim->NamedOperation("CNOT", {}, {Qs[0], Qs[1]}, false);
634+
635+
CHECK(sim->ResourcesGetNumGates() == 6);
636+
CHECK(sim->ResourcesGetNumQubits() == 4);
637+
638+
// Applying an empty matrix is fine for NullQubit
639+
sim->MatrixOperation({}, {Qs[0]}, false);
640+
sim->MatrixOperation({}, {Qs[0]}, false, {Qs[1]});
641+
sim->MatrixOperation({}, {Qs[0]}, true);
642+
sim->MatrixOperation({}, {Qs[0]}, true, {Qs[1], Qs[2]});
643+
644+
CHECK(sim->ResourcesGetNumGates() == 10);
645+
CHECK(sim->ResourcesGetNumQubits() == 4);
646+
647+
// Capture resources usage
648+
sim->PrintResourceUsage(resource_file_w);
649+
fclose(resource_file_w);
650+
651+
// Open the file of resource data
652+
std::ifstream resource_file_r(RESOURCES_FNAME);
653+
CHECK(resource_file_r.is_open()); // fail-fast if file failed to create
654+
655+
std::vector<std::string> resource_names = {"PauliX",
656+
"C(Adj(T))",
657+
"Adj(T)",
658+
"C(S)",
659+
"2C(S)",
660+
"S",
661+
"CNOT",
662+
"Adj(ControlledQubitUnitary)",
663+
"ControlledQubitUnitary",
664+
"Adj(QubitUnitary)",
665+
"QubitUnitary"};
666+
667+
// Check all fields have the correct value
668+
std::string full_json;
669+
while (resource_file_r) {
670+
std::string line;
671+
std::getline(resource_file_r, line);
672+
if (line.find("num_qubits") != std::string::npos) {
673+
CHECK(line.find("4") != std::string::npos);
674+
}
675+
if (line.find("num_gates") != std::string::npos) {
676+
CHECK(line.find("10") != std::string::npos);
677+
}
678+
// If one of the resource names is in the line, check that there is precisely 1
679+
for (const auto &name : resource_names) {
680+
if (line.find(name) != std::string::npos) {
681+
CHECK(line.find("1") != std::string::npos);
682+
break;
683+
}
684+
}
685+
full_json += line + "\n";
686+
}
687+
resource_file_r.close();
688+
std::remove(RESOURCES_FNAME);
689+
690+
// Ensure all expected fields are present
691+
CHECK(full_json.find("num_qubits") != std::string::npos);
692+
CHECK(full_json.find("num_gates") != std::string::npos);
693+
for (const auto &name : resource_names) {
694+
CHECK(full_json.find(name) != std::string::npos);
695+
}
696+
697+
// Check that releasing resets
698+
sim->ReleaseAllQubits();
699+
700+
CHECK(sim->ResourcesGetNumGates() == 0);
701+
CHECK(sim->ResourcesGetNumQubits() == 0);
702+
}

0 commit comments

Comments
 (0)