Está en la página 1de 29

03/02/2015

Introducción a GPGPU y CUDA


Doctorado en Tecnologías de la Información y las
Telecomunicaciones

ETSI Telecomunicación
Universidad de Valladolid
Mario Martínez-Zarzuela
Febrero de 2015

CONTENIDO
• Introducción
• Diferencias entre CPU y GPU
• Desarrollo de aplicaciones para GPU
• Ejemplo: suma de vectores
• Ejercicios: suma de vectores, suma de
matrices
• Conclusiones

1
03/02/2015

INTRODUCCIÓN

CPU vs GPU by Mythbusters

• Cazadores de mitos en el NVISION08


– Adam & Jaime contratados por NVIDIA

2
03/02/2015

GPGPU & CUDA


• GPU - Graphics Processing Unit
– Unidad de Procesamiento Gráfico – Procesador gráfico
– Coprocesador de la CPU tareas gráficas
• GPGPU - General-Purpose computing on GPU
– GPU de Propósito General
– 2004 Inicios de GPGPU
• Primeros desarrolladores
• Compleja programación gráfica
– API OpenGL
– Shaders Cg
• Brook (Universidad de Standford), Lib Sh

GPGPU & CUDA


• Pipeline gráfica programable
– Vértices
• X,Y,Z,W
– Rasterizador
• Interpolación
– Fragmentos
• R,G,B,A
• Pre-cuda
– Shaders
– Operaciones vectores
de 4 componentes

3
03/02/2015

GPGPU & CUDA


• 2007 NVIDIA CUDA Toolkit
– Supercomputación para las masas
– CUDA - Compute Unified Device Architecture
• Plataforma de cálculo en paralelo y modelo de programación
– Inicialmente extensiones de lenguaje C
• Compilador nvcc
– Librería en tiempo de ejecución
– La primera GPU habilitada por NVIDIA fue la GeForce
G80 (2006)
• Cada vez más herramientas y lenguajes
– Entornos de desarrollo integrados
• Visual Studio Nsight y Eclipse Nsight Edition
– Herramientas de depurado y profiling
– Librerías y plugins
– 3rd party

GPGPU y hardware

Tesla Tegra
GeForce/Quadro (GPU + ARM)
Tegra
(GPU + ARM)

4
03/02/2015

DIFERENCIAS ENTRE CPU Y GPU

Rendimiento computacional
y ancho de banda GPU vs. СPU

5
03/02/2015

Características CPU Intel Core i7


• Varios núcleos independientes de alto Core I7-3960x,
rendimiento
– 2,4,6,8 núcleos
6 núcleos, 15MB L3
– 2,66—3,6GHz cada uno
– Hyper-Threading : cada núcleo es visto
por el SO como dos núcleos lógicos,
que pueden ejecutar dos hilos de
forma simultánea.
• 3 niveles de caché, gran caché L3
– Cada núcleo: L1=32KB (datos)
+ 32KB (instrucciones), L2=256KB
– L3 compartida, hasta 15 MB
• Peticiones a memoria gestionadas de
forma independiente por cada
hilo/proceso

Características GPU GeForce 780 Ti


(Kepler GK110)
15 Multiprocesadores
SMX
• 2880 núcleos en
total
• < 1 GHz
2 niveles de caché
• Cada núcleo:
L1=64KB (datos)
• L2 compartida=1492
KB
Las peticiones a memoria
son realizadas por grupos
de hilos
• Ancho de banda 384-
bit GDDR5
• 3072 MB

6
03/02/2015

GK1xx GPU Kepler Streaming


Multiprocessor (SMX)
• Formados por:
• 192 núcleos CUDA @ ~ 1 GHz cada uno
• Disposición en grupos de 48
• 64 unidades FP
• 32 unidades de Funciones Especiales
• 32 unidades de Carga/Almacenamiento
dedicadas al acceso a memoria
– Las peticiones a memoria son realizadas por
grupos de hilos
• 65536 registros x 32 bit (256KB)
• 64KB memoria compartida/ L1 caché
– Configurable por el usuario
• 48KB caché de solo lectura
• 48KB caché 2D para texturas

