Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ __pycache__/

# environment
.pyenv
.venv*/

# Local wheel artifacts
*.whl

# Distribution / packaging
.Python
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,21 @@ pip install spams
Make sure you have install libblas & liblapack (see above)
```bash
git clone https://github.com/getspams/spams-python
cd spams-python
# spams-python

## OpenBLAS / OpenMP warning
If you see messages like:

> OpenBLAS Warning : Detect OpenMP Loop and this application may hang. Please rebuild the library with USE_OPENMP=1 option

This comes from **OpenBLAS**, typically when:
- SPAMS was built with OpenMP enabled (for parallelism), and
- the linked OpenBLAS was built **without** OpenMP support.

Usually it is just a warning, but if you want to silence/avoid it you can:
- Set `OPENBLAS_NUM_THREADS=1` (and/or `OMP_NUM_THREADS=1`) at runtime.
- Install an OpenBLAS build compiled with OpenMP (`USE_OPENMP=1`) or use MKL.
- Rebuild SPAMS without OpenMP by setting `SPAMS_DISABLE_OPENMP=1` (or `SPAMS_USE_OPENMP=0`) when installing.
pip install -e .
```

Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[build-system]
requires = ["oldest-supported-numpy",
"setuptools<60.0",
"distro",
"wheel"]
requires = [
"setuptools>=64",
"wheel",
"numpy>=2.0.0",
]
build-backend = "setuptools.build_meta"
165 changes: 87 additions & 78 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext

from distutils.sysconfig import get_python_inc
import sysconfig
# from distutils.util import get_platform

def check_openmp():
Expand Down Expand Up @@ -43,93 +43,93 @@ def check_openmp():
# output : 0 if ok, 1 if not
return result

def _split_env_paths(value: str):
if not value:
return []
return [p for p in value.split(os.pathsep) if p]


def _split_env_args(value: str):
"""Split a string of args like: "-Wl,--as-needed -lm"."""
if not value:
return []
return value.split()


def _split_env_libs(value: str):
"""Split a list of libraries from an env var.

Accepts comma and/or whitespace separated values.
"""
if not value:
return []
# allow: "openblas" or "blas,lapack" or "blas lapack"
return [x for x in value.replace(',', ' ').split() if x]


def _env_truthy(value: str) -> bool:
return value.strip().lower() not in {"0", "false", "no", "off", ""}


def get_config():
# Import numpy here, only when headers are needed
import numpy as np
from numpy.distutils.system_info import blas_info

incs = ['spams_wrap']
for x in ['linalg', 'prox', 'decomp', 'dictLearn']:
incs.append(os.path.join('spams_wrap', x))

# NumPy + Python headers
incs.append(np.get_include())
incs.append(get_python_inc())
incs.extend(blas_info().get_include_dirs())
incs.append(sysconfig.get_path('include'))

# Optional user overrides
incs.extend(_split_env_paths(os.environ.get('SPAMS_INCLUDE_DIRS', '')))

cc_flags = ['-fPIC', '-Wunused-variable', '-Wno-uninitialized']
if sys.maxsize > 2**32:
cc_flags.append('-m64')
else:
cc_flags.append('-m32')

# blas/lapack compile/linking info
try:
blas_opt_info = np.__config__.blas_opt_info
except:
try:
blas_opt_info = np.__config__.blas_ilp64_opt_info
except:
blas_opt_info = None

try:
lapack_opt_info = np.__config__.lapack_opt_info
except:
try:
lapack_opt_info = np.__config__.lapack_ilp64_opt_info
except:
lapack_opt_info = None

# blas extra compile args
if blas_opt_info is not None:
for _ in blas_opt_info.get('extra_compile_args', []):
if _ not in cc_flags:
cc_flags.append(_)

# lapack extra compile args
if lapack_opt_info is not None:
for _ in lapack_opt_info.get('extra_compile_args', []):
if _ not in cc_flags:
cc_flags.append(_)

# linking flags
link_flags = []

# blas extra linking flags
if blas_opt_info is not None:
for _ in blas_opt_info.get('extra_link_args', []):
if _ not in link_flags:
link_flags.append(_)

# lapack extra linking flags
if lapack_opt_info is not None:
for _ in lapack_opt_info.get('extra_link_args', []):
if _ not in link_flags:
link_flags.append(_)

# libs
libs = ['stdc++']

# mkl ?
is_mkl = False
if blas_opt_info is not None:
for lib in blas_opt_info.get('libraries', []):
if 'mkl' in lib:
is_mkl = True
break

