Параллельный mapM на массивах Repa

90

В своей недавней работе с Gibbs sampling, я делал большую пользу из RVarкоторых, на мой взгляд, обеспечивает почти идеальный интерфейс для генерации случайных чисел. К сожалению, мне не удалось использовать Repa из-за невозможности использовать монадические действия на картах.

Хотя очевидно, что монадические карты не могут быть распараллелены в целом, мне кажется, что это RVarможет быть по крайней мере один пример монады, где эффекты могут быть безопасно распараллелены (по крайней мере, в принципе; я не очень хорошо знаком с внутренней работой RVar) . А именно, я хочу написать что-то вроде следующего:

drawClass :: Sample -> RVar Class
drawClass = ...

drawClasses :: Array U DIM1 Sample -> RVar (Array U DIM1 Class)
drawClasses samples = A.mapM drawClass samples

где A.mapMбы выглядело что-то вроде,

mapM :: ParallelMonad m => (a -> m b) -> Array r sh a -> m (Array r sh b)

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

Интуитивно кажется, что эта же идея может быть обобщена на некоторые другие монады.

Итак, у меня вопрос: можно ли построить класс ParallelMonadмонад, для которых эффекты могут быть безопасно распараллелены (предположительно, населены, по крайней мере, RVar)?

Как это может выглядеть? Какие еще монады могут населять этот класс? Рассматривали ли другие возможность того, как это может работать в Repa?

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

бгамари
источник
1
Я предполагаю, что камнем преткновения является «рисование нового случайного начального числа для каждого порожденного потока» - как должен работать этот шаг и как следует снова объединить семена после того, как все потоки вернутся?
Daniel Wagner
1
Интерфейс RVar почти наверняка потребует некоторых дополнений для создания нового генератора с заданным семенем. По общему признанию, неясно, как работает эта механика, и это действительно кажется довольно RandomSourceспецифичным. Моя наивная попытка нарисовать семя заключалась бы в том, чтобы сделать что-то простое и, вероятно, очень неправильное, например нарисовать вектор элементов (в случае mwc-random) и добавить 1 к каждому элементу, чтобы создать семя для первого рабочего, добавить 2 для второго рабочий и т. д. Ужасно неадекватно, если вам нужна энтропия криптографического качества; надеюсь, хорошо, если вам просто нужна случайная прогулка.
bgamari
3
Я столкнулся с этим вопросом, пытаясь решить аналогичную проблему. Я использую MonadRandom и System.Random для параллельных монадических случайных вычислений. Это возможно только с splitфункцией System.Random . У него есть недостаток, заключающийся в том, что он дает разные результаты (из-за природы, splitно он действительно работает. Однако я пытаюсь распространить это на массивы Repa, и мне не очень повезло. Вы добились какого-либо прогресса в этом или это мертвый- конец?
Том Сэвидж
1
Монада без последовательности и зависимостей между вычислениями для меня больше похожа на прикладную.
Джон Тайри
1
Я колеблюсь. Как отмечает Том Сэвидж, splitобеспечивает необходимую основу, но обратите внимание на комментарий к источнику о том, как splitэто реализовано: «- для этого нет статистической основы!». Я склонен думать, что любой метод разделения ГПСЧ оставит пригодную для использования корреляцию между его ветвями, но у меня нет статистических данных, подтверждающих это. Что касается общего вопроса, я не уверен что
isturdy

Ответы:

7

Прошло 7 лет с тех пор, как этот вопрос был задан, и до сих пор кажется, что никто не нашел хорошего решения этой проблемы. Repa не имеет функции mapM/ traverselike, даже такой, которая могла бы работать без распараллеливания. Более того, учитывая прогресс, достигнутый за последние несколько лет, маловероятно, что это произойдет.

Из-за устаревшего состояния многих библиотек массивов в Haskell и моего общего недовольства их наборами функций я потратил пару лет на работу над библиотекой массивов massiv, которая заимствует некоторые концепции из Repa, но выводит ее на совершенно другой уровень. Хватит вступления.

До сегодняшнего дня, было три монадическая карта как функции в massiv(не считая синонимом типа функций: imapM, forM. И др):

  • mapM- обычное отображение в произвольное Monad. Невозможно распараллелить по очевидным причинам, а также немного медленнее (как обычно, mapMчем список медленно)
  • traversePrim- здесь мы ограничены PrimMonad, что значительно быстрее, чем mapM, но причина этого не важна для данного обсуждения.
  • mapIO- этот, как следует из названия, ограничен IO(или, скорее MonadUnliftIO, не имеет значения). Поскольку мы находимся внутри, IOмы можем автоматически разделить массив на столько частей, сколько есть ядер, и использовать отдельные рабочие потоки для сопоставления IOдействия с каждым элементом в этих фрагментах. В отличие от pure fmap, который также можно распараллеливать, мы должны быть IOздесь из-за недетерминизма планирования в сочетании с побочными эффектами нашего действия сопоставления.

Итак, как только я прочитал этот вопрос, я подумал про себя, что проблема практически решена massiv, но не так быстро. Генераторы случайных чисел, такие как in mwc-randomи другие in, random-fuне могут использовать один и тот же генератор во многих потоках. Это означает, что единственная часть головоломки, которой мне не хватало, была: «рисование нового случайного начального числа для каждого порожденного потока и выполнение как обычно». Другими словами, мне нужно было две вещи:

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

Так я и поступил.

Сначала я приведу примеры с использованием специально созданных randomArrayWSи initWorkerStatesфункций, так как они более непосредственное отношение к вопросу , а затем перейти к более общей монадической карте. Вот их типовые подписи:

randomArrayWS ::
     (Mutable r ix e, MonadUnliftIO m, PrimMonad m)
  => WorkerStates g -- ^ Use `initWorkerStates` to initialize you per thread generators
  -> Sz ix -- ^ Resulting size of the array
  -> (g -> m e) -- ^ Generate the value using the per thread generator.
  -> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)

