Skip to content

Support code-declared inlined feedback in memory circuits#665

Draft
kvmto wants to merge 4 commits into
NVIDIA:mainfrom
kvmto:inlined_feedback
Draft

Support code-declared inlined feedback in memory circuits#665
kvmto wants to merge 4 commits into
NVIDIA:mainfrom
kvmto:inlined_feedback

Conversation

@kvmto

@kvmto kvmto commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

memory_circuit hardcodes the record→detector combination rule: every detector is "same record slot, two consecutive rounds" plus fixed boundary closures. This assumes each returned ancilla measurement is already a clean stabilizer value. Measurement schemes whose readout carries record-conditioned byproducts — e.g. superdense (paired-ancilla) extraction, where one ancilla's outcome heralds a Pauli byproduct on part of the plaquette — cannot express their true detector parities. Today such codes must apply the correction as a physical gate (an extra noise location — on hardware this correction is a classical frame update, not a gate) or abandon the framework path entirely.

Stim solves this with record-controlled Paulis plus with_inlined_feedback(); the CUDA-Q kernel language has no equivalent, and dem_from_kernel rejects measurement-dependent branching. This PR provides the cudaqx-level resolution.

Design

Make the record→detector combination rule a declarative property of the code, with the identity default preserving today's behavior exactly:

extract records ──► inlined feedback (declared by code; identity default) ──► detectors ──► decoder

The code declares data, not behavior (device kernels cannot call host callbacks mid-trace; the data rides the same rail as the parity matrices):

  • virtual cudaqx::tensor<uint8_t> code::get_inlined_feedback() const — shape [numCols × numCols], numCols = num_ancilla_qubits, [Z][X] per-round record order. Entry (j, k) = 1: the cross-round detector for record j additionally XORs record k of the earlier round; the final boundary detector for record j additionally XORs record k of the last round. Default: empty (identity).
  • virtual cudaqx::tensor<uint8_t> code::get_observable_inlined_feedback() const — shape [num_observables × numCols]; entry (m, k) = 1: observable m additionally XORs record k of every round. Default: empty.

experiments.cpp validates and threads the flattened data into every memory_circuit invocation; inside the kernel, empty feedback keeps the existing code paths verbatim (the legacy cudaq::detectors(prev, curr) line and observable emission are untouched under the default), while non-empty feedback emits per-record cudaq::detector parities, extended boundary detectors, and per-round observable record collection (one logical_observable call per observable, order unchanged). Python plugins simply define the two methods returning numpy arrays; the nanobind bridge forwards them.

Wrong declarations fail loudly: stim's determinism analysis inside dem_from_kernel validates every declared parity on each call.

Tests

  • Identity guarantee: all existing C++ suites (test_qec 46, test_qec_stim 7, test_decoders 43, test_dem_sampling 36) and Python test_code.py pass unchanged — with empty feedback the emitted circuit structure is bit-identical to main.
  • Behavioral proof (C++): a 2-qubit toy code with a paired-ancilla superdense round carrying an explicit uncorrected byproduct. With declared feedback ([[0,1],[0,0]] / [[0,1]] — derivation documented in-code; verified unique by exhaustive search): noiseless sample_memory_circuit yields all-zero syndromes and deterministic logical readout. Negative control: the identical circuit without the declaration throws stim's "non-deterministic detectors" — proving the feature does the work.
  • Python bridge: the same toy as a @qec.code plugin (positive + negative), exercising the trampoline and handle paths.

