5 puntos por GN⁺ 2023-10-22 | 1 comentarios | Compartir por WhatsApp
  • Las GPU tienen una arquitectura que prioriza el throughput masivo en paralelo por encima de una baja latencia por instrucción, por eso destacan en tareas que ejecutan grandes volúmenes del mismo tipo de operación, como deep learning, gráficos y cómputo numérico
  • Mientras que la CPU reduce la latencia de ejecución secuencial con pipelining, ejecución fuera de orden, ejecución especulativa y cachés multinivel, la GPU oculta la latencia con muchas ALU y muchos hilos para elevar el throughput
  • En precisión de 32 bits, la Nvidia Ampere A100 alcanza 19.5 TFLOPS, mientras que un procesador Intel de 24 núcleos de 2021 llega a 0.66 TFLOPS, por lo que la brecha de throughput en cómputo numérico sigue creciendo
  • En un kernel de CUDA, el host code en la CPU prepara la ejecución y el device code en la GPU se ejecuta con una estructura de grid·block·thread; los hilos se agrupan en unidades de 32 llamadas warp y se procesan con el modelo SIMT
  • El rendimiento real depende en gran medida de cómo se repartan los registros, la memoria compartida, los slots de bloque y los slots de hilo del SM; si la occupancy es baja, cuesta más ocultar la latencia y puede no alcanzarse el throughput máximo

Diferencias en los objetivos de diseño de CPU y GPU

  • La CPU está diseñada principalmente para procesar rápido la ejecución secuencial de instrucciones
    • Para reducir la latencia de ejecución de instrucciones usa funciones como instruction pipelining, out-of-order execution, speculative execution y multilevel cache
    • Una operación única como sumar dos números o un flujo corto de operaciones puede ejecutarse con menor latencia en CPU que en GPU
  • La GPU está diseñada alrededor de la paralelización masiva y un alto throughput
    • Esta arquitectura encaja bien con tareas que requieren ejecutar rápidamente muchas operaciones de álgebra lineal y cómputo numérico, como videojuegos, gráficos, cálculo numérico y deep learning
    • En millones o miles de millones de operaciones del mismo tipo, la GPU puede procesarlas mucho más rápido que la CPU gracias a su paralelismo masivo
  • El rendimiento en cómputo numérico se mide en FLOPS, es decir, operaciones de punto flotante por segundo
    • La Nvidia Ampere A100 ofrece un throughput de 19.5 TFLOPS en precisión de 32 bits
    • Un procesador Intel de 24 núcleos, en 2021, ronda los 0.66 TFLOPS en precisión de 32 bits
    • La brecha de throughput entre GPU y CPU crece año tras año

Cómo la GPU oculta la latencia

  • La GPU logra tolerancia a la latencia aprovechando muchos hilos y recursos de cómputo, incluso si la latencia de cada instrucción individual es alta
  • Mientras un hilo espera el resultado de una instrucción, la GPU ejecuta otros hilos que no estén bloqueados
  • Gracias a esta planificación, las unidades de cómputo pueden seguir trabajando tanto como sea posible y mantener un alto throughput

Arquitectura de cómputo de la GPU

  • Una GPU está compuesta por un arreglo de varios streaming multiprocessors (SM)
  • Cada SM incluye varios streaming processor, core y thread
    • La Nvidia H100 tiene 132 SM y cada SM cuenta con 64 cores, para un total de 8,448 cores
  • Cada SM tiene memoria on-chip limitada compartida por todos los cores
    • A esta memoria se le llama shared memory o scratchpad
  • Los recursos de la unidad de control del SM también son compartidos por los cores
  • Cada SM cuenta con un thread scheduler basado en hardware para ejecutar hilos
  • Según la carga de trabajo, también puede incluir unidades funcionales especiales o aceleradores de cómputo, como tensor core y ray tracing unit

Jerarquía de memoria de la GPU

  • Registros

    • Cada SM tiene una gran cantidad de registros
    • La Nvidia A100 y la H100 tienen 65,536 registros por SM
    • Los registros son compartidos por los cores y se asignan dinámicamente según las necesidades de los hilos
    • Los registros asignados a un hilo específico durante la ejecución son exclusivos de ese hilo, por lo que otros hilos no pueden leerlos ni escribirlos
  • Constant cache

    • Almacena en caché los datos constantes usados por el código que se ejecuta en el SM
    • Para que la GPU pueda guardarlos en constant cache, el programador debe declarar explícitamente esos objetos como constantes en el código
  • Shared memory

    • Es una SRAM on-chip pequeña, rápida, programable y de baja latencia presente en cada SM
    • La comparten los thread block que se ejecutan en el mismo SM
    • Cuando varios hilos usan el mismo fragmento de datos, un solo hilo puede leerlo desde global memory y compartirlo con los demás para reducir cargas redundantes
    • También se usa como mecanismo de sincronización entre hilos dentro de un thread block
  • Caché L1 y caché L2

    • Cada SM tiene una caché L1 que almacena datos de acceso frecuente desde la caché L2
    • La caché L2 es compartida por todos los SM y almacena datos de acceso frecuente desde global memory para reducir la latencia
    • Como las cachés L1 y L2 operan de manera transparente para el SM, desde la perspectiva del SM parece que los datos vienen de global memory
  • Global memory

    • La GPU tiene una global memory off-chip, que es DRAM de gran capacidad y alto ancho de banda
    • La Nvidia H100 cuenta con 80GB de HBM y un ancho de banda de 3000GB/s
    • La global memory está lejos de los SM, por lo que su latencia es alta, pero la jerarquía de memoria on-chip y la gran cantidad de unidades de cómputo ayudan a ocultarla

Kernels de CUDA y estructura de hilos

  • CUDA es una interfaz de programación para escribir programas para GPU Nvidia
  • Los cálculos que se ejecutan en la GPU se expresan como un kernel con una forma similar a una función de C/C++
    • Un ejemplo es un kernel de suma de vectores que recibe dos vectores como entrada, suma sus elementos uno a uno y escribe el resultado en un tercer vector
  • Al ejecutar un kernel se lanzan muchos hilos, y al conjunto completo se le llama grid
    • Un grid se compone de uno o más thread block
    • Cada thread block se compone de uno o más thread
  • La cantidad de bloques y de hilos depende del tamaño de los datos y del paralelismo deseado
    • En una suma de vectores de dimensión 256, se puede usar un solo bloque de 256 hilos para que cada hilo procese un elemento del vector
    • En problemas más grandes, puede que el número de hilos disponibles en la GPU no alcance, por lo que cada hilo puede procesar varios puntos de datos
  • Una implementación en CUDA se divide en dos partes
    • El host code se ejecuta en la CPU y se encarga de cargar datos, asignar memoria en la GPU y lanzar el kernel con el grid de hilos configurado
    • El device code se ejecuta en la GPU y define la función real del kernel

Etapas de ejecución de un kernel en la GPU

  • Copia de datos del host al device

    • Antes de ejecutar el kernel, los datos necesarios deben copiarse desde la memoria de la CPU a la global memory de la GPU
    • En hardware GPU moderno también puede leerse directamente desde la memoria del host usando unified virtual memory
  • Programación de thread block en los SM

    • Cuando los datos necesarios ya están listos en la memoria de la GPU, los thread block se asignan a los SM
    • Todos los hilos de un mismo bloque se procesan al mismo tiempo en el mismo SM
    • Antes de ejecutar, la GPU debe asegurar los recursos del SM requeridos por esos hilos
    • En la práctica, varios thread block pueden asignarse simultáneamente al mismo SM
    • Como el número de SM es limitado y un kernel grande puede tener muchísimos bloques, no todos los bloques se ejecutan de inmediato
    • La GPU mantiene una lista de bloques en espera y, cuando un bloque termina, asigna uno de los bloques pendientes a ejecución
  • SIMT y warp

    • Los hilos asignados a un SM se agrupan nuevamente en unidades de 32, llamadas warp
    • En las GPU Nvidia actuales el tamaño del warp es 32, aunque podría cambiar en hardware futuro
    • El SM obtiene y emite la misma instrucción para todos los hilos dentro de un warp
    • Los hilos ejecutan la misma instrucción al mismo tiempo, pero procesan distintas porciones de datos
    • Este modelo se conoce como single instruction multiple threads (SIMT) y es similar a las instrucciones SIMD de CPU
    • Las GPU modernas posteriores a Volta también cuentan con independent thread scheduling, que permite concurrencia total entre hilos independientemente del warp
  • Programación de warp y tolerancia a la latencia

    • Aunque todos los processing block dentro de un SM puedan manejar warps, en un momento dado solo una parte de ellos está ejecutando instrucciones realmente
    • La razón es que el número de unidades de ejecución del SM es limitado
    • Si un warp espera el resultado de una instrucción costosa, el SM lo deja en espera y ejecuta otro warp que no necesite esperar
    • Como cada hilo de cada warp tiene su propio conjunto de registros, cambiar entre warps no tiene overhead adicional
    • En cambio, el context switching de procesos en CPU es costoso porque requiere guardar registros en la memoria principal y restaurar el estado de otro proceso
  • Copia de datos de resultados del device al host

    • Cuando todos los hilos del kernel terminan su ejecución, los resultados se copian de vuelta a la memoria del host

