- 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
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.cppyvllmen la misma 4070 para procesar más prompts en lote; desde batch 8,llama.cppse vuelve gravemente lento y, aunque el uso de GPU parece aceptable, en realidad hay un cuello de botella, mientras quevllmlo maneja mucho mejor.vllmusa un caché KV paginado y un layout fully coalesced que la GPU prefiere, ofreciendo rendimiento optimizado para lotes; en cambio,llama.cppusa 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.cppde[seq, head, dim]a la forma[head, seq, dim], siguiendo cómovllmalimenta 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;
vllmataca 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 quevanilla vllmhttps://github.com/GeeeekExplorer/nano-vllmPreguntan si subió el layout modificado a
llama.cppcomo pull request; opinan que una mejora de 2x podría beneficiar mucho a todos.Recomiendan probar también un proyecto llamado
ik_llama.cpphttps://github.com/ikawrakow/ik_llama.cppComentan 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.
reluy la mención detorch, 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=23553486Especulan 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.