sábado, 28 de noviembre de 2009

PCI vs AGP vs PCIe

La tecnología de los buses utilizados con las tarjetas gráficas ha evolucionado para adaptarse a las necesidades. Vamos a hablar de los más utilizados en la informática de consumo durante los últimos años.

En primer lugar el bus PCI apareció de la mano de Intel al comienzo de la década de los 90. Se trata de un bus síncrono de 32 bits de ancho con funcionando a 5 V y a una frecuencia de 33 MHz, lo que supone un ancho de banda de 133 MB/s. Este ancho de banda se comparte entre los componentes que estén conectados y que se reparten el uso del bus. Los dispositivos PCI funcionan junto con un árbitro que decide quien tiene permiso para realizar transacciones usando el bus. Más adelante hubo modificaciones al estándar que disminuyeron el voltaje a 3.3 V, aumentaron el bus a 64 bits y la frecuencia de funcionamiento a 66 MHz. Además de esto apareció una versión llamada PCI-X con mejores prestaciones (mayor frecuencia de reloj) para servidores.

Al final de la década de los 90 apareció AGP (accelerated graphic port) al hacerse necesario un aumento en el ancho de banda disponible para la tarjeta gráfica. En este caso, AGP no se trata de un bus sino que es un puerto por lo que no se comparte con otros dispositivos. Está destinado únicamente a comunicar la tarjeta gráfica con el chipset norte o el dispositivo que corresponda. Las características de este bus son similares a las de PCI. Su ancho de banda es 32 bits, funciona a 66 MHz, realizaba una transferencia por ciclo y el voltaje de señalización es 3.3 V lo que da un ancho de banda de 266 MB/s, el doble que PCI. Después de esta versión conocida como AGP 1x, hubo revisiones que aumentaban el número de transferencias por ciclo y en algunos casos reducían el voltaje, apareciendo AGP 2x (2 transferencias, 533 MB/s), AGP 4x (1.5V, 4 transferencias, 1066MB/s), AGP 8x (0.8V, 8 transferencias, 2133MB/s). Aparecieró una variación con un conector físico mayor que permitía un mayor consumo de potencia por parte de la tarjeta gráfica (AGP PRO) manteniendo el resto de características. En la actualidad, salvo algunas excepciones, está en desuso.

PCI-Express (abreviado como PCIe) es una tecnología de entrada/salida que tiene multitud de utilidades y que en la práctica ha sustituido a AGP. Aunque tome el nombre del bus PCI, lo cierto es que no tiene ningún parecido con él. El motivo es la compatibilidad lógica con el anterior estándar. En este caso no se trata de un bus paralelo compartido por varios dispositivos, sino que la arquitectura consiste en enlaces serie que puede unir dos dispositivos entre sí, o varios dispositivos que enlazan individualmente con un switch que los intercomunica. Esta arquitectura es mucho más parecida a una red full-duplex que a un bus clásico. Respecto a su rendimiento, la frecuencia de reloj es 1.25 GHz y realiza 2 transferencias por ciclo, lo que supone 2.5Gb/s. Al utilizar una codificación 8b/10b, el ancho de banda final es de 250MB/s en un enlace. El estándar permite usar varios enlaces simultaneos. En la práctica, las tarjetas gráficas suelen usar 16, por lo que el ancho de banda en cada sentido es 16*250 = 4 GB/s. Más adelante se estandarizó la versión 2.0 que es la que está en uso actualmente, la mayor diferencia es que la frecuencia de reloj es el doble, con lo que se consigue el doble de tasa de transferencia por lane (500 MB/s) y una tarjeta gráfica PCIe x16 (16 enlaces) tiene 8GB/s en cada dirección. La versión 3.0 está en proceso. Entre los cambios más importantes la frecuencia de reloj llega a 4 GHz y la codificación 8b/10b se ha eliminado por una técnica de scrambling, por lo que se espera 1 GB/s en cada sentido usando un solo lane.

Patxi Astiz

gestión de entrada/salida

A la hora de gestionar la comunicación del procesador con elementos externos (disco duro, tarjeta de red, etc...) hay varias estrategias posibles.

