NVIDIA/cuda-quantum

[RFC] [Language] Quantum allocation with state initialization

amccaskey opened this issue ยท 10 comments

TODO:

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). If q[0] corresponds to the least significant (qu)bit and the state is interpreted as a number, then the state of q[0] is |0b1> and thus mz(q[0]) returns true.
  • 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). Hence q[0] is |0> and mz(q[0]) returns false.

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). If q[0] corresponds to the least significant (qu)bit and the state is interpreted as a number, then the state of q[0] is |0b1> and thus mz(q[0]) returns true.
  • 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). Hence q[0] is |0> and mz(q[0]) returns false.

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 support cudaq::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 represent 4?
  • Would we allow users to easily access the qubits, e.g., using q[0]? If we do, what would q[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 of cudaq::qint, does the interpretation of |001> changes? For example, if the type is cudaq::qvector we interpret the state as |0, 0, 1> and q[0] state is |0>; if the type is cudaq::qint we interpret the state as |0b001> and q[0] is |1>.

@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?

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 represent 4?
  • Would we allow users to easily access the qubits, e.g., using q[0]? If we do, what would q[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 of cudaq::qint, does the interpretation of |001> changes? For example, if the type is cudaq::qvector we interpret the state as |0, 0, 1> and q[0] state is |0>; if the type is cudaq::qint we interpret the state as |0b001> and q[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