Vamos a considerar las GPUs nuevas (Tesla K20m)
, por lo cual es bueno entender un par de detalles acerca del Hardware:
La RAM de la GPU (Memoria Global) es 4800 MB
, a esta memoria acceden todas las hebras de las funciones que llamemos,
pero el acceso es lento, pues es caro a nivel de hardware, por lo mismolas hebras tienen unas memorias chicas a las cuales
pueden acceder más rápido, las cuales no están dentro de la memoria global; éstas son los Registros y la Memoria compartida.
Los Registros son la memoria que tiene cada hebra, y que ninguna otra hebra puede acceder, es decir es única por cada hebra.
La memoria compartida es una memoria que es como si fuera un registro pero compartido por todas las hebras de un bloque.
Cuando lanzamos una función en la GPU (Kernel) decimos, quiero lanzar X bloques
de Y hebras
,
eso quiere decir que tendremos un total de Y*X hebras
distintas corriendo en paralelo.
La memoria compartida POR BLOQUE es de 49152 bytes
.
Los máximos registros POR BLOQUE es de 65536 bytes
.
Tanto los bloques como las hebras se pueden enviar en 3 dimensiones, pero no es mas que una multiplicación,
por ejemplo, enviar (2,2,2)
bloques y (3,3,3)
hebras serían (2*2*2) * (3*3*3) = 216
hebras en total,
la gracia es que podemos mandar muchos bloques y lo que hace la GPU es que si superan la memoria de la GPU ejecuta unos bloques
primero y cuando se va desocupando la memoria va mandando los otros,
los limites son:
Máximo número de bloques: (2147483647, 65535, 65535)
Máximo número de hebras: (1024, 1024, 64)
Y un par de datos adicionales, en 1 GPU tenemos 13
Multiprocesadores con 192
CUDA cores,
es decir tenemos 2496
CUDA cores, la mayor cantidad de hebras que podemos tener por Multiprocesador es 2048
,
y el máximo de hebras por bloque son 1024
.
Entonces, para interactuar con la GPU debemos considerar, que vamos a enviar información, por lo general arreglos, los cuales quedan en la memoria global, y si queremos escribir ciertos resultados, debemos reservar espacio tambien, entonces:
Cada feature es un flotante, o sea 4 bytes
, son 11 por cada pixel.
La matriz de co_occ es de 256x256 y si la dejamos con unsigned int
sería 1 byte x 256 x 256,
entonces el total es de: 65536 bytes
Las submatrices que queremos enviar a la GPU son cuadradas y de tamaño (borde*2 + 1),
consideremos borde=1, tendríamos submatrices de 3x3, tambien como unsigned int, entonces sería
9 bytes
.
lo cual tambien depende de la cantidad de pixeles que tenga la imagen,
pues vamos a tener 1 sub-matrix por cada pixel, consideremos una imagen de 200x300 pixeles,
tambien como unsigned int, o sea: 60000 bytes
.
Resumiendo:
- Arreglo con las sub-matrices: 6000 bytes (pixeles imagen) x 9 bytes (sub matrices) = 540000 bytes = 527.34375 Kbytes =
0.514 Mbytes
- Matrices de co-ocurrencia: 65536 bytes (tamaño) x 60000 (cantidad de pixeles) = 3932160000 bytes = 3840000.0 Kbyes =
3750.0 Mbytes
- Arreglo de salida con las features: 11x4 bytes (features float) x 60000 (cantidad de pixeles) = 2640000 bytes = 2578.125 Kbytes = 2.51 Mbytes
- Total:
3753 Mbytes = 3.67 Gbytes
Si te das cuenta, no estamos usando toda la memoria de la GPU, pero estamos usando un ejemplo de juguete, una imagen de 200x300 y un borde igual a 1.
Mi idea era la siguiente, cada hebra toma una sub-matriz y calcula la matriz de co-ocurrencia, luego escribe los resultados de los features a un arreglo de salida. ¿Por qué puede salir mal? yo quería generar la matriz de co-ocurrencia como registro de la hebra para que se accediera rápido, pero el máximo de registros por bloque de hebras es 65536 bytes, que es justo el tamaño de una matriz de co-ocurrencia.
Entonces, cual es el acercamiento que quiero probar, dejar TODO en memoria global y ver que tan lento es comparado a la versión Matlab® y C++, entonces despues podemos comenzar a hacer a mejorar la implementación cargando pedacitos de datos en la memoria compartida o registros.