24 puntos por GN⁺ 2025-06-25 | 1 comentarios | Compartir por WhatsApp
  • Las GPU tienen una velocidad de cómputo muy superior a la velocidad de acceso a memoria, por lo que la jerarquía de memoria se convierte en un cuello de botella de rendimiento
  • Según la intensidad aritmética (Arithmetic Intensity, AI), una operación puede clasificarse como limitada por memoria o limitada por cómputo, y el punto crítico de una GPU A100 es de aproximadamente 13 FLOPs/Byte
  • Entre las principales estrategias de optimización de rendimiento están la fusión de operaciones (Fusion) y el tiling; Fusion reduce viajes innecesarios a memoria y Tiling maximiza la reutilización de datos
  • Entender las características estructurales del hardware de GPU, como la sincronización, Coalesced Load y la resolución de conflictos de bancos, es clave para escribir kernels de alto rendimiento
  • Consideraciones adicionales como la ocupación (Occupancy), minimizar la divergencia de hilos y la cuantización (Quantization) también tienen un impacto importante en el rendimiento real

Jerarquía de cómputo y memoria de la GPU

  • Una GPU suele tener una velocidad de procesamiento aritmético mucho mayor que su ancho de banda de memoria
  • Por ejemplo, una NVIDIA A100 ofrece alrededor de 19.5 TFLOPS (punto flotante de 32 bits), pero su ancho de banda de memoria ronda los 1.5 TB/s
  • Como puede procesar decenas de cálculos mientras lee 4 bytes de datos, el movimiento de datos es el cuello de botella del rendimiento
  • La memoria global (VRAM) es la memoria lenta fuera del chip donde residen todos los datos, mientras que el Streaming Multiprocessor (SM) se encarga del cómputo
  • Cada SM tiene Shared Memory (SRAM) rápida dentro del chip, que puede usarse como una caché administrada directamente por el programa
  • El hilo es la unidad mínima de ejecución, y cada hilo tiene su propio conjunto de registros individuales
  • 32 hilos forman un Warp, y un Block es una cuadrícula de hilos que se ejecuta en el mismo SM

Regímenes de rendimiento: limitado por memoria vs limitado por cómputo

  • El rendimiento de un kernel puede estar limitado por memoria (restringido por la velocidad de movimiento de datos) o por cómputo (restringido por la capacidad de operación del SM)
  • La intensidad aritmética (AI) se define como Total FLOPs / Total Bytes Accessed, y es una métrica clave
  • Modelo Roofline: un gráfico donde el eje x es AI y el eje y es FLOPS/s, y que muestra el rendimiento alcanzable de un kernel
    • Si la AI es baja y está limitado por memoria, sigue una línea diagonal (techo del ancho de banda de memoria)
    • Si la AI es alta y está limitado por cómputo, sigue una línea horizontal (techo del rendimiento máximo de cómputo)
  • El Ridge Point de la A100 es 19.5 TFLOPS / 1.5 TB/s ≈ 13 FLOPs/Byte
  • Si se aumenta la AI, el rendimiento mejora y el kernel puede llegar a estar limitado por cómputo

Estrategias para aumentar la intensidad aritmética

  • Modelo simple: un hilo calcula un solo C[i,j] → AI = 0.25 (muy baja, limitado por memoria)
  • Incluso si un hilo calcula un tile de 2x2, AI = 0.5 (sigue siendo baja)
  • Para aumentar la AI, varios hilos deben cargar tiles grandes en Shared Memory a nivel de bloque para maximizar la reutilización de datos
  • Con la cooperación de los hilos dentro del Block, se puede elevar AI > 13 y entrar en la zona limitada por cómputo

Estado limitado por sobrecarga

  • Puede haber sobrecarga en el proceso en que la CPU (host) asigna trabajo a la GPU
  • Si los kernels de GPU son demasiado pequeños o hay demasiados, la GPU puede quedarse esperando trabajo
  • Los frameworks modernos introducen ejecución asíncrona, poniendo en cola los command streams con anticipación para minimizar la sobrecarga

Dos estrategias clave para mejorar el rendimiento: Fusion y Tiling

Operator Fusion (fusión de operaciones)

  • En una cadena de operaciones simple, por ejemplo y = relu(x + 1), si cada operación corre en un kernel separado, los datos van y vienen de la memoria global
  • Fusion combina varias operaciones en un solo kernel, evitando guardar valores intermedios en memoria global; en su lugar, procesa en registros y solo escribe el resultado final
  • Ejemplos: compiladores JIT como Triton y torch.compile Inductor automatizan este proceso

Tiling

  • En operaciones complejas como la multiplicación de matrices, el modelo de un solo hilo tiene una AI baja
  • Después de dividir en tiles por bloque, todos los hilos del bloque cooperan para cargar tiles de datos en Shared Memory, logrando una gran reutilización de datos
  • El cómputo sigue un patrón de 3 pasos: "Load (global -> Shared Memory) - Synchronize (sincronización) - Compute (cómputo)"

