Tutorial CUDA Univ. de Santiago. 6 y 7 de Agosto, 2013 La suma por reducción Este código realiza la suma de un vector de N elementos mediante un operador binario de reducción, es decir, en log 2 (N) pasos. Suponemos un N suficientemente grande como para que requiera la comunicación intra-bloque e inter-bloque entre los hilos CUDA implicados en la computación. Conceptos que pondremos en práctica Cómo alojar y liberar memoria en el dispositivo. Cómo copiar datos entre los distintos espacios de memoria. Cómo realizar la reducción suma en la GPU. Realizar mediciones del tiempo de ejecución y los conflictos en el acceso a bancos de memoria compartida usando el CUDA profiler. Observar cómo los distintos esquemas de reducción afectan al número de conflictos en accesos a bancos de memoria compartida. Cómo acceder a los ficheros fuente Se encuentran dentro del directorio Codigos/projects del paquete básico del tutorial. En este caso, proponemos dos opciones, una para alumnos con poca o ninguna experiencia en programación con CUDA, y otra para valientes alumnos con más experiencia en programación con CUDA. Nivel de dificultad 1: utilizad los ficheros del subdirectorio lab1.2-reduction.1. Nivel de dificultad 2: utilizad los ficheros del subdirectorio lab1.2-reduction.2. Reducciones El objetivo de este ejercicio es familiarizarse con un tipo de operaciones muy común en computación científica: las reducciones. Una reducción es una combinación de todos los elementos de un vector en un valor único, utilizando para ello algún tipo de operador asociativo. Las implementaciones paralelas aprovechan esta asociatividad para calcular varias operaciones en paralelo, calculando el resultado en O(logN) pasos sin incrementar el número de operaciones realizadas. Un ejemplo de este tipo de operación se muestra en la siguiente figura: cms.ac.uma.es/gpu/index.php/codigos-a-desarrollar/suma-por-reduccion 1/6
Modificaciones a realizar sobre el código CUDA Paso 1 Modi fica la función computeondevice(... )de finida en vector_reduction.cu. Primera parte del código: 1. Reserva memoria en el dispositivo. 2. Copia los datos de entrada desde la memoria del host hasta la memoria del dispositivo. 3. Copia los resultados desde la memoria de dispositivo de vuelta a la memoria del host. Paso 2: En el fi chero vector_reduction_kernel.cu, modifi ca la función para implementar el esquema de reducción que hemos ilustrado en la anterior figura. Tu implementación debería usar memoria compartida para incrementar la eficiencia. Segunda parte del código: 1. Carga datos desde memoria global a memoria compartida. 2. Realiza la reducción sobre los datos en memoria compartida. 3. Almacena de vuelta los datos en memoria compartida. cms.ac.uma.es/gpu/index.php/codigos-a-desarrollar/suma-por-reduccion 2/6
Paso 3: Una vez fi nalizado el código, simplemente utiliza el script de lanzamiento que encontrarás en el directorio de trabajo para compilar y ejecutar el código en la GPU. Esta aplicación tiene dos modos de operación distintos. La única diferencia es la entrada que se le proporciona tanto a las implementaciones en GPU y en CPU del código de reducción. En cualquier caso, la versión sobre GPU siempre es ejecutada primero, seguida de la versión sobre CPU, que sirve como referencia para comprobar el resultado. Si el resultado es correcto, se imprime por pantalla la frase Test Passed. En caso contrario, se imprime la frase Test Failed. Sin argumentos: El programa crea un array de datos aleatorios como entrada a las funciones de reducción. Para ejecutarlo: $>../../bin/linux/release/vector_reduction Con un argumento: El programa utiliza el fi chero de datos asociado como entrada a las dos implementaciones de la reducción. Para ejecutarlo: $>../../bin/linux/release/vector_reduction data.txt Ten en cuenta que, en este caso, los ejercicios no proporcionan información sobre tiempos de ejecución. Si lo deseas, intenta añadir esta información a tus implementaciones. De cualquier modo, extraeremos dicha temporización directamente del CUDA profi ler. La salida del programa correcto debería tener este aspecto: Test PASSED device: host: Input vector: random generated. Paso 4 (opcional, en función del tiempo, pasaremos al Paso 5): Una vez hayas implementado el esquema de reducción mostrado en la anterior figura, compararemos los resultados con otros dos esquemas de reducción. Edita la función reduction(... )en el fi chero vector_reduction_kernel.cu, donde todavía hay dos esquemas vacíos por implementar. Los esquemas corresponden con los de las cms.ac.uma.es/gpu/index.php/codigos-a-desarrollar/suma-por-reduccion 3/6
siguientes figuras: ESQUEMA 2 ESQUEMA 3 En el esquema mostrado en la segunda estrategia de reducción, cada thread es responsable de la suma de dos nodos adyacentes en una distancia potencia de 2 para cada nivel de la operación. En la tercera estrategia, cada thread es responsable de la suma de dos nodos en distancias que van desde n-1 hasta 1. Por ejemplo, en el primer nivel de la operación, el thread número 1 suma los elementos 1 y 8, siendo la distancia entre los dos nodos 8-1 = 7. El thread número 2 suma los elementos 2 y (8-1). La distancia correspondiente es pues 7-2 = 5. cms.ac.uma.es/gpu/index.php/codigos-a-desarrollar/suma-por-reduccion 4/6
Y las distancias para los nodos sumados por los threads número 3 y 4 son 6-3 = 2 y 5-4 = 1, respectivamente. Tened en cuenta el patrón de acceso regular en ambos esquemas. La mayoría de aplicaciones utilizan patrones de este tipo para eliminar divergencias innecesarias o conflictos en accesos a bancos de memoria. Paso 5: Una vez fi nalizado el código, utiliza la siguiente orden para activar el CUDA pro filer. También existe una versión gráfi ca del profi ler, aunque en este caso no la utilizaremos. $> CUDA_PROFILE=1 CUDA_PROFILE_CONFIG=./profile_config \../../bin/linux/release/vector_reduction Por defecto, el fi chero de salida para el CUDA pro filer es cuda_profi le.log. Más abajo verás un ejemplo de ejecución: 1. gputimemuestra los microsegundos que necesita cada kernel CUDA para ser ejecutado. 2. l1_shared_bank_conflict se refi ere al número de threads en un warp ejecutados secuencialmente. En otras palabras, realmente se refi ere al número de conflictos en el acceso a bancos de memoria compartida. En el ejemplo, no hay conflictos. Z9reductionPfies el nombre dado por el runtime CUDA a la función reduction(... ). 3. shared_load y shared_storese refieren al número de accesos en modo lectura y escritura en memoria compartida, respectivamente. Un posible resultado de la ejecución del programa utilizando el profiler de CUDA es el siguiente: # CUDA_PROFILE_LOG_VERSION 2.0 # CUDA_DEVICE 0 GeForce GTX 480 # TIMESTAMPFACTOR fffff7067548c2a0 method,gputime,cputime,occupancy,l1_shared_bank_conflict,shared_load,shared_store method=[ memcpyhtod ] gputime=[ 2.656 ] cputime=[ 5.000 ] method=[ _Z9reductionPfi ] gputime=[ 7.680 ] cputime=[ 24.000 ] occupancy=[ 0.167 ] l1_shared_bank_conflict=[ 209 ] shared_load=[ 33 ] shared_store=[ 36 ] method=[ memcpydtoh ] gputime=[ 1.312 ] cputime=[ 14.000 ] Compara los valores de l1_shared_bank_conflict y gputimepara cada uno de los tres esquemas de cms.ac.uma.es/gpu/index.php/codigos-a-desarrollar/suma-por-reduccion 5/6
reducción implementados. Cuál de ellos es el más rápido? Existen multitud de contadores que podemos consultar para averiguar los puntos débiles de nuestro código. Si tienes curiosidad, puedes encontrar mucha más información en el fichero /opt/cuda_3.1/doc/compute_profiler_3.1.txtde la máquina que estamos utilizando para desarrollo. Esquema l1_shared_bank_conflict gputime 1 2 3 cms.ac.uma.es/gpu/index.php/codigos-a-desarrollar/suma-por-reduccion 6/6