[RFC] [Language] Quantum allocation with state initialization
amccaskey opened this issue ยท 10 comments
TODO:
- Library mode implementation (@amccaskey)
- Python support, builder and kernel (@annagrin @amccaskey)
- C++ Bridge Support for qvector / qubit initialization (@schweitzpgi)
- C++ kernel_builder support for qalloc (@amccaskey)
- Validate C++ kernel_builder approach (check Alex on his work, @schweitzpgi)
- Error checking on number of elements in MLIR Verifier (@schweitzpgi )
- Simulation subclass work (implementing
CircuitSimulator::addQubitsToState(with data)
), need kron-prod on GPU (@anthony-santana) - Qubit initializer list
- Check
vector<complex>
kernel input works end-to-end (@anthony-santana) - House-keeping: tests, python tests errors (@anthony-santana, @annagrin)
- Examples (@anthony-santana)
- cudaq::state input (@1tnguyen and @schweitzpgi, requires #1467)
- Density Matrix and TensorNet backends updates will require #1467
- Implement original
from_state
decomposition in MLIR (maybe @boschmitt)
Example with #1467
kernel(cudaq::state& initState) {
cudaq::qvector q = initState;
}
def kernel(initState : cudaq.state):
q = cudaq.qvector(initState)`
I propose we update the language to support quantum allocation with user-provided initial state specification. This should supersede functions like from_state(...)
on the kernel_builder
.
C++:
New constructors
qubit::qubit(const vector<complex<double>>&);
qubit::qubit(const initializer_list<complex<double>>&);
qvector::qvector(const vector<complex<double>>&);
qvector::qvector(const initializer_list<complex<double>>&);
New builder method
QuakeValue qalloc(vector<complex<double>> &)
Python
The Python builder would be similar as in the following.
v = [0., 1., 1., 0.]
qubits = kernel.qalloc(v)
@cudaq.kernel
def test(vec : list[complex]):
q = cudaq.qvector(vec)
...
C++ Usage
The following snippet demonstrates what this might look like:
__qpu__ auto test0() {
// Init from state vector
cudaq::qubit q = {0., 1.};
return mz(q);
}
__qpu__ auto test1() {
// Init from predefined state vectors
cudaq::qubit q = cudaq::ket::one;
return mz(q);
}
__qpu__ void test2() {
// Init from state vector
cudaq::qubit q = {M_SQRT1_2, M_SQRT1_2};
}
__qpu__ void test3() {
// Init from state vector
cudaq::qvector q = {M_SQRT1_2, 0., 0., M_SQRT1_2};
}
__qpu__ void test4(const std::vector<cudaq::complex> &state) {
// State vector from host
cudaq::qvector q = state;
}
void useBuilder() {
std::vector<cudaq::complex> state{M_SQRT1_2, 0., 0., M_SQRT1_2};
{
// (deferred) qubit allocation from concrete state vector
auto kernel = cudaq::make_kernel();
auto qubitsInitialized = kernel.qalloc(state);
}
{
// kernel parameterized on input state data
auto [kernel, inState] = cudaq::make_kernel<std::vector<cudaq::complex>>();
auto qubitsInitialized = kernel.qalloc(inState);
cudaq::sample(kernel, state).dump();
}
}
For library-mode / simulation we pass the state data along to NVQIR. For physical backends, we can replace runtime state data with the result of a circuit synthesis pass (like the current implementation in from_state(...)
.
Thanks @amccaskey for proposing this.
I have a clarification question regarding the semantics of a cudaq::qvector
's state. For example, what will be the return value of the following kernel?
__qpu__ bool foo() {
// Init from state vector
cudaq::qvector q = {0., 1., 0., 0.};
return cudaq::mz(q[0]);
}
I see two possibilities:
- It returns
true
.- Rationale: Index
1
in the initializer list corresponds to state|1>
(or, in binary,|0b01>
---the state is interpreted as a number). Ifq[0]
corresponds to the least significant (qu)bit and the state is interpreted as a number, then the state ofq[0]
is|0b1>
and thusmz(q[0])
returnstrue
.
- Rationale: Index
- It returns
false
:- Rationale: Index
1
in the initializer list corresponds to state|0>|1>
(or|0,1>
---here I could have used the short syntax|01>
but I want to make the point that this state should not be interpreted as a number, but as bitstring---or vector of bits). Henceq[0]
is|0>
andmz(q[0])
returnsfalse
.
- Rationale: Index
Thinking a bit forward, it seems to me that the second option is more appropriate. Eventually, we can define a quantum integer type, say cudaq::qint
, in which the state must be interpreted as a number:
__qpu__ bool foo() {
// Init from state vector
cudaq::qint q = {0., 1., 0., 0.};
return cudaq::mz(q[0]);
}
In this case, the kernel must return true
.
To make the API future-proof, we could also consider adding an optional bit-ordering vector argument (similar to custatevec
API).
cudaq::qvector q({0., 1., 0., 0.}, {0, 1});
=> q[0] should be |1>
cudaq::qvector q({0., 1., 0., 0.}, {1, 0});
=> q[1] should be |1>
The default when none provided could be one of those two endian conventions, e.g., LSB.
Thanks @amccaskey for proposing this.
I have a clarification question regarding the semantics of a
cudaq::qvector
's state. For example, what will be the return value of the following kernel?__qpu__ bool foo() { // Init from state vector cudaq::qvector q = {0., 1., 0., 0.}; return cudaq::mz(q[0]); }I see two possibilities:
It returns
true
.
- Rationale: Index
1
in the initializer list corresponds to state|1>
(or, in binary,|0b01>
---the state is interpreted as a number). Ifq[0]
corresponds to the least significant (qu)bit and the state is interpreted as a number, then the state ofq[0]
is|0b1>
and thusmz(q[0])
returnstrue
.It returns
false
:
- Rationale: Index
1
in the initializer list corresponds to state|0>|1>
(or|0,1>
---here I could have used the short syntax|01>
but I want to make the point that this state should not be interpreted as a number, but as bitstring---or vector of bits). Henceq[0]
is|0>
andmz(q[0])
returnsfalse
.Thinking a bit forward, it seems to me that the second option is more appropriate. Eventually, we can define a quantum integer type, say
cudaq::qint
, in which the state must be interpreted as a number:__qpu__ bool foo() { // Init from state vector cudaq::qint q = {0., 1., 0., 0.}; return cudaq::mz(q[0]); }In this case, the kernel must return
true
.
@boschmitt I prefer we go with bullet 2.
@boschmitt for your qint
example, I was hoping to support cudaq::qint q = 4;
instead of the initializer list. Do you foresee any gotchas there?
One thing to add, it will likely be good to update the cudaq::state
definition to be backend specific, and allow it as input to a CUDA Quantum kernel. If it is backend specific, we can have the sub-type hold a GPU device pointer and avoid copying the large data vector from device to host.
__qpu__ void test4(const cudaq::state &state) {
// Input state could wrap GPU device pointer
cudaq::qvector q = state;
... build off initial state ...
}
void useTest4() {
auto initStateGen = [](...) __qpu__ { ... };
auto intState = cudaq::get_state(initStateGen, ...);
cudaq::sample(test4, initState).dump();
}
I was hoping to support
cudaq::qint q = 4;
instead of the initializer list. Do you foresee any gotchas there?
Would this be interpreted as the bitstring 1,0,0
? You would need to know how many leading zeros are needed, so maybe an additional constructor parameter that is nQubits
.
If the goal is to construct states restricted to the computational basis, I would think rather than qint
we could add qvector(const std::vector<bool>&);
. Here the vector is of length nQubits
, rather than 2**nQubits
, and the construction is just specified by the bitstring.
@boschmitt for your
qint
example, I was hoping to supportcudaq::qint q = 4;
instead of the initializer list. Do you foresee any gotchas there?
We can certainly support it, but we would still have to define what it means with respect to a state vector. There will be more question to answer in order to support this idea. For example:
- How many qubits
cudaq::qint q = 4
creates? A fixed number, say 8, or the minimum necessary to represent4
? - Would we allow users to easily access the qubits, e.g., using
q[0]
? If we do, what wouldq[0]
return? - Would the user be able to create a
cudaq::qint
in which the state is a superposition of different integers ? If we allow, how the indices on the initializer list relate to the integers represented by the state?
Let me try to rephrase my questions: If we have a set of qubits we can try to initialize this set using a state vector, then the we need clarity on:
- How does the index of the state vector relates to the state, e.g. given a 3 qubit state vector, does index
1
corresponds to to the state represented as bitstring|001>
? - Depending on the type, e.g
cudaq::vector
ofcudaq::qint
, does the interpretation of|001>
changes? For example, if the type iscudaq::qvector
we interpret the state as|0, 0, 1>
andq[0]
state is|0>
; if the type iscudaq::qint
we interpret the state as|0b001>
andq[0]
is|1>
.
@boschmitt for your
qint
example, I was hoping to supportcudaq::qint q = 4;
instead of the initializer list. Do you foresee any gotchas there?We can certainly support it, but we would still have to define what it means with respect to a state vector. There will be more question to answer in order to support this idea. For example:
- How many qubits
cudaq::qint q = 4
creates? A fixed number, say 8, or the minimum necessary to represent4
?- Would we allow users to easily access the qubits, e.g., using
q[0]
? If we do, what wouldq[0]
return?- Would the user be able to create a
cudaq::qint
in which the state is a superposition of different integers ? If we allow, how the indices on the initializer list relate to the integers represented by the state?Let me try to rephrase my questions: If we have a set of qubits we can try to initialize this set using a state vector, then the we need clarity on:
- How does the index of the state vector relates to the state, e.g. given a 3 qubit state vector, does index
1
corresponds to to the state represented as bitstring|001>
?- Depending on the type, e.g
cudaq::vector
ofcudaq::qint
, does the interpretation of|001>
changes? For example, if the type iscudaq::qvector
we interpret the state as|0, 0, 1>
andq[0]
state is|0>
; if the type iscudaq::qint
we interpret the state as|0b001>
andq[0]
is|1>
.
I guess qint
may be a bit beyond this RFC, but to answer your first question - for qint
we might want a template parameter for the size of the qubit register qint<N>
and then typedefs for common ones.
I guess
qint
may be a bit beyond this RFC
I agree.
The main point for which I asked clarification is with regards of how the semantics of the state vector relates to the type, cudaq::qvector
, and accessing individual qubits. I provided two takes on it and it seems there is a preference for the second. The cudaq::qint
digression is just a thought experiment to see how our decision will stand the test of time and possible CUDA Quantum evolutions.
qubit initializer list
See PR #1461