diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 44f46cb40..5434f2e2f 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -94,6 +94,7 @@ jobs: uses: actions/checkout@v4 with: set-safe-directory: true + lfs: true # ======================================================================== # CUDA Quantum build diff --git a/docs/sphinx/_static/cudaq_override.css b/docs/sphinx/_static/cudaq_override.css index 4ab0ac179..a259c9003 100644 --- a/docs/sphinx/_static/cudaq_override.css +++ b/docs/sphinx/_static/cudaq_override.css @@ -25,3 +25,9 @@ max-width: 1240px !important; code.code span.pre, code.cpp span.pre, code.docutils span.pre{ color: darkgreen; } + +/* Render the general index in a single column instead of the theme's multi-column layout */ +table.genindextable td { + display: block; + width: 100% !important; +} diff --git a/docs/sphinx/api/qec/chromobius_api.rst b/docs/sphinx/api/qec/chromobius_api.rst index 3fa86ad7d..7351fa11d 100644 --- a/docs/sphinx/api/qec/chromobius_api.rst +++ b/docs/sphinx/api/qec/chromobius_api.rst @@ -1,7 +1,7 @@ .. class:: chromobius A decoder for color codes built on the open-source - `Chromobius `_ Mobius decoder. + `Chromobius `_ Möbius decoder. Unlike the matrix-based decoders, Chromobius is *detector-error-model native*: it is constructed directly from Stim detector-error-model (DEM) text and predicts logical observable flips directly. diff --git a/docs/sphinx/api/qec/cpp_api.rst b/docs/sphinx/api/qec/cpp_api.rst index 9759fbbe0..e62c4da00 100644 --- a/docs/sphinx/api/qec/cpp_api.rst +++ b/docs/sphinx/api/qec/cpp_api.rst @@ -212,8 +212,10 @@ Chromobius Decoder .. include:: chromobius_api.rst -Real-Time Decoding -================== +.. _cpp_realtime_decoding_api: + +Realtime Decoding +================= .. include:: cpp_realtime_decoding_api.rst diff --git a/docs/sphinx/api/qec/cpp_realtime_decoding_api.rst b/docs/sphinx/api/qec/cpp_realtime_decoding_api.rst index 709f9e433..b7c81a80d 100644 --- a/docs/sphinx/api/qec/cpp_realtime_decoding_api.rst +++ b/docs/sphinx/api/qec/cpp_realtime_decoding_api.rst @@ -1,7 +1,4 @@ -.. _cpp_realtime_decoding_api: - - -The Real-Time Decoding API enables low-latency error correction on quantum hardware by allowing CUDA-Q quantum kernels to interact with decoders during circuit execution. This API is designed for use cases where corrections must be calculated and applied within qubit coherence times. +The Realtime Decoding API enables low-latency error correction on quantum hardware by allowing CUDA-Q quantum kernels to interact with decoders during circuit execution. This API is designed for use cases where corrections must be calculated and applied within qubit coherence times. The real-time decoding system supports simulation environments for local testing and hardware integration (e.g., on `Quantinuum's Helios QPU @@ -48,7 +45,7 @@ The configuration API enables setting up decoders before circuit execution. Deco Helper Functions ---------------- -Real-time decoding requires converting matrices to sparse format for efficient decoder configuration. The following utility functions are essential: +Realtime decoding requires converting matrices to sparse format for efficient decoder configuration. The following utility functions are essential: - :cpp:func:`cudaq::qec::pcm_to_sparse_vec` for converting a dense PCM to a sparse PCM. - :cpp:func:`cudaq::qec::pcm_from_sparse_vec` for converting a sparse PCM to a dense PCM. diff --git a/docs/sphinx/api/qec/python_api.rst b/docs/sphinx/api/qec/python_api.rst index 58fb73e9b..243737c38 100644 --- a/docs/sphinx/api/qec/python_api.rst +++ b/docs/sphinx/api/qec/python_api.rst @@ -170,6 +170,23 @@ Decoder Interfaces .. autofunction:: cudaq_qec.get_decoder +.. note:: + **scipy.sparse interop** — :func:`cudaq_qec.get_decoder` and + :class:`cudaq_qec.Decoder` accept a ``scipy.sparse`` matrix (CSR, CSC, + COO, or any other ``scipy.sparse`` format) as the parity-check matrix + ``H``. This is the preferred form for large PCMs because no dense + ``rows x cols`` allocation is made — the matrix is normalised to CSR + internally. Dense NumPy ``uint8`` arrays remain supported. + The PCM utilities :func:`cudaq_qec.reorder_pcm_columns`, + :func:`cudaq_qec.shuffle_pcm_columns`, and + :func:`cudaq_qec.pcm_to_sparse_vec` also accept SciPy sparse matrices + without creating a dense ``cudaqx::tensor``. Reordering and shuffling a + sparse input returns a ``scipy.sparse.csc_matrix``; a dense input continues + to return a NumPy array. + + ``scipy`` is an optional dependency; if it is not installed, pass a dense + NumPy array instead. + Built-in Decoders ================= @@ -213,8 +230,10 @@ Chromobius Decoder .. include:: chromobius_api.rst -Real-Time Decoding -================== +.. _python_realtime_decoding_api: + +Realtime Decoding +================= .. include:: python_realtime_decoding_api.rst @@ -226,6 +245,23 @@ Common .. autofunction:: cudaq_qec.x_sample_memory_circuit .. autofunction:: cudaq_qec.z_sample_memory_circuit +.. _syndrome_measurement_layout: + +.. note:: + **Syndrome measurement layout** — ``sample_memory_circuit`` returns a tuple + ``(syndromes, data)``. The ``syndromes`` tensor has shape + ``(num_shots, num_detectors)`` with columns laid out as ``[ B S S … S B ]``: + + - ``B`` (boundary block) = ``code.get_num_z_stabilizers()`` for Z-basis + preparations (``prep0``/``prep1``), or ``code.get_num_x_stabilizers()`` for + X-basis preparations (``prepp``/``prepm``). + - ``S`` (inter-round block) = ``num_z_stabilizers + num_x_stabilizers`` + detectors per round transition (``num_rounds - 1`` blocks total). + - Total: ``num_detectors = 2*B + (num_rounds - 1)*S``. + + The ``data`` tensor has shape ``(num_shots, block_size)`` and holds the final + data-qubit measurements used to verify logical-state preservation. + .. autofunction:: cudaq_qec.sample_code_capacity .. _dem_sampling_python_api: diff --git a/docs/sphinx/api/qec/python_realtime_decoding_api.rst b/docs/sphinx/api/qec/python_realtime_decoding_api.rst index fff1b44ec..d582637bc 100644 --- a/docs/sphinx/api/qec/python_realtime_decoding_api.rst +++ b/docs/sphinx/api/qec/python_realtime_decoding_api.rst @@ -1,7 +1,4 @@ -.. _python_realtime_decoding_api: - - -The Real-Time Decoding API enables low-latency error correction on quantum hardware by allowing CUDA-Q quantum kernels to interact with decoders during circuit execution. This API is designed for use cases where corrections must be calculated and applied within qubit coherence times. +The Realtime Decoding API enables low-latency error correction on quantum hardware by allowing CUDA-Q quantum kernels to interact with decoders during circuit execution. This API is designed for use cases where corrections must be calculated and applied within qubit coherence times. The real-time decoding system supports simulation environments for local testing and hardware integration (e.g., on `Quantinuum's Helios QPU @@ -180,7 +177,7 @@ Configuration Functions Helper Functions ---------------- -Real-time decoding requires converting matrices to sparse format for efficient decoder configuration. The following utility functions are essential: +Realtime decoding requires converting matrices to sparse format for efficient decoder configuration. The following utility functions are essential: .. py:function:: cudaq_qec.pcm_to_sparse_vec(pcm) diff --git a/docs/sphinx/api/qec/tensor_network_decoder_api.rst b/docs/sphinx/api/qec/tensor_network_decoder_api.rst index 1b57a8783..e9ae5470d 100644 --- a/docs/sphinx/api/qec/tensor_network_decoder_api.rst +++ b/docs/sphinx/api/qec/tensor_network_decoder_api.rst @@ -13,11 +13,10 @@ decoder. Use `pip install cudaq-qec[tensor-network-decoder]` in order to use this decoder. - The Tensor Network Decoder has the same GPU support as the `Quantum Low-Density Parity-Check Decoder `__. + The Tensor Network Decoder has the same GPU support as the :ref:`Quantum Low-Density Parity-Check Decoder `. However, if you are using the V100 GPU (SM70), you will need to pin your cuTensor version to 2.2 by running `pip install cutensor_cu12==2.2`. Note - that this GPU will not be supported by the Tensor Network Decoder when - CUDA-Q 0.5.0 is released. + that this GPU is not supported by the Tensor Network Decoder. .. note:: It is recommended to create decoders using the `cudaq_qec` plugin API: diff --git a/docs/sphinx/components/qec/codes.rst b/docs/sphinx/components/qec/codes.rst new file mode 100644 index 000000000..5a21da55a --- /dev/null +++ b/docs/sphinx/components/qec/codes.rst @@ -0,0 +1,569 @@ +QEC Codes +========= + +The ``cudaq-qec`` code interface (:code:`cudaq::qec::code`) defines what a quantum error correcting code is: a mapping from logical operations to their physical CUDA-Q kernel implementations. This page covers the framework — the class structure and how to define or extend a code — together with the codes that ship with the library. Read it to understand the model or to implement your own code. For a runnable, end-to-end program, see the :doc:`Creating New QEC Codes example `. + +QEC Code Framework :code:`cudaq::qec::code` +------------------------------------------- + +The :code:`cudaq::qec::code` class serves as the base class for all quantum error correcting codes in CUDA-Q QEC. It provides +a flexible extension point for implementing new codes and defines the core interface that all QEC codes must support. + +The core abstraction here is that of a mapping or dictionary of logical operations to their +corresponding physical implementation in the error correcting code as CUDA-Q quantum kernels. + +Class Structure +^^^^^^^^^^^^^^^ + +The code base class provides: + +1. **Operation Enumeration**: Defines supported logical operations + + .. code-block:: cpp + + enum class operation { + x, // Logical X gate + y, // Logical Y gate + z, // Logical Z gate + h, // Logical Hadamard gate + s, // Logical S gate + cx, // Logical CNOT gate + cy, // Logical CY gate + cz, // Logical CZ gate + stabilizer_round, // Stabilizer measurement round + prep0, // Prepare |0⟩ state + prep1, // Prepare |1⟩ state + prepp, // Prepare |+⟩ state + prepm // Prepare |-⟩ state + }; + + +2. **Patch Type**: Defines the structure of a logical qubit patch + + .. code-block:: cpp + + struct patch { + cudaq::qview<> data; // View of data qubits + cudaq::qview<> ancx; // View of X stabilizer ancilla qubits + cudaq::qview<> ancz; // View of Z stabilizer ancilla qubits + }; + + The `patch` type represents a logical qubit in quantum error correction codes. It contains: + + - `data`: A view of the data qubits in the patch + - `ancx`: A view of the ancilla qubits used for X stabilizer measurements + - `ancz`: A view of the ancilla qubits used for Z stabilizer measurements + + This structure is designed for use within CUDA-Q kernel code and provides a + convenient way to access different qubit subsets within a logical qubit patch. + + +3. **Kernel Type Aliases**: Defines quantum kernel signatures + + .. code-block:: cpp + + using one_qubit_encoding = cudaq::qkernel; + using two_qubit_encoding = cudaq::qkernel; + using stabilizer_round = cudaq::qkernel( + patch, const std::vector&, const std::vector&)>; + + The two vector arguments of :code:`stabilizer_round` are the flattened X and + Z stabilizer *schedule* matrices, which can encode an optimized gate order + on top of the parity-check support. See + :cpp:func:`cudaq::qec::code::get_stabilizer_schedule_x` for the encoding + and the default (the plain parity matrices). + +4. **Protected Members**: + + - :code:`operation_encodings`: Maps operations to their quantum kernel implementations. The key is the ``operation`` enum and the value is a variant on the above kernel type aliases. + - :code:`m_stabilizers`: Stores the code's stabilizer generators + +Implementing a New Code +^^^^^^^^^^^^^^^^^^^^^^^ + +To implement a new quantum error correcting code: + +1. **Create a New Class**: + + .. code-block:: cpp + + class my_code : public qec::code { + protected: + // Implement required virtual methods + public: + my_code(const heterogeneous_map& options); + }; + +2. **Implement Required Virtual Methods**: + + .. code-block:: cpp + + // Number of physical data qubits + std::size_t get_num_data_qubits() const override; + + // Total number of ancilla qubits + std::size_t get_num_ancilla_qubits() const override; + + // Number of X-type ancilla qubits + std::size_t get_num_ancilla_x_qubits() const override; + + // Number of Z-type ancilla qubits + std::size_t get_num_ancilla_z_qubits() const override; + +3. **Define Quantum Kernels**: + + Create CUDA-Q kernels for each logical operation: + + .. code-block:: cpp + + __qpu__ void x(patch p) { + // Implement logical X + } + + __qpu__ std::vector stabilizer(patch p, + const std::vector& x_stabs, + const std::vector& z_stabs) { + // Implement stabilizer measurements + } + + .. note:: + + The two vector arguments passed to the :code:`stabilizer_round` kernel + are the flattened X and Z stabilizer *schedule* matrices returned by + :cpp:func:`cudaq::qec::code::get_stabilizer_schedule_x` and + :cpp:func:`cudaq::qec::code::get_stabilizer_schedule_z`. By default + these equal the plain parity-check matrices (every entry 0 or 1), but a + code can override the :code:`get_stabilizer_schedule_*` methods to + encode a gate order, in which case entry :code:`k >= 1` means the + interaction executes at timestep :code:`k` (the built-in + :code:`surface_code` does this to avoid hook errors). A kernel that only + needs the support pattern should therefore test entries for + :code:`!= 0` rather than :code:`== 1`. + +4. **Register Operations**: + + In the constructor, register quantum kernels for each operation: + + .. code-block:: cpp + + my_code::my_code(const heterogeneous_map& options) : code() { + // Register operations + operation_encodings.insert( + std::make_pair(operation::x, x)); + operation_encodings.insert( + std::make_pair(operation::stabilizer_round, stabilizer)); + + // Define stabilizer generators + m_stabilizers = fromPauliWords({"XXXX", "ZZZZ"}); + } + + + Note that in your constructor, you have access to user-provided ``options``. For + example, if your code depends on an integer parameter called ``distance``, you can + retrieve that from the user via + + .. code-block:: cpp + + my_code::my_code(const heterogeneous_map& options) : code() { + // ... fill the map and stabilizers ... + + // Get the user-provided distance, or just + // set to 3 if user did not provide one + this->distance = options.get("distance", /*defaultValue*/ 3); + } + +5. **Register Extension Point**: + + Add extension point registration. + + .. code-block:: cpp + + class my_code : public qec::code { + // ... members from above ... + + CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION( + my_code, + static std::unique_ptr create( + const heterogeneous_map &options) { + return std::make_unique(options); + }) + }; + + CUDAQ_EXT_PT_REGISTER_TYPE(my_code) + +Example: Steane Code +^^^^^^^^^^^^^^^^^^^^^ + +The Steane [[7,1,3]] code provides a complete example implementation: + +1. **Header Definition**: + + - Declares quantum kernels for all logical operations + - Defines the code class with required virtual methods + - Specifies 7 data qubits and 6 ancilla qubits (3 X-type, 3 Z-type) + +2. **Implementation**: + + .. code-block:: cpp + + steane::steane(const heterogeneous_map &options) : code() { + // Register all logical operations + operation_encodings.insert( + std::make_pair(operation::x, x)); + // ... register other operations ... + + // Define stabilizer generators + m_stabilizers = fromPauliWords({ + "XXXXIII", "IXXIXXI", "IIXXIXX", + "ZZZZIII", "IZZIZZI", "IIZZIZZ" + }); + } + +3. **Quantum Kernels**: + + Implements fault-tolerant logical operations: + + .. code-block:: cpp + + __qpu__ void x(patch logicalQubit) { + // Apply logical X to specific data qubits + x(logicalQubit.data[4], logicalQubit.data[5], + logicalQubit.data[6]); + } + + __qpu__ std::vector stabilizer(patch logicalQubit, + const std::vector& x_stabilizers, + const std::vector& z_stabilizers) { + // Measure X stabilizers + h(logicalQubit.ancx); + // ... apply controlled-X gates ... + h(logicalQubit.ancx); + + // Measure Z stabilizers + // ... apply controlled-X gates ... + + // Return measurement results + return mz(logicalQubit.ancz, logicalQubit.ancx); + } + +Implementing a New Code in Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CUDA-Q QEC supports implementing quantum error correction codes in Python +using the :code:`@qec.code` decorator. This provides a more accessible way +to prototype and develop new codes. + +1. **Create a New Python File**: + + Create a new file (e.g., :code:`my_steane.py`) with your code implementation: + + .. literalinclude:: ../../examples/qec/python/my_steane.py + :language: python + :start-after: [Begin Documentation1] + :end-before: [End Documentation1] + +2. **Define Quantum Kernels**: + + Implement the required quantum kernels using the :code:`@cudaq.kernel` decorator: + + .. literalinclude:: ../../examples/qec/python/my_steane.py + :language: python + :start-after: [Begin Documentation2] + :end-before: [End Documentation2] + + .. note:: + + The kernel registered for :code:`stabilizer_round` must be annotated to + return :code:`list[cudaq.measure_handle]`. + + .. note:: + + As in C++, the two list arguments passed to the + :code:`stabilizer_round` kernel are the flattened X and Z stabilizer + schedule matrices, which default to the parity-check matrices. A Python + code can optionally define :code:`get_stabilizer_schedule_x` / + :code:`get_stabilizer_schedule_z` methods returning a 2D array with the + same shape and support pattern as the corresponding parity-check + matrix, where entry :code:`k >= 1` schedules that interaction at + timestep :code:`k`. + +3. **Implement the Code Class**: + + Create a class decorated with :code:`@qec.code` that implements the required interface: + + .. literalinclude:: ../../examples/qec/python/my_steane.py + :language: python + :start-after: [Begin Documentation3] + :end-before: [End Documentation3] + +4. **Using the Code**: + + The code can now be used like any other CUDA-Q QEC code: + + .. literalinclude:: ../../examples/qec/python/my_steane_test.py + :language: python + :start-after: [Begin Documentation] + +Key Points +^^^^^^^^^^^ + +* The :code:`@qec.code` decorator takes the name of the code as an argument +* Operation encodings are registered via the :code:`operation_encodings` dictionary +* Stabilizer generators are defined as a list of :code:`cudaq.SpinOperator` +* The code must implement all required methods from the base class interface + + +Using the Code Framework +^^^^^^^^^^^^^^^^^^^^^^^^^ + +To use an implemented code: + +.. tab:: Python + + .. code-block:: python + + import cudaq_qec as qec + + # Create a code instance + code = qec.get_code("steane") + + # Access stabilizer information + stabilizers = code.get_stabilizers() + parity = code.get_parity() + + # The code can now be used for various numerical + # experiments - see section below. + +.. tab:: C++ + + .. code-block:: cpp + + // Create a code instance + auto code = cudaq::qec::get_code("steane"); + + // Access stabilizer information + auto stabilizers = code->get_stabilizers(); + auto parity = code->get_parity(); + + // The code can now be used for various numerical + // experiments - see section below. + + +Pre-built QEC Codes +------------------- + +CUDA-Q QEC provides several well-studied quantum error correction codes out of the box. Here's a detailed overview of each: + +Steane Code +^^^^^^^^^^^ + +The Steane code is a ``[[7,1,3]]`` CSS (Calderbank-Shor-Steane) code that encodes +one logical qubit into seven physical qubits with a code distance of 3. + +**Key Properties**: + +* Data qubits: 7 +* Encoded qubits: 1 +* Code distance: 3 +* Ancilla qubits: 6 (3 for X stabilizers, 3 for Z stabilizers) + +**Stabilizer Generators**: + +* X-type: ``["XXXXIII", "IXXIXXI", "IIXXIXX"]`` +* Z-type: ``["ZZZZIII", "IZZIZZI", "IIZZIZZ"]`` + +The Steane code can correct any single-qubit error and detect up to two errors. +It is particularly notable for being the smallest CSS code that can implement a universal set of transversal gates. + +Usage: + +.. tab:: Python + + .. code-block:: python + + import cudaq_qec as qec + + # Create Steane code instance + steane = qec.get_code("steane") + +.. tab:: C++ + + .. code-block:: cpp + + auto steane = cudaq::qec::get_code("steane"); + +Repetition Code +^^^^^^^^^^^^^^^ +The repetition code is a simple [[n,1,n]] code that protects against +bit-flip (X) errors by encoding one logical qubit into n physical qubits, where n is the code distance. + +**Key Properties**: + +* Data qubits: n (distance) +* Encoded qubits: 1 +* Code distance: n +* Ancilla qubits: n-1 (all for Z stabilizers) + +**Stabilizer Generators**: + +* For distance 3: ``["ZZI", "IZZ"]`` +* For distance 5: ``["ZZIII", "IZZII", "IIZZI", "IIIZZ"]`` + +The repetition code is primarily educational as it can only correct +X errors. However, it serves as an excellent introduction to QEC concepts. + +Usage: + +.. tab:: Python + + .. code-block:: python + + import cudaq_qec as qec + + # Create distance-3 repetition code + code = qec.get_code('repetition', distance=3) + + # Access stabilizers + stabilizers = code.get_stabilizers() # Returns ["ZZI", "IZZ"] + +.. tab:: C++ + + .. code-block:: cpp + + auto code = qec::get_code("repetition", {{"distance", 3}}); + + // Access stabilizers + auto stabilizers = code->get_stabilizers(); + +Surface Code +^^^^^^^^^^^^ + +The library provides a **rotated surface code** on a two-dimensional qubit +layout with **open boundaries** (a single patch). It is a **CSS** code—:math:`X` +and :math:`Z` errors are handled in separate CSS sectors—encoding **one logical +qubit** into :math:`d^2` data qubits with code distance :math:`d` in this +layout. Stabilizers have weight four in the bulk and weight two on the boundary, following the grid convention +described in `Towards a Standardized Definition of Quantum Circuits for Quantum +Error Correction with Rotated Surface Codes +`__. + +**Key Properties** (distance :math:`d` in this implementation): + +* Data qubits: :math:`d^2` +* Encoded logical qubits: 1 +* Code distance: :math:`d` +* Stabilizers: :math:`d^2 - 1` total—:math:`(d^2 - 1) / 2` :math:`X`-type and + :math:`(d^2 - 1) / 2` :math:`Z`-type. The :code:`patch` type assigns **one + ancilla per stabilizer** measurement, so the ancilla count matches the + stabilizer count here; other hardware layouts could fold or share ancillas + differently. + +**Stabilizer Generators** (example: ``distance`` :math:`= 3`) + +Data qubits are indexed in row-major order (left to right, top to bottom); the +leftmost character of each Pauli string is qubit ``0``, matching the rest of this +document. For :math:`d = 3` there are nine data qubits: + +:: + + d0 d1 d2 + d3 d4 d5 + d6 d7 d8 + +* X-type (weight 2 on the left and right boundaries, weight 4 in the bulk): + + * ``XIIXIIIII`` + * ``IXXIXXIII`` + * ``IIIXXIXXI`` + * ``IIIIIXIIX`` + +* Z-type (weight 2 on the top and bottom boundaries, weight 4 in the bulk): + + * ``IZZIIIIII`` + * ``ZZIZZIIII`` + * ``IIIIZZIZZ`` + * ``IIIIIIZZI`` + +These Pauli words are exactly those used internally for :math:`d=3`; +:code:`get_stabilizers()` returns the same generators in a canonical sorted order +(rather than grouped as X-type then Z-type). + +For other distances, stabilizer supports are generated from the same rotated +grid; use :ref:`stabilizer_grid ` or +:code:`get_stabilizers()` to inspect them. + +You must pass ``distance`` when constructing the code; there is no default. + +**Orientation** + +The surface code accepts an optional ``orientation`` string that selects which +Pauli type occupies the bulk checkerboard and which boundary pair carries the +X- versus Z-type stabilizers. The first character (``X`` or ``Z``) sets the bulk +type; the second character (``H`` or ``V``) sets the boundary placement. Valid +values are ``"XV"``, ``"XH"``, ``"ZV"``, and ``"ZH"`` (aliases ``"O1"``, ``"O2"``, +``"O3"``, and ``"O4"`` respectively; case-insensitive). The default is ``"ZH"``, +which reproduces the layout described above. The logical observables and the CNOT +extraction schedule are orientation-aware, so changing the orientation changes the +returned stabilizers, observables, and measurement schedule consistently. + +The :ref:`stabilizer_grid ` helper documents how +stabilizers and data qubits are indexed on the grid and provides helpers to +print the layout. **Python:** :ref:`cudaq_qec.stabilizer_grid ` — **C++:** +:cpp:class:`cudaq::qec::surface_code::stabilizer_grid` — see +:ref:`API `. The header :file:`cudaq/qec/codes/surface_code.h` +contains the full declaration. + +**Stabilizer measurement schedule** + +The surface code's :code:`stabilizer_round` kernel executes one depth-4 +extraction round: the X- and Z-check CNOTs are interleaved over four shared +timesteps. Within each plaquette the CNOT order follows the standard zigzag +schedule for the rotated surface code (`Tomita & Svore +`__): the X and Z plaquettes traverse their +corners in transposed orders, selected per orientation so that mid-round +ancilla faults ("hook errors", `Dennis et al. +`__) propagate onto data-qubit pairs +perpendicular to the same-type logical operator. This preserves the full code +distance :math:`d` under circuit-level noise; a naive schedule (both plaquette +types in ascending qubit-index order) halves the effective distance of one +memory basis. The schedule is available from the :code:`stabilizer_grid` +helper via +:cpp:func:`~cudaq::qec::surface_code::stabilizer_grid::get_cnot_schedule_x` / +:code:`get_cnot_schedule_z` (matrix form) and +:code:`get_cnot_schedule_pairs_x` / :code:`get_cnot_schedule_pairs_z` (flat +pair-list form). + +Usage: + +.. tab:: Python + + .. code-block:: python + + import cudaq_qec as qec + + # Rotated surface code; distance is required + code = qec.get_code('surface_code', distance=3) # default orientation "ZH" + + # Optionally select an orientation (one of "XV", "XH", "ZV", "ZH") + code_xh = qec.get_code('surface_code', distance=3, orientation='XH') + + stabilizers = code.get_stabilizers() + parity = code.get_parity() + +.. tab:: C++ + + .. code-block:: cpp + + auto code = cudaq::qec::get_code( + "surface_code", cudaqx::heterogeneous_map{{"distance", 3}}); + + // Optionally select an orientation (one of "XV", "XH", "ZV", "ZH") + auto code_xh = cudaq::qec::get_code( + "surface_code", cudaqx::heterogeneous_map{ + {"distance", 3}, + {"orientation", std::string("XH")}}); + + auto stabilizers = code->get_stabilizers(); + auto parity = code->get_parity(); + + diff --git a/docs/sphinx/components/qec/conventions.rst b/docs/sphinx/components/qec/conventions.rst new file mode 100644 index 000000000..d634d8201 --- /dev/null +++ b/docs/sphinx/components/qec/conventions.rst @@ -0,0 +1,68 @@ +Conventions +=========== + +The pre-built ``cudaq-qec`` codes and decoders follow a common set of conventions for how errors, syndromes, and logical observables are laid out. This page documents them; the decoders, examples, and API reference all build on these conventions. + +To address vectors of qubits (`cudaq::qvector`), CUDA-Q indexing starts from 0, and 0 corresponds +to the leftmost position when working with Pauli strings (`cudaq::spin_op`). For example, applying a Pauli X operator +to qubit 1 out of 7 would be `X_1 = IXIIIII`. + +While implementing your own codes and decoders, you are free to follow any convention that is convenient to you. However, +to interact with the pre-built QEC codes and decoders within this library, the following conventions are used. All of these codes +are CSS codes, and so we separate :math:`X`-type and :math:`Z`-type errors. For example, an error vector for 3 qubits will +have 6 entries, 3 bits representing the presence of a bit-flip on each qubit, and 3 bits representing a phase-flip on each qubit. +An error vector representing a bit-flip on qubit 0, and a phase-flip on qubit 1 would look like `E = 100010`. This means that this +error vector is just two error vectors (`E_X, E_Z`) concatenated together (`E = E_X | E_Z`). + +These errors are detected by stabilizers. :math:`Z`-stabilizers detect :math:`X`-type errors and vice versa. Thus we write our +CSS parity check matrices as + +.. math:: + H_{CSS} = \begin{pmatrix} + H_Z & 0 \\ + 0 & H_X + \end{pmatrix}, + +so that when we generate a syndrome vector by multiplying the parity check matrix by an error vector we get + +.. math:: + \begin{align} + S &= H \cdot E\\ + S_X &= H_Z \cdot E_x\\ + S_Z &= H_X \cdot E_Z. + \end{align} + +This means that for the concatenated syndrome vector `S = S_X | S_Z`, the first part, `S_X`, are syndrome bits triggered by `Z` +stabilizers detecting `X` errors. This is because the `Z` stabilizers like `ZZI` and `IZZ` anti-commute with `X` errors like +`IXI`. + +The decoder prediction as to what error happened is `D = D_X | D_Z`. A successful error decoding does not require that `D = E`, +but that `D + E` is not a logical operator. There are a couple ways to check this. +For bitflip errors, we check that the residual error `R = D_X + E_X` is not `L_X`. Since `X` anticommutes +with `Z`, we can check that `L_Z(D_X + E_X) = 0`. This is because we just need to check if they have mutual support on an even +or odd number of qubits. We could also check that `R` is not a stabilizer. + +Similar to the parity check matrix, the logical observables are also stored in a matrix as + +.. math:: + L = \begin{pmatrix} + L_Z & 0 \\ + 0 & L_X + \end{pmatrix}, + +so that when determining logical errors, we can do matrix multiplication + +.. math:: + \begin{align} + P &= L \cdot R\\ + P_X &= L_Z \cdot R_x\\ + P_Z &= L_X \cdot R_Z. + \end{align} + +Here we're using `P` as this can be stored in a Pauli frame tracker to track observable flips. + +Each logical qubit has logical observables associated with it. Depending on what basis the data qubits are measured in, either the +`X` or `Z` logical observables can be measured. The data qubits which support the logical observables are contained in the `qec::code` class as well. + +To do a logical `Z(X)` measurement, measure out all of the data qubits in the `Z(X)` basis. Then check support on the appropriate +`Z(x)` observable. diff --git a/docs/sphinx/components/qec/decoders.rst b/docs/sphinx/components/qec/decoders.rst new file mode 100644 index 000000000..14cf6c666 --- /dev/null +++ b/docs/sphinx/components/qec/decoders.rst @@ -0,0 +1,619 @@ +QEC Decoders +============ + +The ``cudaq-qec`` decoder interface (:code:`cudaq::qec::decoder`) turns syndromes into corrections. This page covers the framework — the class structure and how to implement a decoder — together with a catalog of the decoders that ship with the library. Read it to choose a built-in decoder or to write your own. For runnable programs, see the :doc:`Decoders examples `. + +Decoder Framework :code:`cudaq::qec::decoder` +---------------------------------------------- + +The CUDA-Q QEC decoder framework provides an extensible system for implementing +quantum error correction decoders through the :code:`cudaq::qec::decoder` base class. + +Class Structure +^^^^^^^^^^^^^^^ + +The decoder base class defines the core interface for syndrome decoding: + +.. code-block:: cpp + + class decoder { + protected: + std::size_t block_size; // For [n,k] code, this is n + std::size_t syndrome_size; // For [n,k] code, this is n-k + sparse_binary_matrix H; // Parity check matrix + + public: + struct decoder_result { + bool converged; // Decoder convergence status + std::vector result; // Soft error probabilities + }; + + virtual decoder_result decode( + const std::vector& syndrome) = 0; + + virtual std::vector decode_batch( + const std::vector>& syndrome); + }; + +Key Components: + +* **Parity Check Matrix**: Defines the code structure via the sparse :code:`H` member +* **Block Size**: Number of physical qubits in the code +* **Syndrome Size**: Number of stabilizer measurements +* **Decoder Result**: Contains convergence status and error probabilities +* **Multiple Decoding Modes**: Single syndrome or batch processing + +Implementing a New Decoder in C++ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To implement a new decoder: + +1. **Create Decoder Class**: + +.. code-block:: cpp + + class my_decoder : public qec::decoder { + private: + // Decoder-specific members + + public: + my_decoder(const qec::sparse_binary_matrix& H, + const cudaqx::heterogeneous_map& params) + : decoder(H) { + // Initialize decoder + } + + decoder_result decode( + const std::vector& syndrome) override { + // Implement decoding logic + } + }; + +2. **Register Extension Point**: + +.. code-block:: cpp + + class my_decoder : public qec::decoder { + // ... constructor and decode() from above ... + + CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION( + my_decoder, + static std::unique_ptr create( + const qec::decoder_init& init, + const cudaqx::heterogeneous_map& params) { + return qec::make_pcm_decoder(init, params); + }) + }; + + CUDAQ_EXT_PT_REGISTER_TYPE(my_decoder) + +The :code:`make_pcm_decoder` helper dispatches :code:`decoder_init`. It +passes a stored sparse PCM directly to the decoder constructor; when the +variant contains Stim DEM text, it parses the DEM and constructs the sparse +detector matrix before invoking the same constructor. + +Example: Lookup Table Decoder +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here's a simple lookup table decoder for the Steane code: + +.. code-block:: cpp + + class single_error_lut : public decoder { + private: + std::map single_qubit_err_signatures; + + public: + single_error_lut(const qec::sparse_binary_matrix& H, + const cudaqx::heterogeneous_map& params) + : decoder(H) { + // Canonicalize before using each sparse column as an error + // signature so duplicate row indices cancel over GF(2). + auto H_e2d = H.canonicalize().to_nested_csc(); + + for (std::size_t qErr = 0; qErr < block_size; qErr++) { + std::string err_sig(syndrome_size, '0'); + for (std::uint32_t row : H_e2d[qErr]) + err_sig[row] = '1'; + single_qubit_err_signatures.insert({err_sig, qErr}); + } + } + + decoder_result decode( + const std::vector& syndrome) override { + decoder_result result{false, + std::vector(block_size, 0.0)}; + + // Convert syndrome to string + std::string syndrome_str(syndrome_size, '0'); + for (std::size_t i = 0; i < syndrome_size; i++) + syndrome_str[i] = (syndrome[i] >= 0.5) ? '1' : '0'; + + // Lookup error location + auto it = single_qubit_err_signatures.find(syndrome_str); + if (it != single_qubit_err_signatures.end()) { + result.converged = true; + result.result[it->second] = 1.0; + } + + return result; + } + }; + +Implementing a Decoder in Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CUDA-Q QEC supports implementing decoders in Python using the :code:`@qec.decoder` decorator: + +1. **Create Decoder Class**: + +.. code-block:: python + + @qec.decoder("my_decoder") + class MyDecoder: + def __init__(self, H, **kwargs): + # H is a scipy.sparse matrix or a dense numpy uint8 array, + # mirroring whatever was passed to qec.get_decoder(). + # Pass it unchanged to Decoder.__init__ so the C++ base class + # stores a compact sparse representation without a dense allocation. + qec.Decoder.__init__(self, H) + self.H = H + # Initialize with optional kwargs + + def decode(self, syndrome): + # Create result object + result = qec.DecoderResult() + + # Implement decoding logic + # ... + + # Set results + result.converged = True + result.result = [0.0] * self.get_block_size() + + return result + +2. **Using Custom Parameters**: + +.. code-block:: python + + # Create decoder with custom parameters + decoder = qec.get_decoder("my_decoder", + parity_check_matrix, + custom_param=42) + +Key Features +^^^^^^^^^^^^^ + +* **Soft Decision Decoding**: Results are probabilities in [0,1] +* **Batch Processing**: Support for decoding multiple syndromes +* **Asynchronous Decoding**: Optional async interface for parallel processing +* **Custom Parameters**: Flexible configuration via heterogeneous_map +* **Python Integration**: First-class support for Python implementations + +Usage Example +^^^^^^^^^^^^^^ + +.. tab:: Python + + .. code-block:: python + + import cudaq_qec as qec + + # Get a code instance + steane = qec.get_code("steane") + + # Create decoder with code's parity matrix + decoder = qec.get_decoder('single_error_lut', steane.get_parity()) + + # Run stabilizer measurements + syndromes, dataQubitResults = qec.sample_memory_circuit(steane, numShots=1, numRounds=1) + + # Decode a syndrome + result = decoder.decode(syndromes[0]) + if result.converged: + print("Error locations:", + [i for i,p in enumerate(result.result) if p > 0.5]) + # No errors as we did not include a noise model and + # thus prints: + # Error locations: [] + +.. tab:: C++ + + .. code-block:: cpp + + using namespace cudaq; + + // Get a code instance + auto code = qec::get_code("steane"); + + // Create decoder with code's parity matrix + auto decoder = qec::get_decoder("single_error_lut", + code->get_parity()); + + // Run stabilizer measurements + auto [syndromes, dataQubitResults] = qec::sample_memory_circuit(*code, /*numShots*/ numShots, /*numRounds*/ 1); + + // Copy a single shot syndrome and decode + std::vector syndrome( + syndromes.data(), syndromes.data() + syndromes.shape()[1]); + auto result = decoder->decode(syndrome); + + +.. _detector_error_model: + +Detector Error Model +-------------------- + +A detector error model (DEM) captures how the physical errors in a QEC circuit map to the detectors (syndrome bits) that observe them. CUDA-Q QEC represents it with the ``cudaq.qec.detector_error_model`` type, built from a QEC circuit and a noise model via functions like ``dem_from_memory_circuit()``. For circuit-level noise, the DEM can be put into a canonical form organized by measurement rounds, making it suitable for multi-round decoding. + +The parity check matrix a decoder consumes is derived from the DEM: each row is a detector and each column a possible error mechanism. For a runnable example that generates a DEM from a surface code and decodes with it, see the :doc:`Experiments and Noise Modeling ` example. + +.. _decoding_from_stim_dem_text: + +Decoding from Stim DEM Text +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A DEM does not have to be produced inside CUDA-Q. Decoders can be constructed from either a parity-check matrix or raw Stim detector error model (DEM) text, which is useful when the model is already available in Stim's ``.dem`` format — from a saved file, a Stim workflow, or CUDA-Q DEM generation. + +For PCM-based decoders, CUDA-Q QEC parses the DEM text into a detector error matrix and supplies DEM-derived ``O`` and ``error_rate_vec`` defaults when the user does not provide them. C++ decoder plugins that need full Stim DEM metadata can consume the raw DEM string from the decoder construction input. By default, ``get_decoder(..., dem_text)`` and ``dem_from_stim_text(dem_text)`` parse with ``use_decomp_suggestions=False`` — Stim ``^`` decomposition hints are ignored and each ``error(...)`` instruction becomes one matrix column; passing ``use_decomp_suggestions=True`` splits ``^``-separated components into separate columns. + +For a runnable example, see :ref:`Decoding From Stim DEM Text `. + +.. _dem_sampling: + +DEM Sampling +^^^^^^^^^^^^ + +The ``dem_sampling`` function samples errors and syndromes from a detector error model, which is useful for generating synthetic syndrome data to exercise a decoder. Given a binary check matrix :math:`H` of shape ``[num_checks x num_error_mechanisms]`` and a vector of per-mechanism Bernoulli probabilities, it generates random error vectors and computes :math:`\text{syndromes} = \text{errors} \cdot H^T \pmod{2}`. + +In Python, the ``backend`` parameter (``"auto"``, ``"gpu"``, or ``"cpu"``) controls whether sampling runs on the GPU via cuStabilizer or on the CPU. The function accepts NumPy arrays and PyTorch CUDA tensors. In C++ the CPU and GPU paths live in separate namespaces (``cudaq::qec::dem_sampler::cpu`` and ``cudaq::qec::dem_sampler::gpu``). + +For a complete, runnable walkthrough — including GPU acceleration and input-type handling — see the :ref:`DEM Sampling example `. + + +.. _dynamic_dem_construction: + +Dynamic DEM Construction +^^^^^^^^^^^^^^^^^^^^^^^^^ + +When a Stim circuit is not available — or when the round count must stay flexible until decoder construction — CUDA-Q QEC can build a detector error model directly from CSS generator matrices instead. ``dem_from_css_matrices`` produces a :math:`T`-round code-capacity or phenomenological DEM from ``css_code_matrices`` (Python ``CssCodes``) and ``css_noise_params`` (Python ``CssNoise``). + +For decoders whose round count is chosen at run time, the same model is expressed as composable per-round chunks: ``extended_dem_from_css_matrices`` builds a one-round :math:`\text{ExtendedDem}` chunk, and ``dem_stitch`` / ``dem_close_all`` stitch and close chunks into the same flat DEM. Real-time decoder configs accept this as a YAML ``dem_chunks`` block with ``init`` / ``bulk`` / ``final`` phases, expanded during decoder construction by ``expand_dem_chunks``; omit ``num_rounds`` for streaming decoders. + +For a complete, runnable walkthrough — matrix construction, chunk stitching, the YAML ``dem_chunks`` layout, and closing rules — see the :ref:`Dynamic DEM Construction example `. + + +.. _prebuilt_qec_decoders: + +Pre-built QEC Decoders +---------------------- + +CUDA-Q QEC provides pre-built decoders for a variety of use cases. + +.. list-table:: + :header-rows: 1 + :widths: 20 26 8 8 12 40 + + * - Decoder + - Decoder String Identifier + - Python + - C++ + - Realtime Enabled + - Notes + * - NVIDIA QLDPC Decoder¹ + - `"nv-qldpc-decoder"` + - Yes + - Yes + - Yes + - Supports Relay BP and BP+OSD + * - Tensor Network Decoder¹ + - `"tensor_network_decoder"` + - Yes² + - No + - No + - Exact Maximum Likelihood Decoder + * - TensorRT Decoder¹ + - `"trt_decoder"` + - Yes³ + - Yes + - No + - AI decoder. Bring your own model. + * - PyMatching Decoder + - `"pymatching"` + - Yes + - Yes + - Yes + - MWPM decoder for matchable codes such as the surface code + * - Chromobius Decoder + - `"chromobius"` + - Yes + - Yes + - No + - Color-code (Möbius) decoder; constructed from Stim DEM text + * - Look-Up Table Decoder + - `"single_error_lut"` / `"multi_error_lut"` + - Yes + - Yes + - Yes + - Simple LUT decoders; ``multi_error_lut`` handles up to ``lut_error_depth`` errors + * - Sliding Window Decoder + - `"sliding_window"` + - Yes + - Yes + - No + - Decodes syndromes in a sliding window; pairs with any inner decoder except the TensorRT Decoder + +| ¹ GPU-accelerated decoder +| ² Requires installation with `pip install cudaq-qec[tensor-network-decoder]` for Python +| ³ Requires installation with `pip install cudaq-qec[trt-decoder]` for Python + +Here's a detailed overview of each: + +.. _qldpc_decoder: + +Quantum Low-Density Parity-Check Decoder +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Quantum Low-Density Parity-Check (QLDPC) decoder leverages GPU-accelerated belief propagation (BP) for efficient error correction. +Since belief propagation is an iterative method which may not converge, decoding can be improved with a second-stage post-processing step. The `nv-qldpc-decoder` +API provides various post-processing options, which can be selected through its parameters. + +**Belief Propagation Methods:** + +The decoder supports several belief-propagation algorithms -- sum-product, min-sum, and memory-based variants -- selected via ``bp_method``, with optional BP+OSD post-processing. For the complete list of methods, parameters, and defaults, see the ``nv-qldpc-decoder`` entries in the :ref:`C++ ` and :ref:`Python ` API reference. + +**Highlighted Features:** + +* **Sequential Relay BP** (``composition=1``): chains multiple "relay legs" -- sequential BP runs with different gamma configurations -- to decode syndromes that stall a single BP pass. **Requires:** ``bp_method=3``, ``gamma0``, ``srelay_config``, and either ``gamma_dist`` OR ``explicit_gammas``. +* **Gamma ensembling** (``gamma_ensemble_size``): an extension of Relay BP that explores multiple sets of gamma values in parallel on a single GPU (N independent "lanes"). The first lane to converge lets the decoder exit early, so slow lanes are terminated without adding to the decode time -- narrowing the latency distribution and improving performance on hard-to-decode syndromes that would otherwise stall Relay BP. See :doc:`Improving Relay BP Decoding With Gamma Ensembles ` for a performance study. + +The QLDPC decoder `nv-qldpc-decoder` requires a CUDA-Q compatible GPU. See the `CUDA-Q dependencies and compatibility `_ list. + +The decoder is based on the following references: + +* https://arxiv.org/pdf/2005.07016 +* https://github.com/quantumgizmos/ldpc +* https://arxiv.org/pdf/2506.01779 +* https://github.com/trmue/relay + + +Usage: + +.. tab:: Python + + .. code-block:: python + + import cudaq_qec as qec + import numpy as np + + H_list = [ + [1, 0, 0, 1, 0, 1, 1], + [0, 1, 0, 1, 1, 0, 1], + [0, 0, 1, 0, 1, 1, 1] + ] + + H_np = np.array(H_list, dtype=np.uint8) + + decoder = qec.get_decoder("nv-qldpc-decoder", H_np) + +.. tab:: C++ + + .. code-block:: cpp + + std::size_t block_size = 7; + std::size_t syndrome_size = 3; + cudaqx::tensor H; + + std::vector H_vec = {1, 0, 0, 1, 0, 1, 1, + 0, 1, 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, 1, 1}; + H.copy(H_vec.data(), {syndrome_size, block_size}); + + cudaqx::heterogeneous_map nv_custom_args; + nv_custom_args.insert("use_osd", true); + + auto d1 = cudaq::qec::get_decoder("nv-qldpc-decoder", H, nv_custom_args); + + // Alternatively, configure the decoder without instantiating a heterogeneous_map + auto d2 = cudaq::qec::get_decoder("nv-qldpc-decoder", H, {{"use_osd", true}, {"bp_batch_size", 100}}); + +For a runnable example, see :ref:`Getting Started with the NVIDIA QLDPC Decoder `. + +Tensor Network Decoder +^^^^^^^^^^^^^^^^^^^^^^ + +The ``tensor_network_decoder`` constructs a tensor network representation of a quantum code given its parity check matrix, logical observable(s), and noise model. It can decode individual syndromes or batches of syndromes, returning the probability that a logical observable has flipped. + +Due to the additional dependencies of the Tensor Network Decoder, you must +specify the optional pip package when installing CUDA-Q QEC in order to use this +decoder. Use `pip install cudaq-qec[tensor-network-decoder]` in order to use +this decoder. + +Key Steps: + +1. **Define the parity check matrix**: This matrix encodes the structure of the quantum code. In the example, a simple [3,1] repetition code is used. + +2. **Specify the logical observable**: This is typically a row vector indicating which qubits participate in the logical operator. + +3. **Set the noise model**: The example uses a factorized noise model with independent bit-flip probability for each error mechanism. + +4. **Instantiate the decoder**: Create a decoder object using ``qec.get_decoder("tensor_network_decoder", ...)`` with the code parameters. + +5. **Decode syndromes**: Use the ``decode`` method for single syndromes or ``decode_batch`` for multiple syndromes. + + +Usage: + +.. tab:: Python + + .. code-block:: python + + # This example demonstrates how to use the get_decoder("tensor_network_decoder", ...) API + # from the ``cudaq_qec`` library to decode syndromes for a simple + # quantum error-correcting code using tensor networks. + + import cudaq_qec as qec + import numpy as np + + # Define code parameters + H = np.array([[1, 1, 0], [0, 1, 1]], dtype=np.uint8) + logical_obs = np.array([[1, 1, 1]], dtype=np.uint8) + noise_model = [0.1, 0.1, 0.1] + + decoder = qec.get_decoder("tensor_network_decoder", H, logical_obs=logical_obs, noise_model=noise_model) + + # Decode a single syndrome + syndrome = [0.0, 1.0] + result = decoder.decode(syndrome) + print(result.result) + + # Decode a batch of syndromes + syndrome_batch = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], dtype=np.float32) + batch_results = decoder.decode_batch(syndrome_batch) + for res in batch_results: + print(res.result) + +.. tab:: C++ + + The ``tensor_network_decoder`` is a Python-only implementation and it requires Python 3.11 or higher. C++ APIs are not available for this decoder. + +Output: + +The decoder returns the probability that the logical observable has flipped for each syndrome. This can be used to assess the performance of the code and the decoder under different error scenarios. + +.. note:: + + In general, the Tensor Network Decoder has the same GPU support as the + :ref:`Quantum Low-Density Parity-Check Decoder `. + However, if you are using the V100 GPU (SM70), you will need to pin your + cuTensor version to 2.2 by running `pip install cutensor_cu12==2.2`. + +For a runnable example, see :ref:`Exact Maximum Likelihood Decoding with NVIDIA Tensor Network Decoder `. + + +TensorRT Decoder +^^^^^^^^^^^^^^^^ + +The ``trt_decoder`` deploys a trained neural-network decoder (an ONNX model) through NVIDIA TensorRT for optimized GPU inference. Unlike the algorithmic decoders, it is trained on a specific code and noise model — you bring your own model. Python use requires ``pip install cudaq-qec[trt-decoder]``. See the :ref:`TensorRT Decoder API ` for configuration options, and the :ref:`Deploying AI Decoders with TensorRT example ` for the full train-to-deploy workflow. + +PyMatching Decoder +^^^^^^^^^^^^^^^^^^ + +The ``pymatching`` decoder is a minimum-weight perfect matching (MWPM) decoder built on the open-source `PyMatching `_ library, suitable for matchable codes such as the surface code. It is selected by name through ``get_decoder`` and takes a parity-check matrix whose columns each have one or two set entries; per-edge priors are supplied via ``error_rate_vec``. See the :ref:`PyMatching Decoder API ` and the :ref:`Matching-Based Decoding with PyMatching example `. + +Chromobius Decoder +^^^^^^^^^^^^^^^^^^ + +The ``chromobius`` decoder is a color-code decoder built on the open-source `Chromobius `_ Möbius decoder. Unlike the matrix-based decoders, it is constructed from Stim detector-error-model (DEM) text rather than a parity-check matrix, and predicts logical observable flips directly. See the :ref:`Chromobius Decoder API ` and the :ref:`Color-Code Decoding with Chromobius example `. + +Sliding Window Decoder +^^^^^^^^^^^^^^^^^^^^^^ + +Sliding-window decoding handles **circuit-level noise** across several syndrome +rounds by processing syndromes **before the full measurement sequence arrives**, +which **reduces latency** at the cost of **higher logical error rates** than +decoding the entire sequence at once. + +Whether that tradeoff is worthwhile depends on the **noise model**, **code +parameters**, and **latency budget**. Since **CUDA-Q 0.5.0**, you can use **any +CUDA-Q decoder** as the **inner** decoder and tune behavior mainly via **window +size** and the other settings below. Each round must yield the **same +number of syndrome measurements**; the decoder assumes **no particular temporal +structure** of the noise, so you can still vary noise **from round to round** in +experiments. + +Key Steps: + +1. **Obtain a detector error matrix and rates**: Pass the parity check matrix + ``H`` (for example ``dem.detector_error_matrix``) and ``error_rate_vec`` with + one entry per column of ``H`` (for example ``dem.error_rates`` from the same + DEM). The matrix must be in the sorted form expected by :code:`pcm_is_sorted` + for your ``num_syndromes_per_round``; DEMs from :code:`dem_from_memory_circuit` + (and its single-basis variants :code:`z_dem_from_memory_circuit` / + :code:`x_dem_from_memory_circuit`) are canonicalized. Hand-built matrices may + need :code:`simplify_pcm`. +2. **Set the schedule and window**: Provide ``num_syndromes_per_round`` (the number of + syndrome measurements per round) and ``num_boundary_syndromes`` (the number of + stabilizer syndromes fixed by the state-prep at the beginning and end of the circuit). + Choose ``window_size`` and ``step_size`` so ``window_size`` and + ``step_size`` stay within valid bounds and ``num_rounds - window_size`` is + divisible by ``step_size``, with ``num_rounds`` inferred from ``H`` and + ``num_syndromes_per_round``. +3. **Pick an inner decoder**: Use ``inner_decoder_name`` and + ``inner_decoder_params`` for the decoder that runs inside each window (for + example :code:`nv-qldpc-decoder`). Optional ``straddle_start_round`` / + ``straddle_end_round`` control cross-round mechanisms at window edges. +4. **Construct and run**: Call :code:`get_decoder("sliding_window", H, opts)`, + then ``decode`` or ``decode_batch``. Partial syndromes leave the decoder in an + intermediate state until enough bits arrive; full parameter lists and + behavior are in :doc:`/api/qec/python_api` and :doc:`/api/qec/cpp_api`. + +Background: `Toward Low-latency Iterative Decoding of QLDPC Codes Under Circuit-Level Noise `__. + +Usage: + +.. tab:: Python + + .. code-block:: python + + import cudaq + import cudaq_qec as qec + import numpy as np + + cudaq.set_target('stim') + num_rounds = 5 + code = qec.get_code('surface_code', distance=num_rounds) + noise = cudaq.NoiseModel() + noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.001), 1) + statePrep = qec.operation.prep0 + dem = qec.dem_from_memory_circuit(code, statePrep, num_rounds, noise) + inner_decoder_params = {'use_osd': True, 'max_iterations': 50, 'use_sparsity': True} + opts = { + 'error_rate_vec': np.array(dem.error_rates), + 'window_size': 1, + 'num_syndromes_per_round': code.get_num_z_stabilizers() + code.get_num_x_stabilizers(), + 'num_boundary_syndromes': code.get_num_z_stabilizers(), + 'inner_decoder_name': 'nv-qldpc-decoder', + 'inner_decoder_params': inner_decoder_params, + } + swdec = qec.get_decoder('sliding_window', dem.detector_error_matrix, **opts) + +.. tab:: C++ + + .. code-block:: cpp + + #include "cudaq/qec/code.h" + #include "cudaq/qec/decoder.h" + #include "cudaq/qec/experiments.h" + #include "common/NoiseModel.h" + + int main() { + int num_rounds = 5; + auto code = cudaq::qec::get_code( + "surface_code", cudaqx::heterogeneous_map{{"distance", num_rounds}}); + cudaq::noise_model noise; + noise.add_all_qubit_channel("x", cudaq::depolarization2(0.001), 1); + auto statePrep = cudaq::qec::operation::prep0; + auto dem = cudaq::qec::dem_from_memory_circuit(*code, statePrep, num_rounds, + noise); + auto inner_decoder_params = cudaqx::heterogeneous_map{ + {"use_osd", true}, {"max_iterations", 50}, {"use_sparsity", true}}; + auto opts = cudaqx::heterogeneous_map{ + {"error_rate_vec", dem.error_rates}, + {"window_size", 1}, + {"num_syndromes_per_round", code->get_num_z_stabilizers() + code->get_num_x_stabilizers()}, + {"num_boundary_syndromes", code->get_num_z_stabilizers()}, + {"inner_decoder_name", "nv-qldpc-decoder"}, + {"inner_decoder_params", inner_decoder_params}}; + auto swdec = cudaq::qec::get_decoder("sliding_window", + dem.detector_error_matrix, opts); + return 0; + } + +Output: + +Once a decode step completes, results use the same types as other pre-built +decoders (:class:`cudaq_qec.Decoder` in Python, :cpp:class:`cudaq::qec::decoder` +in C++). + diff --git a/docs/sphinx/components/qec/index.rst b/docs/sphinx/components/qec/index.rst new file mode 100644 index 000000000..132b56ae7 --- /dev/null +++ b/docs/sphinx/components/qec/index.rst @@ -0,0 +1,28 @@ +CUDA-Q QEC - Quantum Error Correction Library +============================================= + +The ``cudaq-qec`` library provides a comprehensive framework for quantum +error correction research and development. It leverages GPU acceleration +for efficient syndrome decoding and error correction simulations (coming soon). + +The library supports both offline analysis and realtime error correction on quantum hardware, +enabling low-latency decoding for practical quantum computing applications. + +``cudaq-qec`` is composed of three main interfaces: + +1. **QEC Codes** (:code:`cudaq::qec::code`) - Define quantum error correcting codes with logical operations +2. **Decoders** (:code:`cudaq::qec::decoder`) - Implement syndrome decoding algorithms +3. **Realtime Decoding** (:code:`cudaq::qec::decoding`) - Enable online error correction on quantum hardware + +These types are meant to be extended by developers to provide new error correcting codes and decoding strategies. + +The pages below document each of these interfaces in depth — the abstraction, how to extend it, and what ships built in. For runnable, copy-pasteable programs, see the :doc:`CUDA-Q QEC examples `. + +.. toctree:: + :maxdepth: 2 + + QEC Codes + QEC Decoders + Realtime Decoding + Experiments and Noise Modeling + Conventions diff --git a/docs/sphinx/components/qec/introduction.rst b/docs/sphinx/components/qec/introduction.rst deleted file mode 100644 index 9e7ab32da..000000000 --- a/docs/sphinx/components/qec/introduction.rst +++ /dev/null @@ -1,1545 +0,0 @@ -CUDA-Q QEC - Quantum Error Correction Library -============================================= - -Overview --------- -The ``cudaq-qec`` library provides a comprehensive framework for quantum -error correction research and development. It leverages GPU acceleration -for efficient syndrome decoding and error correction simulations (coming soon). - -The library supports both offline analysis and real-time error correction on quantum hardware, -enabling low-latency decoding for practical quantum computing applications. - -Core Components ----------------- -``cudaq-qec`` is composed of three main interfaces: - -1. **QEC Codes** (:code:`cudaq::qec::code`) - Define quantum error correcting codes with logical operations -2. **Decoders** (:code:`cudaq::qec::decoder`) - Implement syndrome decoding algorithms -3. **Real-Time Decoding** (:code:`cudaq::qec::decoding`) - Enable online error correction on quantum hardware - -These types are meant to be extended by developers to provide new error correcting codes and decoding strategies. - -QEC Code Framework :code:`cudaq::qec::code` -------------------------------------------- - -The :code:`cudaq::qec::code` class serves as the base class for all quantum error correcting codes in CUDA-Q QEC. It provides -a flexible extension point for implementing new codes and defines the core interface that all QEC codes must support. - -The core abstraction here is that of a mapping or dictionary of logical operations to their -corresponding physical implementation in the error correcting code as CUDA-Q quantum kernels. - -Class Structure -^^^^^^^^^^^^^^^ - -The code base class provides: - -1. **Operation Enumeration**: Defines supported logical operations - - .. code-block:: cpp - - enum class operation { - x, // Logical X gate - y, // Logical Y gate - z, // Logical Z gate - h, // Logical Hadamard gate - s, // Logical S gate - cx, // Logical CNOT gate - cy, // Logical CY gate - cz, // Logical CZ gate - stabilizer_round, // Stabilizer measurement round - prep0, // Prepare |0⟩ state - prep1, // Prepare |1⟩ state - prepp, // Prepare |+⟩ state - prepm // Prepare |-⟩ state - }; - - -2. **Patch Type**: Defines the structure of a logical qubit patch - - .. code-block:: cpp - - struct patch { - cudaq::qview<> data; // View of data qubits - cudaq::qview<> ancx; // View of X stabilizer ancilla qubits - cudaq::qview<> ancz; // View of Z stabilizer ancilla qubits - }; - - The `patch` type represents a logical qubit in quantum error correction codes. It contains: - - `data`: A view of the data qubits in the patch - - `ancx`: A view of the ancilla qubits used for X stabilizer measurements - - `ancz`: A view of the ancilla qubits used for Z stabilizer measurements - - This structure is designed for use within CUDA-Q kernel code and provides a - convenient way to access different qubit subsets within a logical qubit patch. - - -3. **Kernel Type Aliases**: Defines quantum kernel signatures - - .. code-block:: cpp - - using one_qubit_encoding = cudaq::qkernel; - using two_qubit_encoding = cudaq::qkernel; - using stabilizer_round = cudaq::qkernel( - patch, const std::vector&, const std::vector&)>; - - The two vector arguments of :code:`stabilizer_round` are the flattened X and - Z stabilizer *schedule* matrices, which can encode an optimized gate order - on top of the parity-check support. See - :cpp:func:`cudaq::qec::code::get_stabilizer_schedule_x` for the encoding - and the default (the plain parity matrices). - -4. **Protected Members**: - - - :code:`operation_encodings`: Maps operations to their quantum kernel implementations. The key is the ``operation`` enum and the value is a variant on the above kernel type aliases. - - :code:`m_stabilizers`: Stores the code's stabilizer generators - -Implementing a New Code -^^^^^^^^^^^^^^^^^^^^^^^ - -To implement a new quantum error correcting code: - -1. **Create a New Class**: - - .. code-block:: cpp - - class my_code : public qec::code { - protected: - // Implement required virtual methods - public: - my_code(const heterogeneous_map& options); - }; - -2. **Implement Required Virtual Methods**: - - .. code-block:: cpp - - // Number of physical data qubits - std::size_t get_num_data_qubits() const override; - - // Total number of ancilla qubits - std::size_t get_num_ancilla_qubits() const override; - - // Number of X-type ancilla qubits - std::size_t get_num_ancilla_x_qubits() const override; - - // Number of Z-type ancilla qubits - std::size_t get_num_ancilla_z_qubits() const override; - -3. **Define Quantum Kernels**: - - Create CUDA-Q kernels for each logical operation: - - .. code-block:: cpp - - __qpu__ void x(patch p) { - // Implement logical X - } - - __qpu__ std::vector stabilizer(patch p, - const std::vector& x_stabs, - const std::vector& z_stabs) { - // Implement stabilizer measurements - } - - .. note:: - - The two vector arguments passed to the :code:`stabilizer_round` kernel - are the flattened X and Z stabilizer *schedule* matrices returned by - :cpp:func:`cudaq::qec::code::get_stabilizer_schedule_x` and - :cpp:func:`cudaq::qec::code::get_stabilizer_schedule_z`. By default - these equal the plain parity-check matrices (every entry 0 or 1), but a - code can override the :code:`get_stabilizer_schedule_*` methods to - encode a gate order, in which case entry :code:`k >= 1` means the - interaction executes at timestep :code:`k` (the built-in - :code:`surface_code` does this to avoid hook errors). A kernel that only - needs the support pattern should therefore test entries for - :code:`!= 0` rather than :code:`== 1`. - -4. **Register Operations**: - - In the constructor, register quantum kernels for each operation: - - .. code-block:: cpp - - my_code::my_code(const heterogeneous_map& options) : code() { - // Register operations - operation_encodings.insert( - std::make_pair(operation::x, x)); - operation_encodings.insert( - std::make_pair(operation::stabilizer_round, stabilizer)); - - // Define stabilizer generators - m_stabilizers = qec::stabilizers({"XXXX", "ZZZZ"}); - } - - - Note that in your constructor, you have access to user-provided ``options``. For - example, if your code depends on an integer parameter called ``distance``, you can - retrieve that from the user via - - .. code-block:: cpp - - my_code::my_code(const heterogeneous_map& options) : code() { - // ... fill the map and stabilizers ... - - // Get the user-provided distance, or just - // set to 3 if user did not provide one - this->distance = options.get("distance", /*defaultValue*/ 3); - } - -5. **Register Extension Point**: - - Add extension point registration: - - .. code-block:: cpp - - CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION( - my_code, - static std::unique_ptr create( - const heterogeneous_map &options) { - return std::make_unique(options); - } - ) - - CUDAQ_EXT_PT_REGISTER_TYPE(my_code) - -Example: Steane Code -^^^^^^^^^^^^^^^^^^^^^ - -The Steane [[7,1,3]] code provides a complete example implementation: - -1. **Header Definition**: - - - Declares quantum kernels for all logical operations - - Defines the code class with required virtual methods - - Specifies 7 data qubits and 6 ancilla qubits (3 X-type, 3 Z-type) - -2. **Implementation**: - - .. code-block:: cpp - - steane::steane(const heterogeneous_map &options) : code() { - // Register all logical operations - operation_encodings.insert( - std::make_pair(operation::x, x)); - // ... register other operations ... - - // Define stabilizer generators - m_stabilizers = qec::stabilizers({ - "XXXXIII", "IXXIXXI", "IIXXIXX", - "ZZZZIII", "IZZIZZI", "IIZZIZZ" - }); - } - -3. **Quantum Kernels**: - - Implements fault-tolerant logical operations: - - .. code-block:: cpp - - __qpu__ void x(patch logicalQubit) { - // Apply logical X to specific data qubits - x(logicalQubit.data[4], logicalQubit.data[5], - logicalQubit.data[6]); - } - - __qpu__ std::vector stabilizer(patch logicalQubit, - const std::vector& x_stabilizers, - const std::vector& z_stabilizers) { - // Measure X stabilizers - h(logicalQubit.ancx); - // ... apply controlled-X gates ... - h(logicalQubit.ancx); - - // Measure Z stabilizers - // ... apply controlled-X gates ... - - // Return measurement results - return mz(logicalQubit.ancz, logicalQubit.ancx); - } - -Implementing a New Code in Python -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -CUDA-Q QEC supports implementing quantum error correction codes in Python -using the :code:`@qec.code` decorator. This provides a more accessible way -to prototype and develop new codes. - -1. **Create a New Python File**: - - Create a new file (e.g., :code:`my_steane.py`) with your code implementation: - - .. literalinclude:: ../../examples/qec/python/my_steane.py - :language: python - :start-after: [Begin Documentation1] - :end-before: [End Documentation1] - -2. **Define Quantum Kernels**: - - Implement the required quantum kernels using the :code:`@cudaq.kernel` decorator: - - .. literalinclude:: ../../examples/qec/python/my_steane.py - :language: python - :start-after: [Begin Documentation2] - :end-before: [End Documentation2] - - .. note:: - - The kernel registered for :code:`stabilizer_round` must be annotated to - return :code:`list[cudaq.measure_handle]`. - - .. note:: - - As in C++, the two list arguments passed to the - :code:`stabilizer_round` kernel are the flattened X and Z stabilizer - schedule matrices, which default to the parity-check matrices. A Python - code can optionally define :code:`get_stabilizer_schedule_x` / - :code:`get_stabilizer_schedule_z` methods returning a 2D array with the - same shape and support pattern as the corresponding parity-check - matrix, where entry :code:`k >= 1` schedules that interaction at - timestep :code:`k`. - -3. **Implement the Code Class**: - - Create a class decorated with :code:`@qec.code` that implements the required interface: - - .. literalinclude:: ../../examples/qec/python/my_steane.py - :language: python - :start-after: [Begin Documentation3] - :end-before: [End Documentation3] - -4. **Using the Code**: - - The code can now be used like any other CUDA-Q QEC code: - - .. literalinclude:: ../../examples/qec/python/my_steane_test.py - :language: python - :start-after: [Begin Documentation] - -Key Points -^^^^^^^^^^^ - -* The :code:`@qec.code` decorator takes the name of the code as an argument -* Operation encodings are registered via the :code:`operation_encodings` dictionary -* Stabilizer generators are defined using the :code:`qec.Stabilizers` class -* The code must implement all required methods from the base class interface - - -Using the Code Framework -^^^^^^^^^^^^^^^^^^^^^^^^^ - -To use an implemented code: - -.. tab:: Python - - .. code-block:: python - - # Create a code instance - code = qec.get_code("steane") - - # Access stabilizer information - stabilizers = code.get_stabilizers() - parity = code.get_parity() - - # The code can now be used for various numerical - # experiments - see section below. - -.. tab:: C++ - - .. code-block:: cpp - - // Create a code instance - auto code = cudaq::qec::get_code("steane"); - - // Access stabilizer information - auto stabilizers = code->get_stabilizers(); - auto parity = code->get_parity(); - - // The code can now be used for various numerical - // experiments - see section below. - - -Pre-built QEC Codes -------------------- - -CUDA-Q QEC provides several well-studied quantum error correction codes out of the box. Here's a detailed overview of each: - -Steane Code -^^^^^^^^^^^ - -The Steane code is a ``[[7,1,3]]`` CSS (Calderbank-Shor-Steane) code that encodes -one logical qubit into seven physical qubits with a code distance of 3. - -**Key Properties**: - -* Data qubits: 7 -* Encoded qubits: 1 -* Code distance: 3 -* Ancilla qubits: 6 (3 for X stabilizers, 3 for Z stabilizers) - -**Stabilizer Generators**: - -* X-type: ``["XXXXIII", "IXXIXXI", "IIXXIXX"]`` -* Z-type: ``["ZZZZIII", "IZZIZZI", "IIZZIZZ"]`` - -The Steane code can correct any single-qubit error and detect up to two errors. -It is particularly notable for being the smallest CSS code that can implement a universal set of transversal gates. - -Usage: - -.. tab:: Python - - .. code-block:: python - - import cudaq_qec as qec - - # Create Steane code instance - steane = qec.get_code("steane") - -.. tab:: C++ - - .. code-block:: cpp - - auto steane = cudaq::qec::get_code("steane"); - -Repetition Code -^^^^^^^^^^^^^^^ -The repetition code is a simple [[n,1,n]] code that protects against -bit-flip (X) errors by encoding one logical qubit into n physical qubits, where n is the code distance. - -**Key Properties**: - -* Data qubits: n (distance) -* Encoded qubits: 1 -* Code distance: n -* Ancilla qubits: n-1 (all for Z stabilizers) - -**Stabilizer Generators**: - -* For distance 3: ``["ZZI", "IZZ"]`` -* For distance 5: ``["ZZIII", "IZZII", "IIZZI", "IIIZZ"]`` - -The repetition code is primarily educational as it can only correct -X errors. However, it serves as an excellent introduction to QEC concepts. - -Usage: - -.. tab:: Python - - .. code-block:: python - - import cudaq_qec as qec - - # Create distance-3 repetition code - code = qec.get_code('repetition', distance=3) - - # Access stabilizers - stabilizers = code.get_stabilizers() # Returns ["ZZI", "IZZ"] - -.. tab:: C++ - - .. code-block:: cpp - - auto code = qec::get_code("repetition", {{"distance", 3}}); - - // Access stabilizers - auto stabilizers = code->get_stabilizers(); - -Surface Code -^^^^^^^^^^^^ - -The library provides a **rotated surface code** on a two-dimensional qubit -layout with **open boundaries** (a single patch). It is a **CSS** code—:math:`X` -and :math:`Z` errors are handled in separate CSS sectors—encoding **one logical -qubit** into :math:`d^2` data qubits with code distance :math:`d` in this -layout. Stabilizers have weight four in the bulk and weight two on the boundary, following the grid convention -described in `Towards a Standardized Definition of Quantum Circuits for Quantum -Error Correction with Rotated Surface Codes -`__. - -**Key Properties** (distance :math:`d` in this implementation): - -* Data qubits: :math:`d^2` -* Encoded logical qubits: 1 -* Code distance: :math:`d` -* Stabilizers: :math:`d^2 - 1` total—:math:`(d^2 - 1) / 2` :math:`X`-type and - :math:`(d^2 - 1) / 2` :math:`Z`-type. The :code:`patch` type assigns **one - ancilla per stabilizer** measurement, so the ancilla count matches the - stabilizer count here; other hardware layouts could fold or share ancillas - differently. - -**Stabilizer Generators** (example: ``distance`` :math:`= 3`) - -Data qubits are indexed in row-major order (left to right, top to bottom); the -leftmost character of each Pauli string is qubit ``0``, matching the rest of this -document. For :math:`d = 3` there are nine data qubits: - -:: - - d0 d1 d2 - d3 d4 d5 - d6 d7 d8 - -* X-type (weight 2 on the left and right boundaries, weight 4 in the bulk): - - * ``XIIXIIIII`` - * ``IXXIXXIII`` - * ``IIIXXIXXI`` - * ``IIIIIXIIX`` - -* Z-type (weight 2 on the top and bottom boundaries, weight 4 in the bulk): - - * ``IZZIIIIII`` - * ``ZZIZZIIII`` - * ``IIIIZZIZZ`` - * ``IIIIIIZZI`` - -These Pauli words are exactly those used internally for :math:`d=3`; -:code:`get_stabilizers()` returns the same generators in a canonical sorted order -(rather than grouped as X-type then Z-type). - -For other distances, stabilizer supports are generated from the same rotated -grid; use :ref:`stabilizer_grid ` or -:code:`get_stabilizers()` to inspect them. - -You must pass ``distance`` when constructing the code; there is no default. - -**Orientation** - -The surface code accepts an optional ``orientation`` string that selects which -Pauli type occupies the bulk checkerboard and which boundary pair carries the -X- versus Z-type stabilizers. The first character (``X`` or ``Z``) sets the bulk -type; the second character (``H`` or ``V``) sets the boundary placement. Valid -values are ``"XV"``, ``"XH"``, ``"ZV"``, and ``"ZH"`` (aliases ``"O1"``, ``"O2"``, -``"O3"``, and ``"O4"`` respectively; case-insensitive). The default is ``"ZH"``, -which reproduces the layout described above. The logical observables and the CNOT -extraction schedule are orientation-aware, so changing the orientation changes the -returned stabilizers, observables, and measurement schedule consistently. - -The :ref:`stabilizer_grid ` helper documents how -stabilizers and data qubits are indexed on the grid and provides helpers to -print the layout. **Python:** :ref:`cudaq_qec.stabilizer_grid ` — **C++:** -:cpp:class:`cudaq::qec::surface_code::stabilizer_grid` — see -:ref:`API `. The header :file:`cudaq/qec/codes/surface_code.h` -contains the full declaration. - -**Stabilizer measurement schedule** - -The surface code's :code:`stabilizer_round` kernel executes one depth-4 -extraction round: the X- and Z-check CNOTs are interleaved over four shared -timesteps. Within each plaquette the CNOT order follows the standard zigzag -schedule for the rotated surface code (`Tomita & Svore -`__): the X and Z plaquettes traverse their -corners in transposed orders, selected per orientation so that mid-round -ancilla faults ("hook errors", `Dennis et al. -`__) propagate onto data-qubit pairs -perpendicular to the same-type logical operator. This preserves the full code -distance :math:`d` under circuit-level noise; a naive schedule (both plaquette -types in ascending qubit-index order) halves the effective distance of one -memory basis. The schedule is available from the :code:`stabilizer_grid` -helper via -:cpp:func:`~cudaq::qec::surface_code::stabilizer_grid::get_cnot_schedule_x` / -:code:`get_cnot_schedule_z` (matrix form) and -:code:`get_cnot_schedule_pairs_x` / :code:`get_cnot_schedule_pairs_z` (flat -pair-list form). - -Usage: - -.. tab:: Python - - .. code-block:: python - - import cudaq_qec as qec - - # Rotated surface code; distance is required - code = qec.get_code('surface_code', distance=3) # default orientation "ZH" - - # Optionally select an orientation (one of "XV", "XH", "ZV", "ZH") - code_xh = qec.get_code('surface_code', distance=3, orientation='XH') - - stabilizers = code.get_stabilizers() - parity = code.get_parity() - -.. tab:: C++ - - .. code-block:: cpp - - auto code = cudaq::qec::get_code( - "surface_code", cudaqx::heterogeneous_map{{"distance", 3}}); - - // Optionally select an orientation (one of "XV", "XH", "ZV", "ZH") - auto code_xh = cudaq::qec::get_code( - "surface_code", cudaqx::heterogeneous_map{ - {"distance", 3}, - {"orientation", std::string("XH")}}); - - auto stabilizers = code->get_stabilizers(); - auto parity = code->get_parity(); - - -Decoder Framework :code:`cudaq::qec::decoder` ----------------------------------------------- - -The CUDA-Q QEC decoder framework provides an extensible system for implementing -quantum error correction decoders through the :code:`cudaq::qec::decoder` base class. - -Class Structure -^^^^^^^^^^^^^^^ - -The decoder base class defines the core interface for syndrome decoding: - -.. code-block:: cpp - - class decoder { - protected: - std::size_t block_size; // For [n,k] code, this is n - std::size_t syndrome_size; // For [n,k] code, this is n-k - sparse_binary_matrix H; // Parity check matrix - - public: - struct decoder_result { - bool converged; // Decoder convergence status - std::vector result; // Soft error probabilities - }; - - virtual decoder_result decode( - const std::vector& syndrome) = 0; - - virtual std::vector decode_batch( - const std::vector>& syndrome); - }; - -Key Components: - -* **Parity Check Matrix**: Defines the code structure via the sparse :code:`H` member -* **Block Size**: Number of physical qubits in the code -* **Syndrome Size**: Number of stabilizer measurements -* **Decoder Result**: Contains convergence status and error probabilities -* **Multiple Decoding Modes**: Single syndrome or batch processing - -Implementing a New Decoder in C++ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To implement a new decoder: - -1. **Create Decoder Class**: - -.. code-block:: cpp - - class my_decoder : public qec::decoder { - private: - // Decoder-specific members - - public: - my_decoder(const qec::sparse_binary_matrix& H, - const heterogeneous_map& params) - : decoder(H) { - // Initialize decoder - } - - decoder_result decode( - const std::vector& syndrome) override { - // Implement decoding logic - } - }; - -2. **Register Extension Point**: - -.. code-block:: cpp - - CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION( - my_decoder, - static std::unique_ptr create( - const qec::decoder_init& init, - const heterogeneous_map& params) { - return qec::make_pcm_decoder(init, params); - } - ) - - CUDAQ_EXT_PT_REGISTER_TYPE(my_decoder) - -The :code:`make_pcm_decoder` helper dispatches :code:`decoder_init`. It -passes a stored sparse PCM directly to the decoder constructor; when the -variant contains Stim DEM text, it parses the DEM and constructs the sparse -detector matrix before invoking the same constructor. - -Example: Lookup Table Decoder -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Here's a simple lookup table decoder for the Steane code: - -.. code-block:: cpp - - class single_error_lut : public decoder { - private: - std::map single_qubit_err_signatures; - - public: - single_error_lut(const qec::sparse_binary_matrix& H, - const heterogeneous_map& params) - : decoder(H) { - // Canonicalize before using each sparse column as an error - // signature so duplicate row indices cancel over GF(2). - auto H_e2d = H.canonicalize().to_nested_csc(); - - for (std::size_t qErr = 0; qErr < block_size; qErr++) { - std::string err_sig(syndrome_size, '0'); - for (std::uint32_t row : H_e2d[qErr]) - err_sig[row] = '1'; - single_qubit_err_signatures.insert({err_sig, qErr}); - } - } - - decoder_result decode( - const std::vector& syndrome) override { - decoder_result result{false, - std::vector(block_size, 0.0)}; - - // Convert syndrome to string - std::string syndrome_str(syndrome_size, '0'); - for (std::size_t i = 0; i < syndrome_size; i++) - syndrome_str[i] = (syndrome[i] >= 0.5) ? '1' : '0'; - - // Lookup error location - auto it = single_qubit_err_signatures.find(syndrome_str); - if (it != single_qubit_err_signatures.end()) { - result.converged = true; - result.result[it->second] = 1.0; - } - - return result; - } - }; - -Implementing a Decoder in Python -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -CUDA-Q QEC supports implementing decoders in Python using the :code:`@qec.decoder` decorator: - -1. **Create Decoder Class**: - -.. code-block:: python - - @qec.decoder("my_decoder") - class MyDecoder: - def __init__(self, H, **kwargs): - # H is a scipy.sparse matrix or a dense numpy uint8 array, - # mirroring whatever was passed to qec.get_decoder(). - # Pass it unchanged to Decoder.__init__ so the C++ base class - # stores a compact sparse representation without a dense allocation. - qec.Decoder.__init__(self, H) - self.H = H - # Initialize with optional kwargs - - def decode(self, syndrome): - # Create result object - result = qec.DecoderResult() - - # Implement decoding logic - # ... - - # Set results - result.converged = True - result.result = [0.0] * self.block_size - - return result - -2. **Using Custom Parameters**: - -.. code-block:: python - - # Create decoder with custom parameters - decoder = qec.get_decoder("my_decoder", - H=parity_check_matrix, - custom_param=42) - -Key Features -^^^^^^^^^^^^^ - -* **Soft Decision Decoding**: Results are probabilities in [0,1] -* **Batch Processing**: Support for decoding multiple syndromes -* **Asynchronous Decoding**: Optional async interface for parallel processing -* **Custom Parameters**: Flexible configuration via heterogeneous_map -* **Python Integration**: First-class support for Python implementations - -Usage Example -^^^^^^^^^^^^^^ - -.. tab:: Python - - .. code-block:: python - - import cudaq_qec as qec - - # Get a code instance - steane = qec.get_code("steane") - - # Create decoder with code's parity matrix - decoder = qec.get_decoder('single_error_lut', steane.get_parity()) - - # Run stabilizer measurements - syndromes, dataQubitResults = qec.sample_memory_circuit(steane, numShots=1, numRounds=1) - - # Decode a syndrome - result = decoder.decode(syndromes[0]) - if result.converged: - print("Error locations:", - [i for i,p in enumerate(result.result) if p > 0.5]) - # No errors as we did not include a noise model and - # thus prints: - # Error locations: [] - -.. tab:: C++ - - .. code-block:: cpp - - using namespace cudaq; - - // Get a code instance - auto code = qec::get_code("steane"); - - // Create decoder with code's parity matrix - auto decoder = qec::get_decoder("single_error_lut", - code->get_parity()); - - // Run stabilizer measurements - auto [syndromes, dataQubitResults] = qec::sample_memory_circuit(*code, /*numShots*/numShots, /*numRounds*/ 1); - - // Decode syndrome - auto result = decoder->decode(syndromes[0]); - - -DEM Sampling -^^^^^^^^^^^^ - -The ``dem_sampling`` function samples errors and syndromes from a detector error -model (DEM). Given a binary check matrix :math:`H` of shape -``[num_checks x num_error_mechanisms]`` and a vector of per-mechanism Bernoulli -probabilities, it generates random error vectors and computes -:math:`\text{syndromes} = \text{errors} \cdot H^T \pmod{2}`. - -In Python, the ``backend`` parameter (``"auto"``, ``"gpu"``, or ``"cpu"``) -controls whether sampling runs on the GPU via cuStabilizer or on the CPU. The -function accepts NumPy arrays and PyTorch CUDA tensors. In C++ the CPU and GPU -paths live in separate namespaces (``cudaq::qec::dem_sampler::cpu`` and -``cudaq::qec::dem_sampler::gpu``). - -.. tab:: Python - - .. code-block:: python - - import cudaq_qec as qec - import numpy as np - - H = np.array([[1, 1, 0], - [0, 1, 1]], dtype=np.uint8) - error_probs = np.array([0.05, 0.10, 0.05]) - - # backend="auto": GPU when available, else CPU - syndromes, errors = qec.dem_sampling( - H, num_shots=1000, error_probabilities=error_probs, seed=42) - # syndromes: uint8 [1000 x 2], errors: uint8 [1000 x 3] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/dem_sampling.cpp - :language: cpp - :start-after: [Begin Documentation] - - Compile and run with - - .. code-block:: bash - - nvq++ -lcudaq-qec dem_sampling.cpp - ./a.out - -For a complete walkthrough including GPU acceleration, input type handling, and -backend selection details, see the -:doc:`DEM Sampling example `. - - -Dynamic DEM Construction -^^^^^^^^^^^^^^^^^^^^^^^^ - -When a Stim circuit is not available — or when the round count must stay -flexible until decoder construction — build DEMs from CSS generator matrices -and compose them as per-round chunks: - -* ``dem_from_css_matrices`` — :math:`T`-round code-capacity / phenomenological - DEM from ``CssCodes`` / ``css_code_matrices`` and ``CssNoise`` / - ``css_noise_params``. -* ``extended_dem_from_css_matrices``, ``dem_stitch`` / ``dem_close_all`` — - one-round chunks that stitch and close to the same flat DEM. -* YAML ``dem_chunks`` with optional ``num_rounds`` inside — declarative - init / bulk / final phases for real-time decoder configs; expanded by - ``expand_dem_chunks``. Omit ``num_rounds`` for streaming decoders. - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/dyn_dem.py - :language: python - :start-after: [Begin Documentation] - :end-before: [End Documentation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/dyn_dem.cpp - :language: cpp - :start-after: [Begin Documentation] - :end-before: [End Documentation] - - Compile and run with - - .. code-block:: bash - - nvq++ -lcudaq-qec -lcudaq-qec-decoders dyn_dem.cpp - ./a.out - -See the :doc:`Dynamic DEM Construction example ` -for phase specs, YAML ``dem_chunks``, merge semantics, and closing rules. - - -Pre-built QEC Decoders ----------------------- - -CUDA-Q QEC provides pre-built decoders for a variety of use cases. - -+------------------------+-----------------------------+----------+----------+-------------------+--------------------------------------------------+ -| Decoder | Decoder String Identifier | Python | C++ | Real-Time Enabled | Notes | -+========================+=============================+==========+==========+===================+==================================================+ -| NVIDIA QLDPC Decoder¹ | `"nv-qldpc-decoder"` | Yes | Yes | Yes | Supports Relay BP and BP+OSD | -+------------------------+-----------------------------+----------+----------+-------------------+--------------------------------------------------+ -| Tensor Network Decoder¹| `"tensor_network_decoder"` | Yes² | No | No | Exact Maximum Likelihood Decoder | -+------------------------+-----------------------------+----------+----------+-------------------+--------------------------------------------------+ -| TensorRT Decoder¹ | `"trt_decoder"` | Yes³ | Yes | Not yet | AI decoder. Bring your own model. | -+------------------------+-----------------------------+----------+----------+-------------------+--------------------------------------------------+ -| Look-Up Table Decoder | `"single_error_lut"` | Yes | Yes | Yes | Simple decoder with no configurable options | -+ +-----------------------------+----------+----------+-------------------+--------------------------------------------------+ -| | `"multi_error_lut"` | Yes | Yes | Yes | Multi-error decoder that | -| | | | | | can handle up to "lut_error_depth" errors | -+------------------------+-----------------------------+----------+----------+-------------------+--------------------------------------------------+ -| Sliding Window Decoder | `"sliding_window"` | Yes | Yes | Not yet | Decodes syndromes in a sliding window fashion. | -| | | | | | May be paired with any other decoder as an | -| | | | | | inner decoder except Tensor RT Decoder | -+------------------------+-----------------------------+----------+----------+-------------------+--------------------------------------------------+ - -| ¹ GPU-accelerated decoder -| ² Requires installation with `pip install cudaq-qec[tensor-network-decoder]` for Python -| ³ Requires installation with `pip install cudaq-qec[trt-decoder]` for Python - -Here's a detailed overview of each: - -Quantum Low-Density Parity-Check Decoder -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Quantum Low-Density Parity-Check (QLDPC) decoder leverages GPU-accelerated belief propagation (BP) for efficient error correction. -Since belief propagation is an iterative method which may not converge, decoding can be improved with a second-stage post-processing step. The `nv-qldpc-decoder` -API provides various post-processing options, which can be selected through its parameters. - -**Belief Propagation Methods:** - -The decoder supports multiple BP algorithms (configured via ``bp_method``): - -* **Sum-Product BP** (``bp_method=0``, default): Classic belief propagation algorithm that computes exact probabilities. -* **Min-Sum BP** (``bp_method=1``): Approximation to sum-product that uses min operations instead of sum. Optionally accepts ``scale_factor``. -* **Memory-based BP** (``bp_method=2``): Min-sum with uniform memory strength across all variable nodes. **Requires:** ``gamma0``. -* **Disordered Memory BP** (``bp_method=3``): Min-sum with per-variable memory strengths. **Requires:** ``gamma_dist`` [min, max] OR ``explicit_gammas`` (2D vector). - -**Sequential Relay Decoding:** - -Starting with version 0.5.0, the decoder supports Sequential Relay BP (configured via ``composition=1``), which combines disordered memory BP -with multiple "relay legs" - sequential runs with different gamma configurations. **Requires:** ``bp_method=3``, ``gamma0``, ``srelay_config``, and either ``gamma_dist`` OR ``explicit_gammas``. - -The QLDPC decoder `nv-qldpc-decoder` requires a CUDA-Q compatible GPU. See the list below for dependencies and compatibility: -https://nvidia.github.io/cuda-quantum/latest/using/install/local_installation.html#dependencies-and-compatibility - -The decoder is based on the following references: - -* https://arxiv.org/pdf/2005.07016 -* https://github.com/quantumgizmos/ldpc -* https://arxiv.org/pdf/2506.01779 -* https://github.com/trmue/relay - - -Usage: - -.. tab:: Python - - .. code-block:: python - - import cudaq_qec as qec - import numpy as np - - H_list = [ - [1, 0, 0, 1, 0, 1, 1], - [0, 1, 0, 1, 1, 0, 1], - [0, 0, 1, 0, 1, 1, 1] - ] - - H_np = np.array(H_list, dtype=np.uint8) - - decoder = qec.get_decoder("nv-qldpc-decoder", H_np) - -.. tab:: C++ - - .. code-block:: cpp - - std::size_t block_size = 7; - std::size_t syndrome_size = 3; - cudaqx::tensor H; - - std::vector H_vec = {1, 0, 0, 1, 0, 1, 1, - 0, 1, 0, 1, 1, 0, 1, - 0, 0, 1, 0, 1, 1, 1}; - H.copy(H_vec.data(), {syndrome_size, block_size}); - - cudaqx::heterogeneous_map nv_custom_args; - nv_custom_args.insert("use_osd", true); - - auto d1 = cudaq::qec::get_decoder("nv-qldpc-decoder", H, nv_custom_args); - - // Alternatively, configure the decoder without instantiating a heterogeneous_map - auto d2 = cudaq::qec::get_decoder("nv-qldpc-decoder", H, {{"use_osd", true}, {"bp_batch_size", 100}}); - -Tensor Network Decoder -^^^^^^^^^^^^^^^^^^^^^^ - -The ``tensor_network_decoder`` constructs a tensor network representation of a quantum code given its parity check matrix, logical observable(s), and noise model. It can decode individual syndromes or batches of syndromes, returning the probability that a logical observable has flipped. - -Due to the additional dependencies of the Tensor Network Decoder, you must -specify the optional pip package when installing CUDA-Q QEC in order to use this -decoder. Use `pip install cudaq-qec[tensor-network-decoder]` in order to use -this decoder. - -Key Steps: - -1. **Define the parity check matrix**: This matrix encodes the structure of the quantum code. In the example, a simple [3,1] repetition code is used. - -2. **Specify the logical observable**: This is typically a row vector indicating which qubits participate in the logical operator. - -3. **Set the noise model**: The example uses a factorized noise model with independent bit-flip probability for each error mechanism. - -4. **Instantiate the decoder**: Create a decoder object using ``qec.get_decoder("tensor_network_decoder", ...)`` with the code parameters. - -5. **Decode syndromes**: Use the ``decode`` method for single syndromes or ``decode_batch`` for multiple syndromes. - - -Usage: - -.. tab:: Python - - .. code-block:: python - - # This example demonstrates how to use the get_decoder("tensor_network_decoder", ...) API - # from the ``cudaq_qec`` library to decode syndromes for a simple - # quantum error-correcting code using tensor networks. - - import cudaq_qec as qec - import numpy as np - - # Define code parameters - H = np.array([[1, 1, 0], [0, 1, 1]], dtype=np.uint8) - logical_obs = np.array([[1, 1, 1]], dtype=np.uint8) - noise_model = [0.1, 0.1, 0.1] - - decoder = qec.get_decoder("tensor_network_decoder", H, logical_obs=logical_obs, noise_model=noise_model) - - # Decode a single syndrome - syndrome = [0.0, 1.0] - result = decoder.decode(syndrome) - print(result.result) - - # Decode a batch of syndromes - syndrome_batch = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], dtype=np.float32) - batch_results = decoder.decode_batch(syndrome_batch) - for res in batch_results: - print(res.result) - -.. tab:: C++ - - The ``tensor_network_decoder`` is a Python-only implementation and it requires Python 3.11 or higher. C++ APIs are not available for this decoder. - -Output: - -The decoder returns the probability that the logical observable has flipped for each syndrome. This can be used to assess the performance of the code and the decoder under different error scenarios. - -.. note:: - - In general, the Tensor Network Decoder has the same GPU support as the - `Quantum Low-Density Parity-Check Decoder `__. - However, if you are using the V100 GPU (SM70), you will need to pin your - cuTensor version to 2.2 by running `pip install cutensor_cu12==2.2`. Note - that this GPU will not be supported by the Tensor Network Decoder when - CUDA-Q 0.5.0 is released. - - -Sliding Window Decoder -^^^^^^^^^^^^^^^^^^^^^^ - -Sliding-window decoding handles **circuit-level noise** across several syndrome -rounds by processing syndromes **before the full measurement sequence arrives**, -which **reduces latency** at the cost of **higher logical error rates** than -decoding the entire sequence at once. - -Whether that tradeoff is worthwhile depends on the **noise model**, **code -parameters**, and **latency budget**. Since **CUDA-Q 0.5.0**, you can use **any -CUDA-Q decoder** as the **inner** decoder and tune behavior mainly via **window -size** and the other settings below. Each round must yield the **same -number of syndrome measurements**; the decoder assumes **no particular temporal -structure** of the noise, so you can still vary noise **from round to round** in -experiments. - -Key Steps: - -1. **Obtain a detector error matrix and rates**: Pass the parity check matrix - ``H`` (for example ``dem.detector_error_matrix``) and ``error_rate_vec`` with - one entry per column of ``H`` (for example ``dem.error_rates`` from the same - DEM). The matrix must be in the sorted form expected by :code:`pcm_is_sorted` - for your ``num_syndromes_per_round``; DEMs from :code:`dem_from_memory_circuit` - (and its single-basis variants :code:`z_dem_from_memory_circuit` / - :code:`x_dem_from_memory_circuit`) are canonicalized. Hand-built matrices may - need :code:`simplify_pcm`. -2. **Set the schedule and window**: Provide ``num_syndromes_per_round`` (the number of - syndrome measurements per round) and ``num_boundary_syndromes`` (the number of - stabilizer syndromes fixed by the state-prep at the beginning and end of the circuit). - Choose ``window_size`` and ``step_size`` so ``window_size`` and - ``step_size`` stay within valid bounds and ``num_rounds - window_size`` is - divisible by ``step_size``, with ``num_rounds`` inferred from ``H`` and - ``num_syndromes_per_round``. -3. **Pick an inner decoder**: Use ``inner_decoder_name`` and - ``inner_decoder_params`` for the decoder that runs inside each window (for - example :code:`nv-qldpc-decoder`). Optional ``straddle_start_round`` / - ``straddle_end_round`` control cross-round mechanisms at window edges. -4. **Construct and run**: Call :code:`get_decoder("sliding_window", H, opts)`, - then ``decode`` or ``decode_batch``. Partial syndromes leave the decoder in an - intermediate state until enough bits arrive; full parameter lists and - behavior are in :doc:`/api/qec/python_api` and :doc:`/api/qec/cpp_api`. - -Background: `Toward Low-latency Iterative Decoding of QLDPC Codes Under Circuit-Level Noise `__. - -Usage: - -.. tab:: Python - - .. code-block:: python - - import cudaq - import cudaq_qec as qec - import numpy as np - - cudaq.set_target('stim') - num_rounds = 5 - code = qec.get_code('surface_code', distance=num_rounds) - noise = cudaq.NoiseModel() - noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.001), 1) - statePrep = qec.operation.prep0 - dem = qec.dem_from_memory_circuit(code, statePrep, num_rounds, noise) - inner_decoder_params = {'use_osd': True, 'max_iterations': 50, 'use_sparsity': True} - opts = { - 'error_rate_vec': np.array(dem.error_rates), - 'window_size': 1, - 'num_syndromes_per_round': code.get_num_z_stabilizers() + code.get_num_x_stabilizers(), - 'num_boundary_syndromes': code.get_num_z_stabilizers(), - 'inner_decoder_name': 'nv-qldpc-decoder', - 'inner_decoder_params': inner_decoder_params, - } - swdec = qec.get_decoder('sliding_window', dem.detector_error_matrix, **opts) - -.. tab:: C++ - - .. code-block:: cpp - - #include "cudaq/qec/code.h" - #include "cudaq/qec/decoder.h" - #include "cudaq/qec/experiments.h" - #include "common/NoiseModel.h" - - int main() { - int num_rounds = 5; - auto code = cudaq::qec::get_code( - "surface_code", cudaqx::heterogeneous_map{{"distance", num_rounds}}); - cudaq::noise_model noise; - noise.add_all_qubit_channel("x", cudaq::depolarization2(0.001), 1); - auto statePrep = cudaq::qec::operation::prep0; - auto dem = cudaq::qec::dem_from_memory_circuit(*code, statePrep, num_rounds, - noise); - auto inner_decoder_params = cudaqx::heterogeneous_map{ - {"use_osd", true}, {"max_iterations", 50}, {"use_sparsity", true}}; - auto opts = cudaqx::heterogeneous_map{ - {"error_rate_vec", dem.error_rates}, - {"window_size", 1}, - {"num_syndromes_per_round", code->get_num_z_stabilizers() + code->get_num_x_stabilizers()}, - {"num_boundary_syndromes", code->get_num_z_stabilizers()}, - {"inner_decoder_name", "nv-qldpc-decoder"}, - {"inner_decoder_params", inner_decoder_params}}; - auto swdec = cudaq::qec::get_decoder("sliding_window", - dem.detector_error_matrix, opts); - return 0; - } - -Output: - -Once a decode step completes, results use the same types as other pre-built -decoders (:class:`cudaq_qec.Decoder` in Python, :cpp:class:`cudaq::qec::decoder` -in C++). - -Real-Time Decoding ------------------- - -CUDA-Q QEC provides real-time decoding capabilities for quantum error correction on actual quantum hardware. -Real-time decoding enables decoders to process syndromes and compute corrections within qubit coherence times, -making active error correction practical for real quantum computers. - -Key Features -^^^^^^^^^^^^ - -* **In-Kernel Operation**: Syndrome decoding within CUDA-Q kernels. -* **Hardware Integration**: Direct integration with quantum hardware backends (`Quantinuum's Helios QPU `_). -* **Simulation Support**: Test real-time workflows locally before deploying to hardware. -* **Multiple Decoder Types**: For real-time decoders, see the table `Pre-built QEC Decoders `__. -* **GPU Acceleration**: Leverage CUDA for high-performance syndrome decoding. - -Note: The real-time decoding interfaces are experimental, and subject to change. Real-time decoding on Quantinuum's Helios-1 device is currently only available to partners and collaborators. Please email QCSupport@quantinuum.com for more information. - -Workflow and Terminology -^^^^^^^^^^^^^^^^^^^^^^^^ - -The real-time decoding workflow involves configuring a decoder (or many) before CUDA-Q kernel launch, and communicating to the decoders with special in-kernel functions. -A decoder is a single software instance of a decoding algorithm, and all its relevant inputs (parity-check matrices, error rates, etc.) which will remain static for the execution of the quantum program. -A decoder config may contain many decoders, each with different algorithms and input parameters. - -In a quantum kernel, a user interacts with the decoders via the `enqueue_syndromes` and `get_corrections` interfaces. -The behavior of these functions depends on their configuration and their usage. - -The real-time decoding workflow can be described with respect to the offline decoding workflow. -The non-real-time decoders require a detector error model which is specified via a detector error matrix which is the parity check matrix `H` of the decoding problem, and a vector of weights (error rates). -This matrix has dimensions of `[numDetectors, numErrors]`, where the each row is a detector, and each column is a possible error. -For real-time decoding, we first need to convert the circuit measurements into detectors. -This is specified via the detector matrix `D`, which has dimensions `[numDetectors, numMeasurements]`. -Each column of the detector matrix defines which detectors a measurement participates in by including an entry of `1`. -This when, once all `numMeasurements` measurements are enqueued, a matrix-vector multiply can convert this buffer of raw measurements into detectors which are then passed into the decoding algorithm. - -Similarly, an observables flips matrix `O` of size `[numObs, numErrors]` must be provided. -Each column of the observables flips matrix describes for each error, which observables are flipped by that error by including an entry of `1`. -Once the decoding algorithm has process the detectors it provides a vector of predicted errors of length `numErrors`. -This vector then executes a matrix-vector multiply with the observables flips matrix to yield a new vector of length `numObs` which contains an entry of `1` if the observable is predicted to have flipped. - -Thus once a decoder is configured, we can view the real-time decoder as a transformation of data starting from a vector of raw measurements, then transformed into detectors via `D`, then error predictions via `H`, then observable flip predictions via `O`. This last step is what is returned via `get_corrections`. The user configures how many bits of information are returned, and what they represent via the `O` matrix in the decoder config. - -Similarly, the user determines how many measurements are needed for the decoder via the `D` matrix in the decoder config, and they are sent to the decoder via `enqueue_syndromes`. -For flexibility, the user can choose to send all measurements with a single `enqueue_syndromes` call, or send them over several calls. -However they are split up, the decoder will not begin decoding until all `numMeasurements` have been enqueued, and will throw an error if too many are sent. -Thus it is the final `enqueue_syndromes` call which kicks off the decoder, and is an asynchronous function. -Additional quantum gates can be applied, and only when `get_corrections` is called does the kernel sync and wait for the corrections. - -For detailed information on real-time decoding, see: - -* :doc:`/examples_rst/qec/realtime_decoding` - Complete Guide with Examples -* :doc:`/examples_rst/qec/realtime_predecoder_pymatching` - Realtime AI Predecoder Pipeline -* :doc:`/examples_rst/qec/realtime_predecoder_fpga` - Realtime AI Predecoder Pipeline with FPGA -* :ref:`realtime_pipeline_api` - Realtime Pipeline C++ API -* :doc:`/examples_rst/qec/realtime_relay_bp` - Relay BP Decoding with CUDA-Q Realtime -* :doc:`/api/qec/cpp_api` - C++ API Reference (see Real-Time Decoding section) -* :doc:`/api/qec/python_api` - Python API Reference (see Real-Time Decoding section) - - - -Numerical Experiments ---------------------- - -CUDA-Q QEC provides utilities for running numerical experiments with quantum error correction codes. - -Conventions -^^^^^^^^^^^ - -To address vectors of qubits (`cudaq::qvector`), CUDAQ indexing starts from 0, and 0 corresponds -to the leftmost position when working with pauli strings (`cudaq::spin_op`). For example, applying a pauli X operator -to qubit 1 out of 7 would be `X_1 = IXIIIII`. - -While implementing your own codes and decoders, you are free to follow any convention that is convenient to you. However, -to interact with the pre-built QEC codes and decoders within this library, the following conventions are used. All of these codes -are CSS codes, and so we separate :math:`X`-type and :math:`Z`-type errors. For example, an error vector for 3 qubits will -have 6 entries, 3 bits representing the presence of a bit-flip on each qubit, and 3 bits representing a phase-flip on each qubit. -An error vector representing a bit-flip on qubit 0, and a phase-flip on qubit 1 would look like `E = 100010`. This means that this -error vector is just two error vectors (`E_X, E_Z`) concatenated together (`E = E_X | E_Z`). - -These errors are detected by stabilizers. :math:`Z`-stabilizers detect :math:`X`-type errors and vice versa. Thus we write our -CSS parity check matrices as - -.. math:: - H_{CSS} = \begin{pmatrix} - H_Z & 0 \\ - 0 & H_X - \end{pmatrix}, - -so that when we generate a syndrome vector by multiplying the parity check matrix by an error vector we get - -.. math:: - \begin{align} - S &= H \cdot E\\ - S_X &= H_Z \cdot E_x\\ - S_Z &= H_X \cdot E_Z. - \end{align} - -This means that for the concatenated syndrome vector `S = S_X | S_Z`, the first part, `S_X`, are syndrome bits triggered by `Z` -stabilizers detecting `X` errors. This is because the `Z` stabilizers like `ZZI` and `IZZ` anti-commute with `X` errors like -`IXI`. - -The decoder prediction as to what error happened is `D = D_X | D_Z`. A successful error decoding does not require that `D = E`, -but that `D + E` is not a logical operator. There are a couple ways to check this. -For bitflip errors, we check that the residual error `R = D_X + E_X` is not `L_X`. Since `X` anticommutes -with `Z`, we can check that `L_Z(D_X + E_X) = 0`. This is because we just need to check if they have mutual support on an even -or odd number of qubits. We could also check that `R` is not a stabilizer. - -Similar to the parity check matrix, the logical observables are also stored in a matrix as - -.. math:: - L = \begin{pmatrix} - L_Z & 0 \\ - 0 & L_X - \end{pmatrix}, - -so that when determining logical errors, we can do matrix multiplication - -.. math:: - \begin{align} - P &= L \cdot R\\ - P_X &= L_Z \cdot R_x\\ - P_Z &= L_X \cdot R_Z. - \end{align} - -Here we're using `P` as this can be stored in a Pauli frame tracker to track observable flips. - -Each logical qubit has logical observables associated with it. Depending on what basis the data qubits are measured in, either the -`X` or `Z` logical observables can be measured. The data qubits which support the logical observable is contained the `qec::code` class as well. - -To do a logical `Z(X)` measurement, measure out all of the data qubits in the `Z(X)` basis. Then check support on the appropriate -`Z(x)` observable. - - -Memory Circuit Experiments -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Memory circuit experiments test a QEC code's ability to preserve quantum information over time by: - -1. Preparing an initial logical state -2. Performing multiple rounds of stabilizer measurements -3. Measuring data qubits to verify state preservation -4. Optionally applying noise during the process - -Function Variants -~~~~~~~~~~~~~~~~~ - -.. tab:: Python - - .. code-block:: python - - import cudaq - import cudaq_qec as qec - - # Use the stim backend for performance in QEC settings - cudaq.set_target("stim") - - # Get a code instance - code = qec.get_code("steane") - - # Basic memory circuit with |0⟩ state - syndromes, measurements = qec.sample_memory_circuit( - code, # QEC code instance - numShots=1000, # Number of circuit executions - numRounds=1 # Number of stabilizer rounds - ) - - # Memory circuit with custom initial state - syndromes, measurements = qec.sample_memory_circuit( - code, # QEC code instance - op=qec.operation.prep1, # Initial state - numShots=1000, # Number of shots - numRounds=1 # Number of rounds - ) - - # Memory circuit with noise model - noise = cudaq.NoiseModel() - # Configure noise - noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.01), 1) - syndromes, measurements = qec.sample_memory_circuit( - code, # QEC code instance - numShots=1000, # Number of shots - numRounds=1, # Number of rounds - noise=noise # Noise model - ) - -.. tab:: C++ - - .. code-block:: cpp - - // Basic memory circuit with |0⟩ state - auto [syndromes, measurements] = qec::sample_memory_circuit( - code, // QEC code instance - numShots, // Number of circuit executions - numRounds // Number of stabilizer rounds - ); - - // Memory circuit with custom initial state - auto [syndromes, measurements] = qec::sample_memory_circuit( - code, // QEC code instance - operation::prep1, // Initial state preparation - numShots, // Number of circuit executions - numRounds // Number of stabilizer rounds - ); - - // Memory circuit with noise model - auto noise_model = cudaq::noise_model(); - noise_model.add_channel(...); // Configure noise - auto [syndromes, measurements] = qec::sample_memory_circuit( - code, // QEC code instance - numShots, // Number of circuit executions - numRounds, // Number of stabilizer rounds - noise_model // Noise model to apply - ); - -Return Values -~~~~~~~~~~~~~ - -The functions return a tuple containing: - -1. **Syndrome Measurements** (:code:`tensor`): - - * Shape: :code:`(num_shots, num_detectors)` - * Columns follow the layout ``[ B S S … S B ]``, where: - - - ``B`` (boundary block) = ``numAncZ = code.get_num_z_stabilizers()`` for Z-basis - preparations (``prep0``/``prep1``), or ``numAncX = code.get_num_x_stabilizers()`` - for X-basis preparations (``prepp``/``prepm``) - - ``S`` (inter-round block) = ``numAncZ + numAncX`` detectors per round transition - (``num_rounds - 1`` blocks total) - - Total: ``num_detectors = 2*B + (num_rounds - 1)*S`` - * Values are 0 or 1 representing measurement outcomes - -2. **Data Measurements** (:code:`tensor`): - - * Shape: :code:`(num_shots, block_size)` - * Contains final data qubit measurements - * Used to verify logical state preservation - -Example Usage -~~~~~~~~~~~~~ - -Example of running a memory experiment: - -.. tab:: Python - - .. code-block:: python - - import cudaq - import cudaq_qec as qec - - # Use the stim backend for performance in QEC settings - cudaq.set_target("stim") - - # Create code and decoder - code = qec.get_code('steane') - decoder = qec.get_decoder('single_error_lut', - code.get_parity()) - - # Configure noise - noise = cudaq.NoiseModel() - noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.01), 1) - - # Run memory experiment - syndromes, measurements = qec.sample_memory_circuit( - code, - op=qec.operation.prep0, - numShots=1000, - numRounds=10, - noise=noise - ) - - # Analyze results - for shot in range(1000): - # Get syndrome for this shot - syndrome = syndromes[shot].tolist() - - # Decode syndrome - result = decoder.decode(syndrome) - if result.converged: - # Process correction - pass - -.. tab:: C++ - - .. code-block:: cpp - - // Compile and run with: - // nvq++ --target=stim -lcudaq-qec -lcudaq-qec-decoders example.cpp - // ./a.out - - #include "cudaq.h" - #include "cudaq/qec/decoder.h" - #include "cudaq/qec/experiments.h" - #include "cudaq/qec/noise_model.h" - - int main(){ - // Create a Steane code instance - auto code = cudaq::qec::get_code("steane"); - - // Configure noise model - cudaq::noise_model noise; - noise.add_all_qubit_channel("x", cudaq::depolarization2(0.1), - /*num_controls=*/1); - - // Run memory experiment - auto [syndromes, data] = cudaq::qec::sample_memory_circuit( - *code, // Code instance - cudaq::qec::operation::prep0, // Prepare |0⟩ state - 1000, // 1000 shots - 1, // 1 rounds - noise // Apply noise - ); - - // Analyze results - auto decoder = cudaq::qec::get_decoder("single_error_lut", code->get_parity()); - for (std::size_t shot = 0; shot < 1000; shot++) { - // Get syndrome for this shot - std::vector syndrome(syndromes.shape()[1]); - for (std::size_t i = 0; i < syndrome.size(); i++) - syndrome[i] = syndromes.at({shot, i}); - - // Decode syndrome - auto results = decoder->decode(syndrome); - // Process correction - // ... - } - } - -Additional Noise Models -~~~~~~~~~~~~~~~~~~~~~~~ - -.. tab:: Python - - .. code-block:: python - - noise = cudaq.NoiseModel() - - # Add multiple error channels - noise.add_all_qubit_channel('h', cudaq.BitFlipChannel(0.001)) - - # Specify two qubit errors - noise.add_all_qubit_channel("x", cudaq.Depolarization2(p), 1) - -.. tab:: C++ - - .. code-block:: cpp - - cudaq::noise_model noise; - - // Add multiple error channels - noise.add_all_qubit_channel( - "x", cudaq::bit_flip_channel(/*probability*/ 0.01)); - - // Specify two qubit errors - noise.add_all_qubit_channel( - "x", cudaq::depolarization2(/*probability*/ 0.01), - /*numControls*/ 1); - diff --git a/docs/sphinx/components/qec/numerical_experiments.rst b/docs/sphinx/components/qec/numerical_experiments.rst new file mode 100644 index 000000000..e32203208 --- /dev/null +++ b/docs/sphinx/components/qec/numerical_experiments.rst @@ -0,0 +1,75 @@ +Experiments and Noise Modeling +============================== + +The CUDA-Q QEC library lets you run numerical error-correction experiments -- studying how a code and decoder behave under noise. This page introduces several of the most common: modeling noise at the **code-capacity** and **circuit-level**, and running full **memory circuit experiments**. + +For a walkthrough of the experiments described below, see the :doc:`Experiments and Noise Modeling ` example, along with the :doc:`C++ ` and :doc:`Python ` API reference. + +The sections below follow the :doc:`Conventions ` for errors, syndromes, and logical observables. + +Code-Capacity Noise Modeling +---------------------------- + +Quantum error correction (QEC) describes a set of tools used to detect and correct errors which occur to qubits on quantum computers. +CUDA-Q QEC centers on two of the most common objects in QEC: stabilizer codes, and decoders. +A stabilizer code is the quantum generalization of linear codes in classical error correction, which use parity checks to detect errors on noisy bits. +In QEC, we'll perform stabilizer measurements on ancilla qubits to check the parity of our data qubits. +These stabilizer measurements are non-destructive, and thus allow us to check the relative parity of qubits without destroying our quantum information. + +For example, if we prepare two qubits in the state :math:`|\Psi\rangle = a|00\rangle + b|11\rangle`, we may want to check if a bit-flip error happened. +We can measure the stabilizer `ZZ`, which will return 0 if there are no errors or an even number of errors, but will return 1 if either has flipped. +This is how we can perform parity checks in quantum computing, without performing destructive measurements which collapse our superposition. +How these measurements are physically performed is covered in circuit-level noise modeling below. + +We can specify a stabilizer code with either a list of stabilizer operators (like `ZZ` above), or equivalently, a parity check matrix. +We can think of the columns of a parity check matrix as the types of errors that can occur. In this case, each qubit can experience a bit flip `X` or a phase flip `Z` error, so the parity check matrix will have 2N columns where N is the number of data qubits. +Each row represents a stabilizer, or a parity check. +The values are either 0 or 1, where a 1 means that the corresponding column does participate in the parity check, and a 0 means it does not. +Therefore, if a single `X/Z` error happens to a qubit, the supported rows of the parity check matrix will trigger. +This is called the syndrome, a string of 0's and 1's corresponding to which parity checks were violated. +A special class of stabilizer codes are called CSS (Calderbank-Shor-Steane) codes, which means the `X` and `Z` components of their parity check matrix can be separated. + +This brings us to decoding. Decoding is the act of solving the problem: given a syndrome, which underlying errors are most likely? +There are many decoding algorithms; the code-capacity example uses a simple single-error look-up table. +This means that the decoder will enumerate for each single error bit string, what the resulting syndromes are. +Then given a syndrome, it will look up the error string and return that as a result. + +The last ingredient is a way to generate errors. The code-capacity noise model assumes an independent and identical chance that an `X` or `Z` error happens on each qubit with some probability `p`. + +For the runnable code, see :ref:`Code-Capacity Noise Modeling `. + +Circuit-level Noise Modeling +---------------------------- + +Circuit-level noise modeling builds upon the code-capacity model above. +In the circuit-level noise modeling experiment, we have many of the same components from the CUDA-Q QEC library: QEC codes, decoders, and noisy data. +The primary difference here, is that we can begin to run CUDA-Q kernels to generate noisy data, rather than just generating a random bit string to represent our errors. + +Along with the stabilizers, parity check matrices, and logical observables, the QEC code type also has an encoding map. +This map allows codes to define logical gates in terms of gates on the underlying physical qubits. +These encodings operate on the `qec.patch` type, which represents three registers of physical qubits making up a logical qubit. +A data qubit register, an X-stabilizer ancilla register, and a Z-stabilizer ancilla register. + +The most notable encoding stored in the QEC map is the `qec.operation.stabilizer_round`, which encodes a `cudaq.kernel` that stores the gate-level information for performing a stabilizer measurement. +These stabilizer rounds are the gate-level way to encode the parity check matrix of a QEC code into quantum circuits. + +Circuit-level noise modeling simulates a quantum memory experiment. +These experiments model how well QEC cycles, or rounds of stabilizer measurements, can protect the information encoded in a logical qubit. +If noise is turned off, then the information is protected indefinitely. +The circuit-level example models depolarization noise after each CX gate and tracks how many logical errors occur. + +For the runnable code, see :ref:`Circuit-level Noise Modeling `. + +Memory Circuit Experiments +-------------------------- + +Memory circuit experiments test a QEC code's ability to preserve quantum information over time by: + +1. Preparing an initial logical state +2. Performing multiple rounds of stabilizer measurements +3. Measuring data qubits to verify state preservation +4. Optionally applying noise during the process + +A memory circuit experiment measures how well a code and decoder preserve a logical qubit through repeated rounds of noisy stabilizer measurement. After a logical state is prepared, each round extracts a syndrome while noise acts on the qubits; the final data-qubit measurement reconstructs the logical observable so the outcome can be compared against the prepared state. Because the same logical information must survive every round, the experiment exercises the full detection-and-correction loop over time rather than a single shot -- it reveals whether the decoder keeps accumulated errors below the code's threshold, and how the logical error rate grows with the number of rounds and the physical error rate. Running many shots yields the statistics used to estimate a code's logical error rate, and ultimately its threshold. + +For the runnable code -- the ``sample_memory_circuit`` function variants, a full experiment, and additional noise models -- see :ref:`Memory Circuit Experiments `. diff --git a/docs/sphinx/components/qec/realtime_decoding.rst b/docs/sphinx/components/qec/realtime_decoding.rst new file mode 100644 index 000000000..8c53150ac --- /dev/null +++ b/docs/sphinx/components/qec/realtime_decoding.rst @@ -0,0 +1,64 @@ +Realtime Decoding +================= + +This page covers how realtime decoding works, its workflow, and terminology. For runnable applications — a complete walkthrough, the AI predecoder, and Relay BP — see the :doc:`Realtime Decoding examples `. + +CUDA-Q QEC provides realtime decoding for quantum error correction on real quantum hardware: decoders process syndromes and compute corrections within qubit coherence times, making active error correction practical for real quantum computers. The framework supports two primary deployment scenarios: + +1. **Hardware Integration**: Decoders running on classical computers connected to real quantum processing units (QPUs) — such as `Quantinuum's Helios QPU `_ — via low-latency networks. +2. **Simulation Mode**: Decoders operating in simulated environments for testing and development on local systems. + +.. note:: + The realtime decoding interfaces are experimental and subject to change. Realtime decoding on Quantinuum's Helios-1 device is currently available only to partners and collaborators. Please email QCSupport@quantinuum.com for more information. + +Workflow +^^^^^^^^ + +Realtime decoding integrates into quantum error correction pipelines through a carefully designed four-stage workflow. This workflow separates the computationally intensive characterization phase from the latency-critical runtime phase, ensuring that decoders can operate efficiently during quantum circuit execution. + +1. **Detector Error Model (DEM) Generation**: Before running a quantum program, the user first characterizes how errors propagate through the quantum circuit. The library internally uses Memory Syndrome Matrix (MSM) representations to track error propagation, but this complexity is abstracted through helper functions like ``z_dem_from_memory_circuit``. The user simply provides a quantum code, noise model, and circuit parameters, and receives a complete detector error model that maps error mechanisms to syndrome patterns. This step is performed once during development. + +2. **Decoder Configuration and Saving**: Using the DEM, the user configures decoder instances with the specific error model data. This includes converting parity check matrices to sparse format, setting decoder-specific parameters (like lookup table depth or BP iterations), and assigning unique IDs to each logical qubit's decoder. The configuration is then saved to a YAML file, capturing all the information decoders need to interpret syndrome measurements correctly. This creates a portable, reusable configuration that separates characterization from execution. + +3. **Decoder Loading and Initialization**: Just before circuit execution, the user loads the saved YAML configuration file. The library parses the configuration, instantiates the appropriate decoder implementations, initializes internal data structures, and registers the decoders with the CUDA-Q runtime. For GPU-based decoders, matrices are transferred to device memory; for lookup table decoders, syndrome-to-correction mappings are constructed. This initialization takes milliseconds to seconds depending on code size and happens before quantum operations begin. + +4. **Realtime Decoding**: During quantum circuit execution, the decoding API is used within quantum kernels to interact with decoders. As the circuit measures stabilizers, syndromes are enqueued to the decoder, which processes them concurrently. When corrections are needed, the decoder is queried and the suggested operations are applied to the logical qubits. This entire process happens within the coherence time constraints of the quantum hardware. + +Terminology and Data Flow +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The realtime decoding workflow involves configuring a decoder (or many) before CUDA-Q kernel launch, and communicating to the decoders with special in-kernel functions. +A decoder is a single software instance of a decoding algorithm, and all its relevant inputs (parity-check matrices, error rates, etc.) which will remain static for the execution of the quantum program. +A decoder config may contain many decoders, each with different algorithms and input parameters. + +In a quantum kernel, a user interacts with the decoders via the `enqueue_syndromes` and `get_corrections` interfaces. +The behavior of these functions depends on their configuration and their usage. + +The realtime decoding workflow can be described with respect to the offline decoding workflow. +The non-realtime decoders require a detector error model which is specified via a detector error matrix which is the parity check matrix `H` of the decoding problem, and a vector of weights (error rates). +This matrix has dimensions of `[numDetectors, numErrors]`, where each row is a detector, and each column is a possible error. +For realtime decoding, we first need to convert the circuit measurements into detectors. +This is specified via the detector matrix `D`, which has dimensions `[numDetectors, numMeasurements]`. +Each column of the detector matrix defines which detectors a measurement participates in by including an entry of `1`. +Thus, once all `numMeasurements` measurements are enqueued, a matrix-vector multiply can convert this buffer of raw measurements into detectors which are then passed into the decoding algorithm. + +Similarly, an observables flips matrix `O` of size `[numObs, numErrors]` must be provided. +Each column of the observables flips matrix describes for each error, which observables are flipped by that error by including an entry of `1`. +Once the decoding algorithm has processed the detectors it provides a vector of predicted errors of length `numErrors`. +This vector then executes a matrix-vector multiply with the observables flips matrix to yield a new vector of length `numObs` which contains an entry of `1` if the observable is predicted to have flipped. + +Thus once a decoder is configured, we can view the realtime decoder as a transformation of data starting from a vector of raw measurements, then transformed into detectors via `D`, then error predictions via `H`, then observable flip predictions via `O`. This last step is what is returned via `get_corrections`. The user configures how many bits of information are returned, and what they represent via the `O` matrix in the decoder config. + +Similarly, the user determines how many measurements are needed for the decoder via the `D` matrix in the decoder config, and they are sent to the decoder via `enqueue_syndromes`. +For flexibility, the user can choose to send all measurements with a single `enqueue_syndromes` call, or send them over several calls. +However they are split up, the decoder will not begin decoding until all `numMeasurements` have been enqueued, and will throw an error if too many are sent. +Thus it is the final `enqueue_syndromes` call which kicks off the decoder, and is an asynchronous function. +Additional quantum gates can be applied, and only when `get_corrections` is called does the kernel sync and wait for the corrections. + +See Also +^^^^^^^^ + +* :ref:`Pre-built QEC Decoders ` — decoders available for realtime use +* :doc:`Realtime Decoding examples ` — runnable end-to-end examples +* :ref:`realtime_pipeline_api` — Realtime Pipeline C++ API (experimental) +* :ref:`C++ realtime decoding API ` and :ref:`Python realtime decoding API ` diff --git a/docs/sphinx/conf.py.in b/docs/sphinx/conf.py.in index 25feca9ea..44f6aae5b 100644 --- a/docs/sphinx/conf.py.in +++ b/docs/sphinx/conf.py.in @@ -137,6 +137,7 @@ html_theme = 'sphinx_rtd_theme' # documentation. html_theme_options = { "collapse_navigation": False, + "navigation_depth": 4, "sticky_navigation": False, "prev_next_buttons_location": "both", "style_nav_header_background": @@ -229,7 +230,15 @@ intersphinx_mapping = { 'cudaq': ('https://nvidia.github.io/cuda-quantum/latest', None) } -redirects = {"versions": "../latest/releases.html"} +redirects = { + "versions": "../latest/releases.html", + "components/qec/introduction": "index.html", + "examples_rst/qec/code_capacity_noise": "modeling_noise.html", + "examples_rst/qec/circuit_level_noise": "modeling_noise.html", + "examples_rst/qec/stim_dem_decoder": "decoders.html", + "examples_rst/qec/dem_sampling": "decoders.html", + "examples_rst/qec/nv_qldpc_gamma_ensemble_user_guide": "../../performance/nv_qldpc_gamma_ensemble_user_guide.html", +} nitpick_ignore = [ ('cpp:identifier', 'pid_t'), diff --git a/docs/sphinx/examples_rst/qec/circuit_level_noise.rst b/docs/sphinx/examples_rst/qec/circuit_level_noise.rst deleted file mode 100644 index 3d36babb9..000000000 --- a/docs/sphinx/examples_rst/qec/circuit_level_noise.rst +++ /dev/null @@ -1,80 +0,0 @@ -Quantum Error Correction with Circuit-level Noise Modeling ----------------------------------------------------------- -This example builds upon the previous code-capacity noise model example. -In the circuit-level noise modeling experiment, we have many of the same components from the CUDA-Q QEC library: QEC codes, decoders, and noisy data. -The primary difference here, is that we can begin to run CUDA-Q kernels to generate noisy data, rather than just generating random bitstring to represent our errors. - -Along with the stabilizers, parity check matrices, and logical observables, the QEC code type also has an encoding map. -This map allows codes to define logical gates in terms of gates on the underlying physical qubits. -These encodings operate on the `qec.patch` type, which represents three registers of physical qubits making up a logical qubit. -A data qubit register, an X-stabilizer ancilla register, and a Z-stabilizer ancilla register. - -The most notable encoding stored in the QEC map, is how the `qec.operation.stabilizer_round`, which encodes a `cudaq.kernel` which stores the gate-level information for how to do a stabilizer measurement. -These stabilizer rounds are the gate-level way to encode the parity check matrix of a QEC code into quantum circuits. - -This example walks through how to use the CUDA-Q QEC library to perform a quantum memory experiment simulation. -These experiments model how well QEC cycles, or rounds of stabilizer measuments, can protect the information encoded in a logical qubit. -If noise is turned off, then the information is protected indefinitely. -Here, we will model depolarization noise after each CX gate, and track how many logical errors occur. - - -CUDA-Q QEC Implementation -+++++++++++++++++++++++++++++ -Here's how to use CUDA-Q QEC to perform a circuit-level noise model experiment in both Python and C++: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/circuit_level_noise.py - :language: python - :start-after: [Begin Documentation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/circuit_level_noise.cpp - :language: cpp - :start-after: [Begin Documentation] - - Compile and run with - - .. code-block:: bash - - nvq++ --target=stim -lcudaq-qec -lcudaq-qec-decoders circuit_level_noise.cpp -o circuit_level_noise - ./circuit_level_noise - - -1. QEC Code and Decoder types: - - As in the code capacity example, our central objects are the `qec.code` and `qec.decoder` types. - -2. Clifford simulation backend: - - As the size of QEC circuits can grow quite large, Clifford simulation is often the best tool for these simulations. - - `cudaq.set_target("stim")` selects the highly performant Stim simulator as the simulation backend. - -3. Noise model: - - To add noisy gates we use the `cudaq.NoiseModel` type. - - CUDA-Q supports the generation of arbitrary noise channels. Here we use a `cudaq.Depolarization2` channel to add a depolarization channel. - - This is added to the `CX` gate by adding it to the `X` gate with 1 control. - - This noisy gate is added to every qubit via that `noise.add_all_qubit_channel` function. - -4. Getting circuit-level noisy data: - - The `qec.code` is the first input parameter here, as the code's `stabilizer_round` determines the circuits executed. - - Each memory circuit runs for an input number of `nRounds`, which specifies how many `stabilizer_round` kernels are ran. - - After `nRounds` the data qubits are measured and the run is over. - - This is performed `nShots` number of times. - - During a shot, each stabilizer round's syndrome is `xor`'d against the preceding syndrome, so that we can track a sparser flow of data showing which round each parity check was violated. - - The first round returns the syndrome as is, as there is nothing preceding to `xor` against. - -5. Data qubit measurements: - - The data qubits are only read out after the end of each shot, so there are `nShots` worth of data readouts. - - The basis of the data qubit measurements depends on the state preparation used. - - Z-basis readout when preparing the logical `|0>` or logical `|1>` state with the `qec.operation.prep0` or `qec.operation.prep1` kernels. - - X-basis readout when preparing the logical `|+>` or logical `|->` state with the `qec.operation.prepp` or `qec.operation.prepm` kernels. - -6. Logical Errors: - - From here, the decoding procedure is again similar to the code capacity case, expect for we use a pauli frame to track errors that happen each QEC cycle. - - The final values of the pauli frame tell us how our logical state flipped during the experiment, and what needs to be done to correct it. - - We compare our known initial state (corrected by the Pauli frame), against our measured data qubits to determine if a logical error occurred. - - -The CUDA-Q QEC library thus provides a platform for numerical QEC experiments. The `qec.code` can be used to analyze a variety of QEC codes (both library or user provided), with a variety of decoders (both library or user provided). -The CUDA-Q QEC library also provides tools to speed up the automation of generating noisy data and syndromes. - diff --git a/docs/sphinx/examples_rst/qec/code_capacity_noise.rst b/docs/sphinx/examples_rst/qec/code_capacity_noise.rst deleted file mode 100644 index 1abeb4c97..000000000 --- a/docs/sphinx/examples_rst/qec/code_capacity_noise.rst +++ /dev/null @@ -1,86 +0,0 @@ -Quantum Error Correction with Code-Capacity Noise Modeling ----------------------------------------------------------- - -Quantum error correction (QEC) describes a set of tools used to detect and correct errors which occur to qubits on quantum computers. -This example will walk through how the CUDA-Q QEC library handles two of the most common objects in QEC: stabilizer codes, and decoders. -A stabilizer code is the quantum generalization of linear codes in classical error correction, which use parity checks to detect errors on noise bits. -In QEC, we'll perform stabilizer measurements on ancilla qubits to check the parity of our data qubits. -These stabilizer measurements are non-destructive, and thus allow us to check the relative parity of qubits without destroying our quantum information. - -For example, if we prepare two qubits in the state `\Psi = a|00> + b|11>`, we maybe want to check if a bit-flip error happened. -We can measure the stabilizer `ZZ`, which will return 0 if there are no errors or even number of errors, but will return 1 if either has flipped. -This is how we can perform parity checks in quantum computing, without performing destructive measurements which collapse our superposition. -How these measurements are physically performed can be seen in the circuit-level noise QEC example. - -We can specify a stabilizer code with either a list of stabilizer operators (like `ZZ` above), or equivalently, a parity check matrix. -We can think of the columns of a parity check matrix as the types of errors that can occur. In this case, each qubit can experience a bit flip `X` or a phase flip `Z` error, so the parity check matrix will have 2N columns where N is the number of data qubits. -Each row represents a stabilizer, or a parity check. -The values are either 0 or 1, where a 1 means that the corresponding column does participate in the parity check, and a 0 means it does not. -Therefore, if a single `X/Z` error happens to a qubit, the supported rows of the parity check matrix will trigger. -This is called the syndrome, a string of 0's and 1's corresponding to which parity checks were violated. -A special class of stabilizer codes are called CSS (Calderbank-Shor-Steane) codes, which means the `X` and `Z` components of their parity check matrix can be separated. - -This brings us to decoding. Decoding is the act of solving the problem: given a syndrome, which underlying errors are most likely? -There are many decoding algorithms, but this example will use a simple single-error look-up table. -This means that the decoder will enumerate for each single error bit string, what the resulting syndromes are. -Then given a syndrome, it will look up the error string and return that as a result. - -The last thing we need, is a way to generate errors. -This example will go through a code capacity noise model where we have an independent and identical chance that an `X` or `Z` error happens on each qubit with some probability `p`. - -CUDA-Q QEC Implementation -+++++++++++++++++++++++++++++ -Here's how to use CUDA-Q QEC to perform a code capacity noise model experiment in both Python and C++: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/code_capacity_noise.py - :language: python - :start-after: [Begin Documentation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/code_capacity_noise.cpp - :language: cpp - :start-after: [Begin Documentation] - - Compile and run with - - .. code-block:: bash - - nvq++ --target=stim -lcudaq-qec -lcudaq-qec-decoders code_capacity_noise.cpp -o code_capacity_noise - ./code_capacity_noise - - -Code Explanation -++++++++++++++++ - -1. QEC Code type: - - CUDA-Q QEC centers around the `qec.code` type, which contains the data relevant for a given code. - - In particular, this represents a collection of qubits which represent a single logical qubit. - - Here we get one of the most well known QEC codes, the Steane code, with the `qec.get_code` function. - - We can get the stabilizers from a code with the `code.get_stabilizers()` function. - - In this example, we get the parity check matrix of the code. Because the Steane code is a CSS code, we can extract just the `Z` components of the parity check matrix. - - Here, we see this matrix has 3 rows and 7 columns, which means there are 7 data qubits (7 possible single bit-flip errors) and 3 Z-stabilizers (parity checks). Note that `Z` stabilizers check for `X` type errors. - - Lastly, we get the logical `Z` observable for the code. This will allow us to see if the `Z` observable of our logical qubit has flipped. - -2. Decoder type: - - A single-error look-up table (LUT) decoder can be acquired with the `qec.get_decoder` call. - - Passing in the parity check matrix gives the decoder the required information to associated syndromes with underlying error mechanisms. - - Once the decode has been constructed, the `decoder.decode(syndrome)` member function is called, which returns a predicted error given the syndrome. - -3. Noise model: - - To generate noisy data, we call `qec.generate_random_bit_flips(nBits, p)` which will return an array of bits, where each bit has probability `p` to have been flipped into 1, and a `1-p` chance to have remained 0. - - Since we are using the `Z` parity check matrix `H_Z`, we want to simulate random `X` errors on our 7 data qubits. - -4. Logical Errors: - - Once we have noisy data, we see what the resulting syndromes are by multiplying our noisy data vector with our parity check matrix (mod 2). - - From this syndrome, we see what the decoder predicts what errors occurred in the data. - - To classify as a logical error, the decoder does not need to exactly guess what happened to the data, but if there was a flip in the logical observable or not. - - If the decoder guesses this successfully, we have corrected the quantum error. If not, we have incurred a logical error. - -5. Further automation: - - While this workflow is nice for seeing things step by step, the `qec.sample_code_capacity` API is provided to generate a batch of noisy data and their corresponding syndromes. - -The CUDA-Q QEC library thus provides a platform for numerical QEC experiments. The `qec.code` can be used to analyze a variety of QEC codes (both library or user provided), with a variety of decoders (both library or user provided). -The CUDA-Q QEC library also provides tools to speed up the automation of generating noisy data and syndromes. diff --git a/docs/sphinx/examples_rst/qec/creating_qec_codes.rst b/docs/sphinx/examples_rst/qec/creating_qec_codes.rst new file mode 100644 index 000000000..18fec6106 --- /dev/null +++ b/docs/sphinx/examples_rst/qec/creating_qec_codes.rst @@ -0,0 +1,21 @@ +Creating New QEC Codes +====================== + +Below, we demonstrate how to use CUDA-Q QEC to define a new QEC code entirely in Python. This powerful feature allows for rapid prototyping and testing of custom error correction schemes. + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/custom_repetition_code_fine_grain_noise.py + :language: python + :start-after: [Begin Documentation] + +This example illustrates several key concepts for defining custom codes: + +* **Define a Code Class**: A new code is defined by creating a Python class decorated with ``@qec.code(...)``, which registers it with the CUDA-Q QEC runtime. The class must inherit from ``qec.Code``. +* **Implement Required Methods**: The class must implement methods that describe the code's structure, such as ``get_num_data_qubits()`` and ``get_num_ancilla_qubits()``. +* **Define Logical Operations as Kernels**: Quantum operations like state preparation (``prep0``, ``prep1``), logical gates (``x_logical``), and stabilizer measurements (``stabilizer_round``) are implemented as standard CUDA-Q kernels. +* **Map Operations to Kernels**: The ``operation_encodings`` dictionary links abstract QEC operations (e.g., ``qec.operation.prep0``) to the concrete CUDA-Q kernels that implement them. +* **Provide Stabilizers and Observables**: The code's stabilizer generators and logical observables must be defined. This is typically done by creating lists of ``cudaq.SpinOperator`` objects representing the Pauli strings for the stabilizers (e.g., "ZZI") and logical operators (e.g., "ZZZ"). +* **Specify Fine-Grained Noise**: This example demonstrates applying noise at a specific point within a kernel. Inside ``stabilizer_round``, ``cudaq.apply_noise`` is called on each data qubit, offering precise control over the noise model, in contrast to applying noise globally to all gates of a certain type. + +Once defined, the custom code can be instantiated with ``qec.get_code()`` and used with all standard CUDA-Q QEC tools, including ``qec.dem_from_memory_circuit()`` and ``qec.sample_memory_circuit()``. diff --git a/docs/sphinx/examples_rst/qec/decoders.rst b/docs/sphinx/examples_rst/qec/decoders.rst index 032cd535c..f496b971c 100644 --- a/docs/sphinx/examples_rst/qec/decoders.rst +++ b/docs/sphinx/examples_rst/qec/decoders.rst @@ -1,5 +1,5 @@ Decoders --------- +======== In quantum error correction, decoders are responsible for interpreting measurement outcomes (syndromes) to identify and correct quantum errors. We measure a set of stabilizers that give us information about what errors might have happened. The pattern of these measurements is called a syndrome, @@ -9,48 +9,38 @@ The relationship between errors and syndromes is captured mathematically by the stabilizer measurement, while each column represents a possible error. When we multiply an error pattern by this matrix, we get the syndrome that would result from those errors. -.. note:: - **scipy.sparse interop** — :func:`cudaq_qec.get_decoder` and - :class:`cudaq_qec.Decoder` accept a ``scipy.sparse`` matrix (CSR, CSC, - COO, or any other ``scipy.sparse`` format) as the parity-check matrix - ``H``. This is the preferred form for large PCMs because no dense - ``rows x cols`` allocation is made — the matrix is normalised to CSR - internally. Dense NumPy ``uint8`` arrays remain supported. - The PCM utilities :func:`cudaq_qec.reorder_pcm_columns`, - :func:`cudaq_qec.shuffle_pcm_columns`, and - :func:`cudaq_qec.pcm_to_sparse_vec` also accept SciPy sparse matrices - without creating a dense ``cudaqx::tensor``. Reordering and shuffling a - sparse input returns a ``scipy.sparse.csc_matrix``; a dense input continues - to return a NumPy array. +A detector error model (DEM) describes how the errors in a QEC circuit produce the syndrome bits that detect them. The examples below work with DEMs in three ways: the first constructs a decoder directly from raw Stim ``.dem`` text; the second expands a DEM into a multi-round parity check matrix; and the third samples synthetic error and syndrome data from a DEM to exercise a decoder. See :ref:`Detector Error Model ` for more details. - ``scipy`` is an optional dependency; if it is not installed, pass a dense - NumPy array instead. +.. _stim_dem_text_example: -.. note:: - **NumPy result arrays** — As of v0.7.0, a decoder's ``result`` (from - :class:`cudaq_qec.DecoderResult`, and the per-shot results of - :class:`cudaq_qec.BatchDecoderResult` / :class:`cudaq_qec.AsyncDecoderResult`) - is a 1-D NumPy array rather than a Python ``list``. Indexing and iteration are - unchanged. +Decoding From Stim DEM Text ++++++++++++++++++++++++++++ -Detector Error Model -+++++++++++++++++++++ +This example constructs a decoder from raw Stim ``.dem`` text and uses the matching parsed matrix for observable predictions. For what a detector error model is and how the text is parsed, see :ref:`Decoding from Stim DEM Text `. -Here we introduce the `cudaq.qec.detector_error_model` type, which allows us to create a detector error model (DEM) from a QEC circuit and noise model. +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/stim_dem_decoder.py + :language: python + :start-after: [Begin Documentation] + +.. tab:: C++ -The DEM can be generated from a QEC circuit and noise model using functions like `dem_from_memory_circuit()`. For circuit-level noise, the DEM can be put into a -canonical form that's organized by measurement rounds, making it suitable for multi-round decoding. + .. literalinclude:: ../../examples/qec/cpp/stim_dem_decoder.cpp + :language: cpp + :start-after: [Begin Documentation] + + Compile and run with -For a complete example of using the surface code with DEM to generate parity check matrices and perform decoding, see the :doc:`circuit level noise example `. + .. code-block:: bash -If a Stim detector error model is already available as text, the same decoder -entry point can consume that text directly. See :doc:`Decoding From Stim DEM Text ` -for a C++ and Python example. + nvq++ -lcudaq-qec -lcudaq-qec-decoders stim_dem_decoder.cpp -o stim_dem_decoder + ./stim_dem_decoder Generating a Multi-Round Parity Check Matrix ++++++++++++++++++++++++++++++++++++++++++++ -Below, we demonstrate how to use CUDA-Q QEC to construct a multi-round parity check matrix for an error correction code under a circuit-level noise model in Python: +A single-round DEM captures one measurement cycle. Under circuit-level noise, errors accumulate across many rounds, and the DEM expands into a multi-round parity check matrix. The following example constructs one for an error correction code in Python: .. tab:: Python @@ -72,31 +62,140 @@ This example illustrates how to: * Simulate circuit-level noise and collect data Run multiple shots of the memory experiment using ``qec.sample_memory_circuit(...)`` to sample both the data and syndrome measurements from noisy executions. The resulting bitstrings can be used for decoding and performance evaluation of the error correction scheme. -Creating New QEC codes -++++++++++++++++++++++++++++++++++++++++++++ +.. _dem_sampling_example: -Below, we demonstrate how to use CUDA-Q QEC to define a new QEC code entirely in Python. This powerful feature allows for rapid prototyping and testing of custom error correction schemes. +DEM Sampling — Monte-Carlo Sampling from Detector Error Models ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +This example samples synthetic error and syndrome data from a detector error model, then walks through the GPU-accelerated and CPU paths and the supported input types. For the sampling model itself, see :ref:`DEM Sampling `. + +Example +~~~~~~~ .. tab:: Python - .. literalinclude:: ../../examples/qec/python/custom_repetition_code_fine_grain_noise.py + .. literalinclude:: ../../examples/qec/python/dem_sampling.py :language: python :start-after: [Begin Documentation] -This example illustrates several key concepts for defining custom codes: +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/dem_sampling.cpp + :language: cpp + :start-after: [Begin Documentation] + + Compile and run with + + .. code-block:: bash + + nvq++ -lcudaq-qec dem_sampling.cpp + ./a.out + +GPU Acceleration +~~~~~~~~~~~~~~~~ + +When a CUDA-capable GPU is available, ``dem_sampling`` keeps the sampling and +syndrome computation on-device, which is significantly faster than per-shot CPU +sampling, especially for large numbers of shots and sparse error models (low +probabilities): + +1. **Sparse Bernoulli sampling** — Errors are generated directly in compressed + sparse row (CSR) format. For low error probabilities the CSR representation + is compact, and the sampler skips mechanisms with zero probability entirely + rather than evaluating a Bernoulli trial for every mechanism in every shot. + +2. **GF(2) sparse-dense matrix multiply** — Syndromes are computed as + :math:`\text{errors} \times H^T \pmod{2}` using a sparse-dense multiply + over GF(2). The check matrix :math:`H^T` is stored in a bitpacked layout, + reducing memory bandwidth by 8x compared to one byte per entry. + +3. **On-device packing and unpacking** — :math:`H` is transposed and bitpacked + on the GPU in a single kernel. Syndromes are unpacked from the bitpacked + result, and the dense error matrix is produced from the CSR representation + via a fused zero-and-scatter kernel. + +The CPU path uses ``std::bernoulli_distribution`` per mechanism per shot +followed by a dense dot product for the syndrome. + +Input Types and Backend Selection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``backend`` parameter controls where sampling runs: + +- ``"auto"`` (default) — try GPU first, fall back to CPU. +- ``"gpu"`` — require GPU; raise ``RuntimeError`` if unavailable. +- ``"cpu"`` — always use the CPU path. + +The Python binding accepts several input types, each routed through a different +code path: + +1. **NumPy arrays** (most common) — When the GPU is available the bindings + automatically allocate device memory, copy inputs host-to-device, run + cuStabilizer, and copy results back as NumPy ``uint8`` arrays. With + ``backend="cpu"`` the GPU path is skipped entirely. No user action is + required beyond passing standard ``uint8`` and ``float64`` arrays. + +2. **PyTorch CUDA tensors** — The GPU path reads input device pointers directly + via ``data_ptr()`` and writes outputs into ``torch.empty`` tensors on the + same device, avoiding any host-device copies. This is the fastest path when + inputs are already on the GPU. PyTorch is an optional dependency; install + with ``pip install torch``. + +3. **PyTorch CPU tensors** — With ``backend="gpu"`` the tensors are + automatically moved to CUDA (via ``.to(device)``) before sampling. With + ``backend="auto"`` CPU tensors are rejected with an error; convert them to + NumPy with ``.numpy()`` first. + +The C++ API exposes two namespaces: + +- ``cudaq::qec::dem_sampler::cpu::sample_dem`` — takes a ``cudaqx::tensor`` + check matrix and a ``std::vector`` of probabilities; returns + ``(syndromes, errors)`` as tensors. +- ``cudaq::qec::dem_sampler::gpu::sample_dem`` — takes raw device pointers and + writes results into caller-provided device buffers; returns ``false`` if + cuStabilizer is not available at runtime. + +The ``gpu`` overload works with device pointers that you allocate, populate, +and free yourself. Guard the call behind a device-count check and fall back to +the ``cpu`` overload when it returns ``false``: + +.. code-block:: cpp + + #include "cudaq/qec/dem_sampling.h" + #include + + // H: [num_checks x num_mechanisms] uint8, probs: [num_mechanisms] double. + uint8_t *d_H, *d_syndromes, *d_errors; + double *d_probs; + cudaMalloc(&d_H, num_checks * num_mechanisms); + cudaMalloc(&d_probs, num_mechanisms * sizeof(double)); + cudaMalloc(&d_syndromes, num_shots * num_checks); + cudaMalloc(&d_errors, num_shots * num_mechanisms); + cudaMemcpy(d_H, h_data, num_checks * num_mechanisms, cudaMemcpyHostToDevice); + cudaMemcpy(d_probs, prob_data, num_mechanisms * sizeof(double), + cudaMemcpyHostToDevice); + + bool ok = cudaq::qec::dem_sampler::gpu::sample_dem( + d_H, num_checks, num_mechanisms, d_probs, num_shots, /*seed=*/42, + d_syndromes, d_errors); + if (!ok) { + // cuStabilizer unavailable at runtime — use the cpu overload instead. + } + // Copy d_syndromes / d_errors back to host, then cudaFree each buffer. + +See Also +~~~~~~~~ -* **Define a Code Class**: A new code is defined by creating a Python class decorated with ``@qec.code(...)``, which registers it with the CUDA-Q QEC runtime. The class must inherit from ``qec.Code``. -* **Implement Required Methods**: The class must implement methods that describe the code's structure, such as ``get_num_data_qubits()`` and ``get_num_ancilla_qubits()``. -* **Define Logical Operations as Kernels**: Quantum operations like state preparation (``prep0``, ``prep1``), logical gates (``x_logical``), and stabilizer measurements (``stabilizer_round``) are implemented as standard CUDA-Q kernels. -* **Map Operations to Kernels**: The ``operation_encodings`` dictionary links abstract QEC operations (e.g., ``qec.operation.prep0``) to the concrete CUDA-Q kernels that implement them. -* **Provide Stabilizers and Observables**: The code's stabilizer generators and logical observables must be defined. This is typically done by creating lists of ``cudaq.SpinOperator`` objects representing the Pauli strings for the stabilizers (e.g., "ZZI") and logical operators (e.g., "ZZZ"). -* **Specify Fine-Grained Noise**: This example demonstrates applying noise at a specific point within a kernel. Inside ``stabilizer_round``, ``cudaq.apply_noise`` is called on each data qubit, offering precise control over the noise model, in contrast to applying noise globally to all gates of a certain type. +- :doc:`/api/qec/python_api` — ``dem_sampling`` Python API reference +- :doc:`/api/qec/cpp_api` — ``dem_sampler`` C++ API reference -Once defined, the custom code can be instantiated with ``qec.get_code()`` and used with all standard CUDA-Q QEC tools, including ``qec.dem_from_memory_circuit()`` and ``qec.sample_memory_circuit()``. +.. _qldpc_decoder_example: Getting Started with the NVIDIA QLDPC Decoder +++++++++++++++++++++++++++++++++++++++++++++ +The remaining sections describe the built-in decoders that consume the parity check matrices and detector error models above. Each is selected by name through :func:`cudaq_qec.get_decoder` and targets a different regime, trading off speed, accuracy, and the class of codes it supports. We begin with the most general. + Starting with CUDA-Q QEC v0.2, a GPU-accelerated decoder is included with the CUDA-Q QEC library. The library follows the CUDA-Q decoder Python and C++ interfaces (namely :class:`cudaq_qec.Decoder` for Python and @@ -108,14 +207,7 @@ that can be passed to the constructor. Belief Propagation Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``nv-qldpc-decoder`` supports multiple belief propagation (BP) algorithms, each with different trade-offs -between accuracy, convergence, and speed: - -* **Sum-Product BP** (``bp_method=0``): The standard BP algorithm. Good baseline performance. -* **Min-Sum BP** (``bp_method=1``): Faster approximation to sum-product. Can be tuned with ``scale_factor``. -* **Memory-based BP** (``bp_method=2``): Adds uniform memory (``gamma0``) to help escape local minima. Useful when standard BP fails to converge. -* **Disordered Memory BP** (``bp_method=3``): Uses per-variable memory strengths for better adaptability to code structure. -* **Sequential Relay BP** (``composition=1``): Advanced method that runs multiple "relay legs" with different gamma configurations. See examples below for configuration. +The ``nv-qldpc-decoder`` supports several belief-propagation algorithms -- sum-product, min-sum, and memory-based variants, plus Sequential Relay BP -- selected via ``bp_method`` and ``composition``, with optional BP+OSD post-processing. For the complete list of methods, parameters, and defaults, see the ``nv-qldpc-decoder`` entries in the :ref:`C++ ` and :ref:`Python ` API reference. Usage Example ~~~~~~~~~~~~~ @@ -139,9 +231,12 @@ The example demonstrates: .. [#f1] [BCGMRY] Sergey Bravyi, Andrew Cross, Jay Gambetta, Dmitri Maslov, Patrick Rall, Theodore Yoder, High-threshold and low-overhead fault-tolerant quantum memory https://arxiv.org/abs/2308.07915 +.. _tensor_network_decoder_example: + Exact Maximum Likelihood Decoding with NVIDIA Tensor Network Decoder +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +Where belief propagation trades exactness for speed, the tensor network decoder computes the exact maximum-likelihood correction — valuable as an accuracy baseline against which the faster decoders can be measured. Starting with CUDA-Q QEC v0.4.0, a GPU-accelerated Maximum Likelihood Decoder is included with the CUDA-Q QEC library. The library follows the CUDA-Q decoder Python interface, namely :class:`cudaq_qec.Decoder`. @@ -169,6 +264,8 @@ See Also: Deploying AI Decoders with TensorRT +++++++++++++++++++++++++++++++++++++++++++++++++ +The decoders above are algorithmic. CUDA-Q QEC can also deploy a *learned* decoder — a neural network trained on a specific code and noise model. + Starting with CUDA-Q QEC v0.5.0, a GPU-accelerated TensorRT-based decoder is included with the CUDA-Q QEC library. The TensorRT decoder (``trt_decoder``) enables users to leverage custom AI models for quantum error correction, providing a flexible framework for deploying trained models @@ -345,10 +442,12 @@ See Also - `TensorRT Documentation `_ - NVIDIA TensorRT - `Stim Documentation `_ - Fast stabilizer circuit simulator +.. _pymatching_decoder_example: + Matching-Based Decoding with PyMatching +++++++++++++++++++++++++++++++++++++++ -CUDA-Q QEC bundles a minimum-weight perfect matching (MWPM) decoder built on the +For codes whose errors pair up into a matching graph, a dedicated matching decoder is often the simplest and fastest choice. Starting with CUDA-Q QEC v0.7.0, CUDA-Q QEC bundles a minimum-weight perfect matching (MWPM) decoder built on the open-source `PyMatching `_ library, suitable for matchable codes such as the surface code. It is selected by name through :func:`cudaq_qec.get_decoder` and takes a parity-check matrix whose @@ -374,13 +473,15 @@ and parallel edges are combined according to ``merge_strategy``. See the :ref:`PyMatching Decoder API ` for the full list of options. +.. _chromobius_decoder_example: + Color-Code Decoding with Chromobius +++++++++++++++++++++++++++++++++++ -For color codes, CUDA-Q QEC bundles a decoder built on the open-source -`Chromobius `_ Mobius decoder. Unlike +Matching applies to surface-code-like codes; color codes call for a decoder built around their structure. Starting with CUDA-Q QEC v0.7.0, CUDA-Q QEC bundles a color-code decoder built on the open-source +`Chromobius `_ Möbius decoder. Unlike the matrix-based decoders, Chromobius is *detector-error-model native*: it is -constructed from Stim detector-error-model (DEM) **text** rather than a +constructed from Stim detector-error-model (DEM) text rather than a parity-check matrix, and predicts logical observable flips directly. .. tab:: Python diff --git a/docs/sphinx/examples_rst/qec/dem_sampling.rst b/docs/sphinx/examples_rst/qec/dem_sampling.rst deleted file mode 100644 index ad379bf45..000000000 --- a/docs/sphinx/examples_rst/qec/dem_sampling.rst +++ /dev/null @@ -1,150 +0,0 @@ -.. _dem_sampling_example: - -DEM Sampling — Monte-Carlo Sampling from Detector Error Models --------------------------------------------------------------- - -A **detector error model** (DEM) describes the probabilistic relationship -between independent error mechanisms and the detectors (syndrome bits) that -observe them. Given a binary check matrix :math:`H` and a -length-:math:`n_\text{mechanisms}` vector of per-mechanism error probabilities -:math:`p`, DEM sampling generates, over ``num_shots`` independent shots, a -random error matrix and the corresponding syndrome matrix via - -.. math:: - - \text{errors}_{ij} \sim \text{Bernoulli}(p_j), \qquad - \text{syndromes} = \text{errors} \cdot H^T \pmod{2}. - -The objects have shapes :math:`H : [n_\text{checks} \times n_\text{mechanisms}]`, -:math:`\text{errors} : [\text{num\_shots} \times n_\text{mechanisms}]`, and -:math:`\text{syndromes} : [\text{num\_shots} \times n_\text{checks}]`, one row -per shot. The :math:`\text{errors} \cdot H^T` form is the batched, row-vector -version of the :math:`S = H \cdot E` convention used elsewhere in the QEC -documentation. - -In Python, ``cudaq_qec.dem_sampling`` provides this capability with automatic -backend selection: it uses GPU-accelerated sampling via cuStabilizer when -available and falls back to a CPU implementation otherwise. In C++ the CPU and -GPU paths are exposed as separate functions in the ``cudaq::qec::dem_sampler`` -namespace (see :ref:`dem_sampling_cpp_api`). - -Example -+++++++ - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/dem_sampling.py - :language: python - :start-after: [Begin Documentation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/dem_sampling.cpp - :language: cpp - :start-after: [Begin Documentation] - - Compile and run with - - .. code-block:: bash - - nvq++ -lcudaq-qec dem_sampling.cpp - ./a.out - -GPU Acceleration -++++++++++++++++ - -When a CUDA-capable GPU is available, ``dem_sampling`` keeps the sampling and -syndrome computation on-device, which is significantly faster than per-shot CPU -sampling, especially for large numbers of shots and sparse error models (low -probabilities): - -1. **Sparse Bernoulli sampling** — Errors are generated directly in compressed - sparse row (CSR) format. For low error probabilities the CSR representation - is compact, and the sampler skips mechanisms with zero probability entirely - rather than evaluating a Bernoulli trial for every mechanism in every shot. - -2. **GF(2) sparse-dense matrix multiply** — Syndromes are computed as - :math:`\text{errors} \times H^T \pmod{2}` using a sparse-dense multiply - over GF(2). The check matrix :math:`H^T` is stored in a bitpacked layout, - reducing memory bandwidth by 8x compared to one byte per entry. - -3. **On-device packing and unpacking** — :math:`H` is transposed and bitpacked - on the GPU in a single kernel. Syndromes are unpacked from the bitpacked - result, and the dense error matrix is produced from the CSR representation - via a fused zero-and-scatter kernel. - -The CPU path uses ``std::bernoulli_distribution`` per mechanism per shot -followed by a dense dot product for the syndrome. - -Input Types and Backend Selection -+++++++++++++++++++++++++++++++++ - -The ``backend`` parameter controls where sampling runs: - -- ``"auto"`` (default) — try GPU first, fall back to CPU. -- ``"gpu"`` — require GPU; raise ``RuntimeError`` if unavailable. -- ``"cpu"`` — always use the CPU path. - -The Python binding accepts several input types, each routed through a different -code path: - -1. **NumPy arrays** (most common) — When the GPU is available the bindings - automatically allocate device memory, copy inputs host-to-device, run - cuStabilizer, and copy results back as NumPy ``uint8`` arrays. With - ``backend="cpu"`` the GPU path is skipped entirely. No user action is - required beyond passing standard ``uint8`` and ``float64`` arrays. - -2. **PyTorch CUDA tensors** — The GPU path reads input device pointers directly - via ``data_ptr()`` and writes outputs into ``torch.empty`` tensors on the - same device, avoiding any host-device copies. This is the fastest path when - inputs are already on the GPU. PyTorch is an optional dependency; install - with ``pip install torch``. - -3. **PyTorch CPU tensors** — With ``backend="gpu"`` the tensors are - automatically moved to CUDA (via ``.to(device)``) before sampling. With - ``backend="auto"`` CPU tensors are rejected with an error; convert them to - NumPy with ``.numpy()`` first. - -The C++ API exposes two namespaces: - -- ``cudaq::qec::dem_sampler::cpu::sample_dem`` — takes a ``cudaqx::tensor`` - check matrix and a ``std::vector`` of probabilities; returns - ``(syndromes, errors)`` as tensors. -- ``cudaq::qec::dem_sampler::gpu::sample_dem`` — takes raw device pointers and - writes results into caller-provided device buffers; returns ``false`` if - cuStabilizer is not available at runtime. - -The ``gpu`` overload works with device pointers that you allocate, populate, -and free yourself. Guard the call behind a device-count check and fall back to -the ``cpu`` overload when it returns ``false``: - -.. code-block:: cpp - - #include "cudaq/qec/dem_sampling.h" - #include - - // H: [num_checks x num_mechanisms] uint8, probs: [num_mechanisms] double. - uint8_t *d_H, *d_syndromes, *d_errors; - double *d_probs; - cudaMalloc(&d_H, num_checks * num_mechanisms); - cudaMalloc(&d_probs, num_mechanisms * sizeof(double)); - cudaMalloc(&d_syndromes, num_shots * num_checks); - cudaMalloc(&d_errors, num_shots * num_mechanisms); - cudaMemcpy(d_H, h_data, num_checks * num_mechanisms, cudaMemcpyHostToDevice); - cudaMemcpy(d_probs, prob_data, num_mechanisms * sizeof(double), - cudaMemcpyHostToDevice); - - bool ok = cudaq::qec::dem_sampler::gpu::sample_dem( - d_H, num_checks, num_mechanisms, d_probs, num_shots, /*seed=*/42, - d_syndromes, d_errors); - if (!ok) { - // cuStabilizer unavailable at runtime — use the cpu overload instead. - } - // Copy d_syndromes / d_errors back to host, then cudaFree each buffer. - -See Also -++++++++ - -- :doc:`/api/qec/python_api` — ``dem_sampling`` Python API reference -- :doc:`/api/qec/cpp_api` — ``dem_sampler`` C++ API reference -- :doc:`/examples_rst/qec/decoders` — Decoder examples that consume syndromes diff --git a/docs/sphinx/examples_rst/qec/dyn_dem.rst b/docs/sphinx/examples_rst/qec/dyn_dem.rst index 8a6f8f2fe..82ad989ef 100644 --- a/docs/sphinx/examples_rst/qec/dyn_dem.rst +++ b/docs/sphinx/examples_rst/qec/dyn_dem.rst @@ -175,4 +175,4 @@ See also - :ref:`dyn_dem_python_api` — Python API reference - :ref:`dyn_dem_cpp_api` — C++ API reference - :doc:`/examples_rst/qec/realtime_decoding` — decoder configuration YAML -- :doc:`/examples_rst/qec/dem_sampling` — sampling from a flat DEM +- :ref:`DEM Sampling example ` — sampling from a flat DEM diff --git a/docs/sphinx/examples_rst/qec/examples.rst b/docs/sphinx/examples_rst/qec/examples.rst index e5addeb59..9c8a657b5 100644 --- a/docs/sphinx/examples_rst/qec/examples.rst +++ b/docs/sphinx/examples_rst/qec/examples.rst @@ -7,15 +7,7 @@ Examples that illustrate how to use CUDA-QX for application development are avai .. toctree:: :maxdepth: 1 - Code-Capacity QEC - Circuit-Level QEC - Decoding From Stim DEM Text + Creating New QEC Codes + Experiments and Noise Modeling Decoders - Improving Relay BP Decoding With Gamma Ensembles - DEM Sampling - Dynamic DEM Construction - Real-Time Decoding - Realtime Decoding with CUDA-Q Decoding Server - AI Predecoder with CUDA-Q Realtime - AI Predecoder with CUDA-Q Realtime (with FPGA Data Injection) - Relay BP Decoding with CUDA-Q Realtime + Realtime Decoding diff --git a/docs/sphinx/examples_rst/qec/getting_started_realtime_decoding.rst b/docs/sphinx/examples_rst/qec/getting_started_realtime_decoding.rst new file mode 100644 index 000000000..c36c3de5c --- /dev/null +++ b/docs/sphinx/examples_rst/qec/getting_started_realtime_decoding.rst @@ -0,0 +1,674 @@ +Getting Started with Realtime Decoding +====================================== + +This walkthrough builds a complete realtime decoding application end to end: decoder configuration, backend selection, compilation, and troubleshooting. Each stage of the underlying ``real_time_complete`` example is shown in place in the sections below. + +A realtime decoding application has the following main components: + +- Decoder configuration file: Initializes and configures the decoders before circuit execution. + +- Quantum kernel: Uses the realtime decoding API to interact with the decoders, primarily through reset_decoder, enqueue_syndromes, and get_corrections. + +- Syndrome extraction: Measures the stabilizers of the logical qubits. + +- Correction application: Applies the corrections to the logical qubits. + +- Logical observable measurement: Measures the logical observables of the logical qubits. + +- Decoder finalization: Frees up resources after circuit execution. + +The API is designed to be called from within quantum kernels (marked with ``@cudaq.kernel`` in Python or ``__qpu__`` in C++). The runtime automatically routes these calls to the appropriate backend—whether a simulation environment on the local machine or a low-latency connection to quantum hardware. The API is device-agnostic, so the same kernel code works across different deployment scenarios. + +The user is required to provide a configuration file or generate one if it is not present. The generation process depends on the decoder type and the detector error model studied in other sections of the documentation. Moreover, the user must write an appropriate kernel that describes the correct syndrome extraction and correction application logic. + +The next section provides instructions to generate a configuration file, write a quantum kernel, and compile and run the examples correctly. + + +Configuration +------------- + +The configuration process transforms a quantum circuit's error characteristics into a format that decoders can efficiently process. This section walks through each step in detail, showing how to go from circuit simulation to a fully configured realtime decoder. + +Step 1: Generate Detector Error Model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The first step is to characterize the quantum circuit's behavior under noise. +A detector error model (DEM) captures the relationship between physical errors and the syndrome patterns they produce. +This characterization is circuit-specific and depends on the code structure, noise model, and measurement schedule. + +Under the hood, the CUDA-Q QEC library uses the Memory Syndrome Matrix (MSM) representation to efficiently encode error propagation information. The MSM captures all possible error chains and their syndrome signatures, tracking how errors propagate through the circuit over time. However, this complexity is abstracted away from the user through convenient helper functions. + +The library provides a family of ``dem_from_memory_circuit`` functions that automatically handle the MSM generation and processing: + +* ``z_dem_from_memory_circuit``: For circuits measuring Z-basis stabilizers (used in the example below) +* ``x_dem_from_memory_circuit``: For circuits measuring X-basis stabilizers +* ``dem_from_memory_circuit``: General-purpose function for arbitrary stabilizer measurements + +These functions take a quantum code, an initial state preparation operation, the number of measurement rounds, and a noise model, then return a complete detector error model ready for decoder configuration. The user simply needs to configure the noise model and specify the circuit structure—the library handles all the error tracking and matrix construction automatically. + +Here is how to generate a DEM for a circuit: + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/real_time_complete.py + :language: python + :start-after: # [Begin DEM Generation] + :end-before: # [End DEM Generation] + +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp + :language: cpp + :start-after: // [Begin DEM Generation] + :end-before: // [End DEM Generation] + +Step 2: Configure and Save Decoder +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Once a DEM has been generated, the next step is to package this information into a decoder configuration and save it to a YAML file. +The configuration structure holds all the parameters a decoder needs: the parity check matrix (H_sparse), +the observable flip matrix (O_sparse), the detector error matrix (D_sparse), +and decoder-specific tuning parameters. + +These matrices are generated in sparse matrix format, which is crucial for performance. +They can be large considering error correcting codes with large number of physical qubits, and moreover, +realtime decoders process thousands of syndrome measurements per second, and make decisions based on these matrices, so compact representations are essential. +The helper function ``pcm_to_sparse_vec`` is used to convert the dense binary matrices into a space-efficient format where -1 marks row boundaries and integers represent column indices of non-zero elements. + +Each decoder type has its own configuration structure with specific parameters. +For lookup table decoders, the user specifies how many simultaneous errors to consider. +For PyMatching, the user can specify per-error prior probabilities and the edge +merge strategy. The realtime path configures PyMatching as a standard decoder +with ``type: pymatching``; ``O_sparse`` remains the observable matrix used by the +base decoder to accumulate logical corrections returned by ``get_corrections``. +Vanilla PyMatching requires graphlike detector error models, where every +``H_sparse`` column has one or two detector entries. +For belief propagation decoders, the user sets iteration limits and convergence criteria. +Decoder parameters are validated against the parameter schema each decoder registers, ensuring unknown keys are rejected and required parameters are present. + +The configuration is then saved to a YAML file for reuse. The YAML format is human-readable, making it easy to inspect, modify, and share configurations across different execution environments. + +Use :func:`~cudaq_qec.decoder_context_from_memory_circuit` to obtain the parity-check, observable, +and measurement-to-detector matrices in one call, then assemble the decoder config: + +.. code-block:: python + + ctx = qec.decoder_context_from_memory_circuit(code, statePrep, num_rounds, noise) + dem, m2d, m2o = ctx.z_component() # or x_component() / full_component() + + config = qec.decoder_config() + config.id = 0 + config.type = "pymatching" + config.block_size = dem.num_error_mechanisms() + config.syndrome_size = dem.num_detectors() + config.H_sparse = qec.pcm_to_sparse_vec(dem.detector_error_matrix) + config.O_sparse = qec.pcm_to_sparse_vec(dem.observables_flips_matrix) + config.D_sparse = qec.d_sparse(m2d) + + config.decoder_custom_args = { + "error_rate_vec": list(dem.error_rates), + "merge_strategy": "smallest_weight", + } + + multi_config = qec.multi_decoder_config() + multi_config.decoders = [config] + +This produces YAML with a ``pymatching`` decoder and PyMatching-specific custom +arguments: + +.. code-block:: yaml + + decoders: + - id: 0 + type: pymatching + cuda_device_id: 0 # optional: pin this decoder to a CUDA device + block_size: 3 + syndrome_size: 3 + H_sparse: [ 0, -1, 1, -1, 2, -1 ] + O_sparse: [ 0, -1, 1, -1, 2, -1 ] + D_sparse: [ 0, -1, 1, -1, 2, -1 ] + decoder_custom_args: + error_rate_vec: [ 0.1, 0.1, 0.1 ] + merge_strategy: smallest_weight + +The ``decoder_custom_args`` section is converted between YAML and the +parameter map a decoder's constructor receives using a *parameter schema* +registered under the decoder's name. All built-in decoders ship with a +schema, and custom (out-of-tree) decoder plugins can register their own so +their parameters become configurable through the same YAML -- no changes to +the CUDA-Q QEC libraries are required. A plugin registers its schema from a +static initializer in the same shared library that registers the decoder +itself (see ``cudaq/qec/decoder_config_schema.h`` and the in-tree example +plugin ``single_error_lut_example``): + +.. code-block:: cpp + + #include "cudaq/qec/decoder_config_schema.h" + + namespace { + struct schema_registrar { + schema_registrar() { + using k = cudaq::qec::decoding::config::param_kind; + cudaq::qec::decoding::config::decoder_schema schema{ + "my_decoder", + { + {"strength", k::f64}, + {"passes", k::int32}, + {"mode", k::string, /*required=*/true}, + }}; + // Optional: cross-field constraints the per-key specs can't express. + // Unknown keys and missing required keys are already rejected by the + // framework; a decoder never implements those checks itself. + schema.validate = [](const cudaqx::heterogeneous_map &args) { + if (args.contains("strength") && args.get("strength") <= 0.0) + throw std::runtime_error("my_decoder: strength must be positive"); + }; + cudaq::qec::decoding::config::register_decoder_schema( + std::move(schema)); + } + }; + schema_registrar register_schema; + } // namespace + +With the schema in place, a ``decoder_custom_args`` section for +``type: my_decoder`` is validated (unknown keys and missing required keys are +rejected, then the schema's ``validate`` hook runs) and delivered to the +decoder's constructor as a ``cudaqx::heterogeneous_map``. The same checks can +be applied to a configuration built programmatically -- before it is +serialized or used -- by calling ``decoder_config::validate_custom_args()`` +(``config.validate_custom_args()`` in Python, also available on +``multi_decoder_config``). The registered schemas can be inspected from +Python via ``qec.decoder_param_schema("my_decoder")`` and +``qec.registered_decoder_schemas()``. + +The registered schemas can also be exported as a standard JSON Schema +(draft 2020-12) document via ``qec.decoder_config_json_schema()``, so +configuration YAML files can be validated by third-party tooling -- editors, +CI checks, or the `check-jsonschema +`_ command line tool -- without +loading the CUDA-Q QEC libraries: + +.. code-block:: bash + + python3 -c "import cudaq_qec; print(cudaq_qec.decoder_config_json_schema())" > decoder_config_schema.json + check-jsonschema --schemafile decoder_config_schema.json my_config.yaml + +The export is generated from the schemas registered at call time, so decoder +plugins loaded in the process (including out-of-tree ones) appear in it +automatically. Schema ``validate`` hooks are arbitrary code and cannot be +represented in JSON Schema, so a file that passes the exported schema may +still be rejected by a hook when the configuration is parsed. + +``cuda_device_id`` pins a GPU-accelerated decoder (e.g. ``nv-qldpc-decoder`` +or ``trt_decoder``) to a specific CUDA device. The same knob is available as +a construction parameter in C++ and Python +(``qec.get_decoder("trt_decoder", H, cuda_device_id=1)``). The thread that +creates a decoder is pinned to that device and is expected to drive its +decode calls; create each pinned decoder on its own thread to place several +decoders on different GPUs. + +Here is how to create and save a decoder configuration: + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/real_time_complete.py + :language: python + :start-after: # [Begin Save DEM] + :end-before: # [End Save DEM] + +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp + :language: cpp + :start-after: // [Begin Save DEM] + :end-before: // [End Save DEM] + +Step 3: Load Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before running quantum circuits with realtime decoding, the saved decoder configuration must be loaded and initialized. +This step bridges the gap between the offline characterization phase (Steps 1-2) and the online execution phase (Step 4), +preparing the decoder instances for realtime operation. + +The configuration loading process performs several important operations: + +1. **YAML Parsing**: The configuration file is parsed and validated to ensure all required fields are present and properly formatted. This includes checking matrix dimensions, decoder parameters, and metadata. + +2. **Decoder Instantiation**: Based on the decoder type specified in the configuration (e.g., ``multi_error_lut``, ``pymatching``, ``nv-qldpc-decoder``), the appropriate decoder implementation is instantiated and allocated resources on the GPU or CPU. + +3. **Matrix Initialization**: The sparse matrices (H_sparse, O_sparse, D_sparse) are loaded into the decoder's internal data structures. For GPU-based decoders, this includes transferring data to device memory. + +4. **Decoder-Specific Initialization**: Each decoder type performs its own preparation: lookup table decoders build syndrome-to-correction mappings, belief propagation decoders initialize message-passing structures, and sliding window decoders configure their buffering mechanisms. + +5. **Backend Registration**: The decoder instances are registered with the CUDA-Q runtime so they can be accessed from quantum kernels using their unique IDs. + +This initialization happens quickly, typically only a few milliseconds for small codes and up to a few seconds for large distance codes with complex decoders. Since it occurs before quantum circuit execution, it does not impact the latency-critical decoding operations. + +The separation of configuration from execution provides significant benefits: users can maintain a library of configurations for different code distances, noise levels, and decoder types, then simply load the appropriate one when running experiments. Configurations can be version-controlled alongside code, shared across research teams, and validated offline before deployment to quantum hardware. + +Here is how to load a decoder configuration: + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/real_time_complete.py + :language: python + :start-after: # [Begin Load DEM] + :end-before: # [End Load DEM] + +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp + :language: cpp + :start-after: // [Begin Load DEM] + :end-before: // [End Load DEM] + +Step 4: Use in Quantum Kernels +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With decoders configured and initialized, they can be used within quantum kernels. The realtime decoding API provides three key functions that integrate seamlessly with CUDA-Q's quantum programming model: ``reset_decoder`` prepares a decoder for a new shot, ``enqueue_syndromes`` sends syndrome measurements to the decoder for processing, and ``get_corrections`` retrieves the decoder's recommended corrections. + +These functions are designed to be called from within quantum kernels (marked with ``@cudaq.kernel`` in Python or ``__qpu__`` in C++). The runtime automatically routes these calls to the appropriate backend - whether that is a simulation environment on the local machine or a low-latency connection to quantum hardware. The API is device-agnostic, so the same kernel code works across different deployment scenarios. + +The typical usage pattern is: reset the decoder at the start of each shot, enqueue +syndromes after each stabilizer measurement round, then get corrections before +measuring the logical observables. Decoders process syndromes asynchronously, so +by the time ``get_corrections`` is called, the decoder has usually finished its +analysis. If decoding takes longer than expected, ``get_corrections`` will block +until results are available. + +.. note:: + While resetting the decoder at the beginning of each shot isn't strictly + required, it is **strongly** recommended to ensure that when running on a + remote QPU, any potential errors encountered in one shot do not affect future + shot results. + +Here is how to use the realtime decoding API in quantum kernels: + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/real_time_complete.py + :language: python + :start-after: # [Begin QEC Circuit] + :end-before: # [End QEC Circuit] + +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp + :language: cpp + :start-after: // [Begin QEC Circuit] + :end-before: // [End QEC Circuit] + +Backend Selection +----------------- + +CUDA-Q QEC's realtime decoding system is designed to work seamlessly across different execution environments. The backend selection determines where quantum circuits run and how decoders communicate with the quantum processor. Understanding the differences between simulation and hardware backends helps the user develop efficiently and deploy confidently. + +Simulation Backend +^^^^^^^^^^^^^^^^^^ + +The simulation backend is the primary tool during development, testing, and +algorithm validation. It runs entirely on the local machine, using quantum +simulators like Stim to execute circuits while decoders process syndromes and +calculation corrections. This setup is ideal for rapid iteration: the user can +test decoder configurations, validate circuit logic, and debug syndrome +processing without waiting for hardware access or paying for compute time. + +The simulation backend mimics realtime decoding's concurrent operation by +running the decoder(s) within the same process as the simulator. This means that +other than GPU hardware differences between the local environment and the remote +NVQLink decoders, the decoders behave the same way whether testing locally or +running on a quantum computer. The main difference is that simulation does not +have the same strict latency constraints, making it easier to experiment with +complex decoder configurations. + +Use the simulation backend for local development and testing: + +.. tab:: Python + + .. code-block:: python + + import cudaq + import cudaq_qec as qec + + cudaq.set_target("stim") # Or other simulator + qec.configure_decoders_from_file("config.yaml") + + # Create an empty noise model; add noise channels as needed + noise_model = cudaq.NoiseModel() + results = cudaq.run(my_circuit, shots_count=100, + noise_model=noise_model) + +.. tab:: C++ + + .. code-block:: bash + + # Compile with simulation support + nvq++ -std=c++20 my_circuit.cpp -lcudaq-qec \ + -lcudaq-qec-decoders \ + -lcudaq-qec-realtime-decoding \ + -lcudaq-qec-realtime-decoding-simulation + + ./a.out + +Quantinuum Hardware Backend +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Quantinuum hardware backend connects quantum circuits to real ion-trap quantum computers. Unlike the simulation backend where decoders run on the local machine, **the Quantinuum backend uploads the decoder configuration to Quantinuum's infrastructure**, where decoders run on GPU-equipped servers co-located with the quantum hardware. This architecture minimizes latency between syndrome measurements and correction application. + +**Important Setup Requirements:** + +1. **Configuration Upload**: When ``configure_decoders_from_file()`` or ``configure_decoders()`` is called, the decoder configuration is automatically base64-encoded and uploaded to Quantinuum's REST API (``api/gpu_decoder_configs/v1beta/``). This happens before job submission. The configuration includes all decoder parameters, error models, and sparse matrices. + +2. **Extra Payload Provider**: The user **must** specify ``extra_payload_provider="decoder"`` when setting the target. This registers a payload provider that injects the decoder configuration UUID into each job request, telling Quantinuum which decoder configuration to use for the circuit. + +3. **Backend Compilation**: For C++, the user must link against ``-lcudaq-qec-realtime-decoding-quantinuum`` instead of the simulation library. This library implements the Quantinuum-specific communication protocol for syndrome transmission. + +4. **Configuration Lifetime**: Decoder configurations persist on Quantinuum's servers and are referenced by UUID. If the configuration is modified, it must be uploaded again - the system will generate a new UUID and use the new configuration for subsequent jobs. + +Note: The realtime decoding interfaces are experimental, and subject to change. Realtime decoding on Quantinuum's Helios-1 device is currently only available to partners and collaborators. Please email QCSupport@quantinuum.com for more information. + +**Emulation vs. Hardware Modes:** + +Emulation mode (``emulate=True``) is particularly valuable for testing the deployment setup without consuming hardware credits. Running with this flag performs a local, noise-free simulation without any actual submission to Quantinuum's servers. + +Use the Quantinuum backend for hardware or emulation: + +.. tab:: Python + + .. code-block:: python + + cudaq.set_target("quantinuum", + emulate=False, # True for emulation + machine="Helios-1", + extra_payload_provider="decoder") + + qec.configure_decoders_from_file("config.yaml") + results = cudaq.run(my_circuit, shots_count=100) + +.. tab:: C++ + + .. code-block:: bash + + # Compile for Quantinuum + nvq++ --target quantinuum --quantinuum-machine Helios-1 \ + --quantinuum-extra-payload-provider decoder \ + my_circuit.cpp -lcudaq-qec \ + -lcudaq-qec-decoders \ + -lcudaq-qec-realtime-decoding \ + -lcudaq-qec-realtime-decoding-quantinuum \ + -Wl,--export-dynamic + + ./a.out + +Compilation and Execution Examples +----------------------------------- + +This section provides **complete, tested compilation and execution commands** for both simulation and hardware backends, extracted from the CUDA-Q QEC test infrastructure. The section begins with common usage patterns that guide decoder and compilation choices, then provides the specific commands needed for each backend. + +Common Use Cases +^^^^^^^^^^^^^^^^^^^^^^ + +Before diving into compilation details, it is helpful to understand the typical scenarios and how they map to decoder choices and workflow parameters. +A full set of common examples is provided to guide development. +These examples describe the complete workflow for developing an application that uses realtime decoding in a single file. +The relevant C++ and Python examples can be found at the following path: +`libs/qec/unittests/realtime/app_examples `_. +The files have names like ``surface_code-1.cpp`` and ``surface_code_1.py``. The rest of this section shows how to compile and run these 2 examples. + +These examples provide comprehensive support for application development with realtime decoding. +The subsequent step, once the user has chosen the appropriate decoder and the appropriate backend, is to compile and execute the application. +Instructions are provided below for both the simulation and the hardware backends. + +C++ Compilation +^^^^^^^^^^^^^^^ + +**Simulation Backend (Stim)** + +Compile with the simulation backend for local testing: + +.. code-block:: bash + + nvq++ --target stim surface_code-1.cpp \ + -lcudaq-qec \ + -lcudaq-qec-decoders \ + -lcudaq-qec-realtime-decoding \ + -lcudaq-qec-realtime-decoding-simulation \ + -o surface_code-1 + + # Execute + ./surface_code-1 --distance 3 --num_shots 1000 --save_dem config.yaml + +**Key Points:** + +- ``--target stim``: Use the Stim quantum simulator +- ``-lcudaq-qec``: Core QEC library with codes and experiments +- ``-lcudaq-qec-decoders``: Decoder core API (decoders, ``sparse_binary_matrix``, and PCM utilities such as ``pcm_to_sparse_vec``) +- ``-lcudaq-qec-realtime-decoding``: Realtime decoding core API +- ``-lcudaq-qec-realtime-decoding-simulation``: Simulation-specific decoder backend + +**Quantinuum Backend (Hardware)** + +Compile for actual Quantinuum hardware: + +.. code-block:: bash + + nvq++ --target quantinuum \ + --quantinuum-machine Helios-1 \ + --quantinuum-extra-payload-provider decoder \ + surface_code-1.cpp \ + -lcudaq-qec \ + -lcudaq-qec-decoders \ + -lcudaq-qec-realtime-decoding \ + -lcudaq-qec-realtime-decoding-quantinuum \ + -Wl,--export-dynamic \ + -o surface_code-1-quantinuum-hardware + + # Execute + export CUDAQ_QUANTINUUM_CREDENTIALS= + ./surface_code-1-quantinuum-hardware --distance 3 --num_shots 100 --load_dem config.yaml + +**Key Points:** + +- Use Quantinuum target names: ``Helios-1``, ``Helios-1E``, ``Helios-1SC``, etc. +- Currently only ``Helios-1`` will run the GPU decoders. The ``Helios-1E`` emulator will not run the GPU decoders. +- Set ``CUDAQ_QUANTINUUM_CREDENTIALS`` environment variable with the user's credentials. + Check out the `Quantinuum hardware backend documentation `_ for more information. + +**Emulated Quantinuum Compilation Workflow** + +Compile for Quantinuum emulation mode: + +.. code-block:: bash + + nvq++ --target quantinuum --emulate \ + --quantinuum-machine Helios-Fake \ + surface_code-1.cpp \ + -lcudaq-qec \ + -lcudaq-qec-decoders \ + -lcudaq-qec-realtime-decoding \ + -lcudaq-qec-realtime-decoding-quantinuum \ + -Wl,--export-dynamic \ + -o surface_code-1-quantinuum-emulate + + # Execute + ./surface_code-1-quantinuum-emulate --distance 3 --num_shots 1000 --load_dem config.yaml + +**Key Points:** + +- ``--target quantinuum --emulate``: Emulate Quantinuum compilation path +- ``--quantinuum-machine Helios-Fake``: Specify machine (``Helios-Fake`` for emulation) +- ``-lcudaq-qec-realtime-decoding-quantinuum``: Quantinuum-specific decoder backend (replaces ``-simulation``) +- ``-Wl,--export-dynamic``: **Required** linker flag for dynamic symbol resolution + +.. note:: + When running with `--emulate`, there is no noise being applied because there + is currently no way to express noise in target-specific QIR. Therefore, when + running with emulation, users will see noise-free sample data. + +Python Execution +^^^^^^^^^^^^^^^^ + +**Simulation Backend (Stim)** + +.. code-block:: bash + + # Generate a decoder configuration file + python3 surface_code_1.py --distance 3 --save_dem config.yaml + # Run the circuit with the decoder configuration + python3 surface_code_1.py --distance 3 --load_dem config.yaml --num_shots 1000 + + +**Quantinuum Backend (Hardware)** + +.. code-block:: bash + + python3 surface_code_1.py --distance 3 --load_dem config.yaml --num_shots 1000 --target quantinuum --machine_name Helios-1 --project_id + +**Key Points:** + +- Use real machine names (check Quantinuum portal for available machines) +- ``--project_id``: Specify the Quantinuum project ID used for the hardware submission. +- Reduce shot count for hardware experiments (hardware time is expensive) + +**Emulated Quantinuum Compilation Workflow** + +.. code-block:: bash + + python3 surface_code_1.py --distance 3 --load_dem config.yaml --num_shots 1000 --target quantinuum --emulate + +**Key Points:** + +- ``--emulate``: Emulate the Quantinuum execution path +- Decoder config is automatically uploaded to Quantinuum's servers when + ``cudaq_qec.configure_decoders_from_file`` (Python) or + :cpp:func:`cudaq::qec::decoding::config::configure_decoders_from_file` (C++) is called + +Complete Workflow Example +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Given that the user follows the structure of the examples provided, where each executable takes terminal arguments to configure the application, the following workflow can be used to compile and execute the application. + + +.. code-block:: bash + + # Phase 1: Generate Detector Error Model (DEM) + # This is done once per code/distance/noise configuration + + ## C++ + ./surface_code-1 --distance 3 --num_shots 1000 --p_cnot 0.001 \ + --save_dem config_d3.yaml --num_rounds 12 + + ## Python + python3 surface_code_1.py --distance 3 --num_shots 1000 --p_cnot 0.001 \ + --save_dem config_d3.yaml --num_rounds 12 + + # Phase 2: Run with Realtime Decoding + # Use the saved DEM configuration + + ## Simulation + ./surface_code-1 --distance 3 --num_shots 1000 --load_dem config_d3.yaml \ + --num_rounds 12 + + ## Quantinuum Emulation + ./surface_code-1-quantinuum-emulate --distance 3 --num_shots 1000 --load_dem config_d3.yaml \ + --num_rounds 12 + + ## Quantinuum Hardware + export CUDAQ_QUANTINUUM_CREDENTIALS=credentials.json + ./surface_code-1-quantinuum-hardware --distance 3 --num_shots 100 --load_dem config_d3.yaml \ + --num_rounds 12 + +**Application Parameters:** + +- ``--distance``: Code distance (3, 5, 7, etc.) +- ``--num_shots``: Number of circuit repetitions +- ``--p_cnot``: Two-qubit depolarizing rate on CNOT gates for DEM generation +- ``--save_dem``: Generate and save DEM configuration to file +- ``--load_dem``: Load existing DEM configuration from file +- ``--num_rounds``: Total number of syndrome measurement rounds + +Debugging and Environment Variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Useful Environment Variables:** + +.. code-block:: bash + + # Enable decoder configuration debugging + export CUDAQ_QEC_DEBUG_DECODER=1 + + # Set default simulator + export CUDAQ_DEFAULT_SIMULATOR=stim + + # Dump JIT IR for debugging compilation issues + export CUDAQ_DUMP_JIT_IR=1 + + # Set Quantinuum credentials file + export CUDAQ_QUANTINUUM_CREDENTIALS=/path/to/credentials.json + +The variables can be set in the user's environment or in a script. +They are valid both for python and C++ applications, however, they must be set before importing the cudaq or cudaq_qec libraries. + +**Common Compilation Issues:** + +1. **Missing libraries**: Ensure all ``-lcudaq-qec-*`` libraries are linked +2. **Wrong backend library**: Use ``-simulation`` for Stim, ``-quantinuum`` for Quantinuum +3. **Missing** ``-Wl,--export-dynamic`` **flag**: Required for Quantinuum targets +4. **Wrong target flags**: Use ``--emulate`` for emulation. Omit it and provide ``--project_id`` for hardware + +**Common Runtime Issues:** + +1. **"Decoder X not found"**: Call ``configure_decoders_from_file()`` before circuit execution +2. **"Configuration upload failed"**: Check network connectivity and Quantinuum credentials +3. **Dimension mismatch errors**: Verify DEM dimensions match the circuit's syndrome count +4. **High error rates**: Check decoder window size matches DEM generation window + + +Decoder Selection +^^^^^^^^^^^^^^^^^ +The :ref:`Pre-built QEC Decoders ` section provides information about which decoders are compatible with realtime decoding. + +The TRT decoder (``trt_decoder``) can be configured for realtime decoding by specifying +its ``decoder_custom_args`` parameters. This is useful for neural network-based +decoders trained for specific codes and noise models. Note that TRT models +must be trained with the appropriate input/output dimensions matching the +syndrome and error spaces. See :ref:`trt_decoder_api_python` for detailed configuration options. + +Troubleshooting +--------------- + +Even with careful configuration, issues may be encountered during realtime decoding. This section covers the most common problems and their solutions, organized by symptom. When troubleshooting, start by isolating whether the issue is in DEM generation, decoder configuration, or runtime execution. + +Configuration Upload Failures (Quantinuum Backend) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using the Quantinuum backend, the decoder configuration must be uploaded to their REST API before job submission. Upload failures prevent the quantum program from running and can be difficult to diagnose without knowing what to look for. + +**Possible Issues:** + +* **Network connectivity problems**: Connection to Quantinuum's servers is interrupted or unstable +* **Configuration too large**: Decoder configuration exceeds Quantinuum's upload size limits (typically happens with large distance codes and lookup tables) +* **Invalid credentials**: API authentication fails due to expired or incorrect credentials +* **Malformed configuration**: YAML structure is invalid or contains unsupported parameters + +**Solutions**: + +* **Enable debug logging**: Set ``CUDAQ_QEC_DEBUG_DECODER=1`` environment variable to see the exact configuration being uploaded and any error messages from the REST API +* **Check network**: Verify that Quantinuum's API endpoints can be reached before running the program. Test with a simple job submission first. +* **Reduce configuration size**: If uploads fail due to size, switch from lookup table decoders to QLDPC (much more compact), or use sliding window with smaller windows +* **Validate YAML locally**: Before uploading, test that ``multi_decoder_config::from_yaml_str()`` can parse the configuration file without errors +* **Check credentials**: Ensure the Quantinuum API credentials are valid and have not expired. Refresh tokens if necessary. +* **Test with emulation**: Try ``emulate=True`` first - emulation uses the same upload infrastructure but provides faster feedback if there are configuration issues + +**Verification**: + +After fixing configuration issues, the following log messages should appear: + +.. code-block:: text + + [info] Initializing realtime decoding library with config file: config.yaml + [info] Initializing decoders... + [info] Creating decoder 0 of type multi_error_lut + [info] Done initializing decoder 0 in 0.234 seconds + +If errors appear instead, check the full error message - it often contains specific details about what failed (network timeout, size limit, parsing error, etc.). + diff --git a/docs/sphinx/examples_rst/qec/modeling_noise.rst b/docs/sphinx/examples_rst/qec/modeling_noise.rst new file mode 100644 index 000000000..2ce0da630 --- /dev/null +++ b/docs/sphinx/examples_rst/qec/modeling_noise.rst @@ -0,0 +1,352 @@ +Experiments and Noise Modeling +============================== + +These examples walk through several of the most common numerical error-correction experiments with the CUDA-Q QEC library: modeling noise at the **code-capacity** and **circuit-level**, and running full **memory circuit experiments**. For the background behind each, see :doc:`Experiments and Noise Modeling `. + +Code-Capacity Noise Modeling +---------------------------- + +This example implements a code-capacity noise experiment: random ``X``/``Z`` errors are applied directly to the data qubits and decoded with a single-error look-up table. See :ref:`Code-Capacity Noise Modeling ` for more details. + +CUDA-Q QEC Implementation ++++++++++++++++++++++++++++++ +Here's how to use CUDA-Q QEC to perform a code capacity noise model experiment in both Python and C++: + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/code_capacity_noise.py + :language: python + :start-after: [Begin Documentation] + +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/code_capacity_noise.cpp + :language: cpp + :start-after: [Begin Documentation] + + Compile and run with + + .. code-block:: bash + + nvq++ --target=stim -lcudaq-qec -lcudaq-qec-decoders code_capacity_noise.cpp -o code_capacity_noise + ./code_capacity_noise + + +Code Explanation +++++++++++++++++ + +1. QEC Code type: + - CUDA-Q QEC centers around the `qec.code` type, which contains the data relevant for a given code. + - In particular, this represents a collection of qubits which represent a single logical qubit. + - Here we get one of the most well known QEC codes, the Steane code, with the `qec.get_code` function. + - We can get the stabilizers from a code with the `code.get_stabilizers()` function. + - In this example, we get the parity check matrix of the code. Because the Steane code is a CSS code, we can extract just the `Z` components of the parity check matrix. + - Here, we see this matrix has 3 rows and 7 columns, which means there are 7 data qubits (7 possible single bit-flip errors) and 3 Z-stabilizers (parity checks). Note that `Z` stabilizers check for `X` type errors. + - Lastly, we get the logical `Z` observable for the code. This will allow us to see if the `Z` observable of our logical qubit has flipped. + +2. Decoder type: + - A single-error look-up table (LUT) decoder can be acquired with the `qec.get_decoder` call. + - Passing in the parity check matrix gives the decoder the required information to associated syndromes with underlying error mechanisms. + - Once the decode has been constructed, the `decoder.decode(syndrome)` member function is called, which returns a predicted error given the syndrome. + +3. Noise model: + - To generate noisy data, we call `qec.generate_random_bit_flips(nBits, p)` which will return an array of bits, where each bit has probability `p` to have been flipped into 1, and a `1-p` chance to have remained 0. + - Since we are using the `Z` parity check matrix `H_Z`, we want to simulate random `X` errors on our 7 data qubits. + +4. Logical Errors: + - Once we have noisy data, we see what the resulting syndromes are by multiplying our noisy data vector with our parity check matrix (mod 2). + - From this syndrome, we see what errors the decoder predicts occurred in the data. + - To classify as a logical error, the decoder does not need to exactly identify what happened to the data, but only whether there was a flip in the logical observable. + - If the decoder guesses this successfully, we have corrected the quantum error. If not, we have incurred a logical error. + +5. Further automation: + - While this workflow is nice for seeing things step by step, the `qec.sample_code_capacity` API is provided to generate a batch of noisy data and their corresponding syndromes. + +Circuit-level Noise Modeling +---------------------------- +This example runs a circuit-level memory experiment, generating syndromes by executing the stabilizer-measurement circuits under depolarizing noise. See :ref:`Circuit-level Noise Modeling ` for more details. + + +CUDA-Q QEC Implementation ++++++++++++++++++++++++++++++ +Here's how to use CUDA-Q QEC to perform a circuit-level noise model experiment in both Python and C++: + +.. tab:: Python + + .. literalinclude:: ../../examples/qec/python/circuit_level_noise.py + :language: python + :start-after: [Begin Documentation] + +.. tab:: C++ + + .. literalinclude:: ../../examples/qec/cpp/circuit_level_noise.cpp + :language: cpp + :start-after: [Begin Documentation] + + Compile and run with + + .. code-block:: bash + + nvq++ --target=stim -lcudaq-qec -lcudaq-qec-decoders circuit_level_noise.cpp -o circuit_level_noise + ./circuit_level_noise + + +Code Explanation +++++++++++++++++ + +1. QEC Code and Decoder types: + - As in the code capacity example, our central objects are the `qec.code` and `qec.decoder` types. + +2. Clifford simulation backend: + - As the size of QEC circuits can grow quite large, Clifford simulation is often the best tool for these simulations. + - `cudaq.set_target("stim")` selects the highly performant Stim simulator as the simulation backend. + +3. Noise model: + - To add noisy gates we use the `cudaq.NoiseModel` type. + - CUDA-Q supports the generation of arbitrary noise channels. Here we use a `cudaq.Depolarization2` channel to add a depolarization channel. + - This is added to the `CX` gate by adding it to the `X` gate with 1 control. + - This noisy gate is added to every qubit via the `noise.add_all_qubit_channel` function. + +4. Getting circuit-level noisy data: + - The `qec.code` is the first input parameter here, as the code's `stabilizer_round` determines the circuits executed. + - Each memory circuit runs for an input number of `nRounds`, which specifies how many `stabilizer_round` kernels are run. + - After `nRounds` the data qubits are measured and the run is over. + - This is performed `nShots` number of times. + - During a shot, each stabilizer round's syndrome is `xor`'d against the preceding syndrome, so that we can track a sparser flow of data showing which round each parity check was violated. + - The first round returns the syndrome as is, as there is nothing preceding to `xor` against. + +5. Data qubit measurements: + - The data qubits are only read out after the end of each shot, so there are `nShots` worth of data readouts. + - The basis of the data qubit measurements depends on the state preparation used. + - Z-basis readout when preparing the logical `|0>` or logical `|1>` state with the `qec.operation.prep0` or `qec.operation.prep1` kernels. + - X-basis readout when preparing the logical `|+>` or logical `|->` state with the `qec.operation.prepp` or `qec.operation.prepm` kernels. + +6. Logical Errors: + - From here, the decoding procedure is again similar to the code capacity case, except that we use a Pauli frame to track errors that happen each QEC cycle. + - The final values of the Pauli frame tell us how our logical state flipped during the experiment, and what needs to be done to correct it. + - We compare our known initial state (corrected by the Pauli frame), against our measured data qubits to determine if a logical error occurred. + + +The CUDA-Q QEC library thus provides a platform for numerical QEC experiments. The `qec.code` can be used to analyze a variety of QEC codes (both library or user provided), with a variety of decoders (both library or user provided). +The CUDA-Q QEC library also provides tools to speed up the automation of generating noisy data and syndromes. + + +Memory Circuit Experiments +-------------------------- + +The ``sample_memory_circuit`` API runs a memory circuit experiment end to end -- preparing a logical state, running rounds of stabilizer measurement under noise, and measuring the data qubits. See :ref:`Memory Circuit Experiments ` for more details. + +Function Variants ++++++++++++++++++ + +.. tab:: Python + + .. code-block:: python + + import cudaq + import cudaq_qec as qec + + # Use the stim backend for performance in QEC settings + cudaq.set_target("stim") + + # Get a code instance + code = qec.get_code("steane") + + # Basic memory circuit with |0⟩ state + syndromes, measurements = qec.sample_memory_circuit( + code, # QEC code instance + numShots=1000, # Number of circuit executions + numRounds=1 # Number of stabilizer rounds + ) + + # Memory circuit with custom initial state + syndromes, measurements = qec.sample_memory_circuit( + code, # QEC code instance + op=qec.operation.prep1, # Initial state + numShots=1000, # Number of shots + numRounds=1 # Number of rounds + ) + + # Memory circuit with noise model + noise = cudaq.NoiseModel() + # Configure noise + noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.01), 1) + syndromes, measurements = qec.sample_memory_circuit( + code, # QEC code instance + numShots=1000, # Number of shots + numRounds=1, # Number of rounds + noise=noise # Noise model + ) + +.. tab:: C++ + + .. code-block:: cpp + + // Basic memory circuit with |0⟩ state + auto [syndromes, measurements] = qec::sample_memory_circuit( + code, // QEC code instance + numShots, // Number of circuit executions + numRounds // Number of stabilizer rounds + ); + + // Memory circuit with custom initial state + auto [syndromes, measurements] = qec::sample_memory_circuit( + code, // QEC code instance + operation::prep1, // Initial state preparation + numShots, // Number of circuit executions + numRounds // Number of stabilizer rounds + ); + + // Memory circuit with noise model + auto noise_model = cudaq::noise_model(); + noise_model.add_channel(...); // Configure noise + auto [syndromes, measurements] = qec::sample_memory_circuit( + code, // QEC code instance + numShots, // Number of circuit executions + numRounds, // Number of stabilizer rounds + noise_model // Noise model to apply + ); + +Return Values ++++++++++++++ + +The functions return a tuple containing: + +1. **Syndrome Measurements** (:code:`tensor`): + + * Shape: :code:`(num_shots, num_detectors)` + * Columns follow the layout ``[ B S S … S B ]``, where: + + - ``B`` (boundary block) = ``numAncZ = code.get_num_z_stabilizers()`` for Z-basis + preparations (``prep0``/``prep1``), or ``numAncX = code.get_num_x_stabilizers()`` + for X-basis preparations (``prepp``/``prepm``) + - ``S`` (inter-round block) = ``numAncZ + numAncX`` detectors per round transition + (``num_rounds - 1`` blocks total) + - Total: ``num_detectors = 2*B + (num_rounds - 1)*S`` + * Values are 0 or 1 representing measurement outcomes + +2. **Data Measurements** (:code:`tensor`): + + * Shape: :code:`(num_shots, block_size)` + * Contains final data qubit measurements + * Used to verify logical state preservation + +Example Usage ++++++++++++++ + +Example of running a memory experiment: + +.. tab:: Python + + .. code-block:: python + + import cudaq + import cudaq_qec as qec + + # Use the stim backend for performance in QEC settings + cudaq.set_target("stim") + + # Create code and decoder + code = qec.get_code('steane') + decoder = qec.get_decoder('single_error_lut', + code.get_parity()) + + # Configure noise + noise = cudaq.NoiseModel() + noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.01), 1) + + # Run memory experiment + syndromes, measurements = qec.sample_memory_circuit( + code, + op=qec.operation.prep0, + numShots=1000, + numRounds=10, + noise=noise + ) + + # Analyze results + for shot in range(1000): + # Get syndrome for this shot + syndrome = syndromes[shot].tolist() + + # Decode syndrome + result = decoder.decode(syndrome) + if result.converged: + # Process correction + pass + +.. tab:: C++ + + .. code-block:: cpp + + // Compile and run with: + // nvq++ --target=stim -lcudaq-qec -lcudaq-qec-decoders example.cpp + // ./a.out + + #include "cudaq.h" + #include "cudaq/qec/decoder.h" + #include "cudaq/qec/experiments.h" + #include "cudaq/qec/noise_model.h" + + int main(){ + // Create a Steane code instance + auto code = cudaq::qec::get_code("steane"); + + // Configure noise model + cudaq::noise_model noise; + noise.add_all_qubit_channel("x", cudaq::depolarization2(0.1), + /*num_controls=*/1); + + // Run memory experiment + auto [syndromes, data] = cudaq::qec::sample_memory_circuit( + *code, // Code instance + cudaq::qec::operation::prep0, // Prepare |0⟩ state + 1000, // 1000 shots + 1, // 1 rounds + noise // Apply noise + ); + + // Analyze results + auto decoder = cudaq::qec::get_decoder("single_error_lut", code->get_parity()); + for (std::size_t shot = 0; shot < 1000; shot++) { + // Get syndrome for this shot + std::vector syndrome(syndromes.shape()[1]); + for (std::size_t i = 0; i < syndrome.size(); i++) + syndrome[i] = syndromes.at({shot, i}); + + // Decode syndrome + auto results = decoder->decode(syndrome); + // Process correction + // ... + } + } + +Additional Noise Models ++++++++++++++++++++++++ + +.. tab:: Python + + .. code-block:: python + + noise = cudaq.NoiseModel() + + # Add multiple error channels + noise.add_all_qubit_channel('h', cudaq.BitFlipChannel(0.001)) + + # Specify two qubit errors + noise.add_all_qubit_channel("x", cudaq.Depolarization2(p), 1) + +.. tab:: C++ + + .. code-block:: cpp + + cudaq::noise_model noise; + + // Add multiple error channels + noise.add_all_qubit_channel( + "x", cudaq::bit_flip_channel(/*probability*/ 0.01)); + + // Specify two qubit errors + noise.add_all_qubit_channel( + "x", cudaq::depolarization2(/*probability*/ 0.01), + /*numControls*/ 1); diff --git a/docs/sphinx/examples_rst/qec/realtime_decoding.rst b/docs/sphinx/examples_rst/qec/realtime_decoding.rst index 3fb0de238..c2b4b722b 100644 --- a/docs/sphinx/examples_rst/qec/realtime_decoding.rst +++ b/docs/sphinx/examples_rst/qec/realtime_decoding.rst @@ -1,723 +1,23 @@ -Real-Time Decoding +Realtime Decoding ================== -Real-time decoding enables CUDA-Q QEC decoders to operate in low-latency, online environments where decoders run concurrently with quantum computations. This capability is essential for quantum error correction on real quantum hardware, where corrections must be calculated and applied within qubit coherence times. +Realtime decoding runs CUDA-Q QEC decoders concurrently with quantum execution, applying corrections within qubit coherence times. For how it works, the four-stage workflow, and terminology, see :doc:`Realtime Decoding `. -The real-time decoding framework supports two primary deployment scenarios: +The examples below cover realtime decoding end to end — start with Getting Started, then explore the specialized predecoding and decoding workloads: -1. **Hardware Integration**: Decoders running on classical computers connected to real quantum processing units (QPUs) via low-latency networks -2. **Simulation Mode**: Decoders operating in simulated environments for testing and development on local systems +.. toctree:: + :maxdepth: 2 -Workflow Overview ------------------ - -Real-time decoding integrates seamlessly into quantum error correction pipelines through a carefully designed four-stage workflow. This workflow separates the computationally intensive characterization phase from the latency-critical runtime phase, ensuring that decoders can operate efficiently during quantum circuit execution. - -The workflow consists of four stages: - -1. **Detector Error Model (DEM) Generation**: Before running a quantum program, the user first characterizes how errors propagate through the quantum circuit. The library internally uses Memory Syndrome Matrix (MSM) representations to track error propagation, but this complexity is abstracted through helper functions like ``z_dem_from_memory_circuit``. The user simply provides a quantum code, noise model, and circuit parameters, and receives a complete detector error model that maps error mechanisms to syndrome patterns. This step is performed once during development. - -2. **Decoder Configuration and Saving**: Using the DEM, the user configures decoder instances with the specific error model data. This includes converting parity check matrices to sparse format, setting decoder-specific parameters (like lookup table depth or BP iterations), and assigning unique IDs to each logical qubit's decoder. The configuration is then saved to a YAML file, capturing all the information decoders need to interpret syndrome measurements correctly. This creates a portable, reusable configuration that separates characterization from execution. - -3. **Decoder Loading and Initialization**: Just before circuit execution, the user loads the saved YAML configuration file. The library parses the configuration, instantiates the appropriate decoder implementations, initializes internal data structures, and registers the decoders with the CUDA-Q runtime. For GPU-based decoders, matrices are transferred to device memory; for lookup table decoders, syndrome-to-correction mappings are constructed. This initialization takes milliseconds to seconds depending on code size and happens before quantum operations begin. - -4. **Real-Time Decoding**: During quantum circuit execution, the decoding API is used within quantum kernels to interact with decoders. As the circuit measures stabilizers, syndromes are enqueued to the decoder, which processes them concurrently. When corrections are needed, the decoder is queried and the suggested operations are applied to the logical qubits. This entire process happens within the coherence time constraints of the quantum hardware. - -Real-Time Decoding Example --------------------------- - -Here are two examples demonstrating real-time decoding in Python and C++: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/real_time_complete.py - :language: python - :start-after: # [Begin Documentation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp - :language: cpp - :start-after: // [Begin Documentation] - -The examples above showcase the main components of the real-time decoding workflow: - -- Decoder configuration file: Initializes and configures the decoders before circuit execution. - -- Quantum kernel: Uses the real-time decoding API to interact with the decoders, primarily through reset_decoder, enqueue_syndromes, and get_corrections. - -- Syndrome extraction: Measures the stabilizers of the logical qubits. - -- Correction application: Applies the corrections to the logical qubits. - -- Logical observable measurement: Measures the logical observables of the logical qubits. - -- Decoder finalization: Frees up resources after circuit execution. - -The API is designed to be called from within quantum kernels (marked with ``@cudaq.kernel`` in Python or ``__qpu__`` in C++). The runtime automatically routes these calls to the appropriate backend—whether a simulation environment on the local machine or a low-latency connection to quantum hardware. The API is device-agnostic, so the same kernel code works across different deployment scenarios. - -The user is required to provide a configuration file or generate one if it is not present. The generation process depends on the decoder type and the detector error model studied in other sections of the documentation. Moreover, the user must write an appropriate kernel that describes the correct syndrome extraction and correction application logic. - -The next section provides instructions to generate a configuration file, write a quantum kernel, and compile and run the examples correctly. - - -Configuration -------------- - -The configuration process transforms a quantum circuit's error characteristics into a format that decoders can efficiently process. This section walks through each step in detail, showing how to go from circuit simulation to a fully configured real-time decoder. - -Step 1: Generate Detector Error Model -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The first step is to characterize the quantum circuit's behavior under noise. -A detector error model (DEM) captures the relationship between physical errors and the syndrome patterns they produce. -This characterization is circuit-specific and depends on the code structure, noise model, and measurement schedule. - -Under the hood, the CUDA-Q QEC library uses the Memory Syndrome Matrix (MSM) representation to efficiently encode error propagation information. The MSM captures all possible error chains and their syndrome signatures, tracking how errors propagate through the circuit over time. However, this complexity is abstracted away from the user through convenient helper functions. - -The library provides a family of ``dem_from_memory_circuit`` functions that automatically handle the MSM generation and processing: - -* ``z_dem_from_memory_circuit``: For circuits measuring Z-basis stabilizers (used in the example below) -* ``x_dem_from_memory_circuit``: For circuits measuring X-basis stabilizers -* ``dem_from_memory_circuit``: General-purpose function for arbitrary stabilizer measurements - -These functions take a quantum code, an initial state preparation operation, the number of measurement rounds, and a noise model, then return a complete detector error model ready for decoder configuration. The user simply needs to configure the noise model and specify the circuit structure—the library handles all the error tracking and matrix construction automatically. - -Here is how to generate a DEM for a circuit: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/real_time_complete.py - :language: python - :start-after: # [Begin DEM Generation] - :end-before: # [End DEM Generation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp - :language: cpp - :start-after: // [Begin DEM Generation] - :end-before: // [End DEM Generation] - -Step 2: Configure and Save Decoder -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Once a DEM has been generated, the next step is to package this information into a decoder configuration and save it to a YAML file. -The configuration structure holds all the parameters a decoder needs: the parity check matrix (H_sparse), -the observable flip matrix (O_sparse), the detector error matrix (D_sparse), -and decoder-specific tuning parameters. - -These matrices are generated in sparse matrix format, which is crucial for performance. -They can be large considering error correcting codes with large number of physical qubits, and moreover, -real-time decoders process thousands of syndrome measurements per second, and take decision based on these matrices, so compact representations are essential. -The helper function ``pcm_to_sparse_vec`` is used to convert the dense binary matrices into a space-efficient format where -1 marks row boundaries and integers represent column indices of non-zero elements. - -Each decoder type has its own configuration structure with specific parameters. -For lookup table decoders, the user specifies how many simultaneous errors to consider. -For PyMatching, the user can specify per-error prior probabilities and the edge -merge strategy. The real-time path configures PyMatching as a standard decoder -with ``type: pymatching``; ``O_sparse`` remains the observable matrix used by the -base decoder to accumulate logical corrections returned by ``get_corrections``. -Vanilla PyMatching requires graphlike detector error models, where every -``H_sparse`` column has one or two detector entries. -For belief propagation decoders, the user sets iteration limits and convergence criteria. -Decoder parameters are validated against the parameter schema each decoder registers, ensuring unknown keys are rejected and required parameters are present. - -The configuration is then saved to a YAML file for reuse. The YAML format is human-readable, making it easy to inspect, modify, and share configurations across different execution environments. - -Use :func:`~cudaq_qec.decoder_context_from_memory_circuit` to obtain the parity-check, observable, -and measurement-to-detector matrices in one call, then assemble the decoder config: - -.. code-block:: python - - ctx = qec.decoder_context_from_memory_circuit(code, statePrep, num_rounds, noise) - dem, m2d, m2o = ctx.z_component() # or x_component() / full_component() - - config = qec.decoder_config() - config.id = 0 - config.type = "pymatching" - config.block_size = dem.num_error_mechanisms() - config.syndrome_size = dem.num_detectors() - config.H_sparse = qec.pcm_to_sparse_vec(dem.detector_error_matrix) - config.O_sparse = qec.pcm_to_sparse_vec(dem.observables_flips_matrix) - config.D_sparse = qec.d_sparse(m2d) - - config.decoder_custom_args = { - "error_rate_vec": list(dem.error_rates), - "merge_strategy": "smallest_weight", - } - - multi_config = qec.multi_decoder_config() - multi_config.decoders = [config] - -This produces YAML with a ``pymatching`` decoder and PyMatching-specific custom -arguments: - -.. code-block:: yaml - - decoders: - - id: 0 - type: pymatching - cuda_device_id: 0 # optional: pin this decoder to a CUDA device - block_size: 3 - syndrome_size: 3 - H_sparse: [ 0, -1, 1, -1, 2, -1 ] - O_sparse: [ 0, -1, 1, -1, 2, -1 ] - D_sparse: [ 0, -1, 1, -1, 2, -1 ] - decoder_custom_args: - error_rate_vec: [ 0.1, 0.1, 0.1 ] - merge_strategy: smallest_weight - -When the round count is not fixed until run time, describe the DEM as -``dem_chunks`` -- named ``phases``, the ``connections`` between them, the -``seam`` they contract on, and ``num_rounds`` -- instead of spelling out -``H_sparse``, ``O_sparse``, and ``D_sparse``. Decoder construction expands the -phases through ``expand_dem_chunks``. See -:doc:`/examples_rst/qec/dyn_dem` for the chunk layout and a YAML example. - -The ``decoder_custom_args`` section is converted between YAML and the -parameter map a decoder's constructor receives using a *parameter schema* -registered under the decoder's name. All built-in decoders ship with a -schema, and custom (out-of-tree) decoder plugins can register their own so -their parameters become configurable through the same YAML -- no changes to -the CUDA-Q QEC libraries are required. A plugin registers its schema from a -static initializer in the same shared library that registers the decoder -itself (see ``cudaq/qec/decoder_config_schema.h`` and the in-tree example -plugin ``single_error_lut_example``): - -.. code-block:: cpp - - #include "cudaq/qec/decoder_config_schema.h" - - namespace { - struct schema_registrar { - schema_registrar() { - using k = cudaq::qec::decoding::config::param_kind; - cudaq::qec::decoding::config::decoder_schema schema{ - "my_decoder", - { - {"strength", k::f64}, - {"passes", k::int32}, - {"mode", k::string, /*required=*/true}, - }}; - // Optional: cross-field constraints the per-key specs can't express. - // Unknown keys and missing required keys are already rejected by the - // framework; a decoder never implements those checks itself. - schema.validate = [](const cudaqx::heterogeneous_map &args) { - if (args.contains("strength") && args.get("strength") <= 0.0) - throw std::runtime_error("my_decoder: strength must be positive"); - }; - cudaq::qec::decoding::config::register_decoder_schema( - std::move(schema)); - } - }; - schema_registrar register_schema; - } // namespace - -With the schema in place, a ``decoder_custom_args`` section for -``type: my_decoder`` is validated (unknown keys and missing required keys are -rejected, then the schema's ``validate`` hook runs) and delivered to the -decoder's constructor as a ``cudaqx::heterogeneous_map``. The same checks can -be applied to a configuration built programmatically -- before it is -serialized or used -- by calling ``decoder_config::validate_custom_args()`` -(``config.validate_custom_args()`` in Python, also available on -``multi_decoder_config``). The registered schemas can be inspected from -Python via ``qec.decoder_param_schema("my_decoder")`` and -``qec.registered_decoder_schemas()``. - -The registered schemas can also be exported as a standard JSON Schema -(draft 2020-12) document via ``qec.decoder_config_json_schema()``, so -configuration YAML files can be validated by third-party tooling -- editors, -CI checks, or the `check-jsonschema -`_ command line tool -- without -loading the CUDA-Q QEC libraries: - -.. code-block:: bash - - python3 -c "import cudaq_qec; print(cudaq_qec.decoder_config_json_schema())" > decoder_config_schema.json - check-jsonschema --schemafile decoder_config_schema.json my_config.yaml - -The export is generated from the schemas registered at call time, so decoder -plugins loaded in the process (including out-of-tree ones) appear in it -automatically. Schema ``validate`` hooks are arbitrary code and cannot be -represented in JSON Schema, so a file that passes the exported schema may -still be rejected by a hook when the configuration is parsed. - -``cuda_device_id`` pins a GPU-accelerated decoder (e.g. ``nv-qldpc-decoder`` -or ``trt_decoder``) to a specific CUDA device. The same knob is available as -a construction parameter in C++ and Python -(``qec.get_decoder("trt_decoder", H, cuda_device_id=1)``). The thread that -creates a decoder is pinned to that device and is expected to drive its -decode calls; create each pinned decoder on its own thread to place several -decoders on different GPUs. - -Here is how to create and save a decoder configuration: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/real_time_complete.py - :language: python - :start-after: # [Begin Save DEM] - :end-before: # [End Save DEM] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp - :language: cpp - :start-after: // [Begin Save DEM] - :end-before: // [End Save DEM] - -Step 3: Load Configuration -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Before running quantum circuits with real-time decoding, the saved decoder configuration must be loaded and initialized. -This step bridges the gap between the offline characterization phase (Steps 1-2) and the online execution phase (Step 4), -preparing the decoder instances for real-time operation. - -The configuration loading process performs several important operations: - -1. **YAML Parsing**: The configuration file is parsed and validated to ensure all required fields are present and properly formatted. This includes checking matrix dimensions, decoder parameters, and metadata. - -2. **Decoder Instantiation**: Based on the decoder type specified in the configuration (e.g., ``multi_error_lut``, ``pymatching``, ``nv-qldpc-decoder``), the appropriate decoder implementation is instantiated and allocated resources on the GPU or CPU. - -3. **Matrix Initialization**: The sparse matrices (H_sparse, O_sparse, D_sparse) are loaded into the decoder's internal data structures. For GPU-based decoders, this includes transferring data to device memory. - -4. **Decoder-Specific Initialization**: Each decoder type performs its own preparation: lookup table decoders build syndrome-to-correction mappings, belief propagation decoders initialize message-passing structures, and sliding window decoders configure their buffering mechanisms. - -5. **Backend Registration**: The decoder instances are registered with the CUDA-Q runtime so they can be accessed from quantum kernels using their unique IDs. - -This initialization happens quickly, typically only a few milliseconds for small codes and up to a few seconds for large distance codes with complex decoders. Since it occurs before quantum circuit execution, it does not impact the latency-critical decoding operations. - -The separation of configuration from execution provides significant benefits: users can maintain a library of configurations for different code distances, noise levels, and decoder types, then simply load the appropriate one when running experiments. Configurations can be version-controlled alongside code, shared across research teams, and validated offline before deployment to quantum hardware. - -Here is how to load a decoder configuration: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/real_time_complete.py - :language: python - :start-after: # [Begin Load DEM] - :end-before: # [End Load DEM] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp - :language: cpp - :start-after: // [Begin Load DEM] - :end-before: // [End Load DEM] - -Step 4: Use in Quantum Kernels -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -With decoders configured and initialized, they can be used within quantum kernels. The real-time decoding API provides three key functions that integrate seamlessly with CUDA-Q's quantum programming model: ``reset_decoder`` prepares a decoder for a new shot, ``enqueue_syndromes`` sends syndrome measurements to the decoder for processing, and ``get_corrections`` retrieves the decoder's recommended corrections. - -These functions are designed to be called from within quantum kernels (marked with ``@cudaq.kernel`` in Python or ``__qpu__`` in C++). The runtime automatically routes these calls to the appropriate backend - whether that is a simulation environment on the local machine or a low-latency connection to quantum hardware. The API is device-agnostic, so the same kernel code works across different deployment scenarios. - -The typical usage pattern is: reset the decoder at the start of each shot, enqueue -syndromes after each stabilizer measurement round, then get corrections before -measuring the logical observables. Decoders process syndromes asynchronously, so -by the time ``get_corrections`` is called, the decoder has usually finished its -analysis. If decoding takes longer than expected, ``get_corrections`` will block -until results are available. - -.. note:: - While resetting the decoder at the beginning of each shot isn't strictly - required, it is **strongly** recommended to ensure that when running on a - remote QPU, any potential errors encountered in one shot do not affect future - shot results. - -Here is how to use the real-time decoding API in quantum kernels: - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/real_time_complete.py - :language: python - :start-after: # [Begin QEC Circuit] - :end-before: # [End QEC Circuit] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/real_time_complete.cpp - :language: cpp - :start-after: // [Begin QEC Circuit] - :end-before: // [End QEC Circuit] - -Backend Selection ------------------ - -CUDA-Q QEC's real-time decoding system is designed to work seamlessly across different execution environments. The backend selection determines where quantum circuits run and how decoders communicate with the quantum processor. Understanding the differences between simulation and hardware backends helps the user develop efficiently and deploy confidently. - -Simulation Backend -^^^^^^^^^^^^^^^^^^ - -The simulation backend is the primary tool during development, testing, and -algorithm validation. It runs entirely on the local machine, using quantum -simulators like Stim to execute circuits while decoders process syndromes and -calculation corrections. This setup is ideal for rapid iteration: the user can -test decoder configurations, validate circuit logic, and debug syndrome -processing without waiting for hardware access or paying for compute time. - -The simulation backend mimics real-time decoding's concurrent operation by -running the decoder(s) within the same process as the simulator. This means that -other than GPU hardware differences between the local environment and the remote -NVQLink decoders, the decoders behave the same way whether testing locally or -running on a quantum computer. The main difference is that simulation does not -have the same strict latency constraints, making it easier to experiment with -complex decoder configurations. - -Use the simulation backend for local development and testing: - -.. tab:: Python - - .. code-block:: python - - import cudaq - import cudaq_qec as qec - - cudaq.set_target("stim") # Or other simulator - qec.configure_decoders_from_file("config.yaml") - - # Run circuit with noise model - results = cudaq.run(my_circuit, shots_count=100, - noise_model=cudaq.NoiseModel()) - -.. tab:: C++ - - .. code-block:: bash - - # Compile with simulation support - nvq++ -std=c++20 my_circuit.cpp -lcudaq-qec \ - -lcudaq-qec-decoders \ - -lcudaq-qec-realtime-decoding \ - -lcudaq-qec-realtime-decoding-simulation - - ./a.out - -Quantinuum Hardware Backend -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Quantinuum hardware backend connects quantum circuits to real ion-trap quantum computers. Unlike the simulation backend where decoders run on the local machine, **the Quantinuum backend uploads the decoder configuration to Quantinuum's infrastructure**, where decoders run on GPU-equipped servers co-located with the quantum hardware. This architecture minimizes latency between syndrome measurements and correction application. - -**Important Setup Requirements:** - -1. **Configuration Upload**: When ``configure_decoders_from_file()`` or ``configure_decoders()`` is called, the decoder configuration is automatically base64-encoded and uploaded to Quantinuum's REST API (``api/gpu_decoder_configs/v1beta/``). This happens before job submission. The configuration includes all decoder parameters, error models, and sparse matrices. - -2. **Extra Payload Provider**: The user **must** specify ``extra_payload_provider="decoder"`` when setting the target. This registers a payload provider that injects the decoder configuration UUID into each job request, telling Quantinuum which decoder configuration to use for the circuit. - -3. **Backend Compilation**: For C++, the user must link against ``-lcudaq-qec-realtime-decoding-quantinuum`` instead of the simulation library. This library implements the Quantinuum-specific communication protocol for syndrome transmission. - -4. **Configuration Lifetime**: Decoder configurations persist on Quantinuum's servers and are referenced by UUID. If the configuration is modified, it must be uploaded again - the system will generate a new UUID and use the new configuration for subsequent jobs. - -Note: The real-time decoding interfaces are experimental, and subject to change. Real-time decoding on Quantinuum's Helios-1 device is currently only available to partners and collaborators. Please email QCSupport@quantinuum.com for more information. - -**Emulation vs. Hardware Modes:** - -Emulation mode (``emulate=True``) is particularly valuable for testing the deployment setup without consuming hardware credits. Running with this flag performs a local, noise-free simulation without any actual submission to Quantinuum's servers. - -Use the Quantinuum backend for hardware or emulation: - -.. tab:: Python - - .. code-block:: python - - cudaq.set_target("quantinuum", - emulate=False, # True for emulation - machine="Helios-1", - extra_payload_provider="decoder") - - qec.configure_decoders_from_file("config.yaml") - results = cudaq.run(my_circuit, shots_count=100) - -.. tab:: C++ - - .. code-block:: bash - - # Compile for Quantinuum - nvq++ --target quantinuum --quantinuum-machine Helios-1 \ - --quantinuum-extra-payload-provider decoder \ - my_circuit.cpp -lcudaq-qec \ - -lcudaq-qec-decoders \ - -lcudaq-qec-realtime-decoding \ - -lcudaq-qec-realtime-decoding-quantinuum \ - -Wl,--export-dynamic - - ./a.out - -Compilation and Execution Examples ------------------------------------ - -This section provides **complete, tested compilation and execution commands** for both simulation and hardware backends, extracted from the CUDA-Q QEC test infrastructure. The section begins with common usage patterns that guide decoder and compilation choices, then provides the specific commands needed for each backend. - -Common Use Cases -^^^^^^^^^^^^^^^^^^^^^^ - -Before diving into compilation details, it is helpful to understand the typical scenarios and how they map to decoder choices and workflow parameters. -A full set of common examples is provided to guide development. -These examples describe the complete workflow for developing an application that uses real-time decoding in a single file. -The relevant C++ and Python examples can be found at the following path: -`libs/qec/unittests/realtime/app_examples `_. -The files have names like ``surface_code-1.cpp`` and ``surface_code_1.py``. The rest of this section shows how to compile and run these 2 examples. - -These examples provide comprehensive support for application development with real-time decoding. -The subsequent step, once the user has chosen the appropriate decoder and the appropriate backend, is to compile and execute the application. -Instructions are provided below for both the simulation and the hardware backends. - -C++ Compilation -^^^^^^^^^^^^^^^ - -**Simulation Backend (Stim)** - -Compile with the simulation backend for local testing: - -.. code-block:: bash - - nvq++ --target stim surface_code-1.cpp \ - -lcudaq-qec \ - -lcudaq-qec-decoders \ - -lcudaq-qec-realtime-decoding \ - -lcudaq-qec-realtime-decoding-simulation \ - -o surface_code-1 - - # Execute - ./surface_code-1 --distance 3 --num_shots 1000 --save_dem config.yaml - -**Key Points:** - -- ``--target stim``: Use the Stim quantum simulator -- ``-lcudaq-qec``: Core QEC library with codes and experiments -- ``-lcudaq-qec-decoders``: Decoder core API (decoders, ``sparse_binary_matrix``, and PCM utilities such as ``pcm_to_sparse_vec``) -- ``-lcudaq-qec-realtime-decoding``: Real-time decoding core API -- ``-lcudaq-qec-realtime-decoding-simulation``: Simulation-specific decoder backend - -**Quantinuum Backend (Hardware)** - -Compile for actual Quantinuum hardware: - -.. code-block:: bash - - nvq++ --target quantinuum \ - --quantinuum-machine Helios-1 \ - --quantinuum-extra-payload-provider decoder \ - surface_code-1.cpp \ - -lcudaq-qec \ - -lcudaq-qec-decoders \ - -lcudaq-qec-realtime-decoding \ - -lcudaq-qec-realtime-decoding-quantinuum \ - -Wl,--export-dynamic \ - -o surface_code-1-quantinuum-hardware - - # Execute - export CUDAQ_QUANTINUUM_CREDENTIALS= - ./surface_code-1-quantinuum-hardware --distance 3 --num_shots 100 --load_dem config.yaml - -**Key Points:** - -- Use Quantinuum target names: ``Helios-1``, ``Helios-1E``, ``Helios-1SC``, etc. -- Currently only ``Helios-1`` will run the GPU decoders. The ``Helios-1E`` emulator will not run the GPU decoders. -- Set ``CUDAQ_QUANTINUUM_CREDENTIALS`` environment variable with the user's credentials. - Check out the `Quantinuum hardware backend documentation `_ for more information. - -**Emulated Quantinuum Compilation Workflow** - -Compile for Quantinuum emulation mode: - -.. code-block:: bash - - nvq++ --target quantinuum --emulate \ - --quantinuum-machine Helios-Fake \ - surface_code-1.cpp \ - -lcudaq-qec \ - -lcudaq-qec-decoders \ - -lcudaq-qec-realtime-decoding \ - -lcudaq-qec-realtime-decoding-quantinuum \ - -Wl,--export-dynamic \ - -o surface_code-1-quantinuum-emulate - - # Execute - ./surface_code-1-quantinuum-emulate --distance 3 --num_shots 1000 --load_dem config.yaml - -**Key Points:** - -- ``--target quantinuum --emulate``: Emulate Quantinuum compilation path -- ``--quantinuum-machine Helios-Fake``: Specify machine (``Helios-Fake`` for emulation) -- ``-lcudaq-qec-realtime-decoding-quantinuum``: Quantinuum-specific decoder backend (replaces ``-simulation``) -- ``-Wl,--export-dynamic``: **Required** linker flag for dynamic symbol resolution - -.. note:: - When running with `--emulate`, there is no noise being applied because there - is currently no way to express noise in target-specific QIR. Therefore, when - running with emulation, users will see noise-free sample data. - -Python Execution -^^^^^^^^^^^^^^^^ - -**Simulation Backend (Stim)** - -.. code-block:: bash - - # Generate a decoder configuration file - python3 surface_code_1.py --distance 3 --save_dem config.yaml - # Run the circuit with the decoder configuration - python3 surface_code_1.py --distance 3 --load_dem config.yaml --num_shots 1000 - - -**Quantinuum Backend (Hardware)** - -.. code-block:: bash - - python3 surface_code_1.py --distance 3 --load_dem config.yaml --num_shots 1000 --target quantinuum --machine_name Helios-1 --project_id - -**Key Points:** - -- Use real machine names (check Quantinuum portal for available machines) -- ``--project_id``: Specify the Quantinuum project ID used for the hardware submission. -- Reduce shot count for hardware experiments (hardware time is expensive) - -**Emulated Quantinuum Compilation Workflow** - -.. code-block:: bash - - python3 surface_code_1.py --distance 3 --load_dem config.yaml --num_shots 1000 --target quantinuum --emulate - -**Key Points:** - -- ``--emulate``: Emulate the Quantinuum execution path -- Decoder config is automatically uploaded to Quantinuum's servers when - ``cudaq_qec.configure_decoders_from_file`` (Python) or - :cpp:func:`cudaq::qec::decoding::config::configure_decoders_from_file` (C++) is called - -Complete Workflow Example -^^^^^^^^^^^^^^^^^^^^^^^^^^ -Given that the user follows the structure of the examples provided, where each executable takes terminal arguments to configure the application, the following workflow can be used to compile and execute the application. - - -.. code-block:: bash - - # Phase 1: Generate Detector Error Model (DEM) - # This is done once per code/distance/noise configuration - - ## C++ - ./surface_code-1 --distance 3 --num_shots 1000 --p_cnot 0.001 \ - --save_dem config_d3.yaml --num_rounds 12 - - ## Python - python3 surface_code_1.py --distance 3 --num_shots 1000 --p_cnot 0.001 \ - --save_dem config_d3.yaml --num_rounds 12 - - # Phase 2: Run with Real-Time Decoding - # Use the saved DEM configuration - - ## Simulation - ./surface_code-1 --distance 3 --num_shots 1000 --load_dem config_d3.yaml \ - --num_rounds 12 - - ## Quantinuum Emulation - ./surface_code-1-quantinuum-emulate --distance 3 --num_shots 1000 --load_dem config_d3.yaml \ - --num_rounds 12 - - ## Quantinuum Hardware - export CUDAQ_QUANTINUUM_CREDENTIALS=credentials.json - ./surface_code-1-quantinuum-hardware --distance 3 --num_shots 100 --load_dem config_d3.yaml \ - --num_rounds 12 - -**Application Parameters:** - -- ``--distance``: Code distance (3, 5, 7, etc.) -- ``--num_shots``: Number of circuit repetitions -- ``--p_cnot``: Two-qubit depolarizing rate on CNOT gates for DEM generation -- ``--save_dem``: Generate and save DEM configuration to file -- ``--load_dem``: Load existing DEM configuration from file -- ``--num_rounds``: Total number of syndrome measurement rounds - -Debugging and Environment Variables -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Useful Environment Variables:** - -.. code-block:: bash - - # Enable decoder configuration debugging - export CUDAQ_QEC_DEBUG_DECODER=1 - - # Set default simulator - export CUDAQ_DEFAULT_SIMULATOR=stim - - # Dump JIT IR for debugging compilation issues - export CUDAQ_DUMP_JIT_IR=1 - - # Set Quantinuum credentials file - export CUDAQ_QUANTINUUM_CREDENTIALS=/path/to/credentials.json - -The variables can be set in the user's environment or in a script. -They are valid both for python and C++ applications, however, they must be set before importing the cudaq or cudaq_qec libraries. - -**Common Compilation Issues:** - -1. **Missing libraries**: Ensure all ``-lcudaq-qec-*`` libraries are linked -2. **Wrong backend library**: Use ``-simulation`` for Stim, ``-quantinuum`` for Quantinuum -3. **Missing** ``-Wl,--export-dynamic`` **flag**: Required for Quantinuum targets -4. **Wrong target flags**: Use ``--emulate`` for emulation. Omit it and provide ``--project_id`` for hardware - -**Common Runtime Issues:** - -1. **"Decoder X not found"**: Call ``configure_decoders_from_file()`` before circuit execution -2. **"Configuration upload failed"**: Check network connectivity and Quantinuum credentials -3. **Dimension mismatch errors**: Verify DEM dimensions match the circuit's syndrome count -4. **High error rates**: Check decoder window size matches DEM generation window - - -Decoder Selection -^^^^^^^^^^^^^^^^^ -The page `CUDA-Q QEC Decoders `_ provides information about which decoders are compatible with real-time decoding. - -The TRT decoder (``trt_decoder``) can be configured for real-time decoding by specifying -its ``decoder_custom_args`` parameters. This is useful for neural network-based -decoders trained for specific codes and noise models. Note that TRT models -must be trained with the appropriate input/output dimensions matching the -syndrome and error spaces. See :ref:`trt_decoder_api_python` for detailed configuration options. - -Troubleshooting ---------------- - -Even with careful configuration, issues may be encountered during real-time decoding. This section covers the most common problems and their solutions, organized by symptom. When troubleshooting, start by isolating whether the issue is in DEM generation, decoder configuration, or runtime execution. - -Configuration Upload Failures (Quantinuum Backend) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When using the Quantinuum backend, the decoder configuration must be uploaded to their REST API before job submission. Upload failures prevent the quantum program from running and can be difficult to diagnose without knowing what to look for. - -**Possible Issues:** - -* **Network connectivity problems**: Connection to Quantinuum's servers is interrupted or unstable -* **Configuration too large**: Decoder configuration exceeds Quantinuum's upload size limits (typically happens with large distance codes and lookup tables) -* **Invalid credentials**: API authentication fails due to expired or incorrect credentials -* **Malformed configuration**: YAML structure is invalid or contains unsupported parameters - -**Solutions**: - -* **Enable debug logging**: Set ``CUDAQ_QEC_DEBUG_DECODER=1`` environment variable to see the exact configuration being uploaded and any error messages from the REST API -* **Check network**: Verify that Quantinuum's API endpoints can be reached before running the program. Test with a simple job submission first. -* **Reduce configuration size**: If uploads fail due to size, switch from lookup table decoders to QLDPC (much more compact), or use sliding window with smaller windows -* **Validate YAML locally**: Before uploading, test that ``multi_decoder_config::from_yaml_str()`` can parse the configuration file without errors -* **Check credentials**: Ensure the Quantinuum API credentials are valid and have not expired. Refresh tokens if necessary. -* **Test with emulation**: Try ``emulate=True`` first - emulation uses the same upload infrastructure but provides faster feedback if there are configuration issues - -**Verification**: - -After fixing configuration issues, the following log messages should appear: - -.. code-block:: text - - [info] Initializing realtime decoding library with config file: config.yaml - [info] Initializing decoders... - [info] Creating decoder 0 of type multi_error_lut - [info] Done initializing decoder 0 in 0.234 seconds - -If errors appear instead, check the full error message - it often contains specific details about what failed (network timeout, size limit, parsing error, etc.). + Getting Started with Realtime Decoding + Realtime Decoding with CUDA-Q Decoding Server + AI Predecoder with CUDA-Q Realtime + AI Predecoder with CUDA-Q Realtime (with FPGA Data Injection) + Relay BP Decoding with CUDA-Q Realtime + Dynamic DEM Construction See Also -------- -* :doc:`/api/qec/cpp_api` - C++ API Reference (includes Real-Time Decoding) -* :doc:`/api/qec/python_api` - Python API Reference (includes Real-Time Decoding) -* Example source code: ``libs/qec/unittests/realtime/app_examples/`` +* Example source code: `libs/qec/unittests/realtime/app_examples `_ +* :ref:`Realtime Decoding C++ API ` +* :ref:`Realtime Decoding Python API ` diff --git a/docs/sphinx/examples_rst/qec/realtime_predecoder_fpga.rst b/docs/sphinx/examples_rst/qec/realtime_predecoder_fpga.rst index ade3dcb85..7722577fa 100644 --- a/docs/sphinx/examples_rst/qec/realtime_predecoder_fpga.rst +++ b/docs/sphinx/examples_rst/qec/realtime_predecoder_fpga.rst @@ -524,3 +524,9 @@ Run Options - ``8193`` - UDP control port for emulator +See Also +-------- + +* :doc:`Realtime Decoding ` -- concept and workflow +* :doc:`Getting Started with Realtime Decoding ` +* :ref:`C++ ` and :ref:`Python ` realtime decoding API diff --git a/docs/sphinx/examples_rst/qec/realtime_predecoder_pymatching.rst b/docs/sphinx/examples_rst/qec/realtime_predecoder_pymatching.rst index e5fe3463d..052555414 100644 --- a/docs/sphinx/examples_rst/qec/realtime_predecoder_pymatching.rst +++ b/docs/sphinx/examples_rst/qec/realtime_predecoder_pymatching.rst @@ -56,9 +56,11 @@ Additional inputs: engine is built from the ONNX file on first run (this can take 1--2 minutes for large models). - **Syndrome data directory** containing pre-generated detector samples, - observables, and matching graph data (see `Data Directory Layout`_). + observables, and matching graph data (see :ref:`Data Directory Layout `). +.. _sw_data_directory_layout: + Data Directory Layout --------------------- @@ -170,7 +172,7 @@ Named Flags * - Flag - Description * - ``--data-dir `` - - Path to syndrome data directory (see `Data Directory Layout`_). When + - Path to syndrome data directory (see :ref:`Data Directory Layout `). When omitted, random syndromes with 1% error rate are generated. * - ``--num-gpus `` - Number of GPUs to use. Currently clamped to 1 (multi-GPU dispatch is @@ -316,3 +318,10 @@ Printed only when ``--data-dir`` is provided: requests, each shot is replayed approximately 6 times. Correctness verification still compares against the correct ground truth for each replayed shot. + +See Also +-------- + +* :doc:`Realtime Decoding ` -- concept and workflow +* :doc:`Getting Started with Realtime Decoding ` +* :ref:`C++ ` and :ref:`Python ` realtime decoding API diff --git a/docs/sphinx/examples_rst/qec/realtime_relay_bp.rst b/docs/sphinx/examples_rst/qec/realtime_relay_bp.rst index f680c0786..06da8a9a8 100644 --- a/docs/sphinx/examples_rst/qec/realtime_relay_bp.rst +++ b/docs/sphinx/examples_rst/qec/realtime_relay_bp.rst @@ -715,3 +715,10 @@ The bridge enforces this: if ``--num-pages`` is ever passed with a value above ``WQE_NUM``, it clamps to 64 and prints a warning. Supporting a deeper ring would require changing ``WQE_NUM`` (and the per-thread WQE striding) in ``holoscan-sensor-bridge``, diverging from the ``2.6.0-EA2`` tag. + +See Also +-------- + +* :doc:`Realtime Decoding ` -- concept and workflow +* :doc:`Getting Started with Realtime Decoding ` +* :ref:`C++ ` and :ref:`Python ` realtime decoding API diff --git a/docs/sphinx/examples_rst/qec/stim_dem_decoder.rst b/docs/sphinx/examples_rst/qec/stim_dem_decoder.rst deleted file mode 100644 index c832df993..000000000 --- a/docs/sphinx/examples_rst/qec/stim_dem_decoder.rst +++ /dev/null @@ -1,42 +0,0 @@ -Decoding From Stim DEM Text ---------------------------- - -CUDA-Q QEC decoders can be constructed from either a parity-check matrix or raw -Stim detector error model (DEM) text. Passing the DEM text is useful when the -model is already available in Stim's ``.dem`` format, such as from a saved file, -Stim workflow, or CUDA-Q DEM generation. - -For PCM-based decoders, CUDA-Q QEC parses the DEM text into a detector error -matrix and supplies DEM-derived ``O`` and ``error_rate_vec`` defaults when the -user does not provide them. C++ decoder plugins that need full Stim DEM -metadata can consume the raw DEM string from the decoder construction input. - -By default, ``get_decoder(..., dem_text)`` and ``dem_from_stim_text(dem_text)`` -parse with ``use_decomp_suggestions=False``. Stim ``^`` decomposition hints are -ignored and each ``error(...)`` instruction becomes one matrix column. The -example below constructs a decoder this way and uses the matching parsed matrix -for observable predictions. - -``dem_from_stim_text`` also accepts ``use_decomp_suggestions=True`` to split -``^``-separated components into separate columns. That call is shown for -inspection only; it does not change how ``get_decoder`` parses the same DEM -text string. - -.. tab:: Python - - .. literalinclude:: ../../examples/qec/python/stim_dem_decoder.py - :language: python - :start-after: [Begin Documentation] - -.. tab:: C++ - - .. literalinclude:: ../../examples/qec/cpp/stim_dem_decoder.cpp - :language: cpp - :start-after: [Begin Documentation] - - Compile and run with - - .. code-block:: bash - - nvq++ -lcudaq-qec -lcudaq-qec-decoders stim_dem_decoder.cpp -o stim_dem_decoder - ./stim_dem_decoder diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 54feaf521..103501a06 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -18,7 +18,7 @@ solvers. :maxdepth: 1 :caption: Libraries - components/qec/introduction + components/qec/index components/solvers/introduction .. toctree:: @@ -28,6 +28,12 @@ solvers. examples_rst/qec/examples examples_rst/solvers/examples +.. toctree:: + :maxdepth: 1 + :caption: Performance Studies + + performance/index + .. toctree:: :maxdepth: 1 :caption: API Reference @@ -41,7 +47,7 @@ solvers. Key Features ------------- -CUDA-QX is composed of two distinct libraries that build upon CUDA-Q programming model. +CUDA-QX is composed of two distinct libraries that build upon the CUDA-Q programming model. The libraries provided are cudaq-qec, a library enabling performant research workflows for quantum error correction, and cudaq-solvers, a library that provides high-level APIs for common quantum-classical solver workflows. @@ -59,9 +65,8 @@ APIs for common quantum-classical solver workflows. * Quantum Approximate Optimization Algorithm (QAOA) * More to come... -Indices and Tables ------------------- +Indices +------- * :ref:`genindex` -* :ref:`modindex` * :ref:`search` diff --git a/docs/sphinx/performance/index.rst b/docs/sphinx/performance/index.rst new file mode 100644 index 000000000..497efab34 --- /dev/null +++ b/docs/sphinx/performance/index.rst @@ -0,0 +1,14 @@ +Performance Studies +=================== + +In-depth performance studies of CUDA-Q QEC decoders on NVIDIA GPUs -- measuring +decode latency, logical error rate, and the trade-offs behind decoder tuning knobs. + +The first study shows how **gamma ensembling** narrows the Relay BP decode-latency +tail, improving the logical error rate under hard decode deadlines by up to **~89x** +on bivariate-bicycle codes (measured on a single GB200 with CUDA-Q QEC 0.7.0). + +.. toctree:: + :maxdepth: 1 + + Improving Relay BP Decoding With Gamma Ensembles diff --git a/docs/sphinx/examples_rst/qec/nv_qldpc_gamma_ensemble_user_guide.rst b/docs/sphinx/performance/nv_qldpc_gamma_ensemble_user_guide.rst similarity index 93% rename from docs/sphinx/examples_rst/qec/nv_qldpc_gamma_ensemble_user_guide.rst rename to docs/sphinx/performance/nv_qldpc_gamma_ensemble_user_guide.rst index 6d1af85d3..ca4ab243e 100644 --- a/docs/sphinx/examples_rst/qec/nv_qldpc_gamma_ensemble_user_guide.rst +++ b/docs/sphinx/performance/nv_qldpc_gamma_ensemble_user_guide.rst @@ -80,7 +80,7 @@ All experiments below use the same circuit-level noise and decoder settings, and * - ``stopping_criterion`` - ``FirstConv`` -.. image:: ../../../../assets/docs/relaybp_gamma_ensemble_perf.png +.. image:: ../../../assets/docs/relaybp_gamma_ensemble_perf.png :align: center :alt: Iterations to converge, time per iteration, and mean latency versus ensemble size, for several DEMs @@ -91,7 +91,7 @@ Latency Distribution One of the benefits of ensembling is that the latency distribution becomes narrower, leading to more consistent latency for hard-to-decode syndromes. Using the same decoders as above, we now focus on the behavior of the decoding latencies at various percentiles, from the median through the extreme tail. -.. image:: ../../../../assets/docs/relaybp_latency_percentiles.png +.. image:: ../../../assets/docs/relaybp_latency_percentiles.png :align: center :alt: Latency percentiles (p50, p90, p99.9, p99.99) versus ensemble size N, per code @@ -102,14 +102,21 @@ Logical Error Rate Under Hard Deadlines The narrower tail can improve logical error rates considerably for decoders under hard deadlines. Suppose each decode must finish within a wall-clock budget ``t``; a decode is a success only if it both finishes within ``t`` and returns the correct logical outcome, so the logical error rate under that deadline is ``LER(t) = P(latency > t or logical error)``. Using the same decoders as above, we record both the per-syndrome latency and whether the decoded logical is correct. The plot below shows the LER versus deadline for un-ensembled Relay BP (i.e. N = 1) and for each ensemble size N; each curve is measured over 150,000 sampled syndromes per configuration at the circuit-level noise strength given above (``p = 0.002``), using the ``FirstConv`` stopping criterion. -.. image:: ../../../../assets/docs/relaybp_hard_deadline_ler.png +.. image:: ../../../assets/docs/relaybp_hard_deadline_ler.png :align: center :alt: Deadline vs LER — bicycle codes Ensembling contracts the latency tail for all three codes, resulting in substantially lower LER for certain deadlines: beyond a crossover at the tightest deadlines (where the higher per-iteration cost makes the larger ensembles slightly worse), a larger ensemble misses fewer deadlines at looser budgets and reaches a lower floor. The regime under which ensembling is beneficial for these codes depends on the code and deadline, and likely requires specific tuning based on the problem. Below is a plot of the factor by which each ensemble size lowers LER relative to un-ensembled Relay BP as a function of the deadline ``t``; values above 1 mean a lower LER than N = 1. -.. image:: ../../../../assets/docs/relaybp_ler_multiplier.png +.. image:: ../../../assets/docs/relaybp_ler_multiplier.png :align: center :alt: LER improvement multiplier over un-ensembled Relay BP versus hard deadline, per code At the tightest deadlines the larger ensembles are briefly worse (their higher per-iteration cost delays the fastest decodes), but past that crossover a larger ensemble lowers LER substantially — by up to ~41× for ``[[144,12,12]]`` and ~89× for ``[[288,12,18]]`` at deadlines of ~1-5 ms. For those two codes the multiplier then falls back toward N = 1 at loose deadlines, where both the ensemble and N = 1 reach their near-zero logical error floors; for ``[[72,12,6]]`` it settles at ~2×. + +See Also +++++++++ + +* :ref:`Quantum Low-Density Parity-Check Decoder ` -- the nv-qldpc-decoder overview +* :ref:`Getting Started with the NVIDIA QLDPC Decoder ` -- runnable example +* :ref:`C++ ` and :ref:`Python ` API reference