Cela fait 7 ans que cette question a été posée, et il semble toujours que personne n'ait trouvé de bonne solution à ce problème. Repa n'a pas de mapM
/ traverse
comme une fonction, même une qui pourrait fonctionner sans parallélisation. De plus, compte tenu des progrès réalisés au cours des dernières années, il semble peu probable que cela se produise.
En raison de la désuétude de nombreuses bibliothèques de tableaux en Haskell et de mon insatisfaction générale quant à leurs fonctionnalités, j'ai consacré quelques années de travail à la création d'une bibliothèque de tableaux. massiv
qui emprunte certains concepts à Repa, mais l'amène à un niveau complètement différent. Assez avec l'introduction.
Jusqu'à aujourd'hui, il existait trois fonctions monadiques semblables à des cartes dans la bibliothèque de l'UE. massiv
(sans compter les fonctions synonymes comme les fonctions : imapM
, forM
et al.) :
-
mapM
- la correspondance habituelle dans un Monad
. Il n'est pas parallélisable pour des raisons évidentes et est également un peu lent (dans la lignée de ce qui se fait habituellement). mapM
sur une liste lente)
-
traversePrim
- ici nous sommes limités à PrimMonad
qui est nettement plus rapide que mapM
mais la raison de ce choix n'est pas importante pour cette discussion.
-
mapIO
- celui-ci, comme son nom l'indique, est réservé aux personnes suivantes IO
(ou plutôt MonadUnliftIO
mais cela 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 fils de travail distincts pour mettre en correspondance le tableau et les cœurs. IO
sur chaque élément de ces morceaux. Contrairement aux fmap
qui est également parallélisable, nous devons être dans IO
ici en raison du non-déterminisme de l'ordonnancement combiné aux effets secondaires de notre action de mise en correspondance.
Ainsi, lorsque j'ai lu cette question, je me suis dit que le problème est pratiquement résolu en massiv
mais pas si vite. Les générateurs de nombres aléatoires, comme dans mwc-random
et d'autres dans random-fu
ne peut pas utiliser le même générateur sur plusieurs fils. Ce qui veut dire que la seule pièce du puzzle qui me manquait était : "tirer une nouvelle graine aléatoire pour chaque thread créé 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 fils 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 est exécutée.
C'est donc exactement ce que j'ai fait.
Tout d'abord, je vais donner des exemples en utilisant l'outil spécialement conçu à cet effet randomArrayWS
y initWorkerStates
car elles sont plus pertinentes pour la question, avant de passer à 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
le Comp
L'argument est une stratégie de calcul à utiliser, les constructeurs notables sont :
-
Seq
- exécuter le calcul de manière séquentielle, sans bifurquer de threads
-
Par
- créer autant de fils qu'il y a de possibilités et les utiliser pour faire le travail.
Je vais utiliser mwc-random
à titre d'exemple dans un premier temps, puis de passer à 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 distinct par thread en utilisant le caractère aléatoire du système, mais nous aurions pu tout aussi bien utiliser une graine unique par thread en la dérivant de l'attribut WorkerId
argument, qui est un simple Int
l'indice du travailleur. 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 Par
stratégie le scheduler
répartira équitablement le travail de génération entre les travailleurs disponibles et chaque travailleur utilisera son propre générateur, ce qui le rendra thread safe. Rien ne nous empêche de réutiliser le même WorkerStates
un nombre arbitraire de fois, tant qu'elle n'est pas effectuée 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
nous pouvons réutiliser le même concept pour d'autres cas d'utilisation possibles en utilisant des fonctions telles que 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)
y 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
y mersenne-random-pure64
bibliothèques. Nous aurions pu utiliser randomArrayWS
ici aussi, mais pour les besoins de l'exemple, disons que nous avons déjà un tableau avec différents RVarT
dans ce 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 qu'en dépit du fait qu'une implémentation pure de Mersenne Twister est utilisée dans l'exemple ci-dessus, nous ne pouvons pas échapper à l'IO. Cela est dû à l'ordonnancement non déterministe, ce qui signifie que nous ne savons jamais lequel des travailleurs traitera quel morceau du tableau et, par conséquent, quel générateur sera utilisé pour quelle partie du tableau. D'un autre côté, si le générateur est pur et divisible, tel que splitmix
nous pouvons alors utiliser la fonction de génération pure, déterministe et parallélisable : randomArray
mais c'est déjà une autre histoire.
1 votes
Je suppose que le point d'achoppement est "dessiner une nouvelle graine aléatoire pour chaque fil créé" -- comment cette étape doit-elle fonctionner, et comment les graines doivent-elles être fusionnées à nouveau une fois que tous les fils sont revenus ?
1 votes
L'interface RVar nécessitera certainement quelques ajouts pour permettre la création d'un nouveau générateur avec une graine donnée. Il est vrai que la mécanique de ce fonctionnement n'est pas claire et qu'il semble assez difficile d'y parvenir.
RandomSource
spécifique. Ma tentative naïve de dessiner une graine serait de faire quelque chose de simple et probablement très mauvais, comme dessiner un vecteur d'éléments (dans le cas demwc-random
) et ajouter 1 à chaque élément pour produire une graine pour le premier ouvrier, ajouter 2 pour le deuxième ouvrier, etc. C'est très insuffisant si vous avez besoin d'une entropie de qualité cryptographique ; c'est très bien si vous avez juste besoin d'une marche aléatoire.0 votes
J'ai réussi à faire quelque chose de similaire à ce que vous demandez en utilisant
fillChunkedIOP
.3 votes
Je suis tombé sur cette question en essayant de résoudre un problème similaire. J'utilise MonadRandom et System.Random pour effectuer des calculs aléatoires monadiques en parallèle. Cela n'est possible qu'avec la fonction de System.Random
split
fonction. Elle présente l'inconvénient de produire des résultats différents (en raison de la nature de la fonctionsplit
mais ça marche. Cependant, j'essaie d'étendre cette méthode aux tableaux Repa et je n'ai pas beaucoup de chance. Avez-vous fait des progrès dans ce domaine ou est-ce une impasse ?1 votes
Les monades sans séquençage ni dépendance entre les calculs me semblent plus proches de l'applicatif.
1 votes
Je suis hésitant. Comme le note Tom Savage,
split
fournit une base nécessaire, mais notez le commentaire sur la source pour savoir commentsplit
est mis en œuvre : "-- aucun fondement statistique pour ça !". Je suis enclin à penser que toute méthode de division d'un PRNG laissera une corrélation exploitable entre ses branches, mais je n'ai pas les connaissances statistiques pour le prouver. En ce qui concerne la question générale, je ne suis pas certain que