Evolución de arquitecturas

7
03/02/2015

Características GPU GeForce 980


(Maxwell GK204)
16 Multiprocesa-
dores SMM
• 2048 núcleos en
total
Mayor cache L2
• 2048 KB frente a:
1492KB (GK110)
512KB (GK104)
Reducción interfaz
con memoria
• 256-bit
Ancho de banda:
• 224 GB/sec

GM204 GPU Maxwell Multiprocessor


(SMM) –Septiembre 2014
• Novedades:
• 2da generación chip Maxwell, tras GM107
• Menos núcleos por SM
• 128 núcleos CUDA @ ~ 1 GHz cada uno
• +40% eficiencia para cada CUDA core
• Planificador mejorado
• Nuevo damino de datos
• Mayor eficiencia energética
• 2x perf/watt vs GK104
• Diseño en cuadrantes de 32 núcleos
• 1 warp scheduler por cuadrante – 2 instrucciones
por ciclo reloj
• Mayor memoria compartida / L1 cache
• 96 KB frente a 64KB (GK110) / 48KB (GK104)
• Máximo 48KB por thread block
• Hasta 32 block threads activos por SM ( frente a
16 en Kepler.

8
03/02/2015

Maxwell – Desmitificando la conspiración


de la llegada del hombre a la luna
• ¿Montaje?
– Buzz Aldrin en zona de
sombra porque el Sol está
detrás del módulo lunar
• ¿Iluminación de estudio en
L.A.?
• ¡Parece que está
iluminado por múltiples
fuentes de luz!
– No se ven estrellas y los
astronautas no recuerdan
haberlas visto
• ¿Se omitieron debido a la
dificultad de crearlas?

Maxwell – Desmitificando la conspiración


de la llegada del hombre a la luna
• Maxwell: renderizado luz tiempo real por primera vez
– Unreal Engine
– Voxelized Global Illumination (VGI)

9
03/02/2015

X1 – NVIDIA drive
Enero de 2015 (CES Las Vegas)
• Potencia superior a la del mayor
supercomputador de hace
15 años
– GPU Maxwell de 256 núcleos
– 8 núcleos de CPU ARM
– Procesamiento de 12 cámaras
60 fps
• NVIDIA DRIVE PX:
– Autonomous drive
– Deep Neural Network
Computer Vision

GPU vs. CPU

• Cientos de núcleos de cálculo simplificados


– Bajas frecuencias ~1 GHz (en lugar de 2-12 en CPU)
• Cachés pequeñas (valores Kepler)
– 192 núcleos comparten la caché L1 (16 - 48 KB)
– L2 compartida entre todos los núcleos, 1.5 MB, no L3
• Cambios de contexto rápidos en GPU
– Sin sobrecarga por cambiar entre hilos (en hardware)
– Soporte para millones de hilos virtuales

10
03/02/2015

Latencia en los accesos a Memoria

• Objetivo: cargar todos los núcleos


– Problema: latencia en los accesos a memoria
• Solución:
– CPU: compleja jerarquía de cachés
– GPU: miles de hilos listos para ser ejecutados
• Ocultar la latencia realizando cálculos

LENGUAJES Y LIBRERÍAS

DESARROLLO DE APLICACIONES

11
03/02/2015

Ecosistema de Desarrollo GPGPU

Aplicaciones
Lenguajes de
Directivas Programación
Librerías
OpenACC (CUDA)

Aceleración Aceleración Máximo


“Drop-in” rápida de Rendimiento
aplicaciones
existentes

Directivas OpenACC

void saxpy(int n, float a, float *x,


float *restrict y){

#pragma acc parallel loop


for (int i = 0; i < n; ++i)
y[i] = a*x[i] + y[i];
}

...
// Perform SAXPY on 1M elements
DRAM I/F
DRAM I/F

saxpy(1<<20, 2.0, x, y);


...
DRAM I/F
HOST I/F

L2
Giga Thread

DRAM I/F
DRAM I/F

DRAM I/F

12
03/02/2015

Librería Thrust
#include <thrust/device_vector.h>
#include <thrust/transform.h>
#include <thrust/functional.h>
#include <iostream>
int main(void) {
thrust::device_vector<float> X(3);
thrust::device_vector<float> Y(3);
thrust::device_vector<float> Z(3);

X[0] = 10; X[1] = 20; X[2] = 30;


Y[0] = 15; Y[1] = 35; Y[2] = 10;

thrust::transform(X.begin(), X.end(), Y.begin(), Z.begin(),\


thrust::plus<float>());

for (size_t i = 0; i < Z.size(); i++)


std::cout << "Z[" << i << "] =" << Z[i] << "\n";
return 0;
}

Cálculos utilizando la GPU

• GPU (device) es controlado por la CPU (host)


– Co-procesador
– GPU es «pasiva», i.e. no puede llamarse a si misma
– Excepción  Paralelismo dinámico (CUDA 5.X)
• Cálculos en paralelo en el device
– Invocados normalmente por el host
– Desde cualquier parte del programa
– Oportunidad para una optimización “incremental” del
código
• Memorias GPU y CPU separadas
– Excepción  Tarjetas integradas
– Opción de Unified Virtual Address

13
03/02/2015

Cálculos utilizando la GPU

Cálculos utilizando la GPU

• Un programa que utiliza GPU está formado por:


– Código para la GPU (código del device), que contiene:
• Instrucciones de cálculo en paralelo un CUDA Kernel
• Accesos a la memoria de la GPU
– Código para la CPU (código del host), maneja:
Gestión de memoria GPU– reserva/liberación
• Intercambio de datos entre GPU y CPU
• Invocar ejecución de CUDA Kernel en GPU
• Interpretación de resultados y otras etapas de procesamiento
en serie

14
03/02/2015

CUDA: Código del device


• CUDA usa C++ con algunas extensiones:
– Atributos para funciones, variables y estructuras
– Funciones integradas (built-in)
• Funciones matemáticas propias de la GPU
• Funciones de sincronización, operaciones colectivas entre hilos
– Vectores de datos de varias dimensiones
– Variables integradas
• threadIdx, blockIdx, blockDim, gridDim
– Plantillas (templates) para trabajar con texturas
– …
• Compilado con un compilador especial: nvcc
– Basado en el compilador de código abierto LLVM
– Puede ampliar los lenguajes de programación para que soporten
dispositivos compatibles con CUDA

CUDA: Código del host

• Existe una sintaxis especial para lanzar los kernels de


CUDA desde el código del host
– En su forma más simple se invocan:

kernel_routine<<<gridDim, blockDim>>>(args,…);

• Código de CPU se compila con un compilador normal


– gcc, icc, llvm
– Excepto invocación del kernel <<< ... >>>
• Funciones enlazadas mediante librerías dinámicas

15
03/02/2015

CUDA Kernel
• Función especial, punto de entrada para el código ejecutado
en GPU
• No devuelve nada (void)
• Se declara con el calificador __global__
• Solo puede accede a la memoria GPU
– Excepción: memoria host mapeada
• Sin variables estáticas
• Declaración de parámetros y mismo uso que para funciones
normales
__global__ void kernel (int * ptr) {
ptr = ptr + 1;
ptr[0] = 100;
….; //other code for GPU
}

• Host lanza «kernels», device los ejecuta

CUDA grid, bloques e hilos

• Invocación de kernel crea una


jerarquía de hilos de
procesamiento
• Bloque: agrupación de hilos. Hilos
y bloques representan diferentes
niveles de paralelismo
• Grid: conjunto de bloques de hilos
invocados
• Warp: hilos planificados en grupos
de 32 hilos
• Grid
– Múltiples bloques del mismo
tamaño
– Habitualmente definido por
nuestro dominio de cálculo

16
03/02/2015

CUDA grid, bloques e hilos


• Hilos dentro de un bloque, y
bloque dentro de un grid,
indexados de forma especial
– Posición de un hilo en un bloque, y
un bloque en el grid, indexada en
tres dimensiones (x,y,z)
– Tamaño de grid/bloque especificado
por el número de bloques/hilos en
cada dimensión
– Si el grid y el bloque tienen
dimension z=1, estamos ante un grid
de hilos bidimensional o lineal (si
además y=1)

CUDA grid, bloques e hilos


• typedef struct{…} dim3;
– Tipo predefinido en CUDA con 3 miembros x, y, z.
• Usada para variables ‘built-in’:
– dim3 threadIdx – índice del hilo en cada bloque
– dim3 blockIdx – índice del bloque en el grid
– dim3 blockDim – tamaño de bloque en cada dimensión
– dim3 gridDim – tamaño de grid en cada dimensión
• Cálculo del índice absoluto de un hilo de ejecución a partir
de índices relativos.
– Para un grid de hilos lineal:
unsigned int tidx =
threadIdx.x + blockDim.x*blockIdx.x;

17
03/02/2015

CUDA Escalabilidad Automática


• Diferentes GPUs con el mismo chip tienen
diferente número de SMs
– GeForce GTX 980
• 2048 cores
• 16xSM GM204
– Maxwell X1
• 256 cores
• 2xSM GM204

EJEMPLO

SUMA DE VECTORES

18
03/02/2015

Suma de vectores en una dimensión

Hilos

Vector A ld ld ld ld ld ld ld ld ld ld

Vector B ld ld ld ld ld ld ld ld ld ld

Vector C st st st st st st st st st st

Código en el device
__global__ void sum_kernel(int *A, int *B, int *C)
{
int tidx =
threadIdx.x + blockIdx.x * blockDim.x; //calcular índice
int elemA = A[tidx]; //leer elemento de A
int elemB = B[tidx]; //leer elemento de B
C[tidx] = elemA + elemB; //calcular y escribir elemento en C
}

• Cada hilo de CUDA:


– Recibe una copia de los parámetros
• En este ejemplo recibe punteros a vectores en GPU
– Determina su posición en el grid con el índice tidx
– Lee los elementos de dos vectores de entrada usando el índice tidx
– Calcula la suma y escribe los resultados en un vector de salida con el
mismo índice tidx

19
03/02/2015

Código en el host
• 1. …
• 2. …
• 3. …
• 4. Especificar el tamaño del grid y de cada
bloque
• 5. Lanzar el kernel
• 6. …

Código en el host
• 4. Especificar el tamaño del grid y de cada bloque
Depende el tamaño del problema
• 5. Lanzar el kernel
– Supondremos vectores de longitud 1024 elementos
– Tamaño de bloque: 256 (múltiplo de tamaño de warp)
– Tamaño de grid = 1024 / 256 = 4 bloques

//d_A, d_B y d_C son punteros a vectores en el device


dim3 blockdim(256,1,1); //Bloques típicos de 128, 256, 512
dim3 griddim(4,1,1);
sum_kernel<<<griddim,blockdim>>>(d_A,d_B,d_C);

20
03/02/2015

Código en el host
• 1. …
• 2. Reservar memoria en la GPU
• 3. Copiar datos de entrada a la GPU
• 4. Especificar el tamaño del grid y de cada
bloque
• 5. Lanzar el kernel
• 6. Copiar los datos de salida de vuelta al host

Reserva de memoria en el device


• cudaError_t cudaMalloc (void** devPtr,
size_t size)
– Reserva size bytes de memoria lineal en la GPU, y devuelve un
puntero a la memoria ubicada en *devPtr. Los elementos de
memoria no son puestos a cero. Las direcciones de memoria están
alineadas a 512 bytes

• cudaError_t cudaFree (void* devPtr)


– Libera la memoria apuntada por devPtr

• En donde cudaError_t es una enumeración para control de


errores

21
03/02/2015

Transferencia de datos
• cudaError_t cudaMemcpy ( void* dst, const
void* src, size_t count, cudaMemcpyKind kind)
– Copia count bytes de la memoria apuntada por src a la memoria apuntada por dst
– kind especifica la dirección de la transferencia
• cudaMemcpyHostToHost– transferencia de datos entre host
• cudaMemcpyHostToDevice – transferencia de datos de host a device
• cudaMemcpyDeviceToHost – transferencia de datos de device a host
• cudaMemcpyDeviceToDevice – transferencia de datos entre devices
– Una llamada a cudaMemcpy() con kind inconsistente con punteros dst y src, puede
provocar resultados impredecibles
• GPU no sabe si los punteros pertenecen a la CPU o la GPU (excepción: UVA)

Comprobación de Errores
• En donde cudaError_t es una enumeración para control de
errores
– En caso de error el código de error recogido por el sistema no está
necesariamente asociado a la última llamada de biblioteca de CUDA
• Funciones útiles para conocer el último error ocurrido
– cudaError_t cudaPeekAtLastError() – devuelve el último error
ocurrido en cualquiera de las llamadas
– cudaError_t cudaGetLastError() – devuelve el último error ocurrido y
resetea el estado de error a cudaSuccess
– const char* cudaGetErrorString (cudaError_t error) – devuelve la
cadena del mensaje del código de error
• La lista de posibles errores puede consultarse en la documentación
de CUDA

22
03/02/2015

Control de errores
• La mayoría de funciones de biblioteca devuelven en
caso de error una variable de tipo enumeración enum
cudaError_t

cudaError_t err = cudaSuccess;


err = /* Llamada a función de CUDA */
if (err != cudaSuccess){
fprintf(stderr, ”Error (error code %s)!\n",
cudaGetErrorString(err));
exit(EXIT_FAILURE);
}

Control de errores
• Una macro muy útil para gestión de errores es la siguiente
– Traduce el código de error devuelto a un mensaje de
texto

#define CUDA_CALL(x) do{ \


cudaError_t err = (x); \
if (err != cudaSuccess) { \
printf ("Error \"%s\"\n", \
cudaGetErrorString(err)); \
exit(-1); \
}} while (0)

