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

8.3.1. 并行#

一些 scikit-learn 估计器和实用程序使用多个 CPU 内核来并行化代价高昂的操作。

根据估计器的类型,有时还取决于构造函数参数的值,这可以通过以下方式完成:

  • 通过 joblib 进行更高级别的并行。

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

  • 通过 BLAS 进行更低级别的并行,用于 NumPy 和 SciPy 对数组进行通用操作。

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

我们在以下小节中更详细地描述这三种并行类型。

8.3.1.1. 使用 joblib 进行更高级别的并行#

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

注意

目前,使用 joblib 并行化(以及如何并行化)在估计器中通过指定 n_jobs 的方式进行,文档记录不完善。请帮助我们改进文档并解决 问题 14228

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

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

在某些特定情况下(当并行运行的代码释放 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 文档

在实践中,并行化是否有助于提高运行时间取决于许多因素。通常最好进行实验,而不是假设增加 worker 数量总是好事。在某些情况下,并行运行某些估计器或函数的多个副本可能会严重影响性能(请参阅下面的过度订阅)。

8.3.1.2. 使用 OpenMP 的低级并行化#

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

您可以控制使用的线程的确切数量,方法是:

  • 通过 OMP_NUM_THREADS 环境变量,例如在以下情况下:运行 Python 脚本

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

8.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 链接。

8.3.1.4. 过度订阅:生成过多线程#

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

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

来自 MKL、OpenBLAS 或 BLIS 的并行例程嵌套在 joblib 调用中时,也会以相同的方式出现过度订阅。

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 公开了一个上下文管理器,用于更精细地控制其 worker 中的线程数量(请参阅下面链接的 joblib 文档)。

  • 当 joblib 配置为使用 threading 后端时,在 joblib 管理的线程中调用并行本机库时,没有机制可以避免过度订阅。

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

您将在 joblib 文档 中找到有关 joblib 缓解过度订阅的更多详细信息。

您将在 Thomas J. Fan 的此文档 中找到有关数值 Python 库中并行化的更多详细信息。

8.3.2. 配置开关#

8.3.2.1. Python API#

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

8.3.2.2. 环境变量#

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

8.3.2.2.1. SKLEARN_ASSUME_FINITE#

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

8.3.2.2.2. SKLEARN_WORKING_MEMORY#

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

8.3.2.2.3. SKLEARN_SEED#

在运行测试时设置全局随机生成器的种子,以确保可重复性。

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

8.3.2.2.4. SKLEARN_TESTS_GLOBAL_RANDOM_SEED#

控制依赖于 global_random_seed` 固定装置的测试中使用的随机数生成器的播种。

所有使用此固定装置的测试都接受以下约定:它们应该以确定性方式通过,对于从 0 到 99(含)的任何种子值都应该通过。

如果 SKLEARN_TESTS_GLOBAL_RANDOM_SEED 环境变量被设置为 "any"(在 CI 上的 nightly build 中应该如此),fixture 将在上述范围内选择一个任意种子(基于 BUILD_NUMBER 或当前日期),并且所有 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(包括)之间的所有种子运行测试

  • SKLEARN_TESTS_GLOBAL_RANDOM_SEED="any":使用 0 到 99(包括)之间的任意种子运行测试

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

如果未设置该变量,则 42 将以确定性方式用作全局种子。这确保了默认情况下,scikit-learn 测试套件尽可能确定性,以避免扰乱我们友好的第三方软件包维护者。同样,此变量不应在拉取请求的 CI 配置中设置,以确保我们友好的贡献者不是第一个遇到与他们自己的 PR 更改无关的测试中的种子敏感性回归的人。只有观看 nightly build 结果的 scikit-learn 维护者预计会对此感到困扰。

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

SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all" pytest -v -k test_your_test_name

8.3.2.2.5. SKLEARN_SKIP_NETWORK_TESTS#

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

8.3.2.2.6. SKLEARN_RUN_FLOAT32_TESTS#

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

8.3.2.2.7. SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES#

当此环境变量设置为非零值时,Cython 导数 boundscheck 被设置为 True。这对于查找段错误很有用。

8.3.2.2.8. SKLEARN_BUILD_ENABLE_DEBUG_SYMBOLS#

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

8.3.2.2.9. SKLEARN_PAIRWISE_DIST_CHUNK_SIZE#

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

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

8.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 参数不同。