Executing Kernels ================= In CUDA-Q, there are 3 ways in which one can execute quantum kernels: 1. `sample`: yields measurement counts 2. `observe`: yields expectation values 3. `get_state`: yields the quantum statevector of the computation Sample ------ Quantum states collapse upon measurement and hence need to be sampled many times to gather statistics. The CUDA-Q `sample` call enables this: .. tab:: Python .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin Sample] :end-before: [End Sample] .. tab:: C++ .. literalinclude:: ../../examples/cpp/executing_kernels_sample.cpp :language: cpp :start-after: [Begin Sample] :end-before: [End Sample] .. literalinclude:: ../../examples/python/executing_kernels.py :start-after: [Begin SampleOutput] :end-before: [End SampleOutput] Note that there is a subtle difference between how `sample` is executed with the target device set to a simulator or with the target device set to a QPU. In simulation mode, the quantum state is built once and then sampled :math:`s` times where :math:`s` equals the `shots_count`. In hardware execution mode, the quantum state collapses upon measurement and hence needs to be rebuilt over and over again. There are a number of helpful tools that can be found in the `API docs `_ to process the `Sample_Result` object produced by `sample`. Observe ------- The `observe` function allows us to calculate expectation values. We must supply a spin operator in the form of a Hamiltonian, :math:`H`, from which we would like to calculate :math:`\langle\psi|H|\psi\rangle`. .. tab:: Python .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin Observe] :end-before: [End Observe] .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin ObserveOutput] :end-before: [End ObserveOutput] .. tab:: C++ .. literalinclude:: ../../examples/cpp/executing_kernels_observe.cpp :language: cpp :start-after: [Begin Observe] :end-before: [End Observe] .. literalinclude:: ../../examples/cpp/executing_kernels_observe.cpp :language: cpp :start-after: [Begin `ObserveOutput`] :end-before: [End `ObserveOutput`] Get State --------- The `get_state` function gives us access to the quantum statevector of the computation. Remember, that this is only feasible in simulation mode. .. tab:: Python .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin `GetState`] :end-before: [End `GetState`] .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin `GetStateOutput`] :end-before: [End `GetStateOutput`] .. tab:: C++ .. literalinclude:: ../../examples/cpp/executing_kernels_state.cpp :language: cpp :start-after: [Begin `GetState`] :end-before: [End `GetState`] .. literalinclude:: ../../examples/cpp/executing_kernels_state.cpp :language: cpp :start-after: [Begin `GetStateOutput`] :end-before: [End `GetStateOutput`] The statevector generated by the `get_state` command follows Big-endian convention for associating numbers with their binary representations, which places the least significant bit on the left. That is, for the example of a 2-bit system, we have the following translation between integers and bits: .. math:: \begin{matrix} \text{Integer} & \text{Binary representation}\\ & \text{least signinificant bit on left}\\ 0 = \textcolor{red}{0}*2^0 + \textcolor{blue}{0} *2^1 & \textcolor{red}{0}\textcolor{blue}{0} \\ 1 = \textcolor{red}{1}*2^0 + \textcolor{blue}{0} *2^1 & \textcolor{red}{1}\textcolor{blue}{0} \\ 2 = \textcolor{red}{0}*2^0 + \textcolor{blue}{1} *2^1 & \textcolor{red}{0}\textcolor{blue}{1} \\ 3 = \textcolor{red}{1}*2^0 + \textcolor{blue}{1} *2^1 & \textcolor{red}{1}\textcolor{blue}{1} \end{matrix} Parallelization Techniques -------------------------- The most intensive task in the computation is the execution of the quantum kernel hence each execution function: `sample`, `observe` and `get_state` can be parallelized given access to multiple quantum processing units (multi-QPU). Since multi-QPU platforms are not yet feasible, we emulate each QPU with a GPU. Observe Asynchronous -------------------- Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result. `observe` can be a time intensive task. We can parallelize the execution of `observe` via the arguments it accepts. .. tab:: Python .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin `ObserveAsync`] :end-before: [End `ObserveAsync`] .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin `ObserveAsyncOutput`] :end-before: [End `ObserveAsyncOutput`] .. tab:: C++ .. literalinclude:: ../../examples/cpp/executing_kernels_observe_async.cpp :language: cpp :start-after: [Begin `ObserveAsync`] :end-before: [End `ObserveAsync`] .. literalinclude:: ../../examples/cpp/executing_kernels_observe_async.cpp :language: cpp :start-after: [Begin `ObserveAsyncOutput`] :end-before: [End `ObserveAsyncOutput`] Above we parallelized the `observe` call over the `hamiltonian` parameter; however, we can parallelize over any of the arguments it accepts by just iterating over the `qpu_id`. Sample Asynchronous ------------------- Similar to `observe_async` above, sample also supports asynchronous execution for the `arguments it accepts `_. One can parallelize over various kernels, variational parameters or even distribute shots counts over multiple QPUs. .. tab:: Python .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin `SampleAsync`] :end-before: [End `SampleAsync`] .. literalinclude:: ../../examples/python/executing_kernels.py :language: python :start-after: [Begin `SampleAsyncOutput`] :end-before: [End `SampleAsyncOutput`] .. tab:: C++ .. literalinclude:: ../../examples/cpp/executing_kernels_sample_async.cpp :language: cpp :start-after: [Begin `SampleAsync`] :end-before: [End `SampleAsync`] .. literalinclude:: ../../examples/cpp/executing_kernels_sample_async.cpp :language: cpp :start-after: [Begin `SampleAsyncOutput`] :end-before: [End `SampleAsyncOutput`] Get State Asynchronous ---------------------- Similar to `sample_async` above, `get_state_async` also supports asynchronous execution for the `input arguments it accepts `_.