NVIDIA CUDA Tile (cuTile) es un modelo de programación basado en tiles que permite escribir kernels GPU en términos de operaciones a nivel de bloque (cargas, escrituras y multiplicación-acumulación de matrices) en lugar de coordinar manualmente threads, warps y memoria compartida.

cuTile.jl lleva la misma abstracción basada en tiles al lenguaje dinámico Julia, permitiendo escribir kernels GPU personalizados sin recurrir a NVIDIA CUDA C++. Estos kernels propios suelen ser críticos en el ecosistema científico de Julia, que abarca ecuaciones diferenciales, programación probabilística y simulaciones físicas.

cuTile Python tiene una biblioteca creciente de kernels optimizados para aceleración GPU. La capacidad de traducir esos kernels a cuTile.jl le da al ecosistema Julia acceso inmediato a implementaciones probadas en producción, en lugar de reescribir cada una desde cero.

Esta publicación cubre la traducción de kernels GPU entre lenguajes específicos de dominio (DSL): cómo portar kernels de cuTile Python a cuTile.jl. Muestra cómo:

  • Traducir kernels GPU entre cuTile Python y cuTile.jl, con un ejemplo completo de multiplicación de matrices lado a lado.
  • Evitar trampas semánticas que rompen traducciones ingenuas: indexado, broadcasting, layout de memoria y forma de los loops divergen entre los dos DSL, y los desajustes silenciosos producen resultados incorrectos sin errores de compilación.
  • Construir un workflow agéntico repetible basado en skills: el conocimiento de traducción se empaqueta como una skill de LLM en TileGym, que produce kernels Julia validados en una sola pasada.

¿Qué problema resuelve la traducción entre DSL?

Los frontends de cuTile Python y cuTile.jl comparten la misma abstracción de tiles, lo que hace que la traducción sea en gran medida algorítmica. Sin embargo, las diferencias acumuladas a nivel de superficie entre ambos lenguajes no son triviales: indexado base 0 vs. base 1, layout row-major vs. column-major, sintaxis de broadcasting y mapeo de la API del kernel.

Ninguna de estas traducciones es conceptualmente difícil, pero basta con dejar un ct.bid(0) que debería ser ct.bid(1) para producir corrupción silenciosa de datos. Si se usa * en vez de .* para multiplicar elemento a elemento, Julia silenciosamente ejecuta una multiplicación de matrices. Son la clase de bugs que cuestan horas.

Una abstracción compartida con un conjunto finito de errores recurrentes es ideal para un workflow asistido por IA, siempre que al modelo se le enseñe qué tiene que vigilar.

Traduciendo cuTile Python a cuTile.jl

El proceso se entiende mejor con código real. Los siguientes ejemplos vienen de TileGym, donde el equipo portó un conjunto de kernels cuTile Python a cuTile.jl y los empaquetó como un subproyecto Julia autocontenido.

Ejemplo de multiplicación de matrices

El ejemplo de referencia usa matmul, lo bastante complejo para mostrar los desafíos clave de la traducción. Más allá de las diferencias básicas de sintaxis, la traducción debe manejar la estructura del loop, la conversión a tensor cores TF32 y el cambio de layout row-major a column-major.

Python
@ct.kernel
def matmul_kernel(A, B, C, tm: ct.Constant[int], tn: ct.Constant[int],
                  tk: ct.Constant[int]):
    bid_m = ct.bid(0)
    bid_n = ct.bid(1)

    num_k = ct.num_tiles(A, axis=1, shape=(tm, tk))
    acc = ct.full((tm, tn), 0, dtype=ct.float32)

    dtype = ct.tfloat32 if A.dtype == ct.float32 else A.dtype

    for k in range(num_k):
        a = ct.load(A, index=(bid_m, k), shape=(tm, tk),
                    padding_mode=ct.PaddingMode.ZERO)
        b = ct.load(B, index=(k, bid_n), shape=(tk, tn),
                    padding_mode=ct.PaddingMode.ZERO)
        a = a.astype(dtype)
        b = b.astype(dtype)
        acc = ct.mma(a, b, acc)

    acc = ct.astype(acc, C.dtype)
    ct.store(C, index=(bid_m, bid_n), tile=acc)
Julia
function matmul_kernel(A::ct.TileArray{T,2}, B::ct.TileArray{T,2}, C::ct.TileArray{T,2},
                      tm::Int, tn::Int, tk::Int) where {T}
    bid_m = ct.bid(1)
    bid_n = ct.bid(2)

    num_k = ct.num_tiles(A, 2, (tm, tk))
    acc = zeros(Float32, tm, tn)

    U = T === Float32 ? ct.TFloat32 : T

    for k in Int32(1):num_k
        a = ct.load(A; index=(bid_m, k), shape=(tm, tk), padding_mode=ct.PaddingMode.Zero)
        b = ct.load(B; index=(k, bid_n), shape=(tk, tn), padding_mode=ct.PaddingMode.Zero)
        a = convert(ct.Tile{U}, a)
        b = convert(ct.Tile{U}, b)
        acc = muladd(a, b, acc)
    end

    acc = convert(ct.Tile{T}, acc)
    ct.store(C; index=(bid_m, bid_n), tile=acc)
    return
end

Más allá de los cambios de sintaxis, hay que tener en cuenta:

  • El layout cambia: el A(M,K) row-major de Python se convierte en A_jl(K,M) column-major en Julia. El acumulador, los índices de carga y los índices de escritura cambian en consecuencia. Si se equivoca la forma del acumulador (por ejemplo (TM, TN) en lugar de (TN, TM)), el resultado será incorrecto sin que el compilador avise.
  • ct.mma se mapea a muladd: cuTile.jl traduce la operación matrix multiply-accumulate al muladd estándar de Julia, y ct.PaddingMode.ZERO pasa a ser ct.PaddingMode.Zero (PascalCase).

Ejemplo de softmax

El softmax sube la apuesta. En Julia se implementaron tres estrategias (TMA single-tile, online y chunked) para distintos tamaños de tensor. Sobre los patrones del matmul, la función softmax incorpora sintaxis broadcast con punto (ct.exp(ct.sub(a, b)) se vuelve exp.(a .- b)), reducciones renombradas (ct.max → maximum, ct.sum → sum, eje +1) y ct.maximum(a, b) elemento a elemento que pasa a max.(a, b).

Pero el desafío real no es la sintaxis: es mantener correctos los estadísticos de máximo y suma corriente a lo largo de la traducción.

¿Cómo se construye una skill agéntica reutilizable?

Figura 1. La skill de conversión empaqueta reglas de traducción, mapeos de API, ejemplos, validación y tests en un único workflow reutilizable.
Figura 1. La skill de conversión empaqueta reglas de traducción, mapeos de API, ejemplos, validación y tests en un único workflow reutilizable.

El resultado principal del proyecto no fueron los kernels traducidos: fue la skill construida para producirlos.

Una skill, en este contexto, es un directorio de conocimiento estructurado que vive dentro del repositorio y que un agente LLM levanta automáticamente. La ruta de esta skill en particular es: .claude/skills/converting-cutile-to-julia/.

Código
.claude/skills/converting-cutile-to-julia/
├── SKILL.md                           # Punto de entrada: vista general del workflow, principales trampas
├── translations/
│   └── workflow.md                    # Conversión paso a paso con checklists
├── references/
│   ├── api-mapping.md                 # Tabla bidireccional Python↔Julia
│   ├── critical-rules.md              # 17 reglas (indexado, broadcasting, loops, ...)
│   ├── debugging.md                   # Diagnóstico de errores: MethodError, IRError, etc.
│   └── testing.md                     # Patrones de tests, tolerancias por dtype
├── scripts/
│   └── validate_cutile_jl.py          # Validador estático para anti-patrones comunes
└── examples/
    ├── 01_add/                        # Python→Julia para suma vectorial
    ├── 02_matmul/                     # Python→Julia para multiplicación de matrices
    └── 03_softmax/                    # Python→Julia para softmax (3 estrategias)

Solo el archivo critical-rules.md captura 17 trampas que el equipo encontró durante el porte.

También hay un script validador estático que detecta cosas como ct.bid(0) olvidados, for loops dentro de kernels y nombres de tipos al estilo Python, antes de correr en GPU. Con todo esto en su lugar, el modelo no tiene que redescubrir las reglas de conversión cada vez. Lee la skill, sigue el checklist y aplica las reglas.

La skill agéntica en TileGym

El entregable concreto es un subproyecto Julia bajo julia/ en TileGym, que es de código abierto:

Código
julia/
├── Project.toml                # Dependencias: CUDA.jl, cuTile.jl, NNlib.jl, Test
├── kernels/
│   ├── add.jl                  # 1D elemento a elemento con escala alpha
│   ├── matmul.jl               # MMA tiled 2D con layout column-major
│   └── softmax.jl              # 3 estrategias: TMA, online, chunked
└── test/
    ├── runtests.jl             # Test runner
    ├── test_add.jl
    ├── test_matmul.jl
    └── test_softmax.jl

Estos tres kernels fueron seleccionados deliberadamente. El kernel add es el más simple para probar la superficie completa de traducción. El de matmul añade estructura de loop, tensor cores y el cambio de layout. El de softmax introduce algoritmos multipass con invariantes que deben sobrevivir a la traducción. Cada kernel tiene tests que se comparan contra una implementación de referencia en CPU con tolerancias por dtype, incluyendo casos borde donde las dimensiones no se alinean al tamaño de los tiles.

¿Cuánto cuesta una conversión completa?

Con la skill en su lugar, el workflow para cada kernel quedó así:

  • Pre-flight: escanear el código fuente buscando patrones que requieran manejo especial (for loops, ct.mma, order=, etc.).
  • Convertir: aplicar el mapeo de API y las reglas críticas.
  • Validar: correr el validador estático.
  • Probar: ejecutar los tests de Julia contra implementaciones de referencia.
  • Arreglar: si algo falla, usar la guía de debugging, corregir y volver a correr.

Para una conversión representativa de GEMM (general matrix multiply), el proceso tomó alrededor de cuatro minutos y unos 78 mil tokens en un LLM frontier sin intervención manual. Los kernels siguientes fueron más rápidos porque los ejemplos y reglas ya estaban en el repositorio.

La conclusión, según NVIDIA, no es que la IA escribió el código.

"Es la capacidad de capturar lo aprendido en algo que el modelo pueda reutilizar la próxima vez. Un prompt puede decir 'ten cuidado con el indexado'. Una skill puede decir 'aquí están las 17 cosas específicas que salen mal, así se chequean y aquí hay un script que las atrapa automáticamente'", explica el equipo en el blog de NVIDIA Developer.

Ahora los próximos portes pueden partir desde un repositorio que ya tiene ejemplos funcionales, un mapeo de API probado, un validador estático y una guía de debugging. Cada uno cuesta menos esfuerzo que el anterior.

Una conclusión más amplia es que el desafío de usar IA en trabajo de sistemas no es la generación de código: es producir código correcto en dominios donde el compilador no atrapa errores semánticos. Codificar las reglas de dominio en control de versiones, junto al código que describen, es una forma de abordar este problema.

¿Cómo empezar a usar skills agénticas para portar kernels Python a Julia?

Para probar el subproyecto Julia y la skill de conversión basta clonar TileGym, explorar julia/kernels/ (con add.jl, matmul.jl y softmax.jl) y revisar la skill bajo .claude/skills/converting-cutile-to-julia/. Las dependencias declaradas en Project.toml incluyen CUDA.jl, cuTile.jl, NNlib.jl y Test, y los tests corren con el runtests.jl provisto.