La forma más sencilla de tratar una petición de entrada/salida es la espera de respuesta o espera activa. El funcionamiento es muy sencillo. El procesador consulta cíclicamente al dispositivo en cuestión hasta que la operación por la que se esperaba se ha realizado. Mientras tanto el procesador no hace ninguna tarea útil, desaprovechando un tiempo que podría dedicarse a otras tareas. Por este motivo, este patrón de funcionamiento debería evitarse siempre que sea posible.

Otra forma de gestionar la entrada/salida es el uso de interrupciones. Una interrupción es una señal asíncrona que es recibida por el procesador. En ese momento el flujo de ejecución normal se detiene, se guarda el estado del procesador y se pasa a ejecutar lo que se denomina manejador de la excepción. Este manejador consiste en código que puede estar establecido por el sistema operativo o por el programador. De esta manera el procesador puede dedicarse a ejecutar tareas útiles que interrumpe cuando un dispositivo de entrada/salida avisa mediante una interrupción de un evento que necesita ser conocido por el procesador como la finalización de una operación, la llegada de un paquete a un interfaz de red u otros. Entonces el procesador realiza las operaciones necesarias, como copiar datos o procesarlos en el código del manejador.

Un tercer método es el denominado acceso directo a memoria (DMA, Direct Memory Access). Con este método se permite leer y escribir de la memoria a un dispositivo distinto a la CPU y se utiliza para agilizar las operaciones de entrada/salida. Con esta tecnología se libera a la CPU de la tarea de copiar datos entre un dispositivo de entrada salida y la memoria. Normalmente la CPU inicia la transferencia mediante un comando DMA. Mientras se realiza la copia entre un dispositivo y la memoria (o viceversa) el procesador puede realizar trabajo útil. Mediante una interrupción se notifica el final de la copia.

El objetivo final de los métodos anteriores es conseguir que la gestión de entrada/salida suponga el menor esfuerzo posible a la CPU. El siguiente paso es utilizar hardware especializado. El hardware utilizado es generalmente un procesador especializado al que la CPU envía pequeños programas. Estos coprocesadores tienen una memoria local y acceso a la memoria principal y se utiliza cuando se necesitan altas prestaciones en la entrada/salida de la arquitectura.De esta manera el coprocesador puede gestionar la entrada/salida de manera autónoma sin necesidad de mayor intervención por parte de la CPU.

Patxi Astiz

viernes, 27 de noviembre de 2009

Arquitectura de memoria del Phenom

Vamos a fijarnos a modo de ejemplo en una arquitectura actual. En este caso se trata del procesador Phenom de AMD. Este procesador es multi-core y además puede usarse en configuraciones con varios procesadores.

Vamos a comenzar observando una fotografía de un procesador con 4 cores:

Como se puede ver, sobre la foto se han marcado las areas que corresponden a algunas partes del procesador. En concreto en la parte superior izquierda, vemos las zonas más importantes en las que se divide un core. Respecto a la memoria vemos los siguientes componentes:
  • Memoria caché de nivel 1: Al ser un procesador segmentado las instrucciones están separadas del código. Cada core tiene 64 KB para la caché de instrucciones y la misma cantidad para la caché de datos.
  • Cada core tiene su propia caché de nivel 2 de 512 KB
  • Por último, los 4 nucleos comparten una memoria caché de nivel 3 de 2 MB.
Los nucleos del procesador se comunican entre ellos mediante el hardware etiquetado como northbridge en la fotografía, que hace las veces de bus de conexión. Este también conecta el controlador de memoria que se ve a la derecha y los puertos hypertransport de la parte superior e inferior del chip, etiquetados como "HT".

El esquema para una configuración con 4 procesadores es el siguiente:


Según las especificaciones del fabricante, cada uno de estos enlaces realiza hasta 4000MT/s y tiene un ancho de banda de 8 GB/s y hasta 16 GB/s funcionando en modo 3.0. Esto coincide con un bus de 16 bits de ancho, funcionando a 1 GHz. Al ser DDR (Double Data Rate), eso se traduce en 16 * 2 bits/ciclo * 1 GHz = 4GB/s. Se incluyen 2 buses hypertransport, uno en cada sentido, por lo que el total en los 2 sentidos son los 8 GB/s anunciados. El estándar 3.0 aumenta la frecuencia de funcionamiento permitida, por lo que a 2 GHz el bus daría 16 GB/s de ancho de banda en total (sumando los 2 sentidos).

El controlador de memoria DDR2 de doble canal está incorporado en cada procesador y en este caso acepta memoria funcionando a frecuencias hasta 266 MHz. La tecnología DDR2 dobla la frecuencia del bus para obtener un mayor ancho de banda (aumentando la latencia). Además se transmiten 2 bits por ciclo (Double Data Rate). Teniendo en cuenta esto, que el ancho del bus es de 64 bits y el hecho de que el controlador es de doble canal, tenemos un ancho de banda máximo (266 * 2) MHz * 64 pistas/canal * 2 canales * 2 bits/(pista*ciclo) = 17.024 GB/s.

Hay que fijarse que la arquitectura de este ejemplo es NUMA (Non-Uniform Memory Access) por lo que ambos anchos de banda son importantes a la hora de acceder a la memoria ya que un dato podría estar en cualquiera de los bancos de memoria, no necesariamente en el que está directamente conectado al procesador que necesita dicho dato, teniendo que pasar por un enlace hypertransport compartido para más tareas y más lento que los 17 GB/s máximos de ancho de banda del controlador de memoria. En este problema mucha responsabilidad recae en el sistema operativo, que debe repartir los procesos entre los procesadores y las páginas de memoria física entre los bancos de memoria de forma que estén los más cerca posible.

Patxi Astiz

miércoles, 25 de noviembre de 2009

Memoría en GPUs

Las tarjetas gráficas, aunque no siempre, incorporan su propia memoria. La tecnología utilizada en esta es muy similar a la memoria RAM de la CPU de un ordenador.

De hecho las tarjetas gráficas utilizaban memoria SDR, después DDR estándar y en algunos casos memoria DDR2 convencional. Sin embargo en los últimos años han aparecido variaciones específicas para su uso en tarjetas gráficas, con los nombres GDDR2, GDDR3, GDDR4 y GDDR5. Hay que decir que con estas memorias se utilizan diferentes anchos de bus, frecuencias y número de canales, por lo que el ancho de banda de una tarjeta gráfica a su memoria es muy variable y no depende únicamente de la tecnología utilizada.

La primera memoria especialmente pensada para tarjetas gráficas (GDDR2) era en realidad una variante de DDR normal. Funcionaban a 2.5 V e igual que DDR utilizaban el flanco de bajada y de subida para duplicar su tasa de transferencia, solo que a mayor frecuencia de reloj que la memoria DDR convencional. Esto hacía que el consumo y el calor disipado fueran bastante altos. Por ello algunos fabricantes que comenzaron a utilizarla rectificaron y volvieron a DDR. Con este tipo de memoria, funcionando a 900 MHz se han conseguido anchos de banda de hasta 14.4 G/s.

La siguiente variación bautizada GDDR3 fue diseñada por ATI y utilizada tanto por esta compañía como por nVidia. Un pin transmite 4 bits cada 2 ciclos de reloj. En este caso se redujo el voltaje (1.8 ó 2.0 V dependiendo del fabricante), con lo que el consumo es más reducido. Además de esto hay cambios en la señalización y en la terminación eléctrica de esta que permiten mejorar la velocidad de funcionamiento. Este tipo de memoria es muy popular en la tarjeta actuales y se utiliza (no exclusivamente) en las 3 consolas de la actual generación. Como ya se ha dicho, hay una gran variedad de configuraciones para la memoria. Pondremos com ejemplo extremo de esto un dispositivo Tesla de nVidia, que con una frecuencia de reloj de 1600 MHz, 512 bits de bus y 4 canales de memoria (uno por nucleo) llega a un ancho de banda combinado de 410 GB/s con memoria GDDR3. Una tarjeta gráfica normal sin embargo tiene un ancho de banda que puede ir desde los 25 GB/s para buses estrechos y velocidades de reloj bajas, hasta más de 150 GB/s para las configuraciones de memoria más eficientes.



