Прошло 7 лет с тех пор, как этот вопрос был задан, и до сих пор кажется, что никто не нашел хорошего решения этой проблемы. Repa не имеет функции mapM
/ traverse
like, даже такой, которая могла бы работать без распараллеливания. Более того, учитывая прогресс, достигнутый за последние несколько лет, маловероятно, что это произойдет.
Из-за устаревшего состояния многих библиотек массивов в 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
-> Sz ix
-> (g -> m e)
-> 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
-> (ix -> s -> m e)
-> 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)
-> Array r' ix a
-> 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
но это уже отдельная история.