Параллельные или распределенные вычисления — вещь сама по себе весьма нетривиальная. И среда разработки должна поддерживать, и DS специалист должен обладать навыками проведения параллельных вычислений, да и задача должна быть приведена к разделяемому на части виду, если таковой существует. Но при грамотном подходе можно весьма ускорить решение задачи однопоточным R, если у вас под руками есть хотя бы многоядерный процессор (а он есть сейчас почти у всех), с поправкой на теоретическую границу ускорения, определяемую законом Амдала. Однако, в ряде случаев даже его можно обойти.
Является продолжением предыдущих публикаций.
Типовой подход
Как правило, когда аналитик (DS специалист, разрработчик или выберите себе любое подходящее название) пытается ускорить в рамках одного компьютера задачу и начинает двигаться от однопоточного режима к многопоточному, делает он это шаблонным образом. parApply
, doreach%dopar%
, etc. Компактно и доходчиво можно поглядеть, например, здесь: «Parallelism in R». 3 шага:
- делаем
core-1
поток, - запускаем с помощью
foreach
, - собираем ответы и получаем результат.
Для типичных вычислительных задач, занимающих 100% CPU и не требующих передачи большого объема входной информации, это правильный подход. Основной момент, требующий внимания — обеспечить логирование в рамках потоков, чтобы иметь возможность контроля процесса. Без логирования полет пойдет даже без приборов.
В случае «enterprise» задач при их распараллеливании возникает множество дополнительных методологических сложностей, значительно снижающих эффект от приведенного выше прямолинейного подхода:
- возможная сильная разбалансировка нагрузки на потоки;
- требования к производительности CPU в рамках отдельной задачи могут иметь рваный характер с наличием только отдельных резких всплесков;
- каждое отдельное вычисления может требовать значительного объема памяти на вход и выдачу результатов также немалого объема;
- в рамках отдельной задачи может быть микс между вычислениями, работой с диском и запросом внешних систем.
Это совершенно типичный сценарий, когда в рамках процесса надо получить на вход объемное задание, прочитать данные с диска, забрать большой кусок из БД, поспрашивать внешние системы и дождаться от них ответа (классика — REST API запрос), а потом вернуть в родительский процесс N мегабайт в качестве результата.
Map-reduce
по пользователям, локациям, документам, ip- адресам, датам, … (дополните сами). В самых печальных случаях параллельное исполнение может оказаться более длительным, чем однопоточное. Также могут наблюдаться проблемы с нехваткой памяти. Все пропало? Вовсе нет.
Альтернативный путь
Рассмотрим тезисно способ радикального улучшения ситуации. При этом не забываем, что мы живем в рамках полного зоопарка. Продуктивный контур на *nix
, DS ноутбуки на Win*nixMacOS, а надо, чтобы везде работало единообразно.
- Микрозадача: получил на вход пользователя, запросил БД, запросил 2 внешних ИС через REST, скачал и разобрал справочник с диска, провел расчет, сбросил результат на дискБД. Пользователей, например,
10^6
. - Переходим к использованию пакета
future
и универсального адаптераdoFuture
. - Если отдельные задачи таковы, что в рамках отдельных задач процессорное время нужно в малом объеме (ждем ответов сторонних систем), то
doFuture
позволяет в одну строчку перейти от разбиения по потокам к разбиению по отдельным процессам (можно в*nix
вhtop
поглядеть параметры запуска). - Этих процессов можно создать гораздо больше чем ядер. Никакого клинча не произойдет поскольку отдельные процессы бывают в режиме ожидания большую часть времени. Но надо будет опытным путем подобрать оптимальное число процессов исходя из циклограммы типового процесса обработки.
Результат — исходная задача выполняется в разы быстрее. Ускорение может быть даже больше числа доступных ядер.
Кода нет сознательно, поскольку основная задача публикации — поделиться подходом и отличным семейством пакетов future
.
P.S.
Есть еще несколько маленьких нюансов за которыми тоже надо проследить:
- каждый процесс будет потреблять память, включая получаемые и возвращаемые данные. Увеличение числа процессов кратно увеличит требования по доступной оперативной памяти.
doFuture
использует «магию» для автоматического определения состава передаваемых в процесс переменных и пакетов, но пускать все на самотек не стоит, лучше проверить.- в процессах явное управление
gc
и явное удаление переменных с помощьюrm
не помешает. - после завершения вычислений вызывайте
plan(sequential)
. Это закроет все процессы и освободит занимаемую ими память. - если необходимо передать в процесс большой объем данных рассмотрите возможность использования внешнего хранилища (диск, БД). Не забываем, что дескрипторы передать нельзя, открывать источник надо внутри самого процесса.
Предыдущая публикация — «Бизнес-процессы в enterprise компаниях: домыслы и реальность. Проливаем свет с помощью R».
Автор: Илья Шутов