diff --git a/.github/workflows/lib_qec.yaml b/.github/workflows/lib_qec.yaml index 7917e0456..43ad0c44c 100644 --- a/.github/workflows/lib_qec.yaml +++ b/.github/workflows/lib_qec.yaml @@ -135,6 +135,37 @@ jobs: - name: Run example tests run: bash scripts/ci/test_examples.sh qec + # ======================================================================== + # Realtime decoding demo example + # + # Build the two example binaries against the INSTALLED SDK -- this is what + # enforces the installed-headers-only rule: a build-tree-only include + # would fail to compile here. Then drive the delivered decoding_server + # over UDP loopback with a real pymatching decode (the qpu-kernel source). + # No hardware: the FPGA source needs a ConnectX NIC and is not run in CI. + # ======================================================================== + - name: Build & run realtime_decoding_demo (qpu-kernel, UDP loopback) + shell: bash + run: | + export PATH=/cudaq-install/bin:$PATH + # Mirror build_qec.sh: it builds+installs cudaq-realtime to + # $CUDAQ_REALTIME_ROOT, defaulting to /tmp/cudaq-realtime when unset + # (the case for this workflow). Track the same value so this step + # stays correct if the job ever sets CUDAQ_REALTIME_ROOT. + RT="${CUDAQ_REALTIME_ROOT:-/tmp/cudaq-realtime}" + EX=docs/sphinx/examples/qec/realtime_decoding_demo + cmake -S "$EX" -B /tmp/rtdemo-build -G Ninja \ + -DCUDAQ_INSTALL_DIR=/cudaq-install \ + -DCUDAQX_INSTALL_DIR="$HOME/.cudaqx" \ + -DCUDAQ_REALTIME_DIR="$RT" + cmake --build /tmp/rtdemo-build -j + bash "$EX/run_realtime_decoding.sh" \ + --source qpu-kernel --decoder pymatching \ + --install-prefix "$HOME/.cudaqx" \ + --cudaq-prefix /cudaq-install \ + --realtime-lib-dir "$RT" \ + --example-build-dir /tmp/rtdemo-build + # ======================================================================== # Upload build artifacts for GPU tests # ======================================================================== diff --git a/docs/sphinx/conf.py.in b/docs/sphinx/conf.py.in index e60451a95..8edffa3e2 100644 --- a/docs/sphinx/conf.py.in +++ b/docs/sphinx/conf.py.in @@ -95,9 +95,13 @@ master_doc = 'index' # This pattern also affects html_static_path and html_extra_path. # Fragment files pulled in via ``.. include::`` must not also be processed as # standalone documents — doing so causes duplicate C++ / Python domain -# declarations in the Sphinx domain registry. +# declarations in the Sphinx domain registry. Example-directory READMEs are +# for the shipped example tree (rendered on GitHub), not standalone doc pages +# -- without the exclusion they are ingested via the '.md' source suffix and +# warn as toctree orphans. exclude_patterns = [ '_templates', + 'examples/**/README.md', 'api/qec/nv_qldpc_decoder_api.rst', 'api/qec/sliding_window_api.rst', 'api/qec/trt_decoder_api.rst', diff --git a/docs/sphinx/examples/qec/realtime_decoding_demo/CMakeLists.txt b/docs/sphinx/examples/qec/realtime_decoding_demo/CMakeLists.txt new file mode 100644 index 000000000..71f257894 --- /dev/null +++ b/docs/sphinx/examples/qec/realtime_decoding_demo/CMakeLists.txt @@ -0,0 +1,220 @@ +# ============================================================================ # +# Copyright (c) 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +# Realtime-decoding example. +# +# This builds TWO sources, each two ways, against the *installed* CUDA-Q / +# CUDA-QX SDK -- nothing here reaches into a build tree: +# +# surface_code_realtime_decoding generator: --target stim; writes +# the decoder config + syndrome +# files (pymatching / +# multi_error_lut / nv-qldpc). +# surface_code_realtime_decoding-cqr lowered kernel: adds +# -frealtime-lowering -DQEC_APP_CQR; +# the live QPU kernel that streams +# syndromes to the delivered +# decoding_server. +# surface_code_ising_realtime_decoding generator for the Ising decoder +# profile (trt_decoder: TensorRT +# predecoder + PyMatching), built +# from the surface_code-4 lineage +# the Ising artifacts are bound to. +# surface_code_ising_realtime_decoding-cqr its lowered-kernel counterpart. +# +# The decoding_server and the FPGA playback tool are DELIVERABLES (installed, +# not built here); run_realtime_decoding.sh resolves them from --install-prefix. + +cmake_minimum_required(VERSION 3.23) + +# The lowered-kernel binary links the realtime dispatch archive (relocatable +# CUDA device code), so the project enables CUDA and needs an architecture for +# the device-link step. Default to 80 (A100) -- an architecture the shipped +# SDK's dispatch archive includes; override to match your GPU: +# -DCMAKE_CUDA_ARCHITECTURES=90 for Hopper, 100 for Blackwell (e.g. GB200). +if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) + set(CMAKE_CUDA_ARCHITECTURES 80) +endif() + +project(realtime_decoding_demo LANGUAGES CXX CUDA) + +# ---------------------------------------------------------------------------- # +# Locate the installed SDK. +# ---------------------------------------------------------------------------- # +# CUDA-Q: provides nvq++, cudaq/*.h, cudaq/realtime.h and the core runtime libs. +if(NOT CUDAQ_INSTALL_DIR) + if(DEFINED ENV{CUDA_QUANTUM_PATH}) + set(CUDAQ_INSTALL_DIR "$ENV{CUDA_QUANTUM_PATH}") + else() + set(CUDAQ_INSTALL_DIR "/usr/local/cudaq") + endif() +endif() +if(NOT EXISTS "${CUDAQ_INSTALL_DIR}/bin/nvq++") + message(FATAL_ERROR + "nvq++ not found under CUDAQ_INSTALL_DIR=${CUDAQ_INSTALL_DIR}. " + "Pass -DCUDAQ_INSTALL_DIR=.") +endif() + +# CUDA-QX: provides cudaq/qec/*.h and the QEC + realtime-decoding libs/plugins. +if(NOT CUDAQX_INSTALL_DIR) + if(DEFINED ENV{CUDAQX_INSTALL_DIR}) + set(CUDAQX_INSTALL_DIR "$ENV{CUDAQX_INSTALL_DIR}") + else() + message(FATAL_ERROR + "Set -DCUDAQX_INSTALL_DIR= (the directory that " + "holds include/cudaq/qec and lib/libcudaq-qec.so).") + endif() +endif() +if(NOT EXISTS "${CUDAQX_INSTALL_DIR}/include/cudaq/qec/code.h") + message(FATAL_ERROR + "CUDAQX_INSTALL_DIR=${CUDAQX_INSTALL_DIR} does not look like a CUDA-QX " + "install (missing include/cudaq/qec/code.h).") +endif() + +set(NVQPP "${CUDAQ_INSTALL_DIR}/bin/nvq++") +find_package(CUDAToolkit REQUIRED) + +# The realtime dispatch archive carries relocatable (RDC) device code, so the +# -cqr executable must be linked with a CUDA device-link step (see below). +# The explicit realtime prefix is searched FIRST: when the user passes a +# separate CUDAQ_REALTIME_DIR they mean it to win over any copy the CUDA-Q +# install carries, or the build silently mixes realtime revisions. +find_library(CUDAQ_REALTIME_DISPATCH_LIB cudaq-realtime-dispatch + PATHS "${CUDAQ_REALTIME_DIR}" "${CUDAQ_INSTALL_DIR}" PATH_SUFFIXES lib REQUIRED) + +link_directories( + "${CUDAQX_INSTALL_DIR}/lib" + "${CUDAQX_INSTALL_DIR}/lib/decoder-plugins" + "${CUDAQ_INSTALL_DIR}/lib" + "${CUDAQ_INSTALL_DIR}/lib/plugins") + +set(_src "${CMAKE_CURRENT_SOURCE_DIR}/surface_code_realtime_decoding.cpp") +set(_ising_src + "${CMAKE_CURRENT_SOURCE_DIR}/surface_code_ising_realtime_decoding.cpp") + +# Only the CUDA-QX headers need an explicit -I; nvq++ already knows its own +# CUDA-Q include dir (cudaq/*.h, cudaq/realtime.h, common/*.h). +set(_nvqpp_includes "-I${CUDAQX_INSTALL_DIR}/include") + +# CUDA-Q core libraries, listed AFTER the qec + simulation libs on purpose. +# The simulation backend's static initializer registers into a global map owned +# by libcudaq-common; ELF runs initializers in reverse DT_NEEDED (link) order, +# so the qec/simulation libs must be listed FIRST (they initialize last) and the +# cudaq core libs LAST (they initialize first) or the registration dereferences +# a not-yet-constructed map and segfaults at startup. +set(_cudaq_core_libs + cudaq-mlir-runtime nvqir nvqir-stim + cudaq-qec-realtime-decoding cudaq-qec-decoders + cudaq cudaq-platform-default cudaq-em-default + cudaq-common cudaq-operator cudaq-logger) + +# ---------------------------------------------------------------------------- # +# Helper: compile the source with nvq++ into an object, in an isolated working +# directory (nvq++ drops intermediates named after the source, so two compiles +# of the same file must not share a cwd). +# ---------------------------------------------------------------------------- # +function(_add_nvqpp_object out_var obj_name src) + set(_obj "${CMAKE_CURRENT_BINARY_DIR}/${obj_name}.o") + set(_wd "${CMAKE_CURRENT_BINARY_DIR}/${obj_name}.nvqpp") + file(MAKE_DIRECTORY "${_wd}") + add_custom_command( + OUTPUT "${_obj}" + COMMAND ${NVQPP} ${ARGN} -c -fPIC "${src}" -o "${_obj}" ${_nvqpp_includes} + DEPENDS "${src}" + WORKING_DIRECTORY "${_wd}" + COMMENT "Compiling with nvq++ (${obj_name})" + VERBATIM) + set_source_files_properties("${_obj}" PROPERTIES EXTERNAL_OBJECT TRUE GENERATED TRUE) + set(${out_var} "${_obj}" PARENT_SCOPE) +endfunction() + +# ---------------------------------------------------------------------------- # +# Generator (plain): writes the decoder config + syndrome files. +# ---------------------------------------------------------------------------- # +_add_nvqpp_object(_gen_obj generator "${_src}" --target stim) +add_executable(surface_code_realtime_decoding "${_gen_obj}") +set_target_properties(surface_code_realtime_decoding PROPERTIES LINKER_LANGUAGE CXX) +# --no-as-needed: nvqir-stim (the --target stim simulator) and +# cudaq-platform-default are pulled in at runtime by registration, not by a +# direct symbol reference, so the default --as-needed would drop them from +# DT_NEEDED and the process would crash at startup with no platform. +target_link_libraries(surface_code_realtime_decoding PRIVATE + -Wl,--no-as-needed + cudaq-qec cudaq-qec-realtime-decoding + cudaq-qec-realtime-decoding-simulation ${_cudaq_core_libs} + -Wl,--as-needed + "${CUDAQ_REALTIME_DISPATCH_LIB}") +target_link_options(surface_code_realtime_decoding PRIVATE + LINKER:--allow-shlib-undefined LINKER:--export-dynamic) + +# ---------------------------------------------------------------------------- # +# Lowered kernel (-cqr): the live syndrome source over UDP. +# LINKER_LANGUAGE CUDA + CUDA_RESOLVE_DEVICE_SYMBOLS runs the device-link +# (nvcc -dlink) that resolves the dispatch archive's RDC registration symbols. +# ---------------------------------------------------------------------------- # +_add_nvqpp_object(_cqr_obj generator-cqr "${_src}" + --target stim -frealtime-lowering -DQEC_APP_CQR) +add_executable(surface_code_realtime_decoding-cqr "${_cqr_obj}") +set_target_properties(surface_code_realtime_decoding-cqr PROPERTIES + CUDA_SEPARABLE_COMPILATION ON + CUDA_RESOLVE_DEVICE_SYMBOLS ON + CUDA_STANDARD 17 + LINKER_LANGUAGE CUDA) +target_link_libraries(surface_code_realtime_decoding-cqr PRIVATE + -Wl,--no-as-needed + cudaq-qec cudaq-qec-realtime-decoding + cudaq-qec-realtime-decoding-simulation-cqr + # The in-process decoding service; also exports + # cudaqx_qec_device_call_dispatch_count, which the app prints as evidence + # that an external server (not the in-process service) did the decoding. + cudaq-qec-realtime-decoding-server-cqr + cudaq-device-call-runtime + ${_cudaq_core_libs} + -Wl,--as-needed + "${CUDAQ_REALTIME_DISPATCH_LIB}" CUDA::cudart) +target_link_options(surface_code_realtime_decoding-cqr PRIVATE + LINKER:--allow-shlib-undefined LINKER:--export-dynamic) + +# ---------------------------------------------------------------------------- # +# Ising decoder profile (trt_decoder): the surface_code-4-lineage source, same +# two build variants. Requires no extra link inputs -- the TensorRT decoder is +# a runtime-loaded plugin (lib/decoder-plugins/libcudaq-qec-trt-decoder.so in +# the CUDA-QX install), discovered by the serving side, never linked here. +# ---------------------------------------------------------------------------- # +_add_nvqpp_object(_ising_gen_obj ising-generator "${_ising_src}" --target stim) +add_executable(surface_code_ising_realtime_decoding "${_ising_gen_obj}") +set_target_properties(surface_code_ising_realtime_decoding PROPERTIES + LINKER_LANGUAGE CXX) +target_link_libraries(surface_code_ising_realtime_decoding PRIVATE + -Wl,--no-as-needed + cudaq-qec cudaq-qec-realtime-decoding + cudaq-qec-realtime-decoding-simulation ${_cudaq_core_libs} + -Wl,--as-needed + "${CUDAQ_REALTIME_DISPATCH_LIB}") +target_link_options(surface_code_ising_realtime_decoding PRIVATE + LINKER:--allow-shlib-undefined LINKER:--export-dynamic) + +_add_nvqpp_object(_ising_cqr_obj ising-generator-cqr "${_ising_src}" + --target stim -frealtime-lowering -DQEC_APP_CQR) +add_executable(surface_code_ising_realtime_decoding-cqr "${_ising_cqr_obj}") +set_target_properties(surface_code_ising_realtime_decoding-cqr PROPERTIES + CUDA_SEPARABLE_COMPILATION ON + CUDA_RESOLVE_DEVICE_SYMBOLS ON + CUDA_STANDARD 17 + LINKER_LANGUAGE CUDA) +target_link_libraries(surface_code_ising_realtime_decoding-cqr PRIVATE + -Wl,--no-as-needed + cudaq-qec cudaq-qec-realtime-decoding + cudaq-qec-realtime-decoding-simulation-cqr + cudaq-qec-realtime-decoding-server-cqr + cudaq-device-call-runtime + ${_cudaq_core_libs} + -Wl,--as-needed + "${CUDAQ_REALTIME_DISPATCH_LIB}" CUDA::cudart) +target_link_options(surface_code_ising_realtime_decoding-cqr PRIVATE + LINKER:--allow-shlib-undefined LINKER:--export-dynamic) diff --git a/docs/sphinx/examples/qec/realtime_decoding_demo/prepare_ising_artifacts.py b/docs/sphinx/examples/qec/realtime_decoding_demo/prepare_ising_artifacts.py new file mode 120000 index 000000000..a2ad54963 --- /dev/null +++ b/docs/sphinx/examples/qec/realtime_decoding_demo/prepare_ising_artifacts.py @@ -0,0 +1 @@ +../../../../../libs/qec/unittests/realtime/app_examples/prepare_ising_artifacts.py \ No newline at end of file diff --git a/docs/sphinx/examples/qec/realtime_decoding_demo/run_realtime_decoding.sh b/docs/sphinx/examples/qec/realtime_decoding_demo/run_realtime_decoding.sh new file mode 100755 index 000000000..1381185ad --- /dev/null +++ b/docs/sphinx/examples/qec/realtime_decoding_demo/run_realtime_decoding.sh @@ -0,0 +1,1063 @@ +#!/bin/bash +# ============================================================================ # +# Copyright (c) 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # +# +# run_realtime_decoding.sh +# +# Drive the delivered decoding_server from one of two syndrome sources, both +# decoding through the SAME prebuilt server: +# +# --source qpu-kernel The lowered QEC kernel supplies syndromes itself over +# the udp wire, in software (no NIC) -- the portable, +# hardware-free mode -- or over the cpu_roce wire +# (--wire cpu_roce: real RDMA between two loopback- +# cabled RoCE ports; topology from the same +# CUDAQ_CPU_ROCE_TEST_* env vars as the in-tree +# cpu_roce tests). Every decoder is served on host +# dispatch (a CPU thread calls the decoder; nv-qldpc +# still decodes on its GPU). +# +# --source fpga The delivered hsb_fpga_syndrome_playback tool +# streams pre-generated syndromes over RoCE from a REAL +# FPGA into the server's RDMA ring (needs a ConnectX NIC +# cabled to the FPGA). CPU decoders ride the cpu_roce +# wire on host dispatch; nv-qldpc defaults to the +# hololink wire on device_graph dispatch (the GPU +# device-call scheduler). There is NO emulator here -- +# emulator testing lives in the unittests +# hsb_fpga_decoding_server_test.sh. +# +# DELIVERABLES (consumed prebuilt from --install-prefix, never built here): +# decoding_server, hsb_fpga_syndrome_playback, the QEC + realtime libs, +# and the decoder plugins. +# EXAMPLE BINARIES (the only things the user compiles; from --example-build-dir): +# surface_code_realtime_decoding generator: writes config + syndromes +# surface_code_realtime_decoding-cqr lowered kernel: the live syndrome source +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ============================================================================ +# Defaults +# ============================================================================ + +SOURCE="qpu-kernel" # qpu-kernel | fpga +DECODER="pymatching" # pymatching | nv-qldpc-decoder | multi_error_lut + +# Where the deliverables live (bin/ + lib/). Required unless the per-artifact +# overrides below are all given. No dev-tree fallback -- shipped code resolves +# from one install prefix. +INSTALL_PREFIX="${CUDAQX_INSTALL_DIR:-}" +# CUDA-Q install (its runtime libs + realtime libs are needed on the load path). +CUDAQ_PREFIX="${CUDA_QUANTUM_PATH:-/usr/local/cudaq}" +# Optional separate realtime lib dir (udp transport / dispatch), if not colocated. +REALTIME_LIB_DIR="${CUDAQ_REALTIME_DIR:-}" + +# The example's own build directory (holds the two compiled binaries). +EXAMPLE_BUILD_DIR="${SCRIPT_DIR}/build" + +# Per-artifact overrides (power users); empty => resolved from the prefixes. +SERVER_BIN="" +PLAYBACK_BIN="" +GENERATOR_BIN="" +KERNEL_BIN="" +NV_QLDPC_PLUGIN="${CUDAQ_QEC_NV_QLDPC_PLUGIN:-}" +# Ising artifact directory for the trt_decoder profile (six locally prepared +# files: model.onnx, H_csr.bin, O_csr.bin, priors.bin, metadata.txt, +# D_sparse.txt). Nothing in it ships with CUDA-QX; see the docs recipe. +ISING_ARTIFACTS_DIR="${QEC_ISING_ARTIFACTS_DIR:-}" + +# Surface-code experiment parameters. The generator applies two-qubit +# depolarizing noise on the stabilizer-extraction CNOTs (p_cnot); the fixed +# simulator seed makes every run -- and therefore the pass/fail counts -- +# reproducible (pass --seed -1 for unseeded runs). +GEN_DISTANCE=3 +GEN_ROUNDS=4 +GEN_P_CNOT=0.001 +# SPAM (single-qubit depolarizing) rate for the Ising trt_decoder profile, +# whose noise model is SPAM rather than CNOT depolarizing; 0.01 is the +# published model's trained operating point. +GEN_P_SPAM=0.01 +SEED=42 +# Explicit-flag trackers: the trt_decoder profile pins d=7/T=7/SPAM noise and +# must reject conflicting explicit values rather than silently override them +# (and only re-defaults --spacing when the user did not set it). +DISTANCE_EXPLICIT=false +ROUNDS_EXPLICIT=false +PCNOT_EXPLICIT=false +SPACING_EXPLICIT=false + +# Shot counts differ by mode: the FPGA plays back a fixed set through a 64-slot +# RDMA RX ring; the qpu-kernel self-paces via its blocking get_corrections and +# has no ring to overrun, so it runs more shots by default. Both overridable. +FPGA_SHOTS=85 +QPU_KERNEL_SHOTS=200 +NUM_SHOTS="" # explicit override +# The FPGA playback BRAM holds 512 frames and per-round playback spends +# (syndrome slices + 1 get_corrections) frames per shot. The Ising profile's +# d7/T7 geometry has 8 slices/shot -> 9 frames/shot -> at most 56 shots per +# playback run (504/512), just as the d3/T4 default 85 fills 510/512. +TRT_FPGA_SHOTS=56 + +# FPGA pacing (us): the playback engine's timer fires once per FRAME (one +# BRAM window per frame), so a shot takes frames-per-shot x SPACING on the +# wire. Pacing keeps playback from overrunning the server's 64-slot RX +# ring. The qpu-kernel path needs none. +SPACING="10" + +# The decoding server's two knobs (both default from --source/--decoder): +# WIRE which bridge-provider library carries syndromes into the server +# (loaded at runtime as libcudaq-realtime-bridge-.so) +# DISPATCH which engine consumes each decoder's ring: host (a CPU dispatcher +# thread) or device_graph (the GPU device-call scheduler) +# The combination gate below rejects pairings this example has not wired up. +WIRE="" # udp | cpu_roce | hololink +DISPATCH="" # host | device_graph +GPU_ID=0 + +# Network (fpga source only). The FPGA data path is same-/24 routed (no +# gateway), so the bridge NIC's default IP lives on the FPGA's subnet. +DO_SETUP_NETWORK=false +IB_DEVICE="" # auto-detect first Up ConnectX +BRIDGE_IP="192.168.0.1" +FPGA_IP="192.168.0.2" +MTU=4096 +PAGE_SIZE=384 +# RX ring depth for both FPGA paths, bounded by the HSB QP's 64 receive WQEs +# (the FPGA writes frame rid to slot rid % NUM_SLOTS; more slots than WQEs drops +# frames under load). The server clamps the cpu_roce wire's --num-slots to +# this; the device_graph ring is capped to it in run_fpga. +NUM_SLOTS=64 +FRAME_SIZE=64 +# Server lifetime failsafe (seconds). decoding_server's --timeout is a TOTAL +# runtime cap (not inactivity): the server exits once elapsed time exceeds it, +# even mid-run. 300 matches the in-tree surface_code-4 external-server tests; +# the default 60 would kill long runs (large --num-shots, slow decoders). +TIMEOUT=300 +VERIFY=true + +# ============================================================================ +# Argument parsing +# ============================================================================ + +print_usage() { + cat <<'EOF' +Usage: run_realtime_decoding.sh --source {qpu-kernel|fpga} [options] + +Sources: + --source qpu-kernel Lowered kernel streams syndromes over udp (no NIC) or, + with --wire cpu_roce, over real RDMA (see below). + --source fpga Delivered playback streams from a real FPGA over RoCE. + +Common: + --decoder NAME pymatching (default) | nv-qldpc-decoder | + multi_error_lut | trt_decoder (the Ising profile: + TensorRT NN predecoder + PyMatching global decoder; + pinned to d=7, rounds=7, SPAM noise 0.01, and + requires the Ising artifact directory below) + --install-prefix DIR Deliverables prefix (decoding_server, playback, libs, + plugins in DIR/bin and DIR/lib). Required (or give the + per-artifact overrides). + --cudaq-prefix DIR CUDA-Q install (default: $CUDA_QUANTUM_PATH or + /usr/local/cudaq) + --realtime-lib-dir DIR Extra realtime lib dir (udp transport / dispatch) + --example-build-dir DIR The example's build dir with the two binaries + (default: