Red de conocimiento informático - Aprendizaje de programación - Arquitectura de memoria GPU y procesador de comandos de Graphics Pipeline 2011

Arquitectura de memoria GPU y procesador de comandos de Graphics Pipeline 2011

Enlace original

Como se mencionó anteriormente, en la PC, las diversas etapas por las que pasan las instrucciones de renderizado 3D antes de pasar a la GPU. El contenido del procesador de comandos es demasiado largo y. solo ocupa Ahora que hemos terminado, en este artículo comenzaremos con una introducción un poco más detallada a esta parte del contenido. Por supuesto, debido al espacio limitado, no hay forma de cubrir completamente todos los detalles.

Todo el proceso de procesamiento del Command Buffer está relacionado con la memoria, ya sea memoria del sistema a la que se accede a través del bus PCI o memoria de video local, etc. Por lo tanto, si queremos indicar el contenido en el orden de En la canalización, es necesario introducir algún contenido relacionado con la memoria antes de introducir el procesador de comandos.

Dado que el subsistema de memoria de la GPU está diseñado para fines especiales, se diferencia del subsistema de memoria convencional de la CPU u otro hardware. Hay dos diferencias principales:

De hecho. , el aumento significativo en el ancho de banda de la GPU se produce a costa de una mayor latencia. Esto también se basa en las necesidades reales. La GPU concede más importancia al rendimiento que a la latencia. No importa si hay un retraso, podemos hacer otra cosa.

Las GPU no tienen el subsistema de memoria habitual; es diferente de lo que se ve en las CPU de uso general u otro hardware, porque está diseñado para patrones de uso muy diferentes. Hay dos formas fundamentales en las que se utiliza la memoria de una GPU. El subsistema difiere de lo que se ve en una máquina normal:

Esto es todo lo que necesitamos saber sobre la memoria GPU, y hay otra cosa importante sobre la DRAM (memoria dinámica de acceso aleatorio): los chips DRAM están organizados. en forma de cuadrícula 2D, tanto física como lógicamente. La cuadrícula significa que hay filas y columnas de líneas horizontales y verticales, y donde estas líneas se cruzan hay un transistor y un condensador. El punto clave aquí es que la dirección de una determinada ubicación en la DRAM en realidad se divide en direcciones de fila y direcciones de columna. Cuando la DRAM lee y escribe, en realidad leerá los datos del contenido de todas las columnas en una determinada fila (línea de caché). , línea de caché). Esto significa que si los datos que desea leer están en la misma fila de la DRAM, la velocidad de acceso es mucho mayor que si no están en la misma fila. En la actualidad, esta conclusión parece no tener ningún efecto, pero el contenido que se presentará más adelante resaltará gradualmente la importancia de este punto si utilizamos los datos de GPU/CPU presentados anteriormente para ilustrar, es decir, si solo leemos la memoria. Es difícil lograr el valor máximo mencionado anteriormente con un número limitado de bytes. Por ejemplo, si desea acceder a la memoria con el ancho de banda completo, entonces el contenido al que se accede al mismo tiempo debe corresponder preferiblemente a una fila completa en la DRAM.

Desde la perspectiva de un programador de gráficos, el contenido del hardware PCIe parece poco interesante y, de hecho, la arquitectura del hardware GPU tampoco lo es. Sin embargo, cuando el programa de gráficos se ejecuta con lentitud, tenemos que hacer de tripas corazón y comprender la implementación subyacente para poder localizar el cuello de botella y luego buscar estudiantes profesionales que nos ayuden a resolver el problema.

De lo contrario, puede conducir a la siguiente situación: la CPU accede directamente a la memoria de visualización y a los registros en la GPU, mientras que la GPU accede directamente a la memoria principal de la CPU Más tarde, debido al retraso de acceso ultra alto entre los dos (porque. es acceso a datos entre chips), cuando el programa se está ejecutando, tomó aproximadamente una semana terminar un cuadro (risas). El valor máximo del ancho de banda de la memoria de 8 GB/s es en realidad un valor teórico, que corresponde al ancho de banda total de una conexión PCIe 2.0 de 16 vías. Sin embargo, el valor de ejecución real es aproximadamente la mitad o un tercio de este valor. Balanzas numéricas disponibles. A diferencia de estándares anteriores como AGP, la GPU actual es una conexión simétrica punto a punto, es decir, el valor del ancho de banda se refiere al valor bidireccional, el estándar AGP es asimétrico y el ancho de banda de transmisión de la CPU a la GPU. es mayor que al revés.

