Skip to content

Support code-declared inlined feedback in memory circuits#665

Open
kvmto wants to merge 5 commits into
NVIDIA:mainfrom
kvmto:inlined_feedback
Open

Support code-declared inlined feedback in memory circuits#665
kvmto wants to merge 5 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

@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 and others added 4 commits July 13, 2026 16:50
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>
…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>
@kvmto kvmto force-pushed the inlined_feedback branch from ee29796 to 97cbfc0 Compare July 13, 2026 14:55
  Validate feedback matrices before conversion, clarify raw sampling
  semantics, reject unsupported qpp-cpu Python sampling, and expand
  detector/observable conformance coverage.

Signed-off-by: kvmto <kmato@nvidia.com>
@kvmto kvmto force-pushed the inlined_feedback branch from 97cbfc0 to 74e25a5 Compare July 13, 2026 15:36

@eliotheinrich eliotheinrich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, Kevin! I left a few comments; I think the only blocking issue is the Python API for build_inlined_feedback_layout, if I understand correctly.

build_inlined_feedback_layout(const cudaqx::tensor<uint8_t> &feedback,
const cudaqx::tensor<uint8_t> &obs_feedback,
std::size_t num_syndromes_per_round,
std::size_t num_observables);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function part of the public API? I'm a little confused what it's for, as it seems like the feedback tensors are usually accessed via something like flatten_feedback_tensor(code.get_inlined_feedback(), ...). If it is supposed to be exposed, it should probably have a Python API.

Comment on lines +32 to +45
inline __qpu__ std::vector<cudaq::measure_result> cross_round_detector_records(
const std::vector<cudaq::measure_result> &prev,
const std::vector<cudaq::measure_result> &curr, std::size_t j,
const std::vector<std::size_t> &feedback_flat, std::size_t num_cols) {
std::size_t weight = row_support_weight(feedback_flat, j, num_cols);
std::vector<cudaq::measure_result> det(2 + weight);
det[0] = prev[j];
det[1] = curr[j];
std::size_t idx = 2;
for (std::size_t k = 0; k < num_cols; ++k)
if (feedback_flat[j * num_cols + k] != 0)
det[idx++] = prev[k];
return det;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called one per numCol, and does two sweeps over numCols (once in row_support_weight and once on L41-L43). Might it make sense to operate on a sparse feedback_flat?

cudaq::dem_from_kernel(
cudaq::qec::memory_circuit, &noise, /*options=*/{}, m2d, m2o, stabRound,
prep, numData, numAncx, numAncz, numRounds, xVec, zVec, obs_flat, num_obs,
/*measure_in_x_basis=*/false, feedback_flat, obs_feedback_flat);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think end-to-end feedback for X-type circuits is exercised anywhere; could be cheap to add here

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.

2 participants