From 5ea1d824d6f41e2489b3078271f75454ca23038c Mon Sep 17 00:00:00 2001 From: Arthur Lacote Date: Tue, 20 Oct 2026 08:55:26 +0200 Subject: [PATCH 1/3] DOC Recommend "sequential_blas_under_openmp" for BLAS calls in OpenMP loops Co-Authored-By: Claude Opus 5.5 --- README.md | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3f2b196f..32694bda 100644 --- a/README.md +++ b/README.md @@ -426,11 +426,33 @@ An complete example can be found [here]( ### 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). +Libraries that call BLAS from their own OpenMP parallel regions (for instance +from a Cython `prange` loop) usually want each of these BLAS calls to be +sequential, to avoid oversubscription. For this use case, set +`limits="sequential_blas_under_openmp"` rather than `limits=1` and +`user_api="blas"`: + +```python +from threadpoolctl import ThreadpoolController + +controller = ThreadpoolController() + +with controller.limit(limits="sequential_blas_under_openmp"): + # Compiled code with an OpenMP parallel loop that calls BLAS. + my_openmp_kernel(X) +``` + +With OpenBLAS's OpenMP threading layer, OpenBLAS and the calling code share the +same OpenMP runtime, so `limits=1` and `user_api="blas"` also sets the number of +OpenMP threads of the current thread to 1. Inside the limit, any OpenMP parallel +region started from this thread without an explicit number of threads runs on a +single thread, and `omp_get_max_threads()` returns 1. See also +https://github.com/xianyi/OpenBLAS/issues/2985. + +`limits="sequential_blas_under_openmp"` doesn't change anything in this +configuration: OpenBLAS is already sequential by default when it is called from +an OpenMP parallel region. For the other BLAS libraries and threading layers, +it's the same as `limits=1` and `user_api="blas"`. ### Known Limitations @@ -517,6 +539,10 @@ 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. + threadpoolctl then limits OpenBLAS with `omp_set_num_threads`, so the limit + also applies to the OpenMP parallel regions of the calling code. To get + sequential BLAS calls within an OpenMP parallel region, see + [Sequential BLAS within OpenMP parallel region](#sequential-blas-within-openmp-parallel-region). 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 From 83abca0db2ab3af42f53849882af920ee1ca909f Mon Sep 17 00:00:00 2001 From: Arthur Lacote Date: Tue, 20 Oct 2026 09:09:45 +0200 Subject: [PATCH 2/3] DOC Keep the original sentence, say the option is needed Co-Authored-By: Claude Opus 5.5 --- README.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 32694bda..a7458a63 100644 --- a/README.md +++ b/README.md @@ -426,11 +426,13 @@ An complete example can be found [here]( ### Sequential BLAS within OpenMP parallel region -Libraries that call BLAS from their own OpenMP parallel regions (for instance -from a Cython `prange` loop) usually want each of these BLAS calls to be -sequential, to avoid oversubscription. For this use case, set -`limits="sequential_blas_under_openmp"` rather than `limits=1` and -`user_api="blas"`: +When one wants to have sequential BLAS calls within an OpenMP parallel region, it's +necessary to set `limits="sequential_blas_under_openmp"` since setting `limits=1` and +`user_api="blas"` leads to unexpected behavior for OpenBLAS with the OpenMP threading +layer: OpenBLAS and the calling code share the same OpenMP runtime, so it also sets +the number of OpenMP threads of the current thread to 1. Inside the limit, any OpenMP +parallel region started from this thread without an explicit number of threads runs +on a single thread, and `omp_get_max_threads()` returns 1. ```python from threadpoolctl import ThreadpoolController @@ -442,13 +444,6 @@ with controller.limit(limits="sequential_blas_under_openmp"): my_openmp_kernel(X) ``` -With OpenBLAS's OpenMP threading layer, OpenBLAS and the calling code share the -same OpenMP runtime, so `limits=1` and `user_api="blas"` also sets the number of -OpenMP threads of the current thread to 1. Inside the limit, any OpenMP parallel -region started from this thread without an explicit number of threads runs on a -single thread, and `omp_get_max_threads()` returns 1. See also -https://github.com/xianyi/OpenBLAS/issues/2985. - `limits="sequential_blas_under_openmp"` doesn't change anything in this configuration: OpenBLAS is already sequential by default when it is called from an OpenMP parallel region. For the other BLAS libraries and threading layers, From 2b7a3213328a48a1438777e11c7ae988ad1f2cc5 Mon Sep 17 00:00:00 2001 From: Arthur Lacote Date: Tue, 20 Oct 2026 09:11:28 +0200 Subject: [PATCH 3/3] DOC Move the explanation of the OpenBLAS-OpenMP case to a code comment Co-Authored-By: Claude Opus 5.5 --- README.md | 5 ----- threadpoolctl.py | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a7458a63..73a6c1f6 100644 --- a/README.md +++ b/README.md @@ -444,11 +444,6 @@ with controller.limit(limits="sequential_blas_under_openmp"): my_openmp_kernel(X) ``` -`limits="sequential_blas_under_openmp"` doesn't change anything in this -configuration: OpenBLAS is already sequential by default when it is called from -an OpenMP parallel region. For the other BLAS libraries and threading layers, -it's the same as `limits=1` and `user_api="blas"`. - ### Known Limitations - `threadpool_limits` can fail to limit the number of inner threads when nesting diff --git a/threadpoolctl.py b/threadpoolctl.py index 5ff701ef..2eab17c3 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1048,6 +1048,9 @@ def _get_params_for_sequential_blas_under_openmp(self): if self.select( internal_api="openblas", threading_layer="openmp" ).lib_controllers: + # Don't change anything: OpenBLAS is already sequential by default when it + # is called from an OpenMP parallel region, and limiting it would also + # limit the OpenMP parallel regions of the calling thread. return {"limits": None, "user_api": None} return {"limits": 1, "user_api": "blas"}