viernes, 7 de febrero de 2014

Funciones __device__


En ejercicios anteriores hemos aprendido como usar la palabra reservada __global__ para marcar una función como código que el host puede llamar y que origina una invocación de un kernel paralelo en la GPGPU.

Con una función __global__, cada thread CUDA sigue su propio patrón de ejecución en forma serial. En CUDA, los kernels consisten en mayormente en código C/C++ que pueden ser muy rápidos.

Como programadores en paralelo, tendremos la necesidad de abstraer y encapsular el kernel en funciones. La palabra reservada __device__ nos permite marcar una función que es llamada desde threads ejecutándose en la GPGPU. Por ejemplo:

__device__ float mi_funcion_device(float x)
{
   return x + 1;
}

Aunque las funciones __device__ son similares a las funciones __global__ en que pueden ser ejecutadas por threads CUDA, de hecho se comportan más como funciones normales en C. A diferencia de funciones __global__, las funciones __device__ no pueden ser configuradas con <<B, T>>, y no están sujetas a ninguna restricción especial en los tipos de sus parámetros o sus resultados. El código del host no puede llamar a las funciones __device__ directamente; si se desea acceder a una función __device__, necesitamos escribir una función __global__ que la llame.

Como se podría esperar, las funciones __device__ pueden llamar a otras funciones __device__:

__device__ float mi_segunda_funcion_device(float y)
{
   return mi_funcion_device(y)/2;
}

Siempre y cuando no se llamen a si mismas:

__device__ int float mi_funcion_device_ilegal_recursiva(int x)
{
   if (x == 0) return 1;
   return x * mi_funcion_device_ilegal_recursiva(x-1);
}

El siguiente código muestra como se podría usar la funciones __device__ para empaquetar varios bits de código cuando se desarrolla un kernel CUDA.

#include <stdlib.h>
#include <stdio.h>

__device__ int get_global_index(void)
{
  return blockIdx.x * blockDim.x + threadIdx.x;
}

__device__ int get_constant(void)
{
  return 7;
}

__global__ void kernel1(int *array)
{
  int index = get_global_index();
  array[index] = get_constant();
}

__global__ void kernel2(int *array)
{
  int index = get_global_index();
  array[index] = get_global_index();
}

int main(void)
{
  int num_elements = 256;
  int num_bytes = num_elements * sizeof(int);

  int *device_array = 0;
  int *host_array = 0;

  // reserva memoria
  host_array = (int*)malloc(num_bytes);
  cudaMalloc((void**)&device_array, num_bytes);

  int block_size = 128;
  int grid_size = num_elements / block_size;

  // lanza kernel1 e inspecciona sus resultados
  kernel1<<<grid_size,block_size>>>(device_array);
  cudaMemcpy(host_array, device_array, num_bytes, cudaMemcpyDeviceToHost);

  printf("resultados de kernel1:\n");
  for(int i = 0; i < num_elements; ++i)
  {
    printf("%d ", host_array[i]);
  }
  printf("\n\n");

  // lanza kernel2 e inspecciona sus resultados
  kernel2<<<grid_size,block_size>>>(device_array);
  cudaMemcpy(host_array, device_array, num_bytes, cudaMemcpyDeviceToHost);

  printf("resultados de kernel2:\n");
  for(int i = 0; i < num_elements; ++i)
  {
    printf("%d ", host_array[i]);
  }
  printf("\n\n");

  // liberar memoria
  free(host_array);
  cudaFree(device_array);
  return 0;
}

jueves, 6 de febrero de 2014

Jugando con dimensiones en grids y bloques

