Skip to content

build(wheels): source arpack/openblas/boost from conda-forge (Linux + macOS)#1057

Open
IvanaGyro wants to merge 10 commits into
masterfrom
build/conda-forge-wheel-deps
Open

build(wheels): source arpack/openblas/boost from conda-forge (Linux + macOS)#1057
IvanaGyro wants to merge 10 commits into
masterfrom
build/conda-forge-wheel-deps

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

Two independent problems traced back to where the Linux and macOS wheel builds source their native dependencies from:

  • Linux: tools/cibuildwheel_before_all.sh installs boost-devel openblas-devel arpack-devel from EPEL via dnf inside the manylinux_2_28 container. EPEL's arpack-devel links whichever OpenBLAS dnf resolves — a pthreads-threaded build with its own independent thread pool. HPTT and cytnx's own code use OpenMP (-fopenmp, see CytnxBKNDCMakeLists.cmake), so a BLAS call made from inside an OpenMP region oversubscribes the CPU (two independent thread pools competing).
  • macOS (Decide a deliberate strategy for the macOS deployment-target floor and the Linux GCC/libstdc++ floor #963): tools/cibuildwheel_before_all_macos.sh installs arpack/boost/openblas/libomp from Homebrew and computes MACOSX_DEPLOYMENT_TARGET from their minos. Homebrew's bottles are built against whatever deployment target Homebrew itself currently targets, which trends upward release over release — Cytnx's actual minimum supported macOS version was rising as a side effect of Homebrew's own build policy, not a decision made here. Homebrew's OpenBLAS bottle is also pthreads-only, same issue as Linux.

Fix

Both scripts now provision a self-contained micromamba environment from conda-forge instead:

  • openblas=*=*openmp* selects the OpenMP-threaded OpenBLAS build.
  • arpack=*=nompi* avoids vendoring an MPI runtime (conda-forge's arpack also has mpich/openmpi variants).
  • liblapacke / libboost-headers replace the corresponding EPEL/Homebrew packages.
  • On macOS, llvm-openmp replaces Homebrew's libomp, and the existing dylib-minos-probing logic in cibuildwheel_before_all_macos.sh is kept unchanged, just repointed at the new conda-forge prefix — MACOSX_DEPLOYMENT_TARGET keeps tracking whatever floor the actual vendored dylibs require (conda-forge's clang_osx-64/clang_osx-arm64 toolchain targets a pinned, deliberately old SDK) instead of a value guessed in advance.

A second, small commit pins ci-cmake_tests.yml's own mamba install to libopenblas=*=*openmp* explicitly. That workflow already pins _openmp_mutex=*=*_llvm (so the environment settles on one OpenMP runtime), but blas=*=openblas alone doesn't guarantee the solver picks the OpenMP-threaded build over the pthreads one — both are solver-compatible with _openmp_mutex=llvm present, since a pthreads OpenBLAS doesn't touch OpenMP at all.

Threading verification (per repo convention: measure, don't assume)

Sandbox constraints: no docker registry access (egress policy) and no macOS host, so a real cibuildwheel run wasn't possible locally. What was verified directly, using a real micromamba-created environment with this exact package set and real cmake/g++ on the sandbox host:

  • find_library(arpack), find_package(BLAS BLA_VENDOR OpenBLAS), find_package(LAPACK), find_package(LAPACKE), and find_package(Boost) (forced to MODULE mode, since manylinux has no competing system Boost config) all resolve correctly against the conda-forge prefix via CMAKE_PREFIX_PATH.
  • libblas.so.3/liblapack.so.3/liblapacke.so.3/libopenblas.so.0 are confirmed to all symlink to the same physical libopenblasp-r0.3.33.so — one OpenBLAS copy, not two.
  • conda-forge's openmp-variant OpenBLAS declares NEEDED libgomp.so.1, but the environment's _openmp_mutex (llvm variant, conda-forge's current default for this package combination) makes libgomp.so.1 a symlink to libomp.so (LLVM's runtime). Verified libomp.so genuinely exports the full versioned GOMP_1.0GOMP_5.0.1 symbol set GCC's -fopenmp code needs (nm -D), so GCC-compiled cytnx code and OpenBLAS end up sharing one thread pool rather than two.
  • Compiled and ran a real program linking arpack + LAPACKE + Boost + OpenMP, calling LAPACKE_dgeqrf from inside an 8-way #pragma omp parallel for region — correct output, correct thread count, no crashes, no missing-symbol errors.
  • ci-cmake_tests.yml already pins _openmp_mutex=*=*_llvm for its own mamba-based CPU environment — i.e. this exact OpenMP-runtime combination is already proven in this repo's production CI, not a speculative first use.

The macOS script itself couldn't be executed locally (no Mac available in the sandbox), so it relied on reusing the already-proven Homebrew-era probing logic verbatim, repointed at a new prefix. This PR's own CI caught three real bugs that local verification couldn't have found: the ARM64 micromamba download picking an x86_64 binary, a macOS CMake export-safety error from the deps prefix living inside the checkout, and cibuildwheel's environment table not expanding ${VAR:-default} shell syntax the way a real shell does. All three are fixed (see commit history) and all four BuildWheel legs (ubuntu-24.04, ubuntu-24.04-arm, macos-14, macos-15-intel) are now green, along with every other check on the PR.

Testing

  • pre-commit run on all changed files (end-of-file-fixer, trailing-whitespace, large-file check; no C++ touched).
  • YAML/TOML syntax validated (yaml.safe_load, tomllib.load).
  • Threading/linkage verification described above.
  • This PR's own CI matrix (release_pypi.yml's BuildWheel-* jobs, conda_build.yml's BuildAndTest-* jobs) — all green as of the latest commit.

Generated by Claude Code

IvanaGyro and others added 2 commits July 15, 2026 04:16
…acOS

The Linux wheel build installed boost/openblas/arpack from EPEL via dnf
(manylinux_2_28) and macOS installed them from Homebrew
(cibuildwheel_before_all_macos.sh). Two independent problems followed
from that:

* EPEL's arpack-devel links whichever OpenBLAS dnf resolves, which is
  a pthreads-threaded build with its own thread pool, independent of
  the OpenMP runtime HPTT and cytnx's own OpenMP code use
  (CytnxBKNDCMakeLists.cmake's `-fopenmp`). A BLAS call made from
  inside an OpenMP region oversubscribes the CPU as a result. The
  same applies to Homebrew's OpenBLAS bottle on macOS.
* Homebrew's bottled dylibs (arpack/boost/openblas/libomp) encode
  whatever macOS deployment target Homebrew itself currently targets
  for that formula, which trends upward release over release --
  Cytnx's actual minimum supported macOS version rises as a side
  effect of Homebrew's own build policy rather than a decision made
  here.

Both scripts now provision a self-contained micromamba environment
from conda-forge instead: `openblas=*=*openmp*` selects the
OpenMP-threaded OpenBLAS build (verified directly: its ELF NEEDED
entry is libgomp.so.1, and the "openmp" conda-forge build variant --
via the _openmp_mutex coordination package -- routes that soname
through LLVM's libomp, which was confirmed to export the full
GOMP_1.0-5.0.1 versioned symbol set GCC's -fopenmp code needs, so
cytnx's own OpenMP code and OpenBLAS end up sharing one thread pool
rather than two); `arpack=*=nompi*` avoids vendoring an MPI runtime;
`liblapacke`/`libboost-headers` replace the corresponding EPEL/Homebrew
packages. ci-cmake_tests.yml's own mamba-based CPU test environment
already relies on this same conda-forge OpenMP runtime
(_openmp_mutex=*_llvm), so this is proven, not speculative.

On macOS, conda-forge's clang_osx toolchain targets a pinned,
deliberately old SDK, so its packages carry a controlled, low minos
regardless of the CI runner's own OS version -- the existing
dylib-minos-probing logic in cibuildwheel_before_all_macos.sh is kept
unchanged and simply repointed at the new conda-forge prefix, so
MACOSX_DEPLOYMENT_TARGET keeps tracking whatever floor the actual
vendored dylibs require instead of a value guessed in advance.

Verified directly against this exact package set (CMAKE_PREFIX_PATH
pointed at a real micromamba-created environment, no docker/manylinux
access in this environment): find_library(arpack), find_package(BLAS
BLA_VENDOR OpenBLAS), find_package(LAPACK), find_package(LAPACKE), and
find_package(Boost) (forcing MODULE mode, since the manylinux image has
no competing system Boost config) all resolve correctly; a compiled
program linking arpack+LAPACKE+Boost+OpenMP builds and runs correctly,
including from inside an OpenMP parallel region. The macOS path itself
could not be executed in this environment (no Mac available) and relies
on the Linux-side verification plus reusing the existing,
already-proven dylib-probing mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_openmp_mutex=*=*_llvm was already pinned so the environment settles
on a single OpenMP runtime, but blas=*=openblas on its own doesn't
guarantee the solver picks OpenBLAS's OpenMP-threaded build variant
over its pthreads-threaded one -- both are compatible with an
environment where _openmp_mutex=llvm is present, since a
pthreads-threaded OpenBLAS doesn't use OpenMP at all. Pin
libopenblas=*=*openmp* explicitly, matching the wheel build
(tools/cibuildwheel_before_all.sh): HPTT and cytnx's own code use
OpenMP (-fopenmp), and an OpenBLAS build with its own independent
thread pool oversubscribes the CPU when a BLAS call originates from
inside an OpenMP region.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions the CI build wheel process to use conda-forge (via micromamba) instead of Homebrew (on macOS) and dnf (on Linux manylinux) for installing dependencies like OpenBLAS (with OpenMP support), ARPACK, and Boost headers. This resolves issues with CPU oversubscription due to thread pool mismatches and stabilizes the minimum supported macOS version. The review feedback suggests improving the robustness of the installation scripts by explicitly installing bzip2 to ensure successful decompression of the micromamba archive, and adding the -f / --fail flag to curl commands to prevent silent failures on HTTP errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tools/cibuildwheel_before_all.sh Outdated
Comment thread tools/cibuildwheel_before_all.sh Outdated
Comment thread tools/cibuildwheel_before_all_macos.sh Outdated
IvanaGyro and others added 3 commits July 15, 2026 08:05
Three real bugs surfaced by this PR's own CI matrix:

* Linux: the micromamba download URL hardcoded linux-64 regardless of
  the runner's actual architecture. On ubuntu-24.04-arm this fetched
  an x86_64 binary, which failed with "Exec format error" the moment
  before-all tried to run it. Detect the architecture the same way
  the macOS script already does for osx-64/osx-arm64.
* macOS: the conda-forge deps prefix lived at ${PWD}/.cytnx-deps,
  inside the checkout. CMake refuses to export an
  INTERFACE_INCLUDE_DIRECTORIES entry that lives inside the source
  directory ("which is prefixed in the source directory") -- a hard
  configure-time error, hit here because the cytnx target exports that
  property for downstream find_package(Cytnx) consumers (see
  ci-downstream-find-package.yml). Move the prefix (and micromamba's
  own root) to $TMPDIR, matching how the Linux script already used
  /opt/cytnx-deps, outside the checkout by construction.

Also addresses two Gemini review suggestions: install bzip2 explicitly
on the dnf path (tar -xj needs it to decompress the micromamba
archive, and manylinux images don't guarantee it's present), and add
curl -f to both download commands so an HTTP error page can't get
silently piped into tar instead of failing the step outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI showed ccache (and, by the same mechanism, everything else under
the conda-forge prefix) unreachable on macOS: cibuildwheel's
[tool.cibuildwheel.macos].environment table evaluator does not expand
${VAR:-default}-style parameter substitution the way a real shell
does -- only plain $VAR references and $(command) substitutions are
honored, so CMAKE_PREFIX_PATH/PATH never resolved to the directory
tools/cibuildwheel_before_all_macos.sh actually created.

Write the resolved prefix to .cibw_macos_deps_prefix.txt in before-all
and read it back via $(cat ...), the same pattern
MACOSX_DEPLOYMENT_TARGET already uses successfully on the line right
below it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cibuildwheel evaluates [tool.cibuildwheel.macos].environment as part
of setting up before-all's own subprocess environment, before
before-all has run and created .cibw_macos_deps_prefix.txt at all. A
bare `cat` on that not-yet-existing file exits non-zero and
cibuildwheel treats that as a hard failure of the before-all step
itself, never reaching the script that would have created the file.

Read the file the same way MACOSX_DEPLOYMENT_TARGET already does on
the line below -- a Path.exists() check with a fallback value -- so
the one evaluation that happens before before-all runs degrades to a
placeholder instead of aborting. Every evaluation after before-all has
actually run (the build and test steps) sees the real file and
resolves the real prefix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.49%. Comparing base (1908a05) to head (dc316cb).
⚠️ Report is 3 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1057   +/-   ##
=======================================
  Coverage   60.49%   60.49%           
=======================================
  Files         229      229           
  Lines       31873    31873           
  Branches       71       71           
=======================================
  Hits        19282    19282           
  Misses      12570    12570           
  Partials       21       21           
Flag Coverage Δ
cpp 60.45% <ø> (ø)
python 64.13% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 58.04% <ø> (ø)
Python bindings 75.01% <ø> (ø)
Python package 64.13% <ø> (ø)

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1908a05...dc316cb. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@IvanaGyro
IvanaGyro marked this pull request as ready for review July 15, 2026 09:25
Comment thread tools/cibuildwheel_before_all.sh Outdated
Comment thread pyproject.toml Outdated
IvanaGyro and others added 2 commits July 15, 2026 18:51
Alpine's openblas-dev, like EPEL's on manylinux, is a pthreads-threaded
build, and conda-forge (the fix used for manylinux and macOS) publishes
no musl builds to swap in as an alternative. Build OpenBLAS v0.3.33
from source with USE_OPENMP=1 instead, and replace Alpine's copy with
it in place.

Verified directly on this host: openblas_get_num_threads() on an
OpenMP-built OpenBLAS respects OMP_NUM_THREADS and ignores
OPENBLAS_NUM_THREADS entirely (confirmed empirically -- setting only
OPENBLAS_NUM_THREADS left the thread count at its OMP-derived default;
setting both, OMP_NUM_THREADS won), matching OpenBLAS's own documented
runtime-variable precedence for OpenMP builds.

arpack's own NEEDED entry names the exact soname it was linked against
(Alpine's OpenBLAS ABI name); discovered from the installed
libarpack.so directly via readelf rather than hardcoded, so this keeps
working if Alpine changes that soname. patchelf sets the built
OpenBLAS's soname to match, and the replacement lands at the default
system library paths (/usr/lib), so no CMAKE_PREFIX_PATH override is
needed for musllinux.

Not verified end-to-end in this environment (no musllinux/docker
access in this sandbox) -- relying on this PR's own musllinux
BuildWheel CI legs for that signal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The deps prefix only needed to live outside the checkout (see the
prior commit fixing the same requirement), not to vary at all --
deriving it from $TMPDIR needed a file-based round-trip workaround
(cibuildwheel's environment-table evaluator doesn't expand
${VAR:-default} parameter substitution) purely to communicate a value
that could just as well be a fixed literal. Use /tmp/cytnx-deps
directly: pyproject.toml's CMAKE_PREFIX_PATH/PATH reference it as a
plain string, no discovery step needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d4fed9e60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pyproject.toml
Comment thread .github/workflows/ci-cmake_tests.yml Outdated
# merely compatible with, but not actually using, that runtime.
run: |
mamba install _openmp_mutex=*=*_llvm cmake make boost git compilers blas=*=openblas liblapacke arpack gtest graphviz
mamba install _openmp_mutex=*=*_llvm cmake make boost git compilers "blas=*=openblas" "libopenblas=*=*openmp*" liblapacke arpack gtest graphviz

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still need _openmp_mutex==_llvm here and .github/workflows/ci-downstream-find-package.yml?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, still needed in both, and it's not redundant with libopenblas=*=*openmp* — they pin two different things:

  • libopenblas=*=*openmp* picks the OpenMP-threaded OpenBLAS build variant over the pthreads one.
  • _openmp_mutex=*=*_llvm picks which concrete OpenMP runtime (llvm's libomp vs. GNU's libgomp) that OpenBLAS build and the -fopenmp-compiled cytnx/HPTT code both resolve to. Without it the solver could still land on _openmp_mutex=*_gnu, and the two OpenMP consumers wouldn't necessarily share one physical runtime even though both are individually "openmp-based" (see the libgomp.so.1libomp.so symlink mechanism discussed in the P2 review thread above).

ci-downstream-find-package.yml already had _openmp_mutex=*=*_llvm but was missing the libopenblas=*=*openmp* pin ci-cmake_tests.yml has — added it in a0ffe55 for consistency (its own TODO(deps-single-source) comment already flags this dependency list as a near-duplicate of ci-cmake_tests.yml's).


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use libgomp so we can remove _openmp_mutex==_llvm or pick gomp one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the pin should go — I removed _openmp_mutex=*=*_llvm from both ci-cmake_tests.yml and ci-downstream-find-package.yml in dc316cb. But I need to correct my previous reply here: it was wrong on two counts, and I verified the actual solver behavior with a micromamba create --dry-run this time rather than reasoning about it.

  1. "pick gomp one" isn't possible. conda-forge's OpenMP OpenBLAS build hard-requires the llvm mutex — _openmp_mutex=*=*_gnu together with libopenblas=*=*openmp* fails to solve outright:
    libopenblas =* *openmp* is not installable because it requires
      _openmp_mutex =* *_llvm, which conflicts...
    
  2. The explicit _llvm pin was redundant, not load-bearing (contra what I said before). Resolving the ci-cmake_tests.yml spec with the pin removed still pulls _openmp_mutex 4.5 7_kmp_llvm + llvm-openmp transitively from libopenblas=*=*openmp* alone. So dropping it changes nothing about which runtime is selected.

On "we use libgomp": libgomp.so.1 is present, but under _kmp_llvm it's a symlink to libomp.so, so the physical runtime everything (GCC's -fopenmp -lgomp and OpenBLAS) resolves to is LLVM's libomp — which is exactly what makes the single-thread-pool / no-oversubscription property hold. Comments in both files now record this instead of the old (incorrect) "the pin picks the runtime" rationale.


Generated by Claude Code

ci-cmake_tests.yml already pins libopenblas=*=*openmp* alongside
_openmp_mutex=*_llvm so the solver can't substitute a pthreads-threaded
OpenBLAS build. ci-downstream-find-package.yml's own native-dependency
install (a near-duplicate of ci-cmake_tests.yml's, per its own
deps-single-source TODO comment) only had the _openmp_mutex pin, so it
could resolve a different OpenBLAS threading backend than the test
suite despite pinning the same OpenMP runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0ffe55388

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/cibuildwheel_before_all_macos.sh
…scan

tools/cibuildwheel_before_all_macos.sh's minos probe walks every dylib
under the conda-forge prefix to compute MACOSX_DEPLOYMENT_TARGET.
ccache was installed into that same prefix, so any of its own
transitive dependencies -- never linked into or vendored with the
wheel -- could inflate max_minos above what the actual runtime
dependencies (openblas/arpack/boost/llvm-openmp) require, needlessly
narrowing the wheel's supported macOS range.

Moves ccache into a separate /tmp/cytnx-build-tools prefix so the scan
only ever sees the wheel's real runtime dependency closure, and points
pyproject.toml's PATH override at the new prefix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
conda-forge's openmp OpenBLAS build (libopenblas=*=*openmp*) hard-
requires _openmp_mutex=*_llvm: an explicit _gnu pin is unsolvable
against it, and the solver already pulls _openmp_mutex 7_kmp_llvm plus
llvm-openmp transitively from libopenblas=*=*openmp* alone (verified by
dry-run resolving the ci-cmake_tests.yml spec with the explicit pin
removed). The separate _openmp_mutex=*_llvm spec was therefore
redundant in both ci-cmake_tests.yml and ci-downstream-find-package.yml.

Remove it and rewrite the comments to record that libopenblas=*=*openmp*
is what forces the single llvm OpenMP runtime, and that libgomp is
present only as a _kmp_llvm-provided symlink to libomp (so GCC's
-fopenmp -lgomp and OpenBLAS still resolve to that one runtime) rather
than an independent GNU runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Member Author

Heads-up on an interaction with #1060 (enable ASan in ci-cmake_tests), found while profiling #1058.

This PR pins ci-cmake_tests to libopenblas=*=*openmp*. That OpenBLAS build has NEEDED libgomp.so.1 + RPATH $ORIGIN, and in the _openmp_mutex=llvm env libgomp.so.1 is a symlink to libomp.so (LLVM). So the OpenMP OpenBLAS pulls LLVM libomp into every test process via its own RPATH — independent of LD_LIBRARY_PATH.

LLVM libomp makes a one-time internal allocation at its first parallel region (__kmp_allocate_align, 112 bytes) that it never frees. Under LeakSanitizer that's reported as a direct leak, and the process exits non-zero. #1060 turns on ASan and deliberately keeps LeakSanitizer ON for the gtest/ctest step (only the Python-hosted steps get detect_leaks=0). So once both this PR and #1060 are in, the ctest step will fail on a libomp leak in ~every test — a false positive unrelated to Cytnx.

Evidence (local, debug-openblas-cpu + ASan):

Whichever of the two merges second trips this. I've pushed the openmp pin + OMP_WAIT_POLICY=passive to #1060's branch to see it under real CI. If the ctest step reddens on the libomp leak, the fix is env-only and keeps genuine leak detection on — no blanket detect_leaks=0: preload the system GCC libgomp so it wins the libgomp.so.1 soname before OpenBLAS's RPATH can pull libomp, ASan runtime first to satisfy its ordering check, e.g. LD_PRELOAD="$(gcc -print-file-name=libasan.so) /usr/lib/x86_64-linux-gnu/libgomp.so.1" on the gtest step. Verified locally: leak gone, test passes, real-leak detection preserved. (An LSan suppression leak:__kmp_allocate also works but disables detection of any real leak in that call path.)

Reported by Claude (agent session operated by @IvanaGyro).


Generated by Claude Code

@ianmccul

Copy link
Copy Markdown
Collaborator

Don't preload GNU libgomp. Using LSan suppression leak:__kmp_allocate is fine here, Cytnx never calls this function, and we're only interested in Cytnx leaks, not allocations in support libraries. omp_alloc() seems to use a different allocation path, and does not look like it goes via __kmp_allocate. It would nevertheless be worth examining the complete allocation stack. There may be a caller above __kmp_allocate that identifies this particular one-time startup allocation more precisely and would make a better suppression target.

Note that one-off allocations are not normally regarded as leaks as such anyway. For example, the dynamic dispatch tables used by Cytnx, I am slightly surprised that these are freed at program termination -- I guess they use some global std::vector or some similar mechanism. But if they were just created with operator new[], or even malloc, and never freed, that would be OK, the memory isn't really a leak because it never accumulates. In fact it is slightly more efficient because it doesn't call back into the memory manager at program exit - there is no point freeing the memory on exit because its all about to be returned to the operating system.

“Every allocation must be explicitly freed so LSan passes without suppressions” forbids a legitimate process-lifetime allocation strategy. The better policy is:

  • “LSan must pass with documented suppressions”

@IvanaGyro

IvanaGyro commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

I didn't dig into it. The CI test didn't report LSan errors. Somehow the test workflow links with libgomp instead of libomp from conda-forge, so there is no issue there. However, the message is still good for develops to solve the problem.

@yingjerkao yingjerkao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

3 participants