threadpoolctl

Introspect and limit the number of threads used by native BLAS and OpenMP libraries inside a Python process.

Library
PyPI
v3.6.0
431stars
BSD 3-Clause License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
63/100Good
Development Activity68
Maintenance44
Community60
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
79/100Good
Architecture78
Code Quality82
Innovation80
Learning Curve75

threadpoolctl is a small, single-file Python library maintained by the scikit-learn/joblib organization that solves a specific problem in scientific-computing stacks: native libraries like OpenBLAS, MKL, BLIS, and OpenMP runtimes each spin up their own internal thread pools, and when several of them are loaded at once (which happens constantly once NumPy, SciPy, and a joblib-based parallel loop are all in play) they oversubscribe the machine’s CPU cores. threadpoolctl walks the shared libraries already loaded into the current process, matches them against known filename prefixes for each supported backend, and exposes a uniform ThreadpoolController API to read and set each one’s thread count at runtime.

The library ships both a programmatic API (threadpool_info(), threadpool_limits(), ThreadpoolController) and a command-line entry point (python -m threadpoolctl -i numpy scipy.linalg) that dumps a JSON snapshot of every detected thread pool. Limits can be applied globally, scoped to a with block, or applied as a function decorator via .wrap(), and the library takes care of restoring the prior state afterward. It also supports the more experimental FlexiBLAS backend-switching API, letting a program swap which BLAS implementation a process is delegating to at runtime.

Because it operates by inspecting loaded shared libraries via ctypes and dl_iterate_phdr-style introspection rather than requiring any special build flags from the libraries it controls, threadpoolctl works as a drop-in dependency for any project that wants deterministic, oversubscription-free nested parallelism — most notably scikit-learn and joblib themselves, which depend on it directly to keep BLAS calls well-behaved inside their own parallel loops.

What You Get

  • Runtime introspection - threadpool_info() returns a JSON-serializable list describing every detected native thread pool: its filepath, internal API, version, and current thread count.
  • Scoped thread limiting - threadpool_limits() and ThreadpoolController.limit() cap thread counts either process-wide or for the duration of a with block, then restore the previous state automatically.
  • Object-oriented controller - ThreadpoolController caches the set of loaded libraries once so repeated .limit()/.select() calls don’t re-scan the process each time.
  • Function-level decorators - .wrap() on both the class-based and function-based APIs lets you pin a specific function’s thread budget without touching its call sites.
  • Command-line introspection - python -m threadpoolctl -i <module> prints a full thread-pool report after importing arbitrary modules, useful for debugging oversubscription outside of a Python session.
  • Extensible controller registry - a register() function and abstract LibController base class let you add support for additional native libraries that expose their own get/set thread APIs.

Common Use Cases

  • Nested parallelism in scikit-learn/joblib pipelines - wrapping a BLAS-heavy inner loop in threadpool_limits(limits=1, user_api='blas') while an outer joblib Parallel loop uses multiple processes, avoiding CPU oversubscription.
  • Diagnosing multi-OpenMP-runtime crashes - using threadpool_info() to see that both libomp (LLVM/clang) and libiomp (Intel) are loaded simultaneously, which is a common source of segfaults.
  • Deployment environment debugging - running python -m threadpoolctl -i numpy scipy in a container or CI job to confirm which BLAS backend (OpenBLAS vs MKL) actually got linked at runtime.
  • Deterministic benchmarking - pinning BLAS/OpenMP thread counts to 1 before micro-benchmarking single-threaded code paths so background thread-pool activity doesn’t skew timing results.
  • Dynamic FlexiBLAS backend switching - selecting a specific installed BLAS backend (e.g. switching from a reference NETLIB build to OpenBLAS) at runtime without restarting the process.

Under The Hood

Architecture The library is organized around one abstract base class, LibController, and a family of concrete subclasses (OpenBLASController, BLISController, FlexiBLASController, MKLController, OpenMPController in threadpoolctl.py) that each declare filename_prefixes and check_symbols used to recognize a loaded shared library, plus get_num_threads/set_num_threads/get_version implementations that call into the library’s native symbols via ctypes. The top-level ThreadpoolController class walks the process’s loaded shared objects, instantiates the matching controller for each one, and caches the resulting lib_controllers list; .select() filters that list by attributes like user_api or internal_api, and .limit()/threadpool_limits() (built on an internal _ThreadpoolLimiter plus a ContextDecorator subclass) apply and later restore thread limits across the selected controllers. Changing the core LibController contract would ripple through every backend-specific subclass, since each depends on the same _get_symbol/info() machinery from the base class.

Tech Stack Pure Python (with a small amount of Cython under tests/_pyMylib for test fixtures), built and packaged with flit_core per pyproject.toml, requiring Python >=3.9. The only runtime dependency the library itself needs is the standard library’s ctypes module for dynamic-library introspection; it deliberately avoids depending on NumPy/SciPy so it can be a lightweight transitive dependency of libraries that do. Distribution is via PyPI with GitHub Actions handling build/publish on tagged releases through PyPI’s trusted-publishing flow.

Code Quality The project has an extensive pytest-based test suite (tests/test_threadpoolctl.py at over 900 lines, plus a dedicated test_api_introspection.py and helper modules), including tests that spawn subprocesses to check behavior across distinct OpenMP runtimes and that conditionally skip cases depending on which BLAS backend and threading layer are actually present on the CI machine. CI (.github/workflows/test.yml) runs a linting job with black in check mode ahead of the test matrix. Functions are documented with numpy-style docstrings throughout, and the public API uses typing.Literal and Callable type hints for scope enums, though the codebase is not fully statically typed end-to-end.

What Makes It Unique Unlike most numerical libraries that only expose their own thread-count knob, threadpoolctl provides a single cross-vendor API that recognizes and manages OpenBLAS, MKL, BLIS, FlexiBLAS, and OpenMP simultaneously by fingerprinting already-loaded shared libraries at runtime rather than requiring build-time cooperation from them. Its handling of OpenBLAS’s dual behavior (native pthreads vs. delegating to an OpenMP runtime) and its documented, tested workaround for the sequential_blas_under_openmp case reflect detailed, hard-won knowledge of real oversubscription bugs across the BLAS/OpenMP ecosystem that most projects simply don’t attempt to solve generally.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search