23
03/02/2015

Código en el host
#include <stdio.h>
#include <cuda_runtime.h>

#define CUDA_CALL(x) do{ \


//…
}} while (0)

int main(void)
{
int N = 1024;
size_t size = N * sizeof(float);

// Allocate the host input vectors and initialize


float *h_A = (float *)malloc(size);
float *h_B = (float *)malloc(size);
float *h_C = (float *)malloc(size);
//...

float *d_A,*d_B = NULL; //Reservar memoria E/S en el dispositivo


CUDA_CALL(cudaMalloc((void **)&d_A, size));
CUDA_CALL(cudaMalloc((void **)&d_B, size));
float *d_C = NULL;
CUDA_CALL(cudaMalloc((void **)&d_C, size));

Código en el host
//Copiar los vectores de entrada h_A y h_B al dispositivo
CUDA_CALL(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));
CUDA_CALL(cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice));

//Configurar parámetros y lanzar el kernel


int blockdim = 256;
int griddim = 4;
printf("CUDA kernel launch with %d blocks of %d threads\n", griddim, blockdim);
vectorAdd<<<griddim, blockdim>>>(d_A, d_B, d_C, N);
cudaError_t err = cudaGetLastError();
if (err != cudaSuccess)
{
fprintf(stderr, "Failed to launch vectorAdd kernel (error code %s)!\n",
cudaGetErrorString(err));
exit(EXIT_FAILURE);
}

