В своей недавней работе с 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
параллелизма - очень трудный компромисс.
источник
RandomSource
специфичным. Моя наивная попытка нарисовать семя заключалась бы в том, чтобы сделать что-то простое и, вероятно, очень неправильное, например нарисовать вектор элементов (в случаеmwc-random
) и добавить 1 к каждому элементу, чтобы создать семя для первого рабочего, добавить 2 для второго рабочий и т. д. Ужасно неадекватно, если вам нужна энтропия криптографического качества; надеюсь, хорошо, если вам просто нужна случайная прогулка.split
функцией System.Random . У него есть недостаток, заключающийся в том, что он дает разные результаты (из-за природы,split
но он действительно работает. Однако я пытаюсь распространить это на массивы Repa, и мне не очень повезло. Вы добились какого-либо прогресса в этом или это мертвый- конец?split
обеспечивает необходимую основу, но обратите внимание на комментарий к источнику о том, какsplit
это реализовано: «- для этого нет статистической основы!». Я склонен думать, что любой метод разделения ГПСЧ оставит пригодную для использования корреляцию между его ветвями, но у меня нет статистических данных, подтверждающих это. Что касается общего вопроса, я не уверен чтоОтветы:
Прошло 7 лет с тех пор, как этот вопрос был задан, и до сих пор кажется, что никто не нашел хорошего решения этой проблемы. Repa не имеет функции
mapM
/traverse
like, даже такой, которая могла бы работать без распараллеливания. Более того, учитывая прогресс, достигнутый за последние несколько лет, маловероятно, что это произойдет.Из-за устаревшего состояния многих библиотек массивов в Haskell и моего общего недовольства их наборами функций я потратил пару лет на работу над библиотекой массивов
massiv
, которая заимствует некоторые концепции из Repa, но выводит ее на совершенно другой уровень. Хватит вступления.До сегодняшнего дня, было три монадическая карта как функции в
massiv
(не считая синонимом типа функций:imapM
,forM
. И др):mapM
- обычное отображение в произвольноеMonad
. Невозможно распараллелить по очевидным причинам, а также немного медленнее (как обычно,mapM
чем список медленно)traversePrim
- здесь мы ограниченыPrimMonad
, что значительно быстрее, чемmapM
, но причина этого не важна для данного обсуждения.mapIO
- этот, как следует из названия, ограниченIO
(или, скорееMonadUnliftIO
, не имеет значения). Поскольку мы находимся внутри,IO
мы можем автоматически разделить массив на столько частей, сколько есть ядер, и использовать отдельные рабочие потоки для сопоставленияIO
действия с каждым элементом в этих фрагментах. В отличие от purefmap
, который также можно распараллеливать, мы должны бытьIO
здесь из-за недетерминизма планирования в сочетании с побочными эффектами нашего действия сопоставления.Итак, как только я прочитал этот вопрос, я подумал про себя, что проблема практически решена
massiv
, но не так быстро. Генераторы случайных чисел, такие как inmwc-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
здесь, но для примера предположим, что у нас уже есть массив с разнымиRVarT
s, и в этом случае нам понадобится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
но это уже отдельная история.источник
Это, вероятно, не лучшая идея из-за последовательной природы PRNG. Вместо этого вы можете захотеть перенести свой код следующим образом:
main
или что у вас).источник