GDDR4 es una mejora sobre GDDR3. Los cambios más importantes son la reducción del voltaje a 1.5 V y el aumento de la tasa de transferencia por ciclo, pasando a transmitir 8 bits cada 2 ciclos de reloj, doblando (teóricamente) el ancho de banda de GDDR3, aunque con una latencia bastante mayor. Este tipo de memoria no ha tenido una gran acogida en comparación con su predecesora y se utiliza en unos pocos modelos de tarjetas gráficas de ATI. Incluso algunos fabricantes han anunciado que no van a fabricar chips de memoria GDDR4. A modo de ejemplo, la primera tarjeta gráfica que usó este tipo de memoria tenía un ancho de banda entre GPU y memoria de 64 GB/s con un bus de 256 bits y una frecuencia de funcionamiento de la memoria de 1 GHz.

Por último tenemos la memoria GDDR5 cuya principal novedad respecto a GDDR4 es que dobla el número de pins de datos del chip de memoria, doblando la tasa de transferencia de su antecesor. Este tipo de memoria se utiliza en algunos modelos modernos de tarjetas gráficas de ATI. Estas tarjetas logran unos anchos de banda que van desde unos 50 GB/s hasta más de 100 GB/s para cada core de la GPU.

En el futuro se espera que muchos fabricantes adopten la futura DDR3 aunque seguramente tenga que competir con XDR o XDR2 (tecnología RAMBUS) en las tarjetas de gama alta.

Patxi Astiz

lunes, 23 de noviembre de 2009

Organización de una caché

Las memorias caché son elementos básicos en cualquier CPU y GPU actual. Su funcionamiento básico es muy sencillo. Se trata de una tabla. Consta de una serie de marcos en los que se almacenan copias de bloques de la memoria principal. Cada uno de estos marcos va asociado a una etiqueta que permite saber que bloque de memoria se ha copiado en dicho marco. Los datos en una memoria caché pueden organizarse según varios parámetros que afectan a su rendimiento de una manera u otra.

El primer factor importante es el tamaño. En general una caché grande tiene menos fallos (es más probable que los datos pedidos estén en la caché), pero a cambio es lenta, además de que una caché muy grande supone ocupar una superficie del chip considerable.

Otra decisión a tomar es el tamaño de los marcos de la caché. En una caché con marcos pequeños es más fácil que el procesador según vaya pidiendo datos (por ejemplo procesando un array) llegue a pedir alguno que no está en la caché, por lo que hay más probabilidad de tener fallos. Sin embargo, en caso de tener un fallo, copiar un bloque a un marco de la caché es una operación menos costosa que si el marco fuera más grande.

Como hemos visto, las cachés pueden alojar algunos bloques de memoria para poder acceder a ellos rápidamente. Existen estrategias a la hora de emplazar un bloque memoria en un marco de la caché:
  • emplazamiento directo: A un bloque le corresponde un único marco. Esta estrategia es sencilla, hace que la caché sea rápida, pero provoca más fallos de caché.
  • emplazamiento asociativo: Un bloque puede almacenarse en cualquier marco. En este caso se minimizan los fallos, pero la caché es más lenta.
  • emplazamiento asociativo por conjuntos: A un bloque le corresponde un pequeño conjunto de marcos. Es una solución intermedia entre las dos anteriores.

Lo ideal sería poder tener una caché lo más grande posible y lo más rápida posible al mismo tiempo. Tecnológicamente no es posible por lo que se tiende a utilizar varios niveles de caché. Pueden usarse varios niveles de caché que son cada vez más grandes y más lentas. De esta manera el procesador accede a la caché de nivel 1, que es la más rápida, pero la más pequeña. En caso de haber un fallo la información no se busca directamente en la memoria principal, sino que se busca en la caché de nivel 2, que es más lenta, pero aún así sensiblemente más rápida que la RAM. En la actualidad es muy común utilizar 3 niveles de caché. El objetivo es que desde el punto del vista del procesador se vea una memoria del tamaño de la memoria principal (GB) con la velocidad de acceso de la caché de nivel 1 (ns).

