Producto de matrices. Optimizaciones adicionales

Este ejercicio proporciona más flexibilidad para afinar el rendimiento del producto de matrices. El programa permite modificar ciertos parámetros, como el tamaño del bloque de hilos de CUDA, el grado de desenrollado de bucles que puede aplicar el compilador, el uso de la memoria compartida para reducir el número de registros a utilizar (que permite ejecutar más bloques concurrentes en cada multiprocesador cuando el número de registros por hilo es el limitador del paralelismo), y la prebúsqueda de datos en memoria compartida (que permite beneficiarse del gran ancho de banda con la memoria global). Con todo ello podremos observar las variaciones de rendimiento que se van produciendo.

Toma tu tiempo para observar cómo se aplican en el código fuente todas estas variaciones. Los programas proporcionados ya son correctos, y deben compilar sin errores. Además, esta nueva versión del código es más completa, permitiendo ejecutar más tamaños de bloque que en la versión inicial del tiling (por ejemplo, 32x32). Puedes comparar el código fuente de las dos versiones para sacar tus propias conclusiones. Esperamos que tu versión final del ejercicio anterior se parezca mucho a esta nueva versión más completa.

Cómo acceder a los ficheros fuente

Se encuentran en el directorio lab3.1-matrixmul.

Modificaciones a realizar sobre el código CUDA

Paso 1

Edita las definiciones y macros en el fichero matrixmul.h para variar las optimizaciones en el kernel. El código fuente no necesita ser modi ficado en ningún otro punto.

Paso 2:

Compila y ejecuta como has hecho en los ejercicios anteriores. Prueba los códigos para matrices de dimensión 512:

$> ./matrixmul 512

La salida debería ser algo 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...
Optimization parameters:
Block size:
Unrolling factor:
Register spilling:
Data prefetch:
Copy result from device to host.
GPU memory access time:
GPU computation time :
GPU processing time :
Check results with those computed by CPU.
Computing reference solution.
CPU Processing time :
CPU checksum:
GPU checksum:
Comparing file lab3.1-matrixmul.bin with lab3.1-matrixmul.gold ...
Check ok? Passed.

Paso 3:

Ahora vamos a utilizar el pro filer de CUDA para analizar el rendimiento del programa. Ejecuta la siguiente secuencia de órdenes en tu script de lanzamiento:

$> CUDA_PROFILE=1
$> CUDA_PROFILE_CONFIG=./profile_config
$> export CUDA_PROFILE
$> export CUDA_PROFILE_CONFIG

Ahora ejecuta el programa de nuevo:
$> ./matrixmul 512

Verás que se ha creado un nuevo fi chero en el directorio de trabajo, cuyo nombre por defecto es cuda_profi le_0.log. Para ver sus contenidos, puedes utilizar el comando "cat". Un ejemplo podría ser:

$> cat cuda_profile_0.log

# CUDA_PROFILE_LOG_VERSION 2.0

# CUDA_DEVICE 0 GeForce GTX 480
# TIMESTAMPFACTOR fffff6480b4d3788
method,gputime,cputime,regperthread,occupancy,l1_shared_bank_conflict
method=[ memcpyHtoD ] gputime=[ 189.152 ] cputime=[ 356.000 ]
method=[ memcpyHtoD ] gputime=[ 177.856 ] cputime=[ 579.000 ]
method=[ memset32_aligned1D ] gputime=[ 10.752 ] cputime=[ 22.000 ] regperthread=[ 6 ] occupancy=[ 1.000 ] l1_shared_bank_conflict=[ 0 ]
method=[ _Z9matrixMulPfS_S_ii ] gputime=[ 1900.576 ] cputime=[ 5.000 ] regperthread=[ 23 ] occupancy=[ 0.333 ] l1_shared_bank_conflict=[ 0 ]

  • gputime muestra el tiempo, en microsegundos, que ha tardado un kernel CUDA en concreto en ejecutarse (en este caso, matrixMul).
  • regperthread indica, para cada hilo, el número de registros que consume del banco de registros del multiprocesador de la GPU en el que se aloja su bloque de hilos.
  • occupancy (porcentaje de ocupación) muestra el ratio de warps activos con respecto al máximo número de warps soportados en un multiprocesador de la GPU. En las arquitecturas Kepler, por ejemplo, cada multiprocesador SMX admite un máximo de 64 warps, y cuenta con 65536 registros de 32 bits y hasta 48 Kbytes de memoria compartida. El número de warps activos está limitado por el número de registros y memoria compartida necesaria para cada bloque de hilos.
  • l1_shared_bank_conflict indica el número de conflictos que se producen en el acceso a los bancos de memoria compartida. Debido a la estrategia de permutar los índices (x,y) de los hilos en el acceso a la matriz alojada en memoria compartida durante nuestra implementación con tiling, conseguimos evitar este tipo de conflictos.

