Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e2caf0c
FIX Do not drop an OpenBLAS claim a limiter scope did not take.
ogrisel Oct 29, 2026
4a7d126
MNT Keep shared-pool claims scoped to the controller class that owns …
ogrisel Oct 30, 2026
2457283
MNT Move shared-pool claim handling onto a reusable mixin.
ogrisel Oct 30, 2026
7e9832a
FIX Let a scoped limit replace a finished controlling thread without …
ogrisel Oct 30, 2026
a9ab477
MNT Share claim walk and rename owner to controller_class.
ogrisel Oct 30, 2026
c2a1c16
MNT Key shared-pool state by filepath and take an optional controller.
ogrisel Oct 30, 2026
35c6ac2
MNT Make ownership counter helpers private and document per-copy cont…
ogrisel Oct 30, 2026
38a6ebf
DOC Describe the manual shared-pool scope API in the changelog.
ogrisel Oct 30, 2026
9874e94
MNT Pin unset non-OpenMP OpenBLAS thread count in the test fixture
ogrisel Oct 30, 2026
67d7278
MNT Rename the shared-pool ownership singleton to CONTROLLING_THREAD_…
ogrisel Oct 30, 2026
e2b0600
DOC Keep the OpenBLAS pthreads/Windows README section unchanged
ogrisel Oct 30, 2026
13f0758
MNT Rename _require_resizable_openblas to _require_resizable_non_open…
ogrisel Oct 30, 2026
e999cf9
MNT Split OpenBLAS claim clearing into per-library and per-class methods
ogrisel Oct 30, 2026
513f001
MNT Remove unused shared-pool snapshot and share holder clearing
ogrisel Oct 30, 2026
640427c
FIX Give each wrapped call its own limiter so recursion releases the …
ogrisel Oct 30, 2026
89c5226
MNT Skip redundant shared-pool syncs and stop locking to check the ho…
ogrisel Oct 30, 2026
ec5885f
FIX Read the controlling thread under its lock when dropping unheld s…
ogrisel Oct 30, 2026
102ba24
FIX Ignore the libomp/libiomp warning while the test fixture scans li…
ogrisel Oct 30, 2026
7eed92d
MNT Assert the shared-pool holder in OpenBLAS ownership tests
ogrisel Oct 30, 2026
4f3b7a3
DOC Explain the OpenBLAS controlling thread in terms of loaded libraries
ogrisel Oct 30, 2026
52cac5c
Apply suggestion from @ogrisel
ogrisel Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
- When OpenBLAS uses a non-OpenMP threading layer, you can only set the size of
its thread pool from a single controlling thread; since it has a shared
process-wide thread pool, having multiple Python threads fight over control
wouldn't work well. A new API
``OpenBLASController.clear_controlling_thread()`` allows resetting which
thread controlls OpenBLAS in these cases, and decorator and context manager
limit APIs will automatically call that API when they are done.
wouldn't work well. Each loaded OpenBLAS library has its own pool and its own
controlling thread. A plain ``threadpool_limits(...)`` call or a direct
``set_num_threads()`` keeps that role until ``clear_controlling_thread()`` on
that library. ``OpenBLASController.clear_all_controlling_threads()`` drops
every loaded OpenBLAS library. Context managers and decorators release it when the
outermost scope that took the claim exits. A thread that has finished no
longer holds the claim; the next ``set_num_threads()`` can take it. The
``CONTROLLING_THREAD_OWNERSHIP`` singleton can be used to manually manage the
scope of ``set_num_threads`` calls.
https://github.com/joblib/threadpoolctl/pull/275
https://github.com/joblib/threadpoolctl/pull/289
https://github.com/joblib/threadpoolctl/pull/290

- Add a new API to get a cached `ThreadpoolController` instance
(`get_cached_controller()`), as an optimization for cases where limits are set
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,17 @@ OpenBLAS+pthreads) and in each worker thread.

> **Why you need both:** When setting a limit on OpenBLAS with pthreads or the
> Windows threading layer, setting the limit affects the whole process, because
> it uses a process-wide shared thread pool. (This in contrast to some other
> BLAS/OpenMP libraries, or OpenBLAS backed by OpenMP, that allow setting the
> limit per-thread.) Having multiple Python threads set limits in this case
> makes no sense, since they would effectively be arguing over a globally shared
> resource. ``threadpoolctl`` therefore constrains the limiting API in the
> OpenBLAS pthreads/Windows case to only work from the first thread that uses
> it, the so-called controlling thread.
> each loaded OpenBLAS library uses one process-wide shared thread pool. (This in
> contrast to some other BLAS/OpenMP libraries, or OpenBLAS backed by OpenMP,
> that allow setting the limit per-thread.) A process can load more than one
> OpenBLAS library, for example one bundled with NumPy and another bundled with
> SciPy, and each has its own pool. Having multiple Python threads set limits on
> the same library makes no sense, since they would be arguing over its pool.
> So each loaded OpenBLAS library has its own controlling thread: the first
> thread to set thread limits on it. Other threads do nothing. The controlling
> thread is released automatically when the ``with`` block or decorated function
> that took it exits, and once that thread has finished. See the docstring of
> ``OpenBLASController`` for more details.
>
> For many other libraries, for example OpenMP, the limit is set per thread. As
> a result, you want to set limits both ways in generic code that doesn't know
Expand Down
38 changes: 35 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import warnings

import pytest

collect_ignore = [
Expand All @@ -9,11 +11,41 @@
]


def _ensure_openblas_thread_count_is_restorable():
"""Replace an unset non-OpenMP OpenBLAS count with the processor count.

``get_num_threads() == 0`` means OpenBLAS has not chosen an active count.
``set_num_threads(0)`` does not return to that state: a non-positive value
means the number of worker threads already created, and that number only
grows. A positive count can be saved and restored.
"""
from threadpoolctl import OpenBLASController, ThreadpoolController

# Building a controller warns when libomp and libiomp are both loaded.
# That warning is the subject of test_libomp_libiomp_warning, and pytest
# runs with -W error, so the fixture must not emit it.
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message=r"\s*Found Intel OpenMP",
category=RuntimeWarning,
)
controller = ThreadpoolController().select(internal_api="openblas")
if controller:
for lib in controller.lib_controllers:
if lib.threading_layer == "openmp" or lib.get_num_threads() != 0:
continue
get_num_procs = lib._get_symbol("openblas_get_num_procs")
nprocs = get_num_procs() if get_num_procs is not None else 1
if nprocs >= 1:
lib.set_num_threads(nprocs)
OpenBLASController.clear_all_controlling_threads(force=True)


@pytest.fixture(autouse=True)
def reset_openblas_controlling_thread():
from threadpoolctl import OpenBLASController

_ensure_openblas_thread_count_is_restorable()
try:
yield
finally:
OpenBLASController.clear_controlling_thread(force=True)
_ensure_openblas_thread_count_is_restorable()
2 changes: 2 additions & 0 deletions continuous_integration/check_no_test_skipped.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
# numpy or scipy and we don't want to pin old versions of these libraries.
SAFE_SKIPPED_TESTS = [
"test_multiple_shipped_openblas",
# Runs only when two distinct non-OpenMP OpenBLAS copies are loaded.
"test_openblas_controlling_thread_is_per_library",
"test_threadpool_limits_by_prefix[1-libblas]",
"test_threadpool_limits_by_prefix[3-libblas]",
]
Expand Down
Loading
Loading