Allocating and Using Quantum Memory in CUDA Quantum¶
CUDA Quantum provides a quantum memory model that enables one to think about
general qudits of information, dynamic and static registers of those qudits,
and whether those registers are owning or non-owning. The latter point
very much follows the same pattern one sees in modern C++, where we have
generic owning container types like the std::vector<T>
and
std::array<T>
, as well as non-owning container types like
the std::span<T>
.
To this end, CUDA Quantum defines a non-copyable unit of quantum information,
the cudaq::qudit<Levels>
template type. Because it is non-copyable,
instances of this type cannot be passed by value and must always be
passed by reference, in an effort to avoid copying, or cloning, the underlying
quantum information.
Note
Thus far (as of this beta release), the majority of CUDA Quantum
development work has focused on cudaq::qudit<2>
(which we
typedef
as cudaq::qubit
) but the demonstrations and
discussions that follow are meant to be general on qudits.
The CUDA Quantum quantum memory container types are the
cudaq::qarray<NQudits, Levels>
and the
cudaq::qview<NQudits, Levels>
, representing owning and non-owning
semantics, respectively. Notice that the first template parameter represents
the number of qudits contained. If the number of qubits is not known at compile
time, one can use the cudaq::qvector
container. These quantum memory
types are specifically designed to throw compile-time errors when they are
incorrectly used. An example of this for quantum memory and its underlying
ownership model can be seen in this snippet.
__qpu__ void fooBad(cudaq::qubit q) { ... };
__qpu__ void fooGood(cudaq::qubit& q) { ... };
__qpu__ void barBad(cudaq::qvector<> q) { ... };
__qpu__ void barGood(cudaq::qvector<>& q) { ... };
__qpu__ void barGoodWithView(cudaq::qview q) { ... };
struct myEntryPointKernel {
void operator()(int runtimeKnownInteger) __qpu__ {
// Allocate array-like compile-time-known
// register of 2 qubits. Owns the qubits.
cudaq::qarray<2> a;
// fooBad (a[0]); // Compile Error, cannot pass qubits by value (no copy)
// auto alias = a[0]; // Compile Error, cannot copy (auto defaults to by-value)
auto& alias = a[0]; // Must alias by reference
fooGood (a[0]); // Can pass by reference, no copy
// barBad(a); // Compile Error, cannot pass qarray by value
barGood(a); // Can pass by reference, no copy
// Allocate vector-like register of qubits
// Owns the qubits
cudaq::qvector b(runtimeKnownInteger);
// Get the front 2 qubits, which returns
// a cudaq::qview<>, it does not own the qubits.
auto sub_view = b.front(2);
// cudaq::qview is non-owning, it can be passed by value
barGoodWithView(sub_view);
// cudaq::qvector can also be passed to a kernel that accepts a view
barGoodWithView(a);
// Front with no size provided will
// return a reference to the first qubit.
// You must define this as a reference variable.
auto& frontQubit = b.front();
// a, b go out of scope, qubits deallocated
// returned to infinite global register of qubits
// NOTE automated uncomputation not currently implemented.
}
};
cudaq::qubit
and cudaq::qvector
types are owning types and
therefore cannot be passed by value to invoked pure quantum device kernels.
In order to share allocated registers with other quantum function calls,
one must pass by reference or define the invoked kernel to take the qubits
as a cudaq::qview
. Note that all slicing operations intended to
extract a sub-register from a given cudaq::qvector
will return a
non-owning qview
.