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 中使用的 BLAS 和 LAPACK 库在 scikit-learn 中使用的操作,始终由环境变量或 threadpoolctl 控制,如下所述。请注意,一些估计器可以在其训练和预测方法的不同点利用这三种并行化。

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

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

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

注意

目前,使用 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 的文档 获取更多详细信息。

实际上,并行化是否有助于提高运行时间取决于许多因素。通常最好进行实验,而不是假设增加 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=8HistGradientBoostingClassifier(使用 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 库中并行处理的更多详细信息。

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`fixture 的测试中使用的随机数生成器的种子。

所有使用此 fixture 的测试都接受这样的约定:对于从 0 到 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(包含)之间的所有种子运行测试

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

如果未设置此变量,则以确定性的方式将 42 用作全局种子。这确保了 scikit-learn 测试套件默认尽可能确定性,以避免扰乱我们友好的第三方包维护者。同样,不应在拉取请求的 CI 配置中设置此变量,以确保我们的友好贡献者不会成为第一个遇到与他们自己的 PR 更改无关的测试中的种子敏感性回归的人。只有监控夜间构建结果的 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 会检查文档构建(特别是运行示例)是否产生任何警告。这与-W sphinx-build参数不同,后者捕获rst文件中的语法警告。