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 устанавливает количество потоков, используемых 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 год) пакеты 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»: запускать тесты с фиксированным сид 42

  • SKLEARN_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-файлах.