Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-threadpoolctl for openSUSE:Factory checked in at 2024-04-03 17:18:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-threadpoolctl (Old) and /work/SRC/openSUSE:Factory/.python-threadpoolctl.new.1905 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-threadpoolctl" Wed Apr 3 17:18:23 2024 rev:9 rq:1164225 version:3.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-threadpoolctl/python-threadpoolctl.changes 2023-12-09 22:49:56.892354323 +0100 +++ /work/SRC/openSUSE:Factory/.python-threadpoolctl.new.1905/python-threadpoolctl.changes 2024-04-03 17:18:41.645689566 +0200 @@ -1,0 +2,16 @@ +Tue Apr 2 18:54:36 UTC 2024 - Dirk Müller <dmuel...@suse.com> + +- update to 3.4.0: + * Added support for Python interpreters statically linked + against libc or linked against alternative implementations + of libc like musl + * Added support for Pyodide + * Extended FlexiBLAS support to be able to switch backend at + runtime. + * Added support for FlexiBLAS + * Fixed a bug where an unsupported library would be detected + because it shares a common prefix with one of the supported + libraries. Now the symbols are also checked to + identify the supported libraries. + +------------------------------------------------------------------- Old: ---- threadpoolctl-3.2.0.tar.gz New: ---- threadpoolctl-3.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-threadpoolctl.spec ++++++ --- /var/tmp/diff_new_pack.6Jzvuq/_old 2024-04-03 17:18:42.617725381 +0200 +++ /var/tmp/diff_new_pack.6Jzvuq/_new 2024-04-03 17:18:42.617725381 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-threadpoolctl # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-threadpoolctl -Version: 3.2.0 +Version: 3.4.0 Release: 0 Summary: Thread-pool Controls License: BSD-3-Clause ++++++ threadpoolctl-3.2.0.tar.gz -> threadpoolctl-3.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/.azure_pipeline.yml new/threadpoolctl-3.4.0/.azure_pipeline.yml --- old/threadpoolctl-3.2.0/.azure_pipeline.yml 2023-07-13 16:19:36.077411700 +0200 +++ new/threadpoolctl-3.4.0/.azure_pipeline.yml 2024-02-14 16:28:49.147424000 +0100 @@ -25,7 +25,7 @@ steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.11' + versionSpec: '3.12' - script: | pip install black displayName: install black @@ -39,21 +39,20 @@ vmImage: windows-latest matrix: pylatest_conda_forge_mkl: - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' PACKAGER: 'conda-forge' BLAS: 'mkl' py310_conda_forge_openblas: - VERSION_PYTHON: '3.10' + PYTHON_VERSION: '3.10' PACKAGER: 'conda-forge' BLAS: 'openblas' py39_conda: - VERSION_PYTHON: '3.9' + PYTHON_VERSION: '3.9' PACKAGER: 'conda' py38_pip: - VERSION_PYTHON: '3.8' + PYTHON_VERSION: '3.8' PACKAGER: 'pip' - - template: continuous_integration/posix.yml parameters: name: Linux @@ -64,19 +63,19 @@ py38_ubuntu_atlas_gcc_gcc: PACKAGER: 'ubuntu' APT_BLAS: 'libatlas3-base libatlas-base-dev' - VERSION_PYTHON: '3.8' + PYTHON_VERSION: '3.8' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'gcc' py38_ubuntu_openblas_gcc_gcc: PACKAGER: 'ubuntu' APT_BLAS: 'libopenblas-base libopenblas-dev' - VERSION_PYTHON: '3.8' + PYTHON_VERSION: '3.8' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'gcc' # Linux + Python 3.9 and homogeneous runtime nesting. py39_conda_openblas_clang_clang: PACKAGER: 'conda' - VERSION_PYTHON: '3.9' + PYTHON_VERSION: '3.9' BLAS: 'openblas' CC_OUTER_LOOP: 'clang-10' CC_INNER_LOOP: 'clang-10' @@ -84,7 +83,7 @@ # threadpoolctl) to only test the warning from multiple OpenMP. pylatest_conda_mkl_clang_gcc: PACKAGER: 'conda' - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' BLAS: 'mkl' CC_OUTER_LOOP: 'clang-10' CC_INNER_LOOP: 'gcc' @@ -92,7 +91,7 @@ # Linux environment with MKL, safe for threadpoolctl. pylatest_conda_mkl_gcc_gcc: PACKAGER: 'conda' - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' BLAS: 'mkl' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'gcc' @@ -101,13 +100,13 @@ # and heterogeneous OpenMP runtimes. py38_pip_openblas_gcc_clang: PACKAGER: 'pip' - VERSION_PYTHON: '3.8' + PYTHON_VERSION: '3.8' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'clang-10' # Linux environment with numpy from conda-forge channel and openblas-openmp pylatest_conda_forge: PACKAGER: 'conda-forge' - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' BLAS: 'openblas' OPENBLAS_THREADING_LAYER: 'openmp' CC_OUTER_LOOP: 'gcc' @@ -116,14 +115,14 @@ pylatest_conda_nonumpy_gcc_clang: PACKAGER: 'conda' NO_NUMPY: 'true' - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'clang-10' # Linux environment with numpy linked to BLIS pylatest_blis_gcc_clang_openmp: PACKAGER: 'conda' - VERSION_PYTHON: '*' - INSTALL_BLIS: 'true' + PYTHON_VERSION: '*' + INSTALL_BLAS: 'blis' BLIS_NUM_THREADS: '4' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'gcc' @@ -131,8 +130,8 @@ BLIS_ENABLE_THREADING: 'openmp' pylatest_blis_clang_gcc_pthreads: PACKAGER: 'conda' - VERSION_PYTHON: '*' - INSTALL_BLIS: 'true' + PYTHON_VERSION: '*' + INSTALL_BLAS: 'blis' BLIS_NUM_THREADS: '4' CC_OUTER_LOOP: 'clang-10' CC_INNER_LOOP: 'clang-10' @@ -140,13 +139,21 @@ BLIS_ENABLE_THREADING: 'pthreads' pylatest_blis_no_threading: PACKAGER: 'conda' - VERSION_PYTHON: '*' - INSTALL_BLIS: 'true' + PYTHON_VERSION: '*' + INSTALL_BLAS: 'blis' BLIS_NUM_THREADS: '1' CC_OUTER_LOOP: 'gcc' CC_INNER_LOOP: 'gcc' BLIS_CC: 'gcc-8' BLIS_ENABLE_THREADING: 'no' + pylatest_flexiblas: + PACKAGER: 'conda' + PYTHON_VERSION: '*' + INSTALL_BLAS: 'flexiblas' + PLATFORM_SPECIFIC_PACKAGES: 'mkl' + CC_OUTER_LOOP: 'gcc' + CC_INNER_LOOP: 'gcc' + - template: continuous_integration/posix.yml parameters: @@ -156,7 +163,7 @@ # MacOS environment with OpenMP installed through homebrew py38_conda_homebrew_libomp: PACKAGER: 'conda' - VERSION_PYTHON: '3.8' + PYTHON_VERSION: '3.8' BLAS: 'openblas' CC_OUTER_LOOP: 'clang' CC_INNER_LOOP: 'clang' @@ -164,7 +171,7 @@ # MacOS env with OpenBLAS and OpenMP installed through conda-forge compilers py39_conda_forge_clang_openblas: PACKAGER: 'conda-forge' - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' BLAS: 'openblas' CC_OUTER_LOOP: 'clang' CC_INNER_LOOP: 'clang' @@ -172,11 +179,18 @@ # MacOS environment with OpenMP installed through conda-forge compilers pylatest_conda_forge_clang: PACKAGER: 'conda-forge' - VERSION_PYTHON: '*' + PYTHON_VERSION: '*' BLAS: 'mkl' CC_OUTER_LOOP: 'clang' CC_INNER_LOOP: 'clang' INSTALL_LIBOMP: 'conda-forge' + pylatest_flexiblas: + PACKAGER: 'conda' + PYTHON_VERSION: '*' + INSTALL_BLAS: 'flexiblas' + PLATFORM_SPECIFIC_PACKAGES: 'mkl' + CC_OUTER_LOOP: 'clang' + CC_INNER_LOOP: 'clang' - stage: jobs: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/CHANGES.md new/threadpoolctl-3.4.0/CHANGES.md --- old/threadpoolctl-3.2.0/CHANGES.md 2023-07-13 16:44:13.625080800 +0200 +++ new/threadpoolctl-3.4.0/CHANGES.md 2024-03-20 14:36:20.768322000 +0100 @@ -1,3 +1,27 @@ +3.4.0 (2024-03-20) +================== + +- Added support for Python interpreters statically linked against libc or linked against + alternative implementations of libc like musl (on Alpine Linux for instance). + https://github.com/joblib/threadpoolctl/pull/171 + +- Added support for Pyodide + https://github.com/joblib/threadpoolctl/pull/169 + +3.3.0 (2024-02-14) +================== + +- Extended FlexiBLAS support to be able to switch backend at runtime. + https://github.com/joblib/threadpoolctl/pull/163 + +- Added support for FlexiBLAS + https://github.com/joblib/threadpoolctl/pull/156 + +- Fixed a bug where an unsupported library would be detected because it shares a common + prefix with one of the supported libraries. Now the symbols are also checked to + identify the supported libraries. + https://github.com/joblib/threadpoolctl/pull/151 + 3.2.0 (2023-07-13) ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/PKG-INFO new/threadpoolctl-3.4.0/PKG-INFO --- old/threadpoolctl-3.2.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/threadpoolctl-3.4.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: threadpoolctl -Version: 3.2.0 +Version: 3.4.0 Summary: threadpoolctl Home-page: https://github.com/joblib/threadpoolctl License: BSD-3-Clause @@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development :: Libraries :: Python Modules # Thread-pool Controls [](https://dev.azure.com/joblib/threadpoolctl/_build/latest?definitionId=1&branchName=master) [](https://codecov.io/gh/joblib/threadpoolctl) @@ -211,6 +212,92 @@ ... ``` +### Switching the FlexiBLAS backend + +`FlexiBLAS` is a BLAS wrapper for which the BLAS backend can be switched at runtime. +`threadpoolctl` exposes python bindings for this feature. Here's an example but note +that this part of the API is experimental and subject to change without deprecation: + +```python +>>> from threadpoolctl import ThreadpoolController +>>> import numpy as np +>>> controller = ThreadpoolController() + +>>> controller.info() +[{'user_api': 'blas', + 'internal_api': 'flexiblas', + 'num_threads': 1, + 'prefix': 'libflexiblas', + 'filepath': '/usr/local/lib/libflexiblas.so.3.3', + 'version': '3.3.1', + 'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'], + 'loaded_backends': ['NETLIB'], + 'current_backend': 'NETLIB'}] + +# Retrieve the flexiblas controller +>>> flexiblas_ct = controller.select(internal_api="flexiblas").lib_controllers[0] + +# Switch the backend with one predefined at build time (listed in "available_backends") +>>> flexiblas_ct.switch_backend("OPENBLASPTHREAD") +>>> controller.info() +[{'user_api': 'blas', + 'internal_api': 'flexiblas', + 'num_threads': 4, + 'prefix': 'libflexiblas', + 'filepath': '/usr/local/lib/libflexiblas.so.3.3', + 'version': '3.3.1', + 'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'], + 'loaded_backends': ['NETLIB', 'OPENBLASPTHREAD'], + 'current_backend': 'OPENBLASPTHREAD'}, + {'user_api': 'blas', + 'internal_api': 'openblas', + 'num_threads': 4, + 'prefix': 'libopenblas', + 'filepath': '/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.8.so', + 'version': '0.3.8', + 'threading_layer': 'pthreads', + 'architecture': 'Haswell'}] + +# It's also possible to directly give the path to a shared library +>>> flexiblas_controller.switch_backend("/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so") +>>> controller.info() +[{'user_api': 'blas', + 'internal_api': 'flexiblas', + 'num_threads': 2, + 'prefix': 'libflexiblas', + 'filepath': '/usr/local/lib/libflexiblas.so.3.3', + 'version': '3.3.1', + 'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'], + 'loaded_backends': ['NETLIB', + 'OPENBLASPTHREAD', + '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so'], + 'current_backend': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so'}, + {'user_api': 'openmp', + 'internal_api': 'openmp', + 'num_threads': 4, + 'prefix': 'libomp', + 'filepath': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libomp.so', + 'version': None}, + {'user_api': 'blas', + 'internal_api': 'openblas', + 'num_threads': 4, + 'prefix': 'libopenblas', + 'filepath': '/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.8.so', + 'version': '0.3.8', + 'threading_layer': 'pthreads', + 'architecture': 'Haswell'}, + {'user_api': 'blas', + 'internal_api': 'mkl', + 'num_threads': 2, + 'prefix': 'libmkl_rt', + 'filepath': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so.2', + 'version': '2024.0-Product', + 'threading_layer': 'gnu'}] +``` + +You can observe that the previously linked OpenBLAS shared object stays loaded by +the Python program indefinitely, but FlexiBLAS itself no longer delegates BLAS calls +to OpenBLAS as indicated by the `current_backend` attribute. ### Writing a custom library controller Currently, `threadpoolctl` has support for `OpenMP` and the main `BLAS` libraries. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/README.md new/threadpoolctl-3.4.0/README.md --- old/threadpoolctl-3.2.0/README.md 2023-07-12 10:17:49.739681000 +0200 +++ new/threadpoolctl-3.4.0/README.md 2024-02-14 16:28:49.151424000 +0100 @@ -192,6 +192,92 @@ ... ``` +### Switching the FlexiBLAS backend + +`FlexiBLAS` is a BLAS wrapper for which the BLAS backend can be switched at runtime. +`threadpoolctl` exposes python bindings for this feature. Here's an example but note +that this part of the API is experimental and subject to change without deprecation: + +```python +>>> from threadpoolctl import ThreadpoolController +>>> import numpy as np +>>> controller = ThreadpoolController() + +>>> controller.info() +[{'user_api': 'blas', + 'internal_api': 'flexiblas', + 'num_threads': 1, + 'prefix': 'libflexiblas', + 'filepath': '/usr/local/lib/libflexiblas.so.3.3', + 'version': '3.3.1', + 'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'], + 'loaded_backends': ['NETLIB'], + 'current_backend': 'NETLIB'}] + +# Retrieve the flexiblas controller +>>> flexiblas_ct = controller.select(internal_api="flexiblas").lib_controllers[0] + +# Switch the backend with one predefined at build time (listed in "available_backends") +>>> flexiblas_ct.switch_backend("OPENBLASPTHREAD") +>>> controller.info() +[{'user_api': 'blas', + 'internal_api': 'flexiblas', + 'num_threads': 4, + 'prefix': 'libflexiblas', + 'filepath': '/usr/local/lib/libflexiblas.so.3.3', + 'version': '3.3.1', + 'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'], + 'loaded_backends': ['NETLIB', 'OPENBLASPTHREAD'], + 'current_backend': 'OPENBLASPTHREAD'}, + {'user_api': 'blas', + 'internal_api': 'openblas', + 'num_threads': 4, + 'prefix': 'libopenblas', + 'filepath': '/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.8.so', + 'version': '0.3.8', + 'threading_layer': 'pthreads', + 'architecture': 'Haswell'}] + +# It's also possible to directly give the path to a shared library +>>> flexiblas_controller.switch_backend("/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so") +>>> controller.info() +[{'user_api': 'blas', + 'internal_api': 'flexiblas', + 'num_threads': 2, + 'prefix': 'libflexiblas', + 'filepath': '/usr/local/lib/libflexiblas.so.3.3', + 'version': '3.3.1', + 'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'], + 'loaded_backends': ['NETLIB', + 'OPENBLASPTHREAD', + '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so'], + 'current_backend': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so'}, + {'user_api': 'openmp', + 'internal_api': 'openmp', + 'num_threads': 4, + 'prefix': 'libomp', + 'filepath': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libomp.so', + 'version': None}, + {'user_api': 'blas', + 'internal_api': 'openblas', + 'num_threads': 4, + 'prefix': 'libopenblas', + 'filepath': '/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.8.so', + 'version': '0.3.8', + 'threading_layer': 'pthreads', + 'architecture': 'Haswell'}, + {'user_api': 'blas', + 'internal_api': 'mkl', + 'num_threads': 2, + 'prefix': 'libmkl_rt', + 'filepath': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so.2', + 'version': '2024.0-Product', + 'threading_layer': 'gnu'}] +``` + +You can observe that the previously linked OpenBLAS shared object stays loaded by +the Python program indefinitely, but FlexiBLAS itself no longer delegates BLAS calls +to OpenBLAS as indicated by the `current_backend` attribute. ### Writing a custom library controller Currently, `threadpoolctl` has support for `OpenMP` and the main `BLAS` libraries. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/install.cmd new/threadpoolctl-3.4.0/continuous_integration/install.cmd --- old/threadpoolctl-3.2.0/continuous_integration/install.cmd 2022-01-20 18:05:26.048133900 +0100 +++ new/threadpoolctl-3.4.0/continuous_integration/install.cmd 2023-12-13 10:38:54.935551200 +0100 @@ -9,8 +9,10 @@ @rem Deactivate any environment call deactivate @rem Clean up any left-over from a previous build and install version of python +conda update -n base conda conda-libmamba-solver -q -y +conda config --set solver libmamba conda remove --all -q -y -n %VIRTUALENV% -conda create -n %VIRTUALENV% -q -y python=%VERSION_PYTHON% +conda create -n %VIRTUALENV% -q -y python=%PYTHON_VERSION% call activate %VIRTUALENV% python -m pip install -U pip diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/install.sh new/threadpoolctl-3.4.0/continuous_integration/install.sh --- old/threadpoolctl-3.2.0/continuous_integration/install.sh 2023-07-12 15:11:15.498224700 +0200 +++ new/threadpoolctl-3.4.0/continuous_integration/install.sh 2023-12-13 10:38:54.939551400 +0100 @@ -17,7 +17,6 @@ if [[ "$UNAMESTR" == "Darwin" ]]; then if [[ "$INSTALL_LIBOMP" == "conda-forge" ]]; then # Install an OpenMP-enabled clang/llvm from conda-forge - # assumes conda-forge is set on priority channel TO_INSTALL="$TO_INSTALL compilers llvm-openmp" @@ -35,12 +34,14 @@ export LDFLAGS="$LDFLAGS -Wl,-rpath,/usr/local/opt/libomp/lib -L/usr/local/opt/libomp/lib -lomp" fi fi + conda update -n base conda conda-libmamba-solver -q --yes + conda config --set solver libmamba conda create -n $VIRTUALENV -q --yes $TO_INSTALL source activate $VIRTUALENV } if [[ "$PACKAGER" == "conda" ]]; then - TO_INSTALL="python=$VERSION_PYTHON pip" + TO_INSTALL="python=$PYTHON_VERSION pip" if [[ "$NO_NUMPY" != "true" ]]; then TO_INSTALL="$TO_INSTALL numpy scipy blas[build=$BLAS]" fi @@ -49,7 +50,7 @@ elif [[ "$PACKAGER" == "conda-forge" ]]; then conda config --prepend channels conda-forge conda config --set channel_priority strict - TO_INSTALL="python=$VERSION_PYTHON numpy scipy blas[build=$BLAS]" + TO_INSTALL="python=$PYTHON_VERSION numpy scipy blas[build=$BLAS]" if [[ "$BLAS" == "openblas" && "$OPENBLAS_THREADING_LAYER" == "openmp" ]]; then TO_INSTALL="$TO_INSTALL libopenblas=*=*openmp*" fi @@ -58,7 +59,7 @@ elif [[ "$PACKAGER" == "pip" ]]; then # Use conda to build an empty python env and then use pip to install # numpy and scipy - TO_INSTALL="python=$VERSION_PYTHON pip" + TO_INSTALL="python=$PYTHON_VERSION pip" make_conda $TO_INSTALL if [[ "$NO_NUMPY" != "true" ]]; then pip install numpy scipy diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/install_with_blis.sh new/threadpoolctl-3.4.0/continuous_integration/install_with_blis.sh --- old/threadpoolctl-3.2.0/continuous_integration/install_with_blis.sh 2023-07-12 15:09:55.561690000 +0200 +++ new/threadpoolctl-3.4.0/continuous_integration/install_with_blis.sh 2024-02-06 09:45:04.404626800 +0100 @@ -13,7 +13,10 @@ sudo apt-get install libomp-dev # create conda env -conda create -n $VIRTUALENV -q --yes -c conda-forge python=$VERSION_PYTHON pip cython +conda update -n base conda conda-libmamba-solver -q --yes +conda config --set solver libmamba +conda create -n $VIRTUALENV -q --yes -c conda-forge python=$PYTHON_VERSION \ + pip cython meson-python pkg-config source activate $VIRTUALENV if [[ "$BLIS_CC" == "gcc-8" ]]; then @@ -36,20 +39,31 @@ git clone https://github.com/numpy/numpy.git pushd numpy git submodule update --init -echo "[blis] -libraries = blis -library_dirs = $ABS_PATH/BLIS_install/lib -include_dirs = $ABS_PATH/BLIS_install/include/blis -runtime_library_dirs = $ABS_PATH/BLIS_install/lib" > site.cfg -python setup.py develop + +echo "libdir=$ABS_PATH/BLIS_install/lib/ +includedir=$ABS_PATH/BLIS_install/include/blis/ +version=latest +extralib=-lm -lpthread -lgfortran +Name: blis +Description: BLIS +Version: \${version} +Libs: -L\${libdir} -lblis +Libs.private: \${extralib} +Cflags: -I\${includedir}" > blis.pc + +PKG_CONFIG_PATH=$ABS_PATH/numpy/ pip install . -v --no-build-isolation -Csetup-args=-Dblas=blis popd popd python -m pip install -q -r dev-requirements.txt -CFLAGS=-I$ABS_PATH/BLIS_install/include/blis LDFLAGS=-L$ABS_PATH/BLIS_install/lib \ +CFLAGS=-I$ABS_PATH/BLIS_install/include/blis \ + LDFLAGS="-L$ABS_PATH/BLIS_install/lib -Wl,-rpath,$ABS_PATH/BLIS_install/lib" \ bash ./continuous_integration/build_test_ext.sh +# Check that BLIS is linked +ldd tests/_openmp_test_helper/nested_prange_blas.cpython*.so + python --version python -c "import numpy; print(f'numpy {numpy.__version__}')" || echo "no numpy" python -c "import scipy; print(f'scipy {scipy.__version__}')" || echo "no scipy" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/install_with_flexiblas.sh new/threadpoolctl-3.4.0/continuous_integration/install_with_flexiblas.sh --- old/threadpoolctl-3.2.0/continuous_integration/install_with_flexiblas.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/threadpoolctl-3.4.0/continuous_integration/install_with_flexiblas.sh 2024-02-14 16:28:49.151424000 +0100 @@ -0,0 +1,84 @@ +#!/bin/bash + +set -e + +pushd .. +ABS_PATH=$(pwd) +popd + +# create conda env +conda update -n base conda conda-libmamba-solver -q --yes +conda config --set solver libmamba +conda create -n $VIRTUALENV -q --yes -c conda-forge python=$PYTHON_VERSION \ + pip cython openblas $PLATFORM_SPECIFIC_PACKAGES meson-python pkg-config compilers +source activate $VIRTUALENV + +pushd .. + +# build & install FlexiBLAS +mkdir flexiblas_install +git clone https://github.com/mpimd-csc/flexiblas.git +pushd flexiblas + +mkdir build +pushd build + +EXTENSION=".so" +if [[ $(uname) == "Darwin" ]]; then + EXTENSION=".dylib" +fi + +# We intentionally restrict the list of backends to make it easier to +# write platform agnostic tests. In particular, we do not detect OS +# provided backends such as macOS' Apple/Accelerate/vecLib nor plaftorm +# specific BLAS implementations such as MKL that cannot be installed on +# arm64 hardware. +cmake ../ -DCMAKE_INSTALL_PREFIX=$ABS_PATH"/flexiblas_install" \ + -DBLAS_AUTO_DETECT="OFF" \ + -DEXTRA="OPENBLAS_CONDA" \ + -DFLEXIBLAS_DEFAULT="OPENBLAS_CONDA" \ + -DOPENBLAS_CONDA_LIBRARY=$CONDA_PREFIX"/lib/libopenblas"$EXTENSION \ +make +make install + +# Check that all 3 BLAS are listed in FlexiBLAS configuration +$ABS_PATH/flexiblas_install/bin/flexiblas list +popd +popd + +# build & install numpy +git clone https://github.com/numpy/numpy.git +pushd numpy +git submodule update --init + +echo "libdir=$ABS_PATH/flexiblas_install/lib/ +includedir=$ABS_PATH/flexiblas_install/include/flexiblas/ +version=3.3.1 +extralib=-lm -lpthread -lgfortran +Name: flexiblas +Description: FlexiBLAS - a BLAS wrapper +Version: \${version} +Libs: -L\${libdir} -lflexiblas +Libs.private: \${extralib} +Cflags: -I\${includedir}" > flexiblas.pc + +PKG_CONFIG_PATH=$ABS_PATH/numpy/ pip install . -v --no-build-isolation -Csetup-args=-Dblas=flexiblas -Csetup-args=-Dlapack=flexiblas +popd + +popd + +python -m pip install -q -r dev-requirements.txt +CFLAGS=-I$ABS_PATH/flexiblas_install/include/flexiblas \ + LDFLAGS="-L$ABS_PATH/flexiblas_install/lib -Wl,-rpath,$ABS_PATH/flexiblas_install/lib" \ + bash ./continuous_integration/build_test_ext.sh + +# Check that FlexiBLAS is linked +if [[ $(uname) != "Darwin" ]]; then + ldd tests/_openmp_test_helper/nested_prange_blas.cpython*.so +fi + +python --version +python -c "import numpy; print(f'numpy {numpy.__version__}')" || echo "no numpy" +python -c "import scipy; print(f'scipy {scipy.__version__}')" || echo "no scipy" + +python -m flit install --symlink diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/posix.yml new/threadpoolctl-3.4.0/continuous_integration/posix.yml --- old/threadpoolctl-3.2.0/continuous_integration/posix.yml 2023-07-13 16:19:36.077411700 +0200 +++ new/threadpoolctl-3.4.0/continuous_integration/posix.yml 2024-02-07 13:42:18.877825700 +0100 @@ -22,12 +22,16 @@ condition: eq('${{ parameters.name }}', 'macOS') - script: | continuous_integration/install.sh - displayName: 'Install without BLIS' - condition: ne(variables['INSTALL_BLIS'], 'true') + displayName: 'Install without custom BLAS' + condition: eq(variables['INSTALL_BLAS'], '') - script: | continuous_integration/install_with_blis.sh displayName: 'Install with BLIS' - condition: eq(variables['INSTALL_BLIS'], 'true') + condition: eq(variables['INSTALL_BLAS'], 'blis') + - script: | + continuous_integration/install_with_flexiblas.sh + displayName: 'Install with FlexiBLAS' + condition: eq(variables['INSTALL_BLAS'], 'flexiblas') - script: | continuous_integration/test_script.sh displayName: 'Test Library' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/test_script.cmd new/threadpoolctl-3.4.0/continuous_integration/test_script.cmd --- old/threadpoolctl-3.2.0/continuous_integration/test_script.cmd 2022-01-20 18:05:26.048133900 +0100 +++ new/threadpoolctl-3.4.0/continuous_integration/test_script.cmd 2023-12-13 10:38:54.939551400 +0100 @@ -1,5 +1,8 @@ call activate %VIRTUALENV% +# Display version information +python -m pip list + # Use the CLI to display the effective runtime environment prior to # launching the tests: python -m threadpoolctl -i numpy scipy.linalg tests._openmp_test_helper.openmp_helpers_inner diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/continuous_integration/test_script.sh new/threadpoolctl-3.4.0/continuous_integration/test_script.sh --- old/threadpoolctl-3.2.0/continuous_integration/test_script.sh 2022-01-20 18:05:26.048133900 +0100 +++ new/threadpoolctl-3.4.0/continuous_integration/test_script.sh 2024-01-17 11:19:28.785481200 +0100 @@ -4,11 +4,14 @@ if [[ "$PACKAGER" == conda* ]]; then source activate $VIRTUALENV + conda list elif [[ "$PACKAGER" == "pip" ]]; then # we actually use conda to install the base environment: source activate $VIRTUALENV + pip list elif [[ "$PACKAGER" == "ubuntu" ]]; then source $VIRTUALENV/bin/activate + pip list fi set -x diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/pyproject.toml new/threadpoolctl-3.4.0/pyproject.toml --- old/threadpoolctl-3.2.0/pyproject.toml 2023-07-12 10:17:49.743681400 +0200 +++ new/threadpoolctl-3.4.0/pyproject.toml 2024-02-09 15:27:12.738413800 +0100 @@ -1,7 +1,9 @@ [build-system] -requires = ["flit_core"] +requires = ["flit_core >=2,<4"] build-backend = "flit_core.buildapi" +# TODO: replace the following section by the standard [project] section to be able +# to use flit v4. [tool.flit.metadata] module = "threadpoolctl" author = "Thomas Moreau" @@ -18,10 +20,11 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", ] [tool.black] line-length = 88 -target_version = ['py38', 'py39', 'py310', 'py311'] +target_version = ['py38', 'py39', 'py310', 'py311', 'py312'] preview = true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/_openmp_test_helper/nested_prange_blas.pyx new/threadpoolctl-3.4.0/tests/_openmp_test_helper/nested_prange_blas.pyx --- old/threadpoolctl-3.2.0/tests/_openmp_test_helper/nested_prange_blas.pyx 2021-09-29 18:23:34.090675000 +0200 +++ new/threadpoolctl-3.4.0/tests/_openmp_test_helper/nested_prange_blas.pyx 2023-12-13 10:38:54.939551400 +0100 @@ -2,23 +2,7 @@ from cython.parallel import parallel, prange import numpy as np - -IF USE_BLIS: - cdef extern from 'cblas.h' nogil: - ctypedef enum CBLAS_ORDER: - CblasRowMajor=101 - CblasColMajor=102 - ctypedef enum CBLAS_TRANSPOSE: - CblasNoTrans=111 - CblasTrans=112 - CblasConjTrans=113 - void dgemm 'cblas_dgemm' ( - CBLAS_ORDER Order, CBLAS_TRANSPOSE TransA, - CBLAS_TRANSPOSE TransB, int M, int N, - int K, double alpha, double *A, int lda, - double *B, int ldb, double beta, double *C, int ldc) -ELSE: - from scipy.linalg.cython_blas cimport dgemm +from scipy.linalg.cython_blas cimport dgemm from threadpoolctl import ThreadpoolController @@ -53,13 +37,8 @@ prange_num_threads_ptr[0] = openmp.omp_get_num_threads() for i in prange(n_chunks): - IF USE_BLIS: - dgemm(CblasRowMajor, CblasNoTrans, CblasTrans, - chunk_size, n, k, alpha, &A[i * chunk_size, 0], k, - &B[0, 0], k, beta, &C[i * chunk_size, 0], n) - ELSE: - dgemm(trans, no_trans, &n, &chunk_size, &k, - &alpha, &B[0, 0], &k, &A[i * chunk_size, 0], &k, - &beta, &C[i * chunk_size, 0], &n) + dgemm(trans, no_trans, &n, &chunk_size, &k, + &alpha, &B[0, 0], &k, &A[i * chunk_size, 0], &k, + &beta, &C[i * chunk_size, 0], &n) return np.asarray(C), prange_num_threads, inner_info[0] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/_openmp_test_helper/nested_prange_blas_custom.pyx new/threadpoolctl-3.4.0/tests/_openmp_test_helper/nested_prange_blas_custom.pyx --- old/threadpoolctl-3.2.0/tests/_openmp_test_helper/nested_prange_blas_custom.pyx 1970-01-01 01:00:00.000000000 +0100 +++ new/threadpoolctl-3.4.0/tests/_openmp_test_helper/nested_prange_blas_custom.pyx 2024-02-07 13:42:18.885825900 +0100 @@ -0,0 +1,55 @@ +cimport openmp +from cython.parallel import parallel, prange + +import numpy as np + +cdef extern from 'cblas.h' nogil: + ctypedef enum CBLAS_ORDER: + CblasRowMajor=101 + CblasColMajor=102 + ctypedef enum CBLAS_TRANSPOSE: + CblasNoTrans=111 + CblasTrans=112 + CblasConjTrans=113 + void dgemm 'cblas_dgemm' ( + CBLAS_ORDER Order, CBLAS_TRANSPOSE TransA, + CBLAS_TRANSPOSE TransB, int M, int N, + int K, double alpha, double *A, int lda, + double *B, int ldb, double beta, double *C, int ldc) + +from threadpoolctl import ThreadpoolController + + +def check_nested_prange_blas(double[:, ::1] A, double[:, ::1] B, int nthreads): + """Run multithreaded BLAS calls within OpenMP parallel loop""" + cdef: + int m = A.shape[0] + int n = B.shape[0] + int k = A.shape[1] + + double[:, ::1] C = np.empty((m, n)) + int n_chunks = 100 + int chunk_size = A.shape[0] // n_chunks + + double alpha = 1.0 + double beta = 0.0 + + int i + int prange_num_threads + int *prange_num_threads_ptr = &prange_num_threads + + inner_info = [None] + + with nogil, parallel(num_threads=nthreads): + if openmp.omp_get_thread_num() == 0: + with gil: + inner_info[0] = ThreadpoolController().info() + + prange_num_threads_ptr[0] = openmp.omp_get_num_threads() + + for i in prange(n_chunks): + dgemm(CblasRowMajor, CblasNoTrans, CblasTrans, + chunk_size, n, k, alpha, &A[i * chunk_size, 0], k, + &B[0, 0], k, beta, &C[i * chunk_size, 0], n) + + return np.asarray(C), prange_num_threads, inner_info[0] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/_openmp_test_helper/openmp_helpers_inner.pxd new/threadpoolctl-3.4.0/tests/_openmp_test_helper/openmp_helpers_inner.pxd --- old/threadpoolctl-3.2.0/tests/_openmp_test_helper/openmp_helpers_inner.pxd 2021-09-15 15:26:21.493843300 +0200 +++ new/threadpoolctl-3.4.0/tests/_openmp_test_helper/openmp_helpers_inner.pxd 2024-02-06 09:45:04.408626800 +0100 @@ -1 +1 @@ -cdef int inner_openmp_loop(int) nogil +cdef int inner_openmp_loop(int) noexcept nogil diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/_openmp_test_helper/openmp_helpers_inner.pyx new/threadpoolctl-3.4.0/tests/_openmp_test_helper/openmp_helpers_inner.pyx --- old/threadpoolctl-3.2.0/tests/_openmp_test_helper/openmp_helpers_inner.pyx 2021-12-31 17:52:30.808669800 +0100 +++ new/threadpoolctl-3.4.0/tests/_openmp_test_helper/openmp_helpers_inner.pyx 2024-02-06 09:45:04.408626800 +0100 @@ -15,7 +15,7 @@ return num_threads -cdef int inner_openmp_loop(int n) nogil: +cdef int inner_openmp_loop(int n) noexcept nogil: """Run a short parallel section with OpenMP Return the number of threads that where effectively used by the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/_openmp_test_helper/setup_nested_prange_blas.py new/threadpoolctl-3.4.0/tests/_openmp_test_helper/setup_nested_prange_blas.py --- old/threadpoolctl-3.2.0/tests/_openmp_test_helper/setup_nested_prange_blas.py 2021-09-24 10:18:13.956710000 +0200 +++ new/threadpoolctl-3.4.0/tests/_openmp_test_helper/setup_nested_prange_blas.py 2024-02-07 13:42:18.877825700 +0100 @@ -12,13 +12,15 @@ set_cc_variables("CC_OUTER_LOOP") openmp_flag = get_openmp_flag() - use_blis = os.getenv("INSTALL_BLIS", False) - libraries = ["blis"] if use_blis else [] + use_custom_blas = os.getenv("INSTALL_BLAS", False) + libraries = [use_custom_blas] if use_custom_blas else [] + custom_suffix = "_custom" if use_custom_blas else "" + filename = f"nested_prange_blas{custom_suffix}.pyx" ext_modules = [ Extension( "nested_prange_blas", - ["nested_prange_blas.pyx"], + [filename], extra_compile_args=openmp_flag, extra_link_args=openmp_flag, libraries=libraries, @@ -29,7 +31,6 @@ name="_openmp_test_helper_nested_prange_blas", ext_modules=cythonize( ext_modules, - compile_time_env={"USE_BLIS": use_blis}, compiler_directives={ "language_level": 3, "boundscheck": False, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/_pyMylib/__init__.py new/threadpoolctl-3.4.0/tests/_pyMylib/__init__.py --- old/threadpoolctl-3.2.0/tests/_pyMylib/__init__.py 2023-07-12 10:17:49.743681400 +0200 +++ new/threadpoolctl-3.4.0/tests/_pyMylib/__init__.py 2023-12-13 10:38:54.943551300 +0100 @@ -19,6 +19,14 @@ # instance. filename_prefixes = ("my_threaded_lib",) + # (Optional) Symbols that the linked library is expected to expose. It is used along + # with the `filename_prefixes` to make sure that the correct library is identified. + check_symbols = ( + "mylib_get_num_threads", + "mylib_set_num_threads", + "mylib_get_version", + ) + def get_num_threads(self): # This function should return the current maximum number of threads, # which is reported as "num_threads" by `ThreadpoolController.info`. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/test_threadpoolctl.py new/threadpoolctl-3.4.0/tests/test_threadpoolctl.py --- old/threadpoolctl-3.2.0/tests/test_threadpoolctl.py 2023-07-12 10:17:49.743681400 +0200 +++ new/threadpoolctl-3.4.0/tests/test_threadpoolctl.py 2024-02-14 16:28:49.155423900 +0100 @@ -10,6 +10,7 @@ from threadpoolctl import _ALL_PREFIXES, _ALL_USER_APIS from .utils import cython_extensions_compiled +from .utils import check_nested_prange_blas from .utils import libopenblas_paths from .utils import scipy from .utils import threadpool_info_from_subprocess @@ -406,19 +407,20 @@ test_shipped_openblas() -@pytest.mark.skipif(scipy is None, reason="requires scipy") @pytest.mark.skipif( not cython_extensions_compiled, reason="Requires cython extensions to be compiled" ) +@pytest.mark.skipif( + check_nested_prange_blas is None, + reason="Requires nested_prange_blas to be compiled", +) @pytest.mark.parametrize("nthreads_outer", [None, 1, 2, 4]) def test_nested_prange_blas(nthreads_outer): - # Check that the BLAS linked to scipy effectively uses the number of - # threads requested by the context manager when nested in an outer OpenMP - # loop. + # Check that the BLAS uses the number of threads requested by the context manager + # when nested in an outer OpenMP loop. + # Remark: this test also works with sequential BLAS only because we limit the + # number of threads for the BLAS to 1. import numpy as np - import tests._openmp_test_helper.nested_prange_blas as prange_blas - - check_nested_prange_blas = prange_blas.check_nested_prange_blas skip_if_openblas_openmp() @@ -545,9 +547,20 @@ assert "multiple_openmp.md" in str(wm.message) -def test_command_line_empty(): +def test_command_line_empty_or_system_openmp(): + # When the command line is called without arguments, no library should be + # detected. The only exception is a system OpenMP library that can be + # linked to the Python interpreter, for instance via the libb2.so BLAKE2 + # library that can itself be linked to an OpenMP runtime on Gentoo. output = subprocess.check_output((sys.executable + " -m threadpoolctl").split()) - assert json.loads(output.decode("utf-8")) == [] + results = json.loads(output.decode("utf-8")) + conda_prefix = os.getenv("CONDA_PREFIX") + managed_by_conda = conda_prefix and sys.executable.startswith(conda_prefix) + if not managed_by_conda: # pragma: no cover + # When using a Python interpreter that does not come from a conda + # environment, we should ignore any system OpenMP library. + results = [r for r in results if r["user_api"] != "openmp"] + assert results == [] def test_command_line_command_flag(): @@ -600,12 +613,12 @@ expected_openblas_architectures = ( # XXX: add more as needed by CI or developer laptops "armv8", - "Haswell", - "Prescott", # see: https://github.com/xianyi/OpenBLAS/pull/3485 - "SkylakeX", - "Sandybridge", - "VORTEX", - "Zen", + "haswell", + "prescott", # see: https://github.com/xianyi/OpenBLAS/pull/3485 + "skylakex", + "sandybridge", + "vortex", + "zen", ) expected_blis_architectures = ( # XXX: add more as needed by CI or developer laptops @@ -614,9 +627,9 @@ ) for lib_info in threadpool_info(): if lib_info["internal_api"] == "openblas": - assert lib_info["architecture"] in expected_openblas_architectures + assert lib_info["architecture"].lower() in expected_openblas_architectures elif lib_info["internal_api"] == "blis": - assert lib_info["architecture"] in expected_blis_architectures + assert lib_info["architecture"].lower() in expected_blis_architectures else: # Not supported for other libraries assert "architecture" not in lib_info @@ -641,6 +654,81 @@ assert threading_layer in expected_openblas_threading_layers +# skip test if not run in a azure pipelines job since it relies on a specific flexiblas +# installation. +@pytest.mark.skipif( + "TF_BUILD" not in os.environ, reason="not running in azure pipelines" +) +def test_flexiblas(): + # Check that threadpool_info correctly recovers the FlexiBLAS backends. + flexiblas_controller = ThreadpoolController().select(internal_api="flexiblas") + + if not flexiblas_controller: + pytest.skip("requires FlexiBLAS.") + flexiblas_controller = flexiblas_controller.lib_controllers[0] + + expected_backends = {"NETLIB", "OPENBLAS_CONDA"} + expected_backends_loaded = {"OPENBLAS_CONDA"} + expected_current_backend = "OPENBLAS_CONDA" # set as default at build time + + flexiblas_backends = flexiblas_controller.available_backends + flexiblas_backends_loaded = flexiblas_controller.loaded_backends + current_backend = flexiblas_controller.current_backend + + assert set(flexiblas_backends) == expected_backends + assert set(flexiblas_backends_loaded) == expected_backends_loaded + assert current_backend == expected_current_backend + + +def test_flexiblas_switch_error(): + # Check that an error is raised when trying to switch to an invalid backend. + flexiblas_controller = ThreadpoolController().select(internal_api="flexiblas") + + if not flexiblas_controller: + pytest.skip("requires FlexiBLAS.") + flexiblas_controller = flexiblas_controller.lib_controllers[0] + + with pytest.raises(RuntimeError, match="Failed to load backend"): + flexiblas_controller.switch_backend("INVALID_BACKEND") + + +# skip test if not run in a azure pipelines job since it relies on a specific flexiblas +# installation. +@pytest.mark.skipif( + "TF_BUILD" not in os.environ, reason="not running in azure pipelines" +) +def test_flexiblas_switch(): + # Check that the backend can be switched. + controller = ThreadpoolController() + fb_controller = controller.select(internal_api="flexiblas") + + if not fb_controller: + pytest.skip("requires FlexiBLAS.") + fb_controller = fb_controller.lib_controllers[0] + + # at first mkl is not loaded in the CI jobs where this test runs + assert len(controller.select(internal_api="mkl").lib_controllers) == 0 + + # at first, only "OPENBLAS_CONDA" is loaded + assert fb_controller.current_backend == "OPENBLAS_CONDA" + assert fb_controller.loaded_backends == ["OPENBLAS_CONDA"] + + fb_controller.switch_backend("NETLIB") + assert fb_controller.current_backend == "NETLIB" + assert fb_controller.loaded_backends == ["OPENBLAS_CONDA", "NETLIB"] + + ext = ".so" if sys.platform == "linux" else ".dylib" + mkl_path = f"{os.getenv('CONDA_PREFIX')}/lib/libmkl_rt{ext}" + fb_controller.switch_backend(mkl_path) + assert fb_controller.current_backend == mkl_path + assert fb_controller.loaded_backends == ["OPENBLAS_CONDA", "NETLIB", mkl_path] + # switching the backend triggered a new search for loaded shared libs + assert len(controller.select(internal_api="mkl").lib_controllers) == 1 + + # switch back to default to avoid side effects + fb_controller.switch_backend("OPENBLAS_CONDA") + + def test_threadpool_controller_as_decorator(): # Check that using the decorator can be nested and is restricted to the scope of # the decorated function. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/tests/utils.py new/threadpoolctl-3.4.0/tests/utils.py --- old/threadpoolctl-3.2.0/tests/utils.py 2021-09-29 18:23:34.090675000 +0200 +++ new/threadpoolctl-3.4.0/tests/utils.py 2024-02-07 13:42:18.877825700 +0100 @@ -47,6 +47,12 @@ cython_extensions_compiled = False +try: + from tests._openmp_test_helper.nested_prange_blas import check_nested_prange_blas +except ImportError: + check_nested_prange_blas = None + + def threadpool_info_from_subprocess(module): """Utility to call threadpool_info in a subprocess diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/threadpoolctl-3.2.0/threadpoolctl.py new/threadpoolctl-3.4.0/threadpoolctl.py --- old/threadpoolctl-3.2.0/threadpoolctl.py 2023-07-13 16:44:13.629080800 +0200 +++ new/threadpoolctl-3.4.0/threadpoolctl.py 2024-03-20 14:36:20.768322000 +0100 @@ -4,6 +4,7 @@ thread pools (notably BLAS and OpenMP implementations) and dynamically set the maximal number of threads they can use. """ + # License: BSD 3-Clause # The code to introspect dynamically loaded libraries on POSIX systems is @@ -22,7 +23,7 @@ from functools import lru_cache from contextlib import ContextDecorator -__version__ = "3.2.0" +__version__ = "3.4.0" __all__ = [ "threadpool_limits", "threadpool_info", @@ -104,20 +105,17 @@ """ @final - def __init__(self, *, filepath=None, prefix=None): + def __init__(self, *, filepath=None, prefix=None, parent=None): """This is not meant to be overriden by subclasses.""" + self.parent = parent self.prefix = prefix self.filepath = filepath self.dynlib = ctypes.CDLL(filepath, mode=_RTLD_NOLOAD) self.version = self.get_version() self.set_additional_attributes() - @final def info(self): - """Return relevant info wrapped in a dict - - This is not meant to be overriden by subclasses. - """ + """Return relevant info wrapped in a dict""" exposed_attrs = { "user_api": self.user_api, "internal_api": self.internal_api, @@ -125,6 +123,7 @@ **vars(self), } exposed_attrs.pop("dynlib") + exposed_attrs.pop("parent") return exposed_attrs def set_additional_attributes(self): @@ -157,7 +156,18 @@ user_api = "blas" internal_api = "openblas" filename_prefixes = ("libopenblas", "libblas") - check_symbols = ("openblas_get_num_threads", "openblas_get_num_threads64_") + check_symbols = ( + "openblas_get_num_threads", + "openblas_get_num_threads64_", + "openblas_set_num_threads", + "openblas_set_num_threads64_", + "openblas_get_config", + "openblas_get_config64_", + "openblas_get_parallel", + "openblas_get_parallel64_", + "openblas_get_corename", + "openblas_get_corename64_", + ) def set_additional_attributes(self): self.threading_layer = self._get_threading_layer() @@ -237,7 +247,15 @@ user_api = "blas" internal_api = "blis" filename_prefixes = ("libblis", "libblas") - check_symbols = ("bli_thread_get_num_threads",) + check_symbols = ( + "bli_thread_get_num_threads", + "bli_thread_set_num_threads", + "bli_info_get_version_str", + "bli_info_get_enable_openmp", + "bli_info_get_enable_pthreads", + "bli_arch_query_id", + "bli_arch_string", + ) def set_additional_attributes(self): self.threading_layer = self._get_threading_layer() @@ -266,9 +284,9 @@ def _get_threading_layer(self): """Return the threading layer of BLIS""" - if self.dynlib.bli_info_get_enable_openmp(): + if getattr(self.dynlib, "bli_info_get_enable_openmp", lambda: False)(): return "openmp" - elif self.dynlib.bli_info_get_enable_pthreads(): + elif getattr(self.dynlib, "bli_info_get_enable_pthreads", lambda: False)(): return "pthreads" return "disabled" @@ -286,13 +304,146 @@ return bli_arch_string(bli_arch_query_id()).decode("utf-8") +class FlexiBLASController(LibController): + """Controller class for FlexiBLAS""" + + user_api = "blas" + internal_api = "flexiblas" + filename_prefixes = ("libflexiblas",) + check_symbols = ( + "flexiblas_get_num_threads", + "flexiblas_set_num_threads", + "flexiblas_get_version", + "flexiblas_list", + "flexiblas_list_loaded", + "flexiblas_current_backend", + ) + + @property + def loaded_backends(self): + return self._get_backend_list(loaded=True) + + @property + def current_backend(self): + return self._get_current_backend() + + def info(self): + """Return relevant info wrapped in a dict""" + # We override the info method because the loaded and current backends + # are dynamic properties + exposed_attrs = super().info() + exposed_attrs["loaded_backends"] = self.loaded_backends + exposed_attrs["current_backend"] = self.current_backend + + return exposed_attrs + + def set_additional_attributes(self): + self.available_backends = self._get_backend_list(loaded=False) + + def get_num_threads(self): + get_func = getattr(self.dynlib, "flexiblas_get_num_threads", lambda: None) + num_threads = get_func() + # by default BLIS is single-threaded and get_num_threads + # returns -1. We map it to 1 for consistency with other libraries. + return 1 if num_threads == -1 else num_threads + + def set_num_threads(self, num_threads): + set_func = getattr( + self.dynlib, "flexiblas_set_num_threads", lambda num_threads: None + ) + return set_func(num_threads) + + def get_version(self): + get_version_ = getattr(self.dynlib, "flexiblas_get_version", None) + if get_version_ is None: + return None + + major = ctypes.c_int() + minor = ctypes.c_int() + patch = ctypes.c_int() + get_version_(ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch)) + return f"{major.value}.{minor.value}.{patch.value}" + + def _get_backend_list(self, loaded=False): + """Return the list of available backends for FlexiBLAS. + + If loaded is False, return the list of available backends from the FlexiBLAS + configuration. If loaded is True, return the list of actually loaded backends. + """ + func_name = f"flexiblas_list{'_loaded' if loaded else ''}" + get_backend_list_ = getattr(self.dynlib, func_name, None) + if get_backend_list_ is None: + return None + + n_backends = get_backend_list_(None, 0, 0) + + backends = [] + for i in range(n_backends): + backend_name = ctypes.create_string_buffer(1024) + get_backend_list_(backend_name, 1024, i) + if backend_name.value.decode("utf-8") != "__FALLBACK__": + # We don't know when to expect __FALLBACK__ but it is not a real + # backend and does not show up when running flexiblas list. + backends.append(backend_name.value.decode("utf-8")) + return backends + + def _get_current_backend(self): + """Return the backend of FlexiBLAS""" + get_backend_ = getattr(self.dynlib, "flexiblas_current_backend", None) + if get_backend_ is None: + return None + + backend = ctypes.create_string_buffer(1024) + get_backend_(backend, ctypes.sizeof(backend)) + return backend.value.decode("utf-8") + + def switch_backend(self, backend): + """Switch the backend of FlexiBLAS + + Parameters + ---------- + backend : str + The name or the path to the shared library of the backend to switch to. If + the backend is not already loaded, it will be loaded first. + """ + if backend not in self.loaded_backends: + if backend in self.available_backends: + load_func = getattr(self.dynlib, "flexiblas_load_backend", lambda _: -1) + else: # assume backend is a path to a shared library + load_func = getattr( + self.dynlib, "flexiblas_load_backend_library", lambda _: -1 + ) + res = load_func(str(backend).encode("utf-8")) + if res == -1: + raise RuntimeError( + f"Failed to load backend {backend!r}. It must either be the name of" + " a backend available in the FlexiBLAS configuration " + f"{self.available_backends} or the path to a valid shared library." + ) + + # Trigger a new search of loaded shared libraries since loading a new + # backend caused a dlopen. + self.parent._load_libraries() + + switch_func = getattr(self.dynlib, "flexiblas_switch", lambda _: -1) + idx = self.loaded_backends.index(backend) + res = switch_func(idx) + if res == -1: + raise RuntimeError(f"Failed to switch to backend {backend!r}.") + + class MKLController(LibController): """Controller class for MKL""" user_api = "blas" internal_api = "mkl" filename_prefixes = ("libmkl_rt", "mkl_rt", "libblas") - check_symbols = ("MKL_Get_Max_Threads",) + check_symbols = ( + "MKL_Get_Max_Threads", + "MKL_Set_Num_Threads", + "MKL_Get_Version_String", + "MKL_Set_Threading_Layer", + ) def set_additional_attributes(self): self.threading_layer = self._get_threading_layer() @@ -343,6 +494,10 @@ user_api = "openmp" internal_api = "openmp" filename_prefixes = ("libiomp", "libgomp", "libomp", "vcomp") + check_symbols = ( + "omp_get_max_threads", + "omp_get_num_threads", + ) def get_num_threads(self): get_func = getattr(self.dynlib, "omp_get_max_threads", lambda: None) @@ -359,7 +514,13 @@ # Controllers for the libraries that we'll look for in the loaded libraries. # Third party libraries can register their own controllers. -_ALL_CONTROLLERS = [OpenBLASController, BLISController, MKLController, OpenMPController] +_ALL_CONTROLLERS = [ + OpenBLASController, + BLISController, + MKLController, + OpenMPController, + FlexiBLASController, +] # Helpers for the doc and test names _ALL_USER_APIS = list(set(lib.user_api for lib in _ALL_CONTROLLERS)) @@ -818,6 +979,8 @@ self._find_libraries_with_dyld() elif sys.platform == "win32": self._find_libraries_with_enum_process_module_ex() + elif "pyodide" in sys.modules: + self._find_libraries_pyodide() else: self._find_libraries_with_dl_iterate_phdr() @@ -833,6 +996,10 @@ """ libc = self._get_libc() if not hasattr(libc, "dl_iterate_phdr"): # pragma: no cover + warnings.warn( + "Could not find dl_iterate_phdr in the C standard library.", + RuntimeWarning, + ) return [] # Callback function for `dl_iterate_phdr` which is called for every @@ -865,6 +1032,10 @@ """ libc = self._get_libc() if not hasattr(libc, "_dyld_image_count"): # pragma: no cover + warnings.warn( + "Could not find _dyld_image_count in the C standard library.", + RuntimeWarning, + ) return [] n_dyld = libc._dyld_image_count() @@ -939,6 +1110,33 @@ finally: kernel_32.CloseHandle(h_process) + def _find_libraries_pyodide(self): + """Pyodide specific implementation for finding loaded libraries. + + Adapted from suggestion in https://github.com/joblib/threadpoolctl/pull/169#issuecomment-1946696449. + + One day, we may have a simpler solution. libc dl_iterate_phdr needs to + be implemented in Emscripten and exposed in Pyodide, see + https://github.com/emscripten-core/emscripten/issues/21354 for more + details. + """ + try: + from pyodide_js._module import LDSO + except ImportError: + warnings.warn( + "Unable to import LDSO from pyodide_js._module. This should never " + "happen." + ) + return + + for filepath in LDSO.loadedLibsByName.as_object_map(): + # Some libraries are duplicated by Pyodide and do not exist in the + # filesystem, so we first check for the existence of the file. For + # more details, see + # https://github.com/joblib/threadpoolctl/pull/169#issuecomment-1947946728 + if os.path.exists(filepath): + self._make_controller_from_path(filepath) + def _make_controller_from_path(self, filepath): """Store a library controller if it is supported and selected""" # Required to resolve symlinks @@ -978,11 +1176,24 @@ # duplicate entry in threadpool_info. continue - # filename matches a prefix. Create and store the library + # filename matches a prefix. Now we check if the library has the symbols we + # are looking for. If none of the symbols exists, it's very likely not the + # expected library (e.g. a library having a common prefix with one of the + # our supported libraries). Otherwise, create and store the library # controller. + lib_controller = controller_class( + filepath=filepath, prefix=prefix, parent=self + ) - lib_controller = controller_class(filepath=filepath, prefix=prefix) - self.lib_controllers.append(lib_controller) + if filepath in (lib.filepath for lib in self.lib_controllers): + # We already have a controller for this library. + continue + + if not hasattr(controller_class, "check_symbols") or any( + hasattr(lib_controller.dynlib, func) + for func in controller_class.check_symbols + ): + self.lib_controllers.append(lib_controller) def _check_prefix(self, library_basename, filename_prefixes): """Return the prefix library_basename starts with @@ -997,7 +1208,8 @@ def _warn_if_incompatible_openmp(self): """Raise a warning if llvm-OpenMP and intel-OpenMP are both loaded""" prefixes = [lib_controller.prefix for lib_controller in self.lib_controllers] - msg = textwrap.dedent(""" + msg = textwrap.dedent( + """ Found Intel OpenMP ('libiomp') and LLVM OpenMP ('libomp') loaded at the same time. Both libraries are known to be incompatible and this can cause random crashes or deadlocks on Linux when loaded in the @@ -1005,7 +1217,8 @@ Using threadpoolctl may cause crashes or deadlocks. For more information and possible workarounds, please see https://github.com/joblib/threadpoolctl/blob/master/multiple_openmp.md - """) + """ + ) if "libomp" in prefixes and "libiomp" in prefixes: warnings.warn(msg, RuntimeWarning) @@ -1014,16 +1227,13 @@ """Load the lib-C for unix systems.""" libc = cls._system_libraries.get("libc") if libc is None: - libc_name = find_library("c") - if libc_name is None: # pragma: no cover - warnings.warn( - "libc not found. The ctypes module in Python" - f" {sys.version_info.major}.{sys.version_info.minor} is maybe" - " too old for this OS.", - RuntimeWarning, - ) - return None - libc = ctypes.CDLL(libc_name, mode=_RTLD_NOLOAD) + # Remark: If libc is statically linked or if Python is linked against an + # alternative implementation of libc like musl, find_library will return + # None and CDLL will load the main program itself which should contain the + # libc symbols. We still name it libc for convenience. + # If the main program does not contain the libc symbols, it's ok because + # we check their presence later anyway. + libc = ctypes.CDLL(find_library("c"), mode=_RTLD_NOLOAD) cls._system_libraries["libc"] = libc return libc