libdirs = blas_info().get_lib_dirs()
if is_mkl:
for _ in blas_opt_info.get('include_dirs', []):
if _ not in incs:
incs.append(_)
for _ in blas_opt_info.get('library_dirs', []):
if _ not in libdirs:
libdirs.append(_)
libs.extend(['mkl_rt'])
else:
libs.extend(['blas', 'lapack'])
# BLAS/LAPACK
libs = []
libdirs = _split_env_paths(os.environ.get('SPAMS_LIBRARY_DIRS', ''))

# openMP
if check_openmp() == 0:
env_libs = os.environ.get('SPAMS_BLAS_LIBS', '')
if env_libs:
libs.extend(_split_env_libs(env_libs))
else:
if sys.platform == 'darwin':
# Apple Accelerate provides BLAS+LAPACK
link_flags.extend(['-framework', 'Accelerate'])
else:
libs.extend(['blas', 'lapack'])

# Extra flags (escape hatch)
cc_flags.extend(_split_env_args(os.environ.get('SPAMS_EXTRA_COMPILE_ARGS', '')))
link_flags.extend(_split_env_args(os.environ.get('SPAMS_EXTRA_LINK_ARGS', '')))

# OpenMP
#
# Some OpenBLAS builds (USE_OPENMP=0) emit a runtime warning when called from
# OpenMP regions. We keep OpenMP enabled by default for performance, but
# provide a build-time escape hatch:
# - SPAMS_DISABLE_OPENMP=1 (force off)
# - SPAMS_USE_OPENMP=0/1 (explicit on/off)
disable_openmp = os.environ.get('SPAMS_DISABLE_OPENMP', '')
use_openmp = os.environ.get('SPAMS_USE_OPENMP', '')

enable_openmp = True
if disable_openmp:
enable_openmp = False
elif use_openmp:
enable_openmp = _env_truthy(use_openmp)

if enable_openmp and check_openmp() == 0:
cc_flags.append('-fopenmp')
link_flags.append('-fopenmp')

Expand Down Expand Up @@ -157,13 +157,22 @@ def run(self):

def get_extension():
# Generic initialization of the extension
spams_wrap = Extension('_spams_wrap',
sources=['spams_wrap/spams_wrap.cpp'],
extra_compile_args=['-DNDEBUG',
'-DUSE_BLAS_LIB',
'-std=c++11'],
language='c++',
depends=['spams_wrap/spams.h'])
spams_wrap = Extension(
'spams_wrap._spams_wrap',
sources=['spams_wrap/spams_wrap.cpp'],
define_macros=[
# Build against NumPy 2.x but target the 1.19 C-API feature set.
# (NumPy 2 defaults to this, but we keep it explicit.)
('NPY_TARGET_VERSION', 'NPY_1_19_API_VERSION'),
],
extra_compile_args=[
'-DNDEBUG',
'-DUSE_BLAS_LIB',
'-std=c++11',
],
language='c++',
depends=['spams_wrap/spams.h'],
)

return [spams_wrap]

Expand Down Expand Up @@ -194,8 +203,8 @@ def get_extension():
"Source": "https://github.com/getspams/spams-python",
},
license='GPLv3',
python_requires='>=3',
install_requires=['numpy>=1.12', 'Pillow>=6.0', 'scipy>=1.0'],
python_requires='>=3.10',
install_requires=['numpy>=1.19', 'Pillow>=6.0', 'scipy>=1.0'],
packages=['spams_wrap', 'spams', 'spams.tests'],
cmdclass={'build_ext': CustomBuildExtCommand},
ext_modules=get_extension(),
Expand Down
11 changes: 9 additions & 2 deletions spams/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
__all__ = ['spams']

from pkg_resources import get_distribution
__version__ = get_distribution('spams').version
try:
from importlib.metadata import PackageNotFoundError, version
except ImportError: # pragma: no cover
from importlib_metadata import PackageNotFoundError, version

try:
__version__ = version('spams')
except PackageNotFoundError: # pragma: no cover
__version__ = '0+unknown'

from .spams import *
13 changes: 10 additions & 3 deletions spams_wrap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
__all__ = ['spams_wrap', 'decomp','dictLearn','linalg','prox']
__all__ = ['spams_wrap', 'decomp', 'dictLearn', 'linalg', 'prox']

from pkg_resources import get_distribution
__version__ = get_distribution('spams').version
try:
from importlib.metadata import PackageNotFoundError, version
except ImportError: # pragma: no cover
from importlib_metadata import PackageNotFoundError, version

try:
__version__ = version('spams')
except PackageNotFoundError: # pragma: no cover
__version__ = '0+unknown'

from .spams_wrap import *
Loading