Skip to content

Commit 428cc7e

Browse files
authored
Document the semantics of the QuantumDevice interface (#1680)
This PR adds detailed descriptions of the semantics of each method in the `QuantumDevice` interface used by runtime plugins. **Looking for feedback on**: - whether the semantics make sense as written (mostly they are describing the existing behaviour in particular w.r.t to the lighting devices) - which methods should be considered optional - I gave it my best guest about what a "minimal" working device would need to support - whether anything is missing - whether any TODOs should be implemented right now - At the very least I would say we should remove `shots` from MP signatures, since that was officially deprecated. - I would say removing the `One`/`Zero` can also be at the top of the list, since we already don't use them, and the functions to make use of them are already gone (like `__quantum__rt__compare_result`), so they really don't make any sense anymore. - Some of the type restructuring might also be nice. closes #1512 [sc-81294] While writing these, I identified the following TODOs to improve the interface (possibly at a later date): ``` //////////////////// ////// TODO ////// //////////////////// /* * - Remove the `shots` parameter from sample and counts methods, as that has already been * deprecated in #1310. * - Revisit confusing or inconsistent type usage and/or naming in the runtime/device interface: * - QUBIT type: `QUBIT` (undefined struct) vs `QUBIT*` vs `QubitIdType` * - RESULT type: `RESULT` (bool) vs `Result` (RESULT*) * - OBS type: `ObsId` vs `ObsIdType` * - Remove default argument values from the interface. * - Include types only where they are needed: * - CAPI types in the CAPI header * - Runtime types in CAPI implementation or QuantumDevice header * - Also: Should the QuantumDevice interface use the concrete C++ types instead of the type aliases? * The former might make it clearer for any implementers what types are used. * - Revisit instructions in the QuantumDevice interface: * - move `PrintState` into the runtime or eliminate entirely? * Seems unnecessary as a dedicated function since one can just call `State` + print. * - remove `Result` specific functions (`One`, `Zero`, `compare_result` - already gone I guess)? * With results being forced as `bool*`, there is no reason to keep these functions. * Originally they were meant to allow an arbitrary representation of measurement results, * and would thus require special runtime functions, but this is not the case. * - revisit qubit management functions * - ideally as part of dynamic allocation or multiple register support * - several inconsistencies here, but first and foremost `AllocateQubits` has no counterpart, * only `ReleaseAllQubits` * - QubitUnitary / Hermitian: why creating `vector<complex>` instead of forwarding a `DataView`? * - Why is Jacobian in `Gradient` a `vector<buffer>` instead of a 2D buffer? * - With `Probs` / `PartialProbs` and similar ops, there are separate instructions for acting on a subsystem, * but with `Gradient` the same function is reused for both (dictated by the `trainParams` argument`). * * Important: All devices need to be updated after such interface changes! */ ```
1 parent 6b142ed commit 428cc7e

File tree

24 files changed

+711
-947
lines changed

24 files changed

+711
-947
lines changed

.dep-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ pennylane=0.42.0-dev15
1212

1313
# For a custom LQ/LK version, update the package version here and at
1414
# 'doc/requirements.txt'
15-
lightning=0.42.0-dev7
15+
lightning=0.42.0-dev11

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ endif
170170
endif
171171
@echo "check the Catalyst PyTest suite"
172172
$(ASAN_COMMAND) $(PYTHON) -m pytest frontend/test/pytest --tb=native --backend=$(TEST_BACKEND) --runbraket=$(TEST_BRAKET) $(PYTEST_FLAGS)
173+
ifeq ($(TEST_BRAKET), NONE)
174+
$(ASAN_COMMAND) $(PYTHON) -m pytest frontend/test/async_tests --tb=native --backend=$(TEST_BACKEND)
173175
$(ASAN_COMMAND) $(PYTHON) -m pytest frontend/test/test_oqc/oqc
174176
ifeq ($(ENABLE_OQD), ON)
175177
$(ASAN_COMMAND) $(PYTHON) -m pytest frontend/test/test_oqd/oqd
176178
endif
177-
ifeq ($(TEST_BRAKET), NONE)
178-
$(ASAN_COMMAND) $(PYTHON) -m pytest frontend/test/async_tests --tb=native --backend=$(TEST_BACKEND)
179179
endif
180180

181181
test-demos:

doc/dev/custom_devices.rst

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,81 +5,50 @@ Custom Devices
55
Differences between PennyLane and Catalyst
66
==========================================
77

8-
PennyLane and Catalyst treat devices a bit differently.
9-
In PennyLane, one is able to `define devices <https://docs.pennylane.ai/en/stable/development/plugins.html>`_ in Python.
10-
Catalyst cannot interface with Python devices yet.
11-
Instead, Catalyst can only interact with devices that implement the `QuantumDevice <../api/file_runtime_include_QuantumDevice.hpp.html>`_ class.
8+
PennyLane and Catalyst treat devices a bit differently. In PennyLane, one is able to
9+
`define device plugins <https://docs.pennylane.ai/en/stable/development/plugins.html>`_ in Python.
10+
Catalyst cannot interface with Python devices, instead the Catalyst Runtime provides a plugin system
11+
for C++ devices that implement the
12+
`QuantumDevice <../api/structCatalyst_1_1Runtime_1_1QuantumDevice.html>`_ interface class.
1213

13-
Here is an example of a custom ``QuantumDevice`` in which every single quantum operation is implemented as a no-operation.
14-
Additionally, all measurements will always return ``true``.
14+
Below, you can see an example of a very basic custom ``QuantumDevice`` in which quantum operations
15+
have been stubbed out. For a complete description of the interface check out the header file or its
16+
`API documentation <../api/structCatalyst_1_1Runtime_1_1QuantumDevice.html>`_.
1517

1618
.. code-block:: c++
1719

1820
#include <QuantumDevice.hpp>
1921

2022
struct CustomDevice final : public Catalyst::Runtime::QuantumDevice {
21-
CustomDevice([[maybe_unused]] const std::string &kwargs = "{}") {}
23+
// Static device parameters can be passed down from the Python device to the constructor.
24+
CustomDevice(const std::string &kwargs = "{}") {}
2225
~CustomDevice() = default;
2326

24-
CustomDevice &operator=(const QuantumDevice &) = delete;
25-
CustomDevice(const CustomDevice &) = delete;
26-
CustomDevice(CustomDevice &&) = delete;
27-
CustomDevice &operator=(QuantumDevice &&) = delete;
27+
auto GetNumQubits() const -> size_t override { return device_nqubits; }
28+
void SetDeviceShots(size_t shots) override { device_shots = shots; }
29+
auto GetDeviceShots() const -> size_t override { return device_shots; }
2830

29-
auto AllocateQubit() -> QubitIdType override { return 0; }
31+
// Allocations produce numeric qubit identifiers that are passed to other operations.
3032
auto AllocateQubits(size_t num_qubits) -> std::vector<QubitIdType> override {
31-
return std::vector<QubitIdType>(num_qubits);
33+
std::vector<QubitIdType> ids(num_qubits);
34+
std::iota(ids.begin(), ids.end(), 0);
35+
device_nqubits = num_qubits;
36+
return ids
3237
}
33-
[[nodiscard]] auto Zero() const -> Result override { return NULL; }
34-
[[nodiscard]] auto One() const -> Result override { return NULL; }
35-
auto Observable(ObsId, const std::vector<std::complex<double>> &,
36-
const std::vector<QubitIdType> &) -> ObsIdType override {
37-
return 0;
38-
}
39-
auto TensorObservable(const std::vector<ObsIdType> &) -> ObsIdType override { return 0; }
40-
auto HamiltonianObservable(const std::vector<double> &, const std::vector<ObsIdType> &)
41-
-> ObsIdType override {
42-
return 0;
43-
}
44-
auto Measure(QubitIdType) -> Result override {
45-
bool *ret = (bool *)malloc(sizeof(bool));
46-
*ret = true;
47-
return ret;
38+
void ReleaseAllQubits() override {}
39+
40+
// Here we don't do anything yet.
41+
void NamedOperation(const std::string &name, const std::vector<double> &params,
42+
const std::vector<QubitIdType> &wires, bool inverse,
43+
const std::vector<QubitIdType> &controlled_wires,
44+
const std::vector<bool> &controlled_values) override {}
45+
auto Measure(QubitIdType wire, std::optional<int32_t> postselect) -> Result override {
46+
return static_cast<Result>(malloc(sizeof(bool)));
4847
}
4948

50-
void ReleaseQubit(QubitIdType) override {}
51-
void ReleaseAllQubits() override {}
52-
[[nodiscard]] auto GetNumQubits() const -> size_t override { return 0; }
53-
void SetDeviceShots(size_t shots) override {}
54-
[[nodiscard]] auto GetDeviceShots() const -> size_t override { return 0; }
55-
void StartTapeRecording() override {}
56-
void StopTapeRecording() override {}
57-
void PrintState() override {}
58-
void NamedOperation(const std::string &, const std::vector<double> &,
59-
const std::vector<QubitIdType> &,
60-
bool,
61-
const std::vector<QubitIdType> &,
62-
const std::vector<bool> &
63-
) override {}
64-
void MatrixOperation(const std::vector<std::complex<double>> &,
65-
const std::vector<QubitIdType> &,
66-
bool,
67-
const std::vector<QubitIdType> &,
68-
const std::vector<bool> &
69-
) override{}
70-
auto Expval(ObsIdType) -> double override { return 0.0; }
71-
auto Var(ObsIdType) -> double override { return 0.0; }
72-
void State(DataView<std::complex<double>, 1> &) override {}
73-
void Probs(DataView<double, 1> &) override {}
74-
void PartialProbs(DataView<double, 1> &, const std::vector<QubitIdType> &) override {}
75-
void Sample(DataView<double, 2> &, size_t) override {}
76-
void PartialSample(DataView<double, 2> &, const std::vector<QubitIdType> &, size_t) override {}
77-
void Counts(DataView<double, 1> &, DataView<int64_t, 1> &, size_t) override {}
78-
79-
void PartialCounts(DataView<double, 1> &, DataView<int64_t, 1> &,
80-
const std::vector<QubitIdType> &, size_t) override {}
81-
82-
void Gradient(std::vector<DataView<double, 1>> &, const std::vector<size_t> &) override {}
49+
private:
50+
size_t device_nqubits;
51+
size_t device_shots;
8352
};
8453

8554
In addition to implementing the ``QuantumDevice`` class, one must implement an entry point for the
@@ -115,8 +84,7 @@ Python devices" section further down for details.
11584

11685
``CustomDevice(kwargs)`` serves as a constructor for your custom device, with ``kwargs``
11786
as a string of device specifications and options, represented in Python dictionary format.
118-
An example could be the default number of device shots, encoded as the following string:
119-
``"{'shots': 1000}"``.
87+
An example could be some noise parameter or other configurable behaviour: ``"{'noise': 0.5}"``.
12088

12189
Note that these parameters are automatically initialized in the frontend if the library is
12290
provided as a PennyLane plugin device (see :func:`qml.device() <pennylane.device>`).
@@ -125,14 +93,15 @@ The destructor of ``CustomDevice`` will be automatically called by the runtime.
12593

12694
.. warning::
12795

128-
This interface might change quickly in the near future.
129-
Please check back regularly for updates and to ensure your device is compatible with
130-
a specific version of Catalyst.
96+
This interface might change quickly in the near future, but breaking changes will be announced
97+
in release changelogs. Please check back regularly for updates and to ensure your device is
98+
compatible with a specific version of Catalyst.
13199

132100
How to compile custom devices
133101
=============================
134102

135-
One can follow the ``catalyst/runtime/tests/third_party/CMakeLists.txt`` `as an example. <https://github.com/PennyLaneAI/catalyst/blob/26b412b298f22565fea529d2019554e7ad9b9624/runtime/tests/third_party/CMakeLists.txt>`_
103+
One can follow the ``catalyst/runtime/tests/third_party/CMakeLists.txt``
104+
`as an example <https://github.com/PennyLaneAI/catalyst/blob/26b412b298f22565fea529d2019554e7ad9b9624/runtime/tests/third_party/CMakeLists.txt>`_.
136105

137106
.. code-block:: cmake
138107
@@ -153,11 +122,17 @@ Integration with Python devices
153122
There are two things that are needed in order to integrate with PennyLane devices:
154123

155124
* Adding a ``get_c_interface`` method to your ``qml.devices.Device`` class.
156-
* Adding a ``config_filepath`` class variable pointing to your configuration file. This file should be a `toml file <https://toml.io/en/>`_ with fields that describe what gates and features are supported by your device.
157-
* Optionally, adding a ``device_kwargs`` dictionary for runtime parameters to pass from the PennyLane device to the ``QuantumDevice`` upon initialization.
158-
159-
If you already have a custom PennyLane device defined in Python and have added a shared object that corresponds to your implementation of the ``QuantumDevice`` class, then all you need to do is to add a ``get_c_interface`` method to your PennyLane device.
160-
The ``get_c_interface`` method should be a static method that takes no parameters and returns the complete path to your shared library with the ``QuantumDevice`` implementation.
125+
* Adding a ``config_filepath`` class variable pointing to your configuration file. This file should
126+
be a `toml file <https://toml.io/en/>`_ with fields that describe what gates and features are
127+
supported by your device.
128+
* Optionally, adding a ``device_kwargs`` dictionary for runtime parameters to pass from the
129+
PennyLane device to the ``QuantumDevice`` upon initialization.
130+
131+
If you already have a custom PennyLane device defined in Python and have added a shared object that
132+
corresponds to your implementation of the ``QuantumDevice`` class, then all you need to do is to add
133+
a ``get_c_interface`` method to your PennyLane device. The ``get_c_interface`` method should be a
134+
static method that takes no parameters and returns the complete path to your shared library with the
135+
``QuantumDevice`` implementation.
161136

162137
.. note::
163138

doc/releases/changelog-dev.md

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,31 @@
1515

1616
<h3>Breaking changes 💔</h3>
1717

18+
* (Device Developers Only) The `QuantumDevice` interface in the Catalyst Runtime plugin system
19+
has been modified, which requires recompiling plugins for binary compatibility.
20+
[(#1680)](https://github.com/PennyLaneAI/catalyst/pull/1680)
21+
22+
As announced in the [0.10.0 release](https://docs.pennylane.ai/projects/catalyst/en/stable/dev/release_notes.html#release-0-10-0),
23+
the `shots` argument has been removed from the `Sample` and `Counts` methods in the interface,
24+
since it unnecessarily duplicated this information. Additionally, `shots` will no longer be
25+
supplied by Catalyst through the `kwargs` parameter of the device constructor. The shot value must
26+
now be obtained through the `SetDeviceShots` method.
27+
28+
Further, the documentation for the interface has been overhauled and now describes the
29+
expected behaviour of each method in detail. A quality of life improvement is that optional
30+
methods are now clearly marked as such and also come with a default implementation in the base
31+
class, so device plugins need only override the methods they wish to support.
32+
33+
Finally, the `PrintState` and the `One`/`Zero` utility functions have been removed, since they
34+
did not serve a convincing purpose.
35+
1836
* Catalyst has removed the `experimental_capture` keyword from the `qjit` decorator in favour of
1937
unified behaviour with PennyLane.
2038
[(#1657)](https://github.com/PennyLaneAI/catalyst/pull/1657)
2139

22-
Instead of enabling program capture with Catalyst via `qjit(experimental_capture=True)`, program capture
23-
can be enabled via the global toggle `qml.capture.enable()`:
24-
40+
Instead of enabling program capture with Catalyst via `qjit(experimental_capture=True)`, program
41+
capture can be enabled via the global toggle `qml.capture.enable()`:
42+
2543
```python
2644
import pennylane as qml
2745
from catalyst import qjit
@@ -42,7 +60,8 @@
4260

4361
Disabling program capture can be done with `qml.capture.disable()`.
4462

45-
* The `ppr_to_ppm` pass has been renamed to `merge_ppr_ppm` (same functionality). A new `ppr_to_ppm` will handle direct decomposition of PPRs into PPMs.
63+
* The `ppr_to_ppm` pass has been renamed to `merge_ppr_ppm` (same functionality). A new `ppr_to_ppm`
64+
will handle direct decomposition of PPRs into PPMs.
4665
[(#1688)](https://github.com/PennyLaneAI/catalyst/pull/1688)
4766

4867
<h3>Deprecations 👋</h3>
@@ -59,11 +78,12 @@
5978

6079
<h3>Internal changes ⚙️</h3>
6180

62-
* Creates a function that allows developers to register an equivalent MLIR transform for a given PLxPR transform.
81+
* Creates a function that allows developers to register an equivalent MLIR transform for a given
82+
PLxPR transform.
6383
[(#1705)](https://github.com/PennyLaneAI/catalyst/pull/1705)
6484

65-
* Stop overriding the `num_wires` property when the operator can exist on `AnyWires`. This allows the deprecation
66-
of `WiresEnum` in pennylane.
85+
* Stop overriding the `num_wires` property when the operator can exist on `AnyWires`. This allows
86+
the deprecation of `WiresEnum` in pennylane.
6787
[(#1667)](https://github.com/PennyLaneAI/catalyst/pull/1667)
6888
[(#1676)](https://github.com/PennyLaneAI/catalyst/pull/1676)
6989

@@ -80,7 +100,8 @@
80100
This runtime stub is currently for mock execution only and should be treated as a placeholder
81101
operation. Internally, it functions just as a computational-basis measurement instruction.
82102

83-
* The utility function `EnsureFunctionDeclaration` is refactored into the `Utils` of the `Catalyst` dialect, instead of being duplicated in each individual dialect.
103+
* The utility function `EnsureFunctionDeclaration` is refactored into the `Utils` of the `Catalyst`
104+
dialect, instead of being duplicated in each individual dialect.
84105
[(#1683)](https://github.com/PennyLaneAI/catalyst/pull/1683)
85106

86107
* The assembly format for some MLIR operations now includes adjoint.

doc/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ lxml_html_clean
3030

3131
# Pre-install PL development wheels
3232
--extra-index-url https://test.pypi.org/simple/
33-
pennylane-lightning-kokkos==0.42.0-dev7
34-
pennylane-lightning==0.42.0-dev7
33+
pennylane-lightning-kokkos==0.42.0-dev11
34+
pennylane-lightning==0.42.0-dev11
3535
pennylane==0.42.0-dev15

frontend/catalyst/device/qjit_device.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,6 @@ def extract_backend_info(
179179
if not pathlib.Path(device_lpath).is_file():
180180
raise CompileError(f"Device at {device_lpath} cannot be found!")
181181

182-
if hasattr(device, "shots"):
183-
shots = get_device_shots(device) or 0
184-
device_kwargs["shots"] = shots
185-
186182
if dname == "braket.local.qubit": # pragma: no cover
187183
device_kwargs["device_type"] = dname
188184
device_kwargs["backend"] = (

0 commit comments

Comments
 (0)