UNIVERSIDAD CARLOS III DE MADRID INGENIERÍA EN INFORMÁTICA. ARQUITECTURA DE COMPUTADORES II 14 de junio de 2010 Para la realización del presente examen se dispondrá de 2 1/2 horas. NO se podrán utilizar libros ni apuntes. Entregar cada ejercicio en hojas separadas. Ejercicio 1 (1 punto) Las siguientes figuras muestran los diagramas de estados de los protocolos de coherencia caché MSI y MESI paraa arquitecturas de memoria compartida. Comentar un ejemplo práctico donde el protocolo MESI es más eficiente que el MSI en términos de reducción del tráfico de bus. SOLUCIÓN Cuando un procesadorr lee datos de forma exclusiva (sin que otro más lo lea) y posteriormente los escribe, el MESI recorre los estados I -> E -> M sin generar tráfico de bus. Por el contra, el MSI recorre los estrados I -> S -> M generando tráfico innecesario en la última transición
Ejercicio 2 (1 punto) Explicar que tipos de contención existen en la comunicación entre nodos dentro de una arquitectura multiprocesador de memoria distribuida. Indicar brevemente a qué se deben y cómo podrían reducirse. SOLUCIÓN Hay dos tipos de contención: Contención de la red: Se produce en los enlaces o switches de la red. Se puede reducir ajustando el patrón de comunicaciones a la topología de la red. Contención en los puntos finales: Se produce en los nodos de procesamiento. El nodo en el que la contención es severa se denomina hot spot. Una de las formas de redecirlo es modificando el paralelismo de la aplicación en alguna de sus fases (Descomposición, asignación, orquestación y/ó mapeo).
Ejercicio 3 (2 puntos) 1.- La siguiente figura muestra una topología en anillo con 9 nodos con las siguientes características: P + M + PC 0 1 2 3 4 S Enlace interno 8 7 6 5 Enlace externo Emplea un protocolo de encaminamiento de conmutación de paquetes (store and fordward). La cabecera de cada paquete incluye el número de nodo destino al que el mensaje está dirigido. Cada nodo tiene un switch de comunicaciones con dos enlaces externos y uno interno (conectado a un único módulo cómputo). Cada módulo tiene su procesador, memoria y procesador de comunicaciones. Cada nodo puede enviar y recibir dos paquetes de forma simultánea (por cada uno de sus dos enlaces externos). El ancho de banda de los enlaces externos e internos es infinito. Los enlaces internos no tienen routing delay (retardo de encaminamiento del switch). Los enlaces externos tienen routing delay (retardo de encaminamiento del switch) de 1 ms. El retardo de envío y recepción del procesador de comunicaciones es de 0 ms. Se pide: 1. Diseñar un algoritmo de encaminamiento aritmético basado en el valor del nodo destino. 2. Diseñar el modo de orquestación de una operación de broadcast iniciada por el nodo 0 sobre todos los nodos. Inidicar el flujo de comunicaciones y el tiempo total de la operación. El tamaño de cada mensaje coincide con el de un paquete. 3. Diseñar el modo de orquestación de una operación de All_Reduce iniciada por el nodo 0 sobre todos los nodos. Inidicar el flujo de comunicaciones y el tiempo total de la operación. El tamaño del mensaje coincide con el del paquete. Nota: una operación All_Reduce es equivalente a la de un Reduce salvo que el resultado final es conocido por todos los procesos que intervienen en la operación.
SOLUCIÓN 1. Cada switch sólo puede tomar tres decisiones: encaminar en sentido horario, encaminar en sentido anti horario y entregar al módulo local. Si Id_sw y Id_dest son el identificativo del switch y nodo destino, respectivamente. El algoritmo de enrutamiento sería: If(Id_sw = Id_dest) then entregar_módulo_local; elseif (abs(id_dest-id_sw)<(8-id_dest+id_sw)) then encaminar_sentido_horario(); else encaminar_sentido_antihorario(); end 2. Una operación de broadcast eficiente consiste en enviar primero a los nodos más alejados (4 y 5) por cada uno de sus enlaces. Posteriormente a los nodos siguientes más alejados (3 y 6), siguiendo por el (2 y 7) y finalizando por el (1 y 8). El tiempo total será el del envío a los nodos más alejados: T=4*T_sw=4*1mn=4ms. 3. Existen dos alternativas: a) Realizar una reducción en paralelo y un broadcast del resultado. La reducción consistiría en enviar el resultado al nodo 0 siguiendo los pasos inversos al apartado anterior: los nodos (4 y 5) enviarían al (3 y 6), en paralelo, el (3 y 6) enviaría al (2 y 7), el (2 y 7) al (1 y 8) y el (1 y 8) al 0. En total el tiempo sería el mismo que en apartado anterior: 4ms. En una segunda etapa, se realizaría el broadcast del resultado, con un tiempo de 4ms. El tiempo total sería 8ms. b) Cada nodo envía un mensaje que recorre en un sentido (horario o anti horario) toda la malla. Al recibir este mensaje tendrá el contenido de la operación de reducción. Dado que cada switch puede enviar y recibir dos mensajes, se puede realizar esta operación en ambos sentidos de forma simultánea. El número de transiciones coincidirá con el diámetro de la red: será T=4*T_sw=4*1mn=4ms Para que sea colectiva todos los procesadores deberán realizar la misma operación (en cada sentido). Estas comunicaciones se realizan en paralelo sin interferir, por lo que el tiempo total será el mismo (4ms).
Ejercicio 4 (2 puntos) 1.- El siguiente código un código secuencial correspondiente a un bucle de 1000 iteraciones. La función f() devuelve valores entre 0 y 1 homogéneamente distribuidos. El vector A tiene N entradas. For (i=0;i<1000;i++) { j=(int)n*f(i); A[j]=A[j]*8; El siguiente código es una versión paralela en OpenMP. El tiempo de ejecución de la sentencia IF es de 1ms y de la sentencia A[j]=A[j]*8 es de 10ms. El tiempo de invocación de las funciones de OpenMP, del la función f() y de la ejecución del lazo es despreciable. Se pide: 1. Calcular la aceleración (speedup) para 2, 10 y 100 hilos (ejecutados cada uno en un procesador diferente de una arquitectura UMA). 2. Repetir los cálculos asumiendo que el lazo (secuencial y paralelo) tiene 2000 iteraciones en vez de 1000. Es el código escalable? #pragma omp parallel private(my_rank,num_threads,lim_inf,lim_sup,i), shared(a,x) { my_rank=omp_get_thread_num(); Num_threads=omp_get_numthreads() Lim_inf=my_rank*N/Num_threads; Lim_sup=(my_rank+1)*N/Num_threads; For (i=0;i<1000;i++) { j=(int)n*f(i); If(j>=Lim_inf && j<lim_sup) { A[j]=A[j]*8;
SOLUCIÓN 1. El tiempo de ejecución secuencial es de T_s=1000*10ms=10000ms. Todos los hilos ejecutan el lazo completo. Ejecutando 1000 sentencias Ifs. Dado que las entradas de j están homogéneamente distribuidas, se puede asumir que cada hilo ejecutará únicamente 1000/Num_threads iteraciones. De este modo, el tiempo de ejecución paralelo es Tp=1000*1ms+(1000/Num_threads)*10ms. Para Num_threads=2 se tiene: Tp=1000+5000=6000 ms; Aceleración=10000/6000. Para Num_threads=10 se tiene: Tp=1000+1000=2000 ms; Aceleración=10000/2000. Para Num_threads=100 se tiene: Tp=1000+100=1100 ms; Aceleración=10000/1100. 2. Aumentando el tamaño del problema (2000) tenemos: El tiempo de ejecución secuencial es de T_s=2000*10ms=20000ms.. Para Num_threads=2 se tiene: Tp=2000+10000=12000 ms; Aceleración=20000/12000 = caso anterior con N=1000 Para Num_threads=10 se tiene: Tp=2000+2000=4000 ms; Aceleración=20000/4000 = caso anterior con N=1000 Para Num_threads=100 se tiene: Tp=2000+200=2200 ms; Aceleración=20000/2200 = caso anterior con N=1000 Se puede apreciar que la aceleración (y eficiencia) es la misma que en el caso anterior, por lo que el código no será escalable (la eficiencia no aumentará con el tamaño del problema).
Ejercicio 5 (2 puntos) El siguiente programa se ejecuta en ocho procesadores en un sistema de memoria compartida basada en un bus. La variable x es un entero y se asume que reside inicialmente en la memoria principal y replicada en la memoria caché de todos los procesadores como compartida. La arquitectura implementa las primitivas lock/unlock usando las instrucciones LL (load-linked) y SC (Store-Conditional). Asumir que se usa un protocolo MSI de coherencia caché. Procesador 1 Procesador [2..7] Procesador 8 lock (cerrojo) lock (cerrojo) lock (cerrojo) x=x+1 unlock (cerrojo) x=x+1 unlock (cerrojo) x=x+1 unlock (cerrojo) Se pide: 1. Calcular el número de transacciones de bus que se generan en el bus asumiendo que los 8 procesos alcanzan el cerrojo a la vez y que inicialmente la cache asociada a cada procesador no almacena ninguna variable compartida y que las variables compartidas cerrojo y x residen en bloques de memoria diferentes. 2. Modificar el código para usar directamente las instrucciones LL y SC sustituyendo las llamadas a lock/unlock.
Solución La implementación de lock y unlock sería: lock: ll.r1, /dir_cerrojo /* LL dir a R1 */ bnz.r1, $lock /* cerrado? */ sc.r2, /dir_cerrojo /* SC R2 en dir */ beqz $lock /* si fallo, again*/ ret unlock: st #0, /dir_cerrojo ret a) Para calcular el número de transacciones de bus que se generan al ejecutar el programa, vamos a mostrar lo que ocurriría para la primera entrada a la sección crítica: Puesto que el contenido de la variable de memoria que implementa el lock no está inicialmente en ninguna cache, todos los LL causan fallo de cache y consecuentemente transacciones BusRd (8 transacciones de bus). Después, la transacción de bus asociada al único SC que no fracasará anula las de las instrucciones SC fallidas, que finalmente no saldrán al bus (1 transacción de bus). Los procesos que no lograron atravesar el lock hacen una espera activa con la instrucción de LL, que fallará la primera vez en cache (7 transacciones de bus). Por su parte, el proceso que está en la sección crítica leerá y posteriormente escribirá el contenido de la variable x (que no está inicialmente en ninguna cache) (2 transacciones de bus). Finalmente, el lock es liberado (1 transacción de bus). En total se requieren 19 transacciones de bus. El resto de las veces se va entrando en la sección crítica requiriendo 2 transacciones de bus menos cada vez (dado que hay un proceso menos activo), con lo que el número total de transacciones de bus requeridas es: 19 + 17 + 15 + 13 + 11 + 9 + 7 + 5 = 96. b) Usando directamente las instrucciones de LL y SC, el código quedaría como sigue: acumula: LL reg1, x addi reg1, reg1, 1 SC x, reg1 beqz acumula
Ejercicio 6 (2 puntos) Se desea implementar un programa capaz de calcular el número de repeticiones de cada letra del alfabeto en un texto. Para ello se carga inicialmente dicho texto en un array de caracteres, siendo necesario repartir el mismo entre un número de procesos que realizarán el cálculo de repeticiones para la región del texto correspondiente. Una vez cada proceso haya calculado las repeticiones de la región asignada, se deben enviar el número de apariciones de cada carácter al proceso Maestro. El siguiente código muestra el esqueleto de una implementación del problema en MPI. #define MASTER_PROCESS 0 #define MAX_CHARACTERS 29 int myrank, size; int counter[max_characters]; char * text = NULL; void main(int argc, char ** argv){ MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); // Master process if (myrank==master_process){ text = readfile(filewithtext); master(); // Slave processes else{ slave(); MPI_Finalize(); Se pide: 1. Implementar las funciones master() y slave() para lograr la funcionalidad pedida. Se deberá explicar la solución justificando las llamadas a MPI utilizadas. Los tipos de datos necesarios para realizar el ejercicio son MPI_CHAR (char) y MPI_INT (int). 2. Comentar la escalabilidad de la solución en el caso de leer un fichero de gran tamaño. 3. Comentar la escalabilidad de la solución en el caso de que se lancen un gran número de procesos MPI. Ejemplo de funcionamiento: a B c a t d h y y e e b y u h r Process 1 Process 2 Process 3 Process 4... En este ejemplo, para el proceso 1, los datos correspondientes de la región procesada se corresponden con el siguiente array, suponiendo que la primera posición del array se corresponde con la letra A y la última, con la letra Z. 2 1 1 0... 0 NOTA: Suponer que la longitud del texto es siempre divisible entre el número de procesos utilizados.
Los tipos de MPI_Op para realizar la operación MPI_Reduce son: MPI_MAX MPI_MIN MPI_MAXLOC MPI_MINLOC MPI_SUM MPI_PROD maximum, max minimum, min maximum and location of maximum minimum and location of minimum sum product int MPI_Send ( void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm ); int MPI_Recv ( void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status ) int MPI_Scatter ( void *sendbuf, int sendcnt, MPI_Datatype sendtype, void *recvbuf, int recvcnt, MPI_Datatype recvtype, int root, MPI_Comm comm ); int MPI_Gather ( void *sendbuf, int sendcnt, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm ) ; int MPI_Reduce ( void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm ) ; int MPI_Bcast ( void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm ) ; size_t strlen(const char *s); /** * Esta función recibe una región de texto y devuelve un array de 29 posiciones * con las frecuencias de cada letra en region. * * NOTA: Suponer esta función implementada e importada. */ int* processregion (char* region);
Solución: void master(){ int numcaracteres; char *localtext; numcaracteres = strlen(text) / size; MPI_Bcast ( &numcaracteres, 1, MPI_INT, MASTER_PROCESS, MPI_COMM_WORLD ) ; MPI_COMM_WORLD); MPI_Scatter ( text, numcaracteres, MPI_CHAR, NULL, numcaracteres, MPI_CHAR, MASTER_PROCESS, MPI_Reduce ( NULL, counter, MAX_CHARACTERS, MPI_INT, MPI_SUM, MASTER_PROCESS, MPI_COMM_WORLD ) ; void slave(){ int numcaracteres; char *localtext; MPI_Bcast ( &numcaracteres, 1, MPI_INT, MASTER_PROCESS, MPI_COMM_WORLD ) ; localtext = (char*) malloc (numcaracteres * sizeof(char)); MPI_COMM_WORLD); MPI_Scatter ( NULL, numcaracteres, MPI_CHAR, localtext, numcaracteres, MPI_CHAR, MASTER_PROCESS, counter = processregion (localtext); MPI_Reduce ( counter, NULL, MAX_CHARACTERS, MPI_INT, MPI_SUM, MASTER_PROCESS, MPI_COMM_WORLD ) ; 2.- Al leer un fichero de gran tamaño se consigue aumentar el volumen de datos y por tanto, la carga computacional. Existe un cuello de botella en el acceso a disco (se realiza a disco) pero el resto de la operación del algoritmo se realiza en paralelo. Asumiendo que la función Process Region es suficientemente costosa elcódigo resulta escalable (un mayor volumen de datos mejoraría el rendimiento del programa con un mayor número de procesadores). Este código no tiene problema de desbalanceo de carga. 3.- Al aumentar el número de procesadores se produce un aumento de las comunicaciones, especialmente en el nodo 0. Esto puede originar un cuello de botella en las comunicaciones (asociado a problemas de contención). Por este motivo, el rendimiento empeorará y la solución no será escalable para este escenario.