Repository navigation
macOS: OMP Error #15 (duplicate libomp) aborts at first parallel region #27
Description
On macOS, a process that loads sbd._core_cpu can abort at SBD's first parallel region:
OMP: Error #15: Initializing libomp.dylib, but found libomp.dylib already initialized.
Signal: Abort trap: 6 (6)
[ 8] libomp.dylib __kmpc_fork_call + 52
[ 9] _core_cpu.cpython-314-darwin.so sbd::GenerateExcitation...
[10] _core_cpu.cpython-314-darwin.so sbd::MakeHelpers...
[11] _core_cpu.cpython-314-darwin.so sbd::tpb::diag...
Both names are libomp.dylib — two copies of the same LLVM runtime, not the more familiar Intel libiomp5 vs LLVM libomp clash.
Root cause
Two Homebrew formulae each insist on their own OpenMP, and they arrive by different routes:
_core_cpu.so ──(-fopenmp, under Homebrew LLVM clang)──> Cellar/llvm/<ver>/lib/libomp.dylib
└─(-lopenblas)──> libopenblas.dylib ─────> Cellar/libomp/<ver>/lib/libomp.dylib
Homebrew's openblas depends on libomp (deps: ['gcc', 'libomp']) and loads that copy at run time. Homebrew's llvm builds the openmp runtime, so its clang carries a libomp.dylib of its own and -fopenmp links that one. Link both and the process holds two.
Only one of those is on SBD's link line; the other is transitive through OpenBLAS, which is why otool -L on _core_cpu.so does not show it.
Measured by enumerating loaded images through dyld after each import stage:
[baseline] 0 OpenMP image(s)
[after pyscf] 0 OpenMP image(s)
[after qiskit_addon_sqd.fermion (pulls jax)] 0 OpenMP image(s)
[after qiskit + preset passmanager] 0 OpenMP image(s)
[after importing sbd] 0 OpenMP image(s)
compiled backends: ['cpu']
[after sbd.get_backend() -- loads _core_cpu] 2 OpenMP image(s)
/opt/homebrew/Cellar/libomp/23.1.0/lib/libomp.dylib
/opt/homebrew/Cellar/llvm/23.1.0/lib/libomp.dylib
Nothing is mapped until get_backend(), because since #23 the backends load lazily, one per process, on first use. That is why the abort lands at the first parallel region rather than at import — and why a probe that only imports sbd sees nothing.
When this bites
Only when the build uses a compiler that ships its own OpenMP. Under Apple clang there is no second copy: Apple clang has no OpenMP of its own, so SBD takes libomp from the standalone Homebrew formula — the same file OpenBLAS loads. One runtime, no conflict. That is why this repo's own macOS CI has always been green.
It reproduces when CC/CXX is pinned to Homebrew LLVM, which is what Qiskit/qiskit-addon-sqd#367 did, in order to build another extension (fulqrum passes a bare -fopenmp that Apple clang rejects).
What this is not
Several plausible explanations were checked and ruled out:
pyscflinks no OpenMP at all. It was the leading suspect and is not involved.qiskit-aerships a bundledlibomp.dylib— the only bundled OpenMP in the environment — but it is never imported by the notebook in question (sys.modules has qiskit_aer: False) and its copy is not among the mapped images.ffsimuses Rust/Rayon, not OpenMP.- Not a cross-notebook effect: under
nbmakeeach notebook gets its own kernel. - Not fixable from the consumer's
LDFLAGS/CPPFLAGSalone:setup.pychose its libomp from hardcoded paths and ignored those variables. (That part is addressed on thedarwin-single-libompbranch; see below.)
Ways out
- Build with Apple clang and let the standalone
libompserve both SBD and OpenBLAS. Simplest, and what this repo's CI does. Downstream this means not pinning a compiler job-wide — #367 now builds each compiled dependency with the compiler it needs, and its macOS job is green with the released 1.6.1, so no new release is required for that. - Use a conda (or pixi) environment, where one
llvm-openmpserves the compiler and the BLAS alike. This is the only option that also covers users rather than just CI, and Conda-friendly builds, AMD GPU support, and macOS OpenMP #23's conda-first preference already points at it. Worth stating outright in the README for macOS. - Use a BLAS not tied to a second OpenMP — Accelerate, or a conda-provided OpenBLAS. This is what would make a Homebrew-LLVM build viable at all.
Note that 1 and 2 leave a sharp edge: if two extensions in one process are built by different compilers, they can still end up with different libomps. Measured downstream — SBD under Apple clang plus fulqrum under Homebrew LLVM gives 2 images again, even though each works alone. The durable fix for that pairing is for fulqrum to accept Apple clang (-Xpreprocessor -fopenmp), after which one runtime serves everything.
Not the fix
KMP_DUPLICATE_LIB_OK=TRUE suppresses the abort, and the runtime's own hint offers it — as "an unsafe, unsupported, undocumented workaround" that "may cause crashes or silently produce incorrect results." For an eigensolver, trading a clean abort for possibly-wrong energies is the wrong trade.
Status
- Linux is unaffected throughout.
- The
darwin-single-libompbranch makessetup.pyprefer a libomp shipped beside the compiler, which removes SBD's direct second copy. It does not fix this issue — OpenBLAS still supplies one — so macOS: take OpenMP from the compiler's own tree when it has one #34 was closed rather than merged; see that PR for the measurements. The branch is a reasonable starting point once option 3 exists. - Two incidental findings worth recording, since each cost a debugging round:
omp.hlives in the clang resource directory (lib/clang/<ver>/include), not<prefix>/include. Askclang++ -print-resource-dir.- tox discards the build backend's output at default verbosity, stdout and stderr alike, so a
setup.pythat picks the wrong OpenMP leaves no trace in CI.tox -vvshows it.
This issue body was updated by Claude Opus 5 under my guidance.
Activity
Add a macOS cell to the development version tests
TEMPORARY: build sbd-eigensolver from darwin-single-libomp
TEMPORARY: report OpenMP linkage of compiled extensions on macOS
Scope the Homebrew LLVM pin to fulqrum's own build step
hfwen0502 commentedon Sep 18, 2026
I think it's very likely that this issue will be resolved once we switch to conda. I cannot reproduce it in Conda environment.
macOS: build the two compiled dependencies as wheels, each with its c…
README: use the documented conda command in the quickstart, not an in…
README: audit the troubleshooting entries against the code
README: use the documented conda command in the quickstart, not an in…
README: audit the troubleshooting entries against the code
Move examples out of the package, split install docs into INSTALL.md (#…
Add guide for using the SBD eigensolver (#367)
macOS: find libomp in the environment being built into (#58)
macOS: find libomp in the environment being built into (#58) (#59)
Give SCIState its amplitudes in PySCF's sign convention (backport #62) (