Sono passati 7 anni da quando è stata posta questa domanda e sembra che nessuno abbia ancora trovato una buona soluzione a questo problema. Repa non ha una funzione mapM
/ traverse
like, anche una che potrebbe essere eseguita senza parallelizzazione. Inoltre, considerando l'entità dei progressi compiuti negli ultimi anni, sembra improbabile che ciò accada.
A causa dello stato obsoleto di molte librerie di array in Haskell e della mia insoddisfazione generale per i loro set di funzionalità, ho messo avanti un paio di anni di lavoro in una libreria di array massiv
, che prende in prestito alcuni concetti da Repa, ma lo porta a un livello completamente diverso. Basta con l'intro.
Prima di oggi, c'era tre mappa monadico come funzioni massiv
(non contando il sinonimo come funzioni: imapM
, forM
et al.):
mapM
- la solita mappatura in modo arbitrario Monad
. Non parallelizzabile per ovvi motivi ed è anche un po 'lento (sulla falsariga del solito mapM
su un elenco lento)
traversePrim
- qui siamo limitati a PrimMonad
, che è significativamente più veloce di mapM
, ma la ragione di ciò non è importante per questa discussione.
mapIO
- questo, come suggerisce il nome, è limitato a IO
(o meglio MonadUnliftIO
, ma è irrilevante). Poiché ci IO
troviamo, possiamo dividere automaticamente l'array in tanti blocchi quanti sono i core e utilizzare thread di lavoro separati per mappare l' IO
azione su ogni elemento in quei blocchi. A differenza di Pure fmap
, che è anche parallelizzabile, dobbiamo essere IO
qui a causa del non determinismo della pianificazione combinato con gli effetti collaterali della nostra azione di mappatura.
Quindi, una volta che ho letto questa domanda, ho pensato a me stesso che il problema è praticamente risolto massiv
, ma non così velocemente. I generatori di numeri casuali, come in mwc-random
e altri in random-fu
non possono utilizzare lo stesso generatore su molti thread. Il che significa che l'unico pezzo del puzzle che mi mancava era: "disegnare un nuovo seme casuale per ogni thread generato e procedere come al solito". In altre parole, avevo bisogno di due cose:
- Una funzione che inizializzerebbe tanti generatori quanti sono i thread di lavoro
- e un'astrazione che fornirebbe perfettamente il generatore corretto alla funzione di mappatura a seconda del thread in cui è in esecuzione l'azione.
Quindi è esattamente quello che ho fatto.
Per prima cosa fornirò esempi usando le funzioni randomArrayWS
e appositamente predisposte initWorkerStates
, poiché sono più rilevanti per la domanda e successivamente passerò alla mappa monadica più generale. Ecco le loro firme di tipo:
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)
Per coloro che non hanno familiarità con massiv
, l' Comp
argomento è una strategia di calcolo da utilizzare, i costruttori notevoli sono:
Seq
- Esegui il calcolo in modo sequenziale, senza forkare alcun thread
Par
- avvia tanti thread quante sono le capacità e usali per fare il lavoro.
mwc-random
Inizialmente userò il pacchetto come esempio e successivamente passerò a RVarT
:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
Sopra abbiamo inizializzato un generatore separato per thread usando la casualità del sistema, ma avremmo potuto usare altrettanto bene un seme univoco per thread derivandolo WorkerId
dall'argomento, che è un semplice Int
indice del worker. E ora possiamo usare quei generatori per creare un array con valori casuali:
λ> 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 ]
]
Usando la Par
strategia la scheduler
libreria dividerà equamente il lavoro di generazione tra i lavoratori disponibili e ogni lavoratore utilizzerà il proprio generatore, rendendolo così thread-safe. Niente ci impedisce di riutilizzare lo stesso WorkerStates
numero arbitrario di volte fintanto che non viene eseguito contemporaneamente, il che altrimenti risulterebbe in un'eccezione:
λ> 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 ]
Ora mettendo mwc-random
da parte possiamo riutilizzare lo stesso concetto per altri possibili casi d'uso utilizzando funzioni come 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)
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)
Ecco l'esempio promesso su come utilizzare questa funzionalità con rvar
, random-fu
e mersenne-random-pure64
le librerie. Avremmo potuto usarlo anche randomArrayWS
qui, ma per motivi di esempio diciamo che abbiamo già un array con RVarT
s differenti , nel qual caso abbiamo bisogno di 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 ]
]
È importante notare che, nonostante il fatto che nell'esempio precedente venga utilizzata la pura implementazione di Mersenne Twister, non possiamo sfuggire all'IO. Ciò è dovuto alla pianificazione non deterministica, il che significa che non sappiamo mai quale dei worker gestirà quale blocco dell'array e di conseguenza quale generatore verrà utilizzato per quale parte dell'array. D'altro canto, se il generatore è puro e divisibile, come splitmix
, allora possiamo usare la funzione di generazione pura, deterministica e parallelizzabile :, randomArray
ma questa è già una storia a parte.