Buscar este blog

sábado, 28 de enero de 2012

No es “Speed Up” todo lo que reluce

Cuando realizamos optimizaciones en nuestro código, debemos ser conscientes de las limitaciones que impone la arquitectura, en muchos casos lo que puede parecer una mejora acaba siendo un cuello de botella.

 Aunque la técnica de “unroll” o desenrollado de bucles descrita en el post anterior parezca la “Panacea[1]”, hay que ser cautelosos, porque no siempre será así. Es necesario hacer un estudio detallado de cada caso para aplicar factores de desenrollado apropiados a cada uno de los algoritmos que se pretenden computar, ya que un desenrollado de bucles agresivo, puede aumentar demasiado las tareas de planificación (scheduling) y aumentar demasiado el uso de registro en el cuerpo del bucle, que en las CPU puede implicar una migración de los valores de los registros a memoria y esto puede desembocar en una degradación del rendimiento del programa. Por otro lado, si se utilizan factores de desenrollado muy altos, el tamaño del cuerpo del bucle resultante podría desbordar la caché de instrucciones, seguido de pérdidas de caché, y en consecuencia reduciéndose el rendimiento.

En programación GPGPU nos encontramos además con una serie de restricciones que limitan el rendimiento de los programas. Antes de proceder a la implementación deberemos estudiar el impacto que supone el desenrollado de bucles para los programas GPGPU en el contexto de las restricciones de esos recursos y hallar el factor de “unroll” óptimo para ese programa y dispositivo.

ILP y Factor de Ocupación (OF): como ya se ha visto, el nivel de paralelismo por instrucción juega un papel importante a la hora de aumentar el rendimiento, ya que el tamaño de la caché de instrucciones “I-cache” es limitado. Es importante no sobrecargar el cuerpo de la función con demasiadas instrucciones que provoquen un fallo de caché y la consiguiente penalización en el rendimiento.  Por otro lado en las GPU el factor de ocupación puede ser igual o más importante que el anterior a la hora de ocultar las latencias de acceso a memoria y de flujo de instrucciones. Debido a la interrelación que hay entre ILP y OF, se hace muy complicado estimar los valores óptimos. A continuación se muestra un caso concreto que sirve de ejemplo.

En el apartado B de esta sección se expuso brevemente el modelo de programación de CUDA compuesto por Mallas (Grids), Bloques (Blocks) e Hilos (Threads), ahora vamos a compararlo con la arquitectura de las GPUs para comprender mejor el problema del factor de ocupación. Una GPU está compuesta por x SM “stream multiprocessor”, cada uno dispone de c SP “stream processor”, en cada uno hay w Warps[2], que a su vez pueden ejecutar t Threads. La clave para comprender el problema del factor de ocupación reside en la diferencia real que existe entre la capacidad computacional de la arquitectura que utilicemos y la capacidad que solicitamos en la llamada al kernel. La Figura 1 muestra las capacidades para distintos dispositivos con arquitectura CUDA.
Device Name
GeForce GTX 480
Tesla C2050
GeForce GTX 295
Compute capability
2.0
2.0
1.3
Memory Informat.
Total global mem
1.536 MB
2.687 MB
896 MB
Total constant Mem
64 Kb
64 Kb
64 Kb
Max mem pitch
2048 MB
2048 MB
2048 MB
MP Information
Multiprocessor count
15
14
30
Shared mem per mp
48
48
16
Registers per mp
32768
32768
16384
Threads in warp
32
32
32
Warps/MP
48
48
32
Blocks/MP
8
8
8
Max threads/block
1024
1024
512
Max thread dim
(1024,1024, 64)
(1024,1024, 64)
(512, 512, 64)
Max grid dim
(65535, 65535, 65535)
(65535,  65535, 65535)
(65535, 65535, 1)
Warps/block in SP
6
6
4
threads in block
192
192
128
total pararell threads
2880
2688
3840
Figura 1. Capacidades de algunos dispositivos

De todos los valores de la tabla ahora mismo nos interesan: “Multiprocessor count”, que es el número de multiprocesadores que tiene el dispositivo, “Blocks/MP” bloques por multiprocesador, “”Warps/MP” que son agrupaciones de hilos dentro de cada bloque y “Threads in warp” que son el número de hilos que ejecutan en cada warp. Sin tener nada más en cuenta, podemos calcular el número total de hilos que cada dispositivo es capaz de ejecutar de forma concurrente: por ejemplo para el dispositivo GeForce GTX 480 tenemos 15 procesadores x 8 bloques x 6 warps x 48 hilos, en total 34.560 hilos concurrentes ejecutando el mismo kernel. Evidentemente la arquitectura nos está imponiendo además ciertas restricciones: “Max threads per block” está indicando un número (1024) mayor al teórico. Si en un bloque hay 6 warps y 32 hilos (192 hilos en total) ¿cómo es posible alcanzar esa cifra? CUDA puede utilizar hasta 8 bloques por procesador, es decir, que si utilizamos 192 hilos por bloque, dispondremos de 8 bloques por procesador, pero si necesitamos 256 hilos, dispondremos de 6 bloques, para 512 hilos solo 3 bloques, para 1024 solo un bloque por procesador. Cuando se solicitan los recursos en las llamadas a los kernels, CUDA realiza una segmentación y planificación de ejecución de todos los bloques solicitados, ahí es donde se pueden producir holguras si no hemos diseñado bien nuestra llamada, reduciéndose el factor de ocupación.

Otra cuestión a considerar, también muy importante es el uso de los registros que necesita nuestro Kernel, a mayor uso de registros, menor disponibilidad de bloques por procesador. Estas dos variables son las que van a modificar el factor de ocupación que, como hemos podido inferir, es el aprovechamiento físico de la arquitectura para cada tanda de instrucciones. El primero se maneja afinando los parámetros de llamada al núcleo y el segundo, mediante la creación de kernels lo más sencillos que se pueda (sin perder de vista el tiempo de latencia). Podemos saber el uso de registros que hace nuestro kernel añadiendo el parámetro (31) – Xptxas –v en la línea de compilación. 


nvcc -Xptxas –v ejemplo.cu
ptxas info   : Compiling entry function 'acos_main'
ptxas info   : Used 4 registers, 60+56 bytes lmem, 44+40 bytes smem, 
20 bytes cmem[1], 
12 bytes cmem[14]
Figura 2. Obteniendo información de nuestro kernel


[1] De panacea universal: Remedio que buscaban los antiguos alquimistas para curar todas las enfermedades
[2] El término “Warp” puede asimilarse al de “tanda” ya que hace referencia al conjunto de hilos de un procesador que se ejecutan en el mismo instante por pertenecer a la misma programación del scheduler. 

No hay comentarios:

Publicar un comentario