You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Homebrew's openblasdepends 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:
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
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.
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.
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.
On macOS, a process that loads
sbd._core_cpucan abort at SBD's first parallel region:Both names are
libomp.dylib— two copies of the same LLVM runtime, not the more familiar Intellibiomp5vs LLVMlibompclash.Root cause
Two Homebrew formulae each insist on their own OpenMP, and they arrive by different routes:
Homebrew's
openblasdepends onlibomp(deps: ['gcc', 'libomp']) and loads that copy at run time. Homebrew'sllvmbuilds theopenmpruntime, so its clang carries alibomp.dylibof its own and-fopenmplinks 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 -Lon_core_cpu.sodoes not show it.Measured by enumerating loaded images through dyld after each import stage:
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 importssbdsees 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
libompfrom 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/CXXis pinned to Homebrew LLVM, which is what Qiskit/qiskit-addon-sqd#367 did, in order to build another extension (fulqrumpasses a bare-fopenmpthat 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.nbmakeeach notebook gets its own kernel.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
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.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.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=TRUEsuppresses 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
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.omp.hlives in the clang resource directory (lib/clang/<ver>/include), not<prefix>/include. Askclang++ -print-resource-dir.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.