Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions libs/qec/include/cudaq/qec/code.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,50 @@ class code : public cudaqx::extension_point<code, const heterogeneous_map &> {
/// @return Tensor representing Lz
cudaqx::tensor<uint8_t> get_observables_z() const;

/// @brief Get the inlined feedback matrix for detector construction
///
/// Shape is [numCols x numCols], where numCols =
/// get_num_ancilla_qubits() (one measurement record per ancilla per
/// round, in [Z][X] order). Entries must be exactly 0 or 1. Entry
/// (j, k) = 1 means the cross-round
/// detector comparing record j between consecutive rounds additionally
/// XORs record k of the earlier round, and the final boundary detector
/// for record j additionally XORs record k of the last round.
/// @return Tensor representing the inlined feedback matrix. An empty
/// tensor (the default) means no feedback is applied and detectors are
/// formed from same-record comparisons only.
virtual cudaqx::tensor<uint8_t> get_inlined_feedback() const;

/// @brief Get the inlined feedback matrix for logical observables measured
/// in the Z basis
///
/// Shape is [num_observables x numCols], where numCols =
/// get_num_ancilla_qubits() (one measurement record per ancilla per
/// round, in [Z][X] order). Entries must be exactly 0 or 1. Entry
/// (m, k) = 1 means logical observable m
/// additionally XORs record k of every round. This getter is consumed when
/// the memory experiment measures its observables in the Z basis (prep0 /
/// prep1 state preparations).
/// @return Tensor representing the Z-basis observable inlined feedback
/// matrix. An empty tensor (the default) means no observable feedback is
/// applied in the Z basis.
virtual cudaqx::tensor<uint8_t> get_observable_inlined_feedback_z() const;

/// @brief Get the inlined feedback matrix for logical observables measured
/// in the X basis
///
/// Shape is [num_observables x numCols], where numCols =
/// get_num_ancilla_qubits() (one measurement record per ancilla per
/// round, in [Z][X] order). Entries must be exactly 0 or 1. Entry
/// (m, k) = 1 means logical observable m
/// additionally XORs record k of every round. This getter is consumed when
/// the memory experiment measures its observables in the X basis (prepp /
/// prepm state preparations).
/// @return Tensor representing the X-basis observable inlined feedback
/// matrix. An empty tensor (the default) means no observable feedback is
/// applied in the X basis.
virtual cudaqx::tensor<uint8_t> get_observable_inlined_feedback_x() const;

/// @brief Get the stabilizer generators
/// @return Reference to stabilizers
const std::vector<cudaq::spin_op_term> &get_stabilizers() const {
Expand Down
2 changes: 1 addition & 1 deletion libs/qec/include/cudaq/qec/detector_error_model.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************-*- C++ -*-****
* Copyright (c) 2025 NVIDIA Corporation & Affiliates. *
* Copyright (c) 2025 - 2026 NVIDIA Corporation & Affiliates. *
* All rights reserved. *
* *
* This source code and the accompanying materials are made available under *
Expand Down
5 changes: 4 additions & 1 deletion libs/qec/include/cudaq/qec/experiments.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ dem_sampling(const cudaqx::tensor<uint8_t> &check_matrix, std::size_t numShots,
/// type matching `statePrep`'s basis, since only that type is deterministic
/// at the circuit's endpoints), then one detector block per each of the
/// `numRounds - 1` inter-round transitions, then `numFixed` more boundary
/// detectors. `dataResults` has shape `(numShots, numDataQubits)`.
/// detectors. `dataResults` has shape `(numShots, numDataQubits)` and contains
/// the raw final data-qubit measurements. In particular, observable inlined
/// feedback is represented in a generated detector error model, but is not
/// folded into `dataResults` as a corrected logical-observable value.
std::tuple<cudaqx::tensor<uint8_t>, cudaqx::tensor<uint8_t>>
sample_memory_circuit(const code &code, operation statePrep,
std::size_t numShots, std::size_t numRounds,
Expand Down
12 changes: 12 additions & 0 deletions libs/qec/lib/code.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ cudaqx::tensor<uint8_t> code::get_observables_z() const {
return to_parity_matrix(m_pauli_observables, stabilizer_type::Z);
}

cudaqx::tensor<uint8_t> code::get_inlined_feedback() const {
return cudaqx::tensor<uint8_t>();
}

cudaqx::tensor<uint8_t> code::get_observable_inlined_feedback_z() const {
return cudaqx::tensor<uint8_t>();
}

cudaqx::tensor<uint8_t> code::get_observable_inlined_feedback_x() const {
return cudaqx::tensor<uint8_t>();
}

std::unique_ptr<code> get_code(const std::string &name,
const std::vector<cudaq::spin_op_term> &stab,
const heterogeneous_map options) {
Expand Down
161 changes: 161 additions & 0 deletions libs/qec/lib/device/inlined_feedback.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/****************************************************************-*- C++ -*-****
* Copyright (c) 2024 - 2025 NVIDIA Corporation & Affiliates. *
* All rights reserved. *
* *
* This source code and the accompanying materials are made available under *
* the terms of the Apache License 2.0 which accompanies this distribution. *
******************************************************************************/

#pragma once

#include "cudaq.h"

namespace cudaq::qec {

/// @brief Number of nonzero entries in row @p row of a row-major 0/1 matrix
/// that has @p num_cols columns (i.e. the weight of one feedback/observable/
/// stabilizer row).
inline __qpu__ std::size_t
row_support_weight(const std::vector<std::size_t> &matrix_flat, std::size_t row,
std::size_t num_cols) {
std::size_t weight = 0;
for (std::size_t k = 0; k < num_cols; ++k)
if (matrix_flat[row * num_cols + k] != 0)
weight++;
return weight;
}

/// @brief Records for the cross-round detector of syndrome record @p j: the
/// earlier-vs-current comparison `{prev[j], curr[j]}` followed by the
/// earlier-round herald records selected by the CSR row for j.
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_indices,
const std::vector<std::size_t> &feedback_offsets) {
std::size_t weight = feedback_offsets[j + 1] - feedback_offsets[j];
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 i = feedback_offsets[j]; i < feedback_offsets[j + 1]; ++i)
det[idx++] = prev[feedback_indices[i]];
return det;
}

/// @brief Records for the boundary detector of syndrome record @p record_row:
/// `{last_syndrome[record_row]}`, then the data-qubit readouts
/// `data_results[q]` for each data qubit q (ascending) in the stabilizer
/// support `stabilizers[row_base + q]`, then the last-round herald records
/// selected by the CSR row for @p record_row. Empty feedback offsets mean the
/// herald part contributes nothing, so one helper serves both feedback and
/// legacy branches.
inline __qpu__ std::vector<cudaq::measure_result> boundary_detector_records(
const std::vector<cudaq::measure_result> &last_syndrome,
std::size_t record_row,
const std::vector<cudaq::measure_result> &data_results,
const std::vector<std::size_t> &stabilizers, std::size_t row_base,
std::size_t num_data, const std::vector<std::size_t> &feedback_indices,
const std::vector<std::size_t> &feedback_offsets) {
std::size_t support_weight = 0;
for (std::size_t q = 0; q < num_data; ++q)
if (stabilizers[row_base + q] != 0)
support_weight++;
std::size_t fb_weight =
feedback_offsets.size() > 0
? feedback_offsets[record_row + 1] - feedback_offsets[record_row]
: 0;
std::vector<cudaq::measure_result> support(1 + support_weight + fb_weight);
support[0] = last_syndrome[record_row];
std::size_t idx = 1;
for (std::size_t q = 0; q < num_data; ++q)
if (stabilizers[row_base + q] != 0)
support[idx++] = data_results[q];
if (feedback_offsets.size() > 0)
for (std::size_t i = feedback_offsets[record_row];
i < feedback_offsets[record_row + 1]; ++i)
support[idx++] = last_syndrome[feedback_indices[i]];
return support;
}

/// @brief Round-major slice offsets into the observable-feedback buffer. Entry
/// m is the start index of observable m's slice; each observable occupies
/// `num_rounds * (obs_feedback_offsets[m+1] - obs_feedback_offsets[m])`
/// records. The returned vector has `num_observables + 1` entries; the last
/// entry is the total buffer size. Returns an empty vector when the input
/// offsets are empty (no observable feedback declared).
inline __qpu__ std::vector<std::size_t> observable_feedback_record_offsets(
const std::vector<std::size_t> &obs_feedback_offsets,
std::size_t num_observables, std::size_t num_rounds) {
// Sized (not default) construction: the __qpu__ dialect forbids the default
// std::vector constructor. Empty CSR offsets yield a size-0 vector.
std::size_t num_offsets =
obs_feedback_offsets.size() > 0 ? num_observables + 1 : 0;
std::vector<std::size_t> offsets(num_offsets);
if (obs_feedback_offsets.size() == 0)
return offsets;
std::size_t total = 0;
for (std::size_t m = 0; m < num_observables; ++m) {
offsets[m] = total;
total +=
num_rounds * (obs_feedback_offsets[m + 1] - obs_feedback_offsets[m]);
}
offsets[num_observables] = total;
return offsets;
}

/// @brief Write the records that observable feedback collects during @p round
/// into the round-major @p obs_fb_records buffer. For each observable m, the
/// round-r block starts at `record_offsets[m] + round * row_weight(m)` and
/// holds the syndrome records selected by CSR row m. Call with round 0 for the
/// first round's syndrome and with the running round index for each subsequent
/// round.
inline __qpu__ void collect_observable_feedback_round(
std::vector<cudaq::measure_result> &obs_fb_records,
const std::vector<std::size_t> &record_offsets,
const std::vector<cudaq::measure_result> &syndrome, std::size_t round,
const std::vector<std::size_t> &obs_feedback_indices,
const std::vector<std::size_t> &obs_feedback_offsets,
std::size_t num_observables) {
for (std::size_t m = 0; m < num_observables; ++m) {
std::size_t weight = obs_feedback_offsets[m + 1] - obs_feedback_offsets[m];
std::size_t idx = record_offsets[m] + round * weight;
for (std::size_t i = obs_feedback_offsets[m];
i < obs_feedback_offsets[m + 1]; ++i)
obs_fb_records[idx++] = syndrome[obs_feedback_indices[i]];
}
}

/// @brief Support records for logical observable @p obs: the feedback records
/// collected in @p obs_fb_records (round-major, occupying
/// `[offsets[obs], offsets[obs + 1])`), followed by the data-qubit readouts
/// `data_results[q]` for each data qubit q (ascending) with
/// `obs_matrix_flat(obs, q) != 0`. When @p offsets is empty (no observable
/// feedback declared) the feedback prefix is empty and only the data support
/// is returned.
inline __qpu__ std::vector<cudaq::measure_result> observable_support_records(
std::size_t obs, const std::vector<std::size_t> &obs_matrix_flat,
std::size_t num_data,
const std::vector<cudaq::measure_result> &data_results,
const std::vector<cudaq::measure_result> &obs_fb_records,
const std::vector<std::size_t> &offsets) {
std::size_t support_weight =
row_support_weight(obs_matrix_flat, obs, num_data);
std::size_t fb_count = 0;
std::size_t base = 0;
if (offsets.size() > 0) {
base = offsets[obs];
fb_count = offsets[obs + 1] - offsets[obs];
}
std::vector<cudaq::measure_result> obs_support(fb_count + support_weight);
for (std::size_t i = 0; i < fb_count; ++i)
obs_support[i] = obs_fb_records[base + i];
std::size_t idx = fb_count;
for (std::size_t q = 0; q < num_data; ++q)
if (obs_matrix_flat[obs * num_data + q] != 0)
obs_support[idx++] = data_results[q];
return obs_support;
}

} // namespace cudaq::qec
108 changes: 60 additions & 48 deletions libs/qec/lib/device/memory_circuit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@
* the terms of the Apache License 2.0 which accompanies this distribution. *
******************************************************************************/
#include "memory_circuit.h"
#include <numeric>
#include "inlined_feedback.h"

namespace cudaq::qec {

__qpu__ void memory_circuit(const code::stabilizer_round &stabilizer_round,
const code::one_qubit_encoding &statePrep,
std::size_t num_data, std::size_t numAncx,
std::size_t numAncz, std::size_t num_rounds,
const std::vector<std::size_t> &x_stabilizers,
const std::vector<std::size_t> &z_stabilizers,
const std::vector<std::size_t> &obs_matrix_flat,
std::size_t num_observables,
bool measure_in_x_basis) {
__qpu__ void
memory_circuit(const code::stabilizer_round &stabilizer_round,
const code::one_qubit_encoding &statePrep, std::size_t num_data,
std::size_t numAncx, std::size_t numAncz, std::size_t num_rounds,
const std::vector<std::size_t> &x_stabilizers,
const std::vector<std::size_t> &z_stabilizers,
const std::vector<std::size_t> &obs_matrix_flat,
std::size_t num_observables, bool measure_in_x_basis,
const std::vector<std::size_t> &feedback_indices,
const std::vector<std::size_t> &feedback_offsets,
const std::vector<std::size_t> &obs_feedback_indices,
const std::vector<std::size_t> &obs_feedback_offsets) {
// Allocate the data and ancilla qubits
cudaq::qvector data(num_data), xstab_anc(numAncx), zstab_anc(numAncz);

Expand All @@ -39,10 +42,44 @@ __qpu__ void memory_circuit(const code::stabilizer_round &stabilizer_round,
cudaq::detector(final_syndrome[fixed_offset + i]);
}

std::size_t numCols = numAncx + numAncz;

// Observable inlined feedback accumulation. Nested std::vector is not
// supported in __qpu__ code, so instead of one record vector per observable
// we use a single flat buffer with per-observable round-major slices; see
// observable_feedback_record_offsets / collect_observable_feedback_round in
// inlined_feedback.h.
std::vector<std::size_t> obs_fb_record_offsets =
observable_feedback_record_offsets(obs_feedback_offsets, num_observables,
num_rounds);
std::size_t obs_fb_total = obs_feedback_offsets.size() > 0
? obs_fb_record_offsets[num_observables]
: 0;
std::vector<cudaq::measure_result> obs_fb_records(obs_fb_total);

// Collect the first-round feedback records for each observable.
if (obs_feedback_offsets.size() > 0)
collect_observable_feedback_round(obs_fb_records, obs_fb_record_offsets,
final_syndrome, 0, obs_feedback_indices,
obs_feedback_offsets, num_observables);

// Generate syndrome data
for (std::size_t round = 1; round < num_rounds; ++round) {
auto syndrome = stabilizer_round(logical, x_stabilizers, z_stabilizers);
cudaq::detectors(final_syndrome, syndrome);
if (obs_feedback_offsets.size() > 0)
collect_observable_feedback_round(obs_fb_records, obs_fb_record_offsets,
syndrome, round, obs_feedback_indices,
obs_feedback_offsets, num_observables);
if (feedback_offsets.size() == 0) {
cudaq::detectors(final_syndrome, syndrome);
} else {
// Cross-round detector for record j: earlier vs current round record,
// augmented with the earlier-round records declared in row j of the
// feedback matrix.
for (std::size_t j = 0; j < numCols; ++j)
cudaq::detector(cross_round_detector_records(
final_syndrome, syndrome, j, feedback_indices, feedback_offsets));
}
final_syndrome = syndrome;
}

Expand All @@ -51,48 +88,23 @@ __qpu__ void memory_circuit(const code::stabilizer_round &stabilizer_round,
}
auto data_results = mz(data);

// Emit one logical_observable per row of the observable matrix.
for (std::size_t obs = 0; obs < num_observables; ++obs) {
std::size_t support_weight = 0;
for (std::size_t q = 0; q < num_data; ++q) {
if (obs_matrix_flat[obs * num_data + q] != 0)
support_weight++;
}
std::vector<cudaq::measure_result> obs_support(support_weight);
std::size_t idx = 0;
for (std::size_t q = 0; q < num_data; ++q) {
if (obs_matrix_flat[obs * num_data + q] != 0)
obs_support[idx++] = data_results[q];
}
cudaq::logical_observable(obs_support);
}
// Emit one logical_observable per row of the observable matrix: the per-round
// feedback records followed by the data-qubit support.
for (std::size_t obs = 0; obs < num_observables; ++obs)
cudaq::logical_observable(
observable_support_records(obs, obs_matrix_flat, num_data, data_results,
obs_fb_records, obs_fb_record_offsets));

// For each stabilizer, form detectors from data qubit readout connected with
// final stabilizer round.
// final stabilizer round. With inlined feedback, the boundary detector for
// record (fixed_offset + x) is extended with the declared last-round records.
const std::vector<size_t> &stabilizers =
measure_in_x_basis ? x_stabilizers : z_stabilizers;

for (std::size_t x = 0; x < num_fixed_measurements; ++x) {
std::size_t row_base = x * num_data;

std::size_t support_weight = 0;
for (std::size_t q = 0; q < num_data; ++q) {
if (stabilizers[row_base + q] != 0) {
support_weight++;
}
}

std::vector<cudaq::measure_result> support(support_weight + 1);
support[0] = final_syndrome[fixed_offset + x];
std::size_t support_idx = 1;
for (std::size_t q = 0; q < num_data; ++q) {
if (stabilizers[row_base + q] != 0) {
support[support_idx++] = data_results[q];
}
}

cudaq::detector(support);
}
for (std::size_t x = 0; x < num_fixed_measurements; ++x)
cudaq::detector(boundary_detector_records(
final_syndrome, fixed_offset + x, data_results, stabilizers,
x * num_data, num_data, feedback_indices, feedback_offsets));
}

} // namespace cudaq::qec
Loading
Loading