9.3. 并行性、资源管理和配置#

9.3.1. 并行性#

一些 scikit-learn 估计器和实用程序使用多个 CPU 核心来并行化耗时的操作。

根据估计器的类型以及有时构造函数参数的值,并行化通过以下方式实现:

  • 通过 joblib 实现更高级别的并行化。

  • 通过 OpenMP 实现更低级别的并行化,用于 C 或 Cython 代码。

  • 通过 BLAS 实现更低级别的并行化,由 NumPy 和 SciPy 用于数组的通用操作。

估计器的 n_jobs 参数始终控制由 joblib 管理的并行量(进程或线程,取决于 joblib 后端)。scikit-learn 自己的 Cython 代码中由 OpenMP 管理的线程级并行化或 scikit-learn 中使用的 NumPy 和 SciPy 操作所使用的 BLAS & LAPACK 库管理的线程级并行化始终由环境变量或 threadpoolctl 控制,如下所述。请注意,某些估计器可以在其训练和预测方法的不同点上利用所有这三种并行化方式。

我们将在接下来的小节中更详细地描述这 3 种并行化类型。

9.3.1.1. 使用 joblib 的更高级别并行化#

当底层实现使用 joblib 时,可以并行生成的工人(线程或进程)数量可以通过 n_jobs 参数控制。

注意

目前,使用 joblib 并指定 n_jobs 的估计器中并行化发生的位置(以及方式)文档记录不佳。请通过改进我们的文档并解决 问题 14228 来帮助我们!

Joblib 能够支持多进程和多线程。joblib 选择生成线程还是进程取决于它使用的 后端

scikit-learn 通常依赖于 loky 后端,这是 joblib 的默认后端。Loky 是一个多进程后端。当执行多进程时,为了避免在每个进程中复制内存(这对于大型数据集来说不合理),当数据大于 1MB 时,joblib 将创建一个所有进程都可以共享的 内存映射(memmap)

在某些特定情况下(当并行运行的代码释放 GIL 时),scikit-learn 会向 joblib 指示多线程后端更可取。

作为用户,您可以通过使用上下文管理器来控制 joblib 将使用的后端(无论 scikit-learn 推荐什么)。

from joblib import parallel_backend

with parallel_backend('threading', n_jobs=2):
    # Your scikit-learn code here

请参阅 joblib 文档 了解更多详细信息。

实际上,并行化是否有助于提高运行时取决于许多因素。通常最好进行实验,而不是假设增加工人数量总是好的。在某些情况下,并行运行某些估计器或函数的多个副本可能会严重损害性能(请参阅下面的 超额认购)。

9.3.1.2. 使用 OpenMP 的更低级别并行化#

OpenMP 用于并行化用 Cython 或 C 编写的代码,完全依赖于多线程。默认情况下,使用 OpenMP 的实现将使用尽可能多的线程,即与逻辑核心一样多的线程。

您可以通过以下方式控制使用的确切线程数:

  • 通过 OMP_NUM_THREADS 环境变量,例如在运行 python 脚本时。

    OMP_NUM_THREADS=4 python my_script.py
    
  • 或通过 threadpoolctl,如 本文档 所述。

9.3.1.3. 来自数值库的并行 NumPy 和 SciPy 例程#

scikit-learn 严重依赖 NumPy 和 SciPy,它们内部调用在 MKL、OpenBLAS 或 BLIS 等库中实现的多线程线性代数例程(BLAS & LAPACK)。

您可以使用环境变量控制 BLAS 为每个库使用的确切线程数,即:

  • MKL_NUM_THREADS 设置 MKL 使用的线程数,

  • OPENBLAS_NUM_THREADS 设置 OpenBLAS 使用的线程数

  • BLIS_NUM_THREADS 设置 BLIS 使用的线程数

请注意,BLAS & LAPACK 实现也可能受到 OMP_NUM_THREADS 的影响。要检查在您的环境中是否如此,您可以在 bash 或 zsh 终端中运行以下命令,并使用不同的 OMP_NUM_THREADS 值来检查这些库实际使用的线程数是如何受到影响的。

OMP_NUM_THREADS=2 python -m threadpoolctl -i numpy scipy

