Cogemos de nuevo el Delorean para ir a la siguiente generación

Pues así es… el viaje será largo… Y espero que al final el concepto que quiero introducir en esta entrada quede claro.

delorean

Este texto es la combinación de varios borradores que buscaban responder varios temas que han ido saliendo en los comentarios del blog así como en algún correo electrónico que me habéis enviado, es una entrada meramente especulativa pero partiendo de información sobre como puede ser una GPU de la siguiente generación teniendo en cuenta hacía donde puede evolucionar la cosa y hacía donde parece que va a evolucionar.

Estamos viendo como en los últimos años se están vendiendo televisores 4K por lo que su parque de usuarios aumentará durante los años siguientes y con ello la demanda por juegos que sean renderizados a dicha resolución:

4k-resolution-on-eyes

Estamos hablando de una resolución que mueve cuatro veces mas pixeles, esto significa que de entrada deberíamos tener una GPU con una cantidad de ROPS que sea cuatro veces superior a la generación actual si la consola esta pensada para 1080P (PS4) y unas 6 veces si esta pensada para 900p (Xbox One), en el primer caso nos iríamos a los 128 ROPS, en el segundo caso a los 96 ROPS en total.

Si aumentas la resolución de pantalla y con ello el número de ROPS tienes que aumentar también en el mismo grado las instancias del Pixel Shader. Esto significa que tienes que hacer lo mismo con  los Stream Processors, el ancho de banda y las unidades de texturas también. Aumentar los ROPS sin un aumento del mismo sentido en las otras partes no es suficiente a no ser que disminuyas la cantidad de operaciones que se pueden realizar por pixel que se tengan como objetivo. Pero la pregunta clave es… ¿Se puede hacer evolucionar la GPU más allá de lo que tenemos en estos momentos? La realidad es que si y después de darle un poco a la cabeza he llegado a la conclusión de como podría ser la siguiente generación.

Existe una relación directa entre la resolución de pantalla el número de ROPS  (Raster OutPut) de la GPU ya que estos son los que escriben en memoria y de ellos depende la tasa de relleno de la GPU y la resolución de esta. Son máquinas de estado y no son programables por lo que no tienen un tipo de shader asignado en el pipeline gráfico con el que lo desarrolladores puedan manipular el resultado final.

219042

Las partes marcadas en azul en este diagrama son el Input Assembler (Procesador de Comandos y Unidades de Planificación), el Rasterizador y el Output Merger que son los ROPS, estas son las tres etapas clásicas de una GPU que no son programables pero realizan tareas sumamente importantes. ¿Pero que ocurre con el Compute Shader? Si os fijáis esta completamente ajeno al resto, el motivo de ello es que se trata de un tipo de shader que es agnóstico al pipeline gráfico convencional y no utiliza los ROPS para escribir en memoria. Esto se ve bien también en un pipeline del OpenGL contemporáneo:

khronos-opengl-es-gdc-2014-7-638

Veamos la definición de los Compute Shaders:

Un Compute Shader es una Étapa Shader que es utilizada enteramente para computar información arbitraria. Mientras puede hacer renderizado, se utiliza normalmente para tareas no directamente relacionados con dibujar triángulos o pixeles.

Los Compute Shaders operan diferente a las otras etapas shader. Todas las etapas shader tienen una bien definida colección de valores de entrada, algunas incluidas de serie y otras definidas por el usuario. La frecuencia a la que la étapa del shader se ejecuta es especifiada por la naturaleza de dicha ésta, lo vertex shader se ejecutan una vez por verte de entrada. Los Fragment/Pixel Shaders son definidos por los fragmentos generados por el proceso de rasterización.

Cuando un polígono se esta procesando en las diferentes etapas  no tiene acceso a la memoria ya que el pipeline gráfico son varias unidades de proceso concatenadas entre si donde cada etapa coge los datos de la etapa anterior hasta llegar a los ROPS que escriben en la memoria el resultado de la operación, de ahí a que estén directamente atados al ancho de banda.

 

