La arquitectura multihilo Von Neumann se remonta al CDC 6600 fabricado a mediados de los 60'. Múltiples unidades funcionales en la CPU 6600 podían ejecutar diferentes operaciones simultáneamente usando una tabla de control.
La primera máquina multiprocesador multihilo fue la Denelcor HEP diseñada por Burton Smith en 1978. HEP fue construida con 16 procesadores dirigidos por un reloj de 10Mhz, y cada procesador podía ejecutar 128 hilos (llamados procesos en la terminología de HEP) simultáneamente. HEP desapareció debido al inadecuado soporte de compiladores y software.
El sistema Tera es un descendiente de HEP pero está implementado con modernos circuitos VLSI y tecnología de empaquetado. Consta de un reloj de 400Mhz para el sistema, que puede ejecutar un máximo de 128 hilos (i-streams en la terminología de Tera) por procesador.
A continuación se describe la arquitectura de Tera, el estado de los procesadores y los hilos, y los registros/memoria etiquetados. Las características del sistema Tera incluyen no sólo el alto grado de multihilado sino también la dependencia explícita "lookahead" y el alto grado de superpipelining en las operaciones procesador-red-memoria que se han alcanzado.
La arquitectura Tera fue diseñada con varios objetivos importantes. Se buscaba un sistema escalable a gran número de procesadores y que tuviese un periodo de reloj corto, para que fuese un sistema muy rápido. La máxima configuración de la primera implementación de la arquitectura tendría 256 procesadores, 512 unidades de memoria, 256 unidades de cache E/S, 256 procesadores de E/S, 4096 nodos de interconexión de red, y un periodo de reloj inferior a 3 ns.
En segundo lugar, era importante que la arquitectura fuese aplicable a un gran número de problemas de distintos tipos. Los programas que no fuesen vectorizables, debido a que contenían un gran número de operaciones escalares o frecuentes bifurcaciones condicionales se debían ejecutar eficientemente. Virtualmente cualquier paralelismo aplicable a la carga de trabajo debe observarse como un aumento de velocidad, desde el paralelismo a nivel operación en bloques básicos de programa hasta la compartición de tiempo y espacio a nivel multiusuario.
Una tercera meta fue la implementación sencilla de un compilador. Aunque el conjunto de instrucciones tiene algunas características inusuales, esto no supone serios problemas para el generador de código. No existen restricciones de direccionamiento de registros o memoria, y sólo existen tres modos de direccionamiento. El conjunto de código condicional es consistente y ortogonal. Aunque la riqueza del conjunto de instrucciones permite varias formas de hacer una cosa, la variación en su coste relativo así como los cambios en el entorno de ejecución tiende a ser muy pequeña.
Ya que la arquitectura permite el libre intercambio de localidad espacial y temporal en el paralelismo, un compilador altamente optimizado puede trabajar intensamente en la mejora de la localidad lo que redundaría en una mayor velocidad. Si existe suficiente paralelismo, el compilador tiene un trabajo relativamente sencillo.
La red de interconexión es un toro 3-D de nodos en pipeline con conmutación de paquetes, cada uno de los cuales está enlazado a alguno de sus vecinos. Cada enlace puede transportar un paquete conteniendo direcciones fuente y destino, una operación, y 64 bits de datos en ambas direcciones simultáneamente para cada 'tic' de reloj. Algunos de los nodos están también enlazados a recursos (procesadores, unidades de memoria, procesadores de E/S, y unidades cache de E/S).
En vez de localizar los procesadores en un lado de la red y las memorias en el otro, los recursos están distribuidos más o menos uniformemente a lo largo de la red. Esto permite que los datos sean colocados en unidades de memoria cercanas al procesador apropiado, cuando esto sea posible, intentando evitar recursos que interfieran entre sí.
La red de interconexión de un sistema Tera de 256 procesadores contiene 4096 nodos organizados en una malla toroidal 3-D de 16x16x16. De los 4096 nodos, 1280 están enlazados con los recursos que comprenden 256 unidades de cache y 256 procesadores de E/S. Los restantes 2816 nodos no están enlazados a recursos sino que proveen ancho de banda para el paso de mensajes.
Para aumentar el rendimiento de cada nodo, algunos de los enlaces se han eliminado. Si tenemos las direcciones x, y, z, los enlaces x e y en niveles z alternos no existen. Esto reduce el grado del nodo de 6 a 4, o de 7 a 5, contando el enlace al recurso. De esta forma el ancho de banda de la red es muy grande.
Como la arquitectura Tera es escalable a un número de procesadores p, el número de nodos de la red crece según p^3/2 mejor que p*logp asociado con la mayoría de redes. Por ejemplo, un sistema de 1024 procesadores puede tener hasta 32768 nodos. La razón para la sobrecarga por procesador de p^1/2 en vez de logp resulta del hecho de que el sistema está limitado por la velocidad de la luz.
La latencia de memoria está completamente enmascarada por el paralelismo solo cuando el número de mensajes que están siendo encaminados por la red es al menos p * l, siendo l la latencia. Como los mensajes ocupan volumen, la red debe tener un volumen proporcional a p * l. Como la velocidad de la luz es finita, el volumen es también proporcional a l^3 y sin embargo l es proporcional a p^1/2 en vez de a logp.
Cada procesador en un ordenador Tera puede ejecutar múltiples streams de instrucciones simultáneamente, esto es, múltiples hilos de ejecución a la vez. En la implementación actual pueden estar activos de 1 a 128 contadores de programa (PC) al mismo tiempo. En cada pulso de reloj, la lógica del procesador selecciona un hilo que está listo para ejecutar y le permite ejecutar su siguiente instrucción. Como la interpretación de la instrucción está completamente "entubada" por el procesador y por la red y memoria también, una nueva instrucción de un hilo diferente puede ser ejecutada durante cada pulso sin interferir con sus predecesores.
Cuando una instrucción termina, el hilo al que pertenece pasa a estar listo para ejecutar la siguiente instrucción. En el momento que hay suficientes hilos en el procesador, la latencia media de instrucción queda solapada con instrucciones de otros hilos, con lo cual el procesador está completamente utilizado. Así pues, sólo es necesario tener suficientes hilos para ocultar la latencia esperada (alrededor de 70 pulsos de media). Cuando la latencia está oculta, el procesador está corriendo en su pico de rendimiento y los hilos adicionales no producen un aumento de rendimiento.
Si a un hilo no se le permite la ejecución de su siguiente instrucción hasta que la instrucción anterior esté completada, entonces se necesitarán alrededor de 70 diferentes hilos en cada procesador para ocultar la latencia esperada. El lookahead permite a los hilos ejecutar múltiples instrucciones en paralelo, lo cual reduce el número de hilos necesarios para alcanzar el pico de rendimiento.
Por ejemplo, se pueden ejecutar tres operaciones simultáneamente por instrucción en cada procesador, para lo cual se dispone de tres pipelines : el pipeline-M es para operaciones de acceso a memoria, el pipeline-A es para operaciones aritméticas, y el pipeline-C es para operaciones de control o aritméticas. Las instrucciones son de 64 bits. Si en una instrucción se especifica más de una operación sobre el mismo registro o códigos de condición, la prioridad es M>A>C.
Se ha estimado que si el procesador está dirigido por un reloj de 333 MHz, se puede alcanzar un pico de velocidad de 1G operaciones por segundo en cada procesador. Normalmente, un hilo particular no superará los 100M de operaciones por segundo, debido a la ejecución entrelazada. El pipeline del procesador es mucho mayor, 70 pulsos, comparado con los 8 pulsos que tenía el pipeline en la máquina HEP.
Cada hilo tiene la siguiente información asociada:
- Una palabra de estado de 64-bits: stream status word (SSW)
- 32 registros de 64-bits de propósito general: (R0-R31)
- 8 registros destino de 64-bits: target (T0-T7)
El cambio de contexto es tan rápido que el procesador no tiene tiempo de intercambiar el estado del procesador del hilo residente. Dispone de 128 SSWs, 4096 registros de propósito general y 1024 registros destinos. Es apropiado comparar estos registros en cantidad y funcionalidad con los registros vectoriales o palabras de caches en otras arquitecturas. En los tres casos el objetivo es mejorar la localidad y evitar la recarga de datos.
Las direcciones de programa son de 32 bits. El contador de programa de cada hilo está localizado en la parte baja de su SSW. La parte alta describe varios modos (redondeo en punto flotante, lookahead deshabilitado), una máscara de bits (alineamiento de datos, overflow, etc), y los cuatro códigos de condición más recientes.
La mayoría de las operaciones tienen una variante _TEST que emite un código de condición, y las operaciones de bifurcación pueden examinar cualquier subconjunto de los cuatro últimos códigos de condición emitidos, bifurcando al lugar correspondiente. Asociado con cada hilo también existen 32 registros de 64 bits de propósito general. El registro R0 es especial ya que es leído como 0, y cualquier salida hacia él es descartada. Por otro lado, todos los registro de propósito general son idénticos.
Los registros destino son usados como destino de las bifurcaciones. El formato de los registros destino es idéntico al del registro SSW, aunque la mayoría de las operaciones de transferencia de control usan sólo los 32 bits bajos para determinar el nuevo PC. Separar la determinación de la dirección destino de bifurcación de la decisión de bifurcar permite al hardware decodificar previamente instrucciones en las bifurcaciones destino, evitando retrasos cuando se toma la decisión de salto. Emplear registros destino hace que las operaciones de salto sean más pequeñas haciendo los bucles más cerrados y estrechos.
Un registro destino (T0) apunta al gestor de trap's (o interrupciones), que normalmente es un programa no privilegiado. Cuando ocurre un trap, el efecto es como si se hubiese ejecutado una llamada a una corrutina hacia un T0. Los gestores de trap's pueden ser cambiados por el usuario para ejecuten específicas cualidades o prioridades sin perder eficiencia.
Si existen suficientes hilos ejecutándose en cada procesador de tal forma que se oculte la latencia del pipeline (alrededor de 70 pulsos), entonces la máquina estará funcionando en su pico de rendimiento. Normalmente, si cada hilo puede ejecutar alguna de sus instrucciones en paralelo (por ejemplo, 2 cargas sucesivas), entonces se necesitan menos hilos y actividades en paralelo para alcanzar el pico de rendimiento.
La solución natural es introducir instruction lookahead (vistazo a las instrucciones); la única dificultad es su control. La aproximación tradicional de reserva de registros requiere una tabla con demasiadas entradas en este tipo de arquitectura. La alternativa tradicional, teniendo en cuenta el pipeline, es también impracticable ya que el multihilo y la latencia no predecible de las operaciones de memoria hacen imposible generar código eficiente y seguro.
La arquitectura Tera emplea una nueva técnica llamada explicit-dependence lookahead. Cada instrucción contiene un campo lookahead de 3 bits que especifica explícitamente cómo serán ejecutadas algunas instrucciones de este hilo antes de encontrar una instrucción que dependa de una actual. Ya que 7 es el valor máximo del campo de lookahead, al menos 8 instrucciones y 24 operaciones pueden ser ejecutadas concurrentemente en cada hilo.
Un hilo está listo para ejecutar una nueva instrucción cuando todas las instrucciones con valores de lookahead referidos a la nueva instrucción hayan sido completadas. Así, si cada hilo mantiene un lookahead de 7, entonces se necesitan 9 hilos para ocultar los 72 pulsos de latencia.
El lookahead con una o más operaciones de bifurcación es gestionado especificando el mínimo de todas las distancias involucradas. Las variantes de operaciones de salto JUMP_OFTEN y JUMP_SELDOM, para ramificaciones de alta y baja probabilidad, respectivamente, facilitan la optimización proporcionando una barrera para el lookahead ante el camino menos probable. Existen también las instrucciones SKIP_OFTEN y SKIP_SELDOM. La aproximación es similar a las instrucciones anteriores excepto que el quantum es de instrucciones en vez de pulsos.
El sistema Tera utiliza múltiples contextos para ocultar la latencia. La máquina realiza un cambio de contexto (context switch) cada ciclo de reloj. Tanto la latencia del pipeline (8 ciclos) como la latencia de memoria están ocultos en la aproximación de HEP o Tera. El mayor interés se centra en la tolerancia a la latencia más que en la reducción de la latencia.
Con 128 contextos por procesador, un gran número de registros (2K) deben ser compartidos entre los hilos. La creación de un hilo debe ser muy rápida (unos pocos ciclos de reloj). Memoria y registros etiquetados con bits son empleados para sincronizar. Mientras exista suficiente paralelismo en los programas de usuario para ocultar la latencia, y soporte del compilador, el rendimiento es potencialmente muy alto.
Las ventajas del sistema Tera pueden convertirse en un número potencial de desventajas. El rendimiento será malo si el paralelismo es limitado, como ocurre en un contexto simple. Un gran número de contextos (hilos) demandando montones de registros y otros recursos hardware en turnos implica un alto coste y complejidad. Finalmente, el limitado enfoque en la reducción de la latencia y en el sistema de cache produce montones de paralelismo inútil para ocultar la latencia, así como un gran ancho de banda para memoria, lo cual supone un alto coste en la construcción de la máquina.
Aunque el sistema Tera representa el intento más importante hasta el momento de construir un sistema multiprocesador multihilo, existen otros sistemas multiprocesador que también han potenciado esta faceta :