En cuanto a la memoria, hay una cosa más que es necesario explicar claramente. Ahora tenemos dos tipos de memoria, memoria de video local y memoria del sistema mapeada. Uno de ellos tarda un día en llegar al Polo Norte, mientras que el otro tarda una semana en llegar al Polo Sur a través del bus PCI. ¿Qué camino elegirás?

La solución más sencilla: crear una línea de dirección adicional para indicarnos qué camino tomar. Esta solución es muy sencilla, pero ha sido verificada muchas veces y es muy eficaz. Si nuestro hardware (como algunas consolas de juegos o teléfonos móviles) adopta una arquitectura de memoria unificada, entonces no tenemos más opción que una sola memoria, por lo que solo hay un camino por recorrer. Si desea hacer las cosas más refinadas, puede considerar agregar una MMU (unidad de administración de memoria) para asignar un espacio de direcciones virtuales, lo que le permite realizar algunos trucos, como mover algunos recursos de textura a los que se accede con frecuencia y colocarlos en la memoria de video ( porque es más rápido), mientras que otros recursos se colocan en la memoria del sistema. La mayoría de los recursos restantes no se asignan directamente y se encuentran en el disco duro (se leen desde el disco duro cuando es necesario. Por supuesto, esto es muy lento. Si el el tiempo de acceso a la memoria se extiende a un día, luego la lectura desde el disco duro HD tardará casi 50 años).

Una MMU permite la desfragmentación de la memoria de vídeo sin realizar una copia real cuando la memoria de vídeo es insuficiente. Además, puede facilitar que múltiples procesos compartan la GPU. MMU es necesaria, pero no estoy seguro de si todas las GPU tienen tal cosa.

Además, existe un motor DMA (Direct Memory Access) que puede realizar copias de memoria sin ocupar hardware 3D/Shader Cores. En términos generales, esto se usa para copiar entre la memoria del sistema y la memoria de video (bidireccional), pero de hecho también se puede usar para copiar entre memorias de video (esta función es muy útil si desea realizar una desfragmentación del disco VRAM), pero no puede. Se utiliza para copiar entre memorias del sistema (debido a que esta es una unidad funcional en la GPU, si necesita copiar la memoria del sistema, simplemente hágalo directamente en la CPU, no la transfiera específicamente a la GPU a través de PCIe, esto es demasiado estúpido) .

Actualización: aquí hay un diagrama para brindar más detalles: la GPU tiene múltiples controladores de memoria, cada controlador controla múltiples bancos de memoria, esto es a través de uno más grueso en el frente Completado por el concentrador

Aquí hay un resumen de todo el contenido. Tenemos un búfer de comando en la CPU y una interfaz de host PCIe (la interfaz de host PCIe se comunica con la GPU a través de la interfaz de host y escribe su dirección en el registro).

Luego, existe la lógica correspondiente en la GPU para leer los datos de esta dirección a través de una instrucción de carga: si es la memoria del sistema, entonces los datos se transmitirán a través de PCIe, y si colocamos el búfer de comando en la memoria de video, entonces KMD Configure directamente una transferencia DMA para la transmisión de datos. Independientemente de la situación anterior, no es necesario consumir recursos de la CPU ni consumir recursos del núcleo del sombreador en la GPU. Entonces podremos obtener una copia de los datos transferidos en la memoria de video. Básicamente se ha abierto todo el camino. Comencemos con la introducción de comandos.

Como se mencionó anteriormente, la situación actual de la GPU es de alto ancho de banda y alta latencia. Una solución a esta situación es ejecutar una gran cantidad de subprocesos independientes. Sin embargo, dado que solo tenemos un búfer de comando, cada subproceso necesita leer las instrucciones del búfer en orden (debido a que las instrucciones de cambio de estado y representación contenidas en el búfer de comando deben ejecutarse en el orden correcto), por lo que el que se proporciona aquí es mejor La solución es establecer un búfer lo suficientemente grande y obtener las instrucciones que deben ejecutarse con anticipación de acuerdo con un lapso mayor para evitar contratiempos (un aumento en el consumo de rendimiento).