//Copiar resultado al host


CUDA_CALL(cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost));

24
03/02/2015

Código en el host
//Comprobar cálculos realizados
for (int i = 0; i < N; ++i)
{
if (fabs(h_A[i] + h_B[i] - h_C[i]) > 1e-5)
{
fprintf(stderr, "Result verification failed at element %d!\n", i);
exit(EXIT_FAILURE);
}
}
printf("Test PASSED\n");

CUDA_CALL(cudaFree(d_A)); // Liberar memoria del dispositivo


CUDA_CALL(cudaFree(d_B));
CUDA_CALL(cudaFree(d_C));

free(h_A); // Free host memory


free(h_B);
free(h_C);

// Resetear el dispositivo y salir


CUDA_CALL(cudaDeviceReset());

return 0;
}

Compilación rápida con nvcc


CUDA amplía C++ de distintas formas:
– Llamadas a los kernels <<< …. >>>
– Variables integradas threadIdx, blockIdx
– Calificadores __global__ __device__ etc.
– …
• Estas extensiones sólo pueden ser procesadas por archivos *.cu
– No por archivos *.cpp
• Es posible ubicar todo nuestro código en archivos *.cu
– Recomendado para códigos pequeños
– Se evita utilizar *.c ó *.cpp
– Compilación y enlazado utilizando nvcc
nvcc test.cu –o test
– Ejecutar desde línea de comandos
./test

