Tengo una colección de modelos computacionales que podrían describirse como autómatas celulares asíncronos. Estos modelos se parecen al modelo de Ising, pero son un poco más complicados. Parece que tales modelos se beneficiarían de ejecutarse en una GPU en lugar de una CPU. Desafortunadamente, no es muy sencillo paralelizar un modelo así, y no tengo muy claro cómo hacerlo. Soy consciente de que hay literatura sobre el tema, pero todo parece estar dirigido a científicos informáticos expertos que están interesados en los detalles de la complejidad algorítmica, en lugar de alguien como yo que solo quiere una descripción de algo que pueda implementar, y en consecuencia lo encuentro bastante inpenetrable.
Para mayor claridad, no estoy buscando un algoritmo óptimo, sino algo que pueda implementar rápidamente en CUDA que probablemente dé una aceleración significativa sobre la implementación de mi CPU. El tiempo del programador es mucho más un factor limitante que el tiempo de la computadora en este proyecto.
También debería aclarar que un autómata celular asíncrono es algo bastante diferente de uno sincrónico, y las técnicas para paralelizar CA sincrónicas (como la vida de Conway) no pueden adaptarse fácilmente a este problema. La diferencia es que una CA síncrona actualiza cada celda simultáneamente en cada paso de tiempo, mientras que una asíncrona actualiza una región local elegida al azar en cada paso de tiempo como se describe a continuación.
Los modelos que deseo paralelizar se implementan en una red (generalmente hexagonal) que consta de ~ 100000 celdas (aunque me gustaría usar más), y el algoritmo no paralelizado para ejecutarlos se ve así:
Elige un par de celdas vecinas al azar
Calcule una función de "energía" basada en un vecindario local que rodea estas células
Con una probabilidad que depende de (con β un parámetro), intercambie los estados de las dos celdas o no haga nada.
Repita los pasos anteriores indefinidamente.
También hay algunas complicaciones que ver con las condiciones de contorno, pero imagino que no supondrán mucha dificultad para la paralelización.
Vale la pena mencionar que estoy interesado en la dinámica transitoria de estos sistemas en lugar de solo el estado de equilibrio, por lo que necesito algo que tenga una dinámica equivalente a la anterior, en lugar de algo que se acerque a la misma distribución de equilibrio. (Entonces, las variaciones del algoritmo de tablero de ajedrez no son lo que estoy buscando).
La principal dificultad para paralelizar el algoritmo anterior son las colisiones. Debido a que todos los cálculos dependen solo de una región local de la red, es posible que muchos sitios de red se actualicen en paralelo, siempre que sus vecindarios no se superpongan. La pregunta es cómo evitar tales superposiciones. Se me ocurren varias formas, pero no sé cuál es la mejor para implementarla. Estos son los siguientes:
Use la CPU para generar una lista de sitios de cuadrícula aleatorios y verificar las colisiones. Cuando el número de sitios de cuadrícula es igual al número de procesadores de GPU, o si se detecta una colisión, envíe cada conjunto de coordenadas a una unidad de GPU para actualizar el sitio de cuadrícula correspondiente. Esto sería fácil de implementar, pero probablemente no aceleraría mucho, ya que verificar las colisiones en la CPU probablemente no sería mucho más barato que hacer toda la actualización en la CPU.
Divida la red en regiones (una por unidad de GPU) y haga que una unidad de GPU se encargue de seleccionar y actualizar aleatoriamente las celdas de la cuadrícula dentro de su región. Pero hay muchos problemas con esta idea que no sé cómo resolver, el más obvio es qué debería suceder exactamente cuando una unidad elige un vecindario que se superpone al borde de su región.
Aproxime el sistema de la siguiente manera: deje que el tiempo avance en pasos discretos. Divide el enrejado en otroconjunto de regiones en cada paso de tiempo de acuerdo con un esquema predefinido, y haga que cada unidad de GPU seleccione y actualice aleatoriamente un par de celdas de cuadrícula cuyo vecindario no se superponga con el límite de la región. Como los límites cambian cada vez que pasa, esta restricción podría no afectar demasiado la dinámica, siempre que las regiones sean relativamente grandes. Esto parece fácil de implementar y es probable que sea rápido, pero no sé qué tan bien se aproximará a la dinámica, o cuál es el mejor esquema para elegir los límites de la región en cada paso de tiempo. Encontré algunas referencias a "autómatas celulares síncronos en bloque", que pueden o no ser lo mismo que esta idea. (No lo sé porque parece que todas las descripciones del método están en ruso o en fuentes a las que no tengo acceso).
Mis preguntas específicas son las siguientes:
¿Alguno de los algoritmos anteriores es una forma sensata de abordar la paralelización de GPU de un modelo de CA asíncrono?
¿Hay una mejor manera?
¿Existe un código de biblioteca existente para este tipo de problema?
¿Dónde puedo encontrar una descripción clara en inglés del método "bloque sincrónico"?
Progreso
Creo que se me ocurrió una forma de paralelizar una CA asincrónica que podría ser adecuada. El algoritmo que se describe a continuación es para una CA asíncrona normal que actualiza solo una celda a la vez, en lugar de un par de celdas vecinas como la mía. Hay algunos problemas para generalizarlo en mi caso específico, pero creo que tengo una idea de cómo resolverlos. Sin embargo, no estoy seguro de cuánto beneficio de velocidad dará, por las razones que se analizan a continuación.
La idea es reemplazar la CA asíncrona (en adelante ACA) con una CA síncrona estocástica (SCA) que se comporta de manera equivalente. Para hacer esto, primero imaginamos que el ACA es un proceso de Poisson. Es decir, el tiempo transcurre continuamente, y cada celda como una probabilidad constante por unidad de tiempo de realizar su función de actualización, independientemente de las otras celdas.
es un parámetro cuyo valor puede elegirse arbitrariamente).
En cada paso de tiempo lógico, las celdas del SCA se actualizan de la siguiente manera:
Creo que esto garantiza que las celdas se actualizarán en un orden que puede "decodificarse" para que corresponda con el ACA original, evitando colisiones y permitiendo que algunas celdas se actualicen en paralelo. Sin embargo, debido al primer punto anterior, significa que la mayoría de los procesadores de GPU estarán inactivos en cada paso de tiempo del SCA, que es menos que ideal.
Necesito pensar un poco más si se puede mejorar el rendimiento de este algoritmo y cómo extender este algoritmo para tratar el caso en el que varias celdas se actualizan simultáneamente en el ACA. Sin embargo, parece prometedor, así que pensé en describirlo aquí en caso de que alguien (a) conozca algo similar en la literatura, o (b) pueda ofrecer alguna idea sobre estos temas restantes.
fuente
exp()
), por lo que no habría pensado que tiene mucho sentido distribuirlo en múltiples hilos. Creo que es mejor (y más fácil para mí) intentar actualizar varios pares en paralelo, con un par por hilo.Respuestas:
Usaría la primera opción y usaría una ejecución de CA sincrónica antes (usando la GPU), para detectar colisiones, ejecutar un paso de una CA hexagonal cuya regla es el valor de la celda central = Suma (vecinos), esta CA debe tener se deben iniciar siete estados con una celda seleccionada al azar y verificar su estado antes de ejecutar la regla de actualización para cada gpu.
Muestra 1. El valor de una celda vecina se comparte
0 0 0 0 0 0 0
0 0 1 0 0 0
0 0 0 0 0 0 0
0 0 0 1 0 0
0 0 0 0 0 0 0
un paso de una CA cuya regla es la celda central hexagonal = Suma (vecinos)
0 0 1 1 0 0 0
0 1 1 1 0 0
0 0 1 2 1 0 0
0 0 1 1 1 0
0 0 0 1 1 0 0
Muestra 2. El valor de una celda para actualizar se tiene en cuenta como vecino en la otra
0 0 0 0 0 0 0
0 0 1 0 0 0
0 0 0 1 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0 0
Después de la iteración
0 0 1 1 0 0 0
0 1 2 2 0 0
0 0 2 2 1 0 0
0 0 1 1 0 0
0 0 0 0 0 0 0
Muestra 3. No hay relación.
0 0 0 0 0 0
0 0 1 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 1 0 0
0 0 0 0 0 0 0
Después de la iteración
0 1 1 0 0 0
0 1 1 1 0 0 0
0 1 1 0 0 0
0 0 0 1 1 0 0
0 0 1 1 1 0
0 0 0 1 1 0 0
fuente
Siguiendo sus respuestas a mis preguntas en los comentarios anteriores, le sugiero que pruebe un enfoque basado en el bloqueo en el que cada hilo intenta bloquear el vecindario que se actualizará antes de calcular la actualización real.
Puede hacer esto usando las operaciones atómicas provistas en CUDA, y una matriz que
int
contiene los bloqueos para cada celda, por ejemplolock
. Cada hilo hace lo siguiente:Tenga en cuenta que este enfoque probablemente no sea el más óptimo, pero podría proporcionar un punto de partida interesante. Si hay muchas colisiones entre hilos, es decir, uno o más por 32 hilos (como en una colisión por urdimbre), habrá un poco de desvío de ramas. Además, las operaciones atómicas pueden ser un poco lentas, pero dado que solo está haciendo operaciones de comparar e intercambiar, debería escalar correctamente.
La sobrecarga de bloqueo puede parecer intimidante, pero en realidad son solo algunas tareas y ramas, no mucho más.
Tenga en cuenta también que estoy siendo rápido y suelto con notación en los lazos de
i
los vecinos.Anexo: Fui lo suficientemente arrogante como para suponer que simplemente podría retroceder cuando los pares colisionan. Si este no es el caso, puede envolver todo a partir de la segunda línea en un
while
bucle y agregar unbreak
al final de laif
declaración final .Todos los hilos tendrán que esperar hasta que se termine el último, pero si las colisiones son raras, debería poder salirse con la suya.
Anexo 2: ¡ No tenga la tentación de agregar llamadas a
__syncthreads()
ninguna parte de este código, especialmente a la versión en bucle descrita en el anexo anterior! La asincronía es esencial para evitar colisiones repetidas en este último caso.fuente
Soy el desarrollador principal de LibGeoDecomp. Si bien estoy de acuerdo con vanCompute en que podría emular su ACA con una CA, tiene razón en que esto no sería muy eficiente, ya que solo algunas celdas en un paso dado deben actualizarse. De hecho, esta es una aplicación muy interesante, ¡y divertida de jugar!
Le sugiero que combine las soluciones propuestas por jlopez1967 y Pedro: el algoritmo de Pedro captura bien el paralelismo, pero esos bloqueos atómicos son muy lentos. La solución de jlopez1967 es elegante cuando se trata de detectar colisiones, pero verificando todas las
n
celdas, cuando solo un subconjunto más pequeño (de ahora en adelante supondré que hay algún parámetrok
que denota el número de celdas que se actualizarán simultáneamente) está activo, Es claramente prohibitivo.En ausencia de una buena sincronización global en la GPU, debe invocar múltiples núcleos para las diferentes fases. En Kepler de Nvidia puedes mover incluso el bucle principal a la GPU, pero no espero que gane mucho.
Los algoritmos logran un grado de paralelismo (configurable). Supongo que la pregunta interesante es si las colisiones afectarán su distribución aleatoria cuando aumente
k
.fuente
Le sugiero que vea este enlace http://www.wolfram.com/training/courses/hpc021.html aproximadamente 14:15 minutos en el video, por supuesto, capacitación matemática donde implementan un autómata celular usando CUDA , desde allí y puedes modificarlo.
fuente