pipeline-v1-05

 

El Compute Shader evita todo el pipeline gráfico incluido los ROPS por lo que este suele necesitar un camino de datos alternativo. Ayer en la entrada Twenty Two NX (II) puse el diagrama de los SoC/GPU de AMD, lo que incluye Xbox One, PS4 y las APU/SoC de AMD de dicha compañía:

AMDUncore

¿Como es que las GPUs en PS4 y Xbox One tienen dos caminos distintos a memoria? Simple, uno es para gráficos y utiliza los ROPS para escribir en memoria. El otro utiliza él Fusión Compute Link/Onion y está conectado al Northbridge del sistema y para sus operaciones no utiliza los ROPS que se encuentran conectados en el otro camino a la memoria. ¿El motivo? Los Compute Shaders se utilizan en el segundo camino de datos ya que están pensados para prescindir de los ROPS en sus operaciones, no hay que olvidar que el Compute Shader y esta pensado para operaciones con la GPU fuera del renderizado de gráficos en tiempo real. Dado que no se encontraba en las GPUs de la anterior generación y dado que ahora estas son multicontexto es en esta generación donde los Compute Shaders empiezan a tomar forma. Mientras que los Vertex, Geómetry y Pixel/Fragment están pensados para un determinado tipo de datos, la particularidad principal de los Compute Shader es que no sólo están pensados para operar con cualquier tipo de datos y esto es importante ya que un dato puede ser un simple entero como una compleja estructura de datos,

¿Pero cual es el problema? Los Shaders utilizan un tipo de lenguaje de alto nivel llamado HLSL/GLSL o cualquier “Shading Language” que se os ocurra, este lenguaje permite el desarrollo de los shaders independientemente de la arquitectura en la que se encuentren ejecutando pero llevan consigo una serie de limitaciones por las limitaciones del mismo lenguaje… ¿Que ocurriría si los Stream Processors los pudiésemos programar en un lenguaje de propósito general mucho más completo como por ejemplo C++?

c-the-complete-reference-the-complete-reference-english

Se que muchos han pensado en el C++ AMP de entrada…

images

 

Pero el C++ AMP esta pensado para el uso global de la GPU, no para el uso local de un shader concreto y el uso del C++ para programar los shaders no es algo que vaya a ser rechazado de pleno por los desarrolladores, sobretodo porque significaría un paso adelante y es un lenguaje ampliamente utilizado. No en vano, una de las cosas de las que he oído a mucha gente que aplauden del ya desaparecido Cell Broadband Engine era la capacidad de poder programar los Stream Processors (SPU) en C++ y la cantidad de memoria local que tenían para realizar sus operaciones. Y al fin al cabo el rendering desde los SPE es Tile Rendering por Software pero dado que solo podía trabajar con los datos previos y posteriores a la entereza del pipeline gráfico su utilidad se reducía en esas etapas y no se podían utilizar para los shaders convencionales por lo que su ventaja se reducía al per-procesado de la escena y al post-procesado de la escena.

¿Pero que se necesita para que los Stream Processors acaben por aceptar un lenguaje con C++? Simplemente su conjunto de instrucciones y conocimiento sobre su arquitectura interna con tal de poder realizar un compilador en un lenguaje de alto nivel, pero con la cantidad de arquitecturas gráficas que van apareciendo eso resulta un problema. Una cosa es que el Input Assembler (Procesador de Comandos) se haya univeralizado y otra muy distinta es que unidades shaders en si mismas lo hayan hecho. Es aquí donde entra un elemento llamado SPIR-V que ha sido propuesto formando parte del sucesor de OpenGL, Vulkan.

SPIR (Standard Portable Intermediate Representation) fue inicialmente desarrollado para ser utilizado por OpenCL y las versiones 1.2 y 2.0 se basaban en LLVM. SPIR ha evolucionado a un verdadero estándar a través de APIS que esta completamente definido por Khronos con soporte nativo para shaders y características de kernel llamado SPIR-V.

¿Que es LLVM? Veamos…

