MapM parallela su array Repa


90

Nel mio recente lavoro con Gibbs sampling, ho fatto un grande uso del RVarquale, a mio avviso, fornisce un'interfaccia quasi ideale per la generazione di numeri casuali. Purtroppo, non sono stato in grado di utilizzare Repa a causa dell'impossibilità di utilizzare azioni monadiche nelle mappe.

Mentre le mappe chiaramente monadiche non possono essere parallelizzate in generale, mi sembra che RVarpossa essere almeno un esempio di una monade in cui gli effetti possono essere parallelizzati in sicurezza (almeno in linea di principio; non ho molta familiarità con il funzionamento interno di RVar) . Vale a dire, voglio scrivere qualcosa come il seguente,

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

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

dove A.mapMsarebbe qualcosa di simile,

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

Anche se chiaramente il modo in cui questo funzionerebbe dipende in modo cruciale dall'implementazione RVare dal suo sottostante RandomSource, in linea di principio si potrebbe pensare che ciò comporterebbe il disegno di un nuovo seme casuale per ogni thread generato e procedere come al solito.

Intuitivamente, sembra che questa stessa idea possa essere generalizzata ad altre monadi.

Quindi, la mia domanda è: si potrebbe costruire una classe ParallelMonaddi monadi per le quali gli effetti possono essere parallelizzati in modo sicuro (presumibilmente abitata, almeno, RVar)?

Come potrebbe essere? Quali altre monadi potrebbero abitare questa classe? Altri hanno considerato la possibilità di come questo potrebbe funzionare in Repa?

Infine, se questa nozione di azioni monadiche parallele non può essere generalizzata, qualcuno vede un modo carino per farlo funzionare nel caso specifico di RVar(dove sarebbe molto utile)? Rinunciare RVaral parallelismo è un compromesso molto difficile.


1
Immagino che il punto critico sia "disegnare un nuovo seme casuale per ogni thread generato" - come dovrebbe funzionare questo passaggio e come dovrebbero essere uniti di nuovo i semi una volta che tutti i thread sono tornati?
Daniel Wagner,

1
L'interfaccia RVar avrebbe quasi certamente bisogno di alcune aggiunte per accogliere la generazione di un nuovo generatore con un dato seme. Certo, non è chiaro come funzionino i meccanismi di questo e sembra piuttosto RandomSourcespecifico. Il mio ingenuo tentativo di disegnare un seme sarebbe fare qualcosa di semplice e probabilmente molto sbagliato come disegnare un vettore di elementi (nel caso di mwc-random) e aggiungere 1 a ciascun elemento per produrre un seme per il primo lavoratore, aggiungere 2 per il secondo lavoratore, ecc. Purtroppo inadeguato se hai bisogno di entropia di qualità crittografica; spero che vada bene se hai solo bisogno di una passeggiata casuale.
bgamari

3
Mi sono imbattuto in questa domanda mentre cercavo di risolvere un problema simile. Sto usando MonadRandom e System.Random per calcoli casuali monadici in parallelo. Questo è possibile solo con la splitfunzione System.Random . Ha lo svantaggio di produrre risultati diversi (a causa della natura di splitma funziona. Tuttavia, sto cercando di estenderlo agli array Repa e non ho molta fortuna. Hai fatto progressi con questo o è un guasto fine?
Tom Savage

1
Monade senza sequenze e dipendenze tra i calcoli mi sembra più applicativo.
John Tyree

1
Sono titubante. Come osserva Tom Savage, splitfornisce una base necessaria, ma nota il commento sulla fonte per come splitviene implementata: "- nessuna base statistica per questo!". Tendo a pensare che qualsiasi metodo per suddividere un PRNG lascerà una correlazione sfruttabile tra i suoi rami, ma non ho il background statistico per dimostrarlo. Per quanto riguarda la domanda generale, non sono certo che
previsto

Risposte:


7

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/ traverselike, 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, forMet al.):

  • mapM- la solita mappatura in modo arbitrario Monad. Non parallelizzabile per ovvi motivi ed è anche un po 'lento (sulla falsariga del solito mapMsu 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 IOtroviamo, possiamo dividere automaticamente l'array in tanti blocchi quanti sono i core e utilizzare thread di lavoro separati per mappare l' IOazione su ogni elemento in quei blocchi. A differenza di Pure fmap, che è anche parallelizzabile, dobbiamo essere IOqui 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-randome altri in random-funon 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 randomArrayWSe 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' Compargomento è 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-randomInizialmente 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 WorkerIddall'argomento, che è un semplice Intindice 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 Parstrategia la schedulerlibreria 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 WorkerStatesnumero 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-randomda 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-fue mersenne-random-pure64le librerie. Avremmo potuto usarlo anche randomArrayWSqui, ma per motivi di esempio diciamo che abbiamo già un array con RVarTs 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 :, randomArrayma questa è già una storia a parte.


Nel caso in cui desideri vedere alcuni benchmark: alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins

4

Probabilmente non è una buona idea farlo a causa della natura intrinsecamente sequenziale dei PRNG. Invece, potresti voler trasferire il tuo codice come segue:

  1. Dichiara una funzione IO ( maino cosa hai).
  2. Leggi tutti i numeri casuali di cui hai bisogno.
  3. Passa i numeri (ora puri) alle tue funzioni repa.

Sarebbe possibile masterizzare ogni PRNG in ogni thread parallelo per creare indipendenza statistica?
J. Abrahamson

@ J.Abrahamson sì, sarebbe possibile. Vedi la mia risposta.
lehins
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.