Skip to content

Load cuQuantum libraries with RTLD_LOCAL and explain fork-after-CUDA-init failures - #450

Merged
ciaranra merged 7 commits into
devfrom
cuda-process-isolation
Aug 7, 2026
Merged

Load cuQuantum libraries with RTLD_LOCAL and explain fork-after-CUDA-init failures#450
ciaranra merged 7 commits into
devfrom
cuda-process-isolation

Conversation

@ciaranra

@ciaranra ciaranra commented Aug 6, 2026

Copy link
Copy Markdown
Member

Problem

Two process-coexistence defects surfaced when PECOS shares a Python process with another CUDA-using program:

  1. The cuQuantum runtime loader opened every CUDA library with RTLD_NOW | RTLD_GLOBAL, injecting unmangled C CUDA symbols into the process-global scope. CUDA symbols are identical across versions, so this pollutes symbol resolution for any other CUDA-using library in the same process (and exposes PECOS to the same interference in reverse).
  2. When a host program initializes CUDA in its main process and then forks multiprocessing workers, a worker constructing a custatevec simulator fails with cudaErrorInitializationError (cupy wrapper) or CUSTATEVEC_STATUS_NOT_INITIALIZED (Rust wrapper) — CUDA contexts do not survive fork. The resulting errors named neither the cause nor the remedy.

Changes

  • pecos-cuquantum-sys loader: RTLD_LOCAL instead of RTLD_GLOBAL. Dependencies are preloaded by full path in dependency order and ELF resolves later NEEDED entries against already-loaded objects by SONAME, so global visibility was never required. Loader comments corrected: shipped cuQuantum libraries statically link cudart (no NEEDED on libcudart), so the cudart preload is best-effort; the cuTENSOR preload is the load-bearing one.
  • pecos-cuquantum error rendering: all four NotInitialized variants (state vector, stabilizer, tensor network, density matrix) now explain the fork-after-CUDA-init cause and advise the multiprocessing "spawn" start method.
  • Legacy cupy-based custatevec wrapper: construction failures with CUDA runtime status 3 (cudaErrorInitializationError) or CUDA driver status 3 (CUDA_ERROR_NOT_INITIALIZED) re-raise with the same guidance, chained to the original exception. Matching is on numeric status codes (cupy exposes no named constants).
  • Docs: CUDA setup guide now covers CUDA + multiprocessing (the constraint is parent-initialized-then-fork; PECOS's built-in multiprocessing engine already uses spawn) and notes the Rust bindings' local symbol visibility.

Verification

  • 27/27 CUDA simulator integration tests pass on an RTX 4090 with the RTLD_LOCAL build; results are identical under RTLD_LOCAL and RTLD_GLOBAL.
  • dlopen-level validation: all six libraries load and all four handle types create under RTLD_LOCAL; dlsym(RTLD_DEFAULT, ...) confirms CUDA symbols no longer enter the global scope; the SONAME-reuse path (libcutensornet resolving a path-preloaded libcutensor absent from every search path) works without global visibility.
  • Fork scenario reproduced end to end: a forked child of a CUDA-initialized parent now receives the actionable error message.
  • New unit tests run without cupy installed (dependency stubs) and also pass against real cupy 14.1.1; the Rust rendering test covers hint presence for all four NotInitialized variants and absence elsewhere.
  • just lint clean.

Known residual: real cuTensorNet contractions / cuDensityMat evolutions (cuTENSOR's runtime-loaded JIT kernels) are not exercised by the repo's tests under either linking mode.

Addendum: fork-poison guard and spawn discipline (second commit)

  • New pecos.simulators._cuda_fork_guard: the Python CUDA simulator wrappers mark their first real CUDA call and register os.register_at_fork hooks. A child forked after PECOS initialized CUDA now fails deterministically (measured 0.04 ms, before any CUDA call) with the same spawn guidance, and the forking parent gets a one-time RuntimeWarning. A neighbor's CUDA initialization cannot be detected; those cases still surface the guided error from the CUDA layer. Direct users of the raw pecos_rslib_cuda extension bypass the guard deliberately; the extended Not initialized messages are their backstop.
  • Availability probes documented as an invariant: is_available() / try_load() only dlopen — never create handles or contexts — so probing can never poison later forks.
  • New spawn-discipline regression test: scans the pecos source tree for fork-hazard patterns (get_context("fork"), set_start_method("fork"), bare multiprocessing.Pool() with an explicitly empty allowlist.
  • Verified: guard fires end-to-end on an RTX 4090 (marked parent + fork + instant child failure + one-time warning); 27/27 CUDA integration tests still pass through the guarded wrappers in normal single-process use; all new unit tests are GPU-free and pass without cupy installed; just lint clean.

@ciaranra
ciaranra merged commit 69753a1 into dev Aug 7, 2026
64 checks passed
@ciaranra
ciaranra deleted the cuda-process-isolation branch August 7, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant