El producto de matrices
El popular algoritmo de la multiplicación de matrices cuadradas y densas nos servirá para ilustrar una serie de optimizaciones relacionadas con la descomposición del código en bloques de hilos y la ubicación de los datos en la jerarquía de memoria de la GPU.
El código C de partida es el siguiente:
for (i=0; i<N; i++) for (j=0; j<N; j++) { c[i][j] = 0; for (k=0; k<N; k++) c[i][j] += a[i][k] * b[k][j]; }
Conceptos que pondremos en práctica
El producto de matrices suele utilizarse para ilustrar muchas de las posibilidades de los lenguajes de programación paralela y arquitecturas paralelas. Completando fragmentos de código incompleto para el producto de matrices, en este ejercicio se verá:
- Cómo alojar y liberar memoria en el dispositivo.
- Cómo copiar datos desde CPU a GPU.
- Cómo copiar datos desde GPU a CPU.
- Cómo medir tiempos para accesos a memoria y tiempos de ejecución.
- Cómo invocar kernels sobre la GPU.
- Cómo escribir un programa para calcular el producto de dos matrices en GPU.
Cómo acceder a los ficheros fuente
Una vez más, se proporcionan los ficheros del código incompletos, debiendo rellenar el alumno algunas de sus sentencias. Se plantean dos versiones de dificultad creciente, ubicadas en los directorios lab1.1-matrixmul.1 y lab1.1-matrixmul.2. La primera, para alumnos con poca o ninguna experiencia en programación con CUDA, y la segunda para programadores con alguna experiencia. Escoge aquella que consideres más afín a tu nivel de destreza. El código se divide en tres ficheros con la siguiente estructura:
- matrixmul.cu: Contiene el programa main() que aloja la memoria y llama al kernel CUDA.
- matrixmul_kernel.cu: Contiene el kernel CUDA que se lanza desde el programa anterior.
- matrixmul_gold.cpp: Es un programa C que computa secuencialmente el resultado en la CPU. Se utiliza para validar los cómputos realizados en paralelo en GPU.
Implementación
En esta versión inicial del producto de matrices declararemos una matriz de NxN hilos CUDA, en la que el (i,j)-ésimo hilo lee la fila i de la matriz A y la columna j de la matriz B para calcular el dato C[i][j] de la matriz resultante.
ATENCIÓN: Antes de ejecutar cualquier código relacionado con el producto de matrices, debes asegurarte de que el directorio desde el que vayas a lanzar las ejecuciones contiene los ficheros con las matrices de entrada al código. Estos ficheros se llaman matrix_8.bin, matrix_8.gold, matrix_128.bin, matrix_128.gold, matrix_512.bin y matrix_512.gold. Si no los tienes, puedes copiarlos del directorio matrix_data que cuelga del directorio padre donde se encuentran los archivos con tus programas fuente. Algunos alumnos prefieren lanzar las ejecuciones desde el mismo directorio en el que compilan, en cuyo caso al escribir en el shell el nombre del archivo ejecutable, éste debe ir precedido de la ruta donde lo ha generado el Makefile; otros alumnos prefieren cambiar al directorio donde está el ejecutable antes de lanzar la ejecución, en cuyo caso sólo se tiene que escribir en el shell el nombre del archivo precedido de "./" para lanzar las ejecuciones. En el primer caso, los archivos del directorio matrix_data deben colocarse en el directorio donde está el archivo Makefile, y en el segundo, en el directorio donde está el archivo ejecutable.
Modificaciones a realizar sobre el código CUDA
Paso 1:
Edita la función runtTest(. . . ) en matrixmul.cu para completar la funcionalidad del producto de matrices en el host. Sigue los comentarios en el código fuente para llevar a cabo esta tarea.
- Primera parte del código:
- Reserva memoria para matrices de entrada.
- Copia la matriz de entrada desde la memoria RAM hasta la memoria del dispositivo.
- Fíjate en el empleo de un temporizador para medir el tiempo necesario para llevar a cabo estas copias. ¿Podrías calcular el ancho de banda efectivo conseguido?
- Segunda parte del código:
- Configura la ejecución del kernel e invócalo.
- Fíjate en el temporizador que se utiliza para medir el tiempo de computación. Piensa en el carácter asíncrono de las invocaciones de los kernels CUDA para descubrir por qué se usa la función cudaThreadSynchronize().
- Tercera parte del código:
- Copia la matriz resultado desde la memoria del dispositivo hasta memoria principal de la CPU.
- Se utiliza otro temporizador para volver a medir los tiempos de transferencia. ¿Qué ancho de banda efectivo has conseguido esta vez? ¿Es comparable con el ancho de banda calculado anteriormente?
- Cuarta parte del código:
- Libera la memoria del dispositivo.
Paso 2:
Edita la función matrixMul(. . . ) en el fichero matrixmul_kernel.cu para completar la funcionalidad del producto de matrices en el dispositivo. Los comentarios en el código te ayudarán en esta tarea.
- Quinta parte del código:
- Define el índice de salida donde cada hilo debería escribir sus datos.
- Itera sobre los elementos de los vectores (filas y columnas) para llevar a cabo el producto escalar en cada hilo.
- Multiplica y acumula elementos sobre la matriz resultado.
Paso 3:
Compila utilizando el comando make.
Ejecuta el programa para matrices de dimensiones 8x8, 128x128 y 512x512, cuyas matrices de entrada se encuentran en el directorio matrix_data en ficheros identificados por los sufijos 8, 128 y 512, respectivamente. Una vez copies estos ficheros a tu directorio local, sólo tienes que cambiar el parámetro "tamaño" de la línea de ejecución siguiente, sustituyéndolo por 8, 128 o 512, según corresponda):
$> ./matrixmul tamaño
Deberías ver una salida de este tipo:
Input matrix file name:
Setup host side environment and launch kernel:
Allocate host memory for matrices M and N.
M:
N:
Allocate memory for the result on host side.
Initialize the input matrices.
Allocate device memory.
Copy host memory data to device.
Allocate device memory for results.
Setup kernel execution parameters.
# of threads in a block:
# of blocks in a grid :
Executing the kernel...
Copy result from device to host.
Transfer time from CPU to GPU:
GPU computation time :
Transfer time from GPU to CPU:
Total GPU processing time :
Check results with those computed by CPU.
Computing reference solution.
CPU Processing time :
CPU checksum:
GPU checksum:
Comparing file lab1.1-matrixmul.bin with lab1.1-matrixmul.gold ...
Check ok? Passed.
Asegúrate, observando la última línea, de que el test es correcto.
Apunta los datos relativos a tiempo de ejecución y transferencia de datos en una tabla como la que sigue:
Tamaño de la matriz | CPU->GPU | GPU->CPU | Ejecución | Ratio comparado con 128x128 |
8x8 | ||||
128x128 | 1 | |||
512x512 |
¿Qué conclusiones puedes sacar a partir de estos números?