A partir de este búfer, ingresa oficialmente a la etapa de procesamiento de comandos, que es esencialmente una máquina de estado que analiza el comando de acuerdo con el formato especificado por el hardware. Algunos comandos se utilizan para procesar operaciones relacionadas con la representación 2D (a menos que se configure un procesador de comandos separado especialmente para transacciones 2D, en cuyo caso el procesador de comandos 3D no tendrá ninguna intersección con este conjunto de comandos), sin importar qué tipo de La situación, incluso en las GPU modernas, es que todavía hay hardware de comando 2D dedicado oculto, como uno en este conjunto de moldes para manejar el modo texto (modo fuente), modos de plano de bits de 4 bits/píxeles, desplazamiento suave y otros. cosas similares como el chip VGA. Algunos comandos transferirán datos del parche a la canalización del sombreador 3D, que se analizará en detalle más adelante; algunos comandos ingresarán a la canalización del sombreador 3D, pero no realizarán ningún procesamiento de renderizado (hay muchas situaciones que se analizarán más adelante). Algunos comandos se utilizan para implementar el cambio de estado. Desde la perspectiva de un programador, el cambio de estado puede considerarse directamente como una modificación de variable y la lógica de implementación es similar. Sin embargo, dado que la GPU es una calculadora de procesamiento paralelo a gran escala, no se pueden simplemente modificar las variables globales en un sistema paralelo y esperar que no ocurran problemas. Existen muchas soluciones de implementación comúnmente utilizadas para el cambio de estado, y básicamente todas El chip elegirá. diferentes soluciones de implementación según diferentes estados:

Como puede ver en lo anterior, a nivel de aplicación, parece una operación simple como modificar un parámetro variable, pero de hecho, requiere mucho procesamiento para evitar Reducción del consumo de rendimiento.

La última parte de las instrucciones que se presentarán son instrucciones que se utilizan específicamente para manejar la sincronización entre CPU/GPU.

En términos generales, el formato de este tipo de instrucción es "Si ocurre el evento X, ejecute la lógica Y". Aquí primero presentamos la parte de ejecución de la lógica Y.