Для тех, кто не знаком с этим massiv, Compаргумент представляет собой стратегию вычислений, примечательными конструкторами являются:

  • Seq - выполнять вычисления последовательно, без разветвления потоков
  • Par - развернуть столько потоков, сколько есть возможностей, и использовать их для работы.

mwc-randomСначала я буду использовать пакет в качестве примера, а затем перейду к RVarT:

λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)

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

λ> randomArrayWS gens (Sz2 2 3) standard :: IO (Array P Ix2 Double)
Array P Par (Sz (2 :. 3))
  [ [ -0.9066144845415213, 0.5264323240310042, -1.320943607597422 ]
  , [ -0.6837929005619592, -0.3041255565826211, 6.53353089112833e-2 ]
  ]

Используя Parстратегию, schedulerбиблиотека равномерно распределяет работу по генерации между доступными воркерами, и каждый воркер будет использовать свой собственный генератор, что сделает его потокобезопасным. Ничто не мешает нам повторно использовать одно и то же WorkerStatesпроизвольное количество раз, если это не выполняется одновременно, что в противном случае привело бы к исключению:

λ> randomArrayWS gens (Sz1 10) (uniformR (0, 9)) :: IO (Array P Ix1 Int)
Array P Par (Sz1 10)
  [ 3, 6, 1, 2, 1, 7, 6, 0, 8, 8 ]

Теперь, отложив mwc-randomв сторону, мы можем повторно использовать ту же концепцию для других возможных вариантов использования, используя такие функции, как generateArrayWS:

generateArrayWS ::
     (Mutable r ix e, MonadUnliftIO m, PrimMonad m)
  => WorkerStates s
  -> Sz ix --  ^ size of new array
  -> (ix -> s -> m e) -- ^ element generating action
  -> m (Array r ix e)

и mapWS:

mapWS ::
     (Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
  => WorkerStates s
  -> (a -> s -> m b) -- ^ Mapping action
  -> Array r' ix a -- ^ Source array
  -> m (Array r ix b)

Вот обещанный пример того , как использовать эту функциональность rvar, random-fuи mersenne-random-pure64библиотеку. Мы могли бы использовать и randomArrayWSздесь, но для примера предположим, что у нас уже есть массив с разными RVarTs, и в этом случае нам понадобится mapWS:

λ> import Data.Massiv.Array
λ> import Control.Scheduler (WorkerId(..), initWorkerStates)
λ> import Data.IORef
λ> import System.Random.Mersenne.Pure64 as MT
λ> import Data.RVar as RVar
λ> import Data.Random as Fu
λ> rvarArray = makeArrayR D Par (Sz2 3 9) (\ (i :. j) -> Fu.uniformT i j)
λ> mtState <- initWorkerStates Par (newIORef . MT.pureMT . fromIntegral . getWorkerId)
λ> mapWS mtState RVar.runRVarT rvarArray :: IO (Array P Ix2 Int)
Array P Par (Sz (3 :. 9))
  [ [ 0, 1, 2, 2, 2, 4, 5, 0, 3 ]
  , [ 1, 1, 1, 2, 3, 2, 6, 6, 2 ]
  , [ 0, 1, 2, 3, 4, 4, 6, 7, 7 ]
  ]

Важно отметить, что, несмотря на то, что в приведенном выше примере используется чистая реализация Mersenne Twister, мы не можем избежать ввода-вывода. Это из-за недетерминированного планирования, что означает, что мы никогда не знаем, какой из рабочих будет обрабатывать, какой кусок массива и, следовательно, какой генератор будет использоваться для какой части массива. С другой стороны, если генератор чистый и разделяемый, например splitmix, тогда мы можем использовать чистую, детерминированную и распараллеливаемую функцию генерации:, randomArrayно это уже отдельная история.

Lehins
источник
Если вы хотите увидеть несколько тестов: alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins
4

Это, вероятно, не лучшая идея из-за последовательной природы PRNG. Вместо этого вы можете захотеть перенести свой код следующим образом:

  1. Объявите функцию ввода-вывода ( mainили что у вас).
  2. Прочтите столько случайных чисел, сколько вам нужно.
  3. Передайте (теперь чистые) числа в ваши функции repa.
Макандре
источник
Можно ли записать каждый ГПСЧ в каждый параллельный поток для создания статистической независимости?
Дж. Абрахамсон
@ J.Abrahamson: Да, это было бы возможно. Смотрите мой ответ.
lehins 04