Programación de GPU con C ++

Gpu Programming With C



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 algunos de los cálculos más rápidos disponibles 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 activar una instancia con tecnología de GPU en Amazon Web Services u otro proveedor de 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://developer.nvidia.com/cuda-downloads



Para Ubuntu, elegiría la descarga basada en .deb. El archivo descargado no tendrá una extensión .deb de forma predeterminada, por lo que recomiendo cambiarle el nombre para que tenga una extensión .deb al final. Luego, puede instalar con:





sudo dpkg -Inombre-paquete.deb

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

Una vez que lo haya hecho, actualice sus repositorios:



sudo apt-get update
sudo apt-get installmilagros-y

Una vez hecho esto, 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 no solo para lidiar con una amplia variedad de necesidades de programas, 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 de E / S, lo que los convierte en un gato para todos los oficios.

Las GPU son todo 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 una especialización extrema.

Código de ejemplo

En el código de ejemplo, sumamos vectores. He agregado una versión de CPU y GPU del código para comparar la velocidad.
gpu-example.cpp contenido a continuación:

#include 'cuda_runtime.h'
#incluir
#incluir
#incluir
#incluir
#incluir

typedefhoras::crono::reloj_de_resolución_altaReloj;

#definir ITER 65535

// Versión de CPU de la función de adición de vectores
vacíovector_add_cpu(En t *a,En t *b,En t *c,En tnorte) {
En tI;

// Suma los elementos vectoriales ayb al vector c
por (I= 0;I<norte; ++I) {
c[I] =a[I] +b[I];
}
}

// Versión GPU de la función de adición de vectores
__global__vacíovector_add_gpu(En t *gpu_a,En t *gpu_b,En t *gpu_c,En tnorte) {
En tI=threadIdx.x;
// No se necesita un bucle for porque el tiempo de ejecución de CUDA
// subirá este ITER veces
gpu_c[I] =gpu_a[I] +gpu_b[I];
}

En tprincipal() {

En t *a,*b,*c;
En t *gpu_a,*gpu_b,*gpu_c;

a= (En t *)malloc(ITER* tamaño de(En t));
b= (En t *)malloc(ITER* tamaño de(En t));
c= (En t *)malloc(ITER* tamaño de(En t));

// Necesitamos variables accesibles a la GPU,
// entonces cudaMallocManaged proporciona estos
cudaMallocManaged(&gpu_a, ITER* tamaño de(En t));
cudaMallocManaged(&gpu_b, ITER* tamaño de(En t));
cudaMallocManaged(&gpu_c, ITER* tamaño de(En t));

por (En tI= 0;I<ITER; ++I) {
a[I] =I;
b[I] =I;
c[I] =I;
}

// Llamar a la función de la CPU y cronometrarla
autocpu_start=Reloj::ahora();
vector_add_cpu(a, b, c, ITER);
autocpu_end=Reloj::ahora();
horas::costo << 'vector_add_cpu:'
<<horas::crono::duration_cast<horas::crono::nanosegundos>(cpu_end-cpu_start).contar()
<< 'nanosegundos. orte';

// Llamar a la función GPU y cronometrarla
// Los soportes de triple ángulo son una extensión de tiempo de ejecución CUDA que permite
// parámetros de una llamada al kernel CUDA que se van a pasar.
// En este ejemplo, estamos pasando un bloque de subprocesos con subprocesos ITER.
autogpu_start=Reloj::ahora();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
autogpu_end=Reloj::ahora();
horas::costo << 'vector_add_gpu:'
<<horas::crono::duration_cast<horas::crono::nanosegundos>(gpu_end-gpu_start).contar()
<< 'nanosegundos. orte';

// Liberar las asignaciones de memoria basadas en la función de la GPU
cudaFree(a);
cudaFree(b);
cudaFree(c);

// Liberar las asignaciones de memoria basadas en la función de la CPU
gratis(a);
gratis(b);
gratis(c);

regreso 0;
}

Makefile contenido a continuación:

C ª= -I/usr/local/milagros/incluir
NVCC=/usr/local/milagros/soy/nvcc
NVCC_OPT= -std = c ++11

todos:
$(NVCC)$(NVCC_OPT)gpu-example.cpp-ogpu-ejemplo

limpio:
-rm -Fgpu-ejemplo

Para ejecutar el ejemplo, compílelo:

hacer

Luego ejecuta el programa:

./gpu-ejemplo

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

De lo contrario, es posible que deba ajustar la definición de ITER en gpu-example.cu a un número mayor. Esto se debe a que el tiempo de configuración de la GPU es más largo que el de algunos bucles de CPU más pequeños. Encontré que 65535 funciona bien en mi máquina, pero su millaje puede variar. Sin embargo, una vez que supera este umbral, la GPU es dramáticamente más rápida que la CPU.

Conclusión

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