{"schemaVersion":1,"id":"https://lucioduran.com/blog/mojo-language-python-superset-performance","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance","title":"Mojo as a Python Superset: Performance on Production ML Workloads","description":"Benchmarking Mojo against Python, Rust, and C++ on actual ML inference pipelines. MLIR compilation, ownership model, SIMD types, and why the '68,000x faster than Python' claim needs serious context.","language":"en","datePublished":"2026-02-11","dateModified":null,"contentFormat":"text/markdown","content":"Programming Languages\n\n# Mojo as a Python Superset: Performance on Production ML Workloads\n\n2026-02-119 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/mojo-language-python-superset-performance>)\n\n## Mojo Language Fundamentals\n\n[Mojo](<https://www.modular.com/mojo>) is a programming language built on MLIR (Multi-Level Intermediate Representation), the compiler infrastructure that also powers TensorFlow, PyTorch compiler graphs, and LLVM. It's designed as a Python superset, you can import Python modules directly, with systems programming features: ownership, SIMD types, compile-time metaprogramming, and manual memory management.\n\nRepresentative Mojo code:\n\n```mojo\nfrom math import sqrt\nfrom sys.info import simdwidthof\nfrom algorithm import vectorize\n\nalias float_type = DType.float32\nalias simd_width = simdwidthof[float_type]()\n\nfn euclidean_distance(\n a: DTypePointer[float_type],\n b: DTypePointer[float_type],\n n: Int\n) -> Float32:\n var sum = SIMD[float_type, simd_width](0)\n\n @parameter\n fn compute_chunk[simd_width: Int](i: Int):\n let diff = a.load[width=simd_width](i) - b.load[width=simd_width](i)\n sum += diff * diff\n\n vectorize[compute_chunk, simd_width](n)\n return sqrt(sum.reduce_add())\n```\n\nA few things to unpack:\n\n1. **` alias `** defines compile-time constants. ` simd_width ` is resolved at compile time based on your CPU's SIMD register width (4 for SSE, 8 for AVX2, 16 for AVX-512).\n2. **` SIMD[float_type, simd_width] `** is an explicit SIMD vector type. When you write ` a.load[width=simd_width](i) `, you're loading 8 floats at once into a 256-bit register (on AVX2).\n3. **` @parameter `** makes the inner function a compile-time parameter, enabling loop unrolling and specialization.\n4. **` vectorize `** is a standard library function that handles the tail loop (when ` n ` isn't a multiple of ` simd_width `).\n\nThis is not Python with type hints. This is a systems language that looks like Python.\n\n## The Ownership Model: Not Rust, Not C++\n\nMojo has ownership semantics, but they're deliberately simpler than Rust's borrow checker. Three modes:\n\n```mojo\n# `owned` - the function takes ownership, caller can't use the value after\nfn consume(owned data: Tensor[float_type]):\n # data is ours, will be destroyed when function ends\n process(data)\n\n# `borrowed` (default) - immutable reference, no copy\nfn inspect(data: Tensor[float_type]):\n # data is read-only, still owned by caller\n print(data.shape())\n\n# `inout` - mutable reference\nfn modify(inout data: Tensor[float_type]):\n # data can be modified in place\n data *= 2.0\n\nfn main():\n var tensor = Tensor[float_type](1000, 1000)\n\n inspect(tensor) # borrowed, tensor still valid\n modify(tensor) # inout, tensor modified in place\n consume(tensor^) # owned, tensor moved (^ is the transfer operator)\n # tensor is no longer valid here\n```\n\nThe ` ^ ` transfer operator is Mojo's version of ` std::move `. It's explicit, you can see every ownership transfer in the code. This is simpler than Rust's implicit moves and explicit borrows, at the cost of less safety. Mojo won't catch use-after-move at compile time in all cases the way Rust does.\n\n## Real Benchmarks: ML Inference Pipeline\n\nThe following benchmark uses a text embedding pipeline, the kind of thing you'd use for RAG (Retrieval Augmented Generation) in production. The pipeline: tokenize text, run it through a transformer model, normalize the output embeddings, compute cosine similarities against a vector database.\n\n**Test setup:**\n\n- Hardware: c6i.4xlarge (16 vCPU, 32GB, Intel Ice Lake with AVX-512)\n- Model: sentence-transformers/all-MiniLM-L6-v2 (384-dim embeddings)\n- Dataset: 10,000 text passages, average 128 tokens\n- Task: Embed all passages and compute top-10 nearest neighbors for 100 queries\n\n### Embedding Computation\n\n```mojo\n# Mojo: Batched embedding normalization + cosine similarity\nfrom tensor import Tensor\nfrom algorithm import vectorize, parallelize\nfrom math import sqrt\n\nfn normalize_embeddings(\n inout embeddings: Tensor[DType.float32],\n num_vectors: Int,\n dim: Int\n):\n @parameter\n fn normalize_row(row: Int):\n var norm = SIMD[DType.float32, 8](0)\n let offset = row * dim\n\n @parameter\n fn accumulate[simd_width: Int](i: Int):\n let v = embeddings.load[width=simd_width](offset + i)\n norm += (v * v).reduce_add()\n\n vectorize[accumulate, 8](dim)\n let inv_norm = 1.0 / sqrt(norm.reduce_add())\n\n @parameter\n fn scale[simd_width: Int](i: Int):\n embeddings.store[width=simd_width](\n offset + i,\n embeddings.load[width=simd_width](offset + i) * inv_norm\n )\n\n vectorize[scale, 8](dim)\n\n parallelize[normalize_row](num_vectors)\n\nfn cosine_similarity_batch(\n queries: Tensor[DType.float32],\n database: Tensor[DType.float32],\n num_queries: Int,\n num_db: Int,\n dim: Int,\n inout results: Tensor[DType.float32]\n):\n @parameter\n fn process_query(q: Int):\n let q_offset = q * dim\n for d in range(num_db):\n let d_offset = d * dim\n var dot = SIMD[DType.float32, 8](0)\n\n @parameter\n fn dot_product[simd_width: Int](i: Int):\n dot += (\n queries.load[width=simd_width](q_offset + i) *\n database.load[width=simd_width](d_offset + i)\n ).reduce_add()\n\n vectorize[dot_product, 8](dim)\n results.store(q * num_db + d, dot.reduce_add())\n\n parallelize[process_query](num_queries)\n```\n\n### Benchmark Results\n\n| Operation | Python (NumPy) | Python (PyTorch) | Mojo | Rust (ndarray) | C++ (Eigen) |\n| --- | --- | --- | --- | --- | --- |\n| Tokenization (10K texts) | 2.8s | 2.8s | 2.9s\\* | N/A | N/A |\n| Embedding (10K, batched) | 4.2s | 3.8s | 3.9s\\*\\* | N/A | N/A |\n| Normalize (10K x 384) | 12ms | 8ms | 1.4ms | 1.6ms | 1.3ms |\n| Cosine sim (100 x 10K) | 89ms | 42ms | 6.1ms | 6.8ms | 5.9ms |\n| Top-10 selection | 34ms | 28ms | 4.2ms | 4.8ms | 4.1ms |\n| **End-to-end** | **7.13s** | **6.70s** | **6.55s** | - | - |\n\n\\*Mojo tokenization calls the Python tokenizers library via interop \\*\\*Mojo embedding uses PyTorch model via interop\n\nThe headline: Mojo is 6-8x faster than NumPy and 2-4x faster than PyTorch on the pure numerical operations (normalize, cosine similarity, top-k selection). But it's within 10% of Rust and C++ on those same operations. And the end-to-end pipeline time is dominated by the model inference step, which everyone calls PyTorch for.\n\nThis is the reality nobody talks about. For the operations where Mojo shines, vectorized math on dense tensors, you're competing with highly optimized BLAS/LAPACK implementations that NumPy and PyTorch already use via MKL or OpenBLAS. Mojo's advantage is when you have custom compute kernels that can't be expressed as standard BLAS calls.\n\n## Where Mojo Actually Wins\n\n**Custom preprocessing pipelines.** A text cleaning pipeline that applies regex, Unicode normalization, token filtering, and feature hashing. In Python, this was 340ms for 10K documents. In Mojo with parallelization: 28ms. A 12x speedup, because the Python version couldn't easily parallelize the regex + string manipulation.\n\n```mojo\nfrom python import Python\nfrom algorithm import parallelize\n\nfn preprocess_batch(\n texts: PythonObject,\n num_texts: Int\n) -> Tensor[DType.float32]:\n var features = Tensor[DType.float32](num_texts, 768)\n\n @parameter\n fn process_text(i: Int):\n let text = texts[i]\n # Clean text (Mojo string operations)\n var cleaned = text.lower()\n cleaned = remove_special_chars(cleaned)\n cleaned = normalize_unicode(cleaned)\n\n # Feature hashing (pure Mojo, vectorized)\n let tokens = tokenize_whitespace(cleaned)\n hash_features(tokens, features, i)\n\n parallelize[process_text](num_texts)\n return features\n```\n\n**Custom loss functions and metrics.** If you're writing research code with non-standard loss functions, Mojo lets you write them with explicit SIMD without dropping to C++:\n\n```mojo\nfn focal_loss(\n predictions: Tensor[DType.float32],\n targets: Tensor[DType.float32],\n gamma: Float32,\n alpha: Float32,\n n: Int\n) -> Float32:\n var total_loss = SIMD[DType.float32, 8](0)\n\n @parameter\n fn compute[simd_width: Int](i: Int):\n let p = predictions.load[width=simd_width](i)\n let t = targets.load[width=simd_width](i)\n\n # Focal loss: -alpha * (1-pt)^gamma * log(pt)\n let pt = t * p + (1 - t) * (1 - p)\n let focal_weight = (1 - pt) ** gamma\n let ce = -(t * log(p) + (1 - t) * log(1 - p))\n total_loss += alpha * focal_weight * ce\n\n vectorize[compute, 8](n)\n return total_loss.reduce_add() / n\n```\n\n## The Python Interop: Seamless but Not Free\n\nThe interop story is Mojo's biggest selling point and its most misunderstood feature. You can import any Python module:\n\n```mojo\nfrom python import Python\n\nfn main() raises:\n let np = Python.import_module(\"numpy\")\n let torch = Python.import_module(\"torch\")\n let transformers = Python.import_module(\"transformers\")\n\n # This works, you're calling Python objects from Mojo\n let model = transformers.AutoModel.from_pretrained(\"bert-base-uncased\")\n let tokenizer = transformers.AutoTokenizer.from_pretrained(\"bert-base-uncased\")\n\n let inputs = tokenizer(\"Hello world\", return_tensors=\"pt\")\n let outputs = model(**inputs)\n\n # But this crosses the Python/Mojo boundary\n # Every call has marshalling overhead\n let embeddings = outputs.last_hidden_state.detach().numpy()\n\n # Convert to Mojo tensor for fast operations\n var mojo_tensor = numpy_to_mojo_tensor(embeddings)\n normalize_embeddings(mojo_tensor, 1, 768)\n```\n\nThe overhead of crossing the Python/Mojo boundary is ~2-5 microseconds per call. That's negligible for coarse operations (model inference) but devastating if you do it in a tight loop. Measurement results:\n\n```\nBoundary crossing overhead (per call):\n Simple scalar: 2.1 µs\n Small array (100 elements): 3.8 µs\n Large array (1M elements): 4.2 µs + copy time\n```\n\nThe array copy is the real cost for large tensors. Mojo can share memory with NumPy arrays (zero-copy), but you need to use ` DTypePointer ` and manage the lifetime carefully.\n\n## Mojo vs Rust: The Honest Comparison\n\nHere is a direct comparison:\n\n**Mojo wins on:** Python ecosystem access, ML-specific ergonomics (SIMD types, tensor operations, ` @parameter ` metaprogramming), ease of adoption for Python developers.\n\n**Rust wins on:** Memory safety guarantees (Mojo's ownership is simpler but less safe), ecosystem maturity (crates.io has 140K+ crates), tooling (cargo, clippy, rust-analyzer are best-in-class), cross-platform deployment.\n\n**They tie on:** Raw numerical performance. Both compile to LLVM IR (Mojo via MLIR). For the same algorithm with explicit SIMD, you'll get within 5-10% of each other.\n\nThe real question is: who writes the code? For teams of Python developers building ML infrastructure, Mojo is the path of least resistance. For teams of systems programmers building general-purpose infrastructure, Rust is the better choice. The language capabilities overlap, but the developer experience diverges sharply.\n\n## What's Missing\n\nMojo is pre-1.0 and it shows:\n\n**Standard library is sparse.** No async runtime, no HTTP client, no database drivers. You call Python for all of that, which works but negates the performance advantage.\n\n**Error messages are cryptic.** The MLIR-based compiler produces error messages that reference intermediate representations instead of source code. This is improving but still rough.\n\n**Debugger support is limited.** No stepping debugger of production quality exists yet. Print debugging is the norm.\n\n**Package management is nascent.** Modular has a package system, but the community package ecosystem is tiny compared to pip or cargo.\n\n**Closed-source compiler.** The Mojo standard library is open-source, but the compiler is proprietary. This is a significant risk factor for long-term adoption. If Modular goes under, you're stuck.\n\n## Assessment\n\nMojo is the most interesting new language for ML infrastructure in years. But the \"Python superset\" framing oversells it. In practice, the pattern is: Mojo for the hot paths and Python for everything else. That's a valid and useful pattern, it's what Cython and C extensions have done for decades, but Mojo does it with much better ergonomics.\n\nIf you're building ML inference servers, custom data pipelines, or numerical computing libraries, Mojo is worth serious evaluation. If you're building web servers, CLI tools, or general-purpose applications, look elsewhere. The ecosystem isn't there yet, and the Python interop overhead means you're not getting Mojo-speed for anything that touches the Python world.\n\nThe 68,000x number is misleading without context. The real speedup on production ML workloads is 2-10x over optimized Python, and 0.9-1.1x compared to C++/Rust. That 2-10x matters when it saves GPU hours, but it is not the revolution the marketing suggests. It is an evolution, and a good one.\n\n## Frequently asked questions\n\n### Is Mojo a replacement for Python?\n\nNo. Mojo is a superset, valid Python is (mostly) valid Mojo. The value proposition is writing performance-critical code in Mojo while keeping your existing Python ecosystem. Think of it as 'Python for the 95% of your code that doesn't need to be fast, Mojo for the 5% that does.'\n\n### How does Mojo compare to Cython or Numba?\n\nCython and Numba are Python accelerators that add type annotations for JIT/AOT compilation. Mojo is a new language with its own compiler (MLIR-based) that happens to be Python-compatible. Mojo gives you ownership semantics, SIMD intrinsics, and compile-time metaprogramming that Cython/Numba can't match.\n\n### Should I learn Mojo for ML engineering?\n\nIf you write custom kernels, data preprocessing pipelines, or inference servers, yes, it's worth exploring. If you primarily use PyTorch/JAX at a high level, Mojo won't change your workflow much yet. The ecosystem is still young.\n\n## Tips\n\n1. **Tip:** Start Mojo files with ` from python import Python ` to call any Python library directly. NumPy, PyTorch, and scikit-learn all work, but the interop has marshalling overhead, so don't call Python in hot loops.\n2. **Tip:** Use ` SIMD[DType.float32, 8] ` for explicit vectorization instead of relying on auto-vectorization. Mojo's SIMD types map directly to hardware SIMD registers, you control the width, the compiler generates the instructions.\n3. **Tip:** The ` @parameter ` decorator in Mojo evaluates at compile time, similar to Zig's comptime. Use it for loop unrolling and specialization: ` @parameter for i in range(4): ` unrolls the loop at compile time.\n\n- mojo\n- python\n- mlir\n- simd\n- performance\n- machine-learning\n- systems-programming\n- rust\n- cpp\n- benchmarks\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Mojo as a Python Superset: Performance on Production ML Workloads","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#title"},{"level":2,"text":"Mojo Language Fundamentals","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#mojo-language-fundamentals"},{"level":2,"text":"The Ownership Model: Not Rust, Not C++","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#the-ownership-model-not-rust-not-c"},{"level":2,"text":"Real Benchmarks: ML Inference Pipeline","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#real-benchmarks-ml-inference-pipeline"},{"level":3,"text":"Embedding Computation","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#embedding-computation"},{"level":3,"text":"Benchmark Results","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#benchmark-results"},{"level":2,"text":"Where Mojo Actually Wins","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#where-mojo-actually-wins"},{"level":2,"text":"The Python Interop: Seamless but Not Free","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#the-python-interop-seamless-but-not-free"},{"level":2,"text":"Mojo vs Rust: The Honest Comparison","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#mojo-vs-rust-the-honest-comparison"},{"level":2,"text":"What's Missing","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#whats-missing"},{"level":2,"text":"Assessment","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#assessment"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#questions"},{"level":3,"text":"Is Mojo a replacement for Python?","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#question-1"},{"level":3,"text":"How does Mojo compare to Cython or Numba?","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#question-2"},{"level":3,"text":"Should I learn Mojo for ML engineering?","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#article-tips"}],"sections":[{"heading":"Mojo as a Python Superset: Performance on Production ML Workloads","level":1,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance","content":"Programming Languages"},{"heading":"Mojo as a Python Superset: Performance on Production ML Workloads","level":1,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#title","content":"2026-02-119 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/mojo-language-python-superset-performance>)"},{"heading":"Mojo Language Fundamentals","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#mojo-language-fundamentals","content":"[Mojo](<https://www.modular.com/mojo>) is a programming language built on MLIR (Multi-Level Intermediate Representation), the compiler infrastructure that also powers TensorFlow, PyTorch compiler graphs, and LLVM. It's designed as a Python superset, you can import Python modules directly, with systems programming features: ownership, SIMD types, compile-time metaprogramming, and manual memory management.\n\nRepresentative Mojo code:\n\n```mojo\nfrom math import sqrt\nfrom sys.info import simdwidthof\nfrom algorithm import vectorize\n\nalias float_type = DType.float32\nalias simd_width = simdwidthof[float_type]()\n\nfn euclidean_distance(\n a: DTypePointer[float_type],\n b: DTypePointer[float_type],\n n: Int\n) -> Float32:\n var sum = SIMD[float_type, simd_width](0)\n\n @parameter\n fn compute_chunk[simd_width: Int](i: Int):\n let diff = a.load[width=simd_width](i) - b.load[width=simd_width](i)\n sum += diff * diff\n\n vectorize[compute_chunk, simd_width](n)\n return sqrt(sum.reduce_add())\n```\n\nA few things to unpack:\n\n1. **` alias `** defines compile-time constants. ` simd_width ` is resolved at compile time based on your CPU's SIMD register width (4 for SSE, 8 for AVX2, 16 for AVX-512).\n2. **` SIMD[float_type, simd_width] `** is an explicit SIMD vector type. When you write ` a.load[width=simd_width](i) `, you're loading 8 floats at once into a 256-bit register (on AVX2).\n3. **` @parameter `** makes the inner function a compile-time parameter, enabling loop unrolling and specialization.\n4. **` vectorize `** is a standard library function that handles the tail loop (when ` n ` isn't a multiple of ` simd_width `).\n\nThis is not Python with type hints. This is a systems language that looks like Python."},{"heading":"The Ownership Model: Not Rust, Not C++","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#the-ownership-model-not-rust-not-c","content":"Mojo has ownership semantics, but they're deliberately simpler than Rust's borrow checker. Three modes:\n\n```mojo\n# `owned` - the function takes ownership, caller can't use the value after\nfn consume(owned data: Tensor[float_type]):\n # data is ours, will be destroyed when function ends\n process(data)\n\n# `borrowed` (default) - immutable reference, no copy\nfn inspect(data: Tensor[float_type]):\n # data is read-only, still owned by caller\n print(data.shape())\n\n# `inout` - mutable reference\nfn modify(inout data: Tensor[float_type]):\n # data can be modified in place\n data *= 2.0\n\nfn main():\n var tensor = Tensor[float_type](1000, 1000)\n\n inspect(tensor) # borrowed, tensor still valid\n modify(tensor) # inout, tensor modified in place\n consume(tensor^) # owned, tensor moved (^ is the transfer operator)\n # tensor is no longer valid here\n```\n\nThe ` ^ ` transfer operator is Mojo's version of ` std::move `. It's explicit, you can see every ownership transfer in the code. This is simpler than Rust's implicit moves and explicit borrows, at the cost of less safety. Mojo won't catch use-after-move at compile time in all cases the way Rust does."},{"heading":"Real Benchmarks: ML Inference Pipeline","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#real-benchmarks-ml-inference-pipeline","content":"The following benchmark uses a text embedding pipeline, the kind of thing you'd use for RAG (Retrieval Augmented Generation) in production. The pipeline: tokenize text, run it through a transformer model, normalize the output embeddings, compute cosine similarities against a vector database.\n\n**Test setup:**\n\n- Hardware: c6i.4xlarge (16 vCPU, 32GB, Intel Ice Lake with AVX-512)\n- Model: sentence-transformers/all-MiniLM-L6-v2 (384-dim embeddings)\n- Dataset: 10,000 text passages, average 128 tokens\n- Task: Embed all passages and compute top-10 nearest neighbors for 100 queries"},{"heading":"Embedding Computation","level":3,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#embedding-computation","content":"```mojo\n# Mojo: Batched embedding normalization + cosine similarity\nfrom tensor import Tensor\nfrom algorithm import vectorize, parallelize\nfrom math import sqrt\n\nfn normalize_embeddings(\n inout embeddings: Tensor[DType.float32],\n num_vectors: Int,\n dim: Int\n):\n @parameter\n fn normalize_row(row: Int):\n var norm = SIMD[DType.float32, 8](0)\n let offset = row * dim\n\n @parameter\n fn accumulate[simd_width: Int](i: Int):\n let v = embeddings.load[width=simd_width](offset + i)\n norm += (v * v).reduce_add()\n\n vectorize[accumulate, 8](dim)\n let inv_norm = 1.0 / sqrt(norm.reduce_add())\n\n @parameter\n fn scale[simd_width: Int](i: Int):\n embeddings.store[width=simd_width](\n offset + i,\n embeddings.load[width=simd_width](offset + i) * inv_norm\n )\n\n vectorize[scale, 8](dim)\n\n parallelize[normalize_row](num_vectors)\n\nfn cosine_similarity_batch(\n queries: Tensor[DType.float32],\n database: Tensor[DType.float32],\n num_queries: Int,\n num_db: Int,\n dim: Int,\n inout results: Tensor[DType.float32]\n):\n @parameter\n fn process_query(q: Int):\n let q_offset = q * dim\n for d in range(num_db):\n let d_offset = d * dim\n var dot = SIMD[DType.float32, 8](0)\n\n @parameter\n fn dot_product[simd_width: Int](i: Int):\n dot += (\n queries.load[width=simd_width](q_offset + i) *\n database.load[width=simd_width](d_offset + i)\n ).reduce_add()\n\n vectorize[dot_product, 8](dim)\n results.store(q * num_db + d, dot.reduce_add())\n\n parallelize[process_query](num_queries)\n```"},{"heading":"Benchmark Results","level":3,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#benchmark-results","content":"| Operation | Python (NumPy) | Python (PyTorch) | Mojo | Rust (ndarray) | C++ (Eigen) |\n| --- | --- | --- | --- | --- | --- |\n| Tokenization (10K texts) | 2.8s | 2.8s | 2.9s\\* | N/A | N/A |\n| Embedding (10K, batched) | 4.2s | 3.8s | 3.9s\\*\\* | N/A | N/A |\n| Normalize (10K x 384) | 12ms | 8ms | 1.4ms | 1.6ms | 1.3ms |\n| Cosine sim (100 x 10K) | 89ms | 42ms | 6.1ms | 6.8ms | 5.9ms |\n| Top-10 selection | 34ms | 28ms | 4.2ms | 4.8ms | 4.1ms |\n| **End-to-end** | **7.13s** | **6.70s** | **6.55s** | - | - |\n\n\\*Mojo tokenization calls the Python tokenizers library via interop \\*\\*Mojo embedding uses PyTorch model via interop\n\nThe headline: Mojo is 6-8x faster than NumPy and 2-4x faster than PyTorch on the pure numerical operations (normalize, cosine similarity, top-k selection). But it's within 10% of Rust and C++ on those same operations. And the end-to-end pipeline time is dominated by the model inference step, which everyone calls PyTorch for.\n\nThis is the reality nobody talks about. For the operations where Mojo shines, vectorized math on dense tensors, you're competing with highly optimized BLAS/LAPACK implementations that NumPy and PyTorch already use via MKL or OpenBLAS. Mojo's advantage is when you have custom compute kernels that can't be expressed as standard BLAS calls."},{"heading":"Where Mojo Actually Wins","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#where-mojo-actually-wins","content":"**Custom preprocessing pipelines.** A text cleaning pipeline that applies regex, Unicode normalization, token filtering, and feature hashing. In Python, this was 340ms for 10K documents. In Mojo with parallelization: 28ms. A 12x speedup, because the Python version couldn't easily parallelize the regex + string manipulation.\n\n```mojo\nfrom python import Python\nfrom algorithm import parallelize\n\nfn preprocess_batch(\n texts: PythonObject,\n num_texts: Int\n) -> Tensor[DType.float32]:\n var features = Tensor[DType.float32](num_texts, 768)\n\n @parameter\n fn process_text(i: Int):\n let text = texts[i]\n # Clean text (Mojo string operations)\n var cleaned = text.lower()\n cleaned = remove_special_chars(cleaned)\n cleaned = normalize_unicode(cleaned)\n\n # Feature hashing (pure Mojo, vectorized)\n let tokens = tokenize_whitespace(cleaned)\n hash_features(tokens, features, i)\n\n parallelize[process_text](num_texts)\n return features\n```\n\n**Custom loss functions and metrics.** If you're writing research code with non-standard loss functions, Mojo lets you write them with explicit SIMD without dropping to C++:\n\n```mojo\nfn focal_loss(\n predictions: Tensor[DType.float32],\n targets: Tensor[DType.float32],\n gamma: Float32,\n alpha: Float32,\n n: Int\n) -> Float32:\n var total_loss = SIMD[DType.float32, 8](0)\n\n @parameter\n fn compute[simd_width: Int](i: Int):\n let p = predictions.load[width=simd_width](i)\n let t = targets.load[width=simd_width](i)\n\n # Focal loss: -alpha * (1-pt)^gamma * log(pt)\n let pt = t * p + (1 - t) * (1 - p)\n let focal_weight = (1 - pt) ** gamma\n let ce = -(t * log(p) + (1 - t) * log(1 - p))\n total_loss += alpha * focal_weight * ce\n\n vectorize[compute, 8](n)\n return total_loss.reduce_add() / n\n```"},{"heading":"The Python Interop: Seamless but Not Free","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#the-python-interop-seamless-but-not-free","content":"The interop story is Mojo's biggest selling point and its most misunderstood feature. You can import any Python module:\n\n```mojo\nfrom python import Python\n\nfn main() raises:\n let np = Python.import_module(\"numpy\")\n let torch = Python.import_module(\"torch\")\n let transformers = Python.import_module(\"transformers\")\n\n # This works, you're calling Python objects from Mojo\n let model = transformers.AutoModel.from_pretrained(\"bert-base-uncased\")\n let tokenizer = transformers.AutoTokenizer.from_pretrained(\"bert-base-uncased\")\n\n let inputs = tokenizer(\"Hello world\", return_tensors=\"pt\")\n let outputs = model(**inputs)\n\n # But this crosses the Python/Mojo boundary\n # Every call has marshalling overhead\n let embeddings = outputs.last_hidden_state.detach().numpy()\n\n # Convert to Mojo tensor for fast operations\n var mojo_tensor = numpy_to_mojo_tensor(embeddings)\n normalize_embeddings(mojo_tensor, 1, 768)\n```\n\nThe overhead of crossing the Python/Mojo boundary is ~2-5 microseconds per call. That's negligible for coarse operations (model inference) but devastating if you do it in a tight loop. Measurement results:\n\n```\nBoundary crossing overhead (per call):\n Simple scalar: 2.1 µs\n Small array (100 elements): 3.8 µs\n Large array (1M elements): 4.2 µs + copy time\n```\n\nThe array copy is the real cost for large tensors. Mojo can share memory with NumPy arrays (zero-copy), but you need to use ` DTypePointer ` and manage the lifetime carefully."},{"heading":"Mojo vs Rust: The Honest Comparison","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#mojo-vs-rust-the-honest-comparison","content":"Here is a direct comparison:\n\n**Mojo wins on:** Python ecosystem access, ML-specific ergonomics (SIMD types, tensor operations, ` @parameter ` metaprogramming), ease of adoption for Python developers.\n\n**Rust wins on:** Memory safety guarantees (Mojo's ownership is simpler but less safe), ecosystem maturity (crates.io has 140K+ crates), tooling (cargo, clippy, rust-analyzer are best-in-class), cross-platform deployment.\n\n**They tie on:** Raw numerical performance. Both compile to LLVM IR (Mojo via MLIR). For the same algorithm with explicit SIMD, you'll get within 5-10% of each other.\n\nThe real question is: who writes the code? For teams of Python developers building ML infrastructure, Mojo is the path of least resistance. For teams of systems programmers building general-purpose infrastructure, Rust is the better choice. The language capabilities overlap, but the developer experience diverges sharply."},{"heading":"What's Missing","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#whats-missing","content":"Mojo is pre-1.0 and it shows:\n\n**Standard library is sparse.** No async runtime, no HTTP client, no database drivers. You call Python for all of that, which works but negates the performance advantage.\n\n**Error messages are cryptic.** The MLIR-based compiler produces error messages that reference intermediate representations instead of source code. This is improving but still rough.\n\n**Debugger support is limited.** No stepping debugger of production quality exists yet. Print debugging is the norm.\n\n**Package management is nascent.** Modular has a package system, but the community package ecosystem is tiny compared to pip or cargo.\n\n**Closed-source compiler.** The Mojo standard library is open-source, but the compiler is proprietary. This is a significant risk factor for long-term adoption. If Modular goes under, you're stuck."},{"heading":"Assessment","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#assessment","content":"Mojo is the most interesting new language for ML infrastructure in years. But the \"Python superset\" framing oversells it. In practice, the pattern is: Mojo for the hot paths and Python for everything else. That's a valid and useful pattern, it's what Cython and C extensions have done for decades, but Mojo does it with much better ergonomics.\n\nIf you're building ML inference servers, custom data pipelines, or numerical computing libraries, Mojo is worth serious evaluation. If you're building web servers, CLI tools, or general-purpose applications, look elsewhere. The ecosystem isn't there yet, and the Python interop overhead means you're not getting Mojo-speed for anything that touches the Python world.\n\nThe 68,000x number is misleading without context. The real speedup on production ML workloads is 2-10x over optimized Python, and 0.9-1.1x compared to C++/Rust. That 2-10x matters when it saves GPU hours, but it is not the revolution the marketing suggests. It is an evolution, and a good one."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#questions","content":""},{"heading":"Is Mojo a replacement for Python?","level":3,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#question-1","content":"No. Mojo is a superset, valid Python is (mostly) valid Mojo. The value proposition is writing performance-critical code in Mojo while keeping your existing Python ecosystem. Think of it as 'Python for the 95% of your code that doesn't need to be fast, Mojo for the 5% that does.'"},{"heading":"How does Mojo compare to Cython or Numba?","level":3,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#question-2","content":"Cython and Numba are Python accelerators that add type annotations for JIT/AOT compilation. Mojo is a new language with its own compiler (MLIR-based) that happens to be Python-compatible. Mojo gives you ownership semantics, SIMD intrinsics, and compile-time metaprogramming that Cython/Numba can't match."},{"heading":"Should I learn Mojo for ML engineering?","level":3,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#question-3","content":"If you write custom kernels, data preprocessing pipelines, or inference servers, yes, it's worth exploring. If you primarily use PyTorch/JAX at a high level, Mojo won't change your workflow much yet. The ecosystem is still young."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/mojo-language-python-superset-performance#article-tips","content":"1. **Tip:** Start Mojo files with ` from python import Python ` to call any Python library directly. NumPy, PyTorch, and scikit-learn all work, but the interop has marshalling overhead, so don't call Python in hot loops.\n2. **Tip:** Use ` SIMD[DType.float32, 8] ` for explicit vectorization instead of relying on auto-vectorization. Mojo's SIMD types map directly to hardware SIMD registers, you control the width, the compiler generates the instructions.\n3. **Tip:** The ` @parameter ` decorator in Mojo evaluates at compile time, similar to Zig's comptime. Use it for loop unrolling and specialization: ` @parameter for i in range(4): ` unrolls the loop at compile time.\n\n- mojo\n- python\n- mlir\n- simd\n- performance\n- machine-learning\n- systems-programming\n- rust\n- cpp\n- benchmarks\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/mojo-language-python-superset-performance"},{"text":"Mojo","url":"https://www.modular.com/mojo"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/mojo-language-python-superset-performance#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/mojo-language-python-superset-performance"},"headline":"Mojo as a Python Superset: Performance on Production ML Workloads","description":"Benchmarking Mojo against Python, Rust, and C++ on actual ML inference pipelines. MLIR compilation, ownership model, SIMD types, and why the '68,000x faster than Python' claim needs serious context.","datePublished":"2026-02-11","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/mojo-language-python-superset-performance","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Programming Languages","keywords":"mojo, python, mlir, simd, performance, machine-learning, systems-programming, rust, cpp, benchmarks","wordCount":1638,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/mojo-language-python-superset-performance"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Mojo as a Python Superset: Performance on Production ML Workloads","item":"https://lucioduran.com/blog/mojo-language-python-superset-performance"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is Mojo a replacement for Python?","acceptedAnswer":{"@type":"Answer","text":"No. Mojo is a superset, valid Python is (mostly) valid Mojo. The value proposition is writing performance-critical code in Mojo while keeping your existing Python ecosystem. Think of it as 'Python for the 95% of your code that doesn't need to be fast, Mojo for the 5% that does.'"}},{"@type":"Question","name":"How does Mojo compare to Cython or Numba?","acceptedAnswer":{"@type":"Answer","text":"Cython and Numba are Python accelerators that add type annotations for JIT/AOT compilation. Mojo is a new language with its own compiler (MLIR-based) that happens to be Python-compatible. Mojo gives you ownership semantics, SIMD intrinsics, and compile-time metaprogramming that Cython/Numba can't match."}},{"@type":"Question","name":"Should I learn Mojo for ML engineering?","acceptedAnswer":{"@type":"Answer","text":"If you write custom kernels, data preprocessing pipelines, or inference servers, yes, it's worth exploring. If you primarily use PyTorch/JAX at a high level, Mojo won't change your workflow much yet. The ecosystem is still young."}}]}]}],"contentHash":"d83557b51e880e25b095a634bc372e9c48604513f0de9eef4eceb0ddbef52153","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/mojo-language-python-superset-performance","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":68772,"contentBytes":14517}}