Para Y, hay dos opciones de ejecución: la primera es el modelo push, en el que la GPU enviará activamente instrucciones para decirle a la CPU qué hacer ("¡Oi! ¡CPU! Estoy ingresando al intervalo de borrado vertical en la pantalla 0 ahora mismo". , así que si quieres voltear los buffers sin romperlos, ¡este sería el momento de hacerlo!"); el segundo es extraer el modelo, la GPU registra cierta información clave y luego la CPU la envía en el momento apropiado. El mensaje obtiene el estado de los datos relevantes ("Digamos, GPU, ¿cuál fue el fragmento de búfer de comando más reciente que comenzó a procesar?" - "Déjeme verificar... ID de secuencia 303"). El primero generalmente se ejecuta de forma interrumpida, porque la interrupción tiene un alto consumo, por lo que se utiliza principalmente para manejar algunos eventos poco comunes de alta prioridad. La implementación de este último requiere el uso de algunos registros de GPU visibles para la CPU y un método para escribir datos desde el búfer de comando en el registro; cuando ocurre un evento.

Por ejemplo, tenemos 16 de estos registros aquí y luego escribimos currentCommandBufferSeqIdd para registrar 0. Luego asigne un número de secuencia a cada búfer de comando enviado a la GPU (KMD) y agregue una lógica de procesamiento al comienzo de cada búfer de comando: cuando se alcance una determinada posición de este búfer de comando, comience a escribir datos para registrar 0. Entonces, ahora sabemos qué búfer de comando está procesando actualmente la GPU y el procesador de comandos procesa los comandos estrictamente en orden, es decir, si el número de serie del primer comando en el búfer de comandos es 303, entonces todos los comandos anteriores, incluido 302, tendrán Todos ejecutado, por lo que el espacio correspondiente a estas instrucciones podrá ser recuperado para otros fines.

Echemos un vistazo al evento desencadenante X. "Alcanzar una determinada ubicación en este búfer de comandos" mencionado anteriormente es en realidad un evento que también incluye "antes de alcanzar una determinada posición en el búfer de comandos, todos los sombreadores tienen completó todas las operaciones de lectura de texturas del lote" (usado para liberar todas las texturas y la memoria RT en un punto determinado), "todo activo El proceso de renderizado de RT/UAV ha finalizado" (usado para garantizar si la textura a la que se necesita acceder actualmente es válido) y así sucesivamente.

Estas operaciones son las que solemos llamar “vallas”. Hay muchas formas de seleccionar el valor que se escribirá en el registro de estado, pero el autor del artículo original cree que la única sólida es usar un contador secuencial para completarlo, pero no mencionó el motivo específico (se dice (Puede que esté escrito en otros blogs además de esta serie) )

Hasta ahora, se ha introducido el mecanismo de sincronización de GPU a CPU, pero todavía falta este contenido. del mecanismo de sincronización de datos dentro de la GPU (computación paralela). Siguiendo usando el ejemplo de RT anterior para ilustrar, RT solo se puede usar como textura cuando se completan todas las operaciones de renderizado (además de algunos otros pasos de procesamiento). En realidad, esto corresponde a una instrucción de tipo espera: espere hasta que el valor en el registro M sea igual a N (también pueden ser otras instrucciones, como instrucciones de comparación, etc.).

En este caso, puede asegurarse de que se complete la sincronización RT antes de enviar un nuevo lote y también puede crear una operación de descarga de GPU pura. La sincronización de datos entre GPU ya está completa. Antes de la introducción de otro mecanismo de sincronización mejor en Compute Shader de DX11, este mecanismo de sincronización era el único mecanismo de sincronización en la GPU, que era suficiente para el renderizado normal.

Además, si la operación de escritura en el registro de la GPU se puede realizar en la CPU, entonces, de acuerdo con el mismo método, también se puede realizar la sincronización de la CPU a la GPU: la CPU envía una copia parcial. Búfer de comando, que contiene la operación de espera para un valor específico. Cuando se cumple la condición de espera, la CPU escribe el valor específico en el registro de la GPU. Este conjunto de lógica se puede utilizar para implementar un proceso de renderizado multiproceso estilo D3D11, en el que se puede enviar un lote a la GPU que hace referencia a VB/IB que está bloqueado por la CPU (quizás en la operación de escritura de otro subproceso). . En este caso, simplemente espere a que se libere la operación de bloqueo antes de que comience el renderizado oficial. Si la GPU no ha alcanzado la posición de instrucción preestablecida en el búfer de comando, entonces la operación de espera aquí es equivalente a no válida; de lo contrario, llevará un tiempo esperar hasta que se libere el estado de bloqueo de datos. De hecho, podemos completar esta solución sin requerir que la CPU escriba permisos en los registros. Solo necesitamos poder modificar los datos que se han enviado al búfer de comando (siempre que haya una instrucción de "salto" en el archivo). búfer de comando).

Por supuesto, no es necesario utilizar la configuración de registro y los modelos de espera mencionados anteriormente; para la sincronización entre GPU, se puede utilizar directamente una instrucción de "barrera RT" para garantizar que el uso de RT sea seguro. Sin embargo, el autor del artículo original cree que el modelo de configuración de registros será mejor (informar el estado de los recursos que aún están en uso a la CPU y completar la sincronización entre las GPU), matando dos pájaros de un tiro.

Actualización: se ha agregado un diagrama de flujo aquí. La situación parece complicada. Prestaremos atención a simplificar los detalles relevantes más adelante. La idea básica es que el procesador de comandos tiene un FIFO al frente y luego ingresa la lógica de decodificación del comando. La ejecución de las instrucciones se completa a través de múltiples bloques que interactúan con la unidad 2D, la unidad de renderizado 3D y la unidad de sombreado. Luego hay un bloque para procesar instrucciones de sincronización/espera (estas instrucciones tienen registros visibles como se mencionó anteriormente), y también hay un bloque para procesar las instrucciones de salto/llamada del búfer de comando (cambiando la dirección del FIFO que actualmente necesita obtenerse) unidad. Todas las unidades que envían tareas enviarán un evento de finalización de comando para informarnos que, por ejemplo, la textura ya no se usa.