¿Puedo / debo ejecutar este código en una GPU?


Estoy trabajando en una aplicación estadística que contiene aproximadamente 10 - 30 millones de valores de coma flotante en una matriz.

Varios métodos que realizan cálculos diferentes, pero independientes, en la matriz en bucles anidados, por ejemplo:

Dictionary<float, int> noOfNumbers = new Dictionary<float, int>();

for (float x = 0f; x < 100f; x += 0.0001f) {
    int noOfOccurrences = 0;

    foreach (float y in largeFloatingPointArray) {
        if (x == y) {
            noOfOccurrences++;
        }
    }

    noOfNumbers.Add(x, noOfOccurrences);
}

La aplicación actual está escrita en C#, se ejecuta en una CPU Intel y necesita varias horas para completarse. No tengo conocimiento de conceptos de programación de GPU y API, así que mis preguntas son:

  • ¿Es posible (y hace que sentido) para utilizar una GPU para acelerar tales cálculos?
  • En caso afirmativo: ¿Alguien conoce algún tutorial o tiene algún código de muestra (el lenguaje de programación no importa)?

Cualquier ayuda sería muy apreciada.

Author: dreamcrash, 2012-11-09

5 answers

ACTUALIZACIÓN Versión de GPU

__global__ void hash (float *largeFloatingPointArray,int largeFloatingPointArraySize, int *dictionary, int size, int num_blocks)
{
    int x = (threadIdx.x + blockIdx.x * blockDim.x); // Each thread of each block will
    float y;                                         // compute one (or more) floats
    int noOfOccurrences = 0;
    int a;

    while( x < size )            // While there is work to do each thread will:
    {
        dictionary[x] = 0;       // Initialize the position in each it will work
        noOfOccurrences = 0;    

        for(int j = 0 ;j < largeFloatingPointArraySize; j ++) // Search for floats
        {                                                     // that are equal 
                                                             // to it assign float
           y = largeFloatingPointArray[j];  // Take a candidate from the floats array 
           y *= 10000;                      // e.g if y = 0.0001f;
           a = y + 0.5;                     // a = 1 + 0.5 = 1;
           if (a == x) noOfOccurrences++;    
        }                                      

        dictionary[x] += noOfOccurrences; // Update in the dictionary 
                                          // the number of times that the float appears 

    x += blockDim.x * gridDim.x;  // Update the position here the thread will work
    }
}

Este solo lo probé para entradas más pequeñas, porque estoy probando mi computadora portátil. Sin embargo, funcionó. Sin embargo, es necesario hacer más testículos.

ACTUALIZACIÓN Versión secuencial

Acabo de hacer esta versión ingenua que realiza su algoritmo para 30,000,000 en menos de 20 segundos (ya cuenta la función para generar datos).

Básicamente, ordena tu matriz de flotadores. Viajará sobre el matriz ordenada, analizando el número de veces que un valor aparece consecutivamente en la matriz y luego coloque este valor en un diccionario junto con el número de veces que aparece.

Puede usar mapa ordenado, en lugar del unordered_map que usé.

Aquí está el código:

#include <stdio.h>
#include <stdlib.h>
#include "cuda.h"
#include <algorithm>
#include <string>
#include <iostream>
#include <tr1/unordered_map>


typedef std::tr1::unordered_map<float, int> Mymap;


void generator(float *data, long int size)
{
    float LO = 0.0;
    float HI = 100.0;

    for(long int i = 0; i < size; i++)
        data[i] = LO + (float)rand()/((float)RAND_MAX/(HI-LO));
}

void print_array(float *data, long int size)
{

    for(long int i = 2; i < size; i++)
        printf("%f\n",data[i]);

}

std::tr1::unordered_map<float, int> fill_dict(float *data, int size)
{
    float previous = data[0];
    int count = 1;
    std::tr1::unordered_map<float, int> dict;

    for(long int i = 1; i < size; i++)
    {
        if(previous == data[i])
            count++;
        else
        {
          dict.insert(Mymap::value_type(previous,count));
          previous = data[i];
          count = 1;         
        }

    }
    dict.insert(Mymap::value_type(previous,count)); // add the last member
    return dict;

}

void printMAP(std::tr1::unordered_map<float, int> dict)
{
   for(std::tr1::unordered_map<float, int>::iterator i = dict.begin(); i != dict.end(); i++)
  {
     std::cout << "key(string): " << i->first << ", value(int): " << i->second << std::endl;
   }
}


int main(int argc, char** argv)
{
  int size = 1000000; 
  if(argc > 1) size = atoi(argv[1]);
  printf("Size = %d",size);

  float data[size];
  using namespace __gnu_cxx;

  std::tr1::unordered_map<float, int> dict;

  generator(data,size);

  sort(data, data + size);
  dict = fill_dict(data,size);

  return 0;
}

Si tiene la biblioteca de empuje instalada en su máquina, debe usar esto:

#include <thrust/sort.h>
thrust::sort(data, data + size);

En lugar de esto

sort(data, data + size);

Seguro que será más rápido.

Original Post

"Estoy trabajando en una aplicación estadística que tiene una gran matriz que contiene entre 10 y 30 millones de valores de coma flotante".

"Es posible (y tiene sentido) utilizar una GPU para acelerar los cálculos?"

Sí, lo es. Hace un mes puse una simulación Dinámica Molecular completamente en la GPU. Uno de los núcleos, que calcula la fuerza entre pares de partículas, recibe 6 matrices cada una con 500.000 dobles, un total de 3 Millones de dobles (22 MB).

Así que está planeando poner 30 Millones de puntos flotantes esto es aproximadamente 114 MB de memoria global, por lo que esto no es un problema, incluso mi computadora portátil tiene 250 MB.

El número de cálculo puede ser un problema en su caso? Basándome en mi experiencia con la Dinámica Molecular (MD), digo que no. La versión secuencial de MD tarda aproximadamente 25 horas en completarse, mientras que en la GPU tardó 45 minutos. Usted dijo que su aplicación tomó un par de horas, también basado en su ejemplo de código se ve más suave que el Dinámica Molecular.

Aquí está el ejemplo de cálculo de fuerza:

__global__ void add(double *fx, double *fy, double *fz,
                    double *x, double *y, double *z,...){

     int pos = (threadIdx.x + blockIdx.x * blockDim.x); 

     ...

     while(pos < particles)
     {

      for (i = 0; i < particles; i++)
      {
              if(//inside of the same radius)
                {
                 // calculate force
                } 
       }
     pos += blockDim.x * gridDim.x;  
     }        
  }

Un ejemplo simple de un código en Cuda podría ser la suma de dos matrices 2D:

En c:

for(int i = 0; i < N; i++)
    c[i] = a[i] + b[i]; 

En Cuda:

__global__ add(int *c, int *a, int*b, int N)
{
  int pos = (threadIdx.x + blockIdx.x)
  for(; i < N; pos +=blockDim.x)
      c[pos] = a[pos] + b[pos];
}

En Cuda que básicamente tomó cada iteración y dividir por cada hilo,

1) threadIdx.x + blockIdx.x*blockDim.x;

Cada bloque tiene un Id de 0 a N-1 (N el número máximo de bloques) y cada bloque tiene un número X de hilos con un id de 0 a X-1.

1) Te da el para iteración que cada subproceso calculará en función de su id y el id de bloque en el que se encuentra el subproceso, el blockDim.x es el número de hilos que tiene un bloque.

Así que si tienes 2 bloques cada uno con 10 hilos y un N = 40, el:

Thread 0 Block 0 will execute pos 0
Thread 1 Block 0 will execute pos 1
...
Thread 9 Block 0 will execute pos 9
Thread 0 Block 1 will execute pos 10
....
Thread 9 Block 1 will execute pos 19
Thread 0 Block 0 will execute pos 20
...
Thread 0 Block 1 will execute pos 30
Thread 9 Block 1 will execute pos 39

Mirando su código hice este borrador de lo que podría ser en cuda:

__global__ hash (float *largeFloatingPointArray, int *dictionary)
    // You can turn the dictionary in one array of int
    // here each position will represent the float
    // Since  x = 0f; x < 100f; x += 0.0001f
    // you can associate each x to different position
    // in the dictionary:

    // pos 0 have the same meaning as 0f;
    // pos 1 means float 0.0001f
    // pos 2 means float 0.0002f ect.
    // Then you use the int of each position 
    // to count how many times that "float" had appeared 


   int x = blockIdx.x;  // Each block will take a different x to work
    float y;

while( x < 1000000) // x < 100f (for incremental step of 0.0001f)
{
    int noOfOccurrences = 0;
    float z = converting_int_to_float(x); // This function will convert the x to the
                                          // float like you use (x / 0.0001)

    // each thread of each block
    // will takes the y from the array of largeFloatingPointArray

    for(j = threadIdx.x; j < largeFloatingPointArraySize; j += blockDim.x)
    {
        y = largeFloatingPointArray[j];
        if (z == y)
        {
            noOfOccurrences++;
        }
    }
    if(threadIdx.x == 0) // Thread master will update the values
      atomicAdd(&dictionary[x], noOfOccurrences);
    __syncthreads();
}

