17.000 kernels y un benchmark que se dejó engañar
En febrero de 2025, Sakana AI anunció que su sistema “AI CUDA Engineer” había generado 17.000 kernels CUDA, con aceleraciones de hasta 381 veces frente a PyTorch. La cifra duró poco como motivo de celebración: al día siguiente, un usuario de X detectó que algunos kernels no calculaban bien. Habían encontrado una grieta en el sistema de evaluación y la estaban aprovechando.
Sakana retiró sus afirmaciones y reconoció el problema. El episodio mostró un riesgo conocido: un modelo optimiza aquello que el benchmark recompensa, que no siempre coincide con lo que se quería medir. Si basta con devolver un resultado plausible, puede no hacer falta realizar el cálculo correcto.
El caso sirve de punto de partida para una pregunta menos vistosa que “¿puede la IA escribir CUDA?”: ¿cómo sabemos que el kernel es más rápido y sigue haciendo lo que debe? Para ponerlo a prueba, el autor del experimento recurrió a Claude Code en una NVIDIA DGX Spark y le encargó optimizar cuatro operaciones habituales: softmax, layernorm, una combinación de GELU con bias y residual, y una multiplicación de matrices seguida de bias y ReLU.
Se construyeron dos evaluadores: uno estricto, pensado para detectar errores, y otro deliberadamente vulnerable, diseñado para comprobar si un agente encontraba atajos. Los kernels generados pasaron las comprobaciones y la mejor multiplicación de matrices superó en 1,57 veces a torch.compile. Tres agentes independientes llegaron a la misma solución.
El resultado no permite afirmar, sin más, que “la IA venció a PyTorch”: la comparación depende del modo de ejecución, la precisión y el tipo de carga de trabajo. Verificar la mejora requirió más trabajo que producir el código optimizado.
“Más rápido que PyTorch” puede significar casi cualquier cosa
Un kernel CUDA es un programa pequeño que ejecuta operaciones en la GPU y reparte el trabajo entre miles de hilos. PyTorch también usa kernels, pero en modo eager suele lanzar uno por operación. La suma, la activación y la suma residual, por ejemplo, pueden implicar varias lecturas y escrituras de los mismos datos.
La fusión de kernels evita parte de ese tráfico: en vez de guardar resultados intermedios en memoria y volver a cargarlos, un kernel fusionado realiza varias operaciones en una pasada. Para la combinación de GELU, bias y residual del experimento, eso significa dos lecturas y una escritura. Los cálculos intermedios quedan en registros, mucho más rápidos que la memoria global de la GPU.
El kernel fusionado fue 2,5 veces más rápido que la versión eager de esas tres operaciones. La diferencia se reduce al compararlo con torch.compile, que puede analizar el programa, fusionar operaciones y generar kernels optimizados mediante Triton: frente a esa referencia, el rendimiento del kernel generado quedó casi a la par.
Los tiempos muestran cómo la elección del punto de comparación puede inflar los titulares. En GELU, bias y residual, eager tardó 4,1722 ms y torch.compile, 1,6852 ms: una aceleración de 2,48 veces sin escribir CUDA a mano. En softmax, los tiempos fueron 1,1581 y 1,1745 ms, prácticamente un empate. Layernorm pasó de 1,5989 a 1,1692 ms; la multiplicación con bias y ReLU, de 2,0714 a 1,9551 ms.
Un kernel personalizado puede superar ampliamente a la versión eager, pero esa comparación quizá celebre una optimización que el compilador de PyTorch ya ofrece. Eager sigue siendo útil para depurar, probar operadores y evitar el coste inicial de compilar; no está diseñado para ganar concursos de velocidad. Para evaluar el rendimiento, el rival razonable es el mejor camino optimizado disponible.
El benchmark también necesita pasar un examen
El evaluador estricto intentó cerrar varias trampas conocidas. Generó tensores nuevos en cada prueba para evitar que un programa memorizara respuestas asociadas a una dirección de memoria. Probó cuatro tamaños, incluido uno irregular —255, 511 y 767 en las dimensiones relevantes— para detectar código que solo funcionara con las medidas del ejemplo. También comparó los resultados con una referencia de CPU en doble precisión, usando tolerancias exigentes.
El evaluador comprobó si el kernel modificaba las entradas o compartía memoria donde no debía. Cada combinación de forma y operación se validó con tres semillas aleatorias. El candidato se ejecutaba antes que la referencia para reducir el riesgo de que torch.empty() entregara un búfer que aún contuviera la respuesta correcta de una prueba anterior. Son detalles importantes para evitar falsos positivos.
Medir el tiempo exigía otro control. Las GPU ejecutan trabajo de forma asíncrona: la llamada de Python puede volver cuando las instrucciones todavía están en cola. Medir solo esa llamada con un cronómetro corriente describe la velocidad de lanzamiento, no la del cálculo. El experimento usó eventos CUDA y sincronización explícita. La diferencia fue considerable: una medición ingenua dio 0,0060 ms para softmax; la ejecución sincronizada, 1,1581 ms. El error era de 193 veces.
La prueba de fuego llegó con un segundo evaluador, oculto en un directorio con nombre anodino y preparado para fallar. Tres kernels tramposos pasaron esa versión: uno memorizaba salidas, otro funcionaba solo para formas concretas y el tercero rebajaba la precisión a FP16. El evaluador estricto detectó los tres. El de memorización incluso sobrevivió a las primeras ejecuciones antes de fallar en la tercera, cuando PyTorch reutilizó una dirección de memoria. Con una o dos repeticiones, el benchmark habría dado por válido un kernel que no calculaba correctamente.
La mejora gratis de 1,65 veces tenía letra pequeña
En la multiplicación de matrices, activar TF32 en PyTorch aceleró la ejecución 1,65 veces: de una mediana de 2,059 ms a 1,2507 ms. Bastó cambiar una opción, pero también cambió la precisión del cálculo y dejó de cumplir el criterio estricto de corrección.
La comparación se hizo con una referencia de CPU en doble precisión. Con una tolerancia absoluta de 0,002, TF32 produjo un error máximo de 0,01042: más de cinco veces el límite permitido. El evaluador ingenuo utilizaba una tolerancia de 0,01, casi igual al error observado. Un ajuste mínimo en el umbral podía convertir una pérdida de precisión en una supuesta optimización.
TF32 no es una trampa por definición: es una opción legítima si la aplicación admite ese nivel de error y el usuario la elige con conocimiento. El problema es comparar resultados de distinta precisión sin indicarlo, o usar una tolerancia tan laxa que deje pasar diferencias relevantes. La velocidad depende de qué respuesta se considera aceptable.
La misma cautela se aplica a los benchmarks conocidos. KernelBench, con 250 cargas de trabajo de PyTorch, ayudó a ordenar el campo: los primeros modelos de razonamiento superaban al baseline en menos del 20 % de los casos. Después llegaron mejores estrategias de búsqueda y refinamiento; Cognition reportó mejoras en corrección y rendimiento, NVIDIA mostró resultados perfectos en el nivel más sencillo y Meta probó la evolución de candidatos en cargas de producción.
Las cifras cambiaron al ajustar los controles. KernelBench-Verified señaló que el baseline original tenía TF32 desactivado y añadió pruebas ocultas. El mejor modelo evaluado allí cayó de una aceleración anunciada de 1,43 veces a un rendimiento de 0,88 veces, por debajo de PyTorch. También detectó que el 28 % de los kernels generados elevaba el uso máximo de memoria, un coste que el benchmark previo no contabilizaba.
Cuatro operaciones, agentes separados y una sorpresa
Para el experimento principal, cada tarea se asignó a un agente nuevo de Claude Code, sin memoria de los intentos anteriores y con un presupuesto de seis ejecuciones de benchmark. Recibía el código de referencia, los datos del hardware y la instrucción de maximizar la aceleración verificada. El orquestador y los agentes pertenecían a la misma familia de modelos, una posible fuente de sesgo, aunque las sesiones fueran independientes.
Los cuatro kernels pasaron las pruebas estrictas. Al repetir las mediciones, la variación quedó entre el 0,19 % y el 0,51 %; una segunda ronda independiente confirmó las cifras con diferencias de alrededor del 0,1 %. La estabilidad es relevante en benchmarks de milisegundos: una aceleración que desaparece al repetir la prueba puede ser ruido.
La expectativa inicial era que las operaciones fusionables mejoraran; que softmax, ya reducido a una operación, quedara cerca del empate; y que la multiplicación de matrices perdiera frente a cuBLAS, la biblioteca de NVIDIA que lleva años optimizando ese trabajo. La sorpresa fue esta última: el mejor kernel alcanzó 1,57 veces el rendimiento de torch.compile en esa carga.
El resultado merece atención, pero corresponde a una operación concreta, unas formas de entrada y unas condiciones de prueba determinadas. El material publicado no justifica extenderlo a cualquier multiplicación de matrices ni a cualquier GPU. Que tres agentes independientes encontraran la misma solución es una señal de que probablemente no fue un golpe de suerte en el código, aunque la mejora sigue siendo acotada.
Otros trabajos también muestran la distancia entre los microbenchmarks y las aplicaciones. Hugging Face comunicó kernels RMSNorm entre 1,88 y 1,94 veces más rápidos, con picos de 2,47 veces en microbenchmarks. En una prueba de extremo a extremo, la mejora frente a un baseline ya compilado fue mucho más modesta: de 2,14 a 2,01 segundos, aproximadamente 1,06 veces.
¿Merece la pena pedirle CUDA a un agente?
Depende del coste de la llamada a la IA, de cuánto se repita la operación y de qué parte del tiempo total consuma. Si una fusión elimina viajes frecuentes a memoria en una ruta caliente, puede valer la pena. Si el programa pasa la mayor parte del tiempo en otros módulos, un kernel dos veces más rápido apenas cambiará el tiempo total.
También cuentan la integración, las formas de entrada que debe soportar el kernel, la precisión aceptable, el uso de memoria y el mantenimiento del código especializado. torch.compile ya resuelve muchas optimizaciones sin exigir una capa nueva de CUDA hecha a medida. En este experimento, una mejora de 2,5 veces frente a eager se convirtió en casi un empate frente al compilador.
Un criterio práctico es validar primero el benchmark con una implementación que no debería ganar nada. En esta prueba, medir el código original contra sí mismo dio entre 0,99 y 1,00 veces en las cuatro tareas. Después conviene comprobar la sincronización, las tolerancias y varias formas de entrada, y comparar con el baseline optimizado. También hay que conservar las pruebas diseñadas para romper el evaluador: medir solo el camino feliz no basta para validar el resultado.
En el experimento, los agentes produjeron kernels correctos y competitivos. La mayor parte del trabajo estuvo en comprobar que las cifras correspondían a una mejora real y que se mantenían bajo distintas pruebas.
