跳转到主要内容

threadpoolctl

项目描述

线程池控制 构建状态 codecov

Python辅助工具,用于限制科学计算和数据科学中使用的常用本地库(例如BLAS和OpenMP)的线程池中使用的线程数。

在涉及嵌套并行的工作负载中,精细控制底层线程池大小可以有助于缓解过度订阅问题。

安装

  • 对于用户,请从 PyPI 安装最新发布的版本

    pip install threadpoolctl
    
  • 对于贡献者,请在开发者模式下从源代码仓库安装

    pip install -r dev-requirements.txt
    flit install --symlink
    

    然后您可以使用 pytest 运行测试

    pytest
    

使用方法

命令行界面

获取导入 Python 包(如 numpy 或 scipy)时初始化的线程池的 JSON 描述

python -m threadpoolctl -i numpy scipy.linalg
[
  {
    "filepath": "/home/ogrisel/miniconda3/envs/tmp/lib/libmkl_rt.so",
    "prefix": "libmkl_rt",
    "user_api": "blas",
    "internal_api": "mkl",
    "version": "2019.0.4",
    "num_threads": 2,
    "threading_layer": "intel"
  },
  {
    "filepath": "/home/ogrisel/miniconda3/envs/tmp/lib/libiomp5.so",
    "prefix": "libiomp",
    "user_api": "openmp",
    "internal_api": "openmp",
    "version": null,
    "num_threads": 4
  }
]

JSON 信息写入到 STDOUT。如果某些包缺失,则会在 STDERR 上显示警告信息。

Python 运行时程序化内省

内省导入 Python 包时加载的线程池启用运行时库的当前状态

>>> from threadpoolctl import threadpool_info
>>> from pprint import pprint
>>> pprint(threadpool_info())
[]

>>> import numpy
>>> pprint(threadpool_info())
[{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libmkl_rt.so',
  'internal_api': 'mkl',
  'num_threads': 2,
  'prefix': 'libmkl_rt',
  'threading_layer': 'intel',
  'user_api': 'blas',
  'version': '2019.0.4'},
 {'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libiomp5.so',
  'internal_api': 'openmp',
  'num_threads': 4,
  'prefix': 'libiomp',
  'user_api': 'openmp',
  'version': None}]

>>> import xgboost
>>> pprint(threadpool_info())
[{'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libmkl_rt.so',
  'internal_api': 'mkl',
  'num_threads': 2,
  'prefix': 'libmkl_rt',
  'threading_layer': 'intel',
  'user_api': 'blas',
  'version': '2019.0.4'},
 {'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libiomp5.so',
  'internal_api': 'openmp',
  'num_threads': 4,
  'prefix': 'libiomp',
  'user_api': 'openmp',
  'version': None},
 {'filepath': '/home/ogrisel/miniconda3/envs/tmp/lib/libgomp.so.1.0.0',
  'internal_api': 'openmp',
  'num_threads': 4,
  'prefix': 'libgomp',
  'user_api': 'openmp',
  'version': None}]

在上面的示例中,numpy是从默认的 anaconda 通道安装的,附带 MKL 和其 Intel OpenMP (libiomp5) 实现,而xgboost是从 pypi.org 安装的,链接到 GNU OpenMP (libgomp),因此两个 OpenMP 运行时都加载到了同一个 Python 程序中。

这些库的状态也可以通过面向对象的 API 访问

>>> from threadpoolctl import ThreadpoolController, threadpool_info
>>> from pprint import pprint
>>> import numpy
>>> controller = ThreadpoolController()
>>> pprint(controller.info())
[{'architecture': 'Haswell',
  'filepath': '/home/jeremie/miniconda/envs/dev/lib/libopenblasp-r0.3.17.so',
  'internal_api': 'openblas',
  'num_threads': 4,
  'prefix': 'libopenblas',
  'threading_layer': 'pthreads',
  'user_api': 'blas',
  'version': '0.3.17'}]

>>> controller.info() == threadpool_info()
True

设置线程池的最大大小

控制 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

线程池也可以通过面向对象的 API 控制,这对于避免每次都搜索所有加载的共享库特别有用。然而,它不会对在 ThreadpoolController 实例化之后加载的库产生影响。

>>> 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

限制到函数的作用域

threadpool_limitsThreadpoolController 也可以用作装饰器,在函数级别设置支持的库使用的最大线程数。装饰器可以通过它们的 wrap 方法访问

>>> 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
...

切换 FlexiBLAS 后端

FlexiBLAS 是一个 BLAS 包装器,其 BLAS 后端可以在运行时切换。threadpoolctl 提供了此功能的 Python 绑定。以下是一个示例,但请注意,此部分的 API 是实验性的,可能会更改而无需弃用

>>> from threadpoolctl import ThreadpoolController
>>> import numpy as np
>>> controller = ThreadpoolController()

>>> controller.info()
[{'user_api': 'blas',
  'internal_api': 'flexiblas',
  'num_threads': 1,
  'prefix': 'libflexiblas',
  'filepath': '/usr/local/lib/libflexiblas.so.3.3',
  'version': '3.3.1',
  'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'],
  'loaded_backends': ['NETLIB'],
  'current_backend': 'NETLIB'}]

# Retrieve the flexiblas controller
>>> flexiblas_ct = controller.select(internal_api="flexiblas").lib_controllers[0]