En CUDA es posible tener estructura de 1D, 2D y 3D para bloques y grids de threads, lo cual se explicó en Identificación de un thread. Para aclarar la forma de utilizar estas posibles configuraciones se resume en lo siguiente, usando configuraciones 1D y 2D para bloques y 1D, 2D y 3D para threads:


  1. Array 1D de bloques donde cada bloque tiene un array 1D de threads.

  2. UniqueBlockIndex = blockIdx.x;
    UniqueThreadIndex = blockIdx.x * blockDim.x + threadIdx.x;
       
  3. Array 1D de bloques donde cada bloque tiene un array 2D de threads.

    UniqueBlockIndex = blockIdx.x;
    UniqueThreadIndex = blockIdx.x * blockDim.x * blockDim.y + threadIdx.y * blockDim.x + threadIdx.x;
                          
  4. Array 1D de bloques donde cada bloque tiene un array 3D de threads.

    UniqueBlockIndex = blockIdx.x;
    UniqueThreadIndex = blockIdx.x * blockDim.x * blockDim.y + threadIdx.y * blockDim.x + threadIdx.x;
                          
  5. Array 2D de bloques donde cada bloque tiene un array 1D de threads.

    UniqueBlockIndex = blockIdx.y * gridDim.x + blockIdx.x;
    UniqueThreadIndex = UniqueBlockIndex * blockDim.x + threadIdx.x;
                                                             
  6. Array 2D de bloques donde cada bloque tiene un array 2D de threads.

    UniqueBlockIndex = blockIdx.y * gridDim.x + blockIdx.x;
    UniqueThreadIndex = UniqueBlockIndex * blockDim.y * blockDim.x + threadIdx.y * blockDim.x + threadIdx.x;
                          
  7. Array 2D de bloques donde cada bloque tiene un array 3D de threads.

    UniqueBlockIndex = blockIdx.y * gridDim.z * + blockIdx.x;
    UniqueThreadIndex = UniqueBlockIndex * blockDim.z * blockDim.y * blockDim.x + threadIdx.z * blockDim.y * blockDim.z + threadIdz.y * blockDim.x + threadIdx.x;

viernes, 31 de enero de 2014

Grids Bidimensionales

Esta entrada mostrará un código donde el kernel será lanzado con bloques de 2D, y un grid de threads 2D para realizar operaciones sobre una matriz de datos. Las operaciones ejemplifican la conversión de los índices del thread y del bloque en 1D, esto debido a que el manejo de los datos en la GPGPU es 1D.


#include <stdlib.h>
#include <stdio.h>

__global__ void kernel(int *array) {

// se obtienen los índices x y y para cada thread.
int index_x = blockIdx.x * blockDim.x + threadIdx.x;
int index_y = blockIdx.y * blockDim.y + threadIdx.y;

// el array viene en forma 1D (a pesar de ser matriz), por lo
// que hay que covertir de 2D a un índice 1D

int grid_width = gridDim.x * blockDim.x;
int index = index_y * grid_width + index_x;

// convierte el índice de bloque de 2D en un índice 1D.
int result = blockIdx.y * gridDim.x + blockIdx.x;

// escribe el resultado
array[index] = result;
}
int main(void) {
int num_elements_x = 16;
int num_elements_y = 16;

int num_bytes = num_elements_x * num_elements_y * sizeof(int);

int *device_array = 0;
int *host_array = 0;

// reserva memoria en host
host_array = (int*) malloc(num_bytes);
// reserva memoria en GPU
cudaMalloc((void**) &device_array, num_bytes);

// crea bloques bidimensionales de threads de 4 x 4
dim3 block_size;
block_size.x = 4;
block_size.y = 4;

// configura un grid bidimensional de 4 x 4
dim3 grid_size;
grid_size.x = num_elements_x / block_size.x;
grid_size.y = num_elements_y / block_size.y;

// se envían el arreglo de bloques y el grid de threads por bloque
kernel<<<grid_size, block_size>>>(device_array);

// copia al host
cudaMemcpy(host_array, device_array, num_bytes, cudaMemcpyDeviceToHost);

// impresión
for (int row = 0; row < num_elements_y; ++row) {
for (int col = 0; col < num_elements_x; ++col) {
printf("%2d ", host_array[row * num_elements_x + col]);
}
printf("\n");
}
printf("\n");

// liberar memoria
free(host_array);
cudaFree(device_array);
}