Coalesced Load y vectorización

  • Al mover datos de la memoria global a Shared Memory, es importante el Coalesced Access (los 32 hilos de un warp acceden a una región contigua de 128 bytes)
  • Con vectorización (por ejemplo, float4) se cargan varios datos a la vez, ahorrando recursos de hardware y aprovechando al máximo el ancho de banda de memoria
  • La alineación de datos es indispensable, y el valor K de bytes en la matriz debe ser múltiplo de 4 para ser eficiente

Bancos de Shared Memory y conflictos de bancos

  • Shared Memory está compuesta por 32 bancos independientes, y para evitar conflictos los 32 hilos de un warp deben acceder a bancos distintos
  • El acceso por fila no genera conflictos, pero el acceso por columna sí los genera (acceso al mismo banco)
  • En el tile B se usa la estrategia de "cargar y transponer", guardándolo transpuesto en Shared Memory para que durante el cómputo predomine el acceso por fila y así evitar conflictos de bancos

Patrones de cómputo rápido dentro del chip

Estrategia básica 1: un hilo calcula una salida

  • Bajo la restricción BLOCK_DIM=32, la AI máxima es 8, así que no puede entrar a la zona limitada por cómputo

Estrategia 2: un hilo calcula varias salidas

  • Si se configura BLOCK_DIM=16 y TILE_DIM=64, un hilo calcula una salida 4x4 → AI=16
  • Como AI>13, en una A100 se puede alcanzar rendimiento limitado por cómputo
  • Es posible lograr cómputo eficiente con cargas vectorizadas como float4 desde Shared Memory

Límite real del tiling: cuantización de tiles

  • Si el tamaño de la matriz no es múltiplo del tamaño del tile, los bloques de borde calculan un área mayor a la real (cómputo innecesario) y requieren padding
  • Los hilos del borde evitan accesos innecesarios a memoria con condiciones de guardia, pero el bucle de cómputo sigue ejecutándose igual y produce cálculos basura (por ejemplo, C += A * 0)

Factores adicionales de ajuste de rendimiento

Ocupación (Occupancy) y ocultamiento de latencia

  • Cuando un warp queda esperando mucho tiempo, por ejemplo en una lectura de memoria, el SM cambia de inmediato a otro warp para reducir el tiempo ocioso (latency hiding)
  • Si se asignan varios Thread Block al mismo tiempo, una ocupación alta minimiza el tiempo de espera
  • Si el tamaño del Block o del tile es demasiado grande, disminuye la cantidad de resident blocks y baja la ocupación, lo que degrada el rendimiento

Minimizar la divergencia de hilos

  • Si dentro de un warp aparece una bifurcación if-else, ambos caminos se ejecutan secuencialmente y el rendimiento efectivo puede caer a la mitad
  • Es necesario minimizar bifurcaciones con código sin ramas, como min o max

Cuantización (Quantization)

  • Al reducir precisión de FP32 a FP16/BFP16, la cantidad de datos movidos en memoria y la cantidad de datos procesables se duplican respectivamente
  • En una A100, las operaciones FP16 pueden alcanzar 312 TFLOPS (hasta 16 veces el rendimiento de los 19.5 TFLOPS de FP32)
  • Con cuantización se puede avanzar simultáneamente hacia la derecha en AI del Roofline (eficiencia de memoria) y hacia arriba (rendimiento máximo de cómputo)

Resumen general

  • La limitación esencial del rendimiento en GPU proviene del desequilibrio entre el ancho de banda de memoria y la capacidad de cómputo dentro del chip
  • La mejora de rendimiento se logra mediante maximizar la reutilización de datos (Tiling) y minimizar el tráfico intermedio de memoria (Fusion)
  • Hay que entender las características del hardware (warps, bancos, accesos coalescidos, sincronización) para poder escribir y optimizar kernels de alto rendimiento
  • En la práctica, factores adicionales como la ocupación, la minimización de bifurcaciones y la cuantización impactan directamente la velocidad real
  • El diseño de cómputo GPU de alto rendimiento requiere considerar de forma conjunta la mejora teórica de AI, el aprovechamiento de las características del hardware y la adaptación al acomodo y tamaño real de los datos