Tienes que usar atomicAdd porque diferentes hilos de diferentes bloques pueden escribir / leer noOfOccurrences al mismo tiempo, por lo que tienes que inseguro mutuo exclusion.

Este es solo un enfoque que incluso puede dar las iteraciones del bucle externo a los hilos en lugar de los bloques.

Tutoriales

La serie Dr Dobbs Journal CUDA: Supercomputing for the masses de Rob Farmer es excelente y cubre casi todo en sus catorce entregas. También comienza con bastante suavidad y, por lo tanto, es bastante amigable para principiantes.

Y otros:

Eche un vistazo al último elemento, encontrará muchos enlaces para aprender CUDA.

OpenCL: OpenCL Tutorials / MacResearch

 76
Author: dreamcrash,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-17 22:19:00

No conozco mucho sobre procesamiento paralelo o GPGPU, pero para este ejemplo específico, podría ahorrar mucho tiempo haciendo una sola pasada sobre la matriz de entrada en lugar de repetirla un millón de veces. Con grandes conjuntos de datos, generalmente querrá hacer las cosas en una sola pasada si es posible. Incluso si está haciendo varios cálculos independientes, si está sobre el mismo conjunto de datos, puede obtener una mejor velocidad al hacerlo todo en el mismo pase, ya que obtendrá una mejor localidad de referencia por ahí. Pero puede que no valga la pena por la mayor complejidad de su código.

Además, realmente no desea agregar una pequeña cantidad a un número de coma flotante de forma repetitiva, el error de redondeo se sumará y no obtendrá lo que pretendía. He agregado una sentencia if a mi muestra a continuación para comprobar si las entradas coinciden con su patrón de iteración, pero omítalo si realmente no lo necesita.

No conozco ningún C#, pero una implementación de un solo paso de su muestra lo haría mira algo como esto:

Dictionary<float, int> noOfNumbers = new Dictionary<float, int>();

foreach (float x in largeFloatingPointArray)
{
    if (math.Truncate(x/0.0001f)*0.0001f == x)
    {
        if (noOfNumbers.ContainsKey(x))
            noOfNumbers.Add(x, noOfNumbers[x]+1);
        else
            noOfNumbers.Add(x, 1);
    }
}

Espero que esto ayude.

 11
Author: AlliedEnvy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-09 03:37:54

Es posible (y tiene sentido) utilizar una GPU para acelerar estos cálculos?

  • Definitivamente , este tipo de algoritmo es típicamente el candidato ideal para el procesamiento masivo de paralelismo de datos , en lo que las GPU son tan buenas.

En caso afirmativo: ¿Alguien conoce algún tutorial o tiene algún código de ejemplo (lenguaje de programación no importa)?

  • Cuando quieres ir de la manera GPGPU tienes dos alternativas : CUDA o OpenCL.

    CUDA está maduro con muchas herramientas, pero está centrado en las GPU NVidia.

    OpenCL es un estándar que se ejecuta en GPU NVidia y AMD, y CPU también. Así que deberías favorecerlo.

  • Para el tutorial tienes una excelente serie sobre CodeProject de Rob Farber : http://www.codeproject.com/Articles/Rob-Farber#Articles

  • Para su caso de uso específico hay una gran cantidad de muestras para histogramas construidos con OpenCL (tenga en cuenta que muchos son histogramas de imagen, pero los principios son los mismos).

  • Al usar C# puedes usar enlaces como OpenCL.Net o Cloo.

  • Si su matriz es demasiado grande para almacenarla en la memoria de la GPU, puede particionarla en bloque y volver a ejecutar su núcleo OpenCL para cada parte fácilmente.

 8
Author: Pragmateek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-13 10:20:17

Además de la sugerencia del póster anterior, use el TPL (biblioteca paralela de tareas) cuando sea apropiado para ejecutarse en paralelo en múltiples núcleos.

El ejemplo anterior podría usar Paralelo.Foreach y ConcurrentDictionary, pero una configuración de map-reduce más compleja donde la matriz se divide en trozos cada uno generando un diccionario que luego se reduciría a un solo diccionario le daría mejores resultados.

No se si todos sus cálculos se asignan correctamente a la GPU capacidades, pero tendrá que usar un algoritmo de reducción de mapas de todos modos para asignar los cálculos a los núcleos de la GPU y luego reducir los resultados parciales a un solo resultado, por lo que también podría hacerlo en la CPU antes de pasar a una plataforma menos familiar.

 6
Author: Eli Algranti,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-09 03:49:05

No estoy seguro de si usar GPU sería una buena combinación dado que Los valores de 'largerFloatingPointArray' deben ser recuperados de la memoria. Tengo entendido que las GPU son más adecuadas para cálculos autónomos.

Creo que convertir esta aplicación de proceso único en una aplicación distribuida que se ejecuta en muchos sistemas y ajustar el algoritmo debería acelerar las cosas considerablemente, dependiendo de cuántos sistemas estén disponibles.

Puedes usar el clásico 'divide y vencerás' enfoque. El enfoque general que adoptaría es el siguiente.

Utilice un sistema para preprocesar 'largeFloatingPointArray' en una tabla hash o una base de datos. Esto se haría en una sola pasada. Usaría el valor de coma flotante como la clave, y el número de ocurrencias en la matriz como el valor. En el peor de los casos, cada valor solo ocurre una vez, pero eso es poco probable. Si largeFloatingPointArray sigue cambiando cada vez que se ejecuta la aplicación, entonces la tabla hash en memoria tiene sentido. Si es estática, entonces la tabla podría guardarse en una base de datos clave-valor como Berkeley DB. Llamemos a esto un sistema de' búsqueda'.

En otro sistema, vamos a llamarlo 'principal', crear trozos de trabajo y 'dispersar' los elementos de trabajo a través de N sistemas, y 'recopilar' los resultados a medida que estén disponibles. Por ejemplo, un elemento de trabajo podría ser tan simple como dos números que indican el rango en el que un sistema debe trabajar. Cuando un sistema completa el trabajo, envía una matriz de ocurrencias y está listo para trabajar otro pedazo de trabajo.

El rendimiento se mejora porque no seguimos iterando sobre largeFloatingPointArray. Si el sistema de búsqueda se convierte en un cuello de botella, entonces podría replicarse en tantos sistemas como sea necesario.

Con un número suficiente de sistemas trabajando en paralelo, debería ser posible reducir el tiempo de procesamiento a minutos.

Estoy trabajando en un compilador para programación paralela en C dirigido a sistemas basados en muchos núcleos, a menudo referido como microservidores, que son/o serán construidos usando múltiples módulos 'system-on-a-chip' dentro de un sistema. Los proveedores de módulos ARM incluyen Calxeda, AMD, AMCC, etc. Intel probablemente también tendrá una oferta similar.

Tengo una versión del compilador funcionando, que podría ser usada para tal aplicación. El compilador, basado en prototipos de funciones C, genera código de red C que implementa código de comunicación entre procesos (IPC) en todos los sistemas. Uno de los mecanismos disponibles de la CIP es socket tcp/ip.

Si necesita ayuda para implementar una solución distribuida, estaré encantado de discutirla con usted.

Añadido el 16 de noviembre de 2012.

Pensé un poco más sobre el algoritmo y creo que esto debería hacerlo en una sola pasada. Está escrito en C y debería ser muy rápido comparado con lo que tienes.

/*
 * Convert the X range from 0f to 100f in steps of 0.0001f
 * into a range of integers 0 to 1 + (100 * 10000) to use as an
 * index into an array.
 */

#define X_MAX           (1 + (100 * 10000))

/*
 * Number of floats in largeFloatingPointArray needs to be defined
 * below to be whatever your value is.
 */

#define LARGE_ARRAY_MAX (1000)

main()
{
    int j, y, *noOfOccurances;
    float *largeFloatingPointArray;

    /*
     * Allocate memory for largeFloatingPointArray and populate it.
     */

    largeFloatingPointArray = (float *)malloc(LARGE_ARRAY_MAX * sizeof(float));    
    if (largeFloatingPointArray == 0) {
        printf("out of memory\n");
        exit(1);
    }

    /*
     * Allocate memory to hold noOfOccurances. The index/10000 is the
     * the floating point number.  The contents is the count.
     *
     * E.g. noOfOccurances[12345] = 20, means 1.2345f occurs 20 times
     * in largeFloatingPointArray.
     */

    noOfOccurances = (int *)calloc(X_MAX, sizeof(int));
    if (noOfOccurances == 0) {  
        printf("out of memory\n");
        exit(1);
    }

    for (j = 0; j < LARGE_ARRAY_MAX; j++) {
        y = (int)(largeFloatingPointArray[j] * 10000);
        if (y >= 0 && y <= X_MAX) {
            noOfOccurances[y]++;
        }   
    }
}
 6
Author: Arun Taylor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-16 19:12:11