注意

在撰写本文时(2022 年),在 pypi.org 上分发的 NumPy 和 SciPy 包(即通过 pip install 安装的包)和在 conda-forge 频道上分发的包(即通过 conda install --channel conda-forge 安装的包)链接到 OpenBLAS,而 Anaconda.org 的 defaults conda 频道上分发的 NumPy 和 SciPy 包(即通过 conda install 安装的包)默认链接到 MKL。

9.3.1.4. 超额认购:生成过多线程#

通常建议避免使用的进程或线程数量明显超过机器上的 CPU 数量。当程序同时运行过多线程时,就会发生超额认购。

假设您有一台有 8 个 CPU 的机器。考虑一个情况,您正在运行一个 GridSearchCV(使用 joblib 并行化),其中 n_jobs=8,针对一个 HistGradientBoostingClassifier(使用 OpenMP 并行化)。HistGradientBoostingClassifier 的每个实例将生成 8 个线程(因为您有 8 个 CPU)。总共是 8 * 8 = 64 个线程,这将导致物理 CPU 资源的线程超额认购,从而导致调度开销。

超额认购也可能以完全相同的方式发生在嵌套在 joblib 调用中的来自 MKL、OpenBLAS 或 BLIS 的并行例程中。

joblib >= 0.14 开始,当使用 loky 后端时(这是默认设置),joblib 会告诉其子 进程 限制它们可以使用的线程数,以避免超额认购。实际上,joblib 使用的启发式方法是告诉进程使用 max_threads = n_cpus // n_jobs,通过它们对应的环境变量。回到上面的示例,由于 GridSearchCV 的 joblib 后端是 loky,每个进程将只能使用 1 个线程而不是 8 个,从而减轻了超额认购问题。

请注意:

  • 手动设置其中一个环境变量(OMP_NUM_THREADSMKL_NUM_THREADSOPENBLAS_NUM_THREADSBLIS_NUM_THREADS)将优先于 joblib 尝试执行的操作。线程总数将为 n_jobs * <LIB>_NUM_THREADS。请注意,设置此限制也会影响主进程中的计算,该进程将仅使用 <LIB>_NUM_THREADS。Joblib 公开了一个上下文管理器,用于更精细地控制其工人中的线程数(请参阅下面链接的 joblib 文档)。

  • 当 joblib 配置为使用 threading 后端时,当在 joblib 管理的线程中调用并行原生库时,没有机制可以避免超额认购。

  • 所有在其 Cython 代码中明确依赖 OpenMP 的 scikit-learn 估计器始终在内部使用 threadpoolctl 来自动调整 OpenMP 和可能嵌套的 BLAS 调用使用的线程数,以避免超额认购。

您可以在 joblib 文档 中找到有关 joblib 缓解超额认购的更多详细信息。

您可以在 Thomas J. Fan 的文档 中找到有关数值 python 库中并行性的更多详细信息。

9.3.2. 配置开关#

9.3.2.1. Python API#

sklearn.set_configsklearn.config_context 可用于更改控制并行性方面的配置参数。

9.3.2.2. 环境变量#

这些环境变量应在导入 scikit-learn 之前设置。

9.3.2.2.1. SKLEARN_ASSUME_FINITE#

设置 sklearn.set_configassume_finite 参数的默认值。

9.3.2.2.2. SKLEARN_WORKING_MEMORY#

设置 sklearn.set_configworking_memory 参数的默认值。

9.3.2.2.3. SKLEARN_SEED#

为测试设置全局随机生成器的种子,以实现可重复性。

请注意,scikit-learn 测试应该以确定性方式运行,明确设置自己的独立 RNG 实例的种子,而不是依赖 numpy 或 Python 标准库 RNG 单例,以确保测试结果独立于测试执行顺序。但是,某些测试可能会忘记使用显式播种,此变量是一种控制上述单例初始状态的方法。

9.3.2.2.4. SKLEARN_TESTS_GLOBAL_RANDOM_SEED#

控制在依赖 global_random_seed fixture 的测试中使用的随机数生成器的播种。

