Dans mon récent travail avec Gibbs sampling
, j'ai beaucoup utilisé le RVar
qui, à mon avis, fournit une interface presque idéale pour la génération de nombres aléatoires. Malheureusement, je n'ai pas pu utiliser Repa en raison de l'incapacité d'utiliser des actions monadiques dans les cartes.
Bien que les cartes clairement monadiques ne puissent pas être parallélisées en général, il me semble que cela RVar
peut être au moins un exemple de monade où les effets peuvent être parallélisés en toute sécurité (du moins en principe; je ne suis pas très familier avec le fonctionnement interne de RVar
) . À savoir, je veux écrire quelque chose comme ce qui suit,
drawClass :: Sample -> RVar Class
drawClass = ...
drawClasses :: Array U DIM1 Sample -> RVar (Array U DIM1 Class)
drawClasses samples = A.mapM drawClass samples
où A.mapM
ressemblerait quelque chose comme,
mapM :: ParallelMonad m => (a -> m b) -> Array r sh a -> m (Array r sh b)
Bien que la manière dont cela fonctionnerait dépende de manière cruciale de la mise en œuvre de RVar
et de ses sous RandomSource
- jacents , en principe, on pourrait penser que cela impliquerait de tirer une nouvelle graine aléatoire pour chaque thread généré et de procéder comme d'habitude.
Intuitivement, il semble que cette même idée puisse se généraliser à d'autres monades.
Donc, ma question est: pourrait-on construire une classe ParallelMonad
de monades pour lesquelles les effets peuvent être parallélisés en toute sécurité (vraisemblablement habités par, au moins RVar
)?
À quoi cela pourrait-il ressembler? Quelles autres monades pourraient habiter cette classe? D'autres ont-ils envisagé comment cela pourrait fonctionner dans Repa?
Enfin, si cette notion d'actions monadiques parallèles ne peut être généralisée, y a-t-il quelqu'un qui voit une manière intéressante de faire fonctionner cela dans le cas spécifique de RVar
(où cela serait très utile)? Renoncer RVar
au parallélisme est un compromis très difficile.
la source
RandomSource
spécifique. Ma tentative naïve de dessiner une graine serait de faire quelque chose de simple et probablement très mal, 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 travailleur, ajouter 2 pour le second ouvrier, etc. Malheureusement insuffisant si vous avez besoin d'une entropie de qualité cryptographique; j'espère bien si vous avez juste besoin d'une promenade aléatoire.split
fonction System.Random . Il a l'inconvénient de produire des résultats différents (en raison de la nature de,split
mais cela fonctionne. Cependant, j'essaie d'étendre cela aux tableaux Repa et je n'ai pas beaucoup de chance. Avez-vous fait des progrès ou est-ce mort- fin?split
fournit une base nécessaire, mais notez le commentaire sur la source pour savoir commentsplit
est implémentée: "- pas de base statistique pour cela!". J'incline à penser que toute méthode de fractionnement d'un PRNG laissera une corrélation exploitable entre ses branches, mais je n'ai pas le contexte statistique pour le prouver. En ce qui concerne la question générale, je ne suis pas certain queRéponses:
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 arbitraireMonad
. Non parallélisable pour des raisons évidentes et est également un peu lent (comme d'habitudemapM
sur une liste lente)traversePrim
- ici, nous sommes limités àPrimMonad
, ce qui est nettement plus rapide quemapM
, mais la raison en est peu importante pour cette discussion.mapIO
- celui-ci, comme son nom l'indique, est limité àIO
(ou plutôtMonadUnliftIO
, 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 à purefmap
, qui est également parallélisable, nous devons êtreIO
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 inmwc-random
et d'autres dansrandom-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:C'est donc exactement ce que j'ai fait.
Je vais d'abord donner des exemples en utilisant les fonctions
randomArrayWS
et lesinitWorkerStates
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: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 threadsPar
- 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
: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 simpleInt
index du worker. Et maintenant, nous pouvons utiliser ces générateurs pour créer un tableau avec des valeurs aléatoires:En utilisant la
Par
stratégie, lascheduler
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êmeWorkerStates
nombre arbitraire de fois tant que cela n'est pas fait simultanément, ce qui sinon entraînerait une exception: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 commegenerateArrayWS
:et
mapWS
:Voici l'exemple promis sur la façon d'utiliser cette fonctionnalité avec
rvar
,random-fu
et lesmersenne-random-pure64
bibliothèques. Nous aurions pu utiliserrandomArrayWS
ici aussi, mais à titre d'exemple, disons que nous avons déjà un tableau avec desRVarT
s différents , auquel cas nous avons besoin d'unmapWS
: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.la source
Ce n'est probablement pas une bonne idée de le faire en raison de la nature intrinsèquement séquentielle des PRNG. Au lieu de cela, vous souhaiterez peut-être effectuer la transition de votre code comme suit:
main
ou quoi que ce soit).la source