Notes for reviewers

  • The Python toy pins the stim target: while testing we found that the qpp target does not preserve cross-round mid-circuit measurement correlations when sampling Python kernels (reproduced with a feedback-free minimal circuit: two consecutive XX ancilla measurements agree in only ~50% of shots on qpp-cpu vs 100% on stim). This is a pre-existing cuda-quantum defect, orthogonal to this PR; we plan to file it upstream with the reproduction.
  • Motivating case: a triangular color code plugin (Add triangular color code plugin with superdense memory circuit #658) registering a superdense stabilizer_round — with this feature its byproduct correction moves from a physical CX into the detector definitions, making the framework-path DEM noise-faithful to the classical-feedforward convention.
  • Natural follow-ups (out of scope here): a batch post-processing helper for raw-record consumers and the realtime/streaming variant of the same declaration; a code-owned detector-emitter kernel for fully custom layouts; per-detector coordinate metadata (color annotations for color-code decoders).

Update: rule extraction (second + third commits)

The initial commit computed the record-composition arithmetic inline in the kernel. Two follow-up commits restructure this into the intended data → rule → emission split, with no user-visible change (all suites bit-identical):

  • Rule (device) — new internal header lib/device/inlined_feedback.h: named, documented __qpu__ record builders (cross_round_detector_records, boundary_detector_records, observable_feedback_offsets, collect_observable_feedback_round, observable_support_records). The memory_circuit kernel is now a thin emitter: state prep, rounds, and cudaq::detector/cudaq::logical_observable calls over the vectors these helpers return. Its signature is unchanged (the two flattened matrices — the declared data, mirroring the getters).
  • Rule (host)cudaq::qec::build_inlined_feedback_layout in detector_error_model: the same composition rule computed host-side as a CSR inlined_feedback_layout, for record-stream consumers that cannot run a kernel (the planned realtime D_sparse augmentation and batch record folds). Unit-tested standalone.
  • Lockstep guarantee — a conformance test (HostLayoutMatchesKernelM2D) derives the expected measurement→detector matrix from the host layout and asserts it equals the M2D that dem_from_kernel extracts from the kernel's emissions on the toy. The device fold and the host rule cannot drift apart silently.

Emission stays in-kernel deliberately: stim builds the probabilistic DEM and runs its determinism gate only over kernel-declared detectors.

Test tally after the refactor: test_qec 55 (49 + 5 layout unit tests + 1 conformance), test_qec_stim 7, test_decoders 43, test_dem_sampling 36, Python test_code.py 21.

Update: basis-split observable feedback (fourth commit)

Working the motivating case (#658) through this API surfaced a defect in the original design: observable inlined feedback is basis-dependent, while the single get_observable_inlined_feedback() was threaded identically into Z- and X-basis memory experiments. In the superdense color code, the heralding records must be folded into the Z-basis logical observable but not the X-basis one; with a single basis-blind matrix, every X-basis run fails stim's determinism gate (confirmed independently by GF(2) analysis of the record recursion and by noiseless sampling).

The fix mirrors the existing basis-selected rail for the observable data matrix (is_z_prep ? get_observables_z() : get_observables_x()): the getter is split into get_observable_inlined_feedback_z() / _x() (empty defaults), and each driver queries only the selected getter — pinned by a selection test in both directions. The detector-feedback matrix remains a single basis-neutral getter, which #658's validation confirms is sufficient (its detector matrix serves both bases unchanged).

Status of the motivating case: with this API, the color-code plugin (#658) declares its matrices and its framework-path DEM matches an independently constructed reference DEM bit-exactly in both bases (mechanism sets and probabilities), replacing the physical correction gate entirely.

Status

Draft for design feedback on the API shape and semantics.

🤖 Generated with Claude Code

Codes may declare record->detector feedback corrections as data via two
new virtuals (get_inlined_feedback / get_observable_inlined_feedback,
empty default = existing behavior); memory_circuit folds the declared
records into its detector and observable definitions, so measurement
schemes with readout byproducts get deterministic detectors through
sample_memory_circuit without a physical correction gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: kvmto <kmato@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

kvmto added 3 commits July 11, 2026 01:17
…d_feedback

The record->detector composition rule now lives in detector_error_model as a
CSR layout builder; the memory_circuit kernel is a thin emitter walking the
layout. Behavior is bit-identical; all suites pass unchanged.

Signed-off-by: kvmto <kmato@nvidia.com>
…amed device helpers

The kernel keeps its flat-matrix signature; all record-composition logic
lives in reusable __qpu__ functions in device/inlined_feedback.h. The host
CSR layout builder (build_inlined_feedback_layout) is retained for
record-stream consumers and pinned to the kernel by an M2D conformance test

Signed-off-by: kvmto <kmato@nvidia.com>
Signed-off-by: kvmto <kmato@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant