Прошло 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
-> 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здесь, но для примера предположим, что у нас уже есть массив с разными 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но это уже отдельная история.