Hoy vamos a tratar dos cuestiones clave a la hora de abordar una implementación en GPU:
los que estén acostumbrados a implementar algoritmos paralelos en CPU tendrán un acceso algo mas llano a la programación GPGPU, pero hay ciertas diferencias a tener en cuenta: la paralelización en CPU difiere
considerablemente de la paralelización sobre GPU. La diferencia fundamental se
encuentra en el número de procesadores con el que contamos en una y otra
arquitectura, si bien en CPU contamos con pocos procesadores pero muy rápidos,
en GPU disponemos de muchos procesadores algo más lentos.
La consecuencia
directa de esta diferencia implica adoptar un enfoque diferente, orientado a la
unidad mínima de procesamiento independiente, evitando en la medida de lo
posible, la dependencia entre estas unidades, esto lo tenemos que reflejar
tanto en el diseño de las estructuras de datos, como en las tareas que realicemos
sobre estas.
Además en GPU podemos adoptar 2 enfoques diferentes:
A. Enfoque orientado al bloque
Cuando implementamos un
kernel en CUDA, éste puede diseñarse de tal manera que deba existir cierta
comunicación entre los hilos que ejecutan las tareas. De alguna manera estos
hilos colaboran entre sí y pueden pasarse información entre ellos,
estableciendo mecanismos de sincronización para acceder a la memoria local
donde residen las variables que son comunes al proceso. En este enfoque existe
una limitación, la comunicación o colaboración sólo es posible entre hilos que
pertenecen al mismo bloque de ejecución. La ventaja principal es que es
posible realizar una comunicación muy
rápida mediante la memoria local, ya que su tiempo de latencia es mínimo (en
comparación con el acceso a memoria global).
La implementación de
este enfoque requiere entonces de dos fases, una primera en la que todos los
hilos de cada bloque procesan en colaboración obteniendo un resultado por
bloque, y una segunda fase en la que son procesados los resultados de cada
bloque. Esta tarea se puede ejecutar en otro kernel de GPU o bien directamente
en CPU.
B. Enfoque orientado
al hilo
Este enfoque implica
una estructura más plana, en la que para una determinada ejecución existe
independencia total entre los elementos de cómputo y la tarea que se aplica.
Por ejemplo, una tarea que asigne un número aleatorio a cada elemento de un
vector puede ser un caso típico.
Aunque inicialmente puede parecer que sí tienen
dependencia dada la naturaleza pseudo-aleatoria de estos números, es posible
hacer una descomposición de tareas de forma que se pueda dar la independencia
de procesos.
Nuestro algoritmo de
reducción de vectores propuesto en post anteriores, está implementado utilizando
este enfoque. Así, para desligar la dependencia se han dividido las ejecuciones del kernel en niveles, que son ejecutados secuencialmente
pero para cada ejecución todas las operaciones son independientes.
El
enfoque orientado al hilo permite de una manera más sencilla alinear las
lecturas de memoria con cada uno de los hilos que hacen uso de esa memoria,
reduciendo el “lag” que produce la latencia de la memoria sobre todo cuando esa
relación es de uno a uno.
En resumen: dependiendo del tipo de problema una estrategia será más beneficiosa que la otra, aunque en muchos casos lo idóneo será una mezcla de todas.
No hay comentarios:
Publicar un comentario