You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
threadpool_limits(1, user_api="blas") also limits OpenMP to 1 thread with OpenBLAS's OpenMP threading layer (3.7.0) #272
Since 3.7.0, with OpenBLAS's OpenMP threading layer, limiting BLAS also limits the OpenMP runtime of the current thread: inside threadpool_limits(limits=1, user_api="blas"), omp_get_max_threads() returns 1, so any OpenMP parallel region started from this thread without an explicit num_threads runs on a single thread. With 3.6.0 and the same OpenBLAS (0.3.34), it stays at 8.
It comes from set_num_threads of the OpenBLAS controller, which now calls omp_set_num_threads when the threading layer is OpenMP, to get a limit per thread rather than per process. But OpenBLAS and the caller share the same OpenMP runtime, so the BLAS limit becomes an OpenMP limit too.
In scikit-learn, the usual pattern for nested parallelism is to limit BLAS to 1 thread around Cython code that calls BLAS inside OpenMP prange loops. Any code that reads the number of OpenMP threads inside that limit now gets 1.
For instance, a KMeans fitted under threadpool_limits(1, user_api="blas") now runs its OpenMP code on 1 thread (see below).
I'm not sure what the best fix is, maybe just some documentation?
In order to work well with Python threads that each want their own limit, you really want to use thread-local limiting APIs when available. And this is what we've got with OpenBLAS.
My impression, which I guess was incorrect, was that usually people set same limits on both.
The ideal solution is to have a working openblas_set_num_threads_local() which is a thread-local setting, the way MKL does. Not sure how it'd work with pthreads backend, though...
For this particular use case... I have vague memory that OpenMP has some APIs for constraining nested parallel loops? So you could set the limit with threadpoolctl for the OpenMP prange(), and then use that OpenMP API before calling into BLAS?
We could also revert threadpoolctl to old behavior of using openblas_set_num_threads() which is not ideal since it affects all threads, but does work... if we can prove the possible memory corruption issue above is not a systemic issue with using the global-setting API but e.g. maybe a bug in that specific Cython code, say.
Isn't this what "sequential_blas_under_openmp" limit is for? And threadpoolctl has special behavior for that for OpenBLAS + OpenMP where it doesn't set a limit of 1 (for reasons I don't quite understand) which... maybe will do the right thing?
Sorry, I opened the issue is a bit too fast using an agent, and didn't read it attentively enough.
The reproducer it wrote is pretty uninformative, the real issue is the following:
If you're writing a function with BLAS calls inside an OMP prange, you want to disable parallelism in BLAS so you'll use threadpool_limits(limits=1, user_api="blas"). But under this limit, omp_get_max_threads will return 1 for OpenBLAS built with OpenMP.
Though, it seems "sequential_blas_under_openmp" already does exactly what we need, indeed my agent tells me:
OpenMP builds of OpenBLAS use 1 thread when omp_in_parallel() is true
So what threadpool_limits does is right, the bug is on scikit-learn's side not to use "sequential_blas_under_openmp". Easy to fix! 🎉
Since #273 is merged and the benchmark results in scikit-learn/scikit-learn#35180 show that this solution fixes the problem, let's close the issue on the threadpoolctl side of things.
Activity
cakedev0 commentedon Oct 16, 2026
cc @itamarst
itamarst commentedon Oct 16, 2026
So the motivation for this was:
The ideal solution is to have a working
openblas_set_num_threads_local()which is a thread-local setting, the way MKL does. Not sure how it'd work with pthreads backend, though...For this particular use case... I have vague memory that OpenMP has some APIs for constraining nested parallel loops? So you could set the limit with threadpoolctl for the OpenMP
prange(), and then use that OpenMP API before calling into BLAS?We could also revert threadpoolctl to old behavior of using
openblas_set_num_threads()which is not ideal since it affects all threads, but does work... if we can prove the possible memory corruption issue above is not a systemic issue with using the global-setting API but e.g. maybe a bug in that specific Cython code, say.itamarst commentedon Oct 16, 2026
Oh except second solution means different code paths for pthreads vs OpenMP OpenBLAS...
itamarst commentedon Oct 19, 2026
Isn't this what
"sequential_blas_under_openmp"limit is for? And threadpoolctl has special behavior for that for OpenBLAS + OpenMP where it doesn't set a limit of 1 (for reasons I don't quite understand) which... maybe will do the right thing?itamarst commentedon Oct 19, 2026
We might be able to improve
"sequential_blas_under_openmp"using the hypothetical nested-loops OpenMP limiting API?cakedev0 commentedon Oct 19, 2026
Sorry, I opened the issue is a bit too fast using an agent, and didn't read it attentively enough.
The reproducer it wrote is pretty uninformative, the real issue is the following:
If you're writing a function with BLAS calls inside an OMP prange, you want to disable parallelism in BLAS so you'll use
threadpool_limits(limits=1, user_api="blas"). But under this limit,omp_get_max_threadswill return 1 for OpenBLAS built with OpenMP.Though, it seems
"sequential_blas_under_openmp"already does exactly what we need, indeed my agent tells me:So what
threadpool_limitsdoes is right, the bug is on scikit-learn's side not to use"sequential_blas_under_openmp". Easy to fix! 🎉itamarst commentedon Oct 19, 2026
So that means this issue can be closed?
cakedev0 commentedon Oct 20, 2026
Let's wait for scikit-learn/scikit-learn#35180 to be merged to close this issue.
ogrisel commentedon Oct 20, 2026
Since #273 is merged and the benchmark results in scikit-learn/scikit-learn#35180 show that this solution fixes the problem, let's close the issue on the threadpoolctl side of things.