Programación de GPU con C ++

Programación de GPU con C ++

Descripción general

En esta guía, exploraremos el poder de la programación de GPU con C++. Los desarrolladores pueden esperar un rendimiento increíble con C ++, y acceder a la potencia fenomenal de la GPU con un lenguaje de bajo nivel puede producir parte del cálculo más rápido disponible actualmente.

Requisitos

Si bien cualquier máquina capaz de ejecutar una versión moderna de Linux puede admitir un compilador C ++, necesitará una GPU basada en Nvidia para seguir este ejercicio. Si no tiene una GPU, puede girar una instancia con GPU en Amazon Web Services u otro proveedor de la nube de su elección.

Si elige una máquina física, asegúrese de tener instalados los controladores patentados de NVIDIA. Puede encontrar instrucciones para esto aquí: https: // linuxhint.com/install-nvidia-drivers-linux/

Además del controlador, necesitará el kit de herramientas CUDA. En este ejemplo, usaremos Ubuntu 16.04 LTS, pero hay descargas disponibles para la mayoría de las distribuciones principales en la siguiente URL: https: // desarrollador.nvidia.com/cuda-downloads

Para Ubuntu, elegirías el .Descarga basada en DEB. El archivo descargado no tendrá un .Extensión de DEB de forma predeterminada, por lo que recomiendo renombrarlo para tener un .Deb al final. Luego, puede instalar con:

sudo dpkg -i nombre de paquete.debutante

Es probable que se le solicite que instale una tecla GPG y, de ser así, siga las instrucciones proporcionadas para hacerlo.

Una vez que haya hecho eso, actualice sus repositorios:

actualización de sudo apt-get
sudo apt -get instalación cuda -y

Una vez hecho, recomiendo reiniciar para asegurarse de que todo esté cargado correctamente.

Los beneficios del desarrollo de GPU

Las CPU manejan muchas entradas y salidas diferentes y contienen una gran variedad de funciones para no solo tratar una amplia variedad de necesidades del programa, sino también para administrar diferentes configuraciones de hardware. También manejan la memoria, el almacenamiento en caché, el bus del sistema, la segmentación y la funcionalidad IO, lo que los convierte en un gato de todos los oficios.

Las GPU son lo contrario: contienen muchos procesadores individuales que se centran en funciones matemáticas muy simples. Debido a esto, procesan tareas muchas veces más rápido que las CPU. Al especializarse en funciones escalares (una función que toma una o más entradas pero devuelve solo una salida), logran un rendimiento extremo a costa de especialización extrema.

Código de ejemplo

En el código de ejemplo, agregamos vectores juntos. He agregado una versión de CPU y GPU del código para la comparación de velocidad.
Ejemplo de GPU.CPP Contenido a continuación:

#Include "CUDA_RUNTIME.H "
#incluir
#incluir
#incluir
#incluir
#incluir
typedef std :: crono :: high_resolution_clock reloj;
#Define Iter 65535
// versión de CPU de la función Vector Agregar
void vector_add_cpu (int *a, int *b, int *c, int n)
int i;
// Agregue los elementos vectoriales A y B al Vector C
para (i = 0; i < n; ++i)
c [i] = a [i] + b [i];


// Versión de GPU de la función ADD Vector
__global__ void vector_add_gpu (int *gpu_a, int *gpu_b, int *gpu_c, int n)
int i = threadidx.X;
// no para el bucle necesario porque el tiempo de ejecución de CUDA
// enhebrará estos tiempos de iter
gpu_c [i] = gpu_a [i] + gpu_b [i];

int main ()
int *a, *b, *c;
int *gpu_a, *gpu_b, *gpu_c;
a = (int *) malloc (iter * sizeof (int));
b = (int *) malloc (iter * sizeof (int));
c = (int *) malloc (iter * sizeof (int));
// Necesitamos variables accesibles para la GPU,
// Entonces CudamAlLocmanEded proporciona estos
cudamallocmanaged (& gpu_a, iter * sizeof (int));
cudamallocManed (& gpu_b, iter * sizeof (int));
cudamallocManed (& gpu_c, iter * sizeof (int));
para (int i = 0; i < ITER; ++i)
a [i] = i;
b [i] = i;
c [i] = i;

// llame a la función de la CPU y la hora
auto cpu_start = clock :: ahora ();
vector_add_cpu (a, b, c, iter);
auto cpu_end = clock :: ahora ();
std :: cout << "vector_add_cpu: "
<< std::chrono::duration_cast(CPU_END - CPU_START).contar()
<< " nanoseconds.\n";
// llama a la función de GPU y tiempo
// Los brakets de ángulo triple son una extensión de tiempo de ejecución CUDA que permite
// Parámetros de una llamada de núcleo Cuda para pasar.
// En este ejemplo, estamos pasando un bloque de hilo con hilos de Iter.
auto gpu_start = clock :: ahora ();
vector_add_gpu <<<1, ITER>>> (GPU_A, GPU_B, GPU_C, ITER);
CUDADEVICESYNCRONIZE ();
auto gpu_end = clock :: ahora ();
std :: cout << "vector_add_gpu: "
<< std::chrono::duration_cast(GPU_END - GPU_START).contar()
<< " nanoseconds.\n";
// Libre las asignaciones de memoria basadas en la función GPU
CudaFree (a);
CudaFree (b);
CUDAFREE (C);
// Libre las asignaciones de memoria basadas en la función de CPU
gratis (a);
gratis (b);
gratis (c);
regresar 0;

Makfile Contenido a continuación:

Inc = -i/usr/local/cuda/incluir
Nvcc =/usr/local/cuda/bin/nvcc
Nvcc_opt = -std = c ++ 11
todo:
$ (NVCC) $ (NVCC_OPT) Ejemplo de GPU.CPP -O GPU -Ejemplo
limpio:
-RM -F GPU -Ejemplo

Para ejecutar el ejemplo, compilarlo:

hacer

Luego ejecute el programa:

./Ejemplo de GPU

Como puede ver, la versión de CPU (vector_add_cpu) se ejecuta considerablemente más lenta que la versión GPU (vector_add_gpu).

Si no es así, es posible que deba ajustar el ITER Definir en el ejemplo de GPU.CU a un número más alto. Esto se debe a que el tiempo de configuración de GPU es más largo que algunos bucles más pequeños de CPU intensivo. Encontré 65535 para funcionar bien en mi máquina, pero su kilometraje puede variar. Sin embargo, una vez que elimina este umbral, la GPU es dramáticamente más rápido que la CPU.

Conclusión

Espero que hayas aprendido mucho de nuestra introducción en la programación de GPU con C++. El ejemplo anterior no logra mucho, pero los conceptos demostrados proporcionan un marco que puede usar para incorporar sus ideas para desatar el poder de su GPU.