Salida del programa:


 0  0  0  0  1  1  1  1  2  2  2  2  3  3  3  3 
 0  0  0  0  1  1  1  1  2  2  2  2  3  3  3  3 
 0  0  0  0  1  1  1  1  2  2  2  2  3  3  3  3 
 0  0  0  0  1  1  1  1  2  2  2  2  3  3  3  3 
 4  4  4  4  5  5  5  5  6  6  6  6  7  7  7  7 
 4  4  4  4  5  5  5  5  6  6  6  6  7  7  7  7 
 4  4  4  4  5  5  5  5  6  6  6  6  7  7  7  7 
 4  4  4  4  5  5  5  5  6  6  6  6  7  7  7  7 
 8  8  8  8  9  9  9  9 10 10 10 10 11 11 11 11 
 8  8  8  8  9  9  9  9 10 10 10 10 11 11 11 11 
 8  8  8  8  9  9  9  9 10 10 10 10 11 11 11 11 
 8  8  8  8  9  9  9  9 10 10 10 10 11 11 11 11 
12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 
12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 
12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 
12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 

miércoles, 22 de enero de 2014

Suma de vectores II

El principal propósito de trabajar con una GPGPU es el de potenciar el cálculo con cantidades inmensa de datos. Hemos descrito anteriormente un post donde se realizaba la suma de vectores funcionando perfectamente, sin embargo a continuación se muestra un código que funciona para una cantidad de elementos mucho mayor del vector de números:


__global__ void sumaVector(long *v1, long *v2, long *v3, long N) {

int threadId = blockIdx.x * blockDim.x + threadIdx.x;
while (threadId < N) {
v3[threadId] = v1[threadId] + v2[threadId];
threadId += blockDim.x * gridDim.x;
}
}

int main(int argc, char** argv) {
        long N = 9000000; // 9 millones

long *v1, *v2, *v3;
v1 = (long *) malloc(N * sizeof(long));
v2 = (long *) malloc(N * sizeof(long));
v3 = (long *) malloc(N * sizeof(long));

for (long i = 0; i < N; i++) {
// datos de prueba
v1[i] = 10;
v2[i] = 11;

}

printf("%ld", v1[1000001]); //OK

long *dv1, *dv2, *dv3;

cudaMalloc((void**) &dv1, N * sizeof(long));
cudaMalloc((void**) &dv2, N * sizeof(long));
cudaMalloc((void**) &dv3, N * sizeof(long));

// copiando memoria a la GPGPU
cudaMemcpy(dv1, v1, N * sizeof(long), cudaMemcpyHostToDevice);
cudaMemcpy(dv2, v2, N * sizeof(long), cudaMemcpyHostToDevice);

// número de bloques
int B = 1024;
        int T = 1024;

// Llamando a ejecutar el kernel
sumaVector<<<B, T>>>(dv1, dv2, dv3, N);

// copiando el resultado a la memoria Host
cudaMemcpy(v3, dv3, N * sizeof(long), cudaMemcpyDeviceToHost);

cudaFree(dv1);
cudaFree(dv2);
cudaFree(dv3);

printf("%ld", v3[900001]); //OK

        return (EXIT_SUCCESS);
}

Características importantes a tomar en cuenta

  • Este cálculo se realiza en dimensión 1D.
  • Se define la llamada del kernel para 1024 bloques y cada bloque con 1024 threads. Por lo tanto la GPGPU ejecutará 1,048,576 threads. 
  • El vector tiene un tamaño de 9 millones de elementos (se usó valores long para disponer en cualquier caso de números grandes).
  • Debido a que el número de elementos excede el número de threads en el kernel, la codificación de éste incluye un ciclo que indica que se deberá estar realizando hasta que se terminen de calcular las 9 millones de veces.