8.3. 并行性、资源管理和配置#
8.3.1. 并行性#
一些scikit-learn估计器和工具通过使用多个CPU核心来并行化昂贵的操作。
根据估计器的类型和有时构造函数参数的值,这可以通过以下方式完成:
通过 joblib 实现的高级并行性。
通过在C或Cython代码中使用的OpenMP实现的低级并行性。
通过NumPy和SciPy用于数组通用操作的BLAS实现的低级并行性。
估计器的 n_jobs
参数始终控制由joblib管理的并行性数量(进程或线程取决于joblib后端)。
scikit-learn自己的Cython代码中的OpenMP或NumPy和SciPy操作使用的BLAS & LAPACK库管理的线程级并行性始终由环境变量或 threadpoolctl
控制,如下所述。
请注意,某些估计器可以在其训练和预测方法的不同点利用所有三种类型的并行性。
我们将在以下小节中更详细地描述这三种类型的并行性。
8.3.1.1. 使用joblib的高级并行性#
当底层实现使用joblib时,可以通过 n_jobs
参数控制并行生成的工人(线程或进程)的数量。
Note
目前,在使用joblib并通过指定 n_jobs
进行并行化的估计器中,并行化发生的位置(以及如何发生)文档记录不足。
请通过改进我们的文档并解决 issue 14228 来帮助我们!
Joblib能够支持多进程和多线程。joblib选择生成线程还是进程取决于它使用的**后端**。
scikit-learn 通常依赖于 loky
后端,这是 joblib 的默认后端。Loky 是一个多进程后端。在进行多进程处理时,为了避免在每个进程中重复内存(这在处理大数据集时是不合理的),joblib 会在数据大于 1MB 时创建一个 memmap ,所有进程都可以共享。
在某些特定情况下(当并行运行的代码释放 GIL 时),scikit-learn 会指示 joblib
优先使用多线程后端。
作为用户,您可以通过使用上下文管理器来控制 joblib 将使用的后端(无论 scikit-learn 推荐什么):
from joblib import parallel_backend
with parallel_backend('threading', n_jobs=2):
# 您的 scikit-learn 代码在这里
请参阅 joblib 的文档 以获取更多详细信息。
在实践中,并行化是否有助于提高运行时间取决于许多因素。通常,进行实验而不是假设增加工作线程数量总是好事是一个好主意。在某些情况下,并行运行某些估计器或函数的多个副本可能对性能极为不利(参见下面的过度订阅)。
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,这些库内部调用了多线程线性代数例程(BLAS & LAPACK),这些例程在MKL、OpenBLAS或BLIS等库中实现。
您可以使用环境变量来控制每个库使用的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
Note
在撰写本文时(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的机器。考虑一个情况,您正在运行
一个使用 joblib 并行化的 GridSearchCV
,其中 n_jobs=8
,
以及一个使用 OpenMP 并行化的 HistGradientBoostingClassifier
。
每个 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_THREADS
、MKL_NUM_THREADS
、OPENBLAS_NUM_THREADS
或BLIS_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_config
和 sklearn.config_context
可以用来改变控制并行性方面的配置参数。
8.3.2.2. 环境变量#
这些环境变量应在导入scikit-learn之前设置。
SKLEARN_ASSUME_FINITE
设置 sklearn.set_config
的 assume_finite
参数的默认值。
SKLEARN_WORKING_MEMORY
设置 sklearn.set_config
的 working_memory
参数的默认值。
SKLEARN_SEED
在运行测试时设置全局随机生成器的种子,以确保可重复性。
请注意,scikit-learn测试预期在明确种子的情况下确定性地运行,而不是依赖于numpy或Python标准库的RNG单例,以确保测试结果与测试执行顺序无关。然而,某些测试可能忘记使用明确的种子,这个变量是一种控制上述单例初始状态的方法。
SKLEARN_TESTS_GLOBAL_RANDOM_SEED
- 控制依赖于随机数生成器的测试中的种子设置。
global_random_seed
固定装置。
所有使用此固定装置的测试都接受以下约定:对于从0到99(含)的任何种子值,它们应确定性地通过。
在夜间CI构建中, SKLEARN_TESTS_GLOBAL_RANDOM_SEED
环境变量在上限范围内随机抽取,所有固定装置测试将针对该特定种子运行。目标是确保随着时间的推移,我们的CI将使用不同的种子运行所有测试,同时保持完整测试套件单次运行的测试持续时间有限。这将检查编写为使用此固定装置的测试的断言不依赖于特定的种子值。
可接受的种子值范围限制为 [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维护者才会因此感到烦恼。
在编写使用此固定装置的新测试函数时,请使用 以下命令确保在本地机器上所有可接受的种子下都能确定性地通过:
SKLEARN_TESTS_GLOBAL_RANDOM_SEED="all" pytest -v -k test_your_test_name
`SKLEARN_SKIP_NETWORK_TESTS`
当此环境变量设置为非零值时,需要网络访问的测试将被跳过。当此环境变量未设置时,网络测试将被跳过。
SKLEARN_RUN_FLOAT32_TESTS
当此环境变量设置为 ‘1’ 时,使用 global_dtype
固定装置的测试也会在 float32 数据上运行。当此环境变量未设置时,测试仅在 float64 数据上运行。
SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES
当此环境变量设置为非零值时, Cython
派生的 boundscheck
被设置为 True
。这对于查找段错误很有用。
SKLEARN_BUILD_ENABLE_DEBUG_SYMBOLS
当此环境变量设置为非零值时,调试符号将包含在编译的 C 扩展中。仅配置了 POSIX 系统的调试符号。
SKLEARN_PAIRWISE_DIST_CHUNK_SIZE
这设置了底层 PairwiseDistancesReductions
实现使用的块大小。默认值为 256
,这在大多数机器上已被证明是足够的。
寻求最佳性能的用户可能希望通过使用 2 的幂来调整此变量,以便为其硬件获得最佳并行行为,特别是考虑到其缓存大小。
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`参数不同。