LLVM (anteriormente conocido como Low Level Virtual Machine, o Máquina Virtual de Nivel Bajo) es una infraestructura para desarrollar compiladores, escrita a su vez en el lenguaje de programación C++, que está diseñada para optimizar el tiempo de compilación, el tiempo de enlazado, el tiempo de ejecución y el “tiempo ocioso” en cualquier lenguaje de programación que el usuario quiera definir. Implementado originalmente para compilar C y C++, el diseño agnóstico de LLVM con respecto al lenguaje, y el éxito del proyecto han engendrado una amplia variedad de lenguajes, incluyendo Objective-CFortranAdaHaskellbytecode de JavaPythonRubyActionScriptGLSLClangRustGambas y otros.

El proyecto LLVM comenzó en 2000 en la Universidad de Illinois en Urbana-Champaign, bajo la dirección de Vikram Adve y Chris Lattner. LLVM fue desarrollado inicialmente bajo la Licencia de código abierto de la Universidad de Illinois, una licencia de tipo BSD. En 2005, Apple Inc. contrató a Lattner y formó un equipo para trabajar en el sistema de LLVM para varios usos dentro de los sistemas de desarrollo de Apple.1 LLVM es parte integrante de las últimas herramientas de desarrollo de Apple para Mac OS X e iOS.2

El nombre “LLVM” era en principio las iniciales de “Low Level Virtual Machine”, pero esta denominación causó una confusión ampliamente difundida, puesto que las máquinas virtuales son solo una de las muchas cosas que se pueden construir con LLVM. Cuando la extensión del proyecto se amplió incluso más, LLVM se convirtió en un proyecto paraguas que incluye una multiplicidad de otros compiladores y tecnologías de bajo nivel, haciendo el nombre aún menos adecuado. Por tanto, el proyecto abandonó3 las iniciales. Actualmente, LLVM es una “marca” que se aplica al proyecto paraguas, la representación intermedia LLVM, el depurador LLVM, la biblioteca estándar de C++ definida por LLVM, etc…

Es decir, SPIR es LLVM ejecutandose en los shaders de una GPU en vez de hacerlo en las ALUs de una CPU por lo que permite el uso de lenguajes que van más allá de los “Shading Languages”.
spir-v_in_out
¿Que ocurre cuando realizas los shaders en C++? Pues entonces tienes un lenguaje de proposito general y volvemos al “rendering por software” pero desde la GPU, es decir… Vamos más allá de los Compute Shaders. ¿Y para que esto? Pues simple y llanamente porque C++ es mucho más eficiente que el HLSL con las estructuras de datos complejas. Pero al mismo tiempo esto lleva consigo una dilema que nos da dos caminos distintos a la hora de diseñar el camino de datos de la siguiente generación.
La primera es mantener el paradigma que hay ahora mismo basado en dos caminos de datos: Uno coherente y el otro No-Coherente, el segundo es tener un único camino de datos completamente coherente y es en el segundo caso donde podemos utilizar el paradigma “Todo es un Compute Shader” o mejor aún, todo se puede programar en cualquier lenguaje de propósito general pero este escenario significaría prescindir de los ROPS a la hora de escribir en memoria… ¿Como? Pues trasladando a los shaders la operación final de los ROPS y esto es algo que ya se puede realizar con los Compute Shaders. ¿Un ejemplo de ello? Imaginad que tenéis que escribir el resultado de una operación en memoria y luego recuperarlo para la siguiente… ¿No sería mejor escribirlo en la cache para recuperar el dato a continuación? El ancho de banda que otorgan las caches de una GPU y su menor latencia son una ventaja para ciertas operaciones… Claro esta que la cache en este caso es una memoria muy pequeña… ¿Como prescindimos de los ROPS definitivamente?  Pues por Tile Rendering por software y es aquí donde volvemos al paradigma de los SPE del CBEA donde estos tenían una memoria local lo suficientemente rápida como para poder trabajar con lo datos sin problemas, pero como he dicho antes tenían el problema de que solo tenían acceso al pipeline gráfico antes del Input Assembler y después del Output Merger, no a ninguna etapa intermedia, claro esta que esto de lo que estoy hablando nos lleva a otro concepto, pensado para la computación cientifica en paralelo, los llamados Knight… originalmente conocidos como Larrabee de Intel y también como Xeon Phi.
Xeon-Phi-2-640x353_original
La idea es la de utilizar núcleos x86 con una extensa unidad vectorial como stream processors.
under-the-armor-of-knights-corner-intel-mic-architecture-at-hotchips-2012-7-728
Pero no estoy hablando de utilizar ninguno de estos procesadores de Intel en concreto, lo que ocurre es que hace unos años Intel los propuso como una alternativa a las GPU y acabo fracasando por completo… ¿El motivo? Lo podéis encontrar aquí:

La planificación de tareas se realiza enteramente por software en Larrabee, en vez de de que lo haga una lógica de función fija.

Imaginaos el hecho de no tener el Input Assembler y tener que gestionar manualmente todos los hilos de ejecución y contextos de la GPU… quitando potencia a la misma GPU en vez de utilizar un elemento que realiza esa tarea de forma transparente y eficiente, este fue el principal motivo por el cual los programadores huyeron de este paradigma pero no es el único.
Algunas operaciones que las GPUs tradicionales realizan con lógica de función fija como la rasterización y el post-shader blending (Se refiere a los ROPS), son realizadas enteramente por software en Larrabee.
No solo no tiene el Input Assembler, sino que tampoco tiene el rasterizador y los ROPS, es decir… carece de toda la función fija esencial para el renderizado de una escena en 3D. Pero estamos comentando un escenario donde poder utilizar los “Compute Shaders” para renderizar y que estos se programen en C++, no un escenario donde la función fija de las GPUs que es indispensable desaparezca. Claro esta que el concepto de rendering por software puede recordar al CBEA y al Larrabee, pero la idea no es un retorno a esos errores/horrores de arquitecturas que son menos eficientes que una GPU, sino el hecho de poder utilizar lenguajes de propósito general para programar los shaders de la GPU como quien programa el núcleo de una CPU.
La diferencia entre el Larrabee y el CBEA es el tema de la memoria, en cada SPE del CBEA había una memoria local que era el único espacio al que tenía acceso el SPE, este es el paradigma que queremos  para el stream profesor/unidad shader de la siguiente generación. Supongamos que cogemos como base los Compute Shaders de la arquitectura GCN.
GCN_CU
Dejemos la cache de primer nivel pero hagamos cambios en el esquema de memoria, el cual es el siguiente:
GCN-CU
Por lo visto podemos leer y escribir datos en la cache de segundo nivel de la GPU… ¿Que permite esto? Pues el hecho de poder utilizar ese espacio de memoria para ciertas operaciones concretas, pero esta cache L2 tiene un problema, es coherente entre todo el procesador y no sirve para el “Tile Rendering por software” que es lo que permitiría eliminar los ROPS de la ecuación.
gs4106-the-amd-gcn-architecture-a-crash-course-by-layla-mah-46-638
Imaginaos que a cada CU se le coloca una cache de segundo nivel local, esta almacena el “Tile” con el que esta operando en ese mismo momento y hace la función de búfer de imagen desde el que se dibuja la escena. Lo que significa llevar las ventajas no solo del Tile Rendering convencional sino que si esto lo combinamos con el uso de lenguajes como C++ puede salir algo realmente grande en cuanto a rendimiento… ¿Pero que ocurre con la cache L2 coherente? Pues que esta pasaría a ser una cache L3 pero teniendo en cuenta la utilidad que tienen en estos momentos en la actual generación.
PRT1-1
La memoria de la GPU donde los Tiles de las texturas se encuentran es la cache L2, pero imaginaos por un momento que tenemos una cache L3 con la misma funcionalidad y que es coherente de cara a toda la GPU pero a cambio tiene una densidad mucho mayor y por tanto puede almacenar datos mucho más grandes. Como un atlas de texturas completo o incluso de cara a otras funciones que utilizan los PRT.
Axxtgrx
¿Por qué no hace una “cache L3” donde almacenar no solo un Atlas sino también un Octree en su interior? Y la mejor solución para ello es el uso de memoria embebida pero no en el chip, sino una solución que utilice memoria embebida en el mismo encapsulado.
igp-intel-bh
Dicha memoria intermedia no sería una cache exactamente sino una memoria local del procesador que serviría para hacer el mismo trabajo que la cache L2 de los GCN actuales pero sin las limitaciones de densidad de esta. Dicha cache intermedia serviría también para almacenar el búfer frontal aparte de los Octree y el Atlas de texturas de la escena por lo que terminaría también con la contención CPU-GPU en lo que a los datos se refiere.
La cache L2 de esta nueva GPU y la memoria local externa no serían accesibles por la CPU por lo que utilizarían el camino de datos no-coherente, seguiría habiendo un camino de datos coherente con la CPU para tareas de co-procesamiento pero el esquema es que dicha memoria local externa en el mismo chip se utilice para tareas gráficas. Por otro lado es obvió que las texturas se mantendrían en la memoria principal, pero el uso del virtual texturing y la creación de un atlas de texturas en dicha memoria local hará que la GPU no accede a la memoria principal para las texturas del fotograma sino al atlas de texturas de la escena.
Xarman en uno de los comentarios recientes ha mencionado una tecnología llamada Granite utilizada en el Unreal Engine 4 como plug-in, sorprende enormemente lo siguiente:
One major benefit from the use of virtual texturing is that you can use much larger textures, even up to 256Kx256K. A texture of that size compressed with DXT1 would occupy 32GB of memory, and loading that texture would take minutes. With Granite, you need only 32MB of VRAM for that texture, and the loading time is unnoticeable.
Xarman en su comentario dice lo siguiente:
y como predijo carmack en el 2000 llega un punto en que no hace falta mas vram si se usa esto.
¿A que se refiere? A las siguientes palabras de John Carmack escritas por él en el año 2000:

Los problemas con las texturas grandes se pueden solucionar simplemente no usando grandes textuas. Tanto las perdidas, como los texels no referenciados pueder ser reducidos recortando todo a texturas de 64×64 o 128×128. Esto requiere preprocesado, añade geometria, y requiere una superposición desordenada de las texturas para ajustar las costuras entre estas.

Actualmente es posible hacer una estimación de cuales son los niveles de Mip Map necesarios  y solamente intercambiar esos. Una aplicación no puede calcular exactamente los niveles de Mip Map que serán referenciados por el hardware, debido a ello hay unas pequeñas variaciones entre chips y el calculo de la pendiente puede acarrear una importante sobrecarga en el procesamiento. Un limite superior conservativo se puede tomar mirando la distancia mínima normal de cualquier referencia al vértice e una textura dada en un foograma. Esto sobre-etimaria las texturas necesarias en un 2X y aún dejaría un gran impacto cuando se cargase el nivel superior del Mip Map para grandes texturas, pero puede permitir el escenario de escenas estilo gran catedral sin que existe un intercambio.

Los programadores inteligentes siempre pueden trabajar duro para sobrepasar los obstaculos, pero en este caso, hay una clara solución por hardware que simplemente da más rendimiento que otra cosa posible por software y nos hace la vida más fácil a cada uno: virtualizar la visión que tiene la tarjeta de su memoria virtual.

Con páginas de tablas, la fragmentación del direccionamiento no es un problema, y con el rasterizador gráfico teniendo que cargar de una página cuando el bloque exacto de 4KB es necesario, los problemas de los niveles de los mip maps y de las texturas ocultas simplemente desaparecen. No se tiene que hacer nada furtivo por parte de la aplicación o el controlador, solo manejar los indices de la páginas.