25
03/02/2015

EJERCICIO
¿Y si la longitud de los vectores no es múltiplo de el tamaño de bloque?
Por ejemplo, bloques de tamaño 512 y vectores de longitud 10E6.
Cambia el código del host:
- Para asegurarte que lanzas exactamente los bloques que son necesarios
Cambia el código del device:
- Para asegurarte de que hay hilos en el último bloque que permanecen en
estado idle.

SUMA DE VECTORES

Código en el host
• ¿Qué ocurre si longitud vectores es N, no
necesariamente múltiplo de 256?
– Parámetros de invocación del kernel genéricos
– Necesario restringir cálculo al dominio de cómputo
– Algunos threads deberán estar en estado de reposo
(idle)

//d_A, d_B y d_C son punteros a vectores en el device


dim3 blockdim(256,1,1); //Bloques típicos de 128, 256, 512
dim3 griddim((N-1)/blockdim.x+1,1,1);
sum_kernel<<<griddim,blockdim>>>(d_A,d_B,d_C,N);

26
03/02/2015

Código en el device
• ¿Qué ocurre si longitud vectores es N, no
necesariamente múltiplo de 256?
– Necesario restringir cálculo al dominio de cómputo
– Algunos threads deberán estar en estado de reposo
(idle)
__global__ void sum_kernel(int *A, int *B, int *C, int N)
{
int tidx =
blockIdx.x * blockDim.x + threadIdx.x; //calcular índice
if(tid<N){
int elemA = A[tidx]; //leer elemento de A
int elemB = B[tidx]; //leer elemento de B
C[tidx] = elemA + elemB; //calcular y escribir elemento en C
}
}

