From d5487271e28a45154b04a6a392261e7b3cf6fd88 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 7 Jul 2026 16:19:56 -0400 Subject: [PATCH 1/4] Documentation for proposed API. --- README.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65fba02c..1091efca 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ oversubscription issues. pytest ``` -## Usage +## Usage: Introspection and debugging ### Command Line Interface @@ -152,6 +152,104 @@ The state of these libraries is also accessible through the object oriented API: True ``` +## Usage when using Python threads: Restricting Controlled Library Thread Pool Sizes + +There a two scenarios in which you might want to use `threadpoolctl`; each +requires you to use different APIs. + +1. You will be parallelizing work using a Python thread pool, and your goal is + therefore to limit controlled libraries' thread pool sizes when used from + Python threads. +2. 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 section will cover the former case, and the latter is covered in the next +usage section. + +### 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 thread must call a method to limit controlled libraries +in that thread. With Python's `multiprocessing.pool.ThreadPool`, you can do so +by passing in an initializer function that will get called on thread startup. + +```python +from threadpoolctl import LimiterForPythonThreads +from multiprocessing.pool import ThreadPool + +with limit_for_python_threads(limits=1, user_api='blas') as limiter: + # Make sure each Python thread calls limiter.limit_in_pythread(). If you're + # using another thread pool class, you will need to do some other way. + with ThreadPool(4, initializer=limiter.limit_in_pythread) as pool: + # ... run some BLAS-using code in the thread pool ... +``` + +To prevent loading shared libraries repeatedly, you can reuse a +`ThreadpoolController` object: + +```python +from threadpoolctl import ThreadpoolController + +# This won't have any side-effects: +CONTROLLER = ThreadpoolController() + +with CONTROLLER.limit_for_python_threads(limits=1) as limiter: + with ThreadPool(4, initializer=limiter.limit_in_pythread) as pool: + # ... run some BLAS-using code in the thread pool ... + +# Later... +with CONTROLLER.limit_for_python_threads(limits=2) as limiter: + with ThreadPool(2, initializer=limiter.limit_in_pythread) as pool: + # ... run some BLAS-using code in the thread pool ... +``` + +### Switching Back And Forth Between Main Thread and Python Threads + +Unfortunately not all controlled libraries providing limiting APIs that are +thread-specific. 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 = ThreadPool(4) +CONTROLLER = ThreadpoolController() + +# 1. Run some work in a Python thread pool, which then runs in OpenMP. +with CONTROLLER.limit_for_python_threads(limits=1) as limiter: + + def limit_then_do_work(*args, **kwargs): + # Set a limit on OpenMP in the current thread: + limiter.limit_in_pythread() + # Do the actual work: + return do_real_work_with_openmp(*args, **kwargs) + + results = POOL.map(limit_then_do_work, args) + + +# 2. Run some work directly in main thread, using OpenMP. + +# Use API for for non-Python threads, see next section for more: +with CONTROLLER.limit(limits=1): + results2 = do_more_work_with_openmp(results) + + +# 3. Do more work in a Python thread pool: +with CONTROLLER.limit_for_python_threads(limits=1) as limiter: + + def limit_then_do_work2(*args, **kwargs): + limiter.limit_in_pythread() + return do_even_more_real_work_with_openmp(*args, **kwargs) + + results3 = POOL.map(limit_then_do_work2, results2) +``` + +## Usage for no Python threads: Restricting Controlled Library Thread Pool Sizes + +This covers APIs to use when you don't expect to use Python thread pools to parallelize work. + ### Setting the Maximum Size of Thread-Pools Control the number of threads used by the underlying runtime libraries @@ -184,11 +282,17 @@ however not act on libraries loaded after the instantiation of the ... a_squared = a @ a ``` +This should only be used for APIs you expect to be called from the main thread +of a process, without the use of any Python thread pools. + ### 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: +decorators are accessible through their `wrap` method. + +This should only be used for functions you expect to be called from the main +thread of a process, without the use of any Python thread pools. ```python >>> from threadpoolctl import ThreadpoolController, threadpool_limits @@ -205,6 +309,8 @@ decorators are accessible through their `wrap` method: ... ``` +## Usage: Additional APIs and details + ### Switching the FlexiBLAS backend `FlexiBLAS` is a BLAS wrapper for which the BLAS backend can be switched at runtime. @@ -291,6 +397,7 @@ that this part of the API is experimental and subject to change without deprecat 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. From 4cbd44739b27eff223e22b3b9cd3e9bedb1952e5 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 8 Jul 2026 13:52:44 -0400 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Olivier Grisel --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1091efca..94ee5966 100644 --- a/README.md +++ b/README.md @@ -158,10 +158,12 @@ There a two scenarios in which you might want to use `threadpoolctl`; each requires you to use different APIs. 1. You will be parallelizing work using a Python thread pool, and your goal is - therefore to limit controlled libraries' thread pool sizes when used from - Python threads. + 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. 2. You do not expect to use any Python threads, so all the work will be started - directly from the main thread in the process. + directly from the main thread in the process. This is a simple case + where we can globally set thread limits. This section will cover the former case, and the latter is covered in the next usage section. From 1e49e1b43d296a090f9a7cc3716371e40b966609 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 8 Jul 2026 13:58:43 -0400 Subject: [PATCH 3/4] Switch to ThreadPoolExecutor. --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 94ee5966..e76aefa4 100644 --- a/README.md +++ b/README.md @@ -172,17 +172,18 @@ usage section. Limiting thread pool size in controlled libraries requires a two-step process. Importantly, each Python thread must call a method to limit controlled libraries -in that thread. With Python's `multiprocessing.pool.ThreadPool`, you can do so -by passing in an initializer function that will get called on thread startup. +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 LimiterForPythonThreads -from multiprocessing.pool import ThreadPool +from concurrent.futures import ThreadPoolExecutor with limit_for_python_threads(limits=1, user_api='blas') as limiter: # Make sure each Python thread calls limiter.limit_in_pythread(). If you're # using another thread pool class, you will need to do some other way. - with ThreadPool(4, initializer=limiter.limit_in_pythread) as pool: + with ThreadPoolExecutor(4, initializer=limiter.limit_in_pythread) as pool: # ... run some BLAS-using code in the thread pool ... ``` @@ -196,12 +197,12 @@ from threadpoolctl import ThreadpoolController CONTROLLER = ThreadpoolController() with CONTROLLER.limit_for_python_threads(limits=1) as limiter: - with ThreadPool(4, initializer=limiter.limit_in_pythread) as pool: + with ThreadPoolExecutor(4, initializer=limiter.limit_in_pythread) as pool: # ... run some BLAS-using code in the thread pool ... # Later... with CONTROLLER.limit_for_python_threads(limits=2) as limiter: - with ThreadPool(2, initializer=limiter.limit_in_pythread) as pool: + with ThreadPoolExecutor(2, initializer=limiter.limit_in_pythread) as pool: # ... run some BLAS-using code in the thread pool ... ``` @@ -216,7 +217,7 @@ 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 = ThreadPool(4) +POOL = ThreadPoolExecutor(4) CONTROLLER = ThreadpoolController() # 1. Run some work in a Python thread pool, which then runs in OpenMP. From a8e95c23b08b8ab52b68458445def7ee2abbcf09 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 8 Jul 2026 14:02:31 -0400 Subject: [PATCH 4/4] Introduce process-wide limits first. --- README.md | 136 +++++++++++++++++++++++++++--------------------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index e76aefa4..8f0e182d 100644 --- a/README.md +++ b/README.md @@ -152,22 +152,85 @@ The state of these libraries is also accessible through the object oriented API: True ``` -## Usage when using Python threads: Restricting Controlled Library Thread Pool Sizes +## Usage when not using Python threads: Restricting Controlled Library Thread Pool Sizes There a two scenarios in which you might want to use `threadpoolctl`; each requires you to use different APIs. -1. You will be parallelizing work using a Python thread pool, and your goal is +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. -2. 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. 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 +``` + +This should only be used for APIs you expect to be called from the main thread +of a process, without the use of any Python thread pools. + +### 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. + +This should only be used for functions you expect to be called from the main +thread of a process, without the use of any Python thread pools. + +```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. + ### 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. @@ -249,69 +312,6 @@ with CONTROLLER.limit_for_python_threads(limits=1) as limiter: results3 = POOL.map(limit_then_do_work2, results2) ``` -## Usage for no Python threads: Restricting Controlled Library Thread Pool Sizes - -This covers APIs to use when you don't expect to use Python thread pools to parallelize work. - -### 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 -``` - -This should only be used for APIs you expect to be called from the main thread -of a process, without the use of any Python thread pools. - -### 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. - -This should only be used for functions you expect to be called from the main -thread of a process, without the use of any Python thread pools. - -```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: Additional APIs and details ### Switching the FlexiBLAS backend