En general un procesador puede incluir diferentes cachés diseñadas especialmente para una función específica, lo que hace que tengan sus peculiaridades. Por ejemplo es habitual que existan cachés separadas para de datos y de instrucciones, al menos en el primer nivel, para evitar los riesgos estructurales.

Patxi Astiz

martes, 10 de noviembre de 2009

Core i7

Los procesadores Core i7 están fabricados con la última arquitectura de Intel. Esta es una evolución de la de los procesadores Core 2. Se han mejorado varios puntos de dicha arquitectura, se ha añadido multithreading y varios elementos al procesador que veremos al final de esta entrada. Primero vamos a concentrarnos en el núcleo de la arquitectura.

Empezaremos por el front buffer, encargado de suministrar las instrucciones a las unidades de ejecución. Es la parte que corresponde a las fases IF (instruction fetch) e ID (instruction decode) de un pipeline sencillo. Veamos un esquema.



Como se puede ver, el procesador tiene una caché de instrucciones de 32 KB de la que se leen para pasar a decodificarlas. Esta caché está conectada a otra de nivel 2, de 256 KB. La decodificación y traducción desde instrucciones x86 hasta microOps, que son las que realmente maneja el procesador internamente se realiza en 2 pasos. El procesador al tener varios decodificadres, uno de ellos complejo y poder fusionar instrucciones, tiene un máximo teórico de 5 instrucciones x86 emitidas por ciclo. En este proceso de decodificación también se realiza la predicción de saltos y la detección de bucles. Aunque Intel no ha revelado qué tipo de predictor de saltos utiliza, se sabe que hay al menos 2 predictores de 2 niveles. También hay un buffer con direcciones de retorno de llamadas a procedimientos. De esta manera se gestionan más rápidamente estos saltos indirectos. Como novedad en esta arquitectura, la detección de bucles se realiza después de terminar la decodificación. De esta manera las instrucciones de un bucle ya están decodificadas y se libera de trabajo a los elementos previos del front-end, incluida la caché de instrucciones. Por último, se reservan registros y se leen los valores del buffer de reordenación que sean necesarios antes enviar una instrucción a la fase de ejecución, que vemos en el siguiente diagrama.

Como se muestra en el diagrama, solo hay una estación de reserva unificada de 128 entradas, que se encarga de suministrar instrucciones a las 6 unidades funcionales siempre que sea posible. De estas unidades, 3 hacen operaciones load/store y otras 3 calculos aritméticos y lógicos. Los resultados se llevan al buffer de reordenación que hemos visto anteriormente. Los resultados de las operaciones load/store además, no acceden directamente a la caché de datos de 32 KB que tiene el procesador, sino que pasan por un buffer intermedio, para evitar los riesgos de datos al leer y escribir en instrucciones diferentes que han podido ser ejecutadas fuera de orden. Desde este buffer pasa a la caché.

De manera resumida hemos visto el funcionamiento de la base de un procesador Core i7, y como en general sigue un diseño típico en los procesadores de propósito general de hoy en día, utilizando las técnicas de optimización comunes, como predicción de saltos, especulación o multithreading. Sin embargo lo que hemos visto corresponde solo a un nucleo del procesador. El diseño del Core i7 se ha hecho de manera modular, por lo que un procesador con esta arquitectura puede tener 2 o más nucleos como el descrito. Todos estos núcleos comparten otros elementos que vamos a ver a continuación:

En este ejemplo, hay 4 núcleos. Cada uno de ellos tiene los elementos que se han descrito anteriormente. Los 4 sin embargo comparten el resto de elementos, entre ellos la caché de nivel 3, cuyo tamaño es 8 MB.