Reparto de recursos y occupancy

  • El uso de los recursos de la GPU se mide con la métrica occupancy
    • La occupancy es la proporción entre el número de warps asignados a un SM y el número máximo de warps que ese SM puede soportar
    • Para lograr el máximo throughput, lo ideal es una occupancy del 100%, aunque no siempre es posible por distintas restricciones
  • El SM tiene recursos de ejecución fijos, como registros, shared memory, thread block slot y thread slot
    • Estos recursos se reparten dinámicamente según los requisitos de los hilos y los límites de la GPU
  • Ejemplo con la Nvidia H100
    • Cada SM puede manejar 32 block, 64 warp, es decir, 2048 thread
    • Soporta como máximo 1024 thread por block
    • Si el tamaño del block es de 1024 thread, los 2048 thread slot se reparten en 2 block
  • La partición dinámica puede usar los recursos de cómputo con mayor eficiencia que una partición fija
    • En una partición fija, cada thread block recibe una cantidad fija de recursos de ejecución
    • En algunos casos, a un hilo se le asignan más recursos de los que necesita, lo que provoca desperdicio y menor throughput
  • Ejemplos de reducción de occupancy
    • Si el tamaño del block es de 32 thread y se necesitan 2048 thread en total, se generan 64 block
    • Sin embargo, cada SM solo puede manejar 32 block al mismo tiempo, así que en realidad solo se ejecutan 1024 thread y la occupancy queda en 50%
    • Si hay 65,536 registros por SM, para ejecutar 2048 thread simultáneamente solo pueden usarse como máximo 32 registros por thread
    • Si el kernel necesita 64 registros por thread, entonces solo podrán ejecutarse 1024 thread por SM y la occupancy vuelve a caer a 50%
  • Una occupancy baja dificulta ocultar suficientemente la latencia y también puede reducir el throughput de cómputo necesario para alcanzar el máximo rendimiento del hardware
  • Para escribir kernels GPU eficientes, hay que distribuir cuidadosamente los recursos para mantener una occupancy alta y a la vez reducir la latencia
    • Usar muchos registros puede acelerar el código en sí, pero también puede bajar la occupancy, así que el equilibrio de optimización es importante

Materiales para profundizar