AVISO: En caso de no obtener información del profiler en el fichero .log, añade una llamada a la función cudaDeviceReset(); antes de que finalice el programa, recompila e inténtalo de nuevo. Esto obliga a que la información del profiler se vuelque a disco desde los búfers internos.

Paso 4:

A continuación obtendremos información sobre el uso de memoria de nuestro programa CUDA. Ejecuta el script fi nd_mem_usage.sh que encontrarás en el directorio de trabajo. Observa las órdenes que se han ejecutado:
$> ./find_mem_usage.sh matrixmul.cu
nvcc --ptxas-options=-v -I. -I../../common/inc -I/usr/local/cuda/include -DUNIX -o
matrixmul.cu.cubin -cubin matrixmul.cu
ptxas info    : Compiling entry function '_Z9matrixMulPfS_S_ii' for 'sm_10'
ptxas info    : Used 10 registers, 544+16 bytes smem, 4 bytes cmem[1]
See matrixmul.cu.cubin for register usage.


El fichero de salida matrixmul.cu.cubin contiene información detallada sobre uso de memoria local, registros, memoria compartida, memoria de constantes, . . . pero se encuentra en un formato ilegible. Gracias a la opción de compilación --ptxas-options=-v podemos pedirle al compilador nvcc que nos suministre, en el penúltimo renglón, la información que necesitamos.

  • Como primer dato (10), el número de registros usado por el compilador para cada hilo CUDA.
  • Como segundo dato (544+16=560), el número de bytes consumidos en memoria compartida.
  • Como tercer dato (4), el número de bytes usados en memoria local del compilador. 

Si no puedes obtener la información con esa opción de compilación, otra forma de hacerlo es utilizando el comando /usr/local/cuda/bin/cudaobjdump -elf matrixmul.cu.cubin -elf matrixmul.cu.cubin para que salga por la línea de comandos una representación más o menos legible de la información contenida en el archivo .cubin (localiza dentro de él una línea con la siguiente sintaxis: BAR=1 REG=<primerdato>     LMEM=<tercerdato> SMEM=<segundodato>).

En caso de que ninguna de estas dos soluciones te funcione, utiliza para LMEM y SMEM los valores que se indicaron entre paréntesis anteriormente como si te los hubiera proporcionado el fichero y poder así completar el ejercicio. Respecto a los registros, hemos cotejado que que el valor regperthread obtenido en el paso 3 es más fiable que el que recogemos aquí, por lo que en caso de encontrar diferencias, usaremos aquél.

Con todo esto, recoge información sobre las ejecuciones en función de los distintos parámetros de optimización, tal y como se muestra en la siguiente tabla:

Tamaño del tile Ocupación Tiempo de GPU (ms.) Memoria local Memoria compartida Registros
8x8          
16x16          

32x32

         

 

Factor de desenrollado Ocupación Tiempo de GPU (ms.) Memoria local Memoria compartida Registros
0          
2          
4          
16          

Paso 5:

Finalmente, recopila información variando los distintos parámetros de optimización, y trata de encontrar la combinación de los mismos que logra el tiempo de ejecución más bajo. En total, serían 48 ejecuciones a medir, ya que es el producto cartesiano de [3 tamaños de tile] x [4 factores de desenrollado] x [2 tipos de spilling (sí/no)] x [2 tipos de prebúsqueda (sí/no)], pero obviamente tu análisis de los primeros resultados deberá conducirte a descartar muchas de estas combinaciones. Cuéntanos cómo llegaste a la combinación óptima y qué combinaciones descartaste, sin tener que medirlas, de la tabla que adjuntamos a continuación.

Tamaño del tile Factor de desenrollado Register spilling Prefetching (prebúsqueda) Ocupación Tiempo de GPU (ms.)
8x8 0 No No    
16x16 0 No No    
32x32 0 No No    
8x8 2 No No    
16x16 2 No No    
32x32 2 No No    
8x8 4 No No    
16x16 4 No No    
32x32 4 No No    
8x8 16 No No    
16x16 16 No No    
32x32 16 No No    
8x8 0 No    
16x16 0 No    
32x32 0 No    
8x8 2 No    
16x16 2 No    
32x32 2 No    
8x8 4 No    
16x16 4 No    
32x32 4 No    
8x8 16 No    
16x16 16 No    
32x32 16 No    
8x8 0 No    
16x16 0 No    
32x32 0 No    
8x8 2 No    
16x16 2 No    
32x32 2 No    
8x8 4 No    
16x16 4 No    
32x32 4 No    
8x8 16 No    
16x16 16 No    
32x32 16 No    
8x8 0    
16x16 0    
32x32 0    
8x8 2    
16x16 2    
32x32 2    
8x8 4    
16x16 4    
32x32 4    
8x8 16    
16x16 16    
32x32 16