9.3. Параллелизм, управление ресурсами и конфигурация#
9.3.1. Параллелизм#
Некоторые оценщики и утилиты scikit-learn распараллеливают затратные операции с использованием нескольких ядер CPU.
В зависимости от типа оценщика и иногда значений параметров конструктора, это выполняется:
с более высокоуровневым параллелизмом через joblib.
с низкоуровневым параллелизмом через OpenMP, используемым в коде на C или Cython.
с низкоуровневым параллелизмом через BLAS, используемым NumPy и SciPy для общих операций над массивами.
The n_jobs параметры оценщиков всегда управляют степенью параллелизма, управляемой joblib (процессы или потоки в зависимости от бэкенда joblib). Параллелизм на уровне потоков, управляемый OpenMP в собственном Cython-коде scikit-learn или библиотеками BLAS & LAPACK, используемыми в операциях NumPy и SciPy, применяемых в scikit-learn, всегда контролируется переменными окружения или threadpoolctl как объяснено ниже. Обратите внимание, что некоторые оценщики могут использовать все три вида параллелизма в разных точках их методов обучения и прогнозирования.
Мы описываем эти 3 типа параллелизма в следующих подразделах более подробно.
9.3.1.1. Высокоуровневый параллелизм с joblib#
Когда базовая реализация использует joblib, количество рабочих процессов (потоков или процессов), создаваемых параллельно, можно контролировать через
n_jobs параметр.
Примечание
Где (и как) происходит параллелизация в оценщиках с использованием joblib, указывая n_jobs в настоящее время плохо документирован.
Пожалуйста, помогите нам, улучшив нашу документацию и разобравшись с issue 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 для получения дополнительной информации.
На практике, помогает ли параллелизм улучшить время выполнения, зависит от многих факторов. Обычно хорошей идеей является экспериментирование, а не предположение, что увеличение количества работников всегда полезно. В некоторых случаях параллельный запуск нескольких копий некоторых оценщиков или функций может быть крайне вреден для производительности (см. переподписка ниже).
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, которые внутри вызывают многопоточные подпрограммы линейной алгебры (BLAS & LAPACK), реализованные в библиотеках, таких как MKL, OpenBLAS или BLIS.
Вы можете контролировать точное количество потоков, используемых BLAS для каждой библиотеки, используя переменные окружения, а именно:
MKL_NUM_THREADSустанавливает количество потоков, которые использует MKL,OPENBLAS_NUM_THREADSустанавливает количество потоков, используемых OpenBLASBLIS_NUM_THREADSустанавливает количество потоков, которые использует BLIS
Методы и алгоритмы для робастной оценки ковариации.
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, поставляемые на defaults канал conda с Anaconda.org (т.е. те, которые установлены через conda install)
по умолчанию связаны с MKL.
9.3.1.4. Избыточная подписка: создание слишком большого количества потоков#
Обычно рекомендуется избегать использования значительно большего количества процессов или потоков, чем число CPU на машине. Переподписка происходит, когда программа запускает слишком много потоков одновременно.
Предположим, у вас есть машина с 8 процессорами. Рассмотрим случай, когда вы запускаете GridSearchCV (параллельно с joblib)
с n_jobs=8 над
HistGradientBoostingClassifier (параллелизовано с OpenMP). Каждый экземпляр
HistGradientBoostingClassifier запустит 8 потоков
(поскольку у вас 8 процессоров). Всего 8 * 8 = 64 потоки, что
приводит к избыточной подписке потоков на физические ресурсы ЦП и, следовательно,
к накладным расходам на планирование.
Переподписка может возникать точно таким же образом с параллелизованными процедурами из 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 *. Обратите внимание, что установка этого ограничения также повлияет на ваши вычисления в основном процессе, который будет использовать только_NUM_THREADS . Joblib предоставляет контекстный менеджер для более точного контроля количества потоков в своих воркерах (см. документацию joblib по ссылке ниже)._NUM_THREADS Когда joblib настроен на использование
threadingbackend, нет механизма для избежания переподписки при вызове параллельных нативных библиотек в потоках, управляемых joblib.Все оценщики scikit-learn, которые явно используют OpenMP в своем Cython-коде, всегда используют
threadpoolctlвнутренне для автоматической адаптации количества потоков, используемых OpenMP и потенциально вложенными вызовами BLAS, чтобы избежать переподписки.
Вы найдёте дополнительные детали о предотвращении переподписки в joblib в документация joblib.
Дополнительные сведения о параллелизме в числовых библиотеках Python вы найдёте в этот документ от Thomas J. Fan.
9.3.2. Конфигурационные переключатели#
9.3.2.1. Python API#
sklearn.set_config и sklearn.config_context может использоваться для изменения параметров конфигурации, которые управляют аспектами параллелизма.
9.3.2.2. Переменные окружения#
Эти переменные окружения должны быть установлены до импорта scikit-learn.
9.3.2.2.1. SKLEARN_ASSUME_FINITE#
Устанавливает значение по умолчанию для assume_finite аргумент
sklearn.set_config.
9.3.2.2.2. SKLEARN_WORKING_MEMORY#
Устанавливает значение по умолчанию для working_memory аргумент
sklearn.set_config.
9.3.2.2.3. SKLEARN_SEED#
Устанавливает начальное значение глобального генератора случайных чисел при запуске тестов для воспроизводимости.
Обратите внимание, что тесты scikit-learn должны выполняться детерминированно с явным заданием начального состояния их собственных независимых экземпляров ГСЧ, а не полагаться на синглтоны ГСЧ numpy или стандартной библиотеки Python, чтобы гарантировать независимость результатов тестов от порядка их выполнения. Однако некоторые тесты могут забыть использовать явное задание начального состояния, и эта переменная является способом управления начальным состоянием упомянутых синглтонов.
9.3.2.2.4. SKLEARN_TESTS_GLOBAL_RANDOM_SEED#
Управляет инициализацией генератора случайных чисел, используемого в тестах, которые полагаются на global_random_seed фикстура.
Все тесты, использующие этот фикстур, принимают условие, что они должны детерминированно проходить для любого значения seed от 0 до 99 включительно.
В ночных сборках CI, SKLEARN_TESTS_GLOBAL_RANDOM_SEED переменная окружения выбирается случайным образом в указанном диапазоне, и все фиксированные тесты будут запущены с этим конкретным сидом. Цель — обеспечить, чтобы со временем наш CI запускал все тесты с разными сидами, сохраняя при этом продолжительность одного запуска полного набора тестов ограниченной. Это проверит, что утверждения тестов, написанных с использованием этого фикстура, не зависят от конкретного значения сида.
Диапазон допустимых значений сида ограничен [0, 99], потому что часто невозможно написать тест, который будет работать для любого возможного сида, и мы хотим избежать тестов, которые случайно проваливаются на 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="all": запустить тесты со всеми значениями seed от 0 до 99 включительно. Это может занять много времени: используйте только для отдельных тестов, а не для полного набора тестов!
Если переменная не установлена, то 42 используется как глобальное начальное число детерминированным образом. Это гарантирует, что по умолчанию тестовый набор scikit-learn максимально детерминирован, чтобы не нарушать работу наших дружелюбных сторонних сопровождающих пакетов. Аналогично, эта переменная не должна устанавливаться в конфигурации CI для pull-request'ов, чтобы убедиться, что наши дружелюбные контрибьюторы не станут первыми, кто столкнётся с регрессией чувствительности к начальному числу в тесте, не связанном с изменениями их собственного PR. Только сопровождающие scikit-learn, которые следят за результатами ночных сборок, ожидают, что это будет их раздражать.
При написании новой тестовой функции, использующей этот фикстур, используйте следующую команду, чтобы убедиться, что она проходит детерминированно для всех допустимых сидов на вашей локальной машине:
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 Фикстуры также запускаются на данных 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 не установлен, или SKLEARN_WARNINGS_AS_ERRORS=0.
Эта переменная окружения использует специальные фильтры предупреждений, чтобы игнорировать некоторые предупреждения, поскольку иногда предупреждения исходят от сторонних библиотек, и мы мало что можем с этим поделать. Вы можете увидеть фильтры предупреждений в
_get_warnings_filters_info_list функция в sklearn/utils/_testing.py.
Обратите внимание, что для сборки документации SKLEARN_WARNING_AS_ERRORS=1 проверяет, что сборка документации, в частности запуск примеров, не производит никаких предупреждений. Это отличается от -W sphinx-build аргумент, который перехватывает синтаксические предупреждения в rst-файлах.