Fix #423: link MKL statically into _scs_mkl; OpenBLAS default backends; clean-container wheel gate - #232
Merged
Merged
Conversation
…er wheel test The 3.3.0 manylinux x86_64 wheels shipped MKL without its CPU dispatch kernels (libmkl_def/avx2/avx512.so.3). libmkl_core dlopens these at runtime, so they are invisible to auditwheel, which only vendors DT_NEEDED libraries. On any machine without MKL already on the loader path the first solve aborted the interpreter with 'Intel oneMKL FATAL ERROR' and exit(2). All backends were affected because every extension, including _scs_direct, linked MKL BLAS/LAPACK. Fixes #423 (cvxgrp/scs). The bug survived CI because cibuildwheel's test phase ran inside the build container, whose LD_LIBRARY_PATH pointed at the container's system oneAPI install: the dlopen always succeeded there regardless of what the wheel vendored. This reverts the wheel linkage to OpenBLAS, the configuration proven through 3.2.11 (macOS/aarch64/musllinux wheels were always OpenBLAS or Accelerate and are unaffected). Windows wheels, MKL-linked since the same change and repaired by delvewheel, which is equally blind to dlopen dependencies, revert likewise. MKL remains available in source builds (-Dlink_mkl=true, e.g. conda). Adds a wheel_smoke_clean_env CI job that installs the built manylinux x86_64 wheel in a pristine python:3.12-slim container (no BLAS, no LD_LIBRARY_PATH) and solves an LP+PSD problem on every shipped backend, so a vendoring gap of this class can never pass CI again.
bodono
added a commit
that referenced
this pull request
Aug 31, 2026
Aligns the AUTO-preference note with the wheel linkage revert (#232): the pre-built wheels no longer ship MKL, so AUTO resolves to MKL only where the environment provides it (e.g. conda source builds).
Rather than vendoring MKL (whose dlopen'd CPU dispatch kernels are invisible to auditwheel -- the root cause of #423) or dropping it, the wheels now build _scs_mkl linked against MKL without vendoring it, and Intel's official 'mkl' + 'intel-openmp' PyPI wheels supply the complete, internally-consistent runtime via the scs[mkl] extra. - meson: new mkl_backend option builds only _scs_mkl against MKL while every other extension links the platform BLAS (OpenBLAS in wheels). link_mkl keeps its all-extensions-on-MKL meaning for conda/source. - scs/py: when importing _scs_mkl fails, preload MKL from the installed mkl/intel-openmp distributions (RTLD_LOCAL, dependency order) and retry; without them the ImportError propagates and AUTO falls back to QDLDL. RTLD_LOCAL avoids interposing MKL onto e.g. NumPy's OpenBLAS. - cibuildwheel: oneAPI is installed for building only; no LD_LIBRARY_PATH in the container environment (the #423 blind spot), the oneAPI paths are scoped to the auditwheel repair command, which excludes libmkl_*/libiomp5 from grafting. - CI: the clean-container smoke test becomes a default/mkl matrix; the mkl variant installs scs[mkl] and requires AUTO to resolve to MKL, also enforcing that the wheel's oneMKL generation matches the pinned mkl PyPI range (mkl>=2026,<2027, the .so.3 ABI). upload_pypi remains gated on both variants. Windows wheels stay OpenBLAS-only for now: Windows MKL was sequential anyway (broken iomp pkg-config), and the conda/pip generation-matching story there needs separate validation.
Without /opt/intel/oneapi/compiler/latest/lib/pkgconfig on PKG_CONFIG_PATH the mkl-dynamic-lp64-iomp dependency fails to resolve and meson silently falls back to sequential MKL; test_mkl_module_links_intel_openmp caught the downgrade. Note this env var carries no LD_LIBRARY_PATH: the mkl tests still run in-container only via the extension's baked RUNPATH to the oneAPI install, and the clean-container smoke job remains the authority on what the wheel actually ships.
…stics The extensions share the PyModuleDef name "_scs" (single-phase init), so __name__ cannot distinguish them. Also surface the pip resolution and the exact ImportError when the mkl variant fails to load.
…oads MKL's component libraries reference each other and cannot be eagerly bound one at a time; ctypes.CDLL always forces RTLD_NOW, so the preload loop silently loaded nothing and _scs_mkl still failed to import (diagnosed in CI: all libraries present in <prefix>/lib, every CDLL swallowed an OSError). Primary fix: after auditwheel repair, patchelf an '$ORIGIN/../../../../lib' RUNPATH onto _scs_mkl (site-packages/scs is four levels below the prefix in every standard layout), so the dynamic loader resolves the whole MKL group in one dlopen with correct mutual binding and no symbol-scope widening. auditwheel preserves existing RUNPATHs (observed in CI), so patching after repair is stable. The Python shim remains as a fallback for non-standard layouts, now calling dlopen(3) directly with RTLD_LAZY | RTLD_LOCAL.
Per review direction: MKL is opt-in but should be loud — for most users on x86-64 Linux it is strictly better than the built-in solver, so the install section now leads with a GitHub IMPORTANT callout for pip install "scs[mkl]", and the backends section defers to it.
bodono
added a commit
that referenced
this pull request
Aug 31, 2026
…e clean gate - add_mkl_rpath.sh uses patchelf --set-rpath: the meson build bakes the container's /opt/intel paths into _scs_mkl and auditwheel preserves them; shipping those would let a build-machine-layout MKL install shadow the scs[mkl] runtime and let container tests silently resolve the build MKL. The script now also asserts the exact final RUNPATH and the exact MKL/iomp NEEDED set. - New wheel_audit job: every .so in every Linux wheel (all pythons, x86_64/aarch64/musllinux) must carry only $ORIGIN-relative RPATH entries, and every _scs_mkl must have the exact expected MKL NEEDED set. Gates upload_pypi. - Clean-container smoke matrix now covers oldest (3.9), newest stable (3.14) and free-threaded (3.14t via uv) wheels, both with and without scs[mkl]; remaining ABIs are covered by the audit. - README: correct scs[mkl] footprint (~300 MB download / ~1 GB on disk), document exactly which install modes the extra supports (manylinux x86-64 glibc 2.28+, standard prefixes; not musllinux, pip --target, or sdist builds), and stop implying Windows wheels contain an MKL backend.
…e clean gate - add_mkl_rpath.sh uses patchelf --set-rpath: the meson build bakes the container's /opt/intel paths into _scs_mkl and auditwheel preserves them; shipping those would let a build-machine-layout MKL install shadow the scs[mkl] runtime and let container tests silently resolve the build MKL. The script now also asserts the exact final RUNPATH and the exact MKL/iomp NEEDED set. - New wheel_audit job: every .so in every Linux wheel (all pythons, x86_64/aarch64/musllinux) must carry only $ORIGIN-relative RPATH entries, and every _scs_mkl must have the exact expected MKL NEEDED set. Gates upload_pypi. - Clean-container smoke matrix now covers oldest (3.9), newest stable (3.14) and free-threaded (3.14t via uv) wheels, both with and without scs[mkl]; remaining ABIs are covered by the audit. - README: correct scs[mkl] footprint (~300 MB download / ~1 GB on disk), document exactly which install modes the extra supports (manylinux x86-64 glibc 2.28+, standard prefixes; not musllinux, pip --target, or sdist builds), and stop implying Windows wheels contain an MKL backend.
bodono
force-pushed
the
fix/wheels-openblas-423
branch
from
August 31, 2026 17:23
897cfdb to
4c6f456
Compare
Per review, the audit was fail-open on three axes: a wheel set with no _scs_mkl at all passed vacuously, readelf failures were silently ignored, and nothing pinned per-platform content. Now: - exact extension inventory per platform (x86_64: direct/indirect/mkl; aarch64+musllinux: direct/indirect, never mkl), - the _scs_mkl count must equal the number of x86_64 manylinux wheels, - readelf exit status is checked; a failure is a finding, - every wheel must vendor OpenBLAS and must never vendor MKL/iomp, - _scs_mkl's RUNPATH must be exactly ['$ORIGIN/../../../../lib'].
…whole wheel - _scs_dense ships in every Linux wheel and was missing from the expected inventories, failing all 27 correct wheels. - Every platform group must be present in the downloaded artifacts and the expected _scs_mkl count must be positive, so an absent artifact group can never make the MKL checks vacuous. - Intel libraries are now rejected anywhere in the wheel, not only under .libs paths. - DT_RPATH and DT_RUNPATH are tracked separately: legacy DT_RPATH is forbidden outright, and _scs_mkl must carry exactly one DT_RUNPATH equal to $ORIGIN/../../../../lib.
auditwheel deliberately writes DT_RPATH on every object it grafts (RPATH outranks LD_LIBRARY_PATH, isolating vendored libraries), so the blanket prohibition failed all 27 correct wheels. The general rule is $ORIGIN-relative entries on either tag type; _scs_mkl -- the object this repo patches itself -- keeps the strict shape of exactly one DT_RUNPATH and no DT_RPATH.
Deriving expected counts from downloaded wheels let one surviving
wheel per platform stand in for the full set. Each platform group must
now carry exactly {cp39..cp315, cp314t, cp315t}; the ABI tag (field 4)
distinguishes free-threaded builds.
bodono
added a commit
that referenced
this pull request
Aug 31, 2026
Aligns the AUTO-preference note with the wheel linkage revert (#232): the pre-built wheels no longer ship MKL, so AUTO resolves to MKL only where the environment provides it (e.g. conda source builds).
…extra The 3.3.0 wheels vendored dynamic MKL and shipped it incomplete: its CPU dispatch kernels are loaded via dlopen, invisible to auditwheel, so every solve aborted on machines without MKL on the loader path (cvxgrp/scs#423). Unvendoring (the previous shape of this PR) fixed that at the cost of a runtime extra, a RUNPATH patch, a dlopen shim and a 276 MB download. Linking Intel's mkl-static archives into _scs_mkl instead makes the wheel self-contained: static MKL dispatches CPU variants internally, so there is nothing to lose; the LP64 interface is fixed at link time, so there is nothing to negotiate (the core's MKL_Set_Interface_Layer reference is now weak); every MKL symbol is hidden, so a process-wide MKL is never interposed; and the sequential threading layer ships no OpenMP runtime that could abort a process already holding one. Measured on the scratch branch: 108 MB on disk, 30 MB compressed, the full core test suite passing in a clean environment (58/58 wheel-default, 66/66 with spectral cones), versus 52 MB for the broken 3.3.0 wheel. - meson: mkl_static_prefix replaces mkl_backend; _scs_mkl links the intel_lp64/sequential/core archives as a group with --exclude-libs,ALL and installs Intel's license notice alongside (ISSL requires it). - cibuildwheel: install_mkl_static.sh lays the archives out under /opt/mkl-static from Intel's PyPI wheels; default auditwheel repair; nothing in the environment points the loader at build-machine paths. - Deleted: the scs[mkl] extra, add_mkl_rpath.sh, the dlopen shim in scs/py/__init__.py, and the ldd linkage test. - wheel_audit keeps the invariants that matter for every Linux wheel: no dynamic MKL dependency in any extension, no Intel shared library in the wheel, OpenBLAS vendored, no absolute rpath entry. - wheel_smoke_clean_env, the release gate: one variant, pristine container, LD_LIBRARY_PATH asserted unset, every backend solved (qdldl, cpu_indirect, dense, mkl, auto) on a problem with a PSD cone; on x86-64 the static MKL backend must import and be AUTO's choice. - scs_source pinned to the core commit carrying the weak reference. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bodono
added a commit
to cvxgrp/scs
that referenced
this pull request
Sep 3, 2026
The wheels now link MKL statically into _scs_mkl (bodono/scs-python#232), so the scs[mkl] extra no longer exists and there is nothing to recommend installing: AUTO selects MKL out of the box. Explain why MKL is linked statically rather than bundled as shared libraries (#423), why the backend is single-threaded, and where Intel's license notice lives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The core's MKL_Set_Interface_Layer reference is weak so that static MKL links without mkl_rt. In the dynamic link_mkl build that made mkl_rt referenced only weakly, --as-needed dropped it from NEEDED, the symbol resolved to NULL and the interface-layer guard silently no longer ran (the ILP64-guard CI lane caught it). Link with --no-as-needed on Linux, as the core's own CMake and Makefile builds already do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bodono
added a commit
to cvxgrp/scs
that referenced
this pull request
Sep 3, 2026
The wheels now link MKL statically into _scs_mkl (bodono/scs-python#232), so the scs[mkl] extra no longer exists and there is nothing to recommend installing: AUTO selects MKL out of the box. Explain why MKL is linked statically rather than bundled as shared libraries (#423), why the backend is single-threaded, and where Intel's license notice lives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bodono
force-pushed
the
fix/wheels-openblas-423
branch
from
September 3, 2026 21:07
788aae7 to
1442623
Compare
- link_mkl builds linked _base_deps + mkl_rt and so dropped the rt that every other extension gets through _deps; use _deps. - The static branch's dl/m/rt dependencies are one required line; the install script loses knobs and fallbacks the container never takes. - The #423 rationale lives in pyproject.toml; meson, the workflow and the README point there instead of retelling it. - wheel_audit keeps the three invariants that matter (no Intel shared library, no dynamic MKL dependency, no absolute rpath entry). - AUTO is resolved once per process. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bodono
force-pushed
the
fix/wheels-openblas-423
branch
from
September 3, 2026 21:17
1442623 to
8e12289
Compare
…rce-MKL lanes The audit only forbade things (dynamic MKL, Intel libraries, absolute rpaths); it now also requires what a wheel must contain: the standard extensions and a vendored OpenBLAS in every Linux wheel, _scs_mkl in exactly the x86-64 manylinux wheels, and at least one such wheel, so a selector or build change cannot drop the MKL backend unnoticed. upload_pypi also waits for build_mkl, since the sdist carries the link_mkl source build. README: MKL ships in the x86-64 manylinux wheels specifically (musllinux has no MKL). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
conda-forge's win-64 libopenblas 0.3.34 (pthreads_h877e47f_1) crashes with an access violation inside dgemm_kernel_HASWELL / dgemm_kernel_ZEN on AMD Zen 4 and Zen 5 CPUs whose hypervisor or firmware exposes AVX-512, on plain dsyrk/dpotrf calls; 0.3.33 and earlier builds are fine on the same machines (conda-forge/openblas-feedstock#196). The wheel CI hit it on the runners that happen to have such CPUs, and every Ryzen 7000/9000 desktop would have. Pin until a fixed build ships. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 3.3.0 manylinux x86-64 wheels vendored dynamic MKL and shipped it incomplete: MKL loads its CPU dispatch kernels via dlopen, which auditwheel cannot see, so every solve aborted on machines without MKL on the loader path (#423). Every extension linked MKL, so the default backend died too.
This PR:
_scs_direct,_scs_indirect,_scs_dense), so no MKL problem can ever take down the default solver again._scs_mkllinks MKL statically from Intel'smkl-staticPyPI archives (LP64, sequential, symbols hidden). Static MKL dispatches CPU variants internally, so there are no kernels to lose; the interface is fixed at link time, so there is nothing to negotiate at runtime (the core'sMKL_Set_Interface_Layerreference is weak, MKL: accept GNU-convention interface layers (compare width bit only) cvxgrp/scs#422); and the sequential threading layer ships no OpenMP runtime that could abort a process already holding one. The wheel is self-contained: about 42 MB, versus 52 MB for the broken 3.3.0 wheel.AUTOselects MKL Pardiso out of the box in the x86-64 manylinux wheels (musllinux has no MKL).wheel_smoke_clean_envinstalls each wheel in a pristine container with nothing on the loader path (asserted) and solves with every backend on a problem with a PSD cone, on Python 3.9, 3.14 and 3.14t;wheel_auditchecks every Linux wheel for the shipped inventory (the standard extensions and a vendored OpenBLAS everywhere,_scs_mklin exactly the x86-64 manylinux wheels, and at least one such wheel), a dynamic MKL dependency, an Intel shared library, or an absolute rpath entry.upload_pypidepends on both and onbuild_mkl, since the sdist carries thelink_mklsource build.LICENSE-INTEL-MKL.txt.Validated before landing on a scratch branch: the full core test suite (58 tests wheel-default, 66 with spectral cones, PSD cones included) against the static library in a clean environment.
🤖 Generated with Claude Code