pax_global_header 0000666 0000000 0000000 00000000064 15252261370 0014515 g ustar 00root root 0000000 0000000 52 comment=75e1860a91838a786cac4d03fbddb5ebca396c6b
joblib-threadpoolctl-804bc7e/ 0000775 0000000 0000000 00000000000 15252261370 0016236 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/.codecov.yml 0000664 0000000 0000000 00000000016 15252261370 0020456 0 ustar 00root root 0000000 0000000 comment: off
joblib-threadpoolctl-804bc7e/.coveragerc 0000664 0000000 0000000 00000000034 15252261370 0020354 0 ustar 00root root 0000000 0000000 [run]
source=threadpoolctl
joblib-threadpoolctl-804bc7e/.github/ 0000775 0000000 0000000 00000000000 15252261370 0017576 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/.github/workflows/ 0000775 0000000 0000000 00000000000 15252261370 0021633 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/.github/workflows/publish-to-pypi.yml 0000664 0000000 0000000 00000007205 15252261370 0025427 0 ustar 00root root 0000000 0000000 name: Publish threadpoolctl 🎮 distribution 📦 to PyPI and TestPyPI
# Taken from:
# https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
on: push
jobs:
build:
name: Build distribution 📦
# Don't run on forked repositories
if: github.event.repository.fork != true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install pypa/build
run: python -m pip install build --user
- name: Build a binary wheel and a source tarball
run: python -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
retention-days: 1
publish-to-pypi:
name: >-
Publish threadpoolctl 🎮 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/threadpoolctl/
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
github-release:
name: >-
Sign the threadpoolctl 🎮 distribution 📦 with Sigstore
and upload them to GitHub Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for sigstore
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
"$GITHUB_REF_NAME"
--repo "$GITHUB_REPOSITORY"
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
"$GITHUB_REF_NAME" dist/**
--repo "$GITHUB_REPOSITORY"
publish-to-testpypi:
name: Publish threadpoolctl 🎮 distribution 📦 to TestPyPI
needs:
- build
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/project/threadpoolctl/
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
# skip-existing is required for .dev0 versions with fixed names but
# different contents. See also: https://github.com/pypa/flit/issues/257
skip-existing: true
repository-url: https://test.pypi.org/legacy/
joblib-threadpoolctl-804bc7e/.github/workflows/test.yml 0000664 0000000 0000000 00000022357 15252261370 0023346 0 ustar 00root root 0000000 0000000 name: CI
permissions:
contents: read
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Daily build at 1:00 AM UTC
- cron: "0 1 * * *"
# Cancel in-progress workflows when pushing
# a new commit on the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install black
run: |
pip install black==25.1.0
- name: Run black
run: |
black --check --diff .
testing:
needs: linting
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
# Windows env with numpy, scipy,MKL installed through conda-forge
- name: pylatest_conda_forge_mkl
os: windows-latest
PYTHON_VERSION: "*"
PACKAGER: "conda-forge"
BLAS: "mkl"
INSTALL_OPENCV: "true"
# Windows env with numpy, scipy, OpenBLAS installed through conda-forge
- name: pylatest_conda_forge_openblas
os: windows-latest
PYTHON_VERSION: "*"
PACKAGER: "conda-forge"
BLAS: "openblas"
INSTALL_OPENCV: "true"
# Windows env with numpy, scipy installed through conda default channel
- name: py311_conda
os: windows-latest
PYTHON_VERSION: "3.11"
PACKAGER: "conda"
INSTALL_OPENCV: "true"
# Windows env with numpy, scipy installed through pip
- name: py310_pip
os: windows-latest
PYTHON_VERSION: "3.10"
PACKAGER: "pip"
INSTALL_OPENCV: "true"
# MacOS env with OpenMP installed through homebrew
- name: py39_conda_homebrew_libomp
os: macos-latest
PYTHON_VERSION: "3.9"
PACKAGER: "conda"
BLAS: "openblas"
CC_OUTER_LOOP: "clang"
CC_INNER_LOOP: "clang"
INSTALL_LIBOMP: "homebrew"
INSTALL_OPENCV: "true"
# MacOS env with OpenBLAS and OpenMP installed through conda-forge compilers
- name: pylatest_conda_forge_clang_openblas
os: macos-latest
PYTHON_VERSION: "*"
PACKAGER: "conda-forge"
BLAS: "openblas"
INSTALL_LIBOMP: "conda-forge"
INSTALL_OPENCV: "true"
# MacOS env with FlexiBLAS
- name: pylatest_flexiblas
os: macos-latest
PYTHON_VERSION: "*"
INSTALL_BLAS: "flexiblas"
PLATFORM_SPECIFIC_PACKAGES: "llvm-openmp"
# Linux environments to test that packages that comes with Ubuntu 22.04
# are correctly handled.
- name: py39_ubuntu_atlas_gcc_gcc
os: ubuntu-22.04
PYTHON_VERSION: "3.9"
PACKAGER: "ubuntu"
APT_BLAS: "libatlas3-base libatlas-base-dev"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
- name: py39_ubuntu_openblas_gcc_gcc
os: ubuntu-22.04
PYTHON_VERSION: "3.9"
PACKAGER: "ubuntu"
APT_BLAS: "libopenblas-base libopenblas-dev"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
# Linux environment with development versions of numpy and scipy
- name: pylatest_pip_dev
os : ubuntu-latest
PACKAGER: "pip-dev"
PYTHON_VERSION: "*"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
INSTALL_OPENCV: "true"
# Linux + Python 3.9 and homogeneous runtime nesting.
- name: py39_conda_openblas_clang_clang
os: ubuntu-latest
PACKAGER: "conda"
PYTHON_VERSION: "3.9"
BLAS: "openblas"
CC_OUTER_LOOP: "clang-18"
CC_INNER_LOOP: "clang-18"
INSTALL_OPENCV: "true"
# Linux environment with MKL and Clang (known to be unsafe for
# threadpoolctl) to only test the warning from multiple OpenMP.
- name: pylatest_conda_mkl_clang_gcc
os: ubuntu-latest
PYTHON_VERSION: "*"
PACKAGER: "conda"
BLAS: "mkl"
CC_OUTER_LOOP: "clang-18"
CC_INNER_LOOP: "gcc"
TESTS: "libomp_libiomp_warning"
# Linux environment with MKL, safe for threadpoolctl.
- name: pylatest_conda_mkl_gcc_gcc
os: ubuntu-latest
PYTHON_VERSION: "*"
PACKAGER: "conda"
BLAS: "mkl"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
MKL_THREADING_LAYER: "INTEL"
INSTALL_OPENCV: "true"
# Linux + Python 3.11 with numpy / scipy installed with pip from PyPI
# and heterogeneous OpenMP runtimes.
- name: py311_pip_openblas_gcc_clang
os: ubuntu-latest
PACKAGER: "pip"
PYTHON_VERSION: "3.11"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "clang-18"
# Linux environment with numpy from conda-forge channel and openblas-openmp
- name: pylatest_conda_forge
os: ubuntu-latest
PACKAGER: "conda-forge"
PYTHON_VERSION: "*"
BLAS: "openblas"
OPENBLAS_THREADING_LAYER: "openmp"
INSTALL_OPENCV: "true"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
# Linux environment with no numpy and heterogeneous OpenMP runtimes.
- name: pylatest_conda_nonumpy_gcc_clang
os: ubuntu-latest
PACKAGER: "conda"
PYTHON_VERSION: "*"
NO_NUMPY: "true"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "clang-18"
# Linux environments with numpy linked to BLIS
- name: pylatest_blis_gcc_clang_openmp
os: ubuntu-latest
PYTHON_VERSION: "*"
INSTALL_BLAS: "blis"
BLIS_NUM_THREAEDS: "4"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
BLIS_CC: "clang-18"
BLIS_ENABLE_THREADING: "openmp"
- name: pylatest_blis_clang_gcc_pthreads
os: ubuntu-latest
PYTHON_VERSION: "*"
INSTALL_BLAS: "blis"
BLIS_NUM_THREADS: "4"
CC_OUTER_LOOP: "clang-18"
CC_INNER_LOOP: "clang-18"
BLIS_CC: "gcc-12"
BLIS_ENABLE_THREADING: "pthreads"
- name: pylatest_blis_no_threading
os: ubuntu-latest
PYTHON_VERSION: "*"
INSTALL_BLAS: "blis"
BLIS_NUM_THREADS: "1"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
BLIS_CC: "gcc-12"
BLIS_ENABLE_THREADING: "no"
# Linux env with FlexiBLAS
- name: pylatest_flexiblas
os: ubuntu-latest
PYTHON_VERSION: "*"
INSTALL_BLAS: "flexiblas"
PLATFORM_SPECIFIC_PACKAGES: "mkl"
CC_OUTER_LOOP: "gcc"
CC_INNER_LOOP: "gcc"
env: ${{ matrix }}
runs-on: ${{ matrix.os }}
defaults:
run:
# Need to use this shell to get conda working properly.
# See https://github.com/marketplace/actions/setup-miniconda#important
shell: 'bash -el {0}'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Restore ccache
if: matrix.INSTALL_BLAS == 'flexiblas'
uses: actions/cache@v4
with:
path: ~/.cache/ccache
key: ccache-flexiblas-${{ matrix.os }}-${{ hashFiles('continuous_integration/install_flexiblas.sh') }}
restore-keys: |
ccache-flexiblas-${{ matrix.os }}-
- name: Setup conda
uses: conda-incubator/setup-miniconda@v3
with:
auto-activate-base: true
auto-update-conda: true
miniforge-version: latest
- name: Install dependencies
run: |
./continuous_integration/install.sh
- name: Test library
run: |
./continuous_integration/run_tests.sh
- name: Upload test results
uses: actions/upload-artifact@v4
with:
# Requires a unique name for each job in the matrix of this run
name: test_result_${{github.run_id}}_${{ matrix.os }}_${{ matrix.name }}
path: test_result.xml
retention-days: 1
- name: Upload to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage.xml
# Meta-test to ensure that at least one of the above CI configurations had
# the necessary platform settings to execute each test without raising
# skipping.
meta_test:
needs: testing
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Checkout code
uses: actions/checkout@v3
- name: Download tests results
uses: actions/download-artifact@v4
with:
path: test_results
- name: Check no test always skipped
run: |
python continuous_integration/check_no_test_skipped.py test_results
joblib-threadpoolctl-804bc7e/.gitignore 0000664 0000000 0000000 00000000456 15252261370 0020233 0 ustar 00root root 0000000 0000000 # Python generated files
*.pyc
__pycache__
.cache
.pytest_cache
# Cython/C generated files
*.o
*.so
*.dylib
tests/_openmp_test_helper/*.c
# Python install files, build and release artifacts
*.egg-info/
build
dist
# Coverage data
.coverage
/htmlcov
# Developer tools
.vscode
# pytest
.pytest_cache
joblib-threadpoolctl-804bc7e/CHANGES.md 0000664 0000000 0000000 00000020763 15252261370 0017640 0 ustar 00root root 0000000 0000000 3.7.0 (2026-09-15)
==================
- Fixed an intermittent `OSError` on Windows when DLLs are loaded or unloaded
concurrently during library discovery (for example when importing conda-forge
OpenCV). On Python 3.14+, discovery uses `ctypes.util.dllist` when available.
If `dllist` raises `OSError`, threadpoolctl emits a `RuntimeWarning` instead
of crashing so the failure can be reported upstream with a minimal
reproducer. Older Pythons use a Toolhelp snapshot enumerator, with graceful
per-module fallbacks.
https://github.com/joblib/threadpoolctl/pull/219
- Added the ability to check whether a limiting API affects just the current
thread or the whole process. Mainly aimed at debugging and diagnostics, and
somewhat unreliable, it is therefore enabled by default only for command-line
usage.
https://github.com/joblib/threadpoolctl/pull/213
- Only warn about simultaneous `libomp` and `libiomp` usage on Linux, where the
incompatibility is known to cause crashes.
https://github.com/joblib/threadpoolctl/pull/222
- Fixed a deadlock triggered by getting or setting MKL's number of threads from
parallel threads when using MKL with libiomp (Intel threading) on Linux.
https://github.com/joblib/threadpoolctl/pull/228
- Going forward, setting the number of threads will only have a thread-local
impact if feasible (for example, at minimum the underlying library must
support this option, and many don't.)
https://github.com/joblib/threadpoolctl/pull/228
- For MKL, setting the number of threads is now thread-local, i.e. limiting the
number of threads won't impact MKL's thread pool size when using MKL in other
Python threads.
https://github.com/joblib/threadpoolctl/pull/228
- For OpenBLAS compiled with OpenMP on Linux and macOS, setting the number of
threads is now thread-local, i.e. won't impact OpenBLAS thread pool size in
other Python threads. On Windows behavior is likely process-wide, but this may
depend on how OpenBLAS was compiled with OpenMP.
https://github.com/joblib/threadpoolctl/pull/228
- Fix OpenBLAS detection for conda package on Windows
https://github.com/joblib/threadpoolctl/pull/240
- Fixed a deadlock on Linux when using threadpoolctl from multiple threads.
https://github.com/joblib/threadpoolctl/pull/243
- Start using Python 3.14's built-in support for listing shared libraries.
- On Linux, start using /proc/self/maps for listing shared libraries.
- Avoid importing ``ctypes.util`` on Linux (and load libc with
``ctypes.CDLL(None)``) so ``threadpool_info()`` does not create libffi
closures that can abort after ``os.fork()`` on some libffi builds.
https://github.com/joblib/threadpoolctl/pull/242
3.6.0 (2025-03-13)
==================
- Added support for libraries with a path longer than 260 on Windows. The supported path
length is now 10 times higher but not unlimited for security reasons.
https://github.com/joblib/threadpoolctl/pull/189
- Dropped official support for Python 3.8.
https://github.com/joblib/threadpoolctl/pull/186
https://github.com/joblib/threadpoolctl/pull/191
3.5.0 (2024-04-29)
==================
- Added support for the Scientific Python version of OpenBLAS
(https://github.com/MacPython/openblas-libs), which exposes symbols with different
names than the ones of the original OpenBLAS library.
https://github.com/joblib/threadpoolctl/pull/175
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)
==================
- Dropped support for Python 3.6 and 3.7.
- Added support for custom library controllers. Custom controllers must inherit from
the `threadpoolctl.LibController` class and be registered to threadpoolctl using the
`threadpoolctl.register` function.
https://github.com/joblib/threadpoolctl/pull/138
- A warning is raised on macOS when threadpoolctl finds both Intel OpenMP and LLVM
OpenMP runtimes loaded simultaneously by the same Python program. See details and
workarounds at https://github.com/joblib/threadpoolctl/blob/master/multiple_openmp.md.
https://github.com/joblib/threadpoolctl/pull/142
3.1.0 (2022-01-31)
==================
- Fixed a detection issue of the BLAS libraires packaged by conda-forge on Windows.
https://github.com/joblib/threadpoolctl/pull/112
- `threadpool_limits` and `ThreadpoolController.limit` now accept the string
"sequential_blas_under_openmp" for the `limits` parameter. It should only be used for
the specific case when one wants to have sequential BLAS calls within an OpenMP
parallel region. It takes into account the unexpected behavior of OpenBLAS with the
OpenMP threading layer.
https://github.com/joblib/threadpoolctl/pull/114
3.0.0 (2021-10-01)
==================
- New object `threadpooctl.ThreadpoolController` which holds controllers for all the
supported native libraries. The states of these libraries is accessible through the
`info` method (equivalent to `threadpoolctl.threadpool_info()`) and their number of
threads can be limited with the `limit` method which can be used as a context
manager (equivalent to `threadpoolctl.threadpool_limits()`). This is especially useful
to avoid searching through all loaded shared libraries each time.
https://github.com/joblib/threadpoolctl/pull/95
- Added support for OpenBLAS built for 64bit integers in Fortran.
https://github.com/joblib/threadpoolctl/pull/101
- Added the possibility to use `threadpoolctl.threadpool_limits` and
`threadpooctl.ThreadpoolController` as decorators through their `wrap` method.
https://github.com/joblib/threadpoolctl/pull/102
- Fixed an attribute error when using old versions of OpenBLAS or BLIS that are
missing version query functions.
https://github.com/joblib/threadpoolctl/pull/88
https://github.com/joblib/threadpoolctl/pull/91
- Fixed an attribute error when python is run with -OO.
https://github.com/joblib/threadpoolctl/pull/87
2.2.0 (2021-07-09)
==================
- `threadpoolctl.threadpool_info()` now reports the architecture of the CPU
cores detected by OpenBLAS (via `openblas_get_corename`) and BLIS (via
`bli_arch_query_id` and `bli_arch_string`).
- Fixed a bug when the version of MKL was not found. The
"version" field is now set to None in that case.
https://github.com/joblib/threadpoolctl/pull/82
2.1.0 (2020-05-29)
==================
- New commandline interface:
python -m threadpoolctl -i numpy
will try to import the `numpy` package and then return the output of
`threadpoolctl.threadpool_info()` on STDOUT formatted using the JSON
syntax. This makes it easier to quickly introspect a Python environment.
2.0.0 (2019-12-05)
==================
- Expose MKL, BLIS and OpenBLAS threading layer in information displayed by
`threadpool_info`. This information is referenced in the `threading_layer`
field.
https://github.com/joblib/threadpoolctl/pull/48
https://github.com/joblib/threadpoolctl/pull/60
- When threadpoolctl finds libomp (LLVM OpenMP) and libiomp (Intel OpenMP)
both loaded, a warning is raised to recall that using threadpoolctl with
this mix of OpenMP libraries may cause crashes or deadlocks.
https://github.com/joblib/threadpoolctl/pull/49
1.1.0 (2019-09-12)
==================
- Detect libraries referenced by symlinks (e.g. BLAS libraries from
conda-forge).
https://github.com/joblib/threadpoolctl/pull/34
- Add support for BLIS.
https://github.com/joblib/threadpoolctl/pull/23
- Breaking change: method `get_original_num_threads` on the `threadpool_limits`
context manager to cheaply access the initial state of the runtime:
- drop the `user_api` parameter;
- instead return a dict `{user_api: num_threads}`;
- fixed a bug when the limit parameter of `threadpool_limits` was set to
`None`.
https://github.com/joblib/threadpoolctl/pull/32
1.0.0 (2019-06-03)
==================
Initial release.
joblib-threadpoolctl-804bc7e/LICENSE 0000664 0000000 0000000 00000002743 15252261370 0017251 0 ustar 00root root 0000000 0000000 Copyright (c) 2019, threadpoolctl contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. joblib-threadpoolctl-804bc7e/README.md 0000664 0000000 0000000 00000055371 15252261370 0017530 0 ustar 00root root 0000000 0000000 # Thread-pool Controls [](https://github.com/joblib/threadpoolctl/actions?query=branch%3Amaster) [](https://codecov.io/gh/joblib/threadpoolctl)
Python helpers to limit the number of threads used in the
threadpool-backed of common native libraries used for scientific
computing and data science (e.g. BLAS and OpenMP).
Fine control of the underlying thread-pool size can be useful in
workloads that involve nested parallelism so as to mitigate
oversubscription issues.
Note that "limiting the number of threads" in practice involves a variety of
potential semantics depending on the underlying third-party library; see the
section on [semantics](#semantics) below.
## Installation
- For users, install the last published version from PyPI:
```bash
pip install threadpoolctl
```
- For contributors, install from the source repository in developer
mode:
```bash
pip install -r dev-requirements.txt
flit install --symlink
```
then you run the tests with pytest:
```bash
pytest
```
## Usage: Introspection and debugging
### Command Line Interface
Get a JSON description of thread-pools initialized when importing python
packages such as numpy or scipy for instance:
```
python -m threadpoolctl -i numpy scipy.linalg
[
{
"filepath": "/home/ogrisel/miniconda3/envs/tmp/lib/libmkl_rt.so",
"prefix": "libmkl_rt",
"user_api": "blas",
"internal_api": "mkl",
"version": "2019.0.4",
"num_threads": 2,
"threading_layer": "intel"
},
{
"filepath": "/home/ogrisel/miniconda3/envs/tmp/lib/libiomp5.so",
"prefix": "libiomp",
"user_api": "openmp",
"internal_api": "openmp",
"version": null,
"num_threads": 4
}
]
```
The JSON information is written on STDOUT. If some of the packages are missing,
a warning message is displayed on STDERR.
### Python Runtime Programmatic Introspection
Introspect the current state of the threadpool-enabled runtime libraries
that are loaded when importing Python packages:
```python
>>> from threadpoolctl import threadpool_info
>>> from pprint import pprint
>>> pprint(threadpool_info())
[]
>>> import numpy
>>> pprint(threadpool_info())
[{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libmkl_rt.so',
'internal_api': 'mkl',
'num_threads': 2,
'prefix': 'libmkl_rt',
'threading_layer': 'intel',
'user_api': 'blas',
'version': '2019.0.4'},
{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libiomp5.so',
'internal_api': 'openmp',
'num_threads': 4,
'prefix': 'libiomp',
'user_api': 'openmp',
'version': None}]
>>> import xgboost
>>> pprint(threadpool_info())
[{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libmkl_rt.so',
'internal_api': 'mkl',
'num_threads': 2,
'prefix': 'libmkl_rt',
'threading_layer': 'intel',
'user_api': 'blas',
'version': '2019.0.4'},
{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libiomp5.so',
'internal_api': 'openmp',
'num_threads': 4,
'prefix': 'libiomp',
'user_api': 'openmp',
'version': None},
{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libgomp.so.1.0.0',
'internal_api': 'openmp',
'num_threads': 4,
'prefix': 'libgomp',
'user_api': 'openmp',
'version': None}]
```
In the above example, `numpy` was installed from the default anaconda channel and comes
with MKL and its Intel OpenMP (`libiomp5`) implementation while `xgboost` was installed
from pypi.org and links against GNU OpenMP (`libgomp`) so both OpenMP runtimes are
loaded in the same Python program.
The state of these libraries is also accessible through the object oriented API:
```python
>>> from threadpoolctl import ThreadpoolController, threadpool_info
>>> from pprint import pprint
>>> import numpy
>>> controller = ThreadpoolController()
>>> pprint(controller.info())
[{'architecture': 'Haswell',
'filepath': '/home/jeremie/miniconda/envs/dev/lib/libopenblasp-r0.3.17.so',
'internal_api': 'openblas',
'num_threads': 4,
'prefix': 'libopenblas',
'threading_layer': 'pthreads',
'user_api': 'blas',
'version': '0.3.17'}]
>>> controller.info() == threadpool_info()
True
```
## Usage when not using Python threads: Restricting Controlled Library Thread Pool Sizes
There are two scenarios in which you might want to use `threadpoolctl`; each
requires you to use different APIs.
1. You do not expect to use any Python threads, so all the work will be started
directly from the main thread in the process. This is a simple case
where we can globally set thread limits.
2. You will be parallelizing work using a Python thread pool, and your goal is
therefore to limit controlled libraries' thread pool sizes when
concurrently called from Python threads. This case is a bit more
complex to handle properly and requires a bit more verbose code.
This section will cover the former case, and the latter is covered in the next
usage section.
### Setting the Maximum Size of Thread-Pools
Control the number of threads used by the underlying runtime libraries
in specific sections of your Python program:
```python
>>> from threadpoolctl import threadpool_limits
>>> import numpy as np
>>> with threadpool_limits(limits=1, user_api='blas'):
... # In this block, calls to blas implementation (like openblas or MKL)
... # will be limited to use only one thread. They can thus be used jointly
... # with thread-parallelism.
... a = np.random.randn(1000, 1000)
... a_squared = a @ a
```
The threadpools can also be controlled via the object oriented API, which is especially
useful to avoid searching through all the loaded shared libraries each time. It will
however not act on libraries loaded after the instantiation of the
`ThreadpoolController`:
```python
>>> from threadpoolctl import ThreadpoolController
>>> import numpy as np
>>> controller = ThreadpoolController()
>>> with controller.limit(limits=1, user_api='blas'):
... a = np.random.randn(1000, 1000)
... a_squared = a @ a
```
### Restricting the Limits to the Scope of a Function
`threadpool_limits` and `ThreadpoolController` can also be used as decorators to set
the maximum number of threads used by the supported libraries at a function level. The
decorators are accessible through their `wrap` method.
```python
>>> from threadpoolctl import ThreadpoolController, threadpool_limits
>>> import numpy as np
>>> controller = ThreadpoolController()
>>> @controller.wrap(limits=1, user_api='blas')
... # or @threadpool_limits.wrap(limits=1, user_api='blas')
... def my_func():
... # Inside this function, calls to blas implementation (like openblas or MKL)
... # will be limited to use only one thread.
... a = np.random.randn(1000, 1000)
... a_squared = a @ a
...
```
## Usage for Python threads: Restricting Controlled Library Thread Pool Sizes
This section covers APIs to use when you will be using Python thread pools to parallelize work.
The usage is more complicated than one might expect because the underlying APIs have a variety of different semantics, as [explained later in the docs](#semantics).
### Setting the Maximum Size of Thread-Pools, When Python Thread Pools Are Used
Limiting thread pool size in controlled libraries requires a two-step process.
**Importantly, each Python worker thread must also call a method to limit
controlled libraries in that thread.** With Python's
`concurrent.futures.ThreadPoolExecutor`, you can do so by passing in an
initializer function that will get called on thread startup.
```python
from threadpoolctl import threadpool_limits
from concurrent.futures import ThreadPoolExecutor
# This top-level limiter doesn't actually change the limits initially; it is
# there to ensure the limits are reset _after_ the Python thread pool is done.
# This is necessary because some underlying limiting APIs operate on a
# process-wide basis.
with threadpool_limits():
# Make sure each Python worker thread also calls threadpool_limits(). If
# you're using another thread pool class, you will need to do so some other
# way.
with ThreadPoolExecutor(4, initializer=lambda: threadpool_limits(limits=1)) as pool:
# ... run some BLAS-using code in the thread pool ...
pool.map(somefunc, someargs)
```
Whenever `threadpool_limits` is called, it needs to do some work (inspecting and getting access to third-party shared libraries) that can take some time.
To prevent the performance cost of doing this work every time, you can reuse a
`ThreadpoolController` object:
```python
from threadpoolctl import ThreadpoolController
# This won't have any side-effects:
CONTROLLER = ThreadpoolController()
with (
CONTROLLER.limit(),
ThreadPoolExecutor(4, initializer=lambda: CONTROLLER.limit(limits=1)) as pool,
):
# ... run some BLAS-using code in the thread pool ...
pool.map(somefunc, someargs)
# Later...
with (
CONTROLLER.limit(),
ThreadPoolExecutor(4, initializer=lambda: CONTROLLER.limit(limits=2)) as pool,
):
# ... run some BLAS-using code in the thread pool ...
pool.map(somefunc, someargs)
```
You can also operate without a context manager:
```python
from threadpoolctl import ThreadpoolController
CONTROLLER = ThreadpoolController()
try:
limiter = CONTROLLER.limit()
with ThreadPoolExecutor(
4, initializer=lambda: CONTROLLER.limit(limits=1)) as pool:
# ... run some BLAS-using code in the thread pool ...
pool.map(somefunc, someargs)
finally:
limiter.restore_original_limits()
```
### Switching Back And Forth Between Main Thread and Python Threads
Unfortunately not all controlled libraries providing limiting APIs that are
thread-specific, as detailed in the section on [semantics](#semantics) below.
Limiting some libraries' thread pool sizes can therefore impact the whole
process. This makes switching back and forth between running code that
uses these libraries in Python threads and running it in the main thread a bit
more complex: you need to set the limits each time you switch back and forth.
Let's say your computer has 4 cores, and you're using some OpenMP API.
```python
POOL = ThreadPoolExecutor(4)
CONTROLLER = ThreadpoolController()
# 1. Run some work in a Python thread pool (OpenMP effectively disabled).
with CONTROLLER.limit(limits=1):
def limit_then_do_work(*args, **kwargs):
# Set a limit on OpenMP in the current thread:
CONTROLLER.limit(limits=1)
# Do the actual work:
return do_real_work_with_openmp(*args, **kwargs)
results = POOL.map(limit_then_do_work, args)
# 2. Run some work with 4-threads OpenMP parallelism:
with CONTROLLER.limit(limits=4):
results2 = do_more_work_with_openmp(results)
# 3. Nest some OpenMP parallelism under Python-level parallelism:
with CONTROLLER.limit(limits=2):
def limit_then_do_work2(*args, **kwargs):
CONTROLLER.limit(limits=2)
return do_even_more_real_work_with_openmp(*args, **kwargs)
results3 = POOL.map(limit_then_do_work2, results2)
```
## Usage: Additional APIs and details
### 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.
However it can also be used to control the threadpool of other native libraries,
provided that they expose an API to get and set the limit on the number of threads.
For that, one must implement a controller for this library and register it to
`threadpoolctl`.
A custom controller must be a subclass of the `LibController` class and implement
the attributes and methods described in the docstring of `LibController`. Then this
new controller class must be registered using the `threadpoolctl.register` function.
An complete example can be found [here](
https://github.com/joblib/threadpoolctl/blob/master/tests/_pyMylib/__init__.py).
### Sequential BLAS within OpenMP parallel region
When one wants to have sequential BLAS calls within an OpenMP parallel region, it's
safer to set `limits="sequential_blas_under_openmp"` since setting `limits=1` and
`user_api="blas"` might not lead to the expected behavior in some configurations
(e.g. OpenBLAS with the OpenMP threading layer
https://github.com/xianyi/OpenBLAS/issues/2985).
### Known Limitations
- `threadpool_limits` can fail to limit the number of inner threads when nesting
parallel loops managed by distinct OpenMP runtime implementations (for instance
libgomp from GCC and libomp from clang/llvm or libiomp from ICC).
See the `test_openmp_nesting` function in [tests/test_threadpoolctl.py](
https://github.com/joblib/threadpoolctl/blob/master/tests/test_threadpoolctl.py)
for an example. More information can be found at:
https://github.com/jeremiedbb/Nested_OpenMP
Note however that this problem does not happen when `threadpool_limits` is
used to limit the number of threads used internally by BLAS calls that are
themselves nested under OpenMP parallel loops. `threadpool_limits` works as
expected, even if the inner BLAS implementation relies on a distinct OpenMP
implementation.
- Using Intel OpenMP (ICC) and LLVM OpenMP (clang) in the same Python program
under Linux is known to cause problems. See the following guide for more details
and workarounds:
https://github.com/joblib/threadpoolctl/blob/master/multiple_openmp.md
- Setting the maximum number of threads of the OpenMP and BLAS libraries has
inconsistent scope and semantics (thread-local vs process-wide) depending on
the underlying library. For more details see
https://github.com/joblib/threadpoolctl/issues/208
For example, if you're using OpenMP with libgomp (gcc) or libomp (clang), the
setting is thread-local and sets how many OpenMP threads will be started in
the current thread. On the other hand, with OpenBLAS with pthreads backend or
on Windows, the setting is process-wide and impacts the size of a process-wide
thread pool shared across all threads in the process.
##
Semantics of thread limiting
Setting the number of threads may seem like a simple operation, but in practice
it can do quite different things depending on the underlying third-party library
being limited.
### Kind of thread pool
* In some libraries, there is a shared process-wide thread pool. Setting the
number of threads changes the size of this shared pool.
* In other libraries, there is a thread pool per calling thread. So each Python
thread gets its own personal thread pool from the third-party library.
To give a concrete example: if you have 10 cores, and you're using OpenBLAS with
the `pthreads` backend on Linux, by default there is a single 10-worker pool of
threads created by OpenBLAS. If you start 10 Python threads, each calling BLAS
routines, all those Python threads will feed into that single 10-thread pool.
In total, you will have 20 threads running (10 Python, 10 OpenBLAS).
On the other hand, if you use MKL, each Python thread gets its own individual
pool of worker threads from MKL, so if each Python threads calls a BLAS routine
in MKL, you will get 10×10 = 100 MKL threads!
### Scope of limiting
If you have a third-party library that creates a thread pool per calling thread,
there is another question about what limiting the number of threads means: which
threads are affected by the limiting API?
* **All threads in the process:** In this case, setting the limits changes it
for all threads in the process. So if you limit from the main Python thread,
all other Python threads will have the same limit.
* **Current thread only:** Only the current thread's thread pool size will be
limited.
MKL for example has two APIs, covering both cases,
[`mkl_set_num_threads()`](https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2026-0/mkl-set-num-threads.html)
and [`mkl_set_num_threads_local()`](https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2026-0/mkl-set-num-threads-local.html) respectively.
### `threadpoolctl`'s policy for thread limiting
When there is a choice between different APIs, `threadpoolctl` will prefer APIs
that:
1. Have a per-thread worker pool (so each Python thread gets its own pool from
the underlying third-party library)
2. Only affect the current thread.
Current libraries where `threadpoolctl` explicitly makes this choice are:
* MKL, for all threading backends.
* OpenBLAS (v0.3.34 or later) when using the OpenMP backend, on Linux and macOS.
When using OpenMP, this is also the default on Linux and macOS. On Windows
OpenMP has a per-thread worker pool but the limiting API affects all threads in
the process, not just the current one.
### Checking the behavior of your installed libraries
When you run `python -m threadpoolctl` it will include the scope of the API
limit in the `"thread_limit_scope"` field, with the value of `"process"`
indicating the API affects the whole process and `"current_thread"` indicating a
per-thread thread pool. Information about the kind of limiting in the former
case (shared process-wide pool, or per-thread pool) is not included.
Here is example output for OpenBLAS with pthreads:
```shell-session
$ python -m threadpoolctl -i numpy
[
{
"user_api": "blas",
"internal_api": "openblas",
"num_threads": 12,
"prefix": "libscipy_openblas",
"version": "0.3.33.112.0",
"threading_layer": "pthreads",
"architecture": "Haswell",
"thread_limit_scope": "process"
}
]
```
You can also use another script to empirically determine both the kind and scope of the API. From a checkout of the [`threadpoolctl` GitHub repo](https://github.com/joblib/threadpoolctl):
```shell-session
$ python -m tests.empirical_scope_observation blas
== Observed behavior: blas ==
10x12 Python threads each running a parallel workload with a 2 thread limit.
Maximum number of observed threads (includes Python and blas threads): 53
Presumed API scope: process-wide shared thread pool
```
You can also call this script with an `openmp` argument instead of `blas`.
## Maintainers
To make a release:
- Create a PR to bump the version number (`__version__`) in `threadpoolctl.py` and
update the release date in `CHANGES.md`.
- Merge the PR and check that the `Publish threadpoolctl distribution to TestPyPI` job
of the `publish-to-pypi.yml` workflow successfully uploaded the wheel and source
distribution to Test PyPI.
- If everything is fine create a tag for the release and push it to github:
```bash
git tag -a X.Y.Z
git push git@github.com:joblib/threadpoolctl.git X.Y.Z
```
- Check that the `Publish threadpoolctl distribution to PyPI` job of the
`publish-to-pypi.yml` workflow successfully uploaded the wheel and source distribution
to PyPI this time.
- Create a PR for the release on the [conda-forge feedstock](https://github.com/conda-forge/threadpoolctl-feedstock) (or wait for the bot to make it).
- Publish the release on github.
If for some reason the steps above can't be achieved and a munual upload of the wheel
and source distribution is needed:
- Build the distribution archives:
```bash
pip install flit
flit build
```
- Upload the wheels and source distribution to PyPI using flit. Since PyPI doesn't
allow password authentication anymore, the username needs to be changed to the
generic name `__token__`:
```bash
FLIT_USERNAME=__token__ flit publish
```
and a PyPI token has to be passed in place of the password.
### Credits
The initial dynamic library introspection code was written by @anton-malakhov
for the smp package available at https://github.com/IntelPython/smp .
threadpoolctl extends this for other operating systems. Contrary to smp,
threadpoolctl does not attempt to limit the size of Python multiprocessing
pools (threads or processes) or set operating system-level CPU affinity
constraints: threadpoolctl only interacts with native libraries via their
public runtime APIs.
joblib-threadpoolctl-804bc7e/benchmarks/ 0000775 0000000 0000000 00000000000 15252261370 0020353 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/benchmarks/bench_context_manager_overhead.py 0000664 0000000 0000000 00000001536 15252261370 0027124 0 ustar 00root root 0000000 0000000 import time
from argparse import ArgumentParser
from pprint import pprint
from statistics import mean, stdev
from threadpoolctl import threadpool_info, threadpool_limits
parser = ArgumentParser(description="Measure threadpool_limits call overhead.")
parser.add_argument(
"--import",
dest="packages",
default=[],
nargs="+",
help="Python packages to import to load threadpool enabled libraries.",
)
parser.add_argument("--n-calls", type=int, default=100, help="Number of iterations")
args = parser.parse_args()
for package_name in args.packages:
__import__(package_name)
pprint(threadpool_info())
timings = []
for _ in range(args.n_calls):
t = time.time()
with threadpool_limits(limits=1):
pass
timings.append(time.time() - t)
print(f"Overhead per call: {mean(timings) * 1e3:.3f} +/-{stdev(timings) * 1e3:.3f} ms")
joblib-threadpoolctl-804bc7e/conftest.py 0000664 0000000 0000000 00000000201 15252261370 0020426 0 ustar 00root root 0000000 0000000 collect_ignore = [
"tests/empirical_scope_observation.py",
"tests/_openmp_test_helper.py",
"tests/_limit_blas.py",
]
joblib-threadpoolctl-804bc7e/continuous_integration/ 0000775 0000000 0000000 00000000000 15252261370 0023047 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/continuous_integration/build_test_ext.sh 0000775 0000000 0000000 00000001012 15252261370 0026416 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -xe
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
pushd tests/_pyMylib
rm -rf *.so *.o
gcc -c -Wall -Werror -fpic -o my_threaded_lib.o my_threaded_lib.c
gcc -shared -o my_threaded_lib.so my_threaded_lib.o
popd
fi
pushd tests/_openmp_test_helper
rm -rf *.c *.so *.dylib build/
python setup_inner.py build_ext -i
python setup_outer.py build_ext -i
# skip scipy required extension if no numpy
if [[ "$NO_NUMPY" != "true" ]]; then
python setup_nested_prange_blas.py build_ext -i
fi
popd
joblib-threadpoolctl-804bc7e/continuous_integration/check_no_test_skipped.py 0000664 0000000 0000000 00000003552 15252261370 0027755 0 ustar 00root root 0000000 0000000 """Check tests are not skipped in every ci job"""
from __future__ import print_function
import os
import sys
import xml.etree.ElementTree as ET
base_dir = sys.argv[1]
# dict {test: result} where result is False if the test was skipped in every
# job and True otherwise.
always_skipped = {}
for name in os.listdir(base_dir):
# all test result files are in base_dir/test_result_*/ dirs
if name.startswith("test_result_"):
print(f"> processing test result from job {name.replace('test_result_', '')}")
print(" > tests skipped:")
result_file = os.path.join(base_dir, name, "test_result.xml")
root = ET.parse(result_file).getroot()
# All tests are identified by the xml tag testcase.
for test in root.iter("testcase"):
test_name = test.attrib["name"]
skipped = any(child.tag == "skipped" for child in test)
if skipped:
print(" -", test_name)
if test_name in always_skipped:
always_skipped[test_name] &= skipped
else:
always_skipped[test_name] = skipped
print("\n------------------------------------------------------------------\n")
# List of tests that we don't want to fail the CI if they are skipped in
# every job. This is useful for tests that depend on specific versions of
# numpy or scipy and we don't want to pin old versions of these libraries.
SAFE_SKIPPED_TESTS = [
"test_multiple_shipped_openblas",
"test_threadpool_limits_by_prefix[1-libblas]",
"test_threadpool_limits_by_prefix[3-libblas]",
]
fail = False
for test, skipped in always_skipped.items():
if skipped:
if test in SAFE_SKIPPED_TESTS:
print(test, "was skipped in every job but it's fine to skip it")
else:
fail = True
print(test, "was skipped in every job")
if fail:
sys.exit(1)
joblib-threadpoolctl-804bc7e/continuous_integration/install.sh 0000775 0000000 0000000 00000011074 15252261370 0025057 0 ustar 00root root 0000000 0000000 #!/bin/bash
# License: BSD 3-Clause
set -xe
UNAMESTR=`uname`
# Install a recent version of clang and libomp if needed
# Only applicable to linux jobs
if [[ "$CC_OUTER_LOOP" == "clang-18" ]] || \
[[ "$CC_INNER_LOOP" == "clang-18" ]] || \
[[ "$BLIS_CC" == "clang-18" ]]
then
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 18
sudo apt-get install libomp-dev
fi
make_conda() {
CHANNEL="$1"
TO_INSTALL="$2"
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"
elif [[ "$INSTALL_LIBOMP" == "homebrew" ]]; then
# Install a compiler with a working openmp
HOMEBREW_NO_AUTO_UPDATE=1 brew install libomp
# enable OpenMP support for Apple-clang
export CC=/usr/bin/clang
export CPPFLAGS="$CPPFLAGS -Xpreprocessor -fopenmp"
export CFLAGS="$CFLAGS -I/opt/homebrew/opt/libomp/include"
export LDFLAGS="$LDFLAGS -Wl,-rpath,/opt/homebrew/opt/libomp/lib -L/opt/homebrew/opt/libomp/lib -lomp"
fi
fi
if [[ "$PYTHON_VERSION" == "*" ]]; then
# Avoid installing free-threaded python
TO_INSTALL="$TO_INSTALL python-gil"
fi
# Need to source conda.sh before first conda command
source "$CONDA/etc/profile.d/conda.sh"
# prevent mixing conda channels
conda config --set channel_priority strict
conda config --add channels $CHANNEL
conda update -n base conda conda-libmamba-solver -q --yes
conda config --set solver libmamba
conda create -n testenv -q --yes python=$PYTHON_VERSION $TO_INSTALL
conda activate testenv
}
if [[ "$PACKAGER" == "conda" ]]; then
TO_INSTALL=""
if [[ "$NO_NUMPY" != "true" ]]; then
TO_INSTALL="$TO_INSTALL numpy scipy"
if [[ -n "$BLAS" ]]; then
TO_INSTALL="$TO_INSTALL blas=*=$BLAS"
fi
fi
if [[ "$INSTALL_OPENCV" == "true" ]]; then
TO_INSTALL="$TO_INSTALL opencv"
fi
make_conda "defaults" "$TO_INSTALL"
elif [[ "$PACKAGER" == "conda-forge" ]]; then
TO_INSTALL="numpy scipy blas=*=$BLAS"
if [[ "$BLAS" == "openblas" && "$OPENBLAS_THREADING_LAYER" == "openmp" ]]; then
TO_INSTALL="$TO_INSTALL libopenblas=*=*openmp*"
fi
if [[ "$INSTALL_OPENCV" == "true" ]]; then
TO_INSTALL="$TO_INSTALL opencv"
fi
make_conda "conda-forge" "$TO_INSTALL"
elif [[ "$PACKAGER" == "pip" ]]; then
# Use conda to build an empty python env and then use pip to install
# numpy and scipy
make_conda "conda-forge" ""
if [[ "$NO_NUMPY" != "true" ]]; then
pip install numpy scipy
fi
if [[ "$INSTALL_OPENCV" == "true" ]]; then
pip install opencv-python
fi
elif [[ "$PACKAGER" == "pip-dev" ]]; then
# Use conda to build an empty python env and then use pip to install
# numpy and scipy dev versions
make_conda "conda-forge" ""
dev_anaconda_url=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
pip install --pre --upgrade --timeout=60 --extra-index $dev_anaconda_url numpy scipy
elif [[ "$PACKAGER" == "ubuntu" ]]; then
# Remove the ubuntu toolchain PPA that seems to be invalid:
# https://github.com/scikit-learn/scikit-learn/pull/13934
sudo add-apt-repository --remove ppa:ubuntu-toolchain-r/test
sudo apt-get update
sudo apt-get install python3-scipy python3-virtualenv $APT_BLAS
python3 -m virtualenv --system-site-packages --python=python3 testenv
source testenv/bin/activate
elif [[ "$INSTALL_BLAS" == "blis" ]]; then
TO_INSTALL="cython meson-python pkg-config"
make_conda "conda-forge" "$TO_INSTALL"
source ./continuous_integration/install_blis.sh
elif [[ "$INSTALL_BLAS" == "flexiblas" ]]; then
TO_INSTALL="ccache cython openblas $PLATFORM_SPECIFIC_PACKAGES meson-python pkg-config compilers"
make_conda "conda-forge" "$TO_INSTALL"
source ./continuous_integration/install_flexiblas.sh
fi
python -m pip install -v -q -r dev-requirements.txt
bash ./continuous_integration/build_test_ext.sh
# Check which BLAS is linked (only available on linux)
if [[ "$UNAMESTR" == "Linux" && "$NO_NUMPY" != "true" ]]; 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
joblib-threadpoolctl-804bc7e/continuous_integration/install_blis.sh 0000664 0000000 0000000 00000002117 15252261370 0026063 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -xe
# Install gcc 12 to build BLIS
if [[ "$BLIS_CC" == "gcc-12" ]]; then
sudo apt install gcc-12
fi
# step outside of threadpoolctl directory
pushd ..
ABS_PATH=$(pwd)
# build & install blis
mkdir BLIS_install
git clone https://github.com/flame/blis.git
pushd blis
./configure --prefix=$ABS_PATH/BLIS_install --enable-cblas --enable-threading=$BLIS_ENABLE_THREADING CC=$BLIS_CC auto
make -j4
make install
popd
# build & install numpy
git clone https://github.com/numpy/numpy.git
pushd numpy
git submodule update --init
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
export CFLAGS=-I$ABS_PATH/BLIS_install/include/blis
export LDFLAGS="-L$ABS_PATH/BLIS_install/lib -Wl,-rpath,$ABS_PATH/BLIS_install/lib"
popd
# back to threadpoolctl directory
popd
joblib-threadpoolctl-804bc7e/continuous_integration/install_flexiblas.sh 0000664 0000000 0000000 00000004500 15252261370 0027101 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -xe
# step outside of threadpoolctl directory
pushd ..
ABS_PATH=$(pwd)
# build & install FlexiBLAS
mkdir flexiblas_install
git clone https://github.com/mpimd-csc/flexiblas.git
pushd flexiblas
export CCACHE_DIR="${CCACHE_DIR:-$HOME/.cache/ccache}"
mkdir -p "$CCACHE_DIR"
ccache --max-size=500M
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.
FLEXIBLAS_LIB=$ABS_PATH/flexiblas_install/lib
cmake ../ -DCMAKE_INSTALL_PREFIX=$ABS_PATH"/flexiblas_install" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_INSTALL_RPATH=$FLEXIBLAS_LIB \
-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=TRUE \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_Fortran_COMPILER_LAUNCHER=ccache \
-DBLAS_AUTO_DETECT="OFF" \
-DEXTRA="OPENBLAS_CONDA" \
-DFLEXIBLAS_DEFAULT="OPENBLAS_CONDA" \
-DOPENBLAS_CONDA_LIBRARY=$CONDA_PREFIX"/lib/libopenblas"$EXTENSION \
make
make install
export LD_LIBRARY_PATH=$FLEXIBLAS_LIB${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
if [[ $(uname) == "Darwin" ]]; then
export DYLD_LIBRARY_PATH=$FLEXIBLAS_LIB${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}
fi
# 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
ccache -s || true
export CFLAGS=-I$ABS_PATH/flexiblas_install/include/flexiblas
export LDFLAGS="-L$FLEXIBLAS_LIB -Wl,-rpath,$FLEXIBLAS_LIB"
popd
# back to threadpoolctl directory
popd
joblib-threadpoolctl-804bc7e/continuous_integration/run_tests.sh 0000775 0000000 0000000 00000001613 15252261370 0025435 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -xe
if [[ "$PACKAGER" == conda* ]] || [[ -z "$PACKAGER" ]]; then
source "$CONDA/etc/profile.d/conda.sh"
conda activate testenv
conda list
elif [[ "$PACKAGER" == pip* ]]; then
# we actually use conda to install the base environment:
source "$CONDA/etc/profile.d/conda.sh"
conda activate testenv
pip list
elif [[ "$PACKAGER" == "ubuntu" ]]; then
source testenv/bin/activate
pip list
fi
# 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
# Introspect API scope:
PYTHONPATH=. python -X faulthandler tests/empirical_scope_observation.py blas
PYTHONPATH=. python -X faulthandler tests/empirical_scope_observation.py openmp
pytest -vlrxXs -W error -k "$TESTS" --junitxml=test_result.xml --cov=threadpoolctl --cov-report xml
joblib-threadpoolctl-804bc7e/dev-requirements.txt 0000664 0000000 0000000 00000000071 15252261370 0022274 0 ustar 00root root 0000000 0000000 flit
coverage
psutil
pytest
pytest-cov
cython
setuptools
joblib-threadpoolctl-804bc7e/multiple_openmp.md 0000664 0000000 0000000 00000007524 15252261370 0022001 0 ustar 00root root 0000000 0000000 # Multiple OpenMP Runtimes
## Context
OpenMP is an API specification for parallel programming. There are many
implementations of it, tied to a compiler most of the time:
- `libgomp` for GCC (GNU C/C++ Compiler),
- `libomp` for Clang (LLVM C/C++ Compiler),
- `libiomp` for ICC (Intel C/C++ Compiler),
- `vcomp` for MSVC (Microsoft Visual Studio C/C++ Compiler).
In general, it is not advised to have different OpenMP runtime libraries (or
even different copies of the same library) loaded at the same time in a
program. It's considered an undefined behavior. Fortunately it is not as bad as
it sounds in most situations.
However this situation is frequent in the Python ecosystem since you can
install packages compiled with different compilers (hence linked to different
OpenMP implementations) and import them together in a Python program.
A typical example is installing NumPy from Anaconda which is linked against MKL
(Intel's math library) and another package that uses multi-threading with OpenMP
directly in a compiled extension, as is the case in Scikit-learn (via Cython
`prange`), LightGBM and XGBoost (via pragmas in the C++ source code).
From our experience, **most OpenMP libraries can seamlessly coexist in a same
program**. For instance, on Linux, we never observed any issue between
`libgomp` and `libiomp`, which is the most common mix (NumPy with MKL + a
package compiled with GCC, the most widely used C compiler on that platform).
## Incompatibility between Intel OpenMP and LLVM OpenMP
The only unrecoverable incompatibility we encountered happens when loading a
mix of compiled extensions linked with **`libomp` (LLVM/Clang) and `libiomp`
(ICC), on Linux and macOS**, manifested by crashes or deadlocks. It can happen
even with the simplest OpenMP calls like getting the maximum number of threads
that will be used in a subsequent parallel region. A possible explanation is that
`libomp` is actually a fork of `libiomp` causing name colliding for instance.
Using `threadpoolctl` may crash your program in such a setting.
**Fortunately this problem is very rare**: at the time of writing, all major
binary distributions of Python packages for Linux use either GCC or ICC to
build the Python scientific packages. Therefore this problem would only happen
if some packagers decide to start shipping Python packages built with
LLVM/Clang instead of GCC (this is the case for instance with conda's default channel).
## Workarounds for Intel OpenMP and LLVM OpenMP case
As far as we know, the only workaround consists in making sure only of one of
the two incompatible OpenMP libraries is loaded. For example:
- Tell MKL (used by NumPy) to use another threading implementation instead of the Intel
OpenMP runtime. It can be the GNU OpenMP runtime on Linux or TBB on Linux and MacOS
for instance. This is done by setting the following environment variable:
export MKL_THREADING_LAYER=GNU
or, if TBB is installed:
export MKL_THREADING_LAYER=TBB
- Install a build of NumPy and SciPy linked against OpenBLAS instead of MKL.
This can be done for instance by installing NumPy and SciPy from PyPI:
pip install numpy scipy
from the conda-forge conda channel:
conda install -c conda-forge numpy scipy
or from the default conda channel:
conda install numpy scipy blas[build=openblas]
- Re-build your OpenMP-enabled extensions from source with GCC (or ICC) instead
of Clang if you want to keep on using NumPy/SciPy linked against MKL with the
default `libiomp`-based threading layer.
## References
The above incompatibility has been reported upstream to the LLVM and Intel
developers on the following public issue trackers/forums along with a minimal
reproducer written in C:
- https://bugs.llvm.org/show_bug.cgi?id=43565
- https://community.intel.com/t5/Intel-C-Compiler/Cannot-call-OpenMP-functions-from-libiomp-after-calling-from/m-p/1176406
joblib-threadpoolctl-804bc7e/pyproject.toml 0000664 0000000 0000000 00000001615 15252261370 0021155 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["flit_core >=3.2,<5"]
build-backend = "flit_core.buildapi"
[project]
name = "threadpoolctl"
authors = [{name = "Thomas Moreau", email = "thomas.moreau.2010@gmail.com"}]
readme = "README.md"
requires-python = ">=3.9"
license = "BSD-3-Clause"
dynamic = ["version", "description"]
classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
]
[project.urls]
homepage = "https://github.com/joblib/threadpoolctl"
[tool.black]
line-length = 88
target_version = ['py39', 'py310', 'py311', 'py312', 'py313']
preview = true
[tool.ruff]
line-length = 88 joblib-threadpoolctl-804bc7e/tests/ 0000775 0000000 0000000 00000000000 15252261370 0017400 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/tests/__init__.py 0000664 0000000 0000000 00000000000 15252261370 0021477 0 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/tests/_limit_blas.py 0000664 0000000 0000000 00000001066 15252261370 0022233 0 ustar 00root root 0000000 0000000 # Used by test_setting_limit_on_thread_local_blas_api_is_actually_thread_local()
from concurrent.futures import ThreadPoolExecutor
from time import sleep
import sys
import numpy as np
import threadpoolctl
ARR = np.ones((1500, 1500))
def in_thread(_):
with threadpoolctl.threadpool_limits(limits=int(sys.argv[1]), user_api="blas"):
ARR.dot(ARR)
# Make sure jobs are evenly distributed and don't end up in one thread.
sleep(0.01)
if __name__ == "__main__":
with ThreadPoolExecutor(2) as pool:
list(pool.map(in_thread, range(2)))
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/ 0000775 0000000 0000000 00000000000 15252261370 0023433 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/__init__.py 0000664 0000000 0000000 00000000000 15252261370 0025532 0 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/build_utils.py 0000664 0000000 0000000 00000001074 15252261370 0026326 0 ustar 00root root 0000000 0000000 import os
import sys
def set_cc_variables(var_name="CC"):
cc_var = os.environ.get(var_name)
if cc_var is not None:
os.environ["CC"] = cc_var
if sys.platform == "darwin":
os.environ["LDSHARED"] = cc_var + " -bundle -undefined dynamic_lookup"
else:
os.environ["LDSHARED"] = cc_var + " -shared"
return cc_var
def get_openmp_flag():
if sys.platform == "win32":
return ["/openmp"]
elif sys.platform == "darwin" and "openmp" in os.getenv("CPPFLAGS", ""):
return []
return ["-fopenmp"]
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/nested_prange_blas.pyx 0000664 0000000 0000000 00000002406 15252261370 0030016 0 ustar 00root root 0000000 0000000 cimport openmp
from cython.parallel import parallel, prange
import numpy as np
from scipy.linalg.cython_blas cimport dgemm
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
char* trans = 't'
char* no_trans = 'n'
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(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]
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/nested_prange_blas_custom.pyx 0000664 0000000 0000000 00000003175 15252261370 0031414 0 ustar 00root root 0000000 0000000 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]
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/openmp_helpers_inner.pxd 0000664 0000000 0000000 00000000057 15252261370 0030365 0 ustar 00root root 0000000 0000000 cdef int inner_openmp_loop(int) noexcept nogil
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/openmp_helpers_inner.pyx 0000664 0000000 0000000 00000001637 15252261370 0030417 0 ustar 00root root 0000000 0000000 cimport openmp
from cython.parallel import prange
def check_openmp_num_threads(int n):
"""Run a short parallel section with OpenMP
Return the number of threads that where effectively used by the
OpenMP runtime.
"""
cdef int num_threads = -1
with nogil:
num_threads = inner_openmp_loop(n)
return num_threads
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
OpenMP runtime.
This function is expected to run without the GIL and can be called
by an outer OpenMP / prange loop written in Cython in another file.
"""
cdef long n_sum = 0
cdef int i, num_threads
for i in prange(n):
num_threads = openmp.omp_get_num_threads()
n_sum += i
if n_sum != (n - 1) * n / 2:
# error
return -1
return num_threads
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/openmp_helpers_outer.pyx 0000664 0000000 0000000 00000001324 15252261370 0030433 0 ustar 00root root 0000000 0000000 cimport openmp
from cython.parallel import prange
from openmp_helpers_inner cimport inner_openmp_loop
def check_nested_openmp_loops(int n, nthreads=None):
"""Run a short parallel section with OpenMP with nested calls
The inner OpenMP loop has not necessarily been built/linked with the
same runtime OpenMP runtime.
"""
cdef:
int outer_num_threads = -1
int inner_num_threads = -1
int num_threads = nthreads or openmp.omp_get_max_threads()
int i
for i in prange(n, num_threads=num_threads, nogil=True):
inner_num_threads = inner_openmp_loop(n)
outer_num_threads = openmp.omp_get_num_threads()
return outer_num_threads, inner_num_threads
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/setup_inner.py 0000664 0000000 0000000 00000002076 15252261370 0026345 0 ustar 00root root 0000000 0000000 import os
from setuptools import Extension, setup
from Cython.Build import cythonize
from build_utils import set_cc_variables
from build_utils import get_openmp_flag
original_environ = os.environ.copy()
try:
# Make it possible to compile the 2 OpenMP enabled Cython extensions
# with different compilers and therefore different OpenMP runtimes.
inner_loop_cc_var = set_cc_variables("CC_INNER_LOOP")
openmp_flag = get_openmp_flag()
ext_modules = [
Extension(
"openmp_helpers_inner",
["openmp_helpers_inner.pyx"],
extra_compile_args=openmp_flag,
extra_link_args=openmp_flag,
)
]
setup(
name="_openmp_test_helper_inner",
ext_modules=cythonize(
ext_modules,
compiler_directives={
"language_level": 3,
"boundscheck": False,
"wraparound": False,
},
compile_time_env={"CC_INNER_LOOP": inner_loop_cc_var or "unknown"},
),
)
finally:
os.environ.update(original_environ)
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/setup_nested_prange_blas.py 0000664 0000000 0000000 00000002111 15252261370 0031037 0 ustar 00root root 0000000 0000000 import os
from setuptools import Extension, setup
from Cython.Build import cythonize
from build_utils import set_cc_variables
from build_utils import get_openmp_flag
original_environ = os.environ.copy()
try:
set_cc_variables("CC_OUTER_LOOP")
openmp_flag = get_openmp_flag()
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",
[filename],
extra_compile_args=openmp_flag,
extra_link_args=openmp_flag,
libraries=libraries,
)
]
setup(
name="_openmp_test_helper_nested_prange_blas",
ext_modules=cythonize(
ext_modules,
compiler_directives={
"language_level": 3,
"boundscheck": False,
"wraparound": False,
},
),
)
finally:
os.environ.update(original_environ)
joblib-threadpoolctl-804bc7e/tests/_openmp_test_helper/setup_outer.py 0000664 0000000 0000000 00000002076 15252261370 0026370 0 ustar 00root root 0000000 0000000 import os
from setuptools import Extension, setup
from Cython.Build import cythonize
from build_utils import set_cc_variables
from build_utils import get_openmp_flag
original_environ = os.environ.copy()
try:
# Make it possible to compile the 2 OpenMP enabled Cython extensions
# with different compilers and therefore different OpenMP runtimes.
outer_loop_cc_var = set_cc_variables("CC_OUTER_LOOP")
openmp_flag = get_openmp_flag()
ext_modules = [
Extension(
"openmp_helpers_outer",
["openmp_helpers_outer.pyx"],
extra_compile_args=openmp_flag,
extra_link_args=openmp_flag,
)
]
setup(
name="_openmp_test_helper_outer",
ext_modules=cythonize(
ext_modules,
compiler_directives={
"language_level": 3,
"boundscheck": False,
"wraparound": False,
},
compile_time_env={"CC_OUTER_LOOP": outer_loop_cc_var or "unknown"},
),
)
finally:
os.environ.update(original_environ)
joblib-threadpoolctl-804bc7e/tests/_pyMylib/ 0000775 0000000 0000000 00000000000 15252261370 0021164 5 ustar 00root root 0000000 0000000 joblib-threadpoolctl-804bc7e/tests/_pyMylib/__init__.py 0000664 0000000 0000000 00000004051 15252261370 0023275 0 ustar 00root root 0000000 0000000 import ctypes
from pathlib import Path
from threadpoolctl import LibController, register
path = Path(__file__).parent / "my_threaded_lib.so"
ctypes.CDLL(path)
class MyThreadedLibController(LibController):
# names for threadpoolctl's context filtering
user_api = "my_threaded_lib"
internal_api = "my_threaded_lib"
# Patterns to identify the name of the linked library to load.
# If a dynamic library with a matching filename is linked to the python
# process, it will be loaded as the `dynlib` attribute of the LibController
# 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`.
return getattr(self.dynlib, "mylib_get_num_threads")()
def set_num_threads(self, num_threads):
# This function limits the maximum number of threads,
# when `ThreadpoolController.limit` is called.
getattr(self.dynlib, "mylib_set_num_threads")(num_threads)
def get_version(self):
# This function returns the version of the linked library if it is exposed,
# which is reported as "version" by `ThreadpoolController.info`.
get_version = getattr(self.dynlib, "mylib_get_version")
get_version.restype = ctypes.c_char_p
return get_version().decode("utf-8")
def set_additional_attributes(self):
# This function is called during the initialization of the LibController.
# Additional information meant to be exposed by `ThreadpoolController.info`
# should be set here as attributes of the LibController instance.
self.some_attr = "some_value"
register(MyThreadedLibController)
joblib-threadpoolctl-804bc7e/tests/_pyMylib/my_threaded_lib.c 0000664 0000000 0000000 00000000371 15252261370 0024444 0 ustar 00root root 0000000 0000000 int NUM_THREADS = 42;
int* NUM_THREADS_p = &NUM_THREADS;
int mylib_get_num_threads(){
return *NUM_THREADS_p;
}
void mylib_set_num_threads(int num_threads){
*NUM_THREADS_p = num_threads;
}
char* mylib_get_version(){
return "2.0";
}
joblib-threadpoolctl-804bc7e/tests/empirical_scope_observation.py 0000664 0000000 0000000 00000006503 15252261370 0025527 0 ustar 00root root 0000000 0000000 """
Run this script to empirically determine if OpenMP and BLAS limiting API set
the size of a shared process-wide thread pool or a per-thread limit.
"""
import sys
from concurrent.futures import ThreadPoolExecutor
from os import cpu_count
from pprint import pprint
from time import time
from threading import Thread
from typing import Callable
import threadpoolctl
import psutil
def start_counting_threads() -> Callable[[], int]:
max_num_threads = 0
stop = False
def in_thread():
nonlocal max_num_threads, stop
process = psutil.Process()
while not stop:
max_num_threads = max(max_num_threads, process.num_threads())
thread = Thread(target=in_thread)
thread.start()
def finish():
nonlocal stop, max_num_threads
stop = True
thread.join()
return max_num_threads
return finish
def set_limits():
threadpoolctl.threadpool_limits(2)
print("== Library info ==")
pprint(threadpoolctl.threadpool_info(debugging_info=True))
print()
def blas(num_python_threads):
try:
import numpy as np
except ImportError:
print("BLAS not available")
return False
set_limits()
A = np.ones((1024, 1024))
def blas_math(_):
A.dot(A)
with ThreadPoolExecutor(num_python_threads) as pool:
t0 = time()
while time() - t0 < 5:
for _ in pool.map(blas_math, range(40)):
pass
return True
def openmp(num_python_threads):
try:
from tests._openmp_test_helper.openmp_helpers_inner import (
check_openmp_num_threads,
)
except ImportError:
print("OpenMP not available")
return False
set_limits()
def run_openmp(_):
check_openmp_num_threads(1000)
with ThreadPoolExecutor(num_python_threads) as pool:
t0 = time()
while time() - t0 < 5:
for _ in pool.map(run_openmp, range(40)):
pass
return True
def run(which: str) -> None:
if which == "blas":
func = blas
else:
func = openmp
num_python_threads = 10 * cpu_count()
count_threads = start_counting_threads()
if not func(num_python_threads):
count_threads()
return
max_num_threads = count_threads()
# We're creating os.cpu_count() * 10 Python threads, and asking for 2
# native threads. If number is close to number of Python threads, we'll
# assume process-wide shared thread pool. If the number is close to double
# the number of Python threads, we'll assume a thread pool per thread.
if max_num_threads < num_python_threads * 1.25:
scope = "process-wide shared thread pool"
elif max_num_threads > num_python_threads * 1.75:
scope = "thread pool per thread"
else:
scope = "not sure"
print(f"== Observed behavior: {which} ==")
print(
f"10x{cpu_count()} Python threads each running a parallel "
"workload with a 2 thread limit."
)
print(
f"Maximum number of observed threads (includes Python and {which} threads):",
max_num_threads,
)
print("Presumed API scope:", scope)
print()
if __name__ == "__main__":
if sys.argv[1] == "openmp":
run("openmp")
elif sys.argv[1] == "blas":
run("blas")
else:
raise SystemExit("Bad argument")
joblib-threadpoolctl-804bc7e/tests/test_api_introspection.py 0000664 0000000 0000000 00000006276 15252261370 0024555 0 ustar 00root root 0000000 0000000 """
Tests for get/set number of threads API introspection.
"""
import sys
from threading import local as threadlocal
from typing import Callable
import pytest
from threadpoolctl import (
LibController,
_determine_thread_limit_scope,
ThreadpoolController,
)
# Make sure we have some BLAS libraries loaded:
from . import utils as _
class FakeThreadLocalAPI(threadlocal):
"""Thread-local num threads setting API."""
def get(self) -> int:
return getattr(self, "num_threads", 17)
def set(self, n: int) -> None:
self.num_threads = n
class FakeProcesswideAPI:
"""Process-wide num threads setting API."""
def __init__(self, num_threads: int):
self.num_threads = num_threads
def get(self) -> int:
return self.num_threads
def set(self, n: int) -> None:
self.num_threads = n
def test_determine_thread_limit_scope_thread_local() -> None:
"""
Check ``_determine_thread_limit_scope()`` can correctly diagnose a trivial
thread-local implementation.
"""
api = FakeThreadLocalAPI()
assert _determine_thread_limit_scope(api.get, api.set) == "current_thread"
@pytest.mark.parametrize("default", [1, 17])
def test_determine_thread_limit_scope_processwide(default: int) -> None:
"""
Check ``_determine_thread_limit_scope()`` can correctly diagnose a trivial
process-wide implementation.
"""
api = FakeProcesswideAPI(default)
assert _determine_thread_limit_scope(api.get, api.set) == "process"
@pytest.mark.parametrize(
["select_filter", "expected_thread_limit_scope", "extra_check"],
[
(
{"internal_api": "openblas"},
"process",
# Different backends have different behavior; on Linux OpenMP is
# thread-local, for example, for libgomp/libomp at least. Since it
# depends on the specific OpenMP behavior, and since the goal here
# iin any case is to test _determine_thread_limit_scope()'s API
# specific ability to detect process-scoped APIs, we only check
# pthreads here.
lambda lib: lib.threading_layer == "pthreads",
),
(
{"user_api": "openmp"},
"current_thread",
# Windows OpenMP is process-wide:
lambda _lib: sys.platform in ("linux", "darwin"),
),
],
)
def test_api_scope(
select_filter: dict[str, str],
expected_thread_limit_scope: str,
extra_check: Callable[[LibController], bool],
) -> None:
"""
Check ``_determine_thread_limit_scope()`` against libraries with known
properties, to make sure it detects them correctly. The test is intended
to be of the function's behavior, not of the libraries.
"""
controller = ThreadpoolController().select(**select_filter)
if not controller.lib_controllers:
pytest.skip(f"{select_filter} controller not found")
libs = [lib for lib in controller.lib_controllers if extra_check(lib)]
if not libs:
pytest.skip("No libraries matched the requirements")
for lib in libs:
assert (
_determine_thread_limit_scope(lib.get_num_threads, lib.set_num_threads)
== expected_thread_limit_scope
)
joblib-threadpoolctl-804bc7e/tests/test_threadpoolctl.py 0000664 0000000 0000000 00000126754 15252261370 0023674 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ctypes
import json
import os
import pytest
import re
import subprocess
import shutil
import sys
from threading import Thread
import threadpoolctl
from threadpoolctl import threadpool_limits, threadpool_info
from threadpoolctl import LibController, ThreadpoolController
from threadpoolctl import _ALL_PREFIXES, _ALL_USER_APIS
from threadpoolctl import _determine_thread_limit_scope
from .utils import cython_extensions_compiled
from .utils import check_nested_prange_blas
from .utils import libopenblas_paths
from .utils import get_openblas_dll_path
from .utils import make_long_windows_path
from .utils import scipy
from .utils import threadpool_info_from_subprocess
from .utils import select
def stdlib_dllist():
"""Return ctypes.util.dllist when _load_libraries would use it, else None."""
if sys.platform in ("linux", "emscripten"):
return None
try:
from ctypes.util import dllist
except ImportError:
return None
return dllist
def is_old_openblas(lib_controller):
# Possible bug in getting maximum number of threads with OpenBLAS < 0.2.16
# and OpenBLAS does not expose its version before 0.3.4.
return lib_controller.internal_api == "openblas" and lib_controller.version is None
def skip_if_openblas_openmp():
"""Helper to skip tests with side effects when OpenBLAS has the OpenMP
threading layer.
"""
if any(
lib_controller.internal_api == "openblas"
and lib_controller.threading_layer == "openmp"
for lib_controller in ThreadpoolController().lib_controllers
):
pytest.skip(
"Setting a limit on OpenBLAS when using the OpenMP threading layer also "
"impact the OpenMP library. They can't be controlled independently."
)
def effective_num_threads(nthreads, max_threads):
if nthreads is None or nthreads > max_threads:
return max_threads
return nthreads
def test_threadpool_info():
# Check consistency between threadpool_info and ThreadpoolController
function_info = threadpool_info()
object_info = ThreadpoolController().lib_controllers
for lib_info, lib_controller in zip(function_info, object_info):
assert lib_info == lib_controller.info()
def test_threadpool_controller_info():
# Check that all keys expected for the private api are in the dicts
# returned by the `info` methods
controller = ThreadpoolController()
assert threadpool_info() == [
lib_controller.info() for lib_controller in controller.lib_controllers
]
assert controller.info() == [
lib_controller.info() for lib_controller in controller.lib_controllers
]
for lib_controller_dict in controller.info():
assert "user_api" in lib_controller_dict
assert "internal_api" in lib_controller_dict
assert "prefix" in lib_controller_dict
assert "filepath" in lib_controller_dict
assert "version" in lib_controller_dict
assert "num_threads" in lib_controller_dict
if lib_controller_dict["internal_api"] in ("mkl", "blis", "openblas"):
assert "threading_layer" in lib_controller_dict
def test_controller_info_actualized():
# Check that the num_threads attribute reflects the actual state of the threadpools
controller = ThreadpoolController()
original_info = controller.info()
with threadpool_limits(limits=1):
assert all(
lib_controller.num_threads == 1
for lib_controller in controller.lib_controllers
)
assert controller.info() == original_info
@pytest.mark.parametrize(
"kwargs",
[
{"user_api": "blas"},
{"prefix": "libgomp"},
{"internal_api": "openblas", "prefix": "libomp"},
{"prefix": ["libgomp", "libomp", "libiomp"]},
],
)
def test_threadpool_controller_select(kwargs):
# Check the behior of the select method of ThreadpoolController
controller = ThreadpoolController().select(**kwargs)
if not controller:
pytest.skip(f"Requires at least one of {list(kwargs.values())}.")
for lib_controller in controller.lib_controllers:
assert any(
getattr(lib_controller, key) in (val if isinstance(val, list) else [val])
for key, val in kwargs.items()
)
@pytest.mark.parametrize("prefix", _ALL_PREFIXES)
@pytest.mark.parametrize("limit", [1, 3])
def test_threadpool_limits_by_prefix(prefix, limit):
# Check that the maximum number of threads can be set by prefix
controller = ThreadpoolController()
original_info = controller.info()
controller_matching_prefix = controller.select(prefix=prefix)
if not controller_matching_prefix:
pytest.skip(f"Requires {prefix} runtime")
with threadpool_limits(limits={prefix: limit}):
for lib_controller in controller_matching_prefix.lib_controllers:
if is_old_openblas(lib_controller):
continue
# threadpool_limits only sets an upper bound on the number of
# threads.
assert 0 < lib_controller.num_threads <= limit
assert ThreadpoolController().info() == original_info
@pytest.mark.parametrize("user_api", (None, "blas", "openmp"))
@pytest.mark.parametrize("limit", [1, 3])
def test_set_threadpool_limits_by_api(user_api, limit):
# Check that the maximum number of threads can be set by user_api
controller = ThreadpoolController()
original_info = controller.info()
if user_api is None:
controller_matching_api = controller
else:
controller_matching_api = controller.select(user_api=user_api)
if not controller_matching_api:
user_apis = _ALL_USER_APIS if user_api is None else [user_api]
pytest.skip(f"Requires a library which api is in {user_apis}")
with threadpool_limits(limits=limit, user_api=user_api):
for lib_controller in controller_matching_api.lib_controllers:
if is_old_openblas(lib_controller):
continue
# threadpool_limits only sets an upper bound on the number of
# threads.
assert 0 < lib_controller.num_threads <= limit
assert ThreadpoolController().info() == original_info
def test_threadpool_limits_function_with_side_effect():
# Check that threadpool_limits can be used as a function with
# side effects instead of a context manager.
original_info = ThreadpoolController().info()
threadpool_limits(limits=1)
try:
for lib_controller in ThreadpoolController().lib_controllers:
if is_old_openblas(lib_controller):
continue
assert lib_controller.num_threads == 1
finally:
# Restore the original limits so that this test does not have any
# side-effect.
threadpool_limits(limits=original_info)
assert ThreadpoolController().info() == original_info
def test_set_threadpool_limits_no_limit():
# Check that limits=None does nothing.
original_info = ThreadpoolController().info()
with threadpool_limits(limits=None):
assert ThreadpoolController().info() == original_info
assert ThreadpoolController().info() == original_info
def test_threadpool_limits_manual_restore():
# Check that threadpool_limits can be used as an object which holds the
# original state of the threadpools and that can be restored thanks to the
# dedicated restore_original_limits method
original_info = ThreadpoolController().info()
limits = threadpool_limits(limits=1)
try:
for lib_controller in ThreadpoolController().lib_controllers:
if is_old_openblas(lib_controller):
continue
assert lib_controller.num_threads == 1
finally:
# Restore the original limits so that this test does not have any
# side-effect.
limits.restore_original_limits()
assert ThreadpoolController().info() == original_info
def test_threadpool_controller_limit():
# Check that using the limit method of ThreadpoolController only impact its
# library controllers.
# This is not True for OpenBLAS with the OpenMP threading layer.
skip_if_openblas_openmp()
blas_controller = ThreadpoolController().select(user_api="blas")
original_openmp_info = ThreadpoolController().select(user_api="openmp").info()
with blas_controller.limit(limits=1):
blas_controller = ThreadpoolController().select(user_api="blas")
openmp_info = ThreadpoolController().select(user_api="openmp").info()
assert all(
lib_controller.num_threads == 1
for lib_controller in blas_controller.lib_controllers
)
# original_blas_controller contains only blas libraries so no opemp library
# should be impacted.
assert openmp_info == original_openmp_info
def test_get_params_for_sequential_blas_under_openmp():
# Test for the behavior of get_params_for_sequential_blas_under_openmp.
controller = ThreadpoolController()
original_info = controller.info()
params = controller._get_params_for_sequential_blas_under_openmp()
if controller.select(
internal_api="openblas", threading_layer="openmp"
).lib_controllers:
assert params["limits"] is None
assert params["user_api"] is None
with controller.limit(limits="sequential_blas_under_openmp"):
assert controller.info() == original_info
else:
assert params["limits"] == 1
assert params["user_api"] == "blas"
with controller.limit(limits="sequential_blas_under_openmp"):
assert all(
lib_info["num_threads"] == 1
for lib_info in controller.info()
if lib_info["user_api"] == "blas"
)
def test_nested_limits():
# Check that exiting the context manager properly restores the original limits even
# when nested.
controller = ThreadpoolController()
original_info = controller.info()
if any(info["num_threads"] < 2 for info in original_info):
pytest.skip("Test requires at least 2 CPUs on host machine")
def check_num_threads(expected_num_threads):
assert all(
lib_controller.num_threads == expected_num_threads
for lib_controller in ThreadpoolController().lib_controllers
)
with controller.limit(limits=1):
check_num_threads(expected_num_threads=1)
with controller.limit(limits=2):
check_num_threads(expected_num_threads=2)
check_num_threads(expected_num_threads=1)
assert ThreadpoolController().info() == original_info
def test_threadpool_limits_bad_input():
# Check that appropriate errors are raised for invalid arguments
match = re.escape(f"user_api must be either in {_ALL_USER_APIS} or None.")
with pytest.raises(ValueError, match=match):
threadpool_limits(limits=1, user_api="wrong")
with pytest.raises(
TypeError, match="limits must either be an int, a list, a dict, or"
):
threadpool_limits(limits=(1, 2, 3))
@pytest.mark.skipif(
not cython_extensions_compiled, reason="Requires cython extensions to be compiled"
)
@pytest.mark.parametrize("num_threads", [1, 2, 4])
def test_openmp_limit_num_threads(num_threads):
# checks that OpenMP effectively uses the number of threads requested by
# the context manager
import tests._openmp_test_helper.openmp_helpers_inner as omp_inner
check_openmp_num_threads = omp_inner.check_openmp_num_threads
old_num_threads = check_openmp_num_threads(100)
with threadpool_limits(limits=num_threads):
assert check_openmp_num_threads(100) in (num_threads, old_num_threads)
assert check_openmp_num_threads(100) == old_num_threads
@pytest.mark.skipif(
not cython_extensions_compiled, reason="Requires cython extensions to be compiled"
)
@pytest.mark.parametrize("nthreads_outer", [None, 1, 2, 4])
def test_openmp_nesting(nthreads_outer):
# checks that OpenMP effectively uses the number of threads requested by
# the context manager when nested in an outer OpenMP loop.
import tests._openmp_test_helper.openmp_helpers_outer as omp_outer
check_nested_openmp_loops = omp_outer.check_nested_openmp_loops
# Find which OpenMP lib is used at runtime for inner loop
inner_info = threadpool_info_from_subprocess(
"tests._openmp_test_helper.openmp_helpers_inner"
)
assert len(inner_info) == 1
inner_omp = inner_info[0]["prefix"]
# Find which OpenMP lib is used at runtime for outer loop
outer_info = threadpool_info_from_subprocess(
"tests._openmp_test_helper.openmp_helpers_outer"
)
if len(outer_info) == 1:
# Only 1 openmp loaded. It has to be this one.
outer_omp = outer_info[0]["prefix"]
else:
# There are 2 openmp, the one from inner and the one from outer.
assert len(outer_info) == 2
# We already know the one from inner. It has to be the other one.
prefixes = {lib_info["prefix"] for lib_info in outer_info}
outer_omp = prefixes - {inner_omp}
outer_num_threads, inner_num_threads = check_nested_openmp_loops(10)
original_info = ThreadpoolController().info()
if inner_omp == outer_omp:
# The OpenMP runtime should be shared by default, meaning that the
# inner loop should automatically be run serially by the OpenMP runtime
assert inner_num_threads == 1
with threadpool_limits(limits=1) as threadpoolctx:
max_threads = threadpoolctx.get_original_num_threads()["openmp"]
nthreads = effective_num_threads(nthreads_outer, max_threads)
# Ask outer loop to run on nthreads threads and inner loop run on 1
# thread
outer_num_threads, inner_num_threads = check_nested_openmp_loops(10, nthreads)
# The state of the original state of all threadpools should have been
# restored.
assert ThreadpoolController().info() == original_info
# The number of threads available in the outer loop should not have been
# decreased:
assert outer_num_threads == nthreads
# The number of threads available in the inner loop should have been
# set to 1 to avoid oversubscription and preserve performance:
if inner_omp != outer_omp:
if inner_num_threads != 1:
# XXX: this does not always work when nesting independent openmp
# implementations. See: https://github.com/jeremiedbb/Nested_OpenMP
pytest.xfail(
f"Inner OpenMP num threads was {inner_num_threads} instead of 1"
)
assert inner_num_threads == 1
def test_shipped_openblas():
# checks that OpenBLAS effectively uses the number of threads requested by
# the context manager
original_info = ThreadpoolController().info()
openblas_controller = ThreadpoolController().select(internal_api="openblas")
with threadpool_limits(1):
for lib_controller in openblas_controller.lib_controllers:
assert lib_controller.num_threads == 1
assert ThreadpoolController().info() == original_info
@pytest.mark.skipif(
len(libopenblas_paths) < 2, reason="need at least 2 shipped openblas library"
)
def test_multiple_shipped_openblas():
# This redundant test is meant to make it easier to see if the system
# has 2 or more active openblas runtimes available just by reading the
# pytest report (whether or not this test has been skipped).
test_shipped_openblas()
@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 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
skip_if_openblas_openmp()
original_info = ThreadpoolController().info()
blas_controller = ThreadpoolController().select(user_api="blas")
blis_controller = ThreadpoolController().select(internal_api="blis")
# skip if the BLAS used by numpy is an old openblas. OpenBLAS 0.3.3 and
# older are known to cause an unrecoverable deadlock at process shutdown
# time (after pytest has exited).
# numpy can be linked to BLIS for CBLAS and OpenBLAS for LAPACK. In that
# case this test will run BLIS gemm so no need to skip.
if not blis_controller and any(
is_old_openblas(lib_controller)
for lib_controller in blas_controller.lib_controllers
):
pytest.skip("Old OpenBLAS: skipping test to avoid deadlock")
A = np.ones((1000, 10))
B = np.ones((100, 10))
with threadpool_limits(limits=1) as threadpoolctx:
max_threads = threadpoolctx.get_original_num_threads()["openmp"]
nthreads = effective_num_threads(nthreads_outer, max_threads)
result = check_nested_prange_blas(A, B, nthreads)
C, prange_num_threads, inner_info = result
assert np.allclose(C, np.dot(A, B.T))
assert prange_num_threads == nthreads
nested_blas_info = select(inner_info, user_api="blas")
assert len(nested_blas_info) == len(blas_controller.lib_controllers)
assert all(lib_info["num_threads"] == 1 for lib_info in nested_blas_info)
assert ThreadpoolController().info() == original_info
# the method `get_original_num_threads` raises a UserWarning due to different
# num_threads from libraries with the same `user_api`. It will be raised only
# in the CI job with 2 openblas (py38_pip_openblas_gcc_clang). It is expected
# so we can safely filter it.
@pytest.mark.filterwarnings("ignore::UserWarning")
@pytest.mark.parametrize("limit", [1, None])
def test_get_original_num_threads(limit):
# Tests the method get_original_num_threads of the context manager
with threadpool_limits(limits=2, user_api="blas") as ctx:
# set different blas num threads to start with (when multiple openblas)
if len(ctx._controller.select(user_api="blas")) > 1:
ctx._controller.lib_controllers[0].set_num_threads(1)
original_info = ThreadpoolController().info()
with threadpool_limits(limits=limit, user_api="blas") as threadpoolctx:
original_num_threads = threadpoolctx.get_original_num_threads()
assert "openmp" not in original_num_threads
blas_info = select(original_info, user_api="blas")
if blas_info:
expected = min(lib_info["num_threads"] for lib_info in blas_info)
assert original_num_threads["blas"] == expected
else:
assert original_num_threads["blas"] is None
if len(libopenblas_paths) >= 2:
with pytest.warns(UserWarning, match="Multiple values possible"):
threadpoolctx.get_original_num_threads()
def test_mkl_threading_layer():
# Check that threadpool_info correctly recovers the threading layer used
# by mkl
mkl_controller = ThreadpoolController().select(internal_api="mkl")
expected_layer = os.getenv("MKL_THREADING_LAYER")
if not (mkl_controller and expected_layer):
pytest.skip("requires MKL and the environment variable MKL_THREADING_LAYER set")
actual_layer = mkl_controller.lib_controllers[0].threading_layer
assert actual_layer == expected_layer.lower()
def test_blis_threading_layer():
# Check that threadpool_info correctly recovers the threading layer used
# by blis
blis_controller = ThreadpoolController().select(internal_api="blis")
expected_layer = os.getenv("BLIS_ENABLE_THREADING")
if expected_layer == "no":
expected_layer = "disabled"
if not (blis_controller and expected_layer):
pytest.skip(
"requires BLIS and the environment variable BLIS_ENABLE_THREADING set"
)
actual_layer = blis_controller.lib_controllers[0].threading_layer
assert actual_layer == expected_layer
@pytest.mark.skipif(
not cython_extensions_compiled, reason="Requires cython extensions to be compiled"
)
def test_libomp_libiomp_warning(recwarn):
# Trigger the import of a potentially clang-compiled extension:
import tests._openmp_test_helper.openmp_helpers_outer # noqa
# Trigger the import of numpy to potentially import Intel OpenMP via MKL
pytest.importorskip("numpy.linalg")
# Check that a warning is raised when both libomp and libiomp are loaded
# It should happen in one CI job (pylatest_conda_mkl_clang_gcc).
controller = ThreadpoolController()
prefixes = [lib_controller.prefix for lib_controller in controller.lib_controllers]
if not ("libomp" in prefixes and "libiomp" in prefixes and sys.platform == "linux"):
pytest.skip("Requires both libomp and libiomp loaded, on Linux")
assert len(recwarn) == 1
wm = recwarn[0]
assert wm.category == RuntimeWarning
assert "Found Intel" in str(wm.message)
assert "LLVM" in str(wm.message)
assert "multiple_openmp.md" in str(wm.message)
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())
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():
pytest.importorskip("numpy")
output = subprocess.check_output(
[sys.executable, "-m", "threadpoolctl", "-c", "import numpy"]
)
cli_info = json.loads(output.decode("utf-8"))
this_process_info = threadpool_info(debugging_info=True)
for lib_info in cli_info:
assert lib_info in this_process_info
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="need recent subprocess.run options"
)
def test_command_line_import_flag():
result = subprocess.run(
[
sys.executable,
"-m",
"threadpoolctl",
"-i",
"numpy",
"scipy.linalg",
"invalid_package",
"numpy.invalid_sumodule",
],
capture_output=True,
check=True,
encoding="utf-8",
)
cli_info = json.loads(result.stdout)
this_process_info = threadpool_info(debugging_info=True)
for lib_info in cli_info:
assert lib_info in this_process_info
warnings = [w.strip() for w in result.stderr.splitlines()]
assert "WARNING: could not import invalid_package" in warnings
assert "WARNING: could not import numpy.invalid_sumodule" in warnings
if scipy is None:
assert "WARNING: could not import scipy.linalg" in warnings
else:
assert "WARNING: could not import scipy.linalg" not in warnings
def test_architecture():
expected_openblas_architectures = (
# XXX: add more as needed by CI or developer laptops
"armv8",
"barcelona",
"cooperlake",
"haswell",
"neoversen1",
"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
"skx",
"haswell",
"zen3",
"generic",
)
for lib_info in threadpool_info():
if lib_info["internal_api"] == "openblas":
assert lib_info["architecture"].lower() in expected_openblas_architectures
elif lib_info["internal_api"] == "blis":
assert lib_info["architecture"].lower() in expected_blis_architectures
else:
# Not supported for other libraries
assert "architecture" not in lib_info
def test_openblas_threading_layer():
# Check that threadpool_info correctly recovers the threading layer used by openblas
openblas_controller = ThreadpoolController().select(internal_api="openblas")
if not (openblas_controller):
pytest.skip("requires OpenBLAS.")
expected_openblas_threading_layers = ("openmp", "pthreads", "disabled")
threading_layer = openblas_controller.lib_controllers[0].threading_layer
if threading_layer == "unknown":
# If we never recover an acceptable value for the threading layer, it will be
# always skipped and caught by check_no_test_always_skipped.
pytest.skip("Unknown OpenBLAS threading layer.")
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(
"GITHUB_ACTIONS" 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(
"GITHUB_ACTIONS" 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"]
if sys.platform == "linux":
mkl_path = f"{os.getenv('CONDA_PREFIX')}/lib/libmkl_rt.so"
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.
controller = ThreadpoolController()
original_info = controller.info()
if any(info["num_threads"] < 2 for info in original_info):
pytest.skip("Test requires at least 2 CPUs on host machine")
if not controller.select(user_api="blas"):
pytest.skip("Requires a blas runtime.")
def check_blas_num_threads(expected_num_threads):
blas_controller = ThreadpoolController().select(user_api="blas")
assert all(
lib_controller.num_threads == expected_num_threads
for lib_controller in blas_controller.lib_controllers
)
@controller.wrap(limits=1, user_api="blas")
def outer_func():
check_blas_num_threads(expected_num_threads=1)
inner_func()
check_blas_num_threads(expected_num_threads=1)
@controller.wrap(limits=2, user_api="blas")
def inner_func():
check_blas_num_threads(expected_num_threads=2)
outer_func()
assert ThreadpoolController().info() == original_info
def test_custom_controller():
# Check that a custom controller can be used to change the number of threads
# used by a library.
try:
import tests._pyMylib # noqa
except:
pytest.skip("requires my_thread_lib to be compiled")
controller = ThreadpoolController()
original_info = controller.info()
mylib_controller = controller.select(user_api="my_threaded_lib")
# my_threaded_lib has been found and there's 1 matching shared library
assert len(mylib_controller.lib_controllers) == 1
mylib_controller = mylib_controller.lib_controllers[0]
# we linked against my_threaded_lib v2.0 and by default it uses 42 thread
assert mylib_controller.version == "2.0"
assert mylib_controller.num_threads == 42
# my_threaded_lib exposes an additional info "some_attr":
assert mylib_controller.info()["some_attr"] == "some_value"
with controller.limit(limits=1, user_api="my_threaded_lib"):
assert mylib_controller.num_threads == 1
assert ThreadpoolController().info() == original_info
def test_threadpool_controller_repeated_init():
"""Stress-test repeated library discovery.
Non-regression test for a Windows-specific problem where DLLs loaded or
unloaded concurrently during discovery could raise an OSError. The failure
was originally reproduced after importing OpenCV on Windows conda-forge;
see https://github.com/joblib/threadpoolctl/issues/217
"""
pytest.importorskip("cv2")
for _ in range(100):
ThreadpoolController()
def test_dllist_oserror_emits_warning():
"""dllist listing failures warn instead of raising, so they can be reported."""
def boom():
raise OSError("EnumProcessModules failed: simulated race")
controller = ThreadpoolController._from_controllers([])
with pytest.warns(RuntimeWarning, match="ctypes.util.dllist failed"):
controller._find_libraries_with_python(boom)
assert controller.lib_controllers == []
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
def test_windows_library_path_longer_than_max_path(tmp_path):
"""OpenBLAS loaded from a path longer than MAX_PATH is discovered.
Covers both Windows enumerators: ``ctypes.util.dllist`` on Python 3.14+
(``GetModuleFileNameW`` with a large buffer) and the Toolhelp /
``GetModuleFileNameExW`` path on older Pythons.
Regression test inspired by the local repro in
https://github.com/joblib/threadpoolctl/pull/189#issuecomment-2714235916
"""
src_dll = get_openblas_dll_path()
if src_dll is None:
pytest.skip("Requires OpenBLAS on Windows")
long_path = make_long_windows_path(
tmp_path, "libopenblas_long_path_test.dll", min_length=261
)
extended_path = os.path.abspath(str(long_path))
# Win32 APIs reject paths longer than MAX_PATH unless they use the
# extended-length prefix ``\\?\``. Skip rewriting if abspath already
# returned that form.
if not extended_path.startswith("\\\\?\\"):
# UNC paths (``\\server\share\...``) need ``\\?\UNC\server\share\...``
# rather than ``\\?\\server\...``.
if extended_path.startswith("\\\\"):
extended_path = "\\\\?\\UNC\\" + extended_path[2:]
else:
extended_path = "\\\\?\\" + extended_path
shutil.copy2(src_dll, extended_path)
ctypes.CDLL(extended_path)
expected_path = os.path.abspath(str(long_path))
# Compare using the canonical path without the ``\\?\`` prefix.
# ``\\?\C:\...`` becomes ``C:\...``; ``\\?\UNC\server\share`` becomes
# ``\\server\share``.
if expected_path.startswith("\\\\?\\"):
expected_path = expected_path[4:]
if expected_path.startswith("UNC\\"):
expected_path = "\\\\" + expected_path[4:]
expected_path = os.path.normcase(os.path.normpath(expected_path))
openblas_info = ThreadpoolController().select(internal_api="openblas").info()
long_path_entries = [info for info in openblas_info if len(info["filepath"]) > 260]
assert len(long_path_entries) >= 1
normalized_filepaths = []
for info in long_path_entries:
filepath = info["filepath"]
# Same prefix stripping as expected_path so discovery results match
# whether the enumerator kept ``\\?\``: dllist (Python 3.14+) or
# GetModuleFileNameExW on the Toolhelp / EnumProcessModulesEx path.
if filepath.startswith("\\\\?\\"):
filepath = filepath[4:]
if filepath.startswith("UNC\\"):
filepath = "\\\\" + filepath[4:]
normalized_filepaths.append(os.path.normcase(os.path.normpath(filepath)))
assert expected_path in normalized_filepaths
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
@pytest.mark.skipif(
stdlib_dllist() is not None,
reason="Python 3.14+ dllist does not apply the internal path length limit",
)
def test_windows_library_path_exceeds_internal_limit(tmp_path, monkeypatch):
"""Libraries with a path longer than the internal limit are ignored.
Regression test inspired by the local repro in
https://github.com/joblib/threadpoolctl/pull/189#issuecomment-2714235916
"""
src_dll = get_openblas_dll_path()
if src_dll is None:
pytest.skip("Requires OpenBLAS on Windows")
monkeypatch.setattr(threadpoolctl, "_WINDOWS_MAX_LIBRARY_PATH_LENGTH", 300)
long_path = make_long_windows_path(
tmp_path, "libopenblas_path_too_long.dll", min_length=400
)
extended_path = os.path.abspath(str(long_path))
# Copy/load still need the Win32 extended-length prefix ``\\?\`` because
# 400 characters is past MAX_PATH, even though this test's internal limit
# is 300. Do not rewrite if abspath already returned that prefix.
if not extended_path.startswith("\\\\?\\"):
# Drive paths become ``\\?\C:\...``. UNC paths (``\\server\share\...``)
# must use ``\\?\UNC\server\share\...``, not ``\\?\\server\...``.
if extended_path.startswith("\\\\"):
extended_path = "\\\\?\\UNC\\" + extended_path[2:]
else:
extended_path = "\\\\?\\" + extended_path
shutil.copy2(src_dll, extended_path)
ctypes.CDLL(extended_path)
expected_path = os.path.abspath(str(long_path))
# Assert absence using the unprefixed form. Discovery may report
# ``\\?\C:\...`` or ``\\?\UNC\...``; stripping those prefixes (and turning
# ``UNC\`` back into ``\\``) keeps the comparison independent of that.
if expected_path.startswith("\\\\?\\"):
expected_path = expected_path[4:]
if expected_path.startswith("UNC\\"):
expected_path = "\\\\" + expected_path[4:]
expected_path = os.path.normcase(os.path.normpath(expected_path))
with pytest.warns(RuntimeWarning, match="path too long"):
info = ThreadpoolController().info()
filepaths = set()
for entry in info:
if "filepath" not in entry:
continue
filepath = entry["filepath"]
# Same prefix stripping as expected_path. Without it, a ``\\?\``
# filepath would not match expected_path and the "ignored" assertion
# could pass even if the library was actually discovered.
if filepath.startswith("\\\\?\\"):
filepath = filepath[4:]
if filepath.startswith("UNC\\"):
filepath = "\\\\" + filepath[4:]
filepaths.add(os.path.normcase(os.path.normpath(filepath)))
assert expected_path not in filepaths
def parse_version(version: str) -> list[int]:
return list(map(int, version.split(".")))
@pytest.fixture(
params=[
(
{"internal_api": "openblas"},
lambda lib: (
lib.threading_layer == "openmp"
# For Windows support, see
# https://github.com/joblib/threadpoolctl/issues/230
and sys.platform in ("linux", "darwin")
and parse_version(lib.version) >= parse_version("0.3.34")
),
),
(
{"internal_api": "mkl"},
lambda _lib: True,
),
],
# ids correspond to the params above:
ids=["openblas-openmp", "mkl"],
)
def thread_local_blas_lib(request) -> LibController:
"""Create all LibControllers that use a thread-local setting."""
select_filter, extra_check = request.param
controller = ThreadpoolController().select(**select_filter)
if not controller.lib_controllers:
pytest.skip(f"{select_filter} controller not found")
libs = [
lib
for lib in controller.lib_controllers
if extra_check(lib) and lib.internal_api == select_filter["internal_api"]
]
if not libs:
pytest.skip("No libraries matched the requirements")
assert len(libs) == 1
return libs[0]
def test_setting_limit_on_thread_local_blas_api_is_reported_as_thread_local(
thread_local_blas_lib: LibController,
) -> None:
"""
Setting the number of threads for libraries that support thread-local
setting API is reported as doing so.
This doesn't check actual behavior, only reported behavior.
"""
lib = thread_local_blas_lib
scope = _determine_thread_limit_scope(lib.get_num_threads, lib.set_num_threads)
assert scope == "current_thread"
@pytest.mark.skipif(
sys.platform != "linux" or shutil.which("strace") is None,
reason="requires strace on Linux",
)
def test_setting_limit_on_thread_local_blas_api_is_actually_thread_local(
thread_local_blas_lib: LibController,
) -> None:
"""
Setting the number of threads for libraries that support thread-local
setting API actually does so.
"""
# The test script uses NumPy, there might be multiple BLAS in this test
# process, and we want to only run if _NumPy_ uses that library.
# So check that before proceeding.
output = json.loads(
subprocess.check_output(
[
"python",
"-c",
"import numpy, json, threadpoolctl; print(json.dumps(threadpoolctl.threadpool_info()))",
]
)
)
found_correct_blas = False
for library in output:
if library["internal_api"] == thread_local_blas_lib.internal_api:
found_correct_blas = True
break
if not found_correct_blas:
pytest.skip("NumPy doesn't use the BLAS we want to test")
def num_threads_created(limit: int) -> int:
result = 0
for line in subprocess.check_output(
[
"strace",
"-f",
"-e",
"clone3",
"python",
"-m",
"tests._limit_blas",
str(limit),
],
stderr=subprocess.STDOUT,
).splitlines():
if b"clone3(" in line and b"CLONE_THREAD" in line:
result += 1
return result
# _limit_blas runs BLAS operations in 2 Python threads, so by changing the
# BLAS limit from 1 to 4 we expect an extra 2 * (4 - 1) == 6 threads.
nmc_1 = num_threads_created(1)
nmc_4 = num_threads_created(4)
assert nmc_4 - nmc_1 == 6
@pytest.mark.skipif(os.getenv("CONDA_PREFIX") is None, reason="conda-specific test")
@pytest.mark.parametrize("module", ["numpy", "scipy.linalg"])
def test_conda_blas_detection_after_import(module):
pytest.importorskip(module)
info = threadpool_info_from_subprocess(module)
conda = shutil.which("conda") or shutil.which("micromamba") or shutil.which("mamba")
if conda is None:
pytest.skip("conda, micromamba, or mamba not found")
conda_list_output = subprocess.check_output([conda, "list", "--json"], text=True)
conda_list_items = json.loads(conda_list_output)
blas_names_from_conda = [
each["name"]
for each in conda_list_items
if any(blas_lib in each["name"] for blas_lib in ["openblas", "mkl"])
]
blas_names_from_conda = [each.replace("lib", "") for each in blas_names_from_conda]
if "accelerate" in conda_list_output:
pytest.skip("threadpoolctl does not know how to inspect Accelerate")
if not blas_names_from_conda:
pytest.skip(
f"{module} has been installed with pip, this is a conda-specific test"
)
blas_info = select(info, user_api="blas")
assert len(blas_info) > 0
# Flexiblas is built from source on our CI. At the time of writing, it is not
# available in the conda-forge channel.
blas_names_from_threadpoolctl = [
each["internal_api"]
for each in blas_info
if each["internal_api"] != "flexiblas"
]
assert set(blas_names_from_threadpoolctl).issubset(blas_names_from_conda)
def test_controller_parallelism_no_deadlocks():
"""Creating a controller in parallel to itself does not cause deadlocks.
Non-regression test for https://github.com/joblib/threadpoolctl/issues/239
Lacking the fixes from PR #243, this deadlocks on Conda environments, at
least, but possibly not on PyPI with Python from a Linux distro.
"""
if sys.platform != "linux" or not hasattr(ctypes.PyDLL(None), "backtrace"):
pytest.skip("Testing glibc on Linux")
# Internally, backtrace() calls dl_iterate_phdr which can result in
# deadlocks if threadpoolctl is also using dl_iterate_phdr.
backtrace_gil = ctypes.PyDLL(None).backtrace
backtrace_gil.argtypes = [ctypes.c_void_p, ctypes.c_int]
backtrace_nogil = ctypes.CDLL(None).backtrace
backtrace_nogil.argtypes = [ctypes.c_void_p, ctypes.c_int]
def create_controllers():
buf = (ctypes.c_void_p * 20)()
for _ in range(100):
limiter = threadpool_limits()
backtrace_gil(buf, 20)
backtrace_nogil(buf, 20)
threads = []
for _ in range(os.cpu_count() * 4):
t = Thread(target=create_controllers)
threads.append(t)
t.start()
for t in threads:
t.join()
@pytest.mark.skipif(
not sys.platform.startswith("linux"),
reason="ctypes.util is only avoided on Linux (#225)",
)
def test_linux_does_not_import_ctypes_util():
# ctypes.util on CPython 3.14 Linux allocates a process-lifetime CFUNCTYPE
# callback; importing it in the parent is enough to abort after fork with
# some libffi builds. See https://github.com/joblib/threadpoolctl/issues/225.
path = os.path.dirname(os.path.dirname(__file__))
env = os.environ.copy()
env["PYTHONPATH"] = path + os.pathsep + env.get("PYTHONPATH", "")
script = """
import sys
import threadpoolctl
assert "ctypes.util" not in sys.modules, sorted(sys.modules)
threadpoolctl.threadpool_info()
assert "ctypes.util" not in sys.modules
"""
subprocess.check_call([sys.executable, "-c", script], env=env)
joblib-threadpoolctl-804bc7e/tests/utils.py 0000664 0000000 0000000 00000010327 15252261370 0021115 0 ustar 00root root 0000000 0000000 import json
import os
import sys
import threadpoolctl
from glob import glob
from os.path import dirname, normpath
from pathlib import Path
from subprocess import check_output
# Path to shipped openblas for libraries such as numpy or scipy
libopenblas_patterns = []
try:
# make sure the mkl/blas are loaded for test_threadpool_limits
import numpy as np
np.dot(np.ones(1000), np.ones(1000))
libopenblas_patterns.append(os.path.join(np.__path__[0], ".libs", "libopenblas*"))
numpy_site_packages = os.path.dirname(np.__path__[0])
libopenblas_patterns.append(
os.path.join(numpy_site_packages, "numpy.libs", "libscipy_openblas*.dll")
)
except ImportError:
pass
if sys.platform == "win32":
libopenblas_patterns.append(
os.path.join(sys.prefix, "Library", "bin", "openblas*.dll")
)
libopenblas_patterns.append(
os.path.join(sys.prefix, "Library", "bin", "libopenblas*.dll")
)
try:
import scipy
import scipy.linalg # noqa: F401
scipy.linalg.svd([[1, 2], [3, 4]])
libopenblas_patterns.append(
os.path.join(scipy.__path__[0], ".libs", "libopenblas*")
)
scipy_site_packages = os.path.dirname(scipy.__path__[0])
libopenblas_patterns.append(
os.path.join(scipy_site_packages, "scipy.libs", "libscipy_openblas*.dll")
)
except ImportError:
scipy = None
libopenblas_paths = set(
path for pattern in libopenblas_patterns for path in glob(pattern)
)
try:
import tests._openmp_test_helper.openmp_helpers_inner # noqa: F401
cython_extensions_compiled = True
except ImportError:
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
`module` is imported before calling threadpool_info
"""
# set PYTHONPATH to import from non sub-modules
path1 = normpath(dirname(threadpoolctl.__file__))
path2 = os.path.join(path1, "tests", "_openmp_test_helper")
pythonpath = os.pathsep.join([path1, path2])
env = os.environ.copy()
try:
env["PYTHONPATH"] = os.pathsep.join([pythonpath, env["PYTHONPATH"]])
except KeyError:
env["PYTHONPATH"] = pythonpath
cmd = [sys.executable, "-m", "threadpoolctl", "-i", module]
out = check_output(cmd, env=env).decode("utf-8")
return json.loads(out)
def select(info, **kwargs):
"""Select a subset of the list of library info matching the request"""
# It's just a utility function to avoid repeating the pattern
# [lib_info for lib_info in info if lib_info[""] == key]
for key, vals in kwargs.items():
kwargs[key] = [vals] if not isinstance(vals, list) else vals
selected_info = [
lib_info
for lib_info in info
if any(lib_info.get(key, None) in vals for key, vals in kwargs.items())
]
return selected_info
def get_openblas_dll_path():
"""Return a path to an OpenBLAS DLL that can be copied for Windows tests."""
if libopenblas_paths:
return sorted(
libopenblas_paths,
key=lambda path: (
0
if "libscipy_openblas" in os.path.basename(path).lower()
else 1 if "libopenblas" in os.path.basename(path).lower() else 2
),
)[0]
controllers = (
threadpoolctl.ThreadpoolController()
.select(internal_api="openblas")
.lib_controllers
)
if not controllers:
return None
filepath = controllers[0].filepath
if os.path.isfile(filepath):
return filepath
return None
def make_long_windows_path(base_dir, filename, min_length=261):
"""Nest padded directories under base_dir until base/.../filename >= min_length."""
padding_segments = ["a" * 100, "b" * 100, "c" * 100, "d" * 100]
current = Path(base_dir)
segment_index = 0
target = current / filename
while len(str(target)) < min_length:
current = current / padding_segments[segment_index % len(padding_segments)]
segment_index += 1
target = current / filename
target.parent.mkdir(parents=True, exist_ok=True)
return target
joblib-threadpoolctl-804bc7e/threadpoolctl.py 0000664 0000000 0000000 00000204176 15252261370 0021466 0 ustar 00root root 0000000 0000000 """threadpoolctl
This module provides utilities to introspect native libraries that relies on
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
# adapted from code by Intel developer @anton-malakhov available at
# https://github.com/IntelPython/smp (Copyright (c) 2017, Intel Corporation)
# and also published under the BSD 3-Clause license
import os
import re
import sys
import ctypes
import itertools
import textwrap
from threading import Thread
from typing import Callable, Literal, final
import warnings
from abc import ABC, abstractmethod
from functools import lru_cache
from contextlib import ContextDecorator
__version__ = "3.7.0"
__all__ = [
"threadpool_limits",
"threadpool_info",
"ThreadpoolController",
"LibController",
"register",
]
# One can get runtime errors or even segfaults due to multiple OpenMP libraries
# loaded simultaneously which can happen easily in Python when importing and
# using compiled extensions built with different compilers and therefore
# different OpenMP runtimes in the same program. In particular libiomp (used by
# Intel ICC) and libomp used by clang/llvm tend to crash. This can happen for
# instance when calling BLAS inside a prange. Setting the following environment
# variable allows multiple OpenMP libraries to be loaded. It should not degrade
# performances since we manually take care of potential over-subscription
# performance issues, in sections of the code where nested OpenMP loops can
# happen, by dynamically reconfiguring the inner OpenMP runtime to temporarily
# disable it while under the scope of the outer OpenMP parallel section.
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "True")
# Hard limit for Windows library paths resolved through GetModuleFileNameExW.
# Kept below unlimited for security reasons; see CHANGES.md and PR #189.
_WINDOWS_MAX_LIBRARY_PATH_LENGTH = 2600
# Structure to cast the info on dynamically loaded library. See
# https://linux.die.net/man/3/dl_iterate_phdr for more details.
_SYSTEM_UINT = ctypes.c_uint64 if sys.maxsize > 2**32 else ctypes.c_uint32
_SYSTEM_UINT_HALF = ctypes.c_uint32 if sys.maxsize > 2**32 else ctypes.c_uint16
class _dl_phdr_info(ctypes.Structure):
_fields_ = [
("dlpi_addr", _SYSTEM_UINT), # Base address of object
("dlpi_name", ctypes.c_char_p), # path to the library
("dlpi_phdr", ctypes.c_void_p), # pointer on dlpi_headers
("dlpi_phnum", _SYSTEM_UINT_HALF), # number of elements in dlpi_phdr
]
# The RTLD_NOLOAD flag for loading shared libraries is not defined on Windows.
try:
_RTLD_NOLOAD = os.RTLD_NOLOAD
except AttributeError:
_RTLD_NOLOAD = ctypes.DEFAULT_MODE
# What scope does the API affect?
_ThreadLimitScope = Literal[
# Using the API sets a limit only on the current thread.
"current_thread",
# Using the API sets a limit for every thread in the process; whether or
# not it's a shared process-wide pool or per-thread limit needs to be
# determined some other way.
"process",
# Something else, unexpected; perhaps another variant, perhaps information
# can't be determined under the current configuration.
"unknown",
]
def _determine_thread_limit_scope(
get_n_threads: Callable[[], int], set_n_threads: Callable[[int], None]
) -> _ThreadLimitScope:
"""
Run some experiments to determine the scope of the given get/set API.
This function might not work if you only have one core available.
This function might not work if you set a limit on a library with an
environment variable.
The function works by changing the number of threads in loaded controllers,
which can be a process-wide change. As such, it is not always thread-safe.
An attempt will be made to restore all settings to their previous state,
but the result may be subtly different, e.g. if "unset" has different
semantics than "set to the default returned value".
"""
previous = get_n_threads()
# Some plausible constraints we need to keep in mind:
#
# 1. The API might not allow setting more than the number of (available, or
# physical) cores.
# 2. Some hard limit on number of threads.
try:
# Choose a desired number of threads that is different than the current
# number, and hopefully achievable under the current configuration:
if previous < 2:
expected = 2
else:
# It's 2 or more, so shrink it slightly:
expected = previous - 1
thread_result = []
def get_and_set() -> None:
set_n_threads(expected)
thread_result.append(get_n_threads())
thread = Thread(target=get_and_set)
thread.start()
thread.join()
# First, getting in the same thread as a set should always give same
# number, if it's a number in a reasonable range. A possible exception
# fo failing this is if the number of thread is limited by available
# CPU, and only one CPU is available. In that case we can't empirically
# determine how the API works. We try to not reach that point here, but
# you can imagine a thread pool implementation that is aware of
# cgroups, in which case a Docker container limited to one core will
# pass the safety check at the start of the function. Perhaps
# cpu_count() from loky should be moved into this package...
if thread_result != [expected]:
return "unknown"
# Now, check this thread:
if get_n_threads() == expected:
# Setting modified this thread's results too:
return "process"
elif get_n_threads() == previous:
# Setting modified the other thread, but not this one:
return "current_thread"
else:
# No idea what's going on:
return "unknown"
finally:
set_n_threads(previous)
class LibController(ABC):
"""Abstract base class for the individual library controllers
A library controller must expose the following class attributes:
- user_api : str
Usually the name of the library or generic specification the library
implements, e.g. "blas" is a specification with different implementations.
- internal_api : str
Usually the name of the library or concrete implementation of some
specification, e.g. "openblas" is an implementation of the "blas"
specification.
- filename_prefixes : tuple
Possible prefixes of the shared library's filename that allow to
identify the library. e.g. "libopenblas" for libopenblas.so.
and implement the following methods: `get_num_threads`, `set_num_threads` and
`get_version`.
Threadpoolctl loops through all the loaded shared libraries and tries to match
the filename of each library with the `filename_prefixes`. If a match is found, a
controller is instantiated and a handler to the library is stored in the `dynlib`
attribute as a `ctypes.CDLL` object. It can be used to access the necessary symbols
of the shared library to implement the above methods.
The following information will be exposed in the info dictionary:
- user_api : standardized API, if any, or a copy of internal_api.
- internal_api : implementation-specific API.
- num_threads : the current thread limit.
- prefix : prefix of the shared library's filename.
- filepath : path to the loaded shared library.
- version : version of the library (if available).
In addition, each library controller may expose internal API specific entries. They
must be set as attributes in the `set_additional_attributes` method.
"""
@final
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._symbol_prefix, self._symbol_suffix = self._find_affixes()
self.version = self.get_version()
self.set_additional_attributes()
def info(self, debugging_info: bool = False):
"""Return relevant info wrapped in a dict.
Parameters
----------
debugging_info : bool
Include extra fields which require more intrusive actions to
obtain, and which can't always reliably be determined.
"""
hidden_attrs = ("dynlib", "parent", "_symbol_prefix", "_symbol_suffix")
result = {
"user_api": self.user_api,
"internal_api": self.internal_api,
"num_threads": self.num_threads,
**{k: v for k, v in vars(self).items() if k not in hidden_attrs},
}
if debugging_info:
result["thread_limit_scope"] = _determine_thread_limit_scope(
self.get_num_threads, self.set_num_threads
)
return result
def set_additional_attributes(self):
"""Set additional attributes meant to be exposed in the info dict"""
@property
def num_threads(self):
"""Exposes the current thread limit as a dynamic property
This is not meant to be used or overriden by subclasses.
"""
return self.get_num_threads()
@abstractmethod
def get_num_threads(self):
"""Return the maximum number of threads available to use"""
@abstractmethod
def set_num_threads(self, num_threads):
"""Set the maximum number of threads to use
When possible, implementations of this method should choose a thread
limiting API that only applies to the current thread.
"""
@abstractmethod
def get_version(self):
"""Return the version of the shared library"""
def _find_affixes(self):
"""Return the affixes for the symbols of the shared library"""
return "", ""
def _get_symbol(self, name):
"""Return the symbol of the shared library accounding for the affixes"""
return getattr(
self.dynlib, f"{self._symbol_prefix}{name}{self._symbol_suffix}", None
)
class OpenBLASController(LibController):
"""Controller class for OpenBLAS"""
user_api = "blas"
internal_api = "openblas"
filename_prefixes = (
"libopenblas",
"libblas", # legacy conda-forge Windows shim, see _make_controller_from_path
"libscipy_openblas",
"openblas", # Windows conda package use openblas.dll
)
_symbol_prefixes = ("", "scipy_")
_symbol_suffixes = ("", "64_", "_64")
# All variations of "openblas_get_num_threads", accounting for the affixes
check_symbols = tuple(
f"{prefix}openblas_get_num_threads{suffix}"
for prefix, suffix in itertools.product(_symbol_prefixes, _symbol_suffixes)
)
def _find_affixes(self):
for prefix, suffix in itertools.product(
self._symbol_prefixes, self._symbol_suffixes
):
if hasattr(self.dynlib, f"{prefix}openblas_get_num_threads{suffix}"):
return prefix, suffix
def set_additional_attributes(self):
self.threading_layer = self._get_threading_layer()
self.architecture = self._get_architecture()
def get_num_threads(self):
# See discussion in set_num_threads for details:
if self.threading_layer == "openmp":
symbol = "omp_get_max_threads"
else:
symbol = "openblas_get_num_threads"
get_num_threads_func = self._get_symbol(symbol)
if get_num_threads_func is not None:
return get_num_threads_func()
return None
def set_num_threads(self, num_threads):
# The OpenBLAS limiting API is process-wide, and we want current thread
# limit if possible. When OpenBLAS is backed by OpenMP, using the
# OpenMP API allows for current thread limiting when OpenMP has that
# behavior. That is the case for libgomp, libomp, and libiomp, what you
# would find on Linux or macOS.
#
# On Windows the Visual C++ OpenMP API is process-wide, unfortunately,
# though this may be fixed if the /openmp:llvm flag is used:
# https://github.com/joblib/threadpoolctl/issues/230
#
# Also worth knowing that before v0.3.34, the OpenBLAS limiting API is
# broken when using OpenMP threading:
# https://github.com/OpenMathLib/OpenBLAS/issues/5806
if self.threading_layer == "openmp":
symbol = "omp_set_num_threads"
else:
symbol = "openblas_set_num_threads"
set_num_threads_func = self._get_symbol(symbol)
if set_num_threads_func is not None:
return set_num_threads_func(num_threads)
return None
def get_version(self):
# None means OpenBLAS is not loaded or version < 0.3.4, since OpenBLAS
# did not expose its version before that.
get_version_func = self._get_symbol("openblas_get_config")
if get_version_func is not None:
get_version_func.restype = ctypes.c_char_p
config = get_version_func().split()
if config[0] == b"OpenBLAS":
return config[1].decode("utf-8")
return None
return None
def _get_threading_layer(self):
"""Return the threading layer of OpenBLAS"""
get_threading_layer_func = self._get_symbol("openblas_get_parallel")
if get_threading_layer_func is not None:
threading_layer = get_threading_layer_func()
if threading_layer == 2:
return "openmp"
elif threading_layer == 1:
return "pthreads"
return "disabled"
return "unknown"
def _get_architecture(self):
"""Return the architecture detected by OpenBLAS"""
get_architecture_func = self._get_symbol("openblas_get_corename")
if get_architecture_func is not None:
get_architecture_func.restype = ctypes.c_char_p
return get_architecture_func().decode("utf-8")
return None
class BLISController(LibController):
"""Controller class for BLIS"""
user_api = "blas"
internal_api = "blis"
filename_prefixes = (
"libblis",
"libblas",
) # libblas: legacy conda-forge Windows shim
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()
self.architecture = self._get_architecture()
def get_num_threads(self):
get_func = getattr(self.dynlib, "bli_thread_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, "bli_thread_set_num_threads", lambda num_threads: None
)
return set_func(num_threads)
def get_version(self):
get_version_ = getattr(self.dynlib, "bli_info_get_version_str", None)
if get_version_ is None:
return None
get_version_.restype = ctypes.c_char_p
return get_version_().decode("utf-8")
def _get_threading_layer(self):
"""Return the threading layer of BLIS"""
if getattr(self.dynlib, "bli_info_get_enable_openmp", lambda: False)():
return "openmp"
elif getattr(self.dynlib, "bli_info_get_enable_pthreads", lambda: False)():
return "pthreads"
return "disabled"
def _get_architecture(self):
"""Return the architecture detected by BLIS"""
bli_arch_query_id = getattr(self.dynlib, "bli_arch_query_id", None)
bli_arch_string = getattr(self.dynlib, "bli_arch_string", None)
if bli_arch_query_id is None or bli_arch_string is None:
return None
# the true restype should be BLIS' arch_t (enum) but int should work
# for us:
bli_arch_query_id.restype = ctypes.c_int
bli_arch_string.restype = ctypes.c_char_p
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, debugging_info: bool = False):
"""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(debugging_info=debugging_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", # legacy conda-forge Windows shim, see _make_controller_from_path
)
check_symbols = (
"MKL_Get_Max_Threads",
"MKL_Set_Num_Threads_Local",
"MKL_Get_Version_String",
"MKL_Set_Threading_Layer",
)
def set_additional_attributes(self):
self.threading_layer = self._get_threading_layer()
def get_num_threads(self):
get_func = getattr(self.dynlib, "MKL_Get_Max_Threads", lambda: None)
return get_func()
def set_num_threads(self, num_threads):
set_func = getattr(
self.dynlib, "MKL_Set_Num_Threads_Local", lambda num_threads: None
)
return set_func(num_threads)
def get_version(self):
if not hasattr(self.dynlib, "MKL_Get_Version_String"):
return None
res = ctypes.create_string_buffer(200)
self.dynlib.MKL_Get_Version_String(res, 200)
version = res.value.decode("utf-8")
group = re.search(r"Version ([^ ]+) ", version)
if group is not None:
version = group.groups()[0]
return version.strip()
def _get_threading_layer(self):
"""Return the threading layer of MKL"""
# The function mkl_set_threading_layer returns the current threading
# layer. Calling it with an invalid threading layer allows us to safely
# get the threading layer
set_threading_layer = getattr(
self.dynlib, "MKL_Set_Threading_Layer", lambda layer: -1
)
layer_map = {
0: "intel",
1: "sequential",
2: "pgi",
3: "gnu",
4: "tbb",
-1: "not specified",
}
return layer_map[set_threading_layer(-1)]
class OpenMPController(LibController):
"""Controller class for OpenMP"""
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)
return get_func()
def set_num_threads(self, num_threads):
set_func = getattr(self.dynlib, "omp_set_num_threads", lambda num_threads: None)
return set_func(num_threads)
def get_version(self):
# There is no way to get the version number programmatically in OpenMP.
return None
# 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,
FlexiBLASController,
]
# Helpers for the doc and test names
_ALL_USER_APIS = list(set(lib.user_api for lib in _ALL_CONTROLLERS))
_ALL_INTERNAL_APIS = [lib.internal_api for lib in _ALL_CONTROLLERS]
_ALL_PREFIXES = list(
set(prefix for lib in _ALL_CONTROLLERS for prefix in lib.filename_prefixes)
)
_ALL_BLAS_LIBRARIES = [
lib.internal_api for lib in _ALL_CONTROLLERS if lib.user_api == "blas"
]
_ALL_OPENMP_LIBRARIES = OpenMPController.filename_prefixes
def register(controller):
"""Register a new controller"""
_ALL_CONTROLLERS.append(controller)
_ALL_USER_APIS.append(controller.user_api)
_ALL_INTERNAL_APIS.append(controller.internal_api)
_ALL_PREFIXES.extend(controller.filename_prefixes)
def _format_docstring(*args, **kwargs):
def decorator(o):
if o.__doc__ is not None:
o.__doc__ = o.__doc__.format(*args, **kwargs)
return o
return decorator
@lru_cache(maxsize=10000)
def _realpath(filepath):
"""Small caching wrapper around os.path.realpath to limit system calls"""
return os.path.realpath(filepath)
@_format_docstring(USER_APIS=list(_ALL_USER_APIS), INTERNAL_APIS=_ALL_INTERNAL_APIS)
def threadpool_info(debugging_info: bool = False):
"""Return the maximal number of threads for each detected library.
Return a list with all the supported libraries that have been found. Each
library is represented by a dict with the following information:
- "user_api" : user API. Possible values are {USER_APIS}.
- "internal_api": internal API. Possible values are {INTERNAL_APIS}.
- "prefix" : filename prefix of the specific implementation.
- "filepath": path to the loaded library.
- "version": version of the library (if available).
- "num_threads": the current thread limit.
In addition, each library may contain internal_api specific entries.
Parameters
----------
debugging_info : bool
Include extra fields which require more intrusive actions to obtain,
and which can't always reliably be determined.
- "thread_limit_scope": When setting the number of threads, what is
affected. Possible values are "process", "current_thread", "unknown".
"""
return ThreadpoolController().info(debugging_info)
class _ThreadpoolLimiter:
"""The guts of ThreadpoolController.limit
Refer to the docstring of ThreadpoolController.limit for more details.
It will only act on the library controllers held by the provided `controller`.
Using the default constructor sets the limits right away such that it can be used as
a callable. Setting the limits can be delayed by using the `wrap` class method such
that it can be used as a decorator.
"""
def __init__(self, controller, *, limits=None, user_api=None):
self._controller = controller
self._limits, self._user_api, self._prefixes = self._check_params(
limits, user_api
)
self._original_info = self._controller.info()
self._set_threadpool_limits()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.restore_original_limits()
@classmethod
def wrap(cls, controller, *, limits=None, user_api=None):
"""Return an instance of this class that can be used as a decorator"""
return _ThreadpoolLimiterDecorator(
controller=controller, limits=limits, user_api=user_api
)
def restore_original_limits(self):
"""Set the limits back to their original values"""
for lib_controller, original_info in zip(
self._controller.lib_controllers, self._original_info
):
lib_controller.set_num_threads(original_info["num_threads"])
# Alias of `restore_original_limits` for backward compatibility
unregister = restore_original_limits
def get_original_num_threads(self):
"""Original num_threads from before calling threadpool_limits
Return a dict `{user_api: num_threads}`.
"""
num_threads = {}
warning_apis = []
for user_api in self._user_api:
limits = [
lib_info["num_threads"]
for lib_info in self._original_info
if lib_info["user_api"] == user_api
]
limits = set(limits)
n_limits = len(limits)
if n_limits == 1:
limit = limits.pop()
elif n_limits == 0:
limit = None
else:
limit = min(limits)
warning_apis.append(user_api)
num_threads[user_api] = limit
if warning_apis:
warnings.warn(
"Multiple values possible for following user apis: "
+ ", ".join(warning_apis)
+ ". Returning the minimum."
)
return num_threads
def _check_params(self, limits, user_api):
"""Suitable values for the _limits, _user_api and _prefixes attributes"""
if isinstance(limits, str) and limits == "sequential_blas_under_openmp":
(
limits,
user_api,
) = self._controller._get_params_for_sequential_blas_under_openmp().values()
if limits is None or isinstance(limits, int):
if user_api is None:
user_api = _ALL_USER_APIS
elif user_api in _ALL_USER_APIS:
user_api = [user_api]
else:
raise ValueError(
f"user_api must be either in {_ALL_USER_APIS} or None. Got "
f"{user_api} instead."
)
if limits is not None:
limits = {api: limits for api in user_api}
prefixes = []
else:
if isinstance(limits, list):
# This should be a list of dicts of library info, for
# compatibility with the result from threadpool_info.
limits = {
lib_info["prefix"]: lib_info["num_threads"] for lib_info in limits
}
elif isinstance(limits, ThreadpoolController):
# To set the limits from the library controllers of a
# ThreadpoolController object.
limits = {
lib_controller.prefix: lib_controller.num_threads
for lib_controller in limits.lib_controllers
}
if not isinstance(limits, dict):
raise TypeError(
"limits must either be an int, a list, a dict, or "
f"'sequential_blas_under_openmp'. Got {type(limits)} instead"
)
# With a dictionary, can set both specific limit for given
# libraries and global limit for user_api. Fetch each separately.
prefixes = [prefix for prefix in limits if prefix in _ALL_PREFIXES]
user_api = [api for api in limits if api in _ALL_USER_APIS]
return limits, user_api, prefixes
def _set_threadpool_limits(self):
"""Change the maximal number of threads in selected thread pools.
Return a list with all the supported libraries that have been found
matching `self._prefixes` and `self._user_api`.
"""
if self._limits is None:
return
for lib_controller in self._controller.lib_controllers:
# self._limits is a dict {key: num_threads} where key is either
# a prefix or a user_api. If a library matches both, the limit
# corresponding to the prefix is chosen.
if lib_controller.prefix in self._limits:
num_threads = self._limits[lib_controller.prefix]
elif lib_controller.user_api in self._limits:
num_threads = self._limits[lib_controller.user_api]
else:
continue
if num_threads is not None:
lib_controller.set_num_threads(num_threads)
class _ThreadpoolLimiterDecorator(_ThreadpoolLimiter, ContextDecorator):
"""Same as _ThreadpoolLimiter but to be used as a decorator"""
def __init__(self, controller, *, limits=None, user_api=None):
self._limits, self._user_api, self._prefixes = self._check_params(
limits, user_api
)
self._controller = controller
def __enter__(self):
# we need to set the limits here and not in the __init__ because we want the
# limits to be set when calling the decorated function, not when creating the
# decorator.
self._original_info = self._controller.info()
self._set_threadpool_limits()
return self
@_format_docstring(
USER_APIS=", ".join(f'"{api}"' for api in _ALL_USER_APIS),
BLAS_LIBS=", ".join(_ALL_BLAS_LIBRARIES),
OPENMP_LIBS=", ".join(_ALL_OPENMP_LIBRARIES),
)
class threadpool_limits(_ThreadpoolLimiter):
"""Change the maximal number of threads that can be used in thread pools.
This object can be used either as a callable (the construction of this object
limits the number of threads), as a context manager in a `with` block to
automatically restore the original state of the controlled libraries when exiting
the block, or as a decorator through its `wrap` method.
Set the maximal number of threads that can be used in thread pools used in
the supported libraries to `limit`. This function works for libraries that
are already loaded in the interpreter and can be changed dynamically.
This effect is global and impacts the whole Python process. There is no thread level
isolation as these libraries do not offer thread-local APIs to configure the number
of threads to use in nested parallel calls.
Parameters
----------
limits : int, dict, 'sequential_blas_under_openmp' or None (default=None)
The maximal number of threads that can be used in thread pools
- If int, sets the maximum number of threads to `limits` for each
library selected by `user_api`.
- If it is a dictionary `{{key: max_threads}}`, this function sets a
custom maximum number of threads for each `key` which can be either a
`user_api` or a `prefix` for a specific library.
- If 'sequential_blas_under_openmp', it will chose the appropriate `limits`
and `user_api` parameters for the specific use case of sequential BLAS
calls within an OpenMP parallel region. The `user_api` parameter is
ignored.
- If None, this function does not do anything.
user_api : {USER_APIS} or None (default=None)
APIs of libraries to limit. Used only if `limits` is an int.
- If "blas", it will only limit BLAS supported libraries ({BLAS_LIBS}).
- If "openmp", it will only limit OpenMP supported libraries
({OPENMP_LIBS}). Note that it can affect the number of threads used
by the BLAS libraries if they rely on OpenMP.
- If None, this function will apply to all supported libraries.
"""
def __init__(self, limits=None, user_api=None):
super().__init__(ThreadpoolController(), limits=limits, user_api=user_api)
@classmethod
def wrap(cls, limits=None, user_api=None):
return super().wrap(ThreadpoolController(), limits=limits, user_api=user_api)
class ThreadpoolController:
"""Collection of LibController objects for all loaded supported libraries
Attributes
----------
lib_controllers : list of `LibController` objects
The list of library controllers of all loaded supported libraries.
"""
# Cache for libc under POSIX and a few system libraries under Windows.
# We use a class level cache instead of an instance level cache because
# it's very unlikely that a shared library will be unloaded and reloaded
# during the lifetime of a program.
_system_libraries = dict()
def __init__(self):
self.lib_controllers = []
self._load_libraries()
self._warn_if_incompatible_openmp()
@classmethod
def _from_controllers(cls, lib_controllers):
new_controller = cls.__new__(cls)
new_controller.lib_controllers = lib_controllers
return new_controller
def info(self, debugging_info: bool = False):
"""Return lib_controllers info as a list of dicts.
Parameters
----------
debugging_info : bool
Include extra fields which require more intrusive actions to
obtain, and which can't always reliably be determined.
"""
return [
lib_controller.info(debugging_info=debugging_info)
for lib_controller in self.lib_controllers
]
def select(self, **kwargs):
"""Return a ThreadpoolController containing a subset of its current
library controllers
It will select all libraries matching at least one pair (key, value) from kwargs
where key is an entry of the library info dict (like "user_api", "internal_api",
"prefix", ...) and value is the value or a list of acceptable values for that
entry.
For instance, `ThreadpoolController().select(internal_api=["blis", "openblas"])`
will select all library controllers whose internal_api is either "blis" or
"openblas".
"""
for key, vals in kwargs.items():
kwargs[key] = [vals] if not isinstance(vals, list) else vals
lib_controllers = [
lib_controller
for lib_controller in self.lib_controllers
if any(
getattr(lib_controller, key, None) in vals
for key, vals in kwargs.items()
)
]
return ThreadpoolController._from_controllers(lib_controllers)
def _get_params_for_sequential_blas_under_openmp(self):
"""Return appropriate params to use for a sequential BLAS call in an OpenMP loop
This function takes into account the unexpected behavior of OpenBLAS with the
OpenMP threading layer.
"""
if self.select(
internal_api="openblas", threading_layer="openmp"
).lib_controllers:
return {"limits": None, "user_api": None}
return {"limits": 1, "user_api": "blas"}
@_format_docstring(
USER_APIS=", ".join('"{}"'.format(api) for api in _ALL_USER_APIS),
BLAS_LIBS=", ".join(_ALL_BLAS_LIBRARIES),
OPENMP_LIBS=", ".join(_ALL_OPENMP_LIBRARIES),
)
def limit(self, *, limits=None, user_api=None):
"""Change the maximal number of threads that can be used in thread pools.
This function returns an object that can be used either as a callable (the
construction of this object limits the number of threads) or as a context
manager, in a `with` block to automatically restore the original state of the
controlled libraries when exiting the block.
Set the maximal number of threads that can be used in thread pools used in
the supported libraries to `limits`. This function works for libraries that
are already loaded in the interpreter and can be changed dynamically.
This effect is global and impacts the whole Python process. There is no thread
level isolation as these libraries do not offer thread-local APIs to configure
the number of threads to use in nested parallel calls.
Parameters
----------
limits : int, dict, 'sequential_blas_under_openmp' or None (default=None)
The maximal number of threads that can be used in thread pools
- If int, sets the maximum number of threads to `limits` for each
library selected by `user_api`.
- If it is a dictionary `{{key: max_threads}}`, this function sets a
custom maximum number of threads for each `key` which can be either a
`user_api` or a `prefix` for a specific library.
- If 'sequential_blas_under_openmp', it will chose the appropriate `limits`
and `user_api` parameters for the specific use case of sequential BLAS
calls within an OpenMP parallel region. The `user_api` parameter is
ignored.
- If None, this function does not do anything.
user_api : {USER_APIS} or None (default=None)
APIs of libraries to limit. Used only if `limits` is an int.
- If "blas", it will only limit BLAS supported libraries ({BLAS_LIBS}).
- If "openmp", it will only limit OpenMP supported libraries
({OPENMP_LIBS}). Note that it can affect the number of threads used
by the BLAS libraries if they rely on OpenMP.
- If None, this function will apply to all supported libraries.
"""
return _ThreadpoolLimiter(self, limits=limits, user_api=user_api)
@_format_docstring(
USER_APIS=", ".join('"{}"'.format(api) for api in _ALL_USER_APIS),
BLAS_LIBS=", ".join(_ALL_BLAS_LIBRARIES),
OPENMP_LIBS=", ".join(_ALL_OPENMP_LIBRARIES),
)
def wrap(self, *, limits=None, user_api=None):
"""Change the maximal number of threads that can be used in thread pools.
This function returns an object that can be used as a decorator.
Set the maximal number of threads that can be used in thread pools used in
the supported libraries to `limits`. This function works for libraries that
are already loaded in the interpreter and can be changed dynamically.
Parameters
----------
limits : int, dict or None (default=None)
The maximal number of threads that can be used in thread pools
- If int, sets the maximum number of threads to `limits` for each
library selected by `user_api`.
- If it is a dictionary `{{key: max_threads}}`, this function sets a
custom maximum number of threads for each `key` which can be either a
`user_api` or a `prefix` for a specific library.
- If None, this function does not do anything.
user_api : {USER_APIS} or None (default=None)
APIs of libraries to limit. Used only if `limits` is an int.
- If "blas", it will only limit BLAS supported libraries ({BLAS_LIBS}).
- If "openmp", it will only limit OpenMP supported libraries
({OPENMP_LIBS}). Note that it can affect the number of threads used
by the BLAS libraries if they rely on OpenMP.
- If None, this function will apply to all supported libraries.
"""
return _ThreadpoolLimiter.wrap(self, limits=limits, user_api=user_api)
def __len__(self):
return len(self.lib_controllers)
def _load_libraries(self):
"""Loop through loaded shared libraries and store the supported ones"""
# ctypes.util is not imported on Linux: on CPython 3.14 it allocates a
# process-lifetime CFUNCTYPE callback that is not fork-safe with some
# libffi builds (#225). dllist also uses dl_iterate_phdr internally
# (#239), which we already avoid on Linux via /proc/self/maps.
dllist = None
if sys.platform not in ("linux", "emscripten"):
try:
from ctypes.util import dllist
except ImportError:
# CPython before 3.14 does not provide dll inspection.
dllist = None
if sys.platform == "linux" and os.path.exists("/proc/self/maps"):
# On glibc, dl_iterate_phdr has an internal lock, and that plus
# calling back into Python and the need to (re)acquire the GIL
# results in deadlocks. To avoid that, use a Linux-specific
# mechanism that doesn't have these issues; since it's Linux, musl
# works fine too.
self._find_libraries_with_linux()
elif dllist is not None:
# On Python 3.14+, this functionality is built-in. Once Python 3.13
# is no longer supported by threadpoolctl, most of the equivalent
# threadpoolctl implementations can be removed.
self._find_libraries_with_python(dllist)
elif sys.platform == "darwin":
self._find_libraries_with_dyld()
elif sys.platform == "win32":
self._find_libraries_on_windows()
elif "pyodide" in sys.modules:
self._find_libraries_pyodide()
else:
# Non-Linux Unix platforms.
self._find_libraries_with_dl_iterate_phdr()
def _find_libraries_with_linux(self):
"""Loop through loaded libraries and return binders on supported ones
Uses a Linux-specific mechanism:
https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html
"""
with open("/proc/self/maps") as f:
maps = f.read()
filepaths = set()
for line in maps.splitlines():
start_index = line.find("/")
if start_index == -1 or ".so" not in line:
continue
filepath = line[start_index:]
if os.path.exists(filepath):
filepaths.add(filepath)
for filepath in filepaths:
self._make_controller_from_path(filepath)
def _find_libraries_with_python(self, dllist):
"""Loop through loaded libraries and return binders on supported ones
Uses Python's built-in support for this functionality.
"""
try:
filepaths = dllist()
except OSError as exc:
# No Toolhelp fallback: if this fires in the wild, collect a
# minimal reproducer and report it upstream so CPython's dllist
# can be hardened against concurrent module list changes.
warnings.warn(
"ctypes.util.dllist failed to list loaded libraries "
f"({exc!r}). Native thread pools will not be inspected for "
"this ThreadpoolController. Please report a minimal "
"reproducer at https://github.com/joblib/threadpoolctl/issues "
"so it can be investigated and possibly be reported upstream to "
"CPython.",
RuntimeWarning,
)
return
if filepaths and filepaths[0] in ("", sys.executable):
filepaths = filepaths[1:]
for filepath in filepaths:
self._make_controller_from_path(filepath)
def _find_libraries_with_dl_iterate_phdr(self):
"""Loop through loaded libraries and return binders on supported ones
This function is expected to work on POSIX system only.
This code is adapted from code by Intel developer @anton-malakhov
available at https://github.com/IntelPython/smp
Copyright (c) 2017, Intel Corporation published under the BSD 3-Clause
license
"""
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 []
filepaths = []
# Callback function for `dl_iterate_phdr` which is called for every
# library loaded in the current process until it returns 1. To minimize
# the potential for deadlocks (see #228), this code should not do
# anything that might result in reentrancy into the library, the dl
# system, or anything else.
def match_library_callback(info, size, data):
# Get the path of the current library
filepath = info.contents.dlpi_name
if filepath:
filepaths.append(filepath)
return 0
c_func_signature = ctypes.CFUNCTYPE(
ctypes.c_int, # Return type
ctypes.POINTER(_dl_phdr_info),
ctypes.c_size_t,
ctypes.c_char_p,
)
c_match_library_callback = c_func_signature(match_library_callback)
data = ctypes.c_char_p(b"")
libc.dl_iterate_phdr(c_match_library_callback, data)
# Now that a list of filepaths is available, load the respective
# libraries:
for filepath in filepaths:
# Store the library controller if it is supported and selected
self._make_controller_from_path(filepath.decode("utf-8"))
def _find_libraries_with_dyld(self):
"""Loop through loaded libraries and return binders on supported ones
This function is expected to work on OSX system only
"""
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()
libc._dyld_get_image_name.restype = ctypes.c_char_p
for i in range(n_dyld):
filepath = ctypes.string_at(libc._dyld_get_image_name(i))
filepath = filepath.decode("utf-8")
# Store the library controller if it is supported and selected
self._make_controller_from_path(filepath)
def _find_libraries_on_windows(self):
"""Loop through loaded libraries and return binders on supported ones
This function is expected to work on windows system only.
Used when ``ctypes.util.dllist`` is unavailable (Python < 3.14).
Module discovery uses a snapshot-first strategy:
``CreateToolhelp32Snapshot`` provides an atomic list of loaded
modules, which is more robust than ``EnumProcessModulesEx`` under
concurrent DLL load/unload. Paths that fit in
``MODULEENTRY32W.szExePath`` (shorter than ``MAX_PATH``) are used as-is.
Truncated or empty snapshot paths are resolved with
``GetModuleFileNameW`` and, when needed, ``GetModuleFileNameExW`` with a
larger buffer. If snapshot creation fails, enumeration falls back to
``EnumProcessModulesEx``.
"""
from ctypes.wintypes import MAX_PATH
ps_api = self._get_windll("Psapi")
kernel_32 = self._get_windll("kernel32")
self._setup_windows_module_apis(ps_api, kernel_32)
h_process = kernel_32.GetCurrentProcess()
# Allocate a buffer for long path names; see _WINDOWS_MAX_LIBRARY_PATH_LENGTH.
max_path = _WINDOWS_MAX_LIBRARY_PATH_LENGTH
path_buf = ctypes.create_unicode_buffer(max_path)
try:
modules = self._snapshot_loaded_modules(kernel_32)
except OSError:
modules = None
if modules is not None:
for h_module, snapshot_path in modules:
if snapshot_path and len(snapshot_path) < MAX_PATH - 1:
filepath = snapshot_path
else:
filepath = self._resolve_module_filepath(
ps_api,
kernel_32,
h_process,
h_module,
max_path=max_path,
path_buf=path_buf,
)
if filepath is not None:
self._make_controller_from_path(filepath)
else:
self._find_libraries_with_enum_process_modules_ex(
ps_api, kernel_32, h_process, max_path, path_buf
)
@classmethod
def _setup_windows_module_apis(cls, ps_api, kernel_32):
"""Set ctypes signatures for Windows module enumeration APIs."""
from ctypes.wintypes import BOOL, DWORD, HANDLE, HMODULE
if getattr(cls, "_windows_module_apis_configured", False):
return
kernel_32.GetCurrentProcess.restype = HANDLE
kernel_32.CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD]
kernel_32.CreateToolhelp32Snapshot.restype = HANDLE
kernel_32.Module32FirstW.argtypes = [HANDLE, ctypes.c_void_p]
kernel_32.Module32FirstW.restype = BOOL
kernel_32.Module32NextW.argtypes = [HANDLE, ctypes.c_void_p]
kernel_32.Module32NextW.restype = BOOL
kernel_32.GetModuleFileNameW.argtypes = [HMODULE, ctypes.c_wchar_p, DWORD]
kernel_32.GetModuleFileNameW.restype = DWORD
kernel_32.CloseHandle.argtypes = [HANDLE]
kernel_32.CloseHandle.restype = BOOL
ps_api.EnumProcessModulesEx.argtypes = [
HANDLE,
ctypes.POINTER(HMODULE),
DWORD,
ctypes.POINTER(DWORD),
DWORD,
]
ps_api.EnumProcessModulesEx.restype = BOOL
ps_api.GetModuleFileNameExW.argtypes = [
HANDLE,
HMODULE,
ctypes.c_wchar_p,
DWORD,
]
ps_api.GetModuleFileNameExW.restype = DWORD
cls._windows_module_apis_configured = True
def _snapshot_loaded_modules(self, kernel_32):
"""Return loaded modules as (hModule, snapshot_path) pairs.
Uses CreateToolhelp32Snapshot for an atomic view of loaded modules, which
is more robust than EnumProcessModulesEx when DLLs are loaded or unloaded
concurrently. ``ERROR_BAD_LENGTH`` is retried a bounded number of times
(the documented transient race when the module list changes mid-snapshot)
and then raised as ``OSError`` so the caller can fall back.
"""
from ctypes.wintypes import DWORD, HANDLE, MAX_PATH
class MODULEENTRY32W(ctypes.Structure):
_fields_ = [
("dwSize", DWORD),
("th32ModuleID", DWORD),
("th32ProcessID", DWORD),
("GlblcntUsage", DWORD),
("ProccntUsage", DWORD),
("modBaseAddr", ctypes.POINTER(ctypes.c_byte)),
("modBaseSize", DWORD),
("hModule", HANDLE),
("szModule", ctypes.c_wchar * 256),
("szExePath", ctypes.c_wchar * MAX_PATH),
]
TH32CS_SNAPMODULE = 0x00000008
TH32CS_SNAPMODULE32 = 0x00000010
ERROR_BAD_LENGTH = 0x0018
ERROR_NO_MORE_FILES = 0x0012
INVALID_HANDLE_VALUE = HANDLE(-1).value
max_snapshot_retries = 16
for _ in range(max_snapshot_retries):
snap_handle = kernel_32.CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, os.getpid()
)
if snap_handle != INVALID_HANDLE_VALUE:
break
err = ctypes.get_last_error()
if err == ERROR_BAD_LENGTH:
continue
msg = ctypes.FormatError(err).strip()
raise OSError(f"CreateToolhelp32Snapshot failed: {msg}")
else:
msg = ctypes.FormatError(ERROR_BAD_LENGTH).strip()
raise OSError(f"CreateToolhelp32Snapshot failed: {msg}")
modules = []
try:
lib_entry = MODULEENTRY32W()
lib_entry.dwSize = ctypes.sizeof(MODULEENTRY32W)
if not kernel_32.Module32FirstW(
snap_handle, ctypes.byref(lib_entry)
): # pragma: no cover
err = ctypes.get_last_error()
msg = ctypes.FormatError(err).strip()
raise OSError(f"Module32FirstW failed: {msg}")
while True:
modules.append((lib_entry.hModule, lib_entry.szExePath))
if not kernel_32.Module32NextW(snap_handle, ctypes.byref(lib_entry)):
err = ctypes.get_last_error()
if err != ERROR_NO_MORE_FILES: # pragma: no cover
msg = ctypes.FormatError(err).strip()
raise OSError(f"Module32NextW failed: {msg}")
break
finally:
kernel_32.CloseHandle(snap_handle)
return modules
def _resolve_module_filepath(
self,
ps_api,
kernel_32,
h_process,
h_module,
max_path,
path_buf,
):
"""Return the full path for a module, or None if it should be skipped."""
from ctypes.wintypes import MAX_PATH
n_size = kernel_32.GetModuleFileNameW(h_module, path_buf, MAX_PATH)
if n_size and n_size < MAX_PATH - 1:
return path_buf.value
n_size = ps_api.GetModuleFileNameExW(h_process, h_module, path_buf, max_path)
if n_size and n_size < max_path - 1:
return path_buf.value
if n_size: # pragma: no cover
filepath = path_buf.value
warnings.warn(
"Could not get the full path of a dynamic library (path too "
"long). This library will be ignored and threadpoolctl might "
"not be able to control or display information about all "
f"loaded libraries. Here's the truncated path: {filepath!r}",
RuntimeWarning,
)
return None
err = ctypes.get_last_error()
msg = ctypes.FormatError(err).strip()
warnings.warn(
"Could not get the path of a dynamic library "
"(GetModuleFileNameW and GetModuleFileNameExW failed). "
"This library will be ignored and threadpoolctl might not be "
"able to control or display information about all loaded "
f"libraries. {msg}",
RuntimeWarning,
)
return None
def _find_libraries_with_enum_process_modules_ex(
self, ps_api, kernel_32, h_process, max_path, path_buf
):
"""Fallback Windows module enumeration using EnumProcessModulesEx.
This code is adapted from code by Philipp Hagemeister @phihag available
at https://stackoverflow.com/questions/17474574
"""
from ctypes.wintypes import DWORD, HMODULE
LIST_LIBRARIES_ALL = 0x03
buf_count = 256
needed = DWORD()
# Grow the buffer until it becomes large enough to hold all the
# module headers
while True:
buf = (HMODULE * buf_count)()
buf_size = ctypes.sizeof(buf)
if not ps_api.EnumProcessModulesEx(
h_process,
buf,
buf_size,
ctypes.byref(needed),
LIST_LIBRARIES_ALL,
):
raise OSError("EnumProcessModulesEx failed")
if buf_size >= needed.value:
break
buf_count = needed.value // (buf_size // buf_count)
count = needed.value // (buf_size // buf_count)
for h_module in map(HMODULE, buf[:count]):
filepath = self._resolve_module_filepath(
ps_api,
kernel_32,
h_process,
h_module,
max_path=max_path,
path_buf=path_buf,
)
if filepath is not None:
self._make_controller_from_path(filepath)
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
if hasattr(LDSO.loadedLibsByName, "as_py_json"): # Pyodide >= 0.29
libs_iter = LDSO.loadedLibsByName.as_py_json()
else:
libs_iter = LDSO.loadedLibsByName.as_object_map() # Pyodide < 0.29
for filepath in libs_iter:
# 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
filepath = _realpath(filepath)
# `lower` required to take account of OpenMP dll case on Windows
# (vcomp, VCOMP, Vcomp, ...)
filename = os.path.basename(filepath).lower()
# Loop through supported libraries to find if this filename corresponds
# to a supported one.
for controller_class in _ALL_CONTROLLERS:
# check if filename matches a supported prefix
prefix = self._check_prefix(filename, controller_class.filename_prefixes)
# filename does not match any of the prefixes of the candidate
# library. move to next library.
if prefix is None:
continue
# Legacy workaround for BLAS libraries that conda-forge used to expose
# on Windows as libblas.dll, disambiguated via implementation-specific
# symbols. Current conda-forge stacks no longer load libblas.dll (e.g.
# MKL is exposed as mkl_rt..dll instead), so this path is
# kept for older installs but cannot be exercised in today's CI.
if prefix == "libblas":
if filename.endswith(".dll"):
libblas = ctypes.CDLL(filepath, _RTLD_NOLOAD)
if not any(
hasattr(libblas, func)
for func in controller_class.check_symbols
):
continue
else:
# Non-Windows libblas DSOs (e.g. from openblas) lack the symbols
# needed to instantiate a controller and would duplicate entries.
continue
# 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
)
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
Return None if none matches.
"""
for prefix in filename_prefixes:
if library_basename.startswith(prefix):
return prefix
return None
def _warn_if_incompatible_openmp(self):
"""Raise a warning if llvm-OpenMP and intel-OpenMP are both loaded"""
if sys.platform != "linux":
# The incompatibility between libomp and libiomp is known to cause
# crashes on Linux. On other platforms, conda-forge may expose both
# libraries without the same runtime conflict (for instance when
# libiomp forwards to libomp on Windows).
return
prefixes = [lib_controller.prefix for lib_controller in self.lib_controllers]
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
same Python program.
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)
@classmethod
def _get_libc(cls):
"""Load the lib-C for unix systems."""
libc = cls._system_libraries.get("libc")
if libc is None:
# dlopen(NULL) rather than ctypes.util.find_library("c"). Importing
# ctypes.util on CPython 3.14 Linux creates a process-lifetime
# CFUNCTYPE callback that is not fork-safe with some libffi builds
# (issue #225). If libc is statically linked or Python is linked
# against musl, the main program still exports the libc symbols we
# need. If it does not, we check for those symbols later anyway.
libc = ctypes.CDLL(None, mode=_RTLD_NOLOAD)
cls._system_libraries["libc"] = libc
return libc
@classmethod
def _get_windll(cls, dll_name):
"""Load a windows DLL"""
dll = cls._system_libraries.get(dll_name)
if dll is None:
dll = ctypes.WinDLL(f"{dll_name}.dll", use_last_error=True)
cls._system_libraries[dll_name] = dll
return dll
def _main():
"""Commandline interface to display thread-pool information and exit."""
import argparse
import importlib
import json
import sys
parser = argparse.ArgumentParser(
usage="python -m threadpoolctl -i numpy scipy.linalg xgboost",
description="Display thread-pool information and exit.",
)
parser.add_argument(
"-i",
"--import",
dest="modules",
nargs="*",
default=(),
help="Python modules to import before introspecting thread-pools.",
)
parser.add_argument(
"-c",
"--command",
help="a Python statement to execute before introspecting thread-pools.",
)
options = parser.parse_args(sys.argv[1:])
for module in options.modules:
try:
importlib.import_module(module, package=None)
except ImportError:
print("WARNING: could not import", module, file=sys.stderr)
if options.command:
exec(options.command)
print(json.dumps(threadpool_info(debugging_info=True), indent=2))
if __name__ == "__main__":
_main()