Skip to content

Commit f6b7dc6

Browse files
authored
Add MKL BLAS64 support and multi-threaded PARDISO in wheels (#192)
* Add MKL BLAS64 support and multi-threaded PARDISO in wheels Previously the MKL backend only supported LP64 (32-bit BLAS integers) and shipped sequential MKL in pip wheels, giving single-threaded PARDISO. This adds ILP64 support and ships multi-threaded MKL. meson.build: - Select LP64 or ILP64 MKL component libraries based on use_blas64 - Prefer iomp (Intel OpenMP) variant for multi-threaded MKL/PARDISO, fall back to sequential if iomp pkg-config is unavailable - Windows uses sequential MKL due to a broken iomp .pc file in conda - Add mkl_rt to MKL backend dependencies so MKL_Set_Interface_Layer() is available at runtime (symbol not exported by component libraries) pyproject.toml: - Add compiler pkgconfig/lib paths to cibuildwheel environment so mkl-dynamic-lp64-iomp is discoverable (its .pc file lives under the compiler runtime, not the MKL directory) - Result: Linux x86_64 wheels now bundle libiomp5 + libmkl_intel_thread instead of libmkl_sequential, enabling multi-threaded PARDISO - Install intel-openmp on Windows for future iomp support build.yml: - Add use_blas64 matrix dimension to build_mkl CI job - Simplify build step to always pass link_mkl, conditionally add use_blas64 Depends on cvxgrp/scs#383 (updates scs_source submodule). * Fail fast on unsupported MKL configurations * Update scs_source MKL initialization order * Stop enforcing MKL threading layer at runtime * Document MKL wheel and BLAS64 contracts * Update scs_source for CMake BLAS64 fix * Fix pixi MKL dependency set in CI * Update scs_source for expanded MKL CMake matrix * Update scs_source for clearer MKL init error * Update scs_source for MKL mismatch CI * Update scs_source for verbose MKL mismatch test * Update scs_source for mismatch test ABI fix * Update scs_source to merged SCS master * Simplify Meson mkl_rt lookup
1 parent e7ec42f commit f6b7dc6

6 files changed

Lines changed: 91 additions & 16 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ jobs:
5858
os: [ ubuntu-latest ]
5959
python-version: [ 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]
6060
link_mkl: [true]
61+
use_blas64: [true, false]
6162

6263
env:
6364
PYTHON_VERSION: ${{ matrix.python-version }}
6465
LINK_MKL: ${{ matrix.link_mkl }}
66+
USE_BLAS64: ${{ matrix.use_blas64 }}
6567

6668
steps:
6769
- uses: actions/checkout@v6
@@ -90,12 +92,11 @@ jobs:
9092
- name: Build
9193
run: |
9294
pixi r python -c "import numpy as np;print(np.show_config())"
93-
if [[ "$LINK_MKL" == "true" ]]; then
94-
95-
pixi r python -m pip install --verbose --no-build-isolation -Csetup-args=-Dlink_mkl=true .
96-
else
97-
pixi r python -m pip install --verbose --no-build-isolation .
95+
MESON_ARGS="-Csetup-args=-Dlink_mkl=true"
96+
if [[ "$USE_BLAS64" == "true" ]]; then
97+
MESON_ARGS="$MESON_ARGS -Csetup-args=-Duse_blas64=true"
9898
fi
99+
pixi r python -m pip install --verbose --no-build-isolation $MESON_ARGS .
99100
- name: Test
100101
run: |
101102
pixi r pytest
@@ -198,7 +199,7 @@ jobs:
198199

199200
- name: Install MKL from conda on Windows
200201
if: runner.os == 'Windows'
201-
run: conda install -y -c https://software.repos.intel.com/python/conda/ -c conda-forge mkl mkl-devel pkgconfig
202+
run: conda install -y -c https://software.repos.intel.com/python/conda/ -c conda-forge mkl mkl-devel intel-openmp pkgconfig
202203

203204
- name: Build wheels
204205
uses: pypa/cibuildwheel@v3.4.1

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ backends can be enabled with build-time flags:
4848
# MKL Pardiso direct solver
4949
pip install . -Csetup-args=-Dlink_mkl=true
5050

51+
# Use 64-bit BLAS/LAPACK integers (ILP64 / BLAS64)
52+
pip install . -Csetup-args=-Duse_blas64=true
53+
5154
# GPU direct solver (cuDSS)
5255
pip install . -Csetup-args=-Dlink_cudss=true -Csetup-args=-Dint32=true
5356

@@ -58,6 +61,11 @@ pip install . -Csetup-args=-Duse_lapack=true
5861
pip install . -Csetup-args=-Duse_spectral_cones=true
5962
```
6063

64+
Notes:
65+
- Linux x86_64 wheels are built and tested against threaded MKL, and CI asserts a `libiomp5` dependency on the packaged `_scs_mkl` extension. Windows currently falls back to sequential MKL because Intel's conda `pkg-config` metadata for the threaded variant is still broken.
66+
- `BLAS64` is a general SCS build mode for ILP64 BLAS/LAPACK libraries, not an MKL-only feature.
67+
- For the MKL Pardiso backend specifically, `BLAS64` must be paired with 64-bit SCS integers (`DLONG` / `int32=false`), and SCS now fails early if another library in the process has already fixed MKL to an incompatible LP64/ILP64 interface layer.
68+
6169
## Usage
6270

6371
```python

meson.build

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,55 @@ print(incdir)
2222

2323
# Get BLAS
2424
blas_deps = []
25+
mkl_pkg_name = ''
2526
if get_option('link_mkl')
26-
# Prefer component libraries (lp64, sequential) so that wheel-repair
27-
# tools (auditwheel / delvewheel) can detect and bundle them.
28-
blas_deps = [dependency('mkl-dynamic-lp64-seq', required : false)]
27+
if get_option('use_blas64') and get_option('int32')
28+
error('MKL BLAS64 requires 64-bit SCS integers. Re-run Meson with -Dint32=false.')
29+
endif
30+
# Link against MKL component libraries. The integer width must match:
31+
# use_blas64=false (default) -> LP64 (32-bit BLAS integers)
32+
# use_blas64=true -> ILP64 (64-bit BLAS integers)
33+
#
34+
# Note: PARDISO integer width is independent — it is controlled by the
35+
# pardiso vs pardiso_64 entry point (selected via DLONG), not by the
36+
# interface layer. See linsys/mkl/direct/private.c for details.
37+
# Prefer Intel OpenMP (iomp) for multi-threaded MKL, fall back to
38+
# sequential if iomp is not available.
39+
#
40+
# TODO: On Windows the conda MKL iomp .pc file (mkl-dynamic-*-iomp)
41+
# has broken Cflags (raw path without -I prefix). Revisit once Intel
42+
# ships a fixed pkg-config file; until then Windows gets sequential MKL.
43+
if get_option('use_blas64')
44+
_mkl_iomp = 'mkl-dynamic-ilp64-iomp'
45+
_mkl_seq = 'mkl-dynamic-ilp64-seq'
46+
else
47+
_mkl_iomp = 'mkl-dynamic-lp64-iomp'
48+
_mkl_seq = 'mkl-dynamic-lp64-seq'
49+
endif
50+
if host_machine.system() == 'windows'
51+
# On Windows the conda MKL iomp .pc file has broken Cflags (raw path
52+
# without -I prefix). Use sequential MKL until Intel ships a fix.
53+
blas_deps = [dependency(_mkl_seq, required : false)]
54+
if blas_deps[0].found()
55+
mkl_pkg_name = _mkl_seq
56+
endif
57+
else
58+
blas_deps = [dependency(_mkl_iomp, required : false)]
59+
if not blas_deps[0].found()
60+
blas_deps = [dependency(_mkl_seq, required : false)]
61+
if blas_deps[0].found()
62+
mkl_pkg_name = _mkl_seq
63+
endif
64+
else
65+
mkl_pkg_name = _mkl_iomp
66+
endif
67+
endif
2968
if not blas_deps[0].found()
3069
blas_deps = [cc.find_library('mkl_rt', required : false)]
3170
endif
3271
if not blas_deps[0].found()
3372
blas_deps = [dependency('mkl-sdl', required : false)]
3473
endif
35-
if not blas_deps[0].found()
36-
blas_deps = [ dependency('mkl-dynamic-ilp64-iomp', required: false) ]
37-
endif
3874
else
3975
if host_machine.system() == 'darwin'
4076
blas_deps = [dependency('Accelerate')]
@@ -276,16 +312,35 @@ if get_option('use_gpu')
276312
endif
277313

278314
if get_option('link_mkl')
315+
# The MKL backend calls MKL_Set_Interface_Layer() for a runtime interface
316+
# check. This symbol is only exported by mkl_rt (the single dynamic library),
317+
# not by component libraries (mkl_intel_lp64 etc.) that pkg-config may link.
318+
# Add mkl_rt explicitly so the symbol is always available.
319+
mkl_rt_dep = dependency('mkl-sdl', required : false)
320+
if not mkl_rt_dep.found()
321+
mkl_rt_dep = cc.find_library('mkl_rt', required : false)
322+
endif
323+
if not mkl_rt_dep.found() and mkl_pkg_name != ''
324+
# Search next to the pkg-config-provided MKL component libraries.
325+
_mkl_libdir = blas_deps[0].get_variable(pkgconfig: 'libdir', default_value: '')
326+
if _mkl_libdir != ''
327+
mkl_rt_dep = cc.find_library('mkl_rt', dirs: [_mkl_libdir], required : false)
328+
endif
329+
endif
330+
if not mkl_rt_dep.found()
331+
error('link_mkl=true requires mkl_rt (or mkl-sdl). _scs_mkl calls MKL_Set_Interface_Layer() at startup and cannot be built safely without it.')
332+
endif
333+
_mkl_deps = _deps + [mkl_rt_dep]
279334
py.extension_module(
280335
'_scs_mkl',
281336
'scs/scspy.c',
282337
'scs_source/linsys/mkl/direct/private.c',
283338
common_linsys_sources,
284339
scs_core_sources,
285340
spectral_cone_sources,
286-
c_args: common_c_args + ['-DPY_MKL'],
341+
c_args: common_c_args + ['-DPY_MKL', '-DSCS_MKL=1'],
287342
include_directories: common_includes + ['scs_source/linsys/mkl/direct'],
288-
dependencies: _deps,
343+
dependencies: _mkl_deps,
289344
install_dir: scs_dir,
290345
install: true,
291346
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ before-all = [
7575
"rpm --import https://yum.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB",
7676
"dnf install -y intel-oneapi-mkl-devel",
7777
]
78-
environment = {PKG_CONFIG_PATH = "/opt/intel/oneapi/mkl/latest/lib/pkgconfig:$PKG_CONFIG_PATH", LD_LIBRARY_PATH = "/opt/intel/oneapi/mkl/latest/lib/intel64:/opt/intel/oneapi/mkl/latest/lib:$LD_LIBRARY_PATH"}
78+
environment = {PKG_CONFIG_PATH = "/opt/intel/oneapi/mkl/latest/lib/pkgconfig:/opt/intel/oneapi/compiler/latest/lib/pkgconfig:$PKG_CONFIG_PATH", LD_LIBRARY_PATH = "/opt/intel/oneapi/mkl/latest/lib/intel64:/opt/intel/oneapi/mkl/latest/lib:/opt/intel/oneapi/compiler/latest/lib:$LD_LIBRARY_PATH"}
7979
config-settings = {setup-args = "-Dlink_mkl=true"}
8080

8181
[[tool.cibuildwheel.overrides]]

test/test_solve_random_cone_prob_mkl.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from __future__ import print_function, division
2+
import os
3+
import subprocess
24
import sys
35
import platform
46
import scs
@@ -41,6 +43,15 @@
4143
params = {"verbose": True, "eps_abs": 1e-7, "eps_rel": 1e-7, "eps_infeas": 1e-7}
4244

4345

46+
@pytest.mark.skipif(
47+
not (sys.platform == "linux" and platform.machine() == "x86_64"),
48+
reason="Threaded MKL linkage is only checked on Linux x86_64",
49+
)
50+
def test_mkl_module_links_intel_openmp():
51+
out = subprocess.check_output(["ldd", os.fspath(_scs_mkl.__file__)], text=True)
52+
assert "libiomp5" in out
53+
54+
4455
def test_solve_feasible():
4556
data, p_star = tools.gen_feasible(K, n=m // 3, density=0.1)
4657
solver = scs.SCS(data, K, linear_solver=scs.LinearSolver.MKL, **params)

0 commit comments

Comments
 (0)