Skip to content

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:

  • pyscf links no OpenMP at all. It was the leading suspect and is not involved.
  • qiskit-aer ships a bundled libomp.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.
  • ffsim uses Rust/Rayon, not OpenMP.
  • Not a cross-notebook effect: under nbmake each notebook gets its own kernel.
  • Not fixable from the consumer's LDFLAGS/CPPFLAGS alone: setup.py chose its libomp from hardcoded paths and ignored those variables. (That part is addressed on the darwin-single-libomp branch; see below.)

Ways out

  1. Build with Apple clang and let the standalone libomp serve 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.
  2. Use a conda (or pixi) environment, where one llvm-openmp serves 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.
  3. 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-libomp branch makes setup.py prefer 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.h lives in the clang resource directory (lib/clang/<ver>/include), not <prefix>/include. Ask clang++ -print-resource-dir.
    • tox discards the build backend's output at default verbosity, stdout and stderr alike, so a setup.py that picks the wrong OpenMP leaves no trace in CI. tox -vv shows it.

This issue body was updated by Claude Opus 5 under my guidance.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions