All skills
nvidia avatar

/cudaq-guide

@46293cb
by NVIDIA Corporationnvidia/skills3.5k stars
424

Use for CUDA-Q setup, simulation targets, QPU access, and @cudaq.kernel authoring guidance.

Use this Skill: https://skilld.dev/gh/nvidia/skills/cudaq-guide

This session only. Nothing lands on disk.

referencesauthoring.md

≈5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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) when cudaq.qvector(num_qubits) with an int parameter works.
  • Fixed-arity gate dispatchers (if nc == 0 / 1 / 2) that silently drop higher-arity multi-controlled gates. Use runtime-arity list comprehensions.
  • NotImplementedError for flags that are no-ops in CUDA-Q, such as a parameterized=True flag. 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 type cudaq.qview.
  • Qview slicing: qubits[a:b] with runtime a and b.
  • for loops with range(start, stop, step) for positive or negative steps.
  • if / elif / else with runtime conditions.
  • Direct sub-kernel calls: my_subkernel(args).
  • cudaq.control(K, ctrl_or_list, *args) to propagate control through every gate in K.
  • 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 for y, z, h, rx, ry, rz, r1, and swap.
  • mz(qubit), mx(qubit), and my(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 typed list[bool] / int kernel result.
  • mz(qview) without return capture for sampling through cudaq.sample.
  • reset(qubit).
  • Float arithmetic with mixed int/float promotion.
  • Typed parameters: int, float, bool, cudaq.qubit, cudaq.qview, List[int], List[float], and List[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)); use range(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 Python int before 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 value

Workaround: 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) + 1

An 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 res

Reference 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 K

Registering 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 becomes T.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=5 enables backend-level instrumentation such as Gate Count and Control 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:

  1. Read the algorithm and identify register layout, qubit order, gate sequence, control/inversion structure, and expected count-key format.
  2. Pick the execution API before writing the kernel.
  3. Move complex Python out of kernels. Precompute angles, operation records, control lists, swap schedules, and parameter bindings in normal Python.
  4. Probe kernel-language features with a small file in /tmp/probe.py; CUDA-Q source introspection means python3 -c and stdin scripts can fail at decoration time.
  5. Run the smallest valid case first and compare raw keys or analytic outputs.
  6. 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.ctrl vs r1.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 contains cudaq.adjoint; hand-roll the inverse.
  • 'quake.extract_ref' op invalid constant index value: a loop bound could exceed allocated qvector size; clamp explicitly.
  • AttributeError on result.keys(): convert SampleResult to a dict.
  • Mid-circuit output looks wrong: compare sample(..., explicit_measurements=True) with a cudaq.run kernel 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:

  1. Pick a small case with a known analytic answer.
  2. Verify the output bitstring matches the analytic expectation at 100% probability on a noiseless simulator.
  3. Scale up only after the small case is correct.
  4. Use cudaq.get_state to 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

  1. CUDA-Q latest documentation: https://nvidia.github.io/cuda-quantum/latest/.
  2. CUDA-Q examples for cudaq.run, List[bool], and mid-circuit measurement: docs/sphinx/examples/python/measuring_kernels.py and sample_to_run_migration.py in the CUDA-Q source tree.
  3. CUDA-Q Academic examples: https://github.com/NVIDIA/cuda-q-academic.
  4. CUDA-Q source-tree tests: python/tests/kernel/test_kernel_features.py and python/tests/kernel/test_measure_handle.py when present.

Source: SKILL.md on GitHub

No alerts19d3 checks · Risk SAFE
  • Gen Agent Trust Hub19d

    The cudaq-guide skill is an onboarding tool for the NVIDIA CUDA-Q platform, providing instructions for installation, quantum programming, and hardware connectivity. It follows security best practices by advising environment-variable-based secret management and links to official documentation.

  • Socket19d

    No alerts

  • Snyk19d

    Risk: LOW · No issues

Signed by skilld at 46293cb. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 weeks ago
title
CUDA-Q Guide
version
1.1.2
author
CUDA-Q Team <cuda-quantum@nvidia.com>
Other metadata
tags
[
  "cuda-quantum",
  "quantum-computing",
  "onboarding",
  "getting-started",
  "authoring",
  "kernels",
  "nvidia"
]
tools
[
  "Read",
  "Glob",
  "Grep"
]
compatibility
Python 3.10+, C++ 20
metadata
{
  "author": "CUDA-Q Team <cuda-quantum@nvidia.com>",
  "tags": [
    "cuda-quantum",
    "quantum-computing",
    "onboarding",
    "getting-started",
    "nvidia"
  ],
  "languages": [
    "python",
    "c++"
  ],
  "domain": "quantum"
}

README badge

README badge for nvidia/skills/cudaq-guide