Параллелим R

  • Tutorial

Введение


Сейчас практически невозможно представить себе мир без параллельных вычислений. Параллелят все и вся, даже у мобильных телефонов теперь несколько ядер, а значит… ну вы понимаете. Но давайте поговорим не о мобильных приложениях, а о более полезных и интересных вещах. О машинном обучении. Тема тоже модная, разрекламированная, про машинное обучение слышали даже домохозяйки и только ленивый еще не трогал это руками. Для машинного обучения, и если быть более точным, для статистических расчетов есть множество разных фреймворков, на мой вкус лучший из них – R (да простят меня поклонники Octave). И речь пойдет именно о нем.

Disclaimer:
я не претендую на особую строгость изложения, моя задача донести до читателей общую мысль.

R всем хорош и пригож, но у него есть два ограничения, которые почти никак не мешают при работе с небольшим количеством данных и очень сильно портят жизнь при работе с большими корпусами:
  • весь код выполняется в одном процессе
  • все данные хранятся в памяти

Это почти всегда так, если не очень задумываться об этом. А что делать, если вы задумались об этом, я и расскажу.

Работа без циклов vs foreach


Те, кто писал на R, знают, что в языке вообще-то не принято использовать циклы. Обычно программы используют операции со списками (apply и его собратьев), которые на практике оказываются эффективнее обычного for, потому что внутри может происходить и происходит всякая магия. Да и в философию языка такой подход гораздо лучше вписывается, посмотрите:
    a <- c(); for (i in 1:4) { a<- c(a, i^2) } # тупо цикл
    a <- 1:4 * 1:4 # прекрасно, не правда ли?

Если нужно сделать что-то нетривиальное, можно применить apply. Например, тот же результат можно получить так:
    a <- sapply(1:4, function(x) {x^2})

Семейство apply функций позволяет много вкусностей, которые работают мгновенно и очень радуют глаз. Конечно, такой подход требует некоторой сноровки, но тренировка окупается с лихвой.

А где же тут параллельность, спросите вы? Тут-то и кроется самое интересное.
Пришли обычные люди и подумали: «Все эти apply, списки и т.д. – это хорошо. Но я не хочу переучиваться. Дайте мне знакомый инструмент». И такой инструмент появился. Найти его можно в пакете foreach.
Собственно, нас интересует одноименная функция, прелесть которой заключается в том, что она умеет комбинировать результаты, полученные на каждом шаге цикла (чего не умеет оригинальный for, см. извращения с ним выше). Причем комбинировать можно не просто какими-то способами – foreach можно скормить ваш собственный комбайнер или любую подходящую функцию. Самые частые и полезные – c, cbind, rbind.
    a <- foreach(i=1:4, .combine='c') %do% {i^2}


Поехали!


Как эффективнее всего запустить параллельный процесс вычисления? Разбить задачу на маленькие независимые кусочки. Именно так и работает параллелизация в R. В качестве основы работает модуль foreach и doSNOW.
Создадим «кластер» для наших вычислений и запустим на нем простенький тест.
    # подключим модули
    library(foreach)
    library(doSNOW)
    cl <- makeCluster(4)
    registerDoSNOW(cl)
    a <- mean(foreach(i=1:10^6, .combine='c') %dopar% {mean(rnorm(i))})
    stopCluster(cl)

Для отладки можно использовать опцию %do% а в реальной жизни – %dopar%.
Важно: код, который запускается внутри параллельного цикла должен быть максимально независимым. К примеру, он не сможет увидеть функции, определенные в вашем workspace, поэтому часто их определяют либо прямо внутри %dopar%, либо выносят все необходимые действия в отдельный исходный файл, а затем подключают его через source('trololo.R').

Кластеры doSNOW бывают разные. По сути, кластер – это набор инстансов R, выполняющих код внутри %dopar%. Все они могут находиться на одной машине, но также могут быть разнесены на разные.
    cl1 <- makeCluster(c("localhost","remotehost"), type = "SOCK")

Всего доступно четыре типа кластеров: PVM(http://www.csm.ornl.gov/pvm/), MPI(http://cran.r-project.org/web/packages/Rmpi/index.html), NWS (http://nws-r.sourceforge.net/) и SOCK. Для первых трех потребуются дополнительные библиотеки (собственно, реализации), по умолчанию запустится socket-кластер.

Для отладки и всякой разной статистики можно использовать функцию snow.time, оборачивая в нее все обращения к кластеру:
    cl <- makeCluster(4)
    registerDoSNOW(cl)
    tm <- snow.time(a <- mean(foreach(i=1:10^6, .combine='c') %dopar% {mean(rnorm(i))}))
    print(tm)
    plot(tm)
    stopCluster(cl)

Получаются примерно такие картинки:


А нафига козе баян?


Наверное, еще много чего можно написать, но после этих примеров, я думаю, что все разберуться куда копать.
Зачем все это может понадобиться? Да много зачем, как минимум, мы оптимизируем расход времени, а как максимум теперь можно дождаться окончания тяжелых подсчетов. Я использовал подобный подход в основном при проверке каких либо гипотез, когда необходимо было либо
  • прогнать расчет на очень большом временном периоде (первоначальная оценка работоспособности биржевой стратегии)
  • запустить расчет на множестве похожих dataset-ов (RxQ-кроссвалидация)

Думаю, что у вас получится использовать этот подход в своих целях. Удачи!
Share post

Comments 11

    0
    Спасибо за статью! С процессами понятно. А что делать если датасет выходит за доступную память?
      +1
      Вот почему не стоит писать посты «через ночь» – забыл написать.

      Я имел в виду простой выход: дать вычислениям больше памяти. А сделать это можно, например, организовав кластер из нескольких машин при помощи модулей snow и doSnow.
      0
      Было бы интересно почитать про опыт интеграции R с другими платформами. Народ, я видел, как-то интегрируется с Hadoop. Вам не приходилось заниматься?
        0
        Нет, такого опыта у меня нет, хотя как раз сейчас я о подобной вещи задумываюсь
      • UFO just landed and posted this here
        0
        А как можно узнать количество доступных ядер процессора?
          0
          Непосредственно через R у меня не было нужды такие вещи узнавать.

          Под linux можно так:
          cat /proc/cpuinfo | grep ^processor |wc -l
            0
            Спасибо. В том же OpenMP есть специальная функция, которая это дело возвращает. Думал, что вполне может быть обертка над ней или чем-то похожим в R.
            0
            library(parallel)
            detectCores(logical = FALSE)
            
              0
              Спасибо!

          Only users with full accounts can post comments. Log in, please.