En este artículo, analizaremos algunos detalles fundamentales de bajo nivel para comprender por qué las GPU son buenas en tareas de gráficos, redes neuronales y aprendizaje profundo y las CPU son buenas en una amplia cantidad de tareas informáticas secuenciales y complejas de propósito general. Hubo varios temas que tuve que investigar y obtener una comprensión un poco más detallada para esta publicación, algunos de los cuales mencionaré de pasada. Se hace deliberadamente para centrarse sólo en los conceptos básicos absolutos del procesamiento de CPU y GPU.
Las computadoras anteriores eran dispositivos dedicados. Los circuitos de hardware y las puertas lógicas se programaron para hacer un conjunto específico de cosas. Si había que hacer algo nuevo, era necesario volver a cablear los circuitos. "Algo nuevo" podría ser tan simple como hacer cálculos matemáticos para dos ecuaciones diferentes. Durante la Segunda Guerra Mundial, Alan Turing estaba trabajando en una máquina programable para vencer a la máquina Enigma y luego publicó el artículo "Turing Machine". Por la misma época, John von Neumann y otros investigadores también estaban trabajando en una idea que proponía fundamentalmente:
Sabemos que todo en nuestra computadora es binario. Cadena, imagen, vídeo, audio, sistema operativo, programa de aplicación, etc., se representan como 1 y 0. Las especificaciones de arquitectura de CPU (RISC, CISC, etc.) tienen conjuntos de instrucciones (x86, x86-64, ARM, etc.) que los fabricantes de CPU deben cumplir y están disponibles para que el sistema operativo interactúe con el hardware.
Los programas del sistema operativo y de aplicación, incluidos los datos, se traducen en conjuntos de instrucciones y datos binarios para procesarlos en la CPU. A nivel de chip, el procesamiento se realiza en transistores y puertas lógicas. Si ejecuta un programa para sumar dos números, la suma (el "procesamiento") se realiza en una puerta lógica en el procesador.
En la CPU según la arquitectura de Von Neumann, cuando sumamos dos números, se ejecuta una única instrucción de suma en dos números en el circuito. ¡Durante una fracción de ese milisegundo, solo se ejecutó la instrucción add en el núcleo (de ejecución) de la unidad de procesamiento! Este detalle siempre me fascinó.
Los componentes del diagrama anterior son evidentes. Para obtener más detalles y una explicación detallada, consulte este excelente artículo . En las CPU modernas, un único núcleo físico puede contener más de una ALU entera, ALU de punto flotante, etc. Nuevamente, estas unidades son puertas lógicas físicas.
Necesitamos comprender el 'hilo de hardware' en el núcleo de la CPU para apreciar mejor la GPU. Un subproceso de hardware es una unidad de computación que se puede realizar en unidades de ejecución de un núcleo de CPU, cada ciclo de reloj de la CPU . Representa la unidad de trabajo más pequeña que se puede ejecutar en un núcleo.
El diagrama anterior ilustra el ciclo de instrucción de la CPU/ciclo de la máquina. Es una serie de pasos que realiza la CPU para ejecutar una sola instrucción (por ejemplo: c=a+b).
Recuperar: el contador de programa (registro especial en el núcleo de la CPU) realiza un seguimiento de qué instrucción se debe recuperar. La instrucción se recupera y almacena en el registro de instrucciones. Para operaciones simples, también se obtienen los datos correspondientes.
Decodificar: la instrucción se decodifica para ver operadores y operandos.
Ejecutar: según la operación especificada, se elige y ejecuta la unidad de procesamiento adecuada.
Acceso a la memoria: si una instrucción es compleja o se necesitan datos adicionales (varios factores pueden causar esto), el acceso a la memoria se realiza antes de la ejecución. (Se ignora en el diagrama anterior por simplicidad). Para una instrucción compleja, los datos iniciales estarán disponibles en el registro de datos de la unidad de cálculo, pero para la ejecución completa de la instrucción, se requiere acceso a los datos desde la caché L1 y L2. Esto significa que podría haber un pequeño tiempo de espera antes de que se ejecute la unidad de cómputo y el subproceso de hardware aún retenga la unidad de cómputo durante el tiempo de espera.
Reescritura: si la ejecución produce una salida (por ejemplo: c=a+b), la salida se vuelve a escribir en el registro/caché/memoria. (Se ignora en el diagrama anterior o en cualquier lugar posterior de la publicación por simplicidad)
En el diagrama anterior, sólo en t2 se realiza el cálculo. El resto del tiempo, el núcleo simplemente está inactivo (no realizamos ningún trabajo).
Las CPU modernas tienen componentes de hardware que esencialmente permiten que los pasos (buscar-decodificar-ejecutar) se produzcan simultáneamente por ciclo de reloj.
Un único subproceso de hardware ahora puede realizar cálculos en cada ciclo de reloj. Esto se llama canalización de instrucciones.
La recuperación, la decodificación, el acceso a la memoria y la reescritura las realizan otros componentes de una CPU. A falta de una palabra mejor, se denominan "hilos de tubería". El subproceso de canalización se convierte en un subproceso de hardware cuando se encuentra en la etapa de ejecución de un ciclo de instrucción.
Como puede ver, obtenemos resultados de cálculo en cada ciclo desde t2. Anteriormente, obteníamos resultados de cálculo una vez cada 3 ciclos. La canalización mejora el rendimiento informático. Esta es una de las técnicas para gestionar los cuellos de botella de procesamiento en Von Neumann Architecture. También hay otras optimizaciones como ejecución fuera de orden, predicción de rama, ejecución especulativa, etc.
Este es el último concepto que quiero analizar en CPU antes de concluir y pasar a las GPU. A medida que aumentaron las velocidades del reloj, los procesadores también se volvieron más rápidos y eficientes. Con el aumento de la complejidad de las aplicaciones (conjunto de instrucciones), los núcleos de procesamiento de la CPU estaban infrautilizados y pasaban más tiempo esperando el acceso a la memoria.
Entonces, vemos un cuello de botella en la memoria. La unidad de cómputo dedica tiempo al acceso a la memoria y no realiza ningún trabajo útil. La memoria es varios órdenes más lenta que la CPU y la brecha no se cerrará pronto. La idea era aumentar el ancho de banda de la memoria en algunas unidades de un solo núcleo de CPU y mantener los datos listos para utilizar las unidades de cómputo cuando están esperando el acceso a la memoria.
Intel puso a disposición Hyper-threading en 2002 en los procesadores Xeon y Pentium 4. Antes del hyper-threading, solo había un hilo de hardware por núcleo. Con Hyper-Threading, habrá 2 subprocesos de hardware por núcleo. ¿Qué significa? Circuito de procesamiento duplicado para algunos registros, contador de programa, unidad de recuperación, unidad de decodificación, etc.
El diagrama anterior solo muestra nuevos elementos de circuito en un núcleo de CPU con hyperthreading. Es así como un único núcleo físico es visible como 2 núcleos para el Sistema Operativo. Si tenía un procesador de 4 núcleos, con Hyper-Threading habilitado, el sistema operativo lo ve como 8 núcleos . El tamaño de la caché L1 - L3 aumentará para dar cabida a registros adicionales. Tenga en cuenta que las unidades de ejecución son compartidas.
Supongamos que tenemos los procesos P1 y P2 haciendo a=b+c, d=e+f, estos se pueden ejecutar simultáneamente en un solo ciclo de reloj debido a los subprocesos HW 1 y 2. Con un solo subproceso HW, como vimos anteriormente, esto no sería posible. Aquí estamos aumentando el ancho de banda de la memoria dentro de un núcleo agregando subprocesos de hardware para que la unidad de procesamiento se pueda utilizar de manera eficiente. Esto mejora la simultaneidad informática.
Algunos escenarios interesantes:
Consulte este artículo y pruebe también el cuaderno Colab . Muestra cómo la multiplicación de matrices es una tarea paralelizable y cómo los núcleos de cómputo paralelos pueden acelerar el cálculo.
A medida que aumentó la potencia informática, también aumentó la demanda de procesamiento de gráficos. Tareas como la representación de la interfaz de usuario y los juegos requieren operaciones paralelas, lo que genera la necesidad de numerosas ALU y FPU a nivel de circuito. Las CPU, diseñadas para tareas secuenciales, no podían manejar estas cargas de trabajo paralelas de manera efectiva. Así, las GPU se desarrollaron para satisfacer la demanda de procesamiento paralelo en tareas gráficas, y luego allanaron el camino para su adopción en la aceleración de algoritmos de aprendizaje profundo.
Recomiendo ampliamente:
Los núcleos, los subprocesos de hardware, la velocidad del reloj, el ancho de banda de la memoria y la memoria en el chip de las CPU y GPU difieren significativamente. Ejemplo:
Este número se utiliza para comparar con la GPU, ya que obtener el máximo rendimiento de la informática de uso general es muy subjetivo. Este número es un límite máximo teórico, lo que significa que los circuitos FP64 se están utilizando al máximo.
Las terminologías que vimos en CPU no siempre se traducen directamente a GPU. Aquí veremos los componentes y el núcleo de la GPU NVIDIA A100. Una cosa que me sorprendió mientras investigaba para este artículo fue que los proveedores de CPU no publican cuántas ALU, FPU, etc. están disponibles en las unidades de ejecución de un núcleo. NVIDIA es muy transparente sobre la cantidad de núcleos y el marco CUDA brinda total flexibilidad y acceso a nivel de circuito.
En el diagrama anterior en GPU, podemos ver que no hay caché L3, caché L2 más pequeña, unidad de control y caché L1 más pequeñas pero con mucha más y una gran cantidad de unidades de procesamiento.
Aquí están los componentes de la GPU en los diagramas anteriores y su equivalente de CPU para nuestra comprensión inicial. No he hecho programación CUDA, por lo que compararla con equivalentes de CPU ayuda con la comprensión inicial. Los programadores de CUDA entienden esto muy bien.
Las tareas de gráficos y aprendizaje profundo exigen una ejecución de tipo SIM(D/T) [instrucción única, múltiples datos/hilo]. es decir, leer y trabajar con grandes cantidades de datos para una sola instrucción.
Hablamos sobre la canalización de instrucciones y el hyper-threading en CPU y GPU que también tienen capacidades. La forma en que se implementa y funciona es ligeramente diferente, pero los principios son los mismos.
A diferencia de las CPU, las GPU (a través de CUDA) brindan acceso directo a Pipeline Threads (obteniendo datos de la memoria y utilizando el ancho de banda de la memoria). Los programadores de GPU funcionan primero intentando llenar las unidades de cómputo (incluidos los registros y caché L1 compartidos asociados para almacenar operandos de cómputo), luego "hilos de canalización" que recuperan datos en registros y HBM. Nuevamente, quiero enfatizar que los programadores de aplicaciones de CPU no piensan en esto y no se publican especificaciones sobre "subprocesos de canalización" y la cantidad de unidades de cómputo por núcleo. Nvidia no sólo los publica sino que también proporciona un control total a los programadores.
Entraré en más detalles sobre esto en una publicación dedicada sobre el modelo de programación CUDA y el "procesamiento por lotes" en la técnica de optimización del servicio de modelos, donde podremos ver cuán beneficioso es esto.
El diagrama anterior muestra la ejecución de subprocesos de hardware en el núcleo de CPU y GPU. Consulte la sección "acceso a la memoria" que analizamos anteriormente en canalización de CPU. Este diagrama muestra eso. La compleja gestión de la memoria de la CPU hace que este tiempo de espera sea lo suficientemente pequeño (unos pocos ciclos de reloj) para recuperar datos de la caché L1 en los registros. Cuando es necesario recuperar datos de L3 o de la memoria principal, el otro hilo para el cual los datos ya están registrados (vimos esto en la sección Hyper-Threading) obtiene el control de las unidades de ejecución.
En las GPU, debido a la sobresuscripción (gran número de subprocesos y registros de canalización) y al conjunto de instrucciones simples, ya hay una gran cantidad de datos disponibles en los registros pendientes de ejecución. Estos subprocesos de canalización que esperan ejecución se convierten en subprocesos de hardware y realizan la ejecución con la frecuencia de cada ciclo de reloj, ya que los subprocesos de canalización en las GPU son livianos.
¿Qué pasa con la portería?
Esta es la razón principal por la que la latencia de la multiplicación de matrices más pequeñas es más o menos la misma en CPU y GPU. Pruébalo .
Las tareas deben ser lo suficientemente paralelas y los datos deben ser lo suficientemente grandes como para saturar los FLOP de cómputo y el ancho de banda de la memoria. Si una sola tarea no es lo suficientemente grande, es necesario empaquetar varias tareas para saturar la memoria y la computación para utilizar completamente el hardware.
Intensidad de cálculo = FLOP/ancho de banda . es decir, la relación entre la cantidad de trabajo que pueden realizar las unidades de cómputo por segundo y la cantidad de datos que puede proporcionar la memoria por segundo.
En el diagrama anterior, vemos que la intensidad de la computación aumenta a medida que avanzamos hacia una mayor latencia y una memoria de menor ancho de banda. Queremos que este número sea lo más pequeño posible para que la computación se utilice por completo. Para eso, necesitamos mantener la mayor cantidad de datos en L1/Registros para que el cálculo pueda realizarse rápidamente. Si recuperamos datos únicos de HBM, solo hay unas pocas operaciones en las que realizamos 100 operaciones con datos únicos para que valga la pena. Si no realizamos 100 operaciones, las unidades de cómputo estaban inactivas. Aquí es donde entra en juego una gran cantidad de subprocesos y registros en las GPU. Mantener la mayor cantidad de datos en L1/Registros para mantener baja la intensidad informática y mantener ocupados los núcleos paralelos.
Existe una diferencia en la intensidad de cálculo de 4X entre los núcleos CUDA y Tensor porque los núcleos CUDA solo pueden realizar un MMA FP64 1x1, mientras que los núcleos Tensor pueden realizar instrucciones MMA FP64 4x4 por ciclo de reloj.
Gran cantidad de unidades de cómputo (núcleos CUDA y Tensor), gran cantidad de subprocesos y registros (sobre suscripción), conjunto de instrucciones reducido, sin caché L3, HBM (SRAM), patrón de acceso a memoria simple y de alto rendimiento (en comparación con las CPU: cambio de contexto , almacenamiento en caché multicapa, paginación de memoria, TLB, etc.) son los principios que hacen que las GPU sean mucho mejores que las CPU en computación paralela (renderización de gráficos, aprendizaje profundo, etc.)
Las GPU se crearon por primera vez para manejar tareas de procesamiento de gráficos. Los investigadores de IA comenzaron a aprovechar CUDA y su acceso directo a un potente procesamiento paralelo a través de núcleos CUDA. La GPU NVIDIA tiene motores de procesamiento de texturas, trazado de rayos, ráster, polimorfismo, etc. (digamos conjuntos de instrucciones específicos de gráficos). Con el aumento en la adopción de la IA, se están agregando núcleos Tensor que son buenos en el cálculo de matrices 4x4 (instrucción MMA) y que están dedicados al aprendizaje profundo.
Desde 2017, NVIDIA ha ido aumentando el número de núcleos Tensor en cada arquitectura. Pero estas GPU también son buenas en el procesamiento de gráficos. Aunque el conjunto de instrucciones y la complejidad son mucho menores en las GPU, no están completamente dedicados al aprendizaje profundo (especialmente la arquitectura Transformer).
FlashAttention 2 , una optimización de la capa de software (simpatía mecánica por el patrón de acceso a la memoria de la capa de atención) para la arquitectura del transformador proporciona una aceleración del doble en las tareas.
Con nuestra comprensión profunda de CPU y GPU basada en los primeros principios, podemos comprender la necesidad de aceleradores de transformadores: un chip dedicado (circuito solo para operaciones de transformadores), con una cantidad incluso mayor de unidades de cómputo para paralelismo, conjunto de instrucciones reducido, sin Cachés L1/L2, DRAM (registros) masivos que reemplazan a HBM, unidades de memoria optimizadas para el patrón de acceso a la memoria de la arquitectura del transformador. Después de todo, los LLM son nuevos compañeros para los humanos (después de la web y los dispositivos móviles) y necesitan chips dedicados para lograr eficiencia y rendimiento.