8.3. Параллелизм, управление ресурсами и конфигурация¶
8.3.1. Параллелизм¶
Некоторые модели и утилиты scikit-learn распараллеливают дорогостоящие операции, используя несколько ядер процессора.
В зависимости от типа модели, а иногда и от значений параметров конструктора, это делается либо:
с параллелизмом более высокого уровня через joblib.
с параллелизмом нижнего уровня через OpenMP, используемый в коде на C или Cython.
с параллелизмом нижнего уровня через BLAS, используемый NumPy и SciPy для общих операций над массивами.
Параметры n_jobs
в моделях всегда контролируют количество параллелизма, управляемого joblib (процессы или потоки в зависимости от бэкенда joblib). Параллелизм на уровне потоков, управляемый OpenMP в собственном коде scikit-learn на Cython или библиотеками BLAS и LAPACK, используемыми в операциях NumPy и SciPy, применяемых в scikit-learn, всегда контролируется переменными окружения или threadpoolctl
, как объясняется ниже. Обратите внимание, что некоторые модели могут использовать все три вида параллелизма на разных этапах обучения и прогнозирования.
Более подробно эти три вида параллелизма описаны в следующих подразделах.
8.3.1.1. Параллелизм более высокого уровня с помощью joblib¶
Когда базовая реализация использует joblib, количество рабочих (потоков или процессов), которые порождаются параллельно, можно контролировать с помощью параметра n_jobs
.
Примечание
Где (и как) происходит распараллеливание в моделях, использующих joblib, путем указания n_jobs
, в настоящее время плохо документировано. Пожалуйста, помогите нам улучшить документацию и решить проблему 14228!
Joblib может поддерживать как многопроцессорность, так и многопоточность. Выберет ли joblib породить поток или процесс, зависит от бэкенда, который он использует.
scikit-learn обычно полагается на бэкенд loky
, который является бэкендом joblib по умолчанию. Loky - это многопроцессорный бэкенд. При многопроцессорной обработке, чтобы избежать дублирования памяти в каждом процессе (что нецелесообразно при больших наборах данных), joblib будет создавать memmap, которую все процессы могут использовать совместно, если размер данных превышает 1 МБ.
В некоторых особых случаях (когда параллельно выполняемый код освобождает 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’а.
На практике, полезен ли параллелизм для улучшения времени выполнения, зависит от многих факторов. Обычно стоит поэкспериментировать, а не считать, что увеличение числа обработчиков всегда хорошо. В некоторых случаях параллельный запуск нескольких копий некоторых модели или функций может сильно ухудшить производительность (см. раздел «Переиспользование» ниже).
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
устанавливает количество потоков, используемых OpenBLASBLIS_NUM_THREADS
устанавливает количество потоков, используемых BLIS
Обратите внимание, что реализации BLAS и LAPACK также могут быть подвержены влиянию OMP_NUM_THREADS
. Чтобы проверить, так ли это в вашем окружении, вы можете проверить, как влияет количество потоков, эффективно используемых этими библиотеками, при выполнении следующей команды в терминале bash или zsh для различных значений OMP_NUM_THREADS
:
OMP_NUM_THREADS=2 python -m threadpoolctl -i numpy scipy
Примечание
На момент написания статьи (2022 год) пакеты NumPy и SciPy, распространяемые на pypi.org (т.е. устанавливаемые через pip install
) и на канале conda-forge (т.е. те, что устанавливаются через conda install --channel conda-forge
), связаны с OpenBLAS, в то время как пакеты NumPy и SciPy, поставляемые по каналу conda defaults
от Anaconda.org (т.е. те, что устанавливаются через conda install
), по умолчанию связаны с MKL.
8.3.1.4. Переиспользование: порождение слишком большого количества потоков¶
Обычно рекомендуется избегать использования значительно большего количества процессов или потоков, чем количество CPU на машине. Переиспользование происходит, когда программа запускает слишком много потоков одновременно.
Предположим, у вас есть машина с 8 процессорами. Рассмотрим случай, когда вы выполняете GridSearchCV
(распараллеленный с помощью joblib) с n_jobs=8
над HistGradientBoostingClassifier
(распараллеленный с помощью OpenMP). Каждый экземпляр HistGradientBoostingClassifier
породит 8 потоков (поскольку у вас 8 процессоров). Итого 8 * 8 = 64
потоков, что приводит к переиспользованию потоков на физические ресурсы CPU и, следовательно, к накладным расходам на планирование.
Точно такая же ситуация может возникнуть с распараллеленными процедурами из MKL, OpenBLAS или BLIS, которые вложены в вызовы joblib.
Начиная с joblib >= 0.14
, когда используется бэкенд loky
(по умолчанию), joblib будет указывать своим дочерним процессам ограничить количество потоков, которые они могут использовать, чтобы избежать переиспользование. На практике эвристика, которую использует joblib, заключается в том, чтобы указать процессам использовать max_threads = n_cpus // n_jobs
, через соответствующую переменную окружения. Возвращаясь к нашему примеру выше, поскольку бэкенд joblib GridSearchCV
является 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 потоках.Все модели scikit-learn, которые явно полагаются на OpenMP в своем Cython-коде, всегда используют
threadpoolctl
для автоматической адаптации количества потоков, используемых OpenMP и потенциально вложенными вызовами BLAS, чтобы избежать переиспользование.
Вы найдете дополнительные сведения о том, как joblib предотвращает переиспользование in joblib documentation.
Вы найдете дополнительные сведения о параллелизме в числовых библиотеках python in this document from Thomas J. Fan.
8.3.2. Конфигурационные переключатели¶
8.3.2.1. API Python¶
sklearn.set_config
и sklearn.config_context
могут быть использованы для изменения параметров конфигурации, которые управляют аспектом параллелизма.
8.3.2.2. Переменные окружения¶
Эти переменные окружения должны быть установлены перед импортом scikit-learn.
8.3.2.2.1. SKLEARN_ASSUME_FINITE
¶
Устанавливает значение по умолчанию для аргумента assume_finite
функции sklearn.set_config
.
8.3.2.2.2. SKLEARN_WORKING_MEMORY
¶
Устанавливает значение по умолчанию для аргумента working_memory
в sklearn.set_config
.
8.3.2.2.3. SKLEARN_SEED
¶
Устанавливает сид глобального генератора случайных чисел при запуске тестов для воспроизводимости.
Обратите внимание, что предполагается, что тесты scikit-learn будут выполняться детерминированно с явным сид собственных независимых экземпляров RNG, а не полагаться на синглтоны RNG стандартной библиотеки numpy или Python, чтобы гарантировать, что результаты тестов не зависят от порядка их выполнения. Однако некоторые тесты могут забыть об использовании явного сид, и эта переменная - способ управления начальным состоянием вышеупомянутых синглтонов.
8.3.2.2.4. SKLEARN_TESTS_GLOBAL_RANDOM_SEED
¶
Управляет загрузкой генератора случайных чисел, используемого в тестах, которые полагаются на фикстуру global_random_seed
.
Все тесты, использующие это приспособление, принимают контракт, согласно которому они должны детерминированно пройти для любого значения seed от 0 до 99 включительно.
Если переменная окружения SKLEARN_TESTS_GLOBAL_RANDOM_SEED
установлена в значение "any"
(что должно быть в ночных сборках на CI), фикстура выберет произвольный сид в указанном диапазоне (на основе BUILD_NUMBER или текущего дня) и все фикстурные тесты будут выполняться для этого конкретного сида. Цель состоит в том, чтобы гарантировать, что со временем наш CI будет запускать все тесты с разными семплами, сохраняя при этом ограниченную длительность одного запуска всего набора тестов. Это позволит проверить, что утверждения тестов, написанных для использования этого приспособления, не зависят от конкретного значения seed.
Диапазон допустимых значений seed ограничен [0, 99], потому что часто невозможно написать тест, который будет работать при любом значении seed, и мы хотим избежать случайных отказов тестов на CI.
Допустимые значения для SKLEARN_TESTS_GLOBAL_RANDOM_SEED
:
SKLEARN_TESTS_GLOBAL_RANDOM_SEED=«42»
: запускать тесты с фиксированным сид 42SKLEARN_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 pull-requests, чтобы убедиться, что наши дружелюбные контрибьюторы не станут первыми, кто столкнется с регрессией чувствительности к сидам в тесте, не связанном с изменениями их собственного PR. Ожидается, что это будет раздражать только мейнтейнеров scikit-learn, которые следят за результатами ночных сборок.
При написании новой тестовой функции, использующей это приспособление, пожалуйста, используйте следующую команду, чтобы убедиться, что она проходит детерминированно для всех допустимых сидах на вашей локальной машине:
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
, также выполняются на данных 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 (Continuous Integration) устанавливают SKLEARN_WARNINGS_AS_ERRORS=1
, например, чтобы убедиться, что мы улавливаем предупреждения об исправлениях из наших зависимостей и адаптируем наш код.
Для локального запуска с теми же настройками «предупреждения как ошибки», что и в этих CI-сборках, вы можете установить SKLEARN_WARNINGS_AS_ERRORS=1
.
По умолчанию предупреждения не превращаются в ошибки. Это происходит, если SKLEARN_WARNINGS_AS_ERRORS
не установлено, или SKLEARN_WARNINGS_AS_ERRORS=0
.
Эта переменная окружения использует специальные фильтры предупреждений для игнорирования некоторых предупреждений, поскольку иногда предупреждения исходят от сторонних библиотек и мы мало что можем с этим поделать. Фильтры предупреждений можно посмотреть в функции _get_warnings_filters_info_list
в sklearn/utils/_testing.py
.
Обратите внимание, что для сборки документации SKLEARN_WARNING_AS_ERRORS=1
проверяет, что сборка документации, в частности запуск примеров, не выдает никаких предупреждений. Это отличается от аргумента -W
sphinx-build
, который отлавливает синтаксические предупреждения в rst-файлах.