QEC decoding server: transport-agnostic bridge providers, one ring per decoder, mixed host + device_graph dispatch - #682
Merged
cketcham2333 merged 45 commits intoJul 24, 2026
Conversation
…ng-server test Extend hsb_fpga_decoding_server_test.sh to drive the nv-qldpc Relay BP decoder through the decoding server's gpu_roce transport -- the self-relaunching device-graph scheduler (GpuRoceTransceiver): enqueue_syndromes / get_corrections / reset_decoder execute as DEVICE_CALLs on the GPU and the captured RelayBP decode graph fires device-side (fire-and-forget + tail-self-relaunch) when a window completes. This replaces hololink_qldpc_graph_decoder_bridge for the decoding-server flow; the old bridge test is untouched. hsb_fpga_decoding_server_test.sh: - --transport cpu_roce|gpu_roce, defaulted from the decoder profile (pymatching -> cpu_roce HOST_CALL path, nv-qldpc-decoder -> gpu_roce), plus --gpu, --proprietary-archive, --nv-qldpc-plugin. - Data generation (surface_code-4-yaml, fresh each run) adds --use-relay-bp for the nv-qldpc profile and injects `transport: gpu_roce` into the generated YAML: DecoderServer selects its transceiver from the per-decoder transport key, the generator omits non-default optionals, and the default (cpu_roce) resolves to the not-yet-implemented CpuRoceTransceiver stub. - gpu_roce server launch: Hololink parameters go via HOLOLINK_* env (the server's gpu_roce mode ignores the cpu_roce CLI flags), remote QP converted to decimal ($((qp)) -- GpuRoceConfig::from_env parses base-10 only), CUDA_MODULE_LOADING=EAGER as with the old bridge launcher, and readiness keyed on "QEC_DECODING_SERVER_READY gpu_roce" (the transceiver prints the QP/RKey/Buffer handshake before that line, without the bridge banner). - Build phase: builds gpu_roce_transceiver (HSB) and cudaq-realtime-bridge-hololink (cuda-quantum), wires CUDAQ_QEC_REALTIME_CUDEVICE_PROPRIETARY_ARCHIVE / GPU_ROCE_TRANSCEIVER_LIB / CUDAQ_REALTIME_BRIDGE_HOLOLINK_LIBRARY into the cudaqx configure, clears the CMake cache (find_library NOTFOUND staleness), and symlinks the nv-qldpc plugin into build/lib/decoder-plugins. All gated on the proprietary archive's presence so pymatching-only rigs build unchanged. The plugin symlink is also created opportunistically on --build-less runs. decoding-server-cqr/CMakeLists.txt (bug fix, separable): - CUDAQ_GPU_ROCE_AVAILABLE was set as CACHE INTERNAL on success, but the plain `set(... FALSE)` at the top of the file shadows the cache under CMP0126 (NEW), so the compile-definition gate for cudaq-qec-decoding-server never fired: the library was silently built WITHOUT gpu_roce (make_transport threw "CUDAQ_GPU_ROCE_AVAILABLE is not set" at runtime) while the sibling decoding_server target -- which reads the cache -- reported "gpu_roce transport enabled". Set the normal variable TRUE alongside the cache entry. Signed-off-by: Chuck Ketcham <cketcham@nvidia.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
With CUDAQ_GPU_ROCE_AVAILABLE now actually reaching the library target (the CMP0126 normal-vs-cache shadow fix), cudaq-qec-decoding-server links libdoca_gpunetio.so and libcudaq-realtime-bridge-hololink.so -- both of which reference the CUDA Driver API (cu*). Consumers of the static lib (test_decoding_server_core, test_decoders_yaml, decoding_server) inherit those dependencies, and on driverless build machines (CI containers: CUDA toolkit present, no GPU driver, no libcuda.so.1) their links fail with dozens of `undefined reference to cuStreamCreate` etc. -- the failure seen on all four QEC Build-and-test CI jobs. Link CUDA::cuda_driver alongside CUDA::cudart in the gpu_roce block: the CMake target resolves to the real driver where one is installed and to the toolkit's stubs (lib/stubs/libcuda.so) in driverless environments. This matches the existing precedent in unittests/utils (playback and the old bridge already link CUDA::cuda_driver). Signed-off-by: Chuck Ketcham <cketcham@nvidia.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
With CUDAQ_GPU_ROCE_AVAILABLE actually reaching the library target (the CMP0126 fix), every consumer of cudaq-qec-decoding-server (test_decoding_server_core, test_decoders_yaml, the CQR plugin) inherited runtime dependencies on libdoca_gpunetio.so and libcudaq-realtime-bridge-hololink.so, which require libcuda.so.1 at LOAD time. On driverless CI machines (CUDA toolkit + HSB + DOCA present, no GPU driver) all four QEC Build-and-test jobs failed: gtest_discover_tests executes the test binaries at build time and the loader cannot satisfy libcuda.so.1 -- link-time driver stubs satisfy ld but not the runtime loader. Restructure so the core library carries no GPU RoCE dependencies while keeping full CI build coverage of the HSB path: - New optional component cudaq-qec-decoding-server-gpuroce holds GpuRoceTransceiver.cpp plus GpuRoceFactory.cpp, the strong definition of the factory (cudaqx_qec_make_gpu_roce_transceiver) that DecodingServer.cpp now references weakly. All DOCA / hololink / CUDA-driver link deps move to this component. - make_transport(gpu_roce) calls the factory when it is linked in and throws a clear "GPU RoCE support is not linked into this binary" error otherwise; the ifdef and the direct GpuRoceTransceiver construction are gone from the core. - The scheduler hookup no longer dynamic_casts to the concrete type: ITransceiver grows a launch_device_scheduler(void*) virtual (default: no device scheduler), overridden by GpuRoceTransceiver to forward to launch_scheduler(). The config-driven constructor keys the hookup on the transport enum instead. - decoding_server links the component WHOLE_ARCHIVE in its proprietary-archive-gated gpu_roce block -- a weak reference does not pull archive members. - A link canary (cudaq-qec-decoding-server-gpuroce-linkcheck; never executed, not installed, not a test) preserves CI link validation of the component: a static archive alone never resolves symbols, and the one real consumer (the daemon's gpu_roce block) is gated on the proprietary cudevice archive, which CI does not provision. The canary forces full link resolution against the hololink / DOCA libraries with the driver stubs -- the same check that used to happen incidentally via the test binaries. - decoding_server's QEC_HAVE_GPU_ROCE_TRANSPORT gate is split from the proprietary-archive gate: the gpu_roce CLI branch references only the (now dependency-free) core DecodingServer API, so it compiles and links whenever HSB/DOCA are present -- CI now builds that branch too. The component + cudevice whole-archive link stays behind the archive gate, so the CI daemon remains loadable on driverless runners. Branch [2a] wraps server startup in try/catch, turning what used to be std::terminate on any gpu_roce bring-up failure into an "ERROR: gpu_roce startup failed: ..." message with exit 1. Net CI coverage: GpuRoceTransceiver.cpp compiles against the HSB/DOCA headers (component builds in `all`), its link resolves (canary), and the daemon's gpu_roce CLI branch compiles and links (split gate). No binary CI executes carries a libcuda.so.1 dependency. On dev rigs the daemon keeps full gpu_roce. As a bonus, the CQR plugin .so is loadable on driverless machines again (it too had inherited the deps). Signed-off-by: Chuck Ketcham <cketcham@nvidia.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
The server now contains no transport-specific code. --transport=<name> resolves to libcudaq-realtime-bridge-<name>.so next to the CUDA-Q realtime libraries (udp and cpu_roce ship there, incl. the TCP rendezvous and hsb_fpga QP exchange that previously lived inline here); --transport=/path/to/lib.so loads a partner's out-of-tree provider verbatim with no changes to this server. Unrecognized CLI arguments are forwarded to the provider's create(), so the existing flags (--port/--num-slots/--slot-size/--device/--local-ip/--qp_config/ --peer-ip/--remote-qp/--frame-size) keep working. Dispatch moves from a hand-rolled thread around cudaq_host_ring_dispatch_loop to the libcudaq-realtime dispatcher object (create/set_ringbuffer/set_function_table/set_control/start), the supported surface after cuda-quantum PR 4869; ring geometry comes from the provider's geometry query and readiness from its endpoint query (READY line format unchanged: port= hoisted first for the sscanf in test_decoding_server.cpp). gpu_roce is untouched: it predates the provider interface and takes a different dispatch shape (device-side scheduler; PR 670 is active in that code). CMake: the server no longer links the udp/cpu_roce transports, the host-dispatch archive, or ibverbs -- providers are runtime artifacts. Requires a CUDA-Q realtime install with bridge interface v2 (endpoint info + ring geometry) and the provider libraries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
The Hololink transceiver bring-up moves behind the CUDA-Q realtime transport-provider interface: the constructor loads the provider (built-in libcudaq-realtime-bridge-hololink.so, or the library named by CUDAQ_REALTIME_BRIDGE_LIB) and adopts its RING_BUFFER context, geometry, and endpoint identity (v2 queries, with config-derived and banner fallbacks for a v1 provider). launch_scheduler()'s device-graph scheduler wiring is unchanged; the provider's launch() now owns the Hololink monitor thread, and disconnect()/destroy() replace direct hololink_close/destroy calls. Consequence: cudaq-qec-decoding-server no longer links Hololink / DOCA / HSB / ibverbs at all -- CUDAQ_GPU_ROCE_AVAILABLE now gates only on the CUDA-Q realtime headers, libcudaq-realtime.so, and the CUDA toolkit, so the gpu_roce path compiles on machines with none of the RDMA stack installed and fails loudly at runtime when the provider .so is absent. (Also applies PR 670's CMP0126 cache-shadowing fix so the compile definition actually fires.) Runtime validation on HSB rigs still required: this box has no DOCA / HSB, so the provider .so cannot be built or exercised here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
# Conflicts: # libs/qec/lib/realtime/decoding-server-cqr/CMakeLists.txt # libs/qec/tools/decoding-server/CMakeLists.txt Signed-off-by: Ben Howe <bhowe@nvidia.com>
…roce Follow-up to the bridge-provider refactor + PR 670 merge, closing the naming leaks that made the device path look like (and require the vocabulary of) a specific wire: - GpuRoceTransceiver/GpuRoceConfig -> DeviceGraphTransceiver/-Config (files, class, weak factory symbol cudaqx_qec_make_device_graph_transceiver, component cudaq-qec-decoding-server-device-graph, defines CUDAQ_QEC_DEVICE_GRAPH_AVAILABLE / QEC_HAVE_DEVICE_GRAPH_DISPATCH). It never was a transport: it is the GPU device-graph dispatch engine riding whatever bridge provider is loaded. - Per-decoder YAML key is now `dispatch: host|device_graph` (DecoderDispatch), naming the dispatch SHAPE instead of a wire. The legacy `transport: cpu_roce|gpu_roce` key and spellings remain input-only aliases. Fixes a subtle parser bug found while testing: the alias mapping must use two-arg mapOptional, or an absent legacy key resets the parsed dispatch value to the default. - Config env vars are QEC_DEVICE_GRAPH_* with the HOLOLINK_* spellings as fallback (values are forwarded to whatever provider is loaded, which need not be Hololink; existing orchestration scripts keep working). - decoding_server routes to the device-graph path from the YAML's declared dispatch shape, not from --transport=gpu_roce (kept as a legacy alias). The CLI/YAML mismatch that used to silently select a stub transceiver is now a loud, specific error, and a non-legacy --transport value selects the provider for the device path the same way it does for the host path. Validated: udp two-process suite 5/5, core 5/5; dispatch: device_graph, legacy transport: gpu_roce + HOLOLINK_* env, and the mismatch error all exercised end-to-end (device path fails at provider load on this box, as expected without the hololink provider .so). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
…split Everything here is new in this development cycle with no deployed consumers, so the transition aliases go: - YAML: the per-decoder legacy key `transport: cpu_roce|gpu_roce` and the wire-named enum spellings are gone; `dispatch: host|device_graph` is the only form. An old config now fails at parse time with "unknown key 'transport'" (and the server reports YAML parse errors cleanly instead of terminating on the exception). - CLI: `--transport=gpu_roce` is gone; --transport only ever names the wire/provider, and the device-graph path is selected solely by the config's dispatch key (default provider: built-in hololink). READY sentinel for that path is now `QEC_DECODING_SERVER_READY device_graph`. - Env: QEC_DEVICE_GRAPH_* only; the HOLOLINK_* fallback spellings are removed. - DeviceGraphTransceiver now REQUIRES a bridge-interface-v2 provider (ring geometry + endpoint identity queries); the v1-provider fallback paths are gone. - CpuRoceTransceiver (a stub whose constructor always threw) is deleted; DecoderDispatch::host in the standalone DecodingServer path throws a direct, accurate error instead. - hsb_fpga_decoding_server_test.sh updated to the new spellings (dispatch: device_graph injection, QEC_DEVICE_GRAPH_* env, no --transport for the device path, new READY sentinel). Validated: udp two-process suite 5/5, core 5/5; device path reached via dispatch: device_graph with QEC_DEVICE_GRAPH_* env; removed spellings all fail loudly (unknown YAML key; HOLOLINK_* env ignored with a clear missing-variable error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
…n plan) Records the bridge-provider refactor end to end so it can be reproduced from the document alone: problem statement, after-architecture with the device-link vs dlopen design rules, the v2 bridge interface contract and the three providers, the server CLI/YAML/env contracts, the dispatch-vs- wire split, the ordered reproduction plan across both repos, validation gates (including the known pre-existing failure), and recorded follow-ups (rig validation, upstreaming, per-decoder rings, fan-in, vp_id). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
…r_id The QEC device wrappers now route every decoding RPC through cudaq::device_call's device-id overload with device_id == decoder_id. The CUDA-Q device_call runtime keys sessions, rings, and dispatchers by device id, so each decoder gets its own ring buffer and dispatcher: no shared ring, no head-of-line blocking between decoders, per-decoder backpressure. The payload keeps decoder_id as a cross-check (and for future multi-source fan-in). The CQR service plugin needs no change for the host path: per-device sessions share the same HOST_CALL handlers and the registry still routes by payload decoder_id. New app example surface_code-5-per-decoder-rings (+ test script, registered in ctest): two pymatching decoders decoded over two rings by one kernel, verified by (1) per-decoder corrections + dispatch count, (2) two per-device session-init lines in the runtime log, and (3) the per-device channel override (CUDAQ_DEVICE_CALL_CHANNEL= host_dispatch,1=device_dispatch) steering device 1 to the GPU channel while device 0 stays on its host ring. Pairs with cuda-quantum commit '[device-call] Per-device sessions on demand + per-device channel spec' (lazy per-device init; channel spec syntax; external channels keep one shared endpoint). Design doc per_decoder_rings_design.md records the topology, the CPU-ring + GPU-ring channel matrix, the recipe for the plugin's Gpu-mode session (the remaining piece for simultaneous CPU+GPU rings), and follow-ups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
The server now opens ONE provider instance per decoder in the YAML -- own endpoint, own ring buffer, own dispatcher -- completing the two-process form of the one-ring-per-decoder topology (the product path; the earlier in-process demo only proved the runtime mechanism). Wire contract additions (single-decoder output unchanged apart from a ring0 token): - READY: 'QEC_DECODING_SERVER_READY port=<P0> transport=udp ring0=<P0> ring1=<P1> ...' -- leading port stays ring 0's for existing single-endpoint consumers, which keep working (all traffic lands on ring 0, payload-demuxed). - Shutdown: one 'QEC_DECODING_SERVER_RING decoder=<id> dispatched=<n>' line per ring, sampled AFTER dispatcher_stop (the loop flushes its stats counter on exit). New test DecodingServerTwoProcess.TwoProcessPerDecoderRings: two decoders, two udp endpoints, one caller wiring each decoder's device_call session to its own ring via device-scoped channel args (udp-port=<P0> udp-port.1=<P1>); asserts correct corrections AND >=3 dispatches on EACH ring -- proving per-ring traffic, not one shared wire. Full suite 6/6. Pairs with cuda-quantum commits 'Register bridge handles created through the cached-provider path' (second bridge instance on one provider was unusable) and 'Per-device external channel endpoints'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
At run time the server is N independent 1-ring consumers constructed by
one setup loop: every decoder gets one bridge provider instance and one
ring (same CQR RPC wire format), and the decoder's dispatch: shape
selects the ring's consumer -- a host dispatcher-object thread, or the
CUDAQ device-graph scheduler. Host decoders and a device_graph decoder
now run simultaneously in one server; an all-device_graph config still
takes the standalone DecodingServer (HSB) path.
- DeviceGraphRingConsumer: the device-graph scheduler as a ring
CONSUMER (3 proprietary DEVICE_CALL entries + the decoder's captured
decode graph over a ring it does not own), extracted from
DeviceGraphTransceiver (which now delegates to it). Exposed through
a weak C ABI so server builds without the proprietary component link
and fail loudly at runtime instead.
- SessionRegistry accepts mixed dispatch shapes; the single-transceiver
DecodingServer paths still require uniformity (required_dispatch()
throws on mixed).
- CQR plugin exports cudaqx_qec_decoding_server_graph_resources(id) so
the server can wire the scheduler to a decoder hosted behind the
plugin (graphs are captured at session creation, before READY).
- Decoder YAML gains an optional per-decoder transport override
('transport: "udp --pinned-rings"', or 'hololink' for the builtin
provider slot). One EXTERNAL provider library per process (loader
limitation) is diagnosed with a clear error.
Validated locally: mixed config brings up ring0 (host) + ring1 (pinned
udp), READY publishes both rings, the non-graph decoder fails loudly at
the graph-capture requirement with clean teardown; full two-process
suite 6/6, per-decoder-rings example green. Positive decode on the
device ring needs a graph-capable decoder built against this tree's
ABI (see design doc follow-ups).
Pairs with cuda-quantum commits 'UDP transport external-rings API +
provider --pinned-rings' and 'Do not re-export transport-archive
symbols from the runtime' (ODR fix this work flushed out).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
Replaces the per-decoder transport override added with the mixed-
dispatch server: the wire is deployment configuration and lives OUTSIDE
the decoders list. Transports differ between rings only by dispatch
shape (a device_graph ring must be GPU-pollable), so the top-level YAML
transport: section carries provider/args plus one shape-keyed
device_graph: override -- decoder entries carry no transport
information and stay portable across environments.
transport:
provider: udp
device_graph:
args: [--pinned-rings]
Per-ring precedence: shape override > section provider/args >
--transport CLI default; an explicit --transport still overrides the
section's provider for one-off experiments.
Validated: mixed config brings up both rings from the YAML alone (no
transport CLI); full two-process suite 6/6; per-decoder-rings example
and device_call tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
… + WSL2 wall The sc4-cqr external channel guard wires per-decoder device_call sessions to their own rings via QEC_DECODING_SERVER_PORT_<id> -> udp-port.<id>= scoped args. Design doc records the positive-validation campaign: nv-qldpc rebuilt against this ABI (cuda-qx scratch merge), two-process per-decoder-ring CONTROL run decodes correctly with two host nv-qldpc rings; the mixed run's GPU scheduler serves DEVICE_CALLs over pinned-udp rings end to end (raw RPC datagram probes: reset + full-window enqueue round-trip status=0); the remaining decode-graph firing step is blocked by a platform limitation isolated with a minimal CUDA probe -- device-side cudaStreamGraphFireAndForget launches silently never execute on this WSL2 stack -- so the final step runs on the rig. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
…aunch The wedge is not a silent no-op: launching any kernel referencing device-side cudaGraphLaunch fails with cudaErrorNotSupported, visible only via cudaGetLastError() immediately after the launch (sync reports success). Documents the 10-second gating check for rig/CI machines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
…s exonerated Rig data showed the original probe failing on target hardware too, which exposed the probe's API-contract violation: fire-and-forget device graph launch is only legal from a kernel inside a device-launched graph; a plain trigger kernel correctly gets cudaErrorNotSupported everywhere. The corrected two-graph probe (trigger inside a device-launched parent) PASSES on WSL2, native SASS and forced-JIT alike, and artifact arch coverage is ruled out. The wedge is a real pipeline issue, locally reproducible; next suspects recorded (triggered-graph instantiation flags/upload, tail relaunch, decode graph execution), plus the missing device-side error surfacing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
Three-way split with a sharpened boundary: - transport_provider_design.md now owns ALL deployment contracts in one place: alongside the provider ABI, section 4.2 gains the top-level YAML transport section (shape-keyed override + precedence), the multi-ring READY tokens and per-ring shutdown lines, the N-ring server flow, and the caller-side per-ring endpoint contract (key.<id>= scoped args, channel spec). Stale follow-ups refreshed. - per_decoder_rings_design.md slims to pure topology (rings, consumers, dispatch shapes, mixed server), pointing at the provider doc for contracts; the validation campaign narrative is replaced by a short status section. - per_decoder_rings_validation_notes.md (new) is the explicit campaign log: what is validated, the open decode-graph wedge with ordered suspects, the probe methodology including the invalid-probe lesson, the proprietary-rebuild recipe, and the build/runtime gotchas. No code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
Add transport_provider_design.md section 4.3 with complete minimal recipes for the nine deployment shapes covered by the 4.2 contracts (udp loopback, per-decoder rings, cpu_roce rendezvous + hsb_fpga, mixed dispatch local/HSB, all-device_graph, partner drop-in, in-process channels). Renumber the two-axis-split section to 4.4 and cross-reference the cookbook from per_decoder_rings_design.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
Root cause of the device-graph scheduler wedge: cooperative-launch co-residency starvation. The decode graph was captured with reserved_sms=0, sizing its cooperative grid for every SM; with the persistent dispatch graph resident, the device-side fire-and-forget launch queued forever and deadlocked at grid.sync(), stalling the tail self-relaunch. DecodingSession now captures with reserved_sms=1 (QEC_DECODE_GRAPH_RESERVED_SMS overrides, e.g. on rigs where Hololink RX/TX kernels are also resident). Same bug class, second instance (documented, plugin-side fix pending): HOST-path nv-qldpc decode launches unreserved cooperative kernels and hangs when a scheduler is resident -- in mixed deployments put a CPU decoder on host rings, or plumb reservation into the host decode. DeviceGraphRingConsumer logs the new trigger diagnostics (cudaq_dispatch_get_trigger_debug) at shutdown, and dispatched() now uses an async copy on a non-blocking stream -- a legacy default-stream cudaMemcpy synchronizes against the persistent scheduler graph and self-deadlocks. Proof (two-process, pinned-udp device ring, WSL2 laptop): sc4 app, 10 shots at p_spam=0.08, multi_error_lut host ring + nv-qldpc RelayBP device ring: trigger rc=0, fires=12 == tail_relaunches, both rings dispatched=72, decoder[1] corrections=2 logical_errors=0/10, clean teardown. Full regression suites stay green. Pairs with cuda-quantum commit 'Surface the device-side trigger-launch result'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
…ss limit Pairs with cuda-quantum '[realtime] Key the bridge-provider loader by library name/path'. The server now passes each ring's resolved provider library to cudaq_bridge_create_from_library directly: no more CUDAQ_REALTIME_BRIDGE_LIB setenv side-channel for the per-ring path, no 'hololink is the builtin slot' special case, and rings may mix provider libraries freely -- the corresponding startup error and design-doc limitation are removed. DeviceGraphTransceiver likewise names its default provider library (hololink) as a plain string, with CUDAQ_REALTIME_BRIDGE_LIB still honored as the replacement override. Also note in --help that the installed cudaq-realtime is the source of truth for available providers and their arguments; the enumerated names and args are examples. Validated: two-process suite 4/4, per-decoder-rings app test, and the full mixed device-graph E2E (trigger rc=0, fires=10=tail_relaunches, both rings dispatched=60, real corrections on both decoders). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
decoding_server.cpp: - Set transport_from_cli when --transport is parsed: the documented "explicit --transport overrides the YAML transport section" precedence was dead code (the section's provider always won). - The all-device_graph path now resolves its provider with the same precedence as the per-ring loop (CLI > section device_graph override > section provider > built-in default) instead of consulting only --transport != udp. - The v1-provider endpoint fallback advertises the ring's own resolved provider, not the process default. - Guard --timeout and the provider port token against non-numeric input (clean error instead of an uncaught std::stoi/stoul exception). - Include DeviceGraphRingConsumer.h so the weak C-ABI redeclarations are compile-checked against the canonical prototypes. CQR components: - Consolidate the SM-reservation knob on the documented device-graph env family: DecodingSession reads QEC_DEVICE_GRAPH_RESERVED_SMS (was the undocumented QEC_DECODE_GRAPH_RESERVED_SMS), and the transceiver's parsed-but-unused reserved_sms field is removed. - Delete dead helpers left behind when the scheduler wiring moved into DeviceGraphRingConsumer (alloc_pinned_mapped, populate_device_call, GPU_CUDA_CHECK) -- they produced -Wunused-function noise. - DeviceGraphRingConsumer.h self-compiles with the component macro off (the C-ABI block's includes were inside the #ifdef). - Free the function table, shutdown flag, and stats allocations when the scheduler-stream create fails (the one error path without a cleanup ladder). - Fix a stale HOLOLINK_FRAME_SIZE error message (QEC_DEVICE_GRAPH_FRAME_SIZE) and a stale SessionRegistry comment; correct the READY-line doc: the leading port= token belongs to the first decoder listed in the YAML, not necessarily decoder id 0. Validated: two-process suite 4/4, per-decoder-rings app test, and the mixed device-graph E2E (trigger rc=0, fires=10=tail_relaunches, both rings dispatched=60, correct RelayBP decodes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
… path The wire's identity lives in the YAML transport section; --transport now applies only to configs that intentionally leave the wire unspecified (one YAML reused across wires, selected per launch -- the pattern every two-process test uses). A config that names a provider combined with an explicit --transport is rejected at startup instead of resolved by a precedence rule, in both the mixed per-ring path and the standalone all-device_graph path. Help text and design docs updated to match. New coverage for the YAML path, which previously had none: - DecoderYAMLTest.TransportSectionAndMixedDispatch: round-trips the transport section (provider/args + device_graph shape override) with a host + device_graph decoder mix, and pins the exact YAML key spelling against a literal document. - DecodingServerTwoProcess.TwoProcessHostDispatchYamlTransportSection: full two-process decode where the server is launched WITHOUT --transport and the YAML names the provider and its args. - DecodingServerTwoProcess.TransportCliConflictsWithYamlSection: the conflict is rejected before READY with a nonzero exit and a pointed error message. - app_examples.surface_code-4-yaml-mixed-dispatch: the flagship mixed host+device_graph two-process flow as a gated ctest (registered when the server links the device-graph component; skips at runtime without a GPU or the nv-qldpc plugin). Asserts per-decoder rings on the READY line, per-decoder results in the app, a healthy scheduler (trigger rc=0, fires == tail_relaunches > 0), and traffic on both rings. ServerProcess (test harness) learns to launch without the CLI transport args, to fold stderr into the captured output, to report the child's exit code, and to bail out on EOF instead of burning the READY timeout. 29/29 in the decoding-server/YAML/session/app regression sweep, including the new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
The after-diagram now reflects the one-ring-per-decoder consumers and the YAML transport section (--transport as fallback, conflict rejected). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ben Howe <bhowe@nvidia.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
Collaborator
Author
|
/ok to test 2229b85 |
1tnguyen
reviewed
Jul 13, 2026
Collaborator
Author
|
/ok to test 0d6b2d1 |
Collaborator
|
Not sure if this is still relevant after this PR as I haven't read the code, but as a note to self: should use config as the single source of truth for parameters and remove env vars used on the realtime path (e.g., gpu roce accepts env var and config) |
…ridge-670 Signed-off-by: Ben Howe <bhowe@nvidia.com> # Conflicts: # .cudaq_version
Collaborator
Author
|
/ok to test 9d0dfe6 |
bmhowe23
commented
Jul 22, 2026
bmhowe23
added a commit
to NVIDIA/cuda-quantum
that referenced
this pull request
Jul 23, 2026
…sions (#4915) Note: this is to be paired with CUDA-Q QEC PR: NVIDIA/cudaqx#682 ## Summary This PR turns the realtime transport layer into a true plug-in boundary and extends the device_call runtime to support one independent ring/dispatcher per device. Transport providers are now plain shared libraries selected by name or path at runtime; the core dispatcher, the device_call runtime, and every consumer (notably the CUDA-QX decoding server, see the companion CUDA-QX PR) speak only the `cudaq_bridge_*` C API and stay wire-agnostic. An external transport provider -- a partner's NIC stack, a lab interconnect, a simulator -- drops in as a single `.so` with zero changes to cudaq or to consumers. --------- Signed-off-by: boschmitt <7152025+boschmitt@users.noreply.github.com> Signed-off-by: Ben Howe <bhowe@nvidia.com> Signed-off-by: Renaud Kauffmann <rkauffmann@nvidia.com> Co-authored-by: boschmitt <7152025+boschmitt@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Renaud Kauffmann <rkauffmann@nvidia.com>
Collaborator
Author
|
/ok to test c62fe8b |
The July 22 merge of main (9d0dfe6) brought in the cpp_realtime_decoding_api.rst additions from NVIDIA#708, which document main's config API. This branch had already renamed DecoderTransport to DecoderDispatch and moved wire selection into the server-level transport section, so Sphinx (running with -n -W) failed the Docs / Build job with five warnings. - Point the doxygenenum directive at DecoderDispatch and add directives for the new transport_config and transport_shape_override structs. - Reword the transport_shape_override doc comment so doxygen does not auto-link transport_config::device_graph; breathe emits no label for variable-member anchors, so that link would itself be a new -W failure once the struct is documented. Verified locally with the CI sphinx flags (-n -W equivalent, --keep-going): all five warnings are gone and no new warnings appear relative to the pre-fix baseline. Signed-off-by: Chuck Ketcham <cketcham@nvidia.com>
Commit 49eb8e1 removed the HSB-specific parsing of the provider's endpoint info (qp/rkey/buffer_addr) and made the string ride through verbatim, but left the doc comment's original first paragraph in place, which still described the parsed QP-number/rkey/buffer-address behavior and contradicted the paragraph below it. Delete the stale paragraph. Signed-off-by: Chuck Ketcham <cketcham@nvidia.com>
The realtime bridge-provider work this branch depends on landed in NVIDIA/cuda-quantum via PR #4915 (squash commit 2a7911fca126, merged 2026-07-23). Repoint the pin from the bmhowe23/cuda-quantum fork ref to the upstream squash commit. The fork ref 26947049 is an ancestor of the merged PR head, so this loses nothing; it picks up the PR's final spelling fixes plus the newer main that was merged into the branch before squashing. Signed-off-by: Chuck Ketcham <cketcham@nvidia.com>
The design docs shipped with development-time framing that goes stale at merge: status headers naming the development branch pair, a "what an agent should do" reproduction plan written against a PR-4869 worktree and pre-merge commit sequencing, and a validation-status section still describing the decode-graph wedge as open (it was resolved -- the reserved_sms co-residency fix -- and the flow is now the gated app_examples.surface_code-4-yaml-mixed-dispatch ctest). - Remove per_decoder_rings_validation_notes.md: a dated campaign log whose rebuild instructions reference internal development infrastructure; not public documentation. Drop its cross-references from both design docs. - Drop branch-pair/date status lines from both design docs. - Delete transport_provider_design.md section 5 (reproduction plan); renumber sections 6-7. Reword the two PR-4869 API references. - Rewrite per_decoder_rings_design.md section 7 to the current validation state and drop the resolved wedge follow-up bullet. - Reword the rig-validation follow-up to not reference "the branch". Signed-off-by: Chuck Ketcham <cketcham@nvidia.com>
cketcham2333
marked this pull request as ready for review
July 23, 2026 19:43
Collaborator
|
/ok to test 2005267 |
cketcham2333
approved these changes
Jul 23, 2026
…nt cleanup
- config schema: advertise the per-decoder `dispatch` key and the
server-level `transport` section; drop the removed per-decoder
`transport: {cpu_roce, gpu_roce}` key. Keeps decoder_config_json_schema()
in sync with MappingTraits<decoder_config>, which had drifted (the schema
still named the old key and omitted the new ones).
- python bindings: expose decoder_dispatch, transport_config,
transport_shape_override, decoder_config.dispatch and
multi_decoder_config.transport; add a jsonschema validation test that
populates dispatch/transport and asserts the schema accepts them and
rejects the removed per-decoder transport key (fails against the old schema).
- ctest: gate app_examples.surface_code-4-yaml-mixed-dispatch on
CUDAQ_QEC_DEVICE_GRAPH_AVAILABLE AND the proprietary target, matching the
server's actual link condition, so it does not false-FAIL where device_graph
is not linked.
- DeviceGraphRingConsumer::dispatched(): pin gpu_id_ for the stat copy and
restore the caller's device (d_stats_ lives on gpu_id_).
- DecodingSession: validate QEC_DEVICE_GRAPH_RESERVED_SMS (accept only a
fully-parsed value >= 1, warn otherwise) so a malformed override cannot
silently reinstate the reserve-all-SMs wedge.
- decoding_server: remove the unused wants_device_graph.
- remove the transient realtime design docs (transport_provider_design.md,
per_decoder_rings_design.md).
- sweep stale gpu_roce / "GPU RoCE" / HOLOLINK_* comments to
device_graph / QEC_DEVICE_GRAPH_*.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
Collaborator
Author
|
/ok to test 6c36ee9 |
Collaborator
|
/ok to test e2bbb3d |
cketcham2333
enabled auto-merge (squash)
July 24, 2026 00:39
cketcham2333
added a commit
that referenced
this pull request
Jul 28, 2026
…unded spin-then-block) (#750) ## Description A HOST_CALL RPC served by the decoding server crossed three thread boundaries — CUDAQ dispatcher → receiver-thread inbox, receiver → session worker queue, worker → dispatcher response promise. Each handoff is a condvar/promise futex sleep/wake costing ~3–4 µs on Grace-class hosts, plus a ~90–115 µs p99 tail when the sleeping core sits in a deep idle state. Those handoffs, not the decoders, dominated the handler latency observed during testing. This PR removes or short-circuits all three, on top of the #682 architecture: 1. **Direct dispatch** — `CqrTransceiver::inject()` hands each translated frame straight to `RpcDispatcher::dispatch()` on the calling CUDAQ dispatcher thread via a sink installed by `DecodingServer` at construction (`ITransceiver::install_dispatch_sink`). No receiver thread is started for CQR transports; the inbox/recv path remains for transports that decline the sink (and stays unit-tested). `RpcDispatcher::dispatch` gains a catch-all so no handler exception can unwind into the transport. 2. **Bounded spin-then-block** (`SpinPolicy.h`) — the per-decoder session worker spins on an atomic queue sequence, and the blocking-RPC waiter on a stack-owned completion flag, before falling back to their untouched condvar/promise waits. Default budget 200 µs; `QEC_DECODING_SERVER_SPIN_US` overrides (`0` = always block, `-1` = never block). Idle threads park after one budget window. 3. **Small levers** — `notify_one()` hoisted out of `queue_mutex` in `try_enqueue`, and an opt-in `/dev/cpu_dma_latency` hold in the standalone tool (`QEC_DECODING_SERVER_CPU_DMA_LATENCY_US`) that removes the deep-idle p99 tail. `HopStats.h` adds env-gated per-request latency probes across the hops (`QEC_DECODING_SERVER_HOP_STATS[=1|total|_CSV]`), off by default (one predicted branch per probe; p50 unchanged within 0.1 µs), plus `QEC_PIN_DISPATCHER/WORKER` thread pinning for controlled measurements. ## Runtime / performance impact Stock configuration (no env vars), HOST_CALL round trip, median (p99), udp two-process rig, d3/r12 surface code, 2000 shots: | RPC | before | after | |---|---|---| | enqueue_syndromes | 11.0 µs (108) | 0.8 µs (5.3) | | get_corrections | 18.1 µs (124) | 1.6 µs (7.2) | | reset_decoder | 18.0 µs (120) | 4.8 µs (8.5) | --------- Signed-off-by: Chuck Ketcham <cketcham@nvidia.com>
melody-ren
added a commit
to melody-ren/cudaqx
that referenced
this pull request
Aug 5, 2026
Conflict resolutions, all in favor of upstream's post-NVIDIA#682/NVIDIA#750 realtime structure with the inproc_rpc path kept out: - qec_realtime_session.cpp: deleted (upstream's touch was to removed code). - GpuRoceTransceiver.cpp: deleted; NVIDIA#754 renamed it to DeviceGraphTransceiver.cpp. The stale qec_realtime_session comment reference the branch scrubbed moved to DeviceGraphRingConsumer.cpp and is scrubbed there instead. - realtime_decoding.cpp: reset_decoder takes NVIDIA#698's pin_decode_device(), dropping the rpc_producer dispatch branch. - decoding_server.cpp: upstream's per-ring rewrite; its comments no longer reference qec_realtime_session, so the branch's scrub is subsumed. - app_examples/CMakeLists.txt: upstream's add_surface_code_4_yaml_test signature (onnx_path folded into ARGN) with an empty test environment. - surface_code-4-yaml-test.sh: keep the num_logical echo, drop the CUDAQ_QEC_REALTIME_MODE echo. - realtime_relay_bp.rst: single Building subsection renamed Hololink -> HSB, target list on the NVIDIA#754 names, CI-unit-test section dropped (the test is removed) and the Surface Code Test section kept. Upstream added no new inproc_rpc surface: DeviceGraphTransceiver, DeviceGraphRingConsumer, surface_code-5-per-decoder-rings and realtime_decoding_demo are all inproc-free. Signed-off-by: Melody Ren <melodyr@nvidia.com>
melody-ren
added a commit
to melody-ren/cudaqx
that referenced
this pull request
Aug 5, 2026
The inproc_rpc removal parked these behind if(FALSE) on the theory that they needed the device-graph scheduler. They did not: --use-relay-bp only sets nv-qldpc parameters when the decoder config is generated, so the cases need the plugin and nothing else. Re-enable them on the direct-call path, on the same gate the dual-parse cases already use, and fold that repeated gate into _SC4_HAVE_NV_QLDPC. What the inproc_rpc cases did provide was relay-BP over a *dispatched* path. Restore that against the external decoding server, which since NVIDIA#682 gives each decoder its own ring: one all-nv-qldpc trio, and the pymatching + trt + nv-qldpc trio when the trt plugin and python-onnx are present. Both assert server-side decode counts, and both get a 600s timeout because relay BP iterates. Signed-off-by: Melody Ren <melodyr@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Built on the companion cuda-quantum PR NVIDIA/cuda-quantum#4915, now merged upstream (squash
2a7911f, 2026-07-23);.cudaq_versionpins that commit, so this branch builds against stockNVIDIA/cuda-quantummain.Summary
This PR re-architects the realtime decoding server around the CUDA-Q bridge-provider boundary. The server no longer contains any transport code: the wire is named in the deployment YAML (or a
--transportfallback), loaded at runtime as alibcudaq-realtime-bridge-<name>.soprovider, and every decoder gets its own ring buffer and its own consumer -- a host dispatcher thread fordispatch: hostdecoders, or the GPU device-graph scheduler fordispatch: device_graphdecoders, both side by side in one server process. A partner transport library drops in as a single.sowith zero decoding-server changes.The guiding litmus test for the layering: cudaq-realtime and its transport providers never say "QEC" or "decoder"; the QEC library code never names a wire (UDP, RoCE, hololink). Wire names appear only where deployments are described: YAML configs, launch scripts, and tests.
Before (main prior to #670; #670 landed the first step)
DT_NEEDEDon the server binary). Adding a wire meant editing the server.--transporton the CLI AND a per-decoder YAML key), with silent misconfiguration when they disagreed.device_id == decoder_id) were impossible.gpu_roce) was hololink-only, named after one wire, and could not coexist with host-dispatch decoders in the same process.transport:key remains the selection knob.After
decoding_serverspeaks only YAML + thecudaq_bridge_*C API. Provider libraries resolve by name next to the cudaq-realtime install (QEC_BRIDGE_PROVIDER_DIR) or verbatim by path (partner drop-in). The QEC libraries link no transport libraries; the rendezvous/hsb_fpga handshake moved down into the cpu_roce provider (companion cuda-quantum PR).dispatch: host|device_graphpicks HOW RPCs execute; the top-level, shape-keyedtransport:section (provider,args, plus adevice_graph:override for the rings that must be GPU-pollable) picks the wire for the whole deployment.--transportis a fallback for configs that intentionally leave the wire unspecified (one YAML reused across wires, selected per launch); a YAML that names a provider combined with an explicit--transportis a startup error, never a silent precedence decision. All pre-release aliases (transport:as a per-decoder key,--transport=gpu_roce,HOLOLINK_*env fallbacks) are removed -- this component is new this cycle, so there is no compatibility surface to keep.QEC_DECODING_SERVER_READY port=<p0> ... ring<id>=<port>) before any blocking connect, and shutdown reports per-ring traffic (QEC_DECODING_SERVER_RING decoder=<id> dispatched=<n>). Callers route withcudaq::device_call(decoder_id, ...)and per-device endpoint args (udp-port.<id>=<port>).DeviceGraphRingConsumerruns the CUDA-Q device-graph scheduler (self-relaunching GPU dispatch graph + the decoder's captured decode graph) as a ring consumer over any GPU-pollable ring -- hololink DOCA rings on a rig, or pinned+mapped udp rings (--pinned-rings) on any CUDA box.GpuRoceTransceiverwas renamed to what it actually is (DeviceGraphTransceiver, the dispatch engine, transport-blind) and now delegates to the ring consumer; the standalone all-device_graph path remains for the HSB flow.grid.sync().DecodingSessionnow captures withreserved_sms=1(QEC_DEVICE_GRAPH_RESERVED_SMSto raise it on rigs where hololink RX/TX kernels are also resident). The same bug class affects host-path GPU-cooperative decodes when a scheduler is resident -- documented, with the mixed-deployment guidance of a CPU decoder on host rings until the plugin plumbs reservation into its host path.QEC_DECODING_SERVER_STATS=1prints per-decoder counters in the mixed server too; they are host-session counters, so adevice_graphring legitimately reportsdecodes=0-- its execution evidence is the trigger diagnostics (fires == tail_relaunches) and the per-ringdispatched=line.app_examples.surface_code-4-yaml-mixed-dispatch-- the flagship mixed flow as a gated ctest (registered only when the server links the device-graph component; skips without a GPU or the nv-qldpc plugin) asserting scheduler health via the new trigger diagnostics (rc=0, fires == tail_relaunches > 0). The generated JSON config schema (decoder_config_json_schema()) advertises the new per-decoderdispatch:key and the top-leveltransport:section (the removed per-decodertransport:key is gone from the schema too), with a pythonjsonschematest that validates a populateddispatch/transportdocument against it and rejects the removed key. The python config bindings expose the new fields as well --decoder_config.dispatch,multi_decoder_config.transport, and thedecoder_dispatch/transport_config/transport_shape_overridetypes.Sample configurations
Three representative deployment shapes (YAML config + exact launch line + matching caller config):
Two host decoders, one udp ring per decoder -- the YAML says nothing about the wire, so
--transport(default udp) selects it per launch; the READY line publishes every ring's endpoint.Caller routes per decoder with device-scoped endpoint args:
--cudaq-device-call=udp udp-host=127.0.0.1 udp-port=<P0> udp-port.1=<P1>.Mixed dispatch (host CPU decoder + GPU device-graph decoder), runs on any CUDA box -- the wire lives in the YAML transport section; the shape-keyed override adds
--pinned-ringsonly to the device_graph ring so the GPU scheduler can poll it. No--transporton the command line (combining both is a startup error). This is exactly what the new gated ctest runs.Partner transport drop-in -- an out-of-tree provider library is named by path, verbatim; its args are forwarded untouched. Zero decoding-server changes.
Validation
hololink_gpu_idenv var #692): two-process suite, DecoderYAMLTest 18/18, per-decoder-rings + mixed-dispatch app tests, python surface_code-1, and the full mixed E2E all green against an nv-qldpc plugin rebuilt for Virtualize methods in realtime decoder API #674's virtualized decoder API (an ABI break for out-of-tree plugin builds).Breaking changes vs. main
All of these surfaces are new this release cycle, so no deprecation aliases are kept:
transport:YAML key and theDecoderTransportenum are replaced by per-decoderdispatch: host|device_graphplus the top-leveltransport:section. A config using the old key fails to parse loudly.cuda_device_id(feat(qec): add cuda_device_id placement knob for GPU decoders #690) is kept unchanged and now also places the device_graph rings and scheduler.QEC_DEVICE_GRAPH_*(wasHOLOLINK_*), and--transport=gpu_roceno longer exists: device_graph is a dispatch shape named in the YAML;--transportonly ever names a wire provider. The HSB test script is updated to match (injectsdispatch: device_graph+cuda_device_id, setsQEC_DEVICE_GRAPH_*, waits forREADY device_graph).CpuRoceTransceiver(the always-throwing placeholder) is removed; cpu_roce is served by its bridge provider.Dependencies
2a7911f) and itself built on cuda-quantum #4869..cudaq_versionpins that squash commit, so this branch builds against stock main. This PR consumescudaq_bridge_create_from_library, the v2 endpoint/geometry queries, per-device sessions and device-scoped channel args, the udp provider's--pinned-rings, andcudaq_dispatch_get_trigger_debug.GpuRoce{Transceiver,Factory,LinkCheck}becomeDeviceGraph*(same weak-factory / optional-component structure, now transport-blind), while qec/realtime: Relay BP (nv-qldpc) gpu_roce profile for the HSB decoding-server test #670's post-review hardening (ring-size and host-page-alignment validation,cuda_device_id-driven GPU placement threaded through the factory) is preserved in the renamed files.CUDAQ_QEC_REALTIME_CUDEVICE_PROPRIETARY_ARCHIVE); builds without them still compile and the device-graph paths fail at runtime with a clear not-linked error, and the gated ctest is simply not registered.🤖 Generated with Claude Code