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 Introducción CONTENIDO Diferencias entre CPU y GPU Desarrollo de aplicaciones para GPU Ejemplo: suma de vectores Ejercicios: suma de vectores, suma de matrices Conclusiones 1
INTRODUCCIÓN CPU vs GPU by Mythbusters Cazadores de mitos en el NVISION08 Adam & Jaime contratados por NVIDIA 2
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
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 3 rd party GPGPU y hardware Tegra GeForce/Quadro Tesla Tegra (GPU + ARM) (GPU + ARM) 4
DIFERENCIAS ENTRE CPU Y GPU Rendimiento computacional y ancho de banda GPU vs. СPU 5
Características CPU Intel Core i7 Varios núcleos independientes de alto rendimiento 2,4,6,8 núcleos 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 Core I7-3960x, 6 núcleos, 15MB L3 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
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
Características GPU GeForce 980 (Maxwell GK204) 16 Multiprocesadores 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
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
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
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
DRAM I/F Giga Thread HOST I/F DRAM I/F 03/02/2015 Ecosistema de Desarrollo GPGPU Librerías Aplicaciones Directivas OpenACC Lenguajes de Programación (CUDA) Aceleración Drop-in Aceleración rápida de aplicaciones existentes Máximo Rendimiento 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]; } L2 DRAM I/F DRAM I/F DRAM I/F DRAM I/F... // Perform SAXPY on 1M elements saxpy(1<<20, 2.0, x, y);... 12
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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