Fundamentos del Lenguaje Mojo

Mojo es un lenguaje de programación construido sobre MLIR (Multi-Level Intermediate Representation), la infraestructura de compilador que también alimenta TensorFlow, grafos del compilador de PyTorch y LLVM. Está diseñado como superset de Python, es posible importar módulos Python directamente, con features de programación de sistemas: ownership, tipos SIMD, metaprogramación en compile time y manejo manual de memoria.

Así se ve código Mojo:

from math import sqrt
from sys.info import simdwidthof
from algorithm import vectorize

alias float_type = DType.float32
alias simd_width = simdwidthof[float_type]()

fn euclidean_distance(
 a: DTypePointer[float_type],
 b: DTypePointer[float_type],
 n: Int
) -> Float32:
 var sum = SIMD[float_type, simd_width](0)

 @parameter
 fn compute_chunk[simd_width: Int](i: Int):
 let diff = a.load[width=simd_width](i) - b.load[width=simd_width](i)
 sum += diff * diff

 vectorize[compute_chunk, simd_width](n)
 return sqrt(sum.reduce_add())

Desempaquemos:

  1. alias define constantes de compile time. simd_width se resuelve en compile time basado en el ancho de registros SIMD de tu CPU (4 para SSE, 8 para AVX2, 16 para AVX-512).

  2. SIMD[float_type, simd_width] es un tipo vector SIMD explícito. Cuando escribirs a.load[width=simd_width](i), se está cargando 8 floats a la vez en un registro de 256 bits (en AVX2).

  3. @parameter hace que la función interna sea un parámetro de compile time, habilitando loop unrolling y especialización.

  4. vectorize es una función de la librería estándar que maneja el tail loop (cuando n no es múltiplo de simd_width).

Esto no es Python con type hints. Es un lenguaje de sistemas que parece Python.

El Modelo de Ownership: Ni Rust, Ni C++

Mojo tiene semántica de ownership, pero deliberadamente más simple que el borrow checker de Rust. Tres modos:

# `owned` - la función toma ownership, el caller no puede usar el valor después
fn consume(owned data: Tensor[float_type]):
 process(data)

# `borrowed` (default) - referencia inmutable, sin copia
fn inspect(data: Tensor[float_type]):
 print(data.shape())

# `inout` - referencia mutable
fn modify(inout data: Tensor[float_type]):
 data *= 2.0

fn main():
 var tensor = Tensor[float_type](1000, 1000)

 inspect(tensor) # borrowed, tensor sigue válido
 modify(tensor) # inout, tensor modificado in place
 consume(tensor^) # owned, tensor movido (^ es el operador de transferencia)
 # tensor ya no es válido aquí

El operador de transferencia ^ es la versión de Mojo de std::move. Es explícito, es posible ver cada transferencia de ownership en el código.

Benchmarks Reales: Pipeline de Inferencia ML

El siguiente benchmark usa un pipeline de embedding de texto, el tipo de cosa que se usaría para RAG en producción. El pipeline: tokenizar texto, pasarlo por un modelo transformer, normalizar los embeddings, computar similitudes coseno contra una base de datos vectorial.

Setup de test:

  • Hardware: c6i.4xlarge (16 vCPU, 32GB, Intel Ice Lake con AVX-512)
  • Modelo: sentence-transformers/all-MiniLM-L6-v2 (embeddings de 384 dims)
  • Dataset: 10.000 pasajes de texto, promedio 128 tokens
Operación Python (NumPy) Python (PyTorch) Mojo Rust (ndarray) C++ (Eigen)
Tokenización (10K textos) 2.8s 2.8s 2.9s* N/A N/A
Embedding (10K, batched) 4.2s 3.8s 3.9s** N/A N/A
Normalizar (10K x 384) 12ms 8ms 1.4ms 1.6ms 1.3ms
Similitud coseno (100 x 10K) 89ms 42ms 6.1ms 6.8ms 5.9ms
Selección Top-10 34ms 28ms 4.2ms 4.8ms 4.1ms
End-to-end 7.13s 6.70s 6.55s - -

*La tokenización de Mojo llama la librería Python tokenizers vía interop **El embedding de Mojo usa modelo PyTorch vía interop

El titular: Mojo es 6-8x más rápido que NumPy y 2-4x más rápido que PyTorch en las operaciones numéricas puras. Pero está dentro del 10% de Rust y C++ en esas mismas operaciones. Y el tiempo end-to-end del pipeline está dominado por el paso de inferencia del modelo, para el que todos llaman a PyTorch.