所有使用此 fixture 的测试都接受以下约定:它们应该对从 0 到 99(包括 99)的任何种子值确定性地通过。

在夜间 CI 构建中,SKLEARN_TESTS_GLOBAL_RANDOM_SEED 环境变量在上述范围内随机抽取,并且所有使用 fixture 的测试都将针对该特定种子运行。目标是确保随着时间的推移,我们的 CI 将使用不同的种子运行所有测试,同时限制单个完整测试套件运行的持续时间。这将检查编写为使用此 fixture 的测试的断言是否不依赖于特定的种子值。

允许的种子值范围限制在 [0, 99],因为通常不可能编写适用于任何可能种子的测试,并且我们希望避免在 CI 上随机失败的测试。

SKLEARN_TESTS_GLOBAL_RANDOM_SEED 的有效值

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="42":使用固定种子 42 运行测试

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="40-42":运行所有种子在 40 到 42 之间(包括 42)的测试

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all":运行所有种子在 0 到 99 之间(包括 99)的测试。这可能需要很长时间:仅用于单个测试,而不是完整的测试套件!

如果未设置此变量,则以确定性方式使用 42 作为全局种子。这确保了在默认情况下,scikit-learn 测试套件尽可能具有确定性,以避免干扰我们友好的第三方包维护者。同样,在拉取请求的 CI 配置中不应设置此变量,以确保我们友好的贡献者不会是第一个遇到与其自己的 PR 更改无关的测试中种子敏感回归的人。只有关注夜间构建结果的 scikit-learn 维护者才需要为此烦恼。

在编写使用此 fixture 的新测试函数时,请使用以下命令确保它在本地机器上对所有允许的种子确定性地通过。

SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all" pytest -v -k test_your_test_name

9.3.2.2.5. SKLEARN_SKIP_NETWORK_TESTS#

当此环境变量设置为非零值时,需要网络访问的测试将被跳过。当未设置此环境变量时,网络测试将被跳过。

9.3.2.2.6. SKLEARN_RUN_FLOAT32_TESTS#

当此环境变量设置为 '1' 时,使用 global_dtype fixture 的测试也会在 float32 数据上运行。当未设置此环境变量时,测试仅在 float64 数据上运行。

9.3.2.2.7. SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES#

当此环境变量设置为非零值时,Cython 派生指令 boundscheck 将设置为 True。这有助于查找段错误。

9.3.2.2.8. SKLEARN_BUILD_ENABLE_DEBUG_SYMBOLS#

当此环境变量设置为非零值时,调试符号将包含在编译的 C 扩展中。仅配置了 POSIX 系统的调试符号。

9.3.2.2.9. SKLEARN_PAIRWISE_DIST_CHUNK_SIZE#

这设置了底层 PairwiseDistancesReductions 实现要使用的块大小。默认值为 256,这已证明在大多数机器上足够。

寻求最佳性能的用户可能希望使用 2 的幂来调整此变量,以便为其硬件获得最佳并行行为,特别是考虑到其缓存大小。

9.3.2.2.10. SKLEARN_WARNINGS_AS_ERRORS#

此环境变量用于在测试和文档构建中将警告转换为错误。

某些 CI(持续集成)构建会设置 SKLEARN_WARNINGS_AS_ERRORS=1,例如确保我们捕获来自依赖项的弃用警告并调整我们的代码。

要在本地使用与这些 CI 构建相同的“将警告视为错误”设置运行,您可以设置 SKLEARN_WARNINGS_AS_ERRORS=1

默认情况下,警告不会转换为错误。如果未设置 SKLEARN_WARNINGS_AS_ERRORS,或者 SKLEARN_WARNINGS_AS_ERRORS=0,则情况如此。

此环境变量使用特定的警告过滤器来忽略某些警告,因为有时警告源自第三方库,我们对此无能为力。您可以在 sklearn/utils/_testing.py 中的 _get_warnings_filters_info_list 函数中查看警告过滤器。

请注意,对于文档构建,SKLEARN_WARNING_AS_ERRORS=1 会检查文档构建(特别是运行示例)是否产生任何警告。这与捕获 rst 文件中语法警告的 -W sphinx-build 参数不同。