cuda-quantum icon indicating copy to clipboard operation
cuda-quantum copied to clipboard

[RFC] [Language] Quantum allocation with state initialization

Open amccaskey opened this issue 1 year ago • 15 comments

TODO:

  • [x] Library mode implementation (@amccaskey)
  • [x] Python support for kernel_builder (@amccaskey)
  • [ ] Python support for ast_bridge kernels (@annagrin @amccaskey)
  • [x] C++ Bridge Support for qvector / qubit initialization (@schweitzpgi)
  • [x] C++ kernel_builder support for qalloc (@amccaskey)
  • [x] Validate C++ kernel_builder approach (check Alex on his work, @schweitzpgi)
  • [x] Error checking on number of elements in MLIR Verifier (@schweitzpgi )
  • [x] Simulation subclass work (implementing CircuitSimulator::addQubitsToState(with data)), need kron-prod on GPU (@anthony-santana)
  • [x] 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)
  • [ ] Documentation page (@anthony-santana @schweitzpgi @amccaskey)
  • [ ] cudaq::state input (@1tnguyen and @schweitzpgi, requires #1467)
  • [ ] Density Matrix and TensorNet backends updates will require #1467
  • [ ] Implement original from_state decomposition in MLIR (@boschmitt)
  • [ ] Simulation scalar type work - compile time error messages for incompatible object files

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();
  }
}

Python usage

Vectors of complex or floating-point numbers

Notes

  • Implicit conversion from a list of float to a list of complex is allowed on argument passing.
  • Automatic conversion of initializer elements will happen if the precision of the numbers in qvector initializer does not match the current simulation precision
    • Emit warning on conversion due to performance concerns, recommend using cudaq.amplitudes or cudaq.complex.
# Passing complex vectors as params
c = [.70710678 + 0j, 0., 0., 0.70710678]
@cudaq.kernel
def kernel(vec: list[complex]):
    q = cudaq.qvector(vec)

# Capturing complex vectors
c = [.70710678 + 0j, 0., 0., 0.70710678]
@cudaq.kernel
def kernel():
    q = cudaq.qvector(c)

# Capturing complex vectors and converting to 
# numpy array inside the kernel
c = [.70710678 + 0j, 0., 0., 0.70710678]
@cudaq.kernel
def kernel():
    q = cudaq.qvector(np.array(c))

# Creating complex arrays inside kernels
@cudaq.kernel
def kernel():
    q = cudaq.qvector([1.0 + 0j, 0., 0., 1.])

Numpy arrays

# From np array created inside a kernel with a complex dtype
c = [.70710678 + 0j, 0., 0., 0.70710678]
@cudaq.kernel
def kernel(vec: list[complex]):
    q = cudaq.qvector(np.array(vec, dtype=complex))

c = [.70710678 + 0j, 0., 0., 0.70710678]
@cudaq.kernel
def kernel(vec: list[complex]):
    q = cudaq.qvector(np.array(vec, dtype=np.complex64))

# Using precision-agnostic API
c = [.70710678 + 0j, 0., 0., 0.70710678]
@cudaq.kernel
def kernel(vec: list[complex]):
    q = cudaq.qvector(np.array(vec, dtype=cudaq.complex()))

c = cudaq.amplitudes([.70710678, 0., 0., 0.70710678])
@cudaq.kernel
def kernel(vec: list[complex]):
    q = cudaq.qvector(vec)


# Passing np arrays as params
c = np.array(c, dtype=cudaq.complex())
@cudaq.kernel
def kernel(vec: np.array):
    q = cudaq.qvector(vec)

c = np.array(c, dtype=cudaq.complex())
@cudaq.kernel
def kernel(vec: np.ndarray):
    q = cudaq.qvector(vec)

c = np.array(c, dtype=cudaq.complex())
@cudaq.kernel
def kernel(vec: np.ndarray[any, complex]):
    q = cudaq.qvector(vec)

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(...).

amccaskey avatar Jan 11 '24 19:01 amccaskey