1 comentarios

 
GN⁺ 2023-10-22
Comentarios de Hacker News
  • Alguien envió un correo de queja sobre este artículo: https://twitter.com/abhi9u/status/1715753871564476597
    Esto viola las reglas de HN. De hecho, es el único punto lo bastante importante como para aparecer tanto en la guía del sitio como en el FAQ, y los usuarios de HN son muy sensibles con este tema
    P: ¿Puedo pedir recomendaciones para mi publicación?
    R: No. Los usuarios deben votar no porque alguien tenga contenido que promocionar, sino cuando ellos mismos lo consideren intelectualmente interesante. Si rompes esta regla, podemos penalizar o bloquear la publicación, la cuenta o el sitio, así que no lo hagas
    https://news.ycombinator.com/newsfaq.html
    No pidas votos, comentarios ni envíos. Los usuarios deben votar y comentar no por motivos de promoción, sino cuando encuentren algo personalmente interesante por su cuenta
    https://news.ycombinator.com/newsguidelines.html

    • No conocía esa regla, y tampoco conozco a la persona que publicó el artículo
      Ahora que lo sé, no volveré a hacerlo
  • Me sorprendió que en la parte de “copiar datos del host al dispositivo” no se mencionara la copia asíncrona. Para aprovechar al máximo la GPU, esta no debería quedarse ociosa mientras se copian datos entre el host y la GPU
    Muchos frameworks ofrecen mecanismos para programar copias asíncronas que pueden ejecutarse junto con el envío asíncrono de trabajo. El artículo se acerca más a una introducción a la GPU, pero en la programación real de GPU hay todo tipo de trucos y técnicas más allá de eso para exprimir hasta el final una GPU costosa. Como ocurre con la mayoría de las optimizaciones hoy en día, hay muchos acantilados ocultos y comportamientos no lineales, así que las herramientas de profiling ayudan muchísimo

    • Probablemente se use coma flotante de 64 bits (double), y en ese caso no todas las GPU van a ayudar mucho. Especialmente si se comparan con una CPU potente
      Aun así, si usas una GPU con muchas unidades FP64, puede haber una gran aceleración. Normalmente esas no son GPU para gaming, pero si tienes por ahí una 4060, su rendimiento FP64 es de unos 300 GFLOPS, así que probablemente supere al de una CPU. Las CPU modernas también son fuertes en este terreno y pueden emitir varias operaciones FP64 por ciclo por núcleo
  • La primera frase, “la mayoría de los programadores entienden profundamente la CPU”, es tan obviamente falsa que, aunque el artículo pudiera ser excelente, hace difícil tomarse en serio el resto

    • ¿Qué tal si se cambia por algo como esto?: “Una cantidad considerable de científicos de la computación, ingenieros en computación, ingenieros eléctricos y desarrolladores aficionados…”
      En la universidad tomé clases de filosofía por diversión, y ahí aprendí a no descartar una frase de inmediato, sino a corregirla mentalmente hacia una forma mejor. Ahora mi cerebro traduce automáticamente las generalizaciones excesivas o las falsedades evidentes a proposiciones razonablemente cercanas a la verdad. A medida que avanza el argumento, recompone esas ideas y puede evaluar el texto completo como algo lógicamente coherente
      Gracias a eso, incluso al leer textos malos me quedan nuevas premisas y afirmaciones verdaderas o falsas sobre temas que me interesan, y con eso mi mundo mental se amplía
    • Definitivamente no es cierto para la mayoría de los programadores, pero quizá el autor se refería a ingenieros con formación en CS. Si pasas por un programa formal de ciencias de la computación, sueles terminar con una comprensión bastante profunda de la CPU, mientras que la GPU muchas veces se trata de forma mucho más superficial
    • No entiendo por qué casi toda publicación en internet tiene al menos un comentario del tipo “dejé de leer en X”. Eso no aporta nada
    • Más de la mitad de esta discusión probablemente dependa de cómo se defina entender profundamente
      En la universidad aprendí los hechos básicos de la arquitectura de CPU, conozco de forma muy general el panorama y de vez en cuando veo actualizaciones limitadas de ese conocimiento, pero no llamaría a eso una “comprensión profunda”. Más bien diría “una comprensión básica de cómo funciona, se diseña y se usa una CPU”
      Si eres hábil con ensamblador, quizá se podría decir que “entiendes profundamente” cómo usar una CPU a bajo nivel, pero aun así suena algo exagerado. Y tampoco es lo mismo que ser un experto en diseño de CPU/GPU
      Así que estoy de acuerdo. Aun así, el artículo es interesante, y en particular los diagramas están buenos
    • Lo aprendí tanto en la carrera como en la clase de Structure and Interpretation of Computer Programs, y recomiendo esa clase a cualquiera que tenga interés en la computación de bajo nivel
  • La parte que dice “los registros asignados a un hilo en ejecución son exclusivos de ese hilo, así que otros hilos no pueden leerlos ni escribirlos” tiene excepciones
    Los wave intrinsics de HLSL y funciones similares en CUDA permiten leer registros de otros hilos dentro del wavefront actual. Además, en el párrafo sobre arquitectura de memoria también valdría la pena mencionar que, aunque la caché no garantiza coherencia entre hilos del mismo dispatch/grid, hay bloques funcionales especiales presentes globalmente en todo el chip que implementan operaciones atómicas sobre memoria global

  • La programación SIMD es realmente brutal
    ¿Quieres ejecutar cálculos sobre todos los píxeles de la pantalla? Sin problema
    ¿Quieres meter una condición de bifurcación? Ay

    • ¿Quieres meter eval? Se detiene todo
    • Siendo justos, esto tiene sentido. Tomar decisiones inteligentes es “más difícil” que escalar cálculos simples a muchos trabajadores
  • ¿Por qué se sigue llamando GPU? PPU (unidad de procesamiento paralelo) suena como un nombre mejor

    • Porque además de las capacidades de GPU de propósito general, también incluye silicio dedicado a gráficos
    • Porque si dices GPU, todo el mundo entiende a qué te refieres
      La relación entre drone y quad-copter es parecida
    • Unidad de procesamiento vectorial sería más apropiado
    • La CPU también es una PPU
    • General Processing Unit
  • Es un texto excelente. Y las GPU han avanzado más y rinden mejor, en lo que hacen, que cualquier otra cosa que se me ocurra.
    Pero me gustaría poner SIMD en la categoría de cosas que no son realmente necesarias una vez que aprendes otros paradigmas más flexibles. Yo prefiero MIMD y los clústeres/transputers, que parecen haber desaparecido por ahí de los 2000. El estado actual exige que el desarrollador mueva los datos manualmente, escriba shaders bajo límites arbitrarios sobre cuántas ubicaciones de memoria pueden accederse al mismo tiempo, duplique trabajo usando lenguajes separados para GPU y CPU, tenga que saber qué hardware existe para funciones como el trazado de rayos, y quede atado a frameworks muy opinionados como OpenGL/Metal/Vulkan. Las GPU son una rama lateral que jamás podrá llevarme adonde quiero ir, así que los últimos 25 años se han sentido como vivir en una línea temporal equivocada.
    Hablando en términos generales, una CPU de propósito general escalable dentro de las limitaciones del fin de la ley de Moore debería ser multinúcleo con memoria local, compartir datos mediante memoria direccionada por contenido con copy-on-write u otros mecanismos de caché, y ofrecer un único espacio de direcciones unificado para que el usuario pueda explorar libremente todas las formas de computación en un entorno de computación de escritorio. Usaría un ensamblador estándar, pero normalmente se programaría en lenguajes funcionales como Erlang/Go, Octave/MATLAB, o idealmente Julia. El renderizado 3D y las bibliotecas de IA serían capas por encima de eso, no elementos fundamentales.
    Curiosamente, las GPU han llegado más o menos a la configuración multinúcleo de la que hablo, pero los drivers separan al usuario del acceso bare metal necesario para un MIMD de propósito general. Pensaba que la única forma de derribar la supremacía de las GPU eran las FPGA, pero quizá podría haber una oportunidad de escribir drivers que hagan que el hardware GPU se vea como un MIMD con memoria unificada. No sé qué tan bien manejan las GPU las operaciones enteras, pero parecería posible aproximarlas con la parte entera de 32 bits del punto flotante de 64 bits. Por ese tipo de concesiones, una máquina MIMD podría ser de 10 a 100 veces más lenta que una GPU, pero aun así de 10 a 100 veces más rápida que una CPU. Y además podría escalar sin depender en exceso de cachés grandes y buses rápidos, que estancaron a las CPU desde alrededor de 2007, cuando el mercado móvil tomó el control y priorizó precio y eficiencia energética por encima del rendimiento. Las máquinas MIMD también podrían agruparse en clústeres para crear redes de cómputo distribuido como SETI@home sin cambiar el código. Para darse una idea de cuánto poder le daría eso al usuario común, sería como comparar BitTorrent de cómputo vs. FTP en vez de datos

  • No termino de entender cómo la arquitectura de Apple Silicon difiere de NVIDIA.
    Cuando veo una frase como “la GPU Nvidia H100 tiene 132 SM, 64 núcleos por SM, para un total de 8448 núcleos”, 8448 núcleos claramente suena impresionante. Pero ¿el Apple M2 Ultra solo tiene 76 núcleos?
    ¿Cómo puede la GPU NVIDIA H100 tener más de 110 veces más núcleos? Obviamente no tiene 110 veces más rendimiento que el M2 Ultra, así que, ¿qué está pasando aquí?

    • En términos generales, el SM de NVIDIA es lo más parecido a una CU de GPU de AMD o a un núcleo de GPU de Apple. Aquí “núcleo”, según recuerdo, es un subcomponente del SM que realiza operaciones individuales.
      Mira este diagrama del blog de NVIDIA: https://developer-blogs.nvidia.com/wp-content/uploads/2021/g...
      (https://developer.nvidia.com/blog/nvidia-ampere-architecture...)
    • NVIDIA llama “núcleo” a algo que en realidad es prácticamente un vector lane, y también usa “hilo” en SIMT para referirse a la ejecución de uno de esos vector lanes, lo cual es deliberadamente ambiguo y, francamente, deshonesto.
      Claro, puede sentirse que hay cierta justificación para llamarlo “hilo” porque cada lane admite su propio contador de programa, pero al final lo que importa es la velocidad y el throughput de la ALU.
    • El H100 sirve para calentar un cuarto. Consume más de 10 veces la energía del M2 Ultra.
  • Ahora entiendo por qué el machine learning usa punto flotante para la precisión. No fue una elección; fue porque el código de gráficos lo usa así.
    Es otra pieza del rompecabezas de “por qué el machine learning es tan ineficiente”.
    Me pregunto cuánto overhead de copia de memoria hay en entornos reales. Si funciona como una carga normal, sería bastante brutal. Por algo se delega el procesamiento TCP al hardware para evitarlo. Aquí hay mucho más datos, aunque se procesan en bloques más grandes.

    • En muchas redes grandes modernas, el tiempo de cómputo en GPU para el cálculo de gradientes y la retropropagación es tan lento que copiar datos de punto flotante por el bus PCIe no es el cuello de botella.
      Es decir, copiar un minibatch de imágenes en punto flotante sigue siendo lo bastante rápido. Esto se debe a que la iteración de gradiente/SGD es lenta y la cantidad de cómputo es muy grande. Incluso usando precisión mixta es así.
      En redes poco profundas, podría haber ventajas en copiar a la memoria GPU solo los datos comprimidos originales y luego hacer la descompresión, etc., en la GPU. Pero si las GPU modernas todavía no adoptan PCIe 5, es porque el rendimiento bruto de cómputo importa más.
      Por último, el impacto de los Tensor Cores también fue grande, y según la red, pueden ser tan rápidos que la utilización termine siendo muy baja.
    • No me parece que elegir números de punto flotante sea algo especialmente ineficiente. Si el framework hubiera sido de punto fijo por defecto, habría sido complicado ajustar el rango dinámico en toda la red.
      Además, las matemáticas del entrenamiento asumen que los números son continuos.
    • El punto flotante ocupa más y sus operaciones también son más difíciles.
      Aun así, me preguntaba por qué los LLM basados en CPU hacen cuantización. Según entiendo, es un proceso para usar menos memoria reduciendo la precisión de los pesos.
      No está claro si la falta de precisión realmente hace una diferencia. Si es así, entonces ¿por qué se usa punto flotante desde el principio? Si la precisión no importa, la precisión extra solo hace que se gasten más recursos sin una razón real, y probablemente se usen varios órdenes de magnitud más recursos de los necesarios.
      Este campo no lo iniciaron personas que entendieran el rendimiento. Usaron herramientas para construir algo, pero no había un “por qué”. Lo hicieron así porque la herramienta lo hacía así.
      Y esto importa por lo siguiente: incluso en una CPU general, una forma de acceder a los datos puede ser varios órdenes de magnitud más rápida que otra, pero hay que saberlo. ¿No querrías reducir el costo de los LLM en varios órdenes de magnitud?
    • ¿Qué tiene de ineficiente el punto flotante? Parece que el machine learning obtiene una gran ventaja de poder acceder a un rango dinámico de varios órdenes de magnitud.
  • También vale la pena ver esta charla y estas diapositivas sobre las partes complicadas de CPU y GPU de hace unos años.
    Alexander Titov — Know your hardware: CPU memory hierarchy https://youtu.be/QOJ2hsop6hM
    https://github.com/alexander-titov/public/blob/master/confer...
    Know Your Hardware - CPU Memory Hierarchy -- Alexander Titov -- C%2B%2B Moscow Meetup March 2019.pdf
    https://github.com/alexander-titov/public/blob/master/confer...
    GPGPU - what it is and why you should care -- Alexander Titov -- CoreHard 2019.pdf