13 de septiembre de 2026 · 5 min de lectura

Fusión de kernels en GPU: por qué una optimización que anda en un backend rompe otro sin avisar

modelos open-source inferencia
Nicolás Neira
Nicolás Neira
Arquitectura de agentes e IA aplicada

Optimizar un kernel de GPU tiene dos formas de fallar. Una es ruidosa: el proceso crashea en el primer token generado. La otra es silenciosa: el modelo sigue respondiendo bien, solo que un poco más lento, y nadie lo nota hasta que alguien corre un benchmark y se pregunta por qué bajó el throughput. Esta semana, los builds de llama.cpp mostraron ejemplos de ambas, y el patrón que dejan es más útil que cualquier build individual.

Cuando el compilador del vendor no coopera

El build b10891 documentó un caso extremo: el compilador Vulkan propietario de Imagination (PowerVR) devuelve VK_ERROR_UNKNOWN al crear los compute pipelines para todo shader de dequantización mul_mat_vec construido con reducción basada solo en subgroups, que exige un subgroup size de al menos 16. Eso cubre los k-quants, los i-quants, TQ2_0, MXFP4 y NVFP4. El fallo es independiente del subgroup size (32, 64 y 128 fallan igual), y como ggml relanza la excepción, el primer token generado de cualquiera de esos modelos mata el proceso. La solución fue agregar una variante de reducción por memoria compartida, que compila y coincide con la referencia de CPU para q2_K a q6_K, aunque la variante híbrida agregada como alternativa cuesta 27% del throughput (3.78 contra 5.20 t/s en Qwen3.5-2B-Q4_K_M).

El build b10938 encontró algo parecido en NVIDIA: un bug del driver hace que dos colas del mismo VkDevice enviando trabajo simultáneamente puedan romper sincronización interna. La solución, hasta que el driver se arregle, fue envolver el queuesubmit en un mutex.

Cuando el bug no crashea, solo degrada

El build b10909 rehizo el sistema de fusión de operadores de Metal en una sola tabla compartida entre el optimizador de grafos y los encoders de cómputo. Durante ese trabajo apareció un bug de indexado: ggml_can_fuse_subgraph_ext espera que el arreglo de salidas contenga índices absolutos del grafo, pero la consulta a la tabla de fusión pasaba un índice relativo. El resultado fue que el último nodo de cada patrón nunca se reconocía como salida, lo que activaba una verificación de conteo de usos que fallaba para casi todas las fusiones. Eso deshabilitó en silencio la fusión de norm más MUL y causó una regresión de aproximadamente 5% en generación de tokens, sin ningún error visible.

El mismo tipo de bug, pero de rendimiento en vez de correctitud, apareció en los kernels mul_mv de tipos IQ. El build b10863 corrigió idle threads en iq3_xxs para matrices con ne00 < 1024: cada thread procesa un chunk de 32 elementos, así que filas con menos de 32 chunks dejan la mitad del simdgroup sin trabajo. El build b10908 generalizó esa corrección a otros seis kernels con el mismo mapeo (iq1_s, iq1_m, iq2_xxs, iq2_xs, iq2_s, iq3_s), y su propia nota aclara que los k-quants tienen el mismo problema pero un mapeo de lanes distinto, así que quedan para un cambio separado. El bug de rendimiento no se arregla de una vez: se arregla kernel por kernel, cuantización por cuantización.

Cuando la corrección estructural tarda varios intentos

El build b10839 mostró cuánto cuesta arreglar bien un bug de alineación de buffers. El shader Vulkan de GET_ROWS asertaba cuando el offset del buffer de un tensor más su view_offs quedaba desalineado respecto al minStorageBufferOffsetAlignment, lo que antes crasheaba de forma dura en modelos que usan ggml_view más ggml_get_rows, como Qwen3-TTS y Qwen3-VL. El primer intento fue un fallback a CPU cuando se detectaba desalineación. Los commits siguientes, dentro del mismo release, encontraron que el path cuantizado (get_rows_quant.comp) no aplicaba los offsets por tensor que sí tenía el path no cuantizado, corrigieron el manejo del offset para operaciones binarias en general, y terminaron agregando cobertura de tests con view_src0=true para F32, F16, Q4_0, Q4_K, Q8_0 e I32, verificando los 223 casos de GET_ROWS en Vulkan sobre una NVIDIA RTX 5060 Ti. Ni siquiera el equipo que escribió el fallback inicial tenía, al principio, el fix estructural completo.

La lección

Un kernel de GPU nunca se prueba contra “GPU”: se prueba contra un vendor, un compilador y una configuración de subgroup específicos. Una optimización de fusión o de row-splitting que funciona perfecto en la GPU de desarrollo puede no compilar en absoluto en otro compilador propietario, romper sincronización en otro driver, o simplemente dejar la mitad de los threads sin trabajo en formas de tensor que nadie probó. Y cuando el bug no crashea, es peor: se mide en porcentaje de throughput perdido, no en un stack trace.

Reglas para no romper backends que no estás mirando

Prueba las optimizaciones que dependen de subgroup size contra más de un valor de subgroup, no asumas que 16 o más siempre está disponible: el fallo de PowerVR fue igual en 32, 64 y 128.

Si tu lógica de fusión indexa nodos de un grafo, usa índices absolutos, no relativos. Un índice mal calculado no crashea, deshabilita la optimización en silencio.

Cualquier operación sobre ggml_view o buffers con offset necesita tests explícitos con view_offs distinto de cero. Los paths de dequantización cuantizados suelen asumir alineación que el path no cuantizado ya maneja, y esa asimetría es donde aparecen los crashes.

Ante un bug de compilador o driver de un vendor específico (PowerVR, NVIDIA), agrega un fallback en runtime, no solo una bandera de capacidad: algunas GPUs directamente no van a compilar el shader optimizado.

Corre benchmarks de regresión de rendimiento, no solo de correctitud. Una regresión de 5% en generación de tokens por un bug de fusión pasó sin error hasta que alguien la midió a propósito.

Fuentes: ggml-org/llama.cpp b10891, ggml-org/llama.cpp b10938, ggml-org/llama.cpp b10909, ggml-org/llama.cpp b10863, ggml-org/llama.cpp b10908, ggml-org/llama.cpp b10839

Seguir leyendo

Nicolás Neira

Esto se demuestra en video, y se sigue a diario en la comunidad

Las demos completas van al canal. El seguimiento release por release, adentro.