El siguiente diagrama es una versión en forma de flujo ordenada de lo que son las Compute Unit de las familias PS4 y Xbox One…

ShaderGCNFlujo

… de lo que son las Compute Units de las arquitectura GCN, independientemente de si hablamos de las CUs estándar o de las NCUs.

kaveri-14b

VegaNCU

En esta entrada me voy a centrar para explicar como funcionan por dentro las Compute Units de la manera más realista posible. Por lo que es necesario ir desglosando los diferentes elementos uno por uno teniendo en cuenta su posición y la comunicación con el resto.

Cache L1: Se encuentra entre la cache L2 de la GPU y un grupo de Compute Units que puede ser de 4…

4CU2

… de 3…

3CU2

… e incluso de 2 (vease el caso de las AMD Vega), dicha Cache L1 se divide en dos subtipos, una de instrucciones y la otra de datos. La Cache de Instrucciones de 32KB y es utilizada por el planificador, la cache de datos es de 16KB y es un utilizada por la unidad escalar de la Compute Unit.

L1CacheDiagram.PNG

Planificador: Se encarga de planificar la ejecución de hasta unas 40 olas funcionando concurrentemente en grupos de 4 en paralelo, el trabajo del planificador es simplemente controlar el orden en el que estas se ejecutan y asegurarse que la ejeución sea del tipo Round-Robin. ¿Que significa esto? El tiempo en ciclos de reloj de cada instruccion esta medido y por tanto es previamente conocido por lo que esto le permite al planificador re-ordenar la ejecución de las diferentes olas en el caso de que hayan eventualidades como que no se encuentren los datos necesarios en los registros correspondientes. En ese caso lo que se hace es retrasar la ejecución de la instrucción y se ejecuta la siguiente instrucción.

El motivo por el cual el planificador tiene tiempos fijos es porque las ALUs (escalar y vectoriales) no ejecutan jamás desde la Cache L1 hacía abajo sino solamente desde sus registros. (SGPR y VGPR) que son la memoria más cercana a las ALUs y lo que se hace cuando se traslada un dato desde la Cache L1 o desde la Cache L1 TA/TD no es leer los datos desde esas memorias sino que se copían por lo que en el caso de las instrucciones gráficas todas aquellas que requieran los datos de alguna textura tienen que esperar a que los datos les lleguen desde la Cache L1 TA/TD, siendo los Fragment/Pixel Shaders los más efectos en cuanto a rendimiento, aunque en general cualquier tipo de shader que utilice datos de textura.

Las instrucciones se dividen en dos tipos distintos:

  • Simples: Se ejecutan en un solo ciclo de reloj
  • Complejas: Necesitan unos cuatro ciclos de reloj para ejecutarse

Además, cada una de las olas 40 olas (10 olas por unidad SIMD) tiene su propio contador de programa independiente al resto y el planificador tiene una unidad de predicción de saltos que requiere unos 16 ciclos de reloj por lo que cuando hay un salto en una de las olas esta se queda parada unos 16 ciclos de reloj en toal, las GPUs no son muy buenas en estas tareas por lo que se evita en el código lo máximo posible.

El tamaño estandar para las olas de AMD es de unos 64 elementos por ola, teniendo en cuenta que cada ola se ejecuta en una unidad SIMD y estas son de 16 elementos esto significa que pueden resolver una ola en unos 4 ciclos de reloj en total. ¿Y cuantas olas es posible enviar? Pues un total de 16 olas para un total de 1024 elementos, ese es el limite de una lista de pantalla de DirectX 11/12 y APIs del mismo calibre como GNM/OpenGL 4/Vulkan… Es decir, el Procesador de Comandos de la GPU que no se encuentra en la Compute Unit envia en grupos de 1024 elementos ordenados en 16 olas… ¿Cual es el problema? No se ejecutan unas 16 olas olas de manera consecutiva sino unas 10 olas por lo que de entrada tenemos un problema y es que los 1024 elementos son un tope máximo donde cada Compute Unit debería poder en teoría ejecutar unos 4096 elementos pero realmente puede ejecutar como mucho unos 640 elementos por unidad SIMD hasta llegar a los 2560 elementos en total, pero el motivo de dicha limitación no esta en el planificador y ya llegaremos a ello llegado el punto.

