Introducción a la computación de altas prestaciones
Índice
- Introducción
- Objetivos
- 1.Motivaciones de la computación de altas prestaciones
- 2.Paralelismo y arquitecturas paralelas
- 3.Programación de aplicaciones paralelas
- 4.Rendimiento de aplicaciones paralelas
- 5.Retos de la computación de altas prestaciones
- 5.1.Concurrencia extrema
- 5.2.Energía
- 5.3.Tolerancia a fallos
- 5.4.Heterogeneidad
- 5.5.Entrada/salida y memoria
- Bibliografía
Introducción
Objetivos
-
Entender las motivaciones de la computación de altas prestaciones y del paralelismo.
-
Conocer los fundamentos del paralelismo y las arquitecturas paralelas, tanto los relacionados con sistemas de memoria compartida como de memoria distribuida.
-
Conocer las características de los sistemas paralelos, como por ejemplo, la jerarquía de memoria, redes de interconexión, etc.
-
Entender las diferencias entre sistemas paralelos y distribuidos.
-
Conocer los modelos de programación para sistemas de altas prestaciones, tanto de memoria compartida como de memoria distribuida.
-
Conocer los fundamentos relacionados con el rendimiento de sistemas de altas prestaciones y del análisis de rendimiento.
-
Estar al corriente de los retos actuales de la computación de altas prestaciones.
1.Motivaciones de la computación de altas prestaciones
1.1.Utilidad de la simulación numérica
-
El problema es muy difícil, como por ejemplo, construir túneles de viento complejos.
-
El problema es muy caro económicamente, como por ejemplo, fabricar un avión solo para experimentar.
-
El problema es demasiado lento, como por ejemplo, esperar el efecto del cambio climático o la evolución de galaxias.
-
El problema es muy peligroso, como por ejemplo, pruebas de armas, diseño de fármacos, experimentación con el medio ambiente, etc.
-
Aplicaciones científicas, como por ejemplo, el cambio climático, los modelos astrofísicos, el análisis genómico, el plegamiento de proteínas (diseño de fármacos), etc.
-
Aplicaciones de ingeniería, como por ejemplo, la simulación de tests de choque, el diseño de semiconductores, los modelos estructurales de terremotos, etc.
-
Aplicaciones de negocio, como por ejemplo, los modelos financieros y económicos, el proceso de transacciones, los servicios web, los motores de busca, etc.
-
Aplicaciones de defensa y militares, como por ejemplo, las simulaciones de pruebas con armas nucleares, la criptografía, etc.
1.2.Tipos básicos de aplicaciones orientadas a altas prestaciones
1.2.1.Aplicaciones HTC
1.2.2.Aplicaciones HPC
Nombre |
Flops |
---|---|
Megaflops |
106 |
Gigaflops |
109 |
Teraflops |
1012 |
Petaflops |
1015 |
Exaflops |
1018 |
Zettaflops |
1021 |
Yottaflops |
1024 |
-
Resolver problemas en un tiempo de ejecución inferior, utilizando más procesadores.
-
Resolver problemas con más precisión, utilizando más memoria.
-
Resolver problemas más reales, utilizando modelos matemáticos más complejos.
2.Paralelismo y arquitecturas paralelas
2.1.Necesidad de la computación paralela
Procesador |
Año |
Número de transistores |
Tecnología |
---|---|---|---|
4004 |
1971 |
2.250 |
10 μm |
8008 |
1972 |
3.500 |
10 μm |
8080 |
1974 |
6.000 |
6 μm |
8086 |
1978 |
29.000 |
3 μm |
286 |
1982 |
134.000 |
1,5 μm |
386 |
1985 |
275.000 |
1 μm |
486 DX |
1989 |
1.200.000 |
0,8 μm |
Pentium |
1993 |
3.100.000 |
0,8 μm |
Pentium II |
1997 |
7.500.000 |
0,35 μm |
Pentium III |
1999 |
28.000.000 |
180 nm |
Pentium IV |
2002 |
55.000.000 |
130 nm |
Core 2 Duo |
2006 |
291.000.000 |
65 nm |
Core i7 (Quad) |
2008 |
731.000.000 |
45 nm |
Core i7 (Sandy Bridge) |
2011 |
2.300.000.000 |
32 nm |
2.2.Arquitecturas de computación paralela
2.2.1.Una instrucción, múltiples datos (SIMD)
Procesadores vectoriales
-
Registros vectoriales: son registros que son capaces de guardar vectores de operandos y operar simultáneamente sobre sus contenidos. El tamaño del vector lo fija el sistema y puede oscilar desde 4 hasta, por ejemplo, 128 elementos de 64 bits.
-
Unidades funcional vectorizadas: hay que tener en cuenta que la misma operación se aplica a cada elemento del vector o, en operaciones como por ejemplo la suma, la misma operación se aplica a cada par de elementos de los dos vectores de una sola vez. Por lo tanto, las operaciones son SIMD.
-
Instrucciones sobre vectores: son instrucciones que operan sobre vectores en vez de sobre escalares. Esto provoca que para realizar las operaciones, en lugar de hacerlo individualmente para cada elemento del vector (por ejemplo, load, add y store para incrementar el valor de cada elemento del vector) se pueda hacer por bloques.
-
Acceso a memoria por intervalos: con este tipo de acceso el programa accede a elementos de un vector localizado a intervalos. Por ejemplo, accediendo al segundo elemento, al sexto, al décimo, etc. sería acceso a memoria en un intervalo de 4.
Procesadores gráficos (GPU)
2.2.2.Múltiples instrucciones, múltiples datos (MIMD)
Sistemas de memoria compartida
Sistemas de memoria distribuida
2.2.3.Coherencia de memoria caché
Tiempo |
Núcleo 0 |
Núcleo 1 |
---|---|---|
0 |
y0 = x; |
y1 = 3 * x; |
1 |
x = 7; |
Código que no involucra x |
2 |
Código que no involucra x |
z1 = 4 * x; |
Técnica de coherencia de memoria caché snooping
Técnica de coherencia de memoria basada en directorio
Falsa compartición
int i, j, m, n; double y[m]; /* Asignar y = 0 */ ... for(i=0; i<m; i++) for(j=0; j<n; j++) y[i] += f(i,j);
/* Variables privadas*/ int i, j, num_iter; /* Variables privadas inicializadas por un núcleo */ int m, n, num_cores; double y[m]; num_iter = m/num_cores; /* El núcleo 0 hace lo siguiente */ for(i=0; i<num_iter; i++) for(j=0; j<n; j++) y[i] += f(i,j); /* El núcleo 1 hace lo siguiente */ for(i=num_iter+1; i<2*num_iter; i++) for(j=0; j<n; j++) y[i] += f(i,j) ... double y[m]; /* Assignar y = 0 */ ...
2.3.Sistemas distribuidos
3.Programación de aplicaciones paralelas
3.1.Modelos de programación de memoria compartida
3.1.1.Programación con flujos
#include <stdio.h> #include <stdlib.h> #include <pthread.h> void *print_message_function( void *ptr ); main() { pthread_t thread1, thread2; char *message1 = "Thread 1"; char *message2 = "Thread 2"; int iret1, iret2; /* Crea flujos independientes, que ejecutarán la función */ iret1 = pthread_create( &thread1, NULL, print_message_function, (void*) message1); iret2 = pthread_create( &thread2, NULL, print_message_function, (void*) message2); /* Espera a que todos los flujos acaben antes de continuar con el programa principal */ pthread_join( thread1, NULL); pthread_join( thread2, NULL); printf("Thread 1 returns: %d\n",iret1); printf("Thread 2 returns: %d\n",iret2); exit(0); } void *print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s \n", message); }
3.1.2.OpenMP
#include <stdio.h> #include <stdlib.h> #include <omp.h> void Hello(void); int main(int argc, char* argv[]) { /* Obtener el número de flujos del intérprete de órdenes */ int thread_count = strtol(argv[1], NULL, 10); # pragma omp parallel num_threads(thread_count) Hello(); return 0; } void Hello(void( { int my_rank = omp_get_thread_num(); int thread_count = omp_get_num_threads(); printf("Hello from thread %d of %d\n", my_rank, thread_count); }
3.1.3.CUDA y OpenCL
-
Reservar memoria en el dispositivo.
-
Transferir los datos necesarios desde el procesador principal al espacio de memoria asignado al dispositivo.
-
Invocar la ejecución del kernel en cuestión.
-
Transferir los datos con los resultados desde el dispositivo hacia el procesador principal.
-
Liberar la memoria del dispositivo (si ya no es necesaria), una vez finalizada la ejecución del kernel.
__global__ matrix_add_gpu (fload *A, float *B, float *C, int N) { int i = blockIdx.x * blockDim.x + threadIdx.x; int j = blockIdx.y * blockDim.y + threadIdx.y; int index = i + j*N; if (i<N && j<N){ C[index] = A[index] + B[index]; } } int main(){ dim3 dimBlock(blocksize, blocksize); dim3 dimGrid(N/dimBlock.x, N/dimBlock.y); matrix_add_gpu<<<dimGrid, dimBlock>>>(a, b, c, N); }
__kernel void matrix_add_opencl ( __global const float *A, __global const float *B, __global float *C, int N) { int i = get_global_id(0); int j = get_global_id(1); int index = i + j*N; if (i<N && j<N){ C[index] = A[index] + B[index]; } }
3.2.Modelos de programación de memoria distribuida
3.2.1.MPI
-
Estandarización. Las implementaciones de la especificación han hecho que se convirtiera en el estándar de paso de mensajes y que no sea necesario desarrollar programas diferentes para máquinas diferentes.
-
Portabilidad. Los programas MPI funcionan sobre multiprocesadores de memoria compartida, multicomputadores de memoria distribuida, clústeres de computadores, sistemas heterogéneos, etc. siempre que haya una versión de MPI para ellos.
-
Altas prestaciones. Los fabricantes han desarrollado implementaciones eficientes para sus equipos.
-
Amplia funcionalidad. MPI incluye gran cantidad de funciones para llevar a cabo de manera sencilla las operaciones que suelen aparecer en programas de paso de mensajes.
#include <stdio.h> #include <mpi.h> int main ( int argc, char *argv[]) { int rank, size; MPI_Init (&argc, &argv); /* starts MPI */ MPI_Comm_rank (MPI_COMM_WORLD, &rank); /* Obtiene el identificador del proceso */ MPI_Comm_size (MPI_COMM_WORLD, &size); /* Obtiene el número de procesos */ printf( "Hello world from process %d of %d\n", rank, size ); MPI_Finalize(); return 0; }
-
Hay que incluir la librería de MPI (mpi.h).
-
Todos los procesos ejecutan el mismo código desde el principio, con lo que todos tienen variables myrank y size. A diferencia de OpenMP, estas variables son diferentes y pueden estar en memorias diferentes en procesadores distintos.
-
Los procesos trabajan de manera independiente hasta que se inicializa MPI con la función MPI_Init. A partir de este punto, los procesos pueden colaborar intercambiando datos, sincronizándose, etc.
-
La función MPI_Finalize se llama cuando ya no es necesario que los procesos colaboren entre sí. Esta función libera todos los recursos reservados por MPI.
-
Las funciones MPI tienen la forma MPI_Nombre(parámetros). De la misma manera que en OpenMP, es necesario que los procesos sepan su identificador (entre 0 y el número de procesos menos 1) y el número de procesos que se han puesto en marcha. Las funciones MPI_Comm_rank y MPI_Comm_size se utilizan para conseguir esto.
-
Vemos que estas funciones tienen un parámetro MPI_COMM_WORLD que es una constante MPI y que identifica el comunicador al que están asociados todos los procesos. Un comunicador es un identificador de un grupo de procesos, y las funciones MPI han de identificar en qué comunicador se están realizando las operaciones que se llaman.
3.2.2.PGAS
3.3.Modelos de programación híbridos
#include <stdio.h> #include "mpi.h" #include <omp.h> int main(int argc, char *argv[]) { int numprocs, rank, namelen; char processor_name[MPI_MAX_PROCESSOR_NAME]; int iam = 0, np = 1; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &numprocs); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Get_processor_name(processor_name, &namelen); #pragma omp parallel default(shared) private(iam, np) { np = omp_get_num_threads(); iam = omp_get_thread_num(); printf("Hello from thread %d out of %d from process %d out of %d on %s\n", iam, np, rank, numprocs, processor_name); } MPI_Finalize(); }
-
Grano de paralelismo. OpenMP es más apropiado para paralelismo de grano más fino, es decir, cuando hay poca computación entre sincronizaciones entre procesos.
-
Arquitectura de ejecución. OpenMP funciona bien en sistemas de memoria compartida, pero en sistemas distribuidos es preferible MPI.
-
Dificultad de programación. En general, los programas OpenMP son más parecidos a los secuenciales que los MPI y es más fácil desarrollar programas OpenMP.
-
Herramientas de depuración. Se dispone de más herramientas de desarrollo, depuración, autoparalelización, etc. para memoria compartida que para paso de mensajes.
-
Creación de procesos. En OpenMP los flujos se crean dinámicamente, mientras que en MPI los procesos se crean estáticamente, a pesar de que algunas versiones de MPI permiten la gestión dinámica.
-
Uso de la memoria. El acceso a la memoria es más sencillo con OpenMP por el hecho de utilizar memoria compartida. Aun así la localización de los datos en la memoria y el acceso simultáneo puede reducir el rendimiento. La utilización de memorias locales con MPI puede producir en accesos más rápidos.
-
Puede ayudar a mejorar la escalabilidad y a mejorar el balanceo de las tareas.
-
Se puede utilizar en aplicaciones que combinan paralelismo de grano fino y grueso o que mezclan paralelismo funcional y de datos.
-
Puede utilizarse para reducir el coste de desarrollo, por ejemplo, si se tienen funciones que utilizan OpenMP y se utilizan dentro de programas MPI.
4.Rendimiento de aplicaciones paralelas
4.1.Speedup y eficiencia
p |
1 |
2 |
4 |
8 |
16 |
---|---|---|---|---|---|
S |
1,0 |
1,9 |
3,6 |
6,5 |
10,8 |
E = S/p |
1,0 |
0,95 |
0,90 |
0,81 |
0,68 |
4.2.Ley de Amdahl
4.3.Escalabilidad
4.4.Análisis de rendimiento de aplicaciones paralelas
-
La fase de instrumentación y monitorización consiste en añadir a la aplicación una cierta información de instrumentación que permite recopilar información respecto al comportamiento de la aplicación (por ejemplo, número de fallos en la memoria caché de nivel 2).
-
La fase de análisis consiste en inspeccionar la información recopilada en la fase anterior para encontrar problemas de rendimiento, deducir las posibles causas y determinar soluciones.
-
La fase de refinamiento consiste en aplicar los cambios oportunos al código de la aplicación para poder corregir los posibles problemas de rendimiento identificados.
4.4.1.Medidas de tiempos
Double start, finish; ... start = get_current_time(); /* Código que queremos medir */ ... finish = get_current_time(); printf("Tiempo transcurrido = %e segundos\n", finish-start);
4.4.2.Interfaces de instrumentación y monitorización
#include "papi.h" #define NUM_EVENTS 2 int Events[NUM_EVENTS] = {PAPI_FP_OPS, PAPI_TOT_CYC}; INT EventSet; Long long valías[NUM_EVENTS]; /* Inicialización de la librería */ retval = PAPI_library_init (PAPI_VER_CURRENT); /* Alocatar espacio para el nuevo elemento */ retval = PAPI_create_eventSet (&EventSet); /*Añade Flops y otros ciclos que hay que monitorizar */ retval = PAPI_add_eventset (&EventSet, Events, NUM_EVENT); /* Empiezan a funcionar los contadores */ retval = PAPI_start(EventSet); hacer_trabajo(); /* Lo que suponemos que monitorizamos */ /* Para los contadores y restaura los valores por defecto */ retval = PAPI_stop(EventSet, valías);
4.4.3.Herramientas de análisis de rendimiento
-
Basadas en tiempo de ejecución, mediante la cual se detecta qué parte de la aplicación paralela se ejecuta más tiempo.
-
Basadas en contadores que indican el número de veces de un determinado acontecimiento en la aplicación (por ejemplo, el caso de PAPI).
-
Basadas en muestreo, que generan medidas periódicamente sobre el estado de la aplicación.
-
Basadas en trazas de acontecimientos, que proporcionan información asociada a acontecimientos concretos definidos en la aplicación.
4.5.Análisis de rendimiento de sistemas de altas prestaciones
4.5.1.Pruebas de rendimiento
-
HPL: es el high performance Linpack que hemos comentado anteriormente.
-
DGEMM: está centrado en la memoria y mide el rendimiento en coma flotante de la ejecución de la multiplicación de matrices de números reales de doble precisión.
-
STREAM: es un benchmark basado en un programa sintético que mide el ancho de banda a memoria sostenido (en GB/s).
-
PTRANS (39) : está centrado en las comunicaciones entre pares de procesadores que se comunican entre ellos simultáneamente. Este benchmark está orientado a la evaluación de la capacidad de la red.
-
Acceso aleatorio: está centrado en medir el rendimiento de accesos aleatorios a memoria (también denominado GUPS). Este benchmark está orientado a ver cuál es la latencia que el subsistema de memoria tiene por lecturas. En este caso, los accesos a memoria se generan de tal manera que no hay nunca reúso en las memorias caché y siempre se accede a la memoria final.
-
FFT: es un benchmark que mide el rendimiento en punto flotante de la ejecución de una transformada discreta de Fourier compleja unidimensional de doble precisión.
-
Ancho de banda y latencia de comunicaciones: es un conjunto de pruebas que miden la latencia y el ancho de banda de varios patrones de comunicaciones simultáneamente. Está basado en el benchmark b_eff (40) .
4.5.2.Lista Top500
# |
Institución |
Computador/Año Fabricante |
Núcleos |
Rmax/ Rpeak |
Potencia |
---|---|---|---|---|---|
1 |
DOE/NNSA/LLNL Estados Unidos |
Sequoia - BlueGene/Q, Power BQC 16C 1.60 GHz, Custom / 2011 IBM |
1572864 |
16324/ 20132 |
7890 |
2 |
RIKEN Advanced Institute for Computational Science (AICS) Japó |
K computer, SPARC64 VIIIfx 2.0GHz, Tofu interconnect / 2011 Fujitsu |
705024 |
10510/ 11280 |
12659 |
3 |
DOE/SC/Argonne National Laboratory Estados Unidos |
Mira - BlueGene/Q, Power BQC 16C 1.60GHz, Custom / 2012 IBM |
786432 |
8162/ 10066 |
3945 |
4 |
Leibniz Rechenzentrum Alemania |
SuperMUC - iDataPlex DX360M4, Xeon E5-2680 8C 2.70GHz, Infiniband FDR / 2012 IBM |
147456 |
2897/ 3185 |
3422 |
5 |
National Supercomputing Center in Tianjin China |
Tianhe-1A - NUDT YH MPP, Xeon X5670 6C 2.93 GHz, NVIDIA 2050 / 2010 NUDT |
186368 |
2566/ 4701 |
4040 |
6 |
DOE/SC/Oak Ridge National Laboratory Estados Unidos |
Jaguar - Cray XK6, Opteron 6274 16C 2.200GHz, Cray Gemini interconnect, NVIDIA 2090 / 2009 Cray Inc. |
298592 |
1941/ 2627 |
5142 |
7 |
CINECA Italia |
Fermi - BlueGene/Q, Power BQC 16C 1.60GHz, Custom / 2012 IBM |
163840 |
1725/ 2097 |
821 |
8 |
Forschungszentrum Juelich (FZJ) Alemania |
JuQUEEN - BlueGene/Q, Power BQC 16C 1.60GHz, Custom / 2012 IBM |
131072 |
1380/ 1677 |
657 |
9 |
CEA/TGCC-GENCI Francia |
Curie thin nodes - Bullx B510, Xeon E5-2680 8C 2.700GHz, Infiniband QDR / 2012 Bull |
77184 |
1359/ 1667 |
2251 |
10 |
National Supercomputing Centre in Shenzhen (NSCS) China |
Nebulae - Dawning TC3600 Blade System, Xeon X5650 6C 2.66GHz, Infiniband QDR, NVIDIA 2050 / 2010 Dawning |
120640 |
1271/ 2984 |
2580 |
5.Retos de la computación de altas prestaciones
5.1.Concurrencia extrema
5.2.Energía
5.3.Tolerancia a fallos
5.4.Heterogeneidad
5.5.Entrada/salida y memoria
Nivel |
Comida |
Tiempo de acceso relativo |
---|---|---|
Caché nivel 1 |
Comer de la boca |
Fracciones de segundo |
Caché nivel 2 |
Coger comida del plato |
1 segundo |
Caché nivel 3 |
Coger comida de la mesa |
Unos pocos segundos |
DRAM |
Coger comida de la cocina |
Unos pocos minutos |
FLASH |
Coger la comida de una tienda cercana |
Unas pocas horas |
Disco duro |
Coger la comida de Marte |
3-5 años |