Mientras que los núcleos de un Core i7 son en gran medida iguales a los de un Core 2 aunque con más recursos (más entradas en la estación de reserva, mayor buffer de reordenación, etc...) y algunas mejoras, en el diseño de esta arquitectura hay cambios muy importantes. Especialmente la inclusión del un controlador de memoria DDR3 de 3 canales en el chip y el uso de un bus (llamado QPI) de interconexión entre procesadores (para entornos multiprocesador) y entre el procesador y el hub de la placa base. De esta manera se evita el clásico FSB (front side bus) que en caso de haber más de un procesador, se compartía para acceder a la memoria así como para comprobar la coherencia de las cachés de cada procesador. La arquitectura Core i7 pasa a ser NUMA (Non-Uniform Memory Access). Este diseño es prácticamente el mismo que introdujo AMD hace varios años en sus procesadores Opteron y posteriores con buenos resultados.

En este diagrama podemos ver la configuración de una máquina con dos procesadores Core 2 y a la derecha la configuración NUMA equivalente con 2 procesadores Core i7.

Mientras que antes los procesadores tenían que repartirse el bus de salida para llegar a cualquier recurso que estuviera fuera del propio procesador, con la arquitectura Core i7, un procesador tiene buses diferentes para acceder al parte de la memoria, al hub de la placa base (y por lo tanto a periféricos como la tarjeta gráfica) y al otro procesador (y a la memoria que este controla), por lo que no habrá conflictos por compartir un mismo bus con el otro procesador. Otra de las ventajas de este diseño es que los accesos a memoria tienen menos latencia, al estar conectada directamente al controlador de memoria del procesador. Incluso en los casos en los que un dato está en los bancos de memoria del otro procesador, la latencia, aunque peor que si la memoria estuviera conectada directamente al procesador que pide el acceso, mejora respecto a las arquitecturas anteriores.

Como hemos visto esta arquitectura no presenta grandes cambios en lo que es el diseño del núcleo del procesador, pero sí supone un avance importante en la forma de conectarse con la memoria y otros elementos externos al procesador y que muchas veces puede ser el cuello de botella de un sistema.

Patxi Astiz

domingo, 8 de noviembre de 2009

Radeon HD5800

A modo de ejemplo vamos a estudiar la arquitectura de una GPU actual. Mas en concreto se trata de la arquitectura de la serie HD5800 de AMD. En primer lugar vamos a ver un esquema general.

Como se puede ver, se trata de una arquitectura unificada, es decir no hay hardware especializado en procesar vertices y hardware separado para procesar fragmentos. Otro aspecto importante es que la mayor parte del diseño lo ocupan unidades de ejecución que están replicadas. En la parte superior del esquema se puede ver lo que han llamado "Command Processor" y "Graphic Engine". Estas estructuras hardware se dedican a recibir las instrucciones que llegan del procesador a través del bus y crear los hilos que sean necesarios, de forma similar a la traducción y emisión de instrucciones de una CPU. Estos hilos se envían a las unidades de ejecución a través del "Ultra-Threaded Dispatch Processor", de manera similar a lo que hace una estación de reserva.

Como se puede ver, las unidades de ejecución están replicadas. En cada una de las 20 unidades que muestra el diagrama (llamadas "SIMD engine"), hay 16 subunidades ("thread procesors"). A su vez cada una de estas tiene 5 elementos que son las que realmente realizan el procesado y se muestran en la siguiente figura. Esto significa que hay un total de 1600 unidades de proceso


Como puede verse, se trata de pequeños procesadores especializados en operaciones en coma flotante y que dependiendo del tamaño de los datos pueden realizar hasta 4 sumas y 4 productos por ciclo. De esta manera se pueden realizar miles de operaciones por ciclo en paralelo, llegando a una capacidad de 2 TFLOPS según el fabricante. También hay una "branch unit" para calcular el destino de un posible salto lo antes posible.

Aunque en las imágenes anteriores se vean los elementos de la jerarquía de memoria, es la siguiente imagen se aprecia mejor.


Hay varios registros compartidos entre cada uno de las unidades ("stream core") del último diagrama. Cada "SIMD engine" tiene tanto 8KB de caché de nivel 1 como 32 KB de memoria local de acceso rápido. Para acceder a dicha caché hay una unidad de texturas para cada 4 "thread processors", es decir 4 unidades de texturas por cada "SIMD engine". Además de esta caché de nivel 1, existen 4 cachés de nivel 2, que están asociadas a un controlador de memoria cada una, que en este diagrama no se han representado. También existen cachés de constantes y de instrucciones para acelerar el funcionamiento del "Ultra-Threaded Dispatch Processor". Por último, vamos a ver lo que en este último diagrama aparece como "memory controller" en la siguiente imagen.

En este diagrama se aprecia como existe un hub que interconecta las 4 controladoras de memoria (cada una con su caché de nivel 2) con el bus de interconexión PCI Express, la salida digital de video y otro elementos a los que se envían datos o desde los que se leen.

A modo de resumen se podría decir que la serie Radeon HD5800 sigue la norma de todas las arquitecturas de procesadores gráficos del momento. El diseño no es especialemente complicado y su base es la replicación de recursos y el paralelismo masivo. También es importante ver que la memoria sí que es cada vez más compleja, ya que es necesario mover una gran cantidad de datos para poder alimentar a todas las unidades de ejecución que existen.

Patxi Astiz

Multithreading en GPUs

El multithreading en los procesadores de proposito general es una técnica muy común. Para aprovechar al máximo los recursos disponibles, en colaboración con el sistema operativo, se pueden procesar intrucciones de 2 o más hilos simultaneamente, aprovechando así el paralelismo a nivel de hilo.

En las tareas para las que se diseñan los procesadores gráficos, el paralelismo es fácilmente aprovechable. Hay cálculos que deben realizarse para cada vértice o para cada fragmento, lo que significa repetir la misma tarea una y otra vez sobre diferentes datos en memoria, por lo que el paralelismo y la idea de multithreading es básica en el diseño de GPUs actuales. Veamos un esquema de la arquitectura de un procesador gráfico moderno.


Como se puede ver, la arquitectura del ejemplo hay unas pocas estructuras hardware que están replicadas varias veces. Esto es una señal de que el diseño está hecho para aprovechar el paralelismo. Cada uno de los cuadrados verdes está representando un pequeño procesador, capaz de realizar operaciones encoma flotante. Las estructuras de control crean un hilo para cada operación que ha de realizase, ya esta esta relacionada con vertices, geometría o fragmentos. Estos hilos se asignan a una de las unidades de ejecución, que realiza la tarea y termina. Estos hilos, aunque comparten nombre con los que usamos en los sistemas operativos y CPUs actuales. La idea es la misma, es decir ejecutar en paralelo distintas partes de código compartiendo la memoria. Sin embargo, no tienen mecanismos como semaforos, o mutexes. Son mucho más simples y no requieren una gestión complicada.

La idea de hilos en una GPU se puede representar con el siguiente ejemplo. Imaginemos que queremos sumar las matrices A y B, de tamaño 1000x1000. Si pensamos en un solo hilo, el algoritmo sería algo parecido a lo siguiente:

for (i = 0; i<1000; i++)
    for (j=0; j<1000; j++)
         C[i,j]=A[i,j]+B[i,j]

Si quisieramos ejecutar esto en una GPU, la estrategia consiste en crear 1000x1000 hilos, cada uno de los cuales hace una suma y entre todos completan la matriz C con el resultado. Estos hilos se distribuyen por las numerosas unidades de ejecución, por lo que el nivel de paralelismo es altísimo. Como ya se ha dicho antes, no hay mecanismos complejos para sincronizar los hilos o reservar recursos. Esto sin embargo no es un problema, ya que como se puede ver, los hilos que se generan son muy simples y no necesitan este tipo de facilidades.

La capacidad de las GPUs actuales de realizar ciertos calculos de una manera muy eficiente, aprovechando el paralelismo como hemos visto, ha hecho que además de para generar gráficos, se utilicen en otras disciplinas como en cálculo científico, donde ciertos problemas pueden descomponerse en multitud de hilos que realizan pequeñas operaciones, ejecutándose en la GPU con un rendimiento mucho mayor que en la CPU.

Patxi Astiz