EJERCICIO
Extiende el ejemplo anterior a suma de matrices indexando los threads
dentro del bloque y los bloques dentro del grid usando dos dimensiones

SUMA MATRICES

27
03/02/2015

Conclusiones
• Arquitectura GPU muy distinta de CPU
– Arquitecturas masivamente paralelas
– Incremento ámbitos de aplicación de GPUs
• Aumentar rendimiento aplicaciones paralelas
– Directivas OpenACC / OpenMP
– Librerías ‘drop in’
– Programación CUDA
• Curva más elevada de aprendizaje
• Asequible con conocimientos previos C/C++, Fortran,…
• Programa en CUDA
– Código en el device
• CUDA Kernel
– Código en el host
• Invoca el CUDA Kernel

Información adicional
• Página web de
NVIDIA
– www.nvidia.es/cud
a
– Controladores
– SDK
– IDE Nsight
• Visual Studio
• Eclipse
– Tutoriales y
documentación

28
03/02/2015

Libros
• CUDA by Example by Jason Sanders and Edward Kandrot
• Programming Massively Parallel Processors: A Hands-on
Approach by David Kirk, Wen-mei Hwu (2nd ed.)
• The CUDA Handbook by Nicholas Wilt
• CUDA Application Design and Development by Rob Farber
• CUDA Programming: A Developer's Guide to Parallel
Computing with GPUs by Shane Cook

Gracias por su atención


marmar@tel.uva.es

Mario Martínez-Zarzuela
Febrero 2015

29

También podría gustarte