# Switch the backend with one predefined at build time (listed in "available_backends")
>>> flexiblas_ct.switch_backend("OPENBLASPTHREAD")
>>> controller.info()
[{'user_api': 'blas',
  'internal_api': 'flexiblas',
  'num_threads': 4,
  'prefix': 'libflexiblas',
  'filepath': '/usr/local/lib/libflexiblas.so.3.3',
  'version': '3.3.1',
  'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'],
  'loaded_backends': ['NETLIB', 'OPENBLASPTHREAD'],
  'current_backend': 'OPENBLASPTHREAD'},
 {'user_api': 'blas',
  'internal_api': 'openblas',
  'num_threads': 4,
  'prefix': 'libopenblas',
  'filepath': '/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.8.so',
  'version': '0.3.8',
  'threading_layer': 'pthreads',
  'architecture': 'Haswell'}]

# It's also possible to directly give the path to a shared library
>>> flexiblas_controller.switch_backend("/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so")
>>> controller.info()
[{'user_api': 'blas',
  'internal_api': 'flexiblas',
  'num_threads': 2,
  'prefix': 'libflexiblas',
  'filepath': '/usr/local/lib/libflexiblas.so.3.3',
  'version': '3.3.1',
  'available_backends': ['NETLIB', 'OPENBLASPTHREAD', 'ATLAS'],
  'loaded_backends': ['NETLIB',
   'OPENBLASPTHREAD',
   '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so'],
  'current_backend': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so'},
 {'user_api': 'openmp',
  'internal_api': 'openmp',
  'num_threads': 4,
  'prefix': 'libomp',
  'filepath': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libomp.so',
  'version': None},
 {'user_api': 'blas',
  'internal_api': 'openblas',
  'num_threads': 4,
  'prefix': 'libopenblas',
  'filepath': '/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.8.so',
  'version': '0.3.8',
  'threading_layer': 'pthreads',
  'architecture': 'Haswell'},
 {'user_api': 'blas',
  'internal_api': 'mkl',
  'num_threads': 2,
  'prefix': 'libmkl_rt',
  'filepath': '/home/jeremie/miniforge/envs/flexiblas_threadpoolctl/lib/libmkl_rt.so.2',
  'version': '2024.0-Product',
  'threading_layer': 'gnu'}]

您可以看到,之前链接的 OpenBLAS 共享对象会无限期地由 Python 程序加载,但 FlexiBLAS 本身不再将 BLAS 调用委托给 OpenBLAS,如 current_backend 属性所示。

编写自定义库控制器

目前,threadpoolctl 支持 OpenMP 和主要的 BLAS 库。但是,它也可以用来控制其他原生库的线程池,前提是它们提供了获取和设置线程数量限制的 API。为此,必须为此库实现一个控制器并将其注册到 threadpoolctl

自定义控制器必须是 LibController 类的子类,并实现 LibController 文档字符串中描述的属性和方法。然后必须使用 threadpoolctl.register 函数注册此新控制器类。一个完整的示例可以在这里找到。

OpenMP 并行区域内的顺序 BLAS

当在 OpenMP 并行区域内部希望有顺序的 BLAS 调用时,设置 limits="sequential_blas_under_openmp" 更安全,因为设置 limits=1user_api="blas" 在某些配置中可能不会产生预期的行为(例如,OpenBLAS 与 OpenMP 线程层 https://github.com/xianyi/OpenBLAS/issues/2985)。

已知限制

  • 当嵌套由不同的 OpenMP 运行时实现管理的并行循环(例如 GCC 的 libgomp 和 clang/llvm 的 libomp 或 ICC 的 libiomp)时,threadpool_limits 可能无法限制内部线程的数量。

    请参阅 test_openmp_nesting 函数,该函数位于 tests/test_threadpoolctl.py 中,以获取示例。更多信息可以在以下链接找到:https://github.com/jeremiedbb/Nested_OpenMP

    但是请注意,当使用 threadpool_limits 限制由 OpenMP 并行循环嵌套的 BLAS 调用内部使用的线程数时,这个问题不会发生。threadpool_limits 的工作方式符合预期,即使内部 BLAS 实现依赖于不同的 OpenMP 实现。

  • 在 Linux 下,同一 Python 程序中使用 Intel OpenMP (ICC) 和 LLVM OpenMP (clang) 已知会导致问题。有关更多详细信息和工作方案,请参阅以下指南:https://github.com/joblib/threadpoolctl/blob/master/multiple_openmp.md

  • 设置 OpenMP 和 BLAS 库的最大线程数具有全局影响,并影响整个 Python 进程。由于这些库不提供线程本地 API 来配置嵌套并行调用中使用的线程数,因此没有线程级别的隔离。

维护者

要发布版本

  • threadpoolctl.py 中增加版本号 (__version__) 并更新 CHANGES.md 中的发布日期。

  • 构建分发存档

pip install flit
flit build

并检查 dist/ 的内容。

  • 如果一切正常,为发布创建一个提交,标记它并将标签推送到 github
git tag -a X.Y.Z
git push git@github.com:joblib/threadpoolctl.git X.Y.Z
  • 使用 flit 将轮和源分发上传到 PyPI。由于 PyPI 不再允许密码身份验证,用户名需要更改为通用名称 __token__
FLIT_USERNAME=__token__ flit publish

并且需要一个 PyPI 令牌代替密码。

  • conda-forge 食品库 上创建一个用于发布的 PR(或等待机器人创建)。

  • 在 github 上发布发布。

致谢

最初的动态库反射代码由 @anton-malakhov 为 https://github.com/IntelPython/smp 上的 smp 包编写。

threadpoolctl 为其他操作系统扩展了此功能。与 smp 不同,threadpoolctl 不尝试限制 Python 多进程池(线程或进程)的大小或设置操作系统级别的 CPU 亲和力约束:threadpoolctl 只通过其公共运行时 API 与本地库交互。

项目详情


下载文件

下载您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分发

threadpoolctl-3.5.0.tar.gz (41.9 kB 查看散列)

上传时间

构建分发

threadpoolctl-3.5.0-py3-none-any.whl (18.4 kB 查看散列)

上传时间 Python 3

支持者