Cela fait 7 ans que cette question a été posée, et il semble toujours que personne n'ait trouvé une bonne solution à ce problème. Repa n'a pas de fonction mapM
/ traverse
like, même celle qui pourrait fonctionner sans parallélisation. De plus, compte tenu de l'ampleur des progrès accomplis ces dernières années, il semble peu probable que cela se produise non plus.
En raison de l'état obsolète de nombreuses bibliothèques de tableaux dans Haskell et de mon mécontentement général concernant leurs ensembles de fonctionnalités, j'ai mis en avant quelques années de travail dans une bibliothèque de tableaux massiv
, qui emprunte certains concepts à Repa, mais l'amène à un niveau complètement différent. Assez avec l'intro.
Avant aujourd'hui, il y avait trois carte monadique comme fonctions massiv
(sans compter les synonymes comme fonctions: imapM
, forM
. Et al):
mapM
- la cartographie habituelle dans un arbitraire Monad
. Non parallélisable pour des raisons évidentes et est également un peu lent (comme d'habitude mapM
sur une liste lente)
traversePrim
- ici, nous sommes limités à PrimMonad
, ce qui est nettement plus rapide que mapM
, mais la raison en est peu importante pour cette discussion.
mapIO
- celui-ci, comme son nom l'indique, est limité à IO
(ou plutôt MonadUnliftIO
, mais ce n'est pas pertinent). Parce que nous sommes dans, IO
nous pouvons automatiquement diviser le tableau en autant de morceaux qu'il y a de cœurs et utiliser des threads de travail séparés pour mapper l' IO
action sur chaque élément de ces morceaux. Contrairement à pure fmap
, qui est également parallélisable, nous devons être IO
ici en raison du non-déterminisme de l'ordonnancement combiné aux effets secondaires de notre action de cartographie.
Donc, une fois que j'ai lu cette question, je me suis dit que le problème était pratiquement résolu massiv
, mais pas si vite. Les générateurs de nombres aléatoires, tels que in mwc-random
et d'autres dans random-fu
ne peuvent pas utiliser le même générateur sur de nombreux threads. Ce qui signifie que la seule pièce du puzzle qui me manquait était: "dessiner une nouvelle graine aléatoire pour chaque fil généré et procéder comme d'habitude". En d'autres termes, j'avais besoin de deux choses:
- Une fonction qui initialiserait autant de générateurs qu'il y aura de threads de travail
- et une abstraction qui donnerait de manière transparente le générateur correct à la fonction de mappage en fonction du thread dans lequel l'action s'exécute.
C'est donc exactement ce que j'ai fait.
Je vais d'abord donner des exemples en utilisant les fonctions randomArrayWS
et les initWorkerStates
fonctions spécialement conçues , car elles sont plus pertinentes pour la question et je passerai plus tard à la carte monadique plus générale. Voici leurs signatures de type:
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)
Pour ceux qui ne sont pas familiers avec massiv
, l' Comp
argument est une stratégie de calcul à utiliser, les constructeurs notables sont:
Seq
- exécuter le calcul séquentiellement, sans forger de threads
Par
- faites tourner autant de threads qu'il y a de capacités et utilisez-les pour faire le travail.
J'utiliserai le mwc-random
package comme exemple initialement et je passerai plus tard à RVarT
:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
Ci-dessus, nous avons initialisé un générateur séparé par thread en utilisant le caractère aléatoire du système, mais nous aurions tout aussi bien pu utiliser un unique par thread en le dérivant de l' WorkerId
argument, qui est un simple Int
index du worker. Et maintenant, nous pouvons utiliser ces générateurs pour créer un tableau avec des valeurs aléatoires:
λ> 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 ]
]
En utilisant la Par
stratégie, la scheduler
bibliothèque répartira uniformément le travail de génération entre les ouvriers disponibles et chaque ouvrier utilisera son propre générateur, le rendant ainsi sûr pour les threads. Rien ne nous empêche de réutiliser le même WorkerStates
nombre arbitraire de fois tant que cela n'est pas fait simultanément, ce qui sinon entraînerait une exception:
λ> 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 ]
Maintenant, en mettant mwc-random
de côté, nous pouvons réutiliser le même concept pour d'autres cas d'utilisation possibles en utilisant des fonctions comme 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)
et 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)
Voici l'exemple promis sur la façon d'utiliser cette fonctionnalité avec rvar
, random-fu
et les mersenne-random-pure64
bibliothèques. Nous aurions pu utiliser randomArrayWS
ici aussi, mais à titre d'exemple, disons que nous avons déjà un tableau avec des RVarT
s différents , auquel cas nous avons besoin d'un 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 ]
]
Il est important de noter que, malgré le fait que la mise en œuvre pure de Mersenne Twister est utilisée dans l'exemple ci-dessus, nous ne pouvons pas échapper à l'IO. Ceci est dû à l'ordonnancement non déterministe, ce qui signifie que nous ne savons jamais lequel des ouvriers manipulera quel morceau du tableau et par conséquent quel générateur sera utilisé pour quelle partie du tableau. Du côté positif, si le générateur est pur et divisible, comme splitmix
, alors nous pouvons utiliser la fonction de génération pure, déterministe et parallélisable :, randomArray
mais c'est déjà une histoire à part.