Unidad Escalar y Registros Escalares (SRGP): Tenemos una sola unidad escalar en toda la Compute Unit, como su nombre indica es la unidad encargada de realizar las operaciones escalares pero no es un simil de las unidades SIMD que hay en la Compute Unit sino que su naturaleza es distinta.

  • Es la única ALU capaz de trabajar con valores de doble precisión (64 bits), no tiene importancia en el mercado doméstico pero si en computación científica.
  • No solo puede acceder a sus registros (4KB SGRP) sino también a la Cache L1 (no confundir con la cache de texturas que es la Cache L1 TA/TD).
  • Todas las operaciones de petición a memorias son realizadas por la unidad escalar.

Los Grupos de Trabajo generados por la API combinan instrucciones escalares y vectoriales junto a operaciones de acceso a memoria pero la unidad escalar es la única que puede escribir los resultados en los registros VGPR mientras que las unidades SIMD/Vectoriales no pueden escribir en los registros SGPR, esto es así porque la unidad escalar es la que recibe los datos de las caches y del controlador de memoria que luego necesitan las unidades SIMD/Vectoriales.

Unidades SIMD/Vectoriales: Hemos de tener en cuenta que las unidades SIMD realizan una sola instrucción por un conjunto de datos de manera simultanea, el conjunto de instrucciones en el caso que nos ocupa puede ser de hasta 16 elementos aunque el nivel de ocupacion no tiene porque ser siempre el 100% y pueden haber casos donde existán ALUs que no se utilicen. En todo caso las unidades SIMD solo puede ejecutar 1 sola instrucción por ciclo, es igual el tiempo que esta tarde por lo que si tenemos una instrucción que ocupa unicamente unos 8 elementos no podremos utilizar las 8 ALUs restantes de la unidad SIMD para la siguiente instrucción por el hecho que las Compute Unit no estan preparadas para ello.

Las unidades SIMD carecen de los modos de ejecución directo e indirecto y solo pueden ejecutar de manera directa los datos que se encuentran en la VGPR, ni tan siquiera tienen acceso directo a las Cache L1 TA/TD que es donde se encuentran las unidades de filtraje de texturas, sino que los datos de las mismas son copiadas en los VGPR para ser leidos por la ALU, mientras el VGPR tenga datos que poder leer la SIMD estara funcionando por lo que es importante que el flujo de datos no cese jamás, mucho más importante que el nivel de utilización de la misma unidad SIMD.

Los tipos de instrucciones que en las unidades SIMD de las CUs en las arquitectura GCN no se  ejecutan son:

  • Instrucciones Escalares, es decir, que solo afectan a un dato dado que son ejecutadas por la unidad escalar.
  • Instrucciones de petición de Lectura y Escritura a memoria.
  • Instrucciones de salto.

Lo único que procesan son instrucciones vectoriales o del tipo SIMD, donde una sola instrucción es aplicada a una serie de datos en paralelo. Hemos de tener en cuenta que en aquí el tamaño máximo de una ola es de unos 64 elementos por lo que se puede aplicar una misma instruccion a unos 64 elementos distintos, siendo la unica diferencia e esto los Pixel/Fragment Shaders donde la configuración es de 4×16 elementos, pero ese es un caso especial al que ya entraremos.

VGPR: Los VGPR son los registros de las unidades SIMD, cada uno de ellos de unos 64KB de tamaño por lo que tenemos unos 256KB en total de registros para las unidades vectoriales de cada Compute Unit, son elementos de uso privado y para trasladar datos se utiliza la Local Data Share, el motivo es evitar un cableado demasiado complejo en el chip que es lo que se necesitaría en el caso de no estar el local data share.

Ahora bien, si hacemos un calculo simple veremos un problema de entrada:

64 KB/(1024 hilos*4 bytes registro)= 16 registros hilo

Dado que cada ola esta compuesta por unos 64 elementos en el caso de AMD porque las ALUs están compuestas de 64 elementos entonces en este caso nos encontraríamos con un nivel de ocupación en las ALUs de un 25% en teoría… Y digo en teoría porque tendríamos el 100% de la ocupación de las ALUS en este caso antes de saltar a la siguiente ola.

Wave16.png

De esta manera podemos concatenar hasta unas 16 olas seguidas peero nos encontramos con que el planificador solo puede organizar unas 10 olas seguidas por lo que…

64 KB/ (640 hilos* 4 bytes/registro)= 25 registros/hilo

Ya no es un múltiple de 16 por lo que tenemos un problema ya que pese a solventarse en dos ciclos solamente por SIMD… hay una desocupacion de casi el 25%.

Wave25.png

Es decir, se recomienda que independientemente del tamaño de la ola que esta se organice en multiples de 16 elementos para poder ocupar todas las ALUs, el problema es que el máximo de 10 olas por SIMD de manera consecutiva no se llevan muy bien con un nivel de ocupacion óptimo de las ALUs.

gcn_vgpr_table

Por lo que el tamaño ideal utilizado para las olas suele ser de 32 registros para cada una de ellas, la ejecución pasa de los 10 ciclos a los 8 ciclos pero la ocupación es del 100% dentro de una ejecución ideal.

Pero la cantidad de ciclos también marca el tiempo en que las Compute Units de dentro de un mismo Shader Engine tardarán a pedir nuevos datos de nuevo, si la ejecución se reduce a unas 8 olas entonces se acaban creando conflictos a la hora de distribuir el trabajo desde el planificador a la Compute Unit. ¿El motivo? El procesador de comandos puede enviar una ola por ciclo de reloj a las Compute Units pero si estas estan ocupadas pues no necesitan que les envien nuevos datos y lo importante es que estén ocupadas. ¿Que ocurre cuando tenemos acortamos el tiempo en el que trabajan las Compute Units colocando olas más grandes en cada envio? Imaginemos que trabajamos con unos 64 registros por ola, esto significa terminar en 4 ciclos con una ocupación del 100% por lo que si tenemos 5 o más Compute Units habrá un conflicto si tenemos un solo procesador de comandos a la hora de distribuir las tareas desde el planificador independientemente de la etapa del pipeline en la que nos encontremos, por lo que es necesario ocupar las Compute Units de más con algo fuera del pipeline gráfico y es ahí donde entra el pipeline de computación, para aprovechar las Compute Units no utilizadas durante la generacion del proceso gráfico.

Obviamente tendremos que mirar la cantidad de Compute Units que tengamos disponibles en el sistema y los desarrolladores han de tener la suficiente habilidad para saber hacía donde y para que destinan los recursos de las Compute Units eligiendo bien el número de registros utilizado en cada ciclo por la unidad SIMD. ¿Lo que resulta más particular en el pipeline gráfico? La etapa del Fragment/Pixel Shader, pero como ya he dicho este lo vamos a dejar para más adelante, esta entrada, pero más adelante.

L1 TA/TD: Cada una de las Compute Units tienen unas 4 unidades de filtraje de texturas con 4 unidades de acceso cada una (16 unidades) a unos 16KB de Cache que tienen acceso directo y cableado a la Cache L2 de la GPU (la cual se encuentra fuera de la Compute Unit y del Shader Engine).

El primer trabajo de la unidad es lo que llamamos el filtraje de texturas, por el ancho de banda que tiene con la GPU puede realizar filtro bilineal sin perdidas de un ciclo dado su ancho de banda. En todo caso las versiones más nuevas pueden realizar anisotropico de un filtro y es entendible desde el momento en que se tiene acceso a unos 16 datos simultaneos de la Cache L1 TA/TD, AMD no ha dado datos de como en sistemas como PS4 Pro y Xbox One existe esta mejora sustancial pero supongo que es porque lo que han hecho es mejorar el cableado de esta parte en las CUs más nuevas para conseguir que cada unidad de texturas tenga los valores de referencia de 16 pixeles y no de 4 solamente.

El trabajo de las unidades TA/TD es además descomprimir los valores de las texturas al vuelo para que la unidad SIMD no gaste un tiempo valioso en descomprimir los datos. Esta operación añade un retardo a la hora de enviar los datos a los registros de la unidad SIMD por lo que es importante tener en cuenta que si no es necesario que haya compresión de texturas o esta empaña la calidad de imagen final pues… Lo mejor es descartarla.

Pero hemos de tener en cuenta que una cosa son los fragmentos rasterizados enviados por el rasterizador y otra muy distinta los valores de textura de estos. El Rasterizador (dentro de cada Shader Engine tenemos uno) envia por ciclo de reloj una ola de 64 elementos que representa un poligono rasterizado de un tamaño de 8×8 pixeles o en su defecto un area de la escena de 8×8 pixeles. Esto son unos 64 registros en total y por tanto unos 64 valores pero nos encontramos con que las unidades de texturas solo pueden enviar unos 16 valores en total por ciclo de reloj y es ahí donde esta la trampa, el Fragmento se divide en 4 bloques de 4×4 cada uno que se reparten a cada unidad SIMD para operar con ellos.

Ahora bien, dependiendo de la cantidad de atributos por textura/pasos la cantidad de etapas variaria y esta esta directamente relacionada con la cantidad de ciclos que hemos comentado antes y que trabajaría la Compute Unit. En teoría el limite deberían ser los 16 ciclos pero nos encontramos con que el limite son los 10 ciclos y tampoco es recomendable por lo que para poder llegar a los 16 ciclos/atributos lo que se hace es operar 8 primero, almacenar el resultado en la Local Data Share y recuperarlos posteriormente en otra tanda.

¿Y que ocurre una vez se han procesado los pixeles correspondientes y tenemos el valor final para generar el búfer de imagen?

SX: La unidad SX es la unidad encargada de comunicar la Compute Unit o mejor dicho las ALUs de la Compute Unit con dos elementos distintos:

  • Cache L2 en el caso del pipeline de computación.
  • RBE/ROPS en el caso del pipeline gráfico.

La unidad SX lo único que hace es exportar los datos del último resultado que se encuentra en la VGPR. Para que la gente lo entienda dado que las unidades SIMD carecen de instrucciones de manejo de memoria en si mismas cuando hacemos una invocación de estas a través de la API es la unidad SX quien extrae lo valores de los VGPR y los lleva a los RBE/ROPS para que generen el búfer de imagen cuando hablamos del pipeline gráfico. En el caso de las unidades más modernas en consolas (PS4 Pro y Xbox One X) se ha añadido lo que se le llama Delta Color Compression que es basicamente la capacidad de comprimir de nuevo los valores de color de los pixeles para minimizar el ancho de banda y para que en el caso de tener que recuperar los datos a posteriori estos no tengán que recuperarse utilizando todo el ancho de banda al no estar los datos comprimidos, pero es una habilidad poco utilizada en las consolas actuales desde el momento en que es una función no disponible en los modelos estandar de PS4 y Xbox One.

Su relacion con los Pixel/Fragment Shaders es importante porque son tipos de Shaders que suelen estar muy asociados directamente a la ratio de los ROPS/RBE en cada momento y estos se encuentran muy atados a la resolución y al mismo tiempo se encuentran atados a la interfaz de memoria.

rbe480x

Es decir, da igual la cantidad de pixeles que podamos estar procesando en la unidad de texturas para generar los pixeles finales que el cuello de botella serán siempre los RBE/ROPS. De ahí a que muchos desarrolladores hayan tirado de los Compute Shaders para la realizacion de efectos de post-procesado. ¿El motivo? Los Compute Shaders pueden continuar en el mismo punto en los VGPR desde donde lo había dejado el pipeline gráfico sin que tenga que pasar por la unidad SX y hacer el camino hacía la memoria o en su defecto se utiliza la Local data Share como punto de coordinación.

Y con esto termino esta tediosa entrada que me va a servir como referencia a futuras entradas. Si alguna duda en los comentarios, sinceramente estas entradas tan abstractas me agotan mentalmente porque son mecanismos sumamemente complejos. En todo caso era una entrada que hacía meses por no decir años que la tenía en los borradores y no conseguia terminar nunca.