CUDA-Q Authoring Reference
Use this reference for /cudaq-guide author and for debugging Python
@cudaq.kernel code. It targets the CUDA-Q 0.14 and 0.15 Python APIs;
re-check version-sensitive behavior against the installed cudaq.__version__.
If the installed version differs from the latest documentation, review the
relevant documentation or source changes while debugging.
For porting Qiskit code to CUDA-Q, use the cudaq-importing skill. That skill
contains Qiskit gate mappings, Qiskit-vs-CUDA-Q bit ordering, precision
comparison guidance, framework-decoupling patterns, and port validation.
Defaults and Disciplines
Prefer decorator mode
Decorator mode (@cudaq.kernel) is the default. Do not use
cudaq.make_kernel() builder mode without explicit user permission.
Builder mode has uneven feature support across CUDA-Q 0.14/0.15-era Python
APIs: no mz(list), no qview slicing, no x.ctrl, no cudaq.adjoint on
builder kernels, apply_call on subkernels can be fragile, and
QuakeValue.__getitem__ rejects NumPy ints. Count-key ordering also differs
between modes. If a case appears to need builder mode, describe the advantage
and tradeoff and wait for confirmation.
Avoid unnecessary restrictions
Do not introduce restrictions unless the user directs. Common mistakes:
- Hardcoded register sizes such as
cudaq.qvector(10)whencudaq.qvector(num_qubits)with anintparameter works. - Fixed-arity gate dispatchers (
if nc == 0 / 1 / 2) that silently drop higher-arity multi-controlled gates. Use runtime-arity list comprehensions. NotImplementedErrorfor flags that are no-ops in CUDA-Q, such as aparameterized=Trueflag. A CUDA-Q kernel is already parameterized over runtime float arguments.- Per-shape kernel factories when a simpler int-parameter kernel works. Use a factory only when a fixed-length return-list literal is genuinely tied to a parameter.
If a cap is genuinely required by a CUDA-Q language constraint, raise
NotImplementedError with a clear message describing what is needed to lift it.
Never silently drop a gate or mis-compute.
Validate small first
Run at the smallest valid configuration before scaling. Compare raw count keys, not just aggregate fidelity, because bit-ordering and IQFT-direction bugs can produce plausible-looking partial fidelities.
Execution API Selection
Pick the API before writing the kernel. The API determines the kernel return signature and output conventions.
| Need | API | Kernel signature | Notes |
|---|---|---|---|
| Aggregate counts only | cudaq.sample(K, *args, shots_count=N) |
ends with mz(qubits), no return type |
Default and fast |
| Per-shot bits captured in Python | cudaq.run(K, *args, shots_count=N) |
-> List[bool] or -> int |
Needed for mid-circuit measurement, post-selection, or feed-forward; can be orders of magnitude slower |
| Repeated sequential measurements | cudaq.sample(..., explicit_measurements=True) |
measurement-order bitstring | Bit order is measurement order, not allocation order |
| Statevector amplitudes | cudaq.get_state(K, *args) |
no mz |
Simulator-only exact distributions and fidelity baselines |
| Pauli expectation value | cudaq.observe(K, hamiltonian, *args) |
no mz |
Use for spin-operator expectation values |
Rule of thumb: when the kernel does not need per-shot data in Python, end with
mz(qubits) and omit the return type so execution uses cudaq.sample.
cudaq.SampleResult is not a full dict. It supports .items() and indexed key
access, but not every dict method reliably. Convert when downstream code expects
a dict:
counts = {k: v for k, v in result.items()}CUDA-Q Kernel Language
Supported inside @cudaq.kernel
- Qubit allocation:
cudaq.qubit(),cudaq.qvector(n), and parameter typecudaq.qview. - Qview slicing:
qubits[a:b]with runtimeaandb. forloops withrange(start, stop, step)for positive or negative steps.if/elif/elsewith runtime conditions.- Direct sub-kernel calls:
my_subkernel(args). cudaq.control(K, ctrl_or_list, *args)to propagate control through every gate inK.cudaq.adjoint(K, *args)at top level, with caveats below.- Multi-control gate forms:
x.ctrl(c, t),x.ctrl([c1, c2], t),x.ctrl(c1, c2, t), and analogous forms fory,z,h,rx,ry,rz,r1, andswap. mz(qubit),mx(qubit), andmy(qubit)captured into variables for mid-circuit conditions and typed returns. CUDA-Q 0.14 treats captures as bool-like in common Python workflows; CUDA-Q 0.15 documents them as measurement handles that are discriminated when used in boolean conditions or returned through a typedlist[bool]/intkernel result.mz(qview)without return capture for sampling throughcudaq.sample.reset(qubit).- Float arithmetic with mixed int/float promotion.
- Typed parameters:
int,float,bool,cudaq.qubit,cudaq.qview,List[int],List[float], andList[bool]. - List comprehensions over runtime-bound lengths, such as
[qubits[idx[k]] for k in range(nc)]. This enables runtime-arity multi-control dispatch.
Unsupported inside @cudaq.kernel
These commonly produce compiler errors:
- Explicit casts
int(x),float(x),bool(x); prefer implicit promotion. reversed(range(n)); userange(n - 1, -1, -1).- Empty list literal
[]. - List multiplication such as
[False] * n. - List
.append()mutation. - Annotated assignment without initializer.
List[List[...]]parameter or return type; flatten to parallel arrays.- f-strings.
- Indexing with
np.int64; cast to Pythonintbefore packing parameters or indexing. np.random.*and other runtime-dynamic library calls. Generate randomness in Python outside the kernel and pass values as arguments.
Silent-Failure and Confusing-Error Pitfalls
cudaq.control does not compose with inner cudaq.adjoint
If kernel K contains cudaq.adjoint(M, ...), wrapping K in
cudaq.control(K, ...) can fail with:
RuntimeError: Could not successfully apply argument synth.Fix: hand-roll M's inverse in a separate kernel by walking gates in reverse
and negating rotation angles. Self-inverse gates such as X, Y, Z, H, CX, and
CSWAP keep the same angle and operands. RY, RX, RZ, R1, RZZ, and similar
rotations negate the angle.
cudaq.adjoint can fail on qview slices or nested loops
Errors include:
operand #1 does not dominate this use
'quake.extract_ref' op invalid constant index valueWorkaround: manually unroll the inverse or write a dedicated inverse kernel.
cudaq.run is much slower than cudaq.sample
At high shot counts, cudaq.run returning List[bool] can be roughly 1000x
slower than cudaq.sample. Only declare a return type when per-shot data is
genuinely needed.
Parameter-bounded loops can trigger JIT lowering errors
If for i in range(N) uses a runtime N to index a fixed allocation, the JIT
may fail even when classical logic guarantees N is in range. Clamp explicitly:
safe_n = bound
if safe_n > num_qubits:
safe_n = num_qubits
for i in range(safe_n):
h(qubits[i])NumPy ints are invalid kernel-side indices
qubits[np.int64(0)] raises an invalid-index error. Cast values to Python
int before packing List[int] kernel parameters.
Count-key conventions differ by mode
In decorator mode, mz(qview) puts qview[0] leftmost in the count key. In
builder mode, count keys are ordered by qubit allocation index. Do not mix modes
within a project without carefully remapping output keys.
reset(q) on noiseless statevector simulation
reset(q) is deterministic projection to |0>, not stochastic collapse.
This is usually fine, but matters for algorithms depending on stochastic
collapse plus post-selection semantics.
Recurring Patterns
Parallel-array gate encoding
When a kernel needs a sequence of multi-controlled gates whose control count, control indices, control states, target, and angle all depend on runtime data, encode the gate sequence as parallel arrays.
Use flat arrays with offsets. For gate i, controls live in
ctrl_indices[ctrl_offsets[i]:ctrl_offsets[i + 1]], with matching
ctrl_states.
@cudaq.kernel
def apply_A(state: cudaq.qview,
kinds: List[int], targets: List[int], thetas: List[float],
ctrl_indices: List[int], ctrl_states: List[int],
ctrl_offsets: List[int]):
L = len(kinds)
for i in range(L):
kind = kinds[i]
target = state[targets[i]]
theta = thetas[i]
c_start = ctrl_offsets[i]
c_end = ctrl_offsets[i + 1]
nc = c_end - c_start
for k in range(c_start, c_end):
if ctrl_states[k] == 0:
x(state[ctrl_indices[k]])
if nc == 0:
if kind == 0:
h(target)
elif kind == 1:
x(target)
elif kind == 2:
ry(theta, target)
else:
ctrls = [state[ctrl_indices[c_start + j]] for j in range(nc)]
if kind == 1:
x.ctrl(ctrls, target)
elif kind == 2:
ry.ctrl(theta, ctrls, target)
for k in range(c_start, c_end):
if ctrl_states[k] == 0:
x(state[ctrl_indices[k]])The key construct is the runtime-bound list comprehension:
ctrls = [state[ctrl_indices[c_start + j]] for j in range(nc)]This avoids fixed-arity dispatchers and hardcoded maximum control counts. X-wrap
plus gate plus X-unwrap is safe under outer cudaq.control.
For an outer loop applying different subcircuits per iteration, concatenate all per-iteration gate records in Python and keep offsets so the kernel can walk the correct slice for each iteration.
Contiguous-swap trick for sparse controls
When a specific intrinsic requires controls as a contiguous qview slice,
precompute swaps in Python and gather sparse controls into a prefix:
def _gather_swaps(op_controls, num_state_qubits):
logical_at_pos = list(range(num_state_qubits))
pos_of_logical = list(range(num_state_qubits))
swaps = []
for target_pos, control in enumerate(op_controls):
source_pos = pos_of_logical[control]
if source_pos == target_pos:
continue
swaps.append((target_pos, source_pos))
a, b = logical_at_pos[target_pos], logical_at_pos[source_pos]
logical_at_pos[target_pos], logical_at_pos[source_pos] = b, a
pos_of_logical[a] = source_pos
pos_of_logical[b] = target_pos
return swaps
@cudaq.kernel
def apply_op(state: cudaq.qview, swap_a: List[int], swap_b: List[int],
num_swaps: int, num_ctrls: int, theta: float):
for i in range(num_swaps):
swap(state[swap_a[i]], state[swap_b[i]])
ry.ctrl(theta, state[0:num_ctrls], state[num_ctrls])
for i_rev in range(num_swaps):
i = num_swaps - 1 - i_rev
swap(state[swap_a[i]], state[swap_b[i]])Prefer the flat-list control form unless a specific intrinsic forces contiguous controls.
Statevector-based expected distribution
Use cudaq.get_state on an unmeasured kernel when an exact distribution is
needed:
@cudaq.kernel
def my_circuit_unmeasured(args...):
# Gates only, no mz.
pass
state = cudaq.get_state(my_circuit_unmeasured, *args)
counts = {}
for i in range(2 ** N):
amp = complex(state[i])
p = amp.real**2 + amp.imag**2
if p > 1e-12:
counts[format(i, f"0{N}b")] = p
counts = {k: round(v * num_shots) for k, v in counts.items()}Pair with a measured kernel containing the same gate body plus trailing
mz(qubits). Since both outputs are CUDA-Q conventions, keys line up.
Variational loop
def expectation(thetas):
counts = cudaq.sample(my_kernel, *args, thetas, shots_count=num_shots)
return objective_from_counts(counts)
res = scipy.optimize.minimize(expectation, init_thetas, method="COBYLA",
options={"maxiter": max_iter})cudaq.sample is synchronous. If a closure captures a loop variable, bind it
with a default argument (def expectation(thetas, _idx=i)).
CUDA-Q has no symbolic-vs-bound distinction for kernel float arguments, so
source-framework parameterized=True flags are often no-ops.
Mid-circuit measurement and post-selection
Use cudaq.run when per-shot measurement values must be captured:
@cudaq.kernel
def my_kernel(args...) -> List[bool]:
a = mz(ancilla)
reset(ancilla)
return [a, mz(qa[N - 1]), mz(qa[N - 2]), mz(qa[0])]
shots = cudaq.run(my_kernel, *args, shots_count=N)
post_selected_counts = {}
for shot in shots:
if not shot[0]:
continue
key = "".join("1" if b else "0" for b in shot[1:])
post_selected_counts[key] = post_selected_counts.get(key, 0) + 1An encoded integer return can be cleaner for fixed-width feed-forward circuits:
@cudaq.kernel
def my_kernel(args...) -> int:
res = 0
if mz(ancilla):
res = res + (1 << N)
for q in range(N):
if mz(qa[q]):
res = res + (1 << q)
return resReference examples in the CUDA-Q source:
docs/sphinx/examples/python/measuring_kernels.py and
sample_to_run_migration.py.
Variable-shape return lists
@cudaq.kernel requires return-list literals to have fixed length at
decoration time.
Preferred option: use mz(qview) plus cudaq.sample when you do not need
per-shot data in Python. This avoids fixed-length return lists and is much
faster.
Factory option: generate one kernel per return shape with exec plus
linecache.cache only when cudaq.run and a fixed-length List[bool] return
are genuinely required.
import linecache
_KERNEL_CACHE = {}
def _ensure_kernel(N):
if N in _KERNEL_CACHE:
return _KERNEL_CACHE[N]
return_list = ", ".join([f"mz(q[{i}])" for i in range(N - 1, -1, -1)])
src = f"""
@cudaq.kernel
def my_kernel_N{N}(...) -> List[bool]:
q = cudaq.qvector({N})
return [{return_list}]
"""
fname = f"<dynamic:my_kernel_N{N}>"
linecache.cache[fname] = (
len(src), None, src.splitlines(keepends=True), fname)
code = compile(src, fname, "exec")
namespace = {"cudaq": cudaq, "List": List}
exec(code, namespace)
K = namespace[f"my_kernel_N{N}"]
_KERNEL_CACHE[N] = K
return KRegistering the source in linecache.cache lets inspect.getsource find the
dynamic kernel source during CUDA-Q decoration.
Hand-rolled inverse
Use hand-rolled inverses when a kernel may be controlled:
@cudaq.kernel
def A_op(state: cudaq.qview, S: int, theta: float):
ry(theta, state[0])
for i in range(S):
x.ctrl(state[0], state[i + 1])
@cudaq.kernel
def A_op_inv(state: cudaq.qview, S: int, theta: float):
for i in range(S):
x.ctrl(state[0], state[i + 1])
ry(-theta, state[0])
@cudaq.kernel
def Q_op(state: cudaq.qview, S: int, theta: float):
A_op_inv(state, S, theta)
@cudaq.kernel
def main(...):
cudaq.control(Q_op, count_ctrl, state, S, theta)Inverse-emission rules:
- Walk gates in reverse order.
- X, Y, Z, H, CX, CY, CZ, SWAP, and CSWAP are self-inverse.
- RX, RY, RZ, R1, RZZ, RXX, RYY, and controlled variants negate the angle.
- S becomes
S.adj; T becomesT.adj.
Resource Metrics
cudaq.estimate_resources(K, *args) returns a resources object. Across
CUDA-Q 0.14/0.15, count() reports total gate count and
count_controls(gate_name, num_controls) reports gate invocations with exactly
that many controls. Newer builds may also expose arity-indexed fields such as
gate_count_by_arity, gate_count_for_arity, depth_for_arity,
multi_qubit_gate_count, and multi_qubit_depth; prefer those when present
and fall back to count_controls for older installations.
resources = cudaq.estimate_resources(K, *args)
total_gates = resources.count()
controlled_gates = ["x", "y", "z", "r1", "rx", "ry", "rz"]
two_q_weighted = 0
for gate in controlled_gates:
for arity in range(1, num_qubits):
two_q_weighted += arity * resources.count_controls(gate, arity)Interpretation caveats:
- A multi-controlled gate may count as one high-level operation or as many effective two-qubit controls after decomposition. Define the metric before comparing tools.
- Resource counters can have IR-shape blind spots. Verify on small known examples before treating a count of zero as real.
CUDAQ_TIMING_TAGS=5enables backend-level instrumentation such asGate CountandControl Count. These totals are aggregate across all kernel executions in a process.- Native timing/control totals may be absent for some kernel shapes. Treat absence as an instrumentation quirk, not zero work.
Correctness and output fidelity are the primary validation targets; resource counts are secondary diagnostics.
Workflow and Debugging
General workflow:
- Read the algorithm and identify register layout, qubit order, gate sequence, control/inversion structure, and expected count-key format.
- Pick the execution API before writing the kernel.
- Move complex Python out of kernels. Precompute angles, operation records, control lists, swap schedules, and parameter bindings in normal Python.
- Probe kernel-language features with a small file in
/tmp/probe.py; CUDA-Q source introspection meanspython3 -cand stdin scripts can fail at decoration time. - Run the smallest valid case first and compare raw keys or analytic outputs.
- If a lowering error depends on input size or value, split the path into a dedicated kernel.
Debugging checklist:
- Counts correct but keys reversed: fix allocation, measurement, or formatting at the boundary.
- Fidelity around 0.4-0.6 with mass on shifted neighbors: suspect IQFT
direction, QPE qubit reversal, or a wrong controlled-gate flavor such as
rz.ctrlvsr1.ctrl. - Fidelity exactly 0.0: often endianness. Verify with a 2- or 3-qubit deterministic probe.
- Only some widths or values fail: inspect integer arithmetic inside the kernel and try precomputing the offending values in Python.
RuntimeError: Could not successfully apply argument synth: a controlled kernel probably containscudaq.adjoint; hand-roll the inverse.'quake.extract_ref' op invalid constant index value: a loop bound could exceed allocated qvector size; clamp explicitly.AttributeErroronresult.keys(): convertSampleResultto a dict.- Mid-circuit output looks wrong: compare
sample(..., explicit_measurements=True)with acudaq.runkernel returning encoded per-shot values. - Sparse-control dispatch needs dynamic lists: use the flat-list pattern or the contiguous-swap schedule instead of builder mode.
Validating a New Algorithm
For new algorithms with no source-framework reference:
- Pick a small case with a known analytic answer.
- Verify the output bitstring matches the analytic expectation at 100% probability on a noiseless simulator.
- Scale up only after the small case is correct.
- Use
cudaq.get_stateto inspect intermediate distributions when feasible.
When task accuracy depends on a specific CUDA-Q version, check
cudaq.__version__, the latest official CUDA-Q documentation, and any relevant
documentation or source changes between the installed version and latest.
External References
- CUDA-Q latest documentation: https://nvidia.github.io/cuda-quantum/latest/.
- CUDA-Q examples for
cudaq.run,List[bool], and mid-circuit measurement:docs/sphinx/examples/python/measuring_kernels.pyandsample_to_run_migration.pyin the CUDA-Q source tree. - CUDA-Q Academic examples: https://github.com/NVIDIA/cuda-q-academic.
- CUDA-Q source-tree tests:
python/tests/kernel/test_kernel_features.pyandpython/tests/kernel/test_measure_handle.pywhen present.