MapM paralelo en arreglos Repa

90

En mi trabajo reciente con Gibbs sampling, he estado haciendo un gran uso del RVarcual, en mi opinión, proporciona una interfaz casi ideal para la generación de números aleatorios. Lamentablemente, no he podido utilizar Repa debido a la imposibilidad de utilizar acciones monádicas en los mapas.

Si bien los mapas claramente monádicos no se pueden paralelizar en general, me parece que RVarpuede ser al menos un ejemplo de una mónada donde los efectos se pueden paralelizar de forma segura (al menos en principio; no estoy muy familiarizado con el funcionamiento interno de RVar) . Es decir, quiero escribir algo como lo siguiente,

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

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

donde A.mapMse vería algo como,

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

Si bien es evidente que cómo funcionaría esto depende de manera crucial de la implementación RVary su base RandomSource, en principio uno pensaría que esto implicaría dibujar una nueva semilla aleatoria para cada hilo generado y proceder como de costumbre.

Intuitivamente, parece que esta misma idea podría generalizarse a algunas otras mónadas.

Entonces, mi pregunta es: ¿Podría uno construir una clase ParallelMonadde mónadas para las cuales los efectos se puedan paralelizar de manera segura (presumiblemente habitada por, al menos, RVar)?

¿A qué se podría parecer? ¿Qué otras mónadas podrían habitar esta clase? ¿Otros han considerado la posibilidad de cómo podría funcionar esto en Repa?

Finalmente, si esta noción de acciones monádicas paralelas no se puede generalizar, ¿alguien ve alguna manera agradable de hacer que esto funcione en el caso específico de RVar(donde sería muy útil)? Renunciar al RVarparalelismo es un compromiso muy difícil.

bgamari
fuente
1
Supongo que el punto de fricción es "dibujar una nueva semilla aleatoria para cada hilo generado": ¿cómo debería funcionar este paso y cómo deberían fusionarse las semillas una vez que todos los hilos regresen?
Daniel Wagner
1
Es casi seguro que la interfaz RVar necesitaría algunas adiciones para acomodar el desove de un nuevo generador con una semilla determinada. Es cierto que no está claro cómo funciona la mecánica de este sistema y parece bastante RandomSourceespecífico. Mi intento ingenuo de dibujar una semilla sería hacer algo simple y probablemente muy incorrecto, como dibujar un vector de elementos (en el caso de mwc-random) y agregar 1 a cada elemento para producir una semilla para el primer trabajador, agregar 2 para el segundo trabajador, etc. Lamentablemente inadecuado si necesita entropía de calidad criptográfica; espero que esté bien si solo necesita un paseo al azar.
bgamari
3
Me encontré con esta pregunta mientras intentaba resolver un problema similar. Estoy usando MonadRandom y System.Random para cálculos aleatorios monádicos en paralelo. Esto solo es posible con la splitfunción de System.Random . Tiene la desventaja de producir resultados diferentes (debido a la naturaleza de, splitpero funciona. Sin embargo, estoy tratando de extender esto a las matrices Repa y no tengo mucha suerte. ¿Has progresado en esto o es un error muerto? ¿Fin?
Tom Savage
1
Mónada sin secuenciación y dependencias entre cálculos me suena más a aplicativo.
John Tyree
1
Estoy indeciso. Como señala Tom Savage, splitproporciona una base necesaria, pero tenga en cuenta el comentario sobre la fuente sobre cómo splitse implementa: "¡No hay base estadística para esto!". Me inclino a pensar que cualquier método para dividir un PRNG dejará una correlación explotable entre sus ramas, pero no tengo los antecedentes estadísticos para demostrarlo. En cuanto a la pregunta general, no estoy seguro de que
isturdy

Respuestas:

7

Han pasado 7 años desde que se hizo esta pregunta, y todavía parece que a nadie se le ocurrió una buena solución a este problema. Repa no tiene una función mapM/ traverselike, ni siquiera una que pueda ejecutarse sin paralelización. Además, considerando la cantidad de progreso que hubo en los últimos años, parece poco probable que suceda.

Debido al estado obsoleto de muchas bibliotecas de matrices en Haskell y a mi insatisfacción general con sus conjuntos de funciones, he realizado un par de años de trabajo en una biblioteca de matrices massiv, que toma prestados algunos conceptos de Repa, pero lo lleva a un nivel completamente diferente. Suficiente con la intro.

Antes de hoy, hubo tres funciones mapa monádica como en massiv(sin contar el sinónimo como funciones: imapM, forMet al.):

  • mapM- el mapeo habitual de forma arbitraria Monad. No se puede paralelizar por razones obvias y también es un poco lento (en la línea de lo habitual mapMen una lista lenta)
  • traversePrim- aquí estamos restringidos a PrimMonad, que es significativamente más rápido que mapM, pero la razón de esto no es importante para esta discusión.
  • mapIO- este, como su nombre indica, está restringido a IO(o más bien MonadUnliftIO, pero eso es irrelevante). Debido a que estamos en IO, podemos dividir automáticamente la matriz en tantos fragmentos como núcleos y usar subprocesos de trabajo separados para mapear la IOacción sobre cada elemento en esos fragmentos. A diferencia de pure fmap, que también es paralelizable, tenemos que estar IOaquí debido al no determinismo de la programación combinado con los efectos secundarios de nuestra acción de mapeo.

Entonces, una vez que leí esta pregunta, pensé que el problema está prácticamente resuelto massiv, pero no tan rápido. Los generadores de números aleatorios, como in mwc-randomy otros en random-fu, no pueden usar el mismo generador en muchos subprocesos. Lo que significa que la única pieza del rompecabezas que me faltaba era: "dibujar una nueva semilla aleatoria para cada hilo generado y proceder como de costumbre". En otras palabras, necesitaba dos cosas:

  • Una función que inicializaría tantos generadores como subprocesos de trabajo
  • y una abstracción que daría sin problemas el generador correcto a la función de mapeo dependiendo del hilo en el que se esté ejecutando la acción.

Así que eso es exactamente lo que hice.

Primero daré ejemplos usando las funciones randomArrayWSy especialmente diseñadas initWorkerStates, ya que son más relevantes para la pregunta y luego pasaré al mapa monádico más general. Aquí están sus firmas de 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)