1 comentarios

 
GN⁺ 2025-06-25
Comentario de Hacker News
  • Curiosidad sobre qué tan bien se está haciendo la optimización de programas completos a nivel de compilador; la forma actual de optimizar cada arquitectura de LLM por separado da la impresión de estar algo rezagada.

  • Comparte la experiencia de haber intentado ejecutar llama.cpp y vllm en la misma 4070 para procesar más prompts en lote; desde batch 8, llama.cpp se vuelve gravemente lento y, aunque el uso de GPU parece aceptable, en realidad hay un cuello de botella, mientras que vllm lo maneja mucho mejor.

    • vllm usa un caché KV paginado y un layout fully coalesced que la GPU prefiere, ofreciendo rendimiento optimizado para lotes; en cambio, llama.cpp usa un layout plano bueno para un solo prompt, así que en escenarios con batch se rompe el patrón de acceso a memoria L2 y baja la velocidad.

    • Comparte que, al intercalar el tensor KV en llama.cpp de [seq, head, dim] a la forma [head, seq, dim], siguiendo cómo vllm alimenta los datos a un fused attention kernel, obtuvo de inmediato una mejora de alrededor de 2x en rendimiento de cómputo.

    • Señala que la causa del cuello de botella no era la GPU en sí, sino cómo se diseña el acceso a shared memory y las lecturas globales; vllm ataca justo ese punto con el cambio de layout.

    • Analizar este cuello de botella le tomó más de dos días; no podía detectarse solo mirando la gráfica de uso de GPU, y casi todo lo descubrió por prueba y error.

    • Plantea la duda de si existe alguna forma de repetir este tipo de experimentos más fácilmente, de manera iterativa y con hot reload.

    • Aunque dijo que la GPU no era el cuello de botella, señalan que en la práctica la ineficiencia del layout de memoria sí terminaba siendo un cuello de botella al reducir la eficiencia de cómputo de la GPU.

    • Mencionan el proyecto nano-vllm, publicado ayer por un empleado de DeepSeek; comparten que solo tiene 1200 líneas y aun así registró mejor rendimiento que vanilla vllm https://github.com/GeeeekExplorer/nano-vllm

    • Preguntan si subió el layout modificado a llama.cpp como pull request; opinan que una mejora de 2x podría beneficiar mucho a todos.

    • Recomiendan probar también un proyecto llamado ik_llama.cpp https://github.com/ikawrakow/ik_llama.cpp

  • Comentan que es un artículo con muy buena información y que, en realidad, trata sobre los factores que NVIDIA elige al desarrollar la arquitectura de sus GPU; enfatizan que no debe malinterpretarse la diferencia con otros fabricantes.

    • Por ejemplo, AMD Instinct MI300 alcanza hasta 160 TFLOPS en FP32 y 6TB/s de ancho de banda HBM3/3E, por lo que cambia el ridge-point: 27 FLOPs/byte, el doble de los 13 FLOPs/byte del A100; además, su memoria HBM de gran capacidad (128~256GB) también modifica los trade-offs reales entre tiling depth y occupancy. Eso sí, estas GPU son caras y tienen el trade-off de no soportar CUDA.

    • Opinan que, hasta que AMD preste más atención al software de cómputo, las GPU de NVIDIA seguirán siendo las únicas con presencia real.

  • Como spoiler, enfatizan que lo realmente importante no es tanto cómo funciona la GPU en sí, sino cómo se aprovecha para cálculos de machine learning.

    • Señalan que, en esencia, el contenido se parece más a un resumen general de CUDA y que, salvo el ejemplo de relu y la mención de torch, no tiene mucha relación con machine learning.
  • Opinan que es indispensable usar colores con suficiente contraste y resaltan la importancia de la legibilidad.

    • Comparten su experiencia con font-weight: 300: muchos diseñadores en Mac desarrollan ajustándose a las opciones de font smoothing y lo configuran para que visualmente parezca "normal"; como Mac hace que incluso las fuentes delgadas se vean medio más gruesas, los diseñadores tienden a usar tipografías más finas para dar una sensación de "normalidad". Comparten un enlace relacionado https://news.ycombinator.com/item?id=23553486

    • Especulan que el autor podría estar editando y maquetando en modo oscuro; mencionan que, si se aplica edge://flags/#enable-force-dark, los enlaces se ven bien.

    • Señalan que les costó especialmente leer los enlaces y los comentarios dentro de los bloques de código; proponen aumentar el contraste y evalúan que la calidad del contenido en sí fue excelente.

    • Critican que el sitio web use transparencia alfa en el texto, lo cual reduce gravemente el contraste y es un gran error.

  • Sugieren que en realidad un mejor título sería algo más cercano a "hechos básicos sobre las GPU de Nvidia"; explican como contexto que el término WARP también es propio de las GPU modernas de Nvidia y que las GPU de Nvidia de alrededor de 2003 eran hardware solo para renderizado de videojuegos, completamente distinto de las GPU actuales de cómputo de propósito general. En resumen, el contenido de la publicación no sería una explicación general aplicable a todas las GPU.

  • Comentan que es un material introductorio realmente muy bueno y agradecen por ello; cuentan que, al armar personalmente una AI PC, pasaron días investigando sobre GPU y que este texto les ayudó mucho porque resume bien tanto lo esencial que hay que saber como las aplicaciones de alto valor agregado (IA generativa). En particular, valoran mucho el diagrama de la jerarquía de memoria de la GPU A100.

  • Expresan extrañeza por el uso de diagramas ASCII.