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

9.3.1. 并行性#

某些scikit-learn估计器和工具利用多核CPU并行化耗时操作。

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

  • 通过joblib实现高级并行。

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

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

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

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

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(即通过pip install安装的)和conda-forge频道(即通过conda install --channel conda-forge安装的)发布的NumPy和SciPy包均链接到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(含)范围内的任何种子值都应确定性地通过。

在夜间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

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未设置或设置为0时,情况即如此。

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

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