Para aquellos que no están familiarizados con massivel Compargumento es una estrategia de cálculo para usar, los constructores notables son:

  • Seq - ejecutar el cálculo secuencialmente, sin bifurcar ningún hilo
  • Par - Gire tantos hilos como capacidades haya y utilícelos para hacer el trabajo.

Usaré el mwc-randompaquete como ejemplo inicialmente y luego pasaré a RVarT:

λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)

Arriba inicializamos un generador separado por hilo usando la aleatoriedad del sistema, pero también podríamos haber usado una semilla única por hilo derivándola del WorkerIdargumento, que es un mero Intíndice del trabajador. Y ahora podemos usar esos generadores para crear una matriz con valores aleatorios:

λ> 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 ]
  ]

Al usar la Parestrategia, la schedulerbiblioteca dividirá uniformemente el trabajo de generación entre los trabajadores disponibles y cada trabajador usará su propio generador, lo que lo hará seguro para subprocesos. Nada nos impide reutilizar la misma WorkerStatescantidad arbitraria de veces siempre que no se haga al mismo tiempo, lo que de lo contrario resultaría en una excepción:

λ> 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 ]

Ahora dejando mwc-randomde lado podemos reutilizar el mismo concepto para otros posibles casos de uso usando funciones como 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)

Aquí está el ejemplo prometido sobre cómo utilizar esta funcionalidad con rvar, random-fuy mersenne-random-pure64bibliotecas. También podríamos haber usado randomArrayWSaquí, pero por ejemplo, digamos que ya tenemos una matriz con diferentes RVarTs, en cuyo caso necesitamos 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 ]
  ]

Es importante tener en cuenta que, a pesar de que en el ejemplo anterior se está utilizando la implementación pura de Mersenne Twister, no podemos escapar del IO. Esto se debe a la programación no determinista, lo que significa que nunca sabemos cuál de los trabajadores manejará qué parte de la matriz y, en consecuencia, qué generador se utilizará para qué parte de la matriz. Por el lado positivo, si el generador es puro y divisible, como splitmix, entonces podemos usar la función de generación pura, determinista y paralelizable:, randomArraypero eso ya es una historia separada.

lehins
fuente
En caso de que desee ver algunos puntos de referencia: alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins
4

Probablemente no sea una buena idea hacer esto debido a la naturaleza inherentemente secuencial de los PRNG. En su lugar, es posible que desee realizar la transición de su código de la siguiente manera:

  1. Declare una función IO ( maino lo que sea).
  2. Lea tantos números aleatorios como necesite.
  3. Pase los números (ahora puros) a sus funciones de reparación.
mcandre
fuente
¿Sería posible grabar cada PRNG en cada hilo paralelo para crear independencia estadística?
J. Abrahamson
@ J.Abrahamson sí, sería posible. Mira mi respuesta.
lehins