cuda-quantum
cuda-quantum copied to clipboard
[RFC] Mid-circuit measurement and sampling in library mode
Background
Now that we are moving simulation based targets to runtime-only library mode (no MLIR compilation), we need to revisit how we enable sampling of dynamic circuits (those circuits with mid-circuit measurement + conditional feedback). An example circuit for this is
struct kernel {
void operator()() __qpu__ {
cudaq::qreg<3> q;
// Initial state preparation
x(q[0]);
// Create Bell pair
h(q[1]);
x<cudaq::ctrl>(q[1], q[2]);
x<cudaq::ctrl>(q[0], q[1]);
h(q[0]);
auto b0 = mz(q[0]);
auto b1 = mz(q[1]);
if (b1)
x(q[2]);
if (b0)
z(q[2]);
mz(q[2]);
}
};
For a standard kernel with no measurements or measurements appended at the end of the function, backend simulations can simulate the circuit a single time and sample the final state, thereby producing the histogram of bit strings and counts. In the presence of non-trivial control flow with conditional statements like above, we cannot do that, and instead must invoke the circuit numShots
times, collecting measured bit strings each time. The question becomes, in a purely runtime model, how does one indicate or know that a CUDA Quantum kernel has conditional statements on measurement results, and thus switch to this second model of sampling (invoking the kernel numShots
times)? In the MLIR compilation mode, this is straightforward because we have the MLIR representation of the kernel at runtime and can look and see if it has cc.if()
operations on values coming from a quake.mz()
operation.
Potential solutions
Tracing the kernel
The first solution here is to borrow from the tracer
PR (#92) with the addition of a defined type for measurement results. Imagine a measure_result
type with the following structure
class measure_result {
private:
bool result = false;
public:
measure_result(const bool &b) : result(b) {};
operator bool();
};
One could implement the implicit bool
conversion operator on a type like this to trip some sort of flag indicating that the current kernel execution has conditional statements on measurement results (if (b0)
invokes measure_result::operator bool()
). With this in place, cudaq::sample(...)
could be updated to first trace the function (no execution), and pick up any flag that was tripped by an implicit operator bool conversion on a measurement result.
This approach is nice because it requires no change to the language specification or the structure of kernel expressions / cudaq::sample()
. I have a prototype for this here.
Kernel function indicator
Another approach could rely on some sort of helper function + user input on when a kernel has a mid-circuit measurement + conditional statement. Something like
struct kernel {
void operator()() __qpu__ {
cudaq::qreg<3> q;
// For simulation / library mode, programmers
// have to indicate this is a dynamic circuit,
// and provide the names of any classical registers
// we'll create
cudaq::is_dynamic_kernel("b0", "b1");
// Initial state preparation
x(q[0]);
// Create Bell pair
h(q[1]);
x<cudaq::ctrl>(q[1], q[2]);
x<cudaq::ctrl>(q[0], q[1]);
h(q[0]);
auto b0 = mz(q[0]);
auto b1 = mz(q[1]);
if (b1)
x(q[2]);
if (b0)
z(q[2]);
mz(q[2]);
}
};
... sampling invoked via
auto counts = cudaq::sample({nShots, /* is dynamic circuit */ true}, kernel{});
This is not as preferable to me, in that we have a different sample
signature for physical vs simulated backends. I'd like those to stay the same. Moreover, it makes kernels targeting physical vs simulated backends different, with the addition of some kind of indicator function.