Esta es la realidad de la que nadie habla. Para las operaciones donde Mojo brilla, math vectorizada en tensores dense es, se está compitiendo con implementaciones BLAS/LAPACK altamente optimizadas que NumPy y PyTorch ya usan vía MKL u OpenBLAS. La ventaja de Mojo es cuando teners kernels de cómputo custom que no se pueden expresar como llamadas BLAS estándar.

Donde Mojo Realmente Gana

Pipelines de preprocesamiento custom. Un pipeline de limpieza de texto que aplica regex, normalización Unicode, filtrado de tokens y feature hashing. En Python, eran 340ms para 10K documentos. En Mojo con paralelización: 28ms. Un speedup de 12x, porque la versión Python no podía paralelizar fácilmente la manipulación de regex + strings.

Funciones de loss y métricas custom. Si escribirs código de research con funciones de loss no estándar, Mojo te deja escribirlas con SIMD explícito sin bajar a C++:

fn focal_loss(
 predictions: Tensor[DType.float32],
 targets: Tensor[DType.float32],
 gamma: Float32,
 alpha: Float32,
 n: Int
) -> Float32:
 var total_loss = SIMD[DType.float32, 8](0)

 @parameter
 fn compute[simd_width: Int](i: Int):
 let p = predictions.load[width=simd_width](i)
 let t = targets.load[width=simd_width](i)
 let pt = t * p + (1 - t) * (1 - p)
 let focal_weight = (1 - pt) ** gamma
 let ce = -(t * log(p) + (1 - t) * log(1 - p))
 total_loss += alpha * focal_weight * ce

 vectorize[compute, 8](n)
 return total_loss.reduce_add() / n

La Interop con Python: Seamless Pero No Gratis

El overhead de cruzar la frontera Python/Mojo es ~2-5 microsegundos por llamada. Negligible para operaciones gruesas (inferencia de modelo) pero devastador en loops ajustados. Resultados de medición:

Overhead de cruce de frontera (por llamada):
 Escalar simple: 2.1 µs
 Array chico (100 elementos): 3.8 µs
 Array grande (1M elementos): 4.2 µs + tiempo de copia

La copia del array es el costo real para tensores grandes. Mojo puede compartir memoria con arrays NumPy (zero-copy), pero se necesita usar DTypePointer y manejar el lifetime cuidadosamente.

Mojo vs Rust: La Comparación Honesta

Acá va una comparación directa:

Mojo gana en: Acceso al ecosistema Python, ergonomía específica para ML (tipos SIMD, operaciones de tensor, metaprogramación @parameter), facilidad de adopción para developers Python.

Rust gana en: Garantías de memory safety (el ownership de Mojo es más simple pero menos seguro), madurez del ecosistema (crates.io tiene 140K+ crates), tooling (cargo, clippy, rust-analyzer son best-in-class), deployment cross-platform.

Empatan en: Performance numérica cruda. Ambos compilan a LLVM IR (Mojo vía MLIR). Para el mismo algoritmo con SIMD explícito, van a estar dentro del 5-10% del otro.

La pregunta real es: ¿quién escribe el código? Para equipos de developers Python construyendo infraestructura ML, Mojo es el camino de menor resistencia. Para equipos de programadores de sistemas construyendo infraestructura general, Rust es la mejor elección.

Lo Que Falta

Mojo es pre-1.0 y se nota:

Librería estándar escasa. Sin runtime async, sin cliente HTTP, sin drivers de base de datos. Llamás a Python para todo eso, lo que funciona pero anula la ventaja de performance.

Mensajes de error crípticos. El compilador basado en MLIR produce errores que referencian representaciones intermedias en vez de código fuente.

Compilador closed-source. La librería estándar de Mojo es open-source, pero el compilador es propietario. Esto es un factor de riesgo significativo para adopción a largo plazo.

Evaluación

Mojo es el lenguaje nuevo más interesante para infraestructura ML en años. Pero el framing de "superset de Python" lo sobrevende. En la práctica, el patrón es: Mojo para los hot paths y Python para todo lo demás.

El número de 68.000x es engañoso sin contexto. El speedup real en workloads ML de producción es 2-10x sobre Python optimizado, y 0.9-1.1x comparado con C++/Rust. Ese 2-10x importa cuando ahorra horas de GPU, pero no es la revolución que el marketing sugiere. Es una evolución, y una buena.