Los requisitos de hardware no son muy pesados. Necesitas búfers de traducción (TLB) en la tarjeta gráfica, la habilidad de cargar automaticamente el TLB desde las paginas de tablas en la memoria local, y la habilidad de mover una página a través del AGP o el PCI a la memoria gráfica y actualizar las páginas de tablas y el contador de referencia. No necesitas incluso tener varios TLB, debido a que los patrones de acceso no van saltando po toda la memoria coo puede la CPU. Incluso con un solo TLB por cada unidad de texturas, las recargas contarían solamente 1/32 del acceso a memoria si las texturas fueran bloques de 4KB. Todo lo que quieres es que el limite superior fuera un TLB lo suficientemente grande para que cada textura cubra los texels referenciados en el rasterizado típico por scanline.

Algunos desarroladores dirán “No quiero que el sistema maneje las texturas, quiero un control total” Hay un par de respuestas a ello, primero el manejo por páginas tiene la flexibilidad que tu no puedes obtener por un esquema por software, por lo que tiene nuevas capacidades. Segundo, aún puedes continuar tratando como si se tratara de un búfer de texturas fijo y manejarlo tu mimo con las actualizaciones. Terceros, incluso si esto FUERA más lento que el esquema por software más astuto posible (lo cual dudo seriamente), se intercambiara tiempo de desarrollo por algo que es en teoría más eficiente y más rápido. ¡Ya no codificamos las superposiciones en lenguaje ensamblador!

Algunos diseñadores de hardware dirán algo como que el motor gráfico esta a la espera mientras estas obteniendo obteniendo datos de una página desde el AGP. Seguro, siempre será mejor tener suficiente espacio para texturas y no tener que hacer siempre el intercambio, y esta caractéristica no os permitiría hablar más de megapixeles o millones de triángulos, pero cada tarjeta termina por no tener suficiente memoria en un determinado punto. Ignorar estos casos del mundo real no ayuda a vuestros clientes. En cualquier caso, la espera infernal de esto es mucho menos que si estas cargando la textura entera desde el FIFO de comandos.

Se supone que 3Dlabs tendrá alguna forma de manejo virtual de memoria en el permedia 3, no estoy familiarizado con los detalles (¡sí alguien de 3D labs puede enviarme las últimas especificaciones registradas, lo apreciare!).

Por aquella época 3D Labs tenía en desarrollo una tarjeta gráfica llamada P10.

Más allá de la implementación por hardware del bus de memoria, 3Dlabs cree que el sistema de “Memoria Virtual” empleado en el P10 tiene mucho más significado e impacto potencial en el mercado de las 3D, de hecho, es algo que John Carmack de id ha estado pidiendo en el hardware durante un largo tiempo. El concepto de la Memoria Virtual es muy parecido al sistema de memoria usado en las CPU: elimina las barreras entre los diferentes subsistemas de memoria en el PC, como el búfer de imagen local, la RAM principal o incluso el espacio del disco duro, y permite al procesador 3 acceder a ellos libremente.

virtualmemory

En el sistema de Memoria Virtual del P10 hay un espacio de direccionamiento lógico de hasta 16GB que esta completamente dividido en páginas de 4KB. La RAM en la tarjeta se convierte esencialmente en una enorme cache L2 para el chip, un sistema el cual es fácil de entender para los compiladores.

Y volviendo a lo que dijo Carmack:

La DRAM embebida debería ser una fuerza conductora. Es posible colocar una gran cantidad de megabytes de alto ancho de banda en un chip con un controlador de video, peor no será posible (por el momento) colocar los 64MB de una GeForce ahí.  Con el texturizado virtualizado, la presión sobre la memoria se ve drásticamente reducida. Incluso con una tarjeta de 8MB sería suficiente para juego a 16 bits y 1024×768 o 32 bits y 800×600, no importa cual sea la carga de texturas.

DRAM embebida pero en el mismo encapsulado=la solución. Pensad que esto se escribió en el año 2000 pero el concepto es justamente el mismo. Es más, se hace más posible gracias al uso de la memoria HBM para esta tarea.

En fin, esto es todo… Si alguna duda a los comentarios.

Anuncios