Когда (если когда-либо) я должен указать R parallel не использовать все ядра?

Я использовал этот код:

library(parallel)
cl <- makeCluster( detectCores() - 1)
clusterCall(cl, function(){library(imager)})

то у меня есть функция-оболочка, которая выглядит примерно так:

d <- matrix  #Loading a batch of data into a matrix
res <- parApply(cl, d, 1, FUN, ...)
# Upload `res` somewhere

Я тестировал на своем ноутбуке с 8 ядрами (4 ядра, гиперпоточность). Когда я запускал его на матрице из 50 000 строк, 800 столбцов, для завершения потребовалось 177,5 с, и большую часть времени 7 ядер были загружены почти на 100% (согласно верхней части), затем он оставался там последние 15 или около того секунд, которые, я думаю, объединяли результаты. Согласно system.time(), пользовательское время составило 14 секунд, так что это совпадает.

Сейчас я работаю на EC2, 36-ядерном процессоре c4.8xlarge, и я вижу, что он проводит почти все свое время только с одним ядром на 100%. Точнее: есть примерно 10-20 секунд всплеска, когда все ядра используются, затем около 90 секунд только одного ядра на 100% (используется R), затем около 45 секунд других вещей (где я сохраняю результаты и загрузить следующий пакет данных). Я делаю партии из 40 000 строк, 800 столбцов.

Средняя долгосрочная нагрузка, по данным top, колеблется в районе 5,00.

Это кажется разумным? Или есть момент, когда параллелизм R тратит больше времени на коммуникационные накладные расходы, и я должен ограничиться, например, 16 ядер. Какие-нибудь эмпирические правила здесь?

Ссылка: спецификация ЦП Я использую "Linux 4.4 .5-15.26.amzn1.x86_64 (amd64)". R версия 3.2.2 (2015-08-14)

ОБНОВЛЕНИЕ: я пробовал с 16 ядрами. Для наименьших данных время выполнения увеличилось с 13,9 до 18,3 с. Для данных среднего размера:

With 16 cores:
   user  system elapsed 
 30.424   0.580  60.034 

With 35 cores:
   user  system elapsed 
 30.220   0.604  54.395 

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

Я также пытался использовать mclapply(), как было предложено в комментариях. Он показался немного быстрее (что-то вроде 330 с против 360 с на конкретных тестовых данных, на которых я пробовал), но это было на моем ноутбуке, где другие процессы или перегрев могли повлиять на результаты. Так что никаких выводов по этому поводу я пока не делаю.


person Darren Cook    schedule 13.09.2016    source источник
comment
Это зависит от конкретной используемой функции и от того, сколько данных необходимо скопировать для каждой задачи. Как правило, затраты на распараллеливание не должны сильно зависеть от количества ядер.   -  person Roland    schedule 13.09.2016
comment
@Roland Это зависит, по крайней мере, линейно от количества ядер, а в более сложных схемах распараллеливания зависимость может быть даже сверхлинейной IIRC.   -  person Konrad Rudolph    schedule 13.09.2016
comment
@KonradRudolph Спасибо. Однако обычно это должно компенсироваться сэкономленным временем за счет использования большего количества ядер (по крайней мере, при сравнении 16 и 36 ядер). Если это не так, OP, вероятно, копирует большие объекты на рабочих и обратно.   -  person Roland    schedule 13.09.2016
comment
@Roland Ну, экономия времени не масштабируется линейно ;-) Она следует сигмоидальной кривой: en.wikipedia.org/wiki/Amdahl%27s_law — вот почему у каждого алгоритма есть предел, при котором выделение большего количества ядер для решения проблемы замедляет выполнение (для заданного размера входных данных).   -  person Konrad Rudolph    schedule 13.09.2016
comment
@KonradRudolph Я согласен с этим. Все, что я говорю, это то, что если это произойдет при использовании 36 ядер, ОП должен тщательно подумать, почему это происходит и нельзя ли этого избежать.   -  person Roland    schedule 13.09.2016
comment
Одна вещь, на которую вы могли бы обратить внимание, — это использование одной из функций из пакета multicore, например mclapply. Мое ограниченное понимание распараллеливания в R заключается в том, что эта функция позволяет совместно использовать память между ядрами, а parApply — нет. переключение на эту или другую функцию из пакета multicore может уменьшить коммуникационные издержки. Краткая история: было 2 пакета multicore и snow. multicore работал на *nix-системах (не на windows), а snow работал на всех системах. Функция parApply происходит от snow.   -  person lmo    schedule 13.09.2016
comment
@lmo mclapply не разделяет память, и, насколько мне известно, общая память принципиально невозможна с форками Unix, за исключением явного отображения общей области (например, mmap).   -  person Konrad Rudolph    schedule 14.09.2016
comment
@KonradRudolph Вот цитата, которая отражает то, что я хотел сказать. В многоядерном режиме все задания совместно используют полное состояние R, когда создаются параллельные дочерние экземпляры, а для создания используется системный вызов «fork» или эквивалент, специфичный для операционной системы. Этот механизм приводит к быстрому созданию и тому преимуществу, что данные или код не нужно копировать или инициализировать в дочерних процессах. Из этого pdf-файла: georglsm.r-forge.r- project.org/site-projects/pdf/paraCompR.pdf   -  person lmo    schedule 14.09.2016
comment
@KonradRudolph @lmo В этом ответе говорится, что mclapply является копированием при записи для передаваемых переменных. /841830 Это могло бы объяснить ускорение, которое я увидел, сохранив копирование данных в каждый поток, хотя, как я отметил в своем редактировании, мне нужно провести более строгие тесты времени.   -  person Darren Cook    schedule 14.09.2016
comment
@lmo Механизм одинаков для любого вызова, потому что оба механизма используют системный вызов fork. Однако фраза «поделиться полным состоянием R» вводит в заблуждение: как заметил Даррен, fork на самом деле реализует копирование при записи. Это означает, что состояние фактически полностью изолировано и логически представляет собой независимую копию. Это реализовано с помощью копирования при записи в качестве оптимизации, что означает, что состояние фактически копируется только тогда, когда процесс выполняет операцию записи. Связь между процессами невозможна. Кроме того, копирование при записи происходит очень быстро (= ложные срабатывания).   -  person Konrad Rudolph    schedule 15.09.2016


Ответы (2)


Полезных эмпирических правил не существует — количество ядер, для которых оптимальна параллельная задача, полностью определяется этой задачей. Для более общего обсуждения см. закон Густафсона.

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

person Konrad Rudolph    schedule 13.09.2016
comment
Ну, это также зависит от других задач — что еще работает на этом компьютере ;-) - person Ott Toomet; 27.10.2016

Я бы добавил, что если вы не знакомы с этим замечательным ресурсом для параллельных вычислений в R, вам может быть очень полезно прочитать недавнюю книгу Нормана Матлоффа Parallel Computing for Data Science: With Examples in R, C++ and CUDA. Я очень рекомендую это (я многому научился, не имея опыта работы с CS).

Книга подробно отвечает на ваш вопрос (особенно в главе 2). Книга дает общий обзор причин накладных расходов, которые приводят к узким местам в параллельных программах.

Цитирование раздела 2.1, который неявно частично отвечает на ваш вопрос:

В параллельном программировании есть две основные проблемы с производительностью:

Затраты на связь: обычно данные должны передаваться между процессами туда и обратно. Это требует времени, которое может сильно сказаться на производительности. Кроме того, процессы могут мешать друг другу, если все они пытаются одновременно получить доступ к одним и тем же данным. Они могут конфликтовать при попытке доступа к одному и тому же каналу связи, одному и тому же модулю памяти и т. д. Это очередной подсос скорости. Термин гранулярность используется для грубого обозначения отношения вычислений к накладным расходам. Крупнозернистые или крупнозернистые алгоритмы включают в себя достаточно большие фрагменты вычислений, поэтому накладные расходы не представляют большой проблемы. В мелкозернистых алгоритмах нам действительно нужно максимально избегать накладных расходов.

^ Когда накладные расходы высоки, меньшее количество ядер для решаемой задачи может сократить общее время вычислений.

Балансировка нагрузки. Как отмечалось в предыдущей главе, если мы не будем осторожны в распределении работы по процессам, мы рискуем назначить одним из них гораздо больше работы, чем другим. Это ставит под угрозу производительность, так как оставляет некоторые процессы непродуктивными в конце выполнения, в то время как работа еще не завершена.

Когда вообще не использовать все ядра? Один пример из моего личного опыта ежедневного запуска cronjobs в R с данными, которые составляют 100-200 ГБ данных в ОЗУ, в которых несколько ядер запускаются для обработки блоков данных, я действительно обнаружил, что работает, скажем, с 6 из 32 доступных ядер. быть быстрее, чем при использовании 20-30 ядер. Основной причиной были требования к памяти для дочерних процессов (после того, как определенное количество дочерних процессов было задействовано, использование памяти стало высоким, и все значительно замедлилось).

person FXQuantTrader    schedule 07.10.2016