Pull to refresh

Несколько соображений по поводу параллельных вычислений в R применительно к «enterprise» задачам

Reading time3 min
Views2.4K

Параллельные или распределенные вычисления — вещь сама по себе весьма нетривиальная. И среда разработки должна поддерживать, и DS специалист должен обладать навыками проведения параллельных вычислений, да и задача должна быть приведена к разделяемому на части виду, если таковой существует. Но при грамотном подходе можно весьма ускорить решение задачи однопоточным R, если у вас под руками есть хотя бы многоядерный процессор (а он есть сейчас почти у всех), с поправкой на теоретическую границу ускорения, определяемую законом Амдала. Однако, в ряде случаев даже его можно обойти.


Является продолжением предыдущих публикаций.


Типовой подход


Как правило, когда аналитик (DS специалист, разработчик или выберите себе любое подходящее название) пытается ускорить в рамках одного компьютера задачу и начинает двигаться от однопоточного режима к многопоточному, делает он это шаблонным образом. parApply, foreach\%dopar%, etc. Компактно и доходчиво можно поглядеть, например, здесь: «Parallelism in R». 3 шага:


  1. делаем core-1поток,
  2. запускаем с помощью foreach,
  3. собираем ответы и получаем результат.

Для типичных вычислительных задач, занимающих 100% CPU и не требующих передачи большого объема входной информации, это правильный подход. Основной момент, требующий внимания — обеспечить логирование в рамках потоков, чтобы иметь возможность контроля процесса. Без логирования полет пойдет даже без приборов.


В случае «enterprise» задач при их распараллеливании возникает множество дополнительных методологических сложностей, значительно снижающих эффект от приведенного выше прямолинейного подхода:


  • возможная сильная разбалансировка нагрузки на потоки;
  • требования к производительности CPU в рамках отдельной задачи могут иметь рваный характер с наличием только отдельных резких всплесков;
  • каждое отдельное вычисления может требовать значительного объема памяти на вход и выдачу результатов также немалого объема;
  • в рамках отдельной задачи может быть микс между вычислениями, работой с диском и запросом внешних систем.

Это совершенно типичный сценарий, когда в рамках процесса надо получить на вход объемное задание, прочитать данные с диска, забрать большой кусок из БД, поспрашивать внешние системы и дождаться от них ответа (классика — REST API запрос), а потом вернуть в родительский процесс N мегабайт в качестве результата.


Map-reduce по пользователям, локациям, документам,  ip- адресам, датам, … (дополните сами). В самых печальных случаях параллельное исполнение может оказаться более длительным, чем однопоточное. Также могут наблюдаться проблемы с нехваткой памяти. Все пропало? Вовсе нет.


Альтернативный путь


Рассмотрим тезисно способ радикального улучшения ситуации. При этом не забываем, что мы живем в рамках полного зоопарка. Продуктивный контур на *nix, DS ноутбуки на Win*nix\MacOS, а надо, чтобы везде работало единообразно.


  1. Микрозадача: получил на вход пользователя, запросил БД, запросил 2 внешних ИС через REST, скачал и разобрал справочник с диска, провел расчет, сбросил результат на диск\БД. Пользователей, например, 10^6.
  2. Переходим к использованию пакета future и универсального адаптера doFuture.
  3.  Если отдельные задачи таковы, что в рамках отдельных задач процессорное время нужно в малом объеме (ждем ответов сторонних систем), то doFuture позволяет в одну строчку перейти от разбиения по потокам к разбиению по отдельным процессам (можно в *nix в htop поглядеть параметры запуска).
  4. Этих процессов можно создать гораздо больше чем ядер. Никакого клинча не произойдет поскольку отдельные процессы бывают в режиме ожидания большую часть времени. Но надо будет опытным путем подобрать оптимальное число процессов исходя из циклограммы типового процесса обработки.

Результат — исходная задача выполняется в разы быстрее. Ускорение может быть даже больше числа доступных ядер.
Кода нет сознательно, поскольку основная задача публикации — поделиться подходом и отличным семейством пакетов future.


P.S.


Есть еще несколько маленьких нюансов за которыми тоже надо проследить:


  • каждый процесс будет потреблять память, включая получаемые и возвращаемые данные. Увеличение числа процессов кратно увеличит требования по доступной оперативной памяти.
  • doFuture использует «магию» для автоматического определения состава передаваемых в процесс переменных и пакетов, но пускать все на самотек не стоит, лучше проверить.
  • в процессах явное управление gc и явное удаление переменных с помощью rm не помешает. Это не панацея и может не сработать, но явно указать удаляемые объекты будет в помощь.
  • после завершения вычислений вызывайте plan(sequential). Это закроет все процессы и освободит занимаемую ими память.
  • если необходимо передать в процесс большой объем данных рассмотрите возможность использования внешнего хранилища (диск, БД). Не забываем, что дескрипторы передать нельзя, открывать источник надо внутри самого процесса.

Предыдущая публикация — «Бизнес-процессы в enterprise компаниях: домыслы и реальность. Проливаем свет с помощью R».

Tags:
Hubs:
+10
Comments2

Articles