threadpoolctl Documentation

repository·master·Indexed 19 days ago

https://github.com/joblib/threadpoolctl

Python helpers to limit the number of threads used by native libraries like BLAS and OpenMP to prevent CPU oversubscription in nested parallel workloads. Features include a CLI for inspecting thread-pools, the ThreadpoolController class for runtime introspection and limiting, and support for custom library controllers. Note: Designed for use where BLAS and OpenMP are called from the main Python thread.

Tokens
2.3K
Snippets
7
Records
13
Agent score
15%

What's inside threadpoolctl

  1. What is threadpoolctl and when to use it

    master

    threadpoolctl provides Python helpers to limit the number of threads used by the thread-pool-backed implementations of common native libraries used in scientific computing and data science, such as BLAS and OpenMP.

    Fine control over the underlying thread-pool size is particularly useful for workloads involving nested parallelism to mitigate oversubscription issues (where too many threads compete for CPU resources, degrading performance).

  2. Important threading limitation for threadpoolctl

    master

    Threading Constraint

    threadpoolctl is currently designed for situations where BLAS and OpenMP are only called from the main Python thread.

    To ensure consistent behavior, threadpoolctl and the BLAS/OpenMP APIs should only ever be called from the same, single Python thread.

    Safe use cases include:

    • Configuring a worker in a process pool, where the worker then calls BLAS or OpenMP APIs directly in its main thread.
    • Running code in a Jupyter notebook cell's main thread.

    Unsafe use cases:

    • Calling BLAS/OpenMP APIs and threadpoolctl from multiple different Python threads. This will lead to inconsistent results.
  3. Sequential BLAS within OpenMP parallel regions

    master
    When running sequential BLAS calls inside an OpenMP parallel region, it is safer to use limits="sequential_blas_under_openmp" instead of limits=1, user_api="blas". This ensures expected behavior in certain configurations, such as OpenBLAS with the OpenMP threading layer.
  4. Write a custom library controller

    master

    To control threadpools for native libraries not currently supported (beyond OpenMP and main BLAS), you can implement a custom controller.

    1. Subclass LibController.
    2. Implement the required attributes and methods defined in the LibController docstring.
    3. Register the new class using threadpoolctl.register(YourCustomController).
  5. Workarounds for Intel OpenMP and LLVM OpenMP incompatibility

    master

    If you encounter crashes or deadlocks due to the mix of libomp and libiomp, use one of the following workarounds to ensure only one of the two incompatible libraries is loaded:

    1. Change MKL's threading layer

    Tell MKL (used by NumPy) to use a different threading implementation instead of the Intel OpenMP runtime.

    • On Linux, use the GNU OpenMP runtime:
      export MKL_THREADING_LAYER=GNU
    • On Linux or macOS, use TBB (if installed):
      export MKL_THREADING_LAYER=TBB

    2. Use OpenBLAS instead of MKL

    Install versions of NumPy and SciPy linked against OpenBLAS. This avoids the Intel OpenMP dependency entirely.

    • From PyPI:
      pip install numpy scipy
    • From the conda-forge channel:
      conda install -c conda-forge numpy scipy
    • From the default conda channel:
      conda install numpy scipy blas[build=openblas]

    3. Re-build extensions from source

    Re-build your OpenMP-enabled extensions using GCC or ICC instead of Clang. This allows you to continue using NumPy/SciPy linked against MKL with the default libiomp-based threading layer.

    export MKL_THREADING_LAYER=GNU
    
    # or
    
    export MKL_THREADING_LAYER=TBB
    
    # or
    
    pip install numpy scipy
    
    # or
    
    conda install -c conda-forge numpy scipy
    
    # or
    
    conda install numpy scipy blas[build=openblas]
  6. Known limitations and troubleshooting

    master

    Nesting Parallel Loops

    threadpool_limits may fail to limit inner threads when nesting parallel loops managed by distinct OpenMP implementations (e.g., libgomp from GCC and libomp from Clang). However, it works correctly when limiting BLAS calls nested under an OpenMP loop, even if the BLAS implementation uses a different OpenMP runtime.

    Multiple OpenMP Runtimes

    Using Intel OpenMP (ICC) and LLVM OpenMP (Clang) in the same Python program under Linux can cause issues.

    Inconsistent Scope

    Setting the maximum number of threads for OpenMP and BLAS libraries has inconsistent semantics depending on the library:

    • Thread-local: For libgomp (GCC) or libomp (Clang), the setting is thread-local and affects how many threads are started in the current thread.
    • Process-wide: For OpenBLAS with the pthreads backend or on Windows, the setting is process-wide and impacts a shared thread pool across all threads in the process.
  7. Handle incompatibility between Intel OpenMP and LLVM OpenMP

    master

    On Linux and macOS, loading a mix of compiled extensions linked with libomp (LLVM/Clang) and libiomp (ICC) can cause crashes or deadlocks. This is an unrecoverable incompatibility.

    Note: Using threadpoolctl may trigger these crashes in this specific environment. This issue typically arises when using packages from certain distributions (like conda's default channel) that use LLVM/Clang alongside packages linked against Intel MKL.

  8. Switch the FlexiBLAS backend

    master

    For FlexiBLAS (a BLAS wrapper), you can switch the backend at runtime. This feature is currently experimental. You can switch to a backend predefined at build time (found in available_backends) or provide a direct path to a shared library.

    Warning: This API is experimental and subject to change.

    from threadpoolctl import ThreadpoolController
    import numpy as np
    
    controller = ThreadpoolController()
    # Retrieve the flexiblas controller from the loaded libraries
    flexiblas_ct = controller.select(internal_api="flexiblas").lib_controllers[0]
    
    # Switch to a predefined backend
    flexiblas_ct.switch_backend("OPENBLASPTHREAD")
    
    # Or switch by providing a direct path to a shared library
    flexiblas_ct.switch_backend("/path/to/libmkl_rt.so")
  9. Introspect thread-pools in Python runtime

    master

    Use threadpool_info() to get a list of dictionaries describing the current state of threadpool-enabled runtime libraries loaded in the process. Alternatively, use the object-oriented ThreadpoolController class and its .info() method for similar results.

    from threadpoolctl import threadpool_info
    from pprint import pprint
    
    # Get info as a list of dicts
    pprint(threadpool_info())
    
    # Using the object-oriented API
    from threadpoolctl import ThreadpoolController
    controller = ThreadpoolController()
    pprint(controller.info())
  10. Restrict thread-pool limits to a function scope

    master

    Both threadpool_limits and ThreadpoolController can be used as decorators to set thread limits for a specific function. Use the .wrap() method to create the decorator.

    from threadpoolctl import ThreadpoolController, threadpool_limits
    import numpy as np
    
    controller = ThreadpoolController()
    
    @controller.wrap(limits=1, user_api='blas')
    def my_func():
        # BLAS calls inside this function are limited to 1 thread
        a = np.random.randn(1000, 1000)
        return a @ a
    
    # Alternatively using threadpool_limits directly:
    @threadpool_limits.wrap(limits=1, user_api='blas')
    def my_func_alt():
        pass
  11. Set the maximum size of thread-pools

    master

    You can limit the number of threads used by underlying runtime libraries in specific code blocks using threadpool_limits or ThreadpoolController.limit(). This is useful for preventing oversubscription when using thread-parallelism alongside BLAS/OpenMP calls.

    Note: ThreadpoolController will not act on libraries loaded after the controller is instantiated.

    from threadpoolctl import threadpool_limits
    import numpy as np
    
    # Using the context manager
    with threadpool_limits(limits=1, user_api='blas'):
        a = np.random.randn(1000, 1000)
        a_squared = a @ a
    
    # Using the object-oriented API
    from threadpoolctl import ThreadpoolController
    controller = ThreadpoolController()
    with controller.limit(limits=1, user_api='blas'):
        a = np.random.randn(1000, 1000)
        a_squared = a @ a