LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Mojo: Python Syntax, Metal-Level Performance

mojopythonaimlperformancemlirsystems-programmingmodular
Contents

There is a recurring tension in AI and ML engineering: Python is the lingua franca of the entire field, but Python is slow. Not “a bit slow” — orders of magnitude slow for numerical kernels. The entire NumPy/SciPy/PyTorch ecosystem exists as a layer of C, C++, and CUDA wrapping a Python interface, because doing the actual math in pure Python would be comically impractical. Every time you call np.matmul, you dive out of Python into a Fortran BLAS routine. Every time you run a PyTorch autograd pass, you’re in C++.

This creates a productivity cliff. ML researchers write clean Python. When performance matters — custom CUDA kernels, highly optimized attention implementations, inference serving at tight latency budgets — you either reach for C++/CUDA, which is painful, or you use Triton/XLA/JAX, which gives you partial escape hatches but with their own constraints. There is no comfortable middle ground.

Mojo is a bet that the middle ground can exist. It was created by Chris Lattner — the author of LLVM, the lead designer of Swift, the creator of MLIR — at a company he co-founded called Modular. The pitch is direct: Python syntax you already know, a type system that enables C-level performance, SIMD and hardware intrinsics as first-class language features, and a compiler built on MLIR rather than just LLVM. All of it designed for the AI/ML hardware landscape, not the 1990s C compiler landscape.

As of April 2026, Mojo is real software that ships, is used in production by Modular and some adopters, and has genuinely impressive benchmark numbers. It is also a language that is still maturing rapidly, has real ecosystem gaps, and carries the non-trivial risk of being controlled by a single commercial company. This post covers all of it — the technical depth, the real capabilities, and the honest limitations.


1. What Mojo Is and Why It Exists

The Creator and the Lineage

Chris Lattner’s compiler pedigree is about as deep as it gets. LLVM, which he created as a Stanford PhD project and took to Apple, became the compiler infrastructure behind Clang, Rust, Swift, Julia, and dozens of others. Swift, which he designed at Apple, showed that you could design a modern high-performance language that felt ergonomic. MLIR, which he designed at Google, introduced a new approach to compiler infrastructure that is now the backbone of TensorFlow XLA, IREE, and the entire TPU compilation stack.

Modular, which Lattner co-founded in 2022, is building a platform for AI inference and model serving. Mojo is the systems language they needed internally — and decided to make public — because no existing language was the right fit for their compiler and runtime work.

The stated design goals, from Modular’s own documentation:

  • Be a superset of Python over time (not a different language that looks like Python)
  • Compile to machine code at C/C++/Rust performance levels
  • Support SIMD, GPU, and custom accelerator programming natively
  • Use MLIR as the compiler backend, not just LLVM
  • Enable the same code to target CPUs, GPUs, and custom hardware without rewrite

MLIR: The Key Architectural Difference

Most languages compile through LLVM IR — a low-level, CPU-centric intermediate representation. LLVM IR is excellent for CPU work, but it has no concept of tensors, SIMD tile operations, GPU grid hierarchies, or the matrix multiply primitives in modern AI hardware.

MLIR (Multi-Level Intermediate Representation) is different. It is a framework for defining dialects — domain-specific IRs that can be progressively lowered into other dialects, eventually reaching LLVM IR or GPU assembly. The LLVM project hosts MLIR officially; it is not Modular-specific.

What this means for Mojo: the compiler can express high-level tensor operations, auto-vectorization hints, hardware-specific matrix unit operations, and memory access patterns at a high level, then progressively lower them to target-specific code. A Mojo program can emit LLVM IR for CPU paths and PTX/AMDGPU for GPU paths from the same source, guided by type information and parametric specialization that Mojo’s frontend provides.

This is why Mojo can do things that Cython, Numba, or PyPy fundamentally cannot: those systems are trying to accelerate Python semantics; Mojo is a different language that happens to look like Python, compiled through an infrastructure designed for heterogeneous AI hardware.

Mojo’s Position in the Ecosystem

It helps to be explicit about what Mojo is competing with or complementing:

  • vs. Python: Mojo is not a Python replacement for general scripting. It is a systems language for performance-sensitive numerical code. Python interop is intentional and first-class.
  • vs. C++: Mojo’s value proposition is “write high-performance numerical code without learning the C++ memory model and template metaprogramming.” For ML kernel authors, this is significant.
  • vs. CUDA: Mojo can target GPUs through its GPU programming model and through the MAX platform. The goal is to eliminate the “must write CUDA C++ for custom GPU kernels” requirement.
  • vs. Triton: OpenAI’s Triton is a Python-embedded DSL for writing GPU kernels. Mojo is a full language with a richer type system, but both are attacking the “write GPU kernels without CUDA C++” problem.
  • vs. Julia: Julia is a dynamic language that JIT-compiles to native code. Mojo compiles statically (ahead-of-time by default) and has a stricter type system in its fast path. Julia has a much more mature ecosystem.
  • vs. Cython/Numba: These accelerate Python by annotating types or JIT-compiling loops. Mojo goes further — it is a different language that interops with Python rather than being a wrapper around it.

2. Current State (2025/2026)

What Has Shipped

As of early 2026, Mojo’s public availability looks like this:

  • Mojo compiler (mojoc): The compiler is available. Modular open-sourced the Mojo standard library in 2024 and has been shipping compiler updates through the magic package manager (their conda-based toolchain manager) and through the modular CLI.
  • MAX platform: Modular’s commercial product, MAX Engine and MAX Serve, are available. MAX Engine is a high-performance inference engine. MAX Graph lets you write custom ops in Mojo.
  • Standard library: A growing standard library in Mojo covers numerics, SIMD types, collections (List, Dict, Set), file I/O, OS interfaces, and testing.
  • Python interop: Mojo can call Python libraries. NumPy, PyTorch, pandas — anything in your Python environment is callable from Mojo.

The Open Source Situation

Mojo’s licensing has evolved. The standard library (mojo/stdlib) is open source under the Apache 2.0 license with LLVM exceptions, available on GitHub at modular/mojo. The compiler itself is not fully open source as of this writing — Modular ships binaries and some components are proprietary. This is an important distinction: you can read, modify, and contribute to the standard library, but you cannot fork the compiler itself. This is similar to Swift’s early years before the compiler was fully open sourced.

Python Compatibility

Mojo is not yet a Python superset in practice, though this is the long-term goal. As of early 2026:

  • Mojo files use .mojo or .🔥 extension (yes, the fire emoji works)
  • Many Python idioms work directly, particularly in def functions
  • Python’s dynamic features (monkey patching, eval, exec, metaclasses) are not supported in Mojo-native code
  • Python C extensions cannot be imported natively; you go through the Python interpreter bridge
  • No Python-equivalent pip for Mojo packages yet; magic and modular are the package management tools
  • The standard library covers common ground but does not replicate all of the Python standard library

The honest framing: Mojo today is a systems programming language that looks like Python and can call Python. It is not “Python that also compiles fast.” The two modes (calling Python libraries, writing fast Mojo) are distinct.

Getting Mojo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Install the modular CLI (Linux, macOS)
curl -ssL https://magic.modular.com | bash

# Install the mojo package
modular install mojo

# Or use magic (Modular's conda-based environment manager)
magic init my-mojo-project --format mojoproject
cd my-mojo-project
magic add mojo

# Run a Mojo file
mojo hello.mojo

# Compile to a binary
mojo build hello.mojo -o hello

# REPL
mojo

A basic Mojo program:

1
2
3
4
5
6
fn main():
    print("Hello from Mojo!")

    var x: Int = 42
    let message: String = "The answer is: "
    print(message, x)

3. The Type System

Mojo has a dual-mode design that is central to understanding it. You can write code that looks and behaves like Python, or you can write code with explicit types and ownership semantics that compiles to tight machine code. The two modes coexist in the same file.

def vs fn

def is Mojo’s Python-compatible function syntax. Arguments are dynamically typed by default (they receive a PythonObject if types are omitted), exceptions can be raised without declaration, and behavior is close to what a Python developer expects:

1
2
3
4
5
6
7
def greet(name):
    return "Hello, " + name

def divide(a, b):
    if b == 0:
        raise ValueError("division by zero")
    return a / b

fn is the systems-programming function. All arguments must have types (or be explicitly typed as object). If it can raise an exception, it must be declared raises. Uninitialized variables are a compile error. This is the fast path:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn add(a: Int, b: Int) -> Int:
    return a + b

fn safe_divide(a: Float64, b: Float64) raises -> Float64:
    if b == 0.0:
        raise Error("division by zero")
    return a / b

fn compute_sum(values: List[Float64]) -> Float64:
    var total: Float64 = 0.0
    for v in values:
        total += v[]
    return total

The fn convention means the compiler knows the exact types at compile time. No Python object boxing occurs. No reference counting overhead for integer arithmetic. LLVM sees clean typed IR and can auto-vectorize, unroll, and inline aggressively.

var and let

Mojo has var for mutable variables and (historically) let for immutable bindings. As of recent Mojo versions, let at the function scope has been folded into the same behavior as var for simplicity, but the concept of immutability is still expressed through argument conventions (covered below). Within struct definitions and for aliases, the distinction remains meaningful:

1
2
3
4
5
6
7
8
fn demonstrate_variables():
    var count: Int = 0          # Mutable
    var name: String = "Mojo"   # Mutable, can be reassigned

    count += 1
    name = "Mojo 2026"

    print(count, name)

alias: Compile-Time Constants

alias defines a value that is resolved at compile time. It is more powerful than a const in C — it can hold types, integers, strings, and any compile-time-evaluable expression:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
alias MAX_SIZE: Int = 1024
alias PI: Float64 = 3.141592653589793
alias DEFAULT_DTYPE = DType.float32

# Aliases can parameterize types
alias Float32x8 = SIMD[DType.float32, 8]
alias Int16x16 = SIMD[DType.int16, 16]

fn process_buffer():
    var buffer = Float32x8(0.0)
    # buffer is a stack-allocated 8-wide SIMD float32 vector
    print(buffer)

struct: Value Semantics and Stack Allocation

Python classes are heap-allocated reference types. Mojo’s struct is a value type — stack-allocated by default, copied on assignment (unless you explicitly use references), with no runtime dispatch overhead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Point:
    var x: Float64
    var y: Float64

    fn __init__(inout self, x: Float64, y: Float64):
        self.x = x
        self.y = y

    fn distance_to(self, other: Point) -> Float64:
        let dx = self.x - other.x
        let dy = self.y - other.y
        return (dx * dx + dy * dy) ** 0.5

    fn __str__(self) -> String:
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"

fn demonstrate_struct():
    var p1 = Point(0.0, 0.0)
    var p2 = Point(3.0, 4.0)
    print(p1.distance_to(p2))   # 5.0

    var p3 = p1   # Copy — p3 is an independent value, not a reference
    p3.x = 10.0
    print(p1.x)   # Still 0.0 — p1 was not affected

Struct methods use self like Python, but self is passed by reference to mutating methods (signaled by inout self) and by value or borrow to read-only methods.

The @value Decorator

Writing __init__, __copyinit__, and __moveinit__ for every struct is tedious. The @value decorator synthesizes all three automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@value
struct Tensor2D:
    var rows: Int
    var cols: Int
    var data: List[Float64]

    # @value auto-generates:
    # fn __init__(inout self, rows: Int, cols: Int, data: List[Float64])
    # fn __copyinit__(inout self, other: Tensor2D)
    # fn __moveinit__(inout self, owned other: Tensor2D)

    fn shape(self) -> Tuple[Int, Int]:
        return (self.rows, self.cols)

    fn at(self, row: Int, col: Int) -> Float64:
        return self.data[row * self.cols + col]

fn use_tensor():
    var t = Tensor2D(3, 3, List[Float64](1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0))
    print(t.at(1, 1))   # 5.0

trait: Interfaces

Traits are Mojo’s version of Rust traits or Go interfaces — they define a set of methods that a type must implement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
trait Numeric:
    fn zero() -> Self: ...
    fn add(self, other: Self) -> Self: ...
    fn scale(self, factor: Float64) -> Self: ...

trait Printable:
    fn __str__(self) -> String: ...

struct Vector2D(Numeric, Printable):
    var x: Float64
    var y: Float64

    fn __init__(inout self, x: Float64, y: Float64):
        self.x = x
        self.y = y

    @staticmethod
    fn zero() -> Vector2D:
        return Vector2D(0.0, 0.0)

    fn add(self, other: Vector2D) -> Vector2D:
        return Vector2D(self.x + other.x, self.y + other.y)

    fn scale(self, factor: Float64) -> Vector2D:
        return Vector2D(self.x * factor, self.y * factor)

    fn __str__(self) -> String:
        return "(" + str(self.x) + ", " + str(self.y) + ")"

fn sum_vectors[T: Numeric](vectors: List[T]) -> T:
    var result = T.zero()
    for v in vectors:
        result = result.add(v[])
    return result

The [T: Numeric] syntax is generic parameterization — T must implement the Numeric trait. This is monomorphized at compile time (like Rust/C++ templates), not dispatched dynamically (like Java generics). Each instantiation is a separately compiled, fully optimized specialization.


4. SIMD and Hardware Intrinsics

This is where Mojo does something genuinely novel: SIMD vectors are a first-class type in the language, not a library or an annotation. This is the core of how Mojo achieves the performance numbers it does.

SIMD[DType, width]

The SIMD type is parameterized by element type and vector width. The compiler maps it directly to the hardware SIMD registers on the target:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from sys.info import simdwidthof
from math import sqrt

fn basic_simd_demo():
    # 8-wide float32 SIMD vector
    var a = SIMD[DType.float32, 8](1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0)
    var b = SIMD[DType.float32, 8](2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0)

    # These operations map to single hardware instructions on AVX2
    var product = a * b          # vmulps ymm0, ymm1, ymm2
    var sum_vec = a + b          # vaddps
    var sq_roots = sqrt(a)       # vsqrtps

    print(product)   # [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0]

fn get_optimal_width():
    # Query the hardware's natural SIMD width for a given dtype
    alias float32_width = simdwidthof[DType.float32]()
    alias float64_width = simdwidthof[DType.float64]()

    print("Float32 SIMD width:", float32_width)   # 8 on AVX2, 16 on AVX-512
    print("Float64 SIMD width:", float64_width)   # 4 on AVX2, 8 on AVX-512

Vectorized Loop Pattern

The canonical way to write a high-performance loop in Mojo uses the vectorize higher-order function combined with the hardware SIMD width:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from algorithm import vectorize
from sys.info import simdwidthof

fn vectorized_add(
    a: DTypePointer[DType.float32],
    b: DTypePointer[DType.float32],
    result: DTypePointer[DType.float32],
    size: Int,
):
    alias simd_width = simdwidthof[DType.float32]()

    @parameter
    fn add_simd_chunk[width: Int](i: Int):
        let chunk_a = a.load[width=width](i)
        let chunk_b = b.load[width=width](i)
        result.store[width=width](i, chunk_a + chunk_b)

    vectorize[add_simd_chunk, simd_width](size)

fn element_wise_relu(
    data: DTypePointer[DType.float32],
    output: DTypePointer[DType.float32],
    size: Int,
):
    alias simd_width = simdwidthof[DType.float32]()
    let zero = SIMD[DType.float32, simd_width](0.0)

    @parameter
    fn relu_chunk[width: Int](i: Int):
        let chunk = data.load[width=width](i)
        output.store[width=width](i, max(chunk, zero))

    vectorize[relu_chunk, simd_width](size)

The @parameter Decorator

@parameter marks a nested function or if statement as something to evaluate at compile time. This is how you write a single function that generates specialized code for different hardware targets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from sys.info import is_x86, has_avx2, has_avx512f, is_apple_silicon

fn hardware_capability_demo():
    @parameter
    if is_apple_silicon():
        print("Running on Apple Silicon — AMX matrix units available")
        # Code here is only compiled for Apple Silicon
    elif has_avx512f():
        print("Running on AVX-512 capable CPU — 512-bit SIMD available")
        # Code here only compiled when AVX-512 is available
    elif has_avx2():
        print("Running on AVX2 capable CPU — 256-bit SIMD available")
    else:
        print("Scalar fallback path")

fn sum_float32[simd_width: Int](data: DTypePointer[DType.float32], size: Int) -> Float32:
    """Sum a float32 array with compile-time-specialized SIMD width."""
    var accumulator = SIMD[DType.float32, simd_width](0.0)
    let chunks = size // simd_width

    for i in range(chunks):
        accumulator += data.load[width=simd_width](i * simd_width)

    # Horizontal sum of the accumulator vector
    var total: Float32 = 0.0
    for i in range(simd_width):
        total += accumulator[i]

    # Handle remainder
    for i in range(chunks * simd_width, size):
        total += data[i]

    return total

@always_inline

@always_inline forces inlining at call sites, eliminating function call overhead for tiny hot-path functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@always_inline
fn fast_clamp(value: Float32, lo: Float32, hi: Float32) -> Float32:
    return min(max(value, lo), hi)

@always_inline
fn fast_sigmoid(x: Float32) -> Float32:
    return 1.0 / (1.0 + exp(-x))

fn apply_activation(
    data: DTypePointer[DType.float32],
    output: DTypePointer[DType.float32],
    size: Int,
):
    alias width = simdwidthof[DType.float32]()

    @parameter
    fn activation_chunk[w: Int](i: Int):
        let chunk = data.load[width=w](i)
        # fast_sigmoid is inlined directly here — no call instruction
        output.store[width=w](i, 1.0 / (1.0 + exp(-chunk)))

    vectorize[activation_chunk, width](size)

Targeting Specific Hardware

Mojo’s parameterization system lets you write code that specializes for hardware at compile time without runtime branches:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from sys.info import simdwidthof, simdbitwidth
from algorithm import vectorize, parallelize

# A matrix-vector product that adapts to available SIMD width
fn matvec[dtype: DType](
    matrix: DTypePointer[dtype],
    vector: DTypePointer[dtype],
    output: DTypePointer[dtype],
    rows: Int,
    cols: Int,
):
    alias simd_width = simdwidthof[dtype]()

    for row in range(rows):
        var acc = SIMD[dtype, simd_width](0)
        let row_start = row * cols

        @parameter
        fn dot_chunk[width: Int](col: Int):
            acc += matrix.load[width=width](row_start + col) * vector.load[width=width](col)

        vectorize[dot_chunk, simd_width](cols)

        # Horizontal reduction
        var row_sum = output[row]
        for i in range(simd_width):
            row_sum += acc[i].cast[DType.float64]().cast[dtype]()
        output[row] = row_sum

# Call it — monomorphized for float32 at compile time
# matvec[DType.float32](mat_ptr, vec_ptr, out_ptr, 1024, 1024)

5. Memory Ownership

Mojo has a memory ownership model that draws significant inspiration from Rust, though it implements it differently. Understanding it is key to writing correct, zero-copy code — especially for tensor operations where unnecessary copies are expensive.

The Three Argument Conventions

Every function argument in Mojo has an ownership convention:

borrowed (default for fn): The caller keeps ownership. The callee gets a read-only reference. No copy, no move:

1
2
3
4
5
6
fn print_tensor_shape(tensor: borrowed Tensor):   # explicit borrowed
    print(tensor.shape())

fn print_name(name: String):   # borrowed is the default for fn
    print(name)
    # name is NOT copied — this is a read-only borrow

inout: The caller keeps ownership but grants mutable access. Changes made inside the function are visible to the caller. Equivalent to C++ lvalue reference or Rust &mut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn normalize_inplace(inout tensor: Tensor):
    let mean = tensor.mean()
    let std = tensor.std()
    tensor = (tensor - mean) / std

fn increment(inout counter: Int):
    counter += 1

fn demonstrate_inout():
    var count = 0
    increment(count)
    print(count)   # 1 — the mutation is visible

owned: The function takes ownership. The caller loses the value. This enables the function to consume, move, or destroy it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn consume_and_process(owned data: List[Float64]) -> Float64:
    # We own data now — we can modify or destroy it
    var sum: Float64 = 0.0
    for i in range(len(data)):
        sum += data[i]
    return sum
    # data is destroyed here

fn build_and_send(owned name: String) -> String:
    # We own name — we can modify it efficiently
    name += " (processed)"
    return name^  # Transfer ownership to caller using ^ operator

The ^ Transfer Operator

The ^ (caret) operator explicitly transfers ownership. It is analogous to std::move in C++ or Rust’s implicit move-on-last-use. Explicit is better than implicit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fn transfer_demo():
    var s = String("hello world")
    var s2 = s^   # s is moved into s2 — s is no longer valid
    # Using s here would be a compile error
    print(s2)

fn pipeline(owned data: LargeBuffer) -> ProcessedResult:
    var filtered = filter_data(data^)      # data moved into filter_data
    var normalized = normalize(filtered^)  # filtered moved into normalize
    return normalized^                      # normalized moved to caller

struct LargeBuffer:
    var data: DTypePointer[DType.float32]
    var size: Int

    fn __init__(inout self, size: Int):
        self.data = DTypePointer[DType.float32].alloc(size)
        self.size = size

    fn __moveinit__(inout self, owned other: LargeBuffer):
        # Move: steal the pointer, zero out the source
        self.data = other.data
        self.size = other.size
        other.data = DTypePointer[DType.float32]()  # Null the source
        other.size = 0

    fn __del__(owned self):
        self.data.free()

Comparison to Rust’s Borrow Checker

Mojo’s ownership model and Rust’s share the same conceptual goals: prevent use-after-free, prevent data races, enable zero-copy transfers. But they differ in how strictly they are enforced and how they handle lifetimes:

Rust: The borrow checker is a formal proof system. It tracks lifetimes explicitly and enforces that references never outlive their referent. This makes it extremely powerful but also the primary source of Rust’s learning curve. The compiler rejects valid-looking code that it cannot statically verify as safe.

Mojo: The ownership model is enforced, but Mojo takes a more pragmatic approach. There is no explicit lifetime annotation syntax (yet — Modular has mentioned lifetime annotations are on the roadmap). The checker is less aggressive. You can use unsafe blocks for pointer arithmetic. The tradeoff is that Mojo is easier to write than Rust for typical ML code, but does not provide Rust’s level of safety guarantee for all patterns.

For ML engineering use cases — writing fast numerical kernels, building inference pipelines, implementing custom ops — the Mojo model is sufficient. Rust’s full lifetime system is overkill for most of this work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Zero-copy tensor slice — no allocation, just a view into existing memory
struct TensorView:
    var data: DTypePointer[DType.float32]
    var offset: Int
    var size: Int

    fn __init__(
        inout self,
        data: DTypePointer[DType.float32],
        offset: Int,
        size: Int,
    ):
        self.data = data
        self.offset = offset
        self.size = size

    fn get(self, idx: Int) -> Float32:
        return self.data[self.offset + idx]

    fn dot(self, other: TensorView) -> Float32:
        """Dot product of two views — no allocation."""
        var result: Float32 = 0.0
        for i in range(min(self.size, other.size)):
            result += self.get(i) * other.get(i)
        return result

Why This Matters for ML Workloads

In PyTorch, tensor operations return new tensors. Most of the time this is fine because GPU memory bandwidth is the bottleneck. But for CPU inference serving, for preprocessing pipelines, and for writing custom ops, unnecessary copies kill performance. Mojo’s ownership model lets you express:

  • “This function takes a tensor slice without copying it” (borrowed)
  • “This function modifies a tensor in place” (inout)
  • “This function consumes a buffer and the caller should not use it again” (owned)

And the compiler enforces these semantics, eliminating the “did I accidentally copy a 2GB embedding?” class of bugs.


6. Metaprogramming

Mojo’s metaprogramming is closer to C++ templates than to Python’s exec/eval. It runs at compile time and generates specialized code. Combined with @parameter, it enables writing one function that becomes multiple highly optimized specializations.

@parameter if: Compile-Time Conditionals

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sys.info import os_is_linux, os_is_macos, is_apple_silicon

fn platform_specific_init() -> String:
    @parameter
    if is_apple_silicon():
        return "Initialized with Apple AMX backend"
    elif os_is_linux():
        return "Initialized with CUDA/ROCm backend"
    else:
        return "Initialized with generic CPU backend"
    # Only one branch is compiled into the binary — no runtime overhead

fn typed_zero[dtype: DType]() -> Scalar[dtype]:
    """Return the zero value for any dtype — resolved at compile time."""
    @parameter
    if dtype == DType.float32:
        return Float32(0.0).cast[dtype]()
    elif dtype == DType.float64:
        return Float64(0.0).cast[dtype]()
    elif dtype == DType.int32:
        return Int32(0).cast[dtype]()
    else:
        return Scalar[dtype](0)

Parametric Types and Functions

Generics in Mojo are parametric — each unique combination of type parameters generates a separate compiled specialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# A ring buffer parameterized by element type and capacity
struct RingBuffer[T: CollectionElement, capacity: Int]:
    var data: InlineArray[T, capacity]
    var head: Int
    var tail: Int
    var count: Int

    fn __init__(inout self):
        self.data = InlineArray[T, capacity]()
        self.head = 0
        self.tail = 0
        self.count = 0

    fn push(inout self, value: T) -> Bool:
        """Returns False if buffer is full."""
        if self.count == capacity:
            return False
        self.data[self.tail] = value
        self.tail = (self.tail + 1) % capacity
        self.count += 1
        return True

    fn pop(inout self) -> Optional[T]:
        if self.count == 0:
            return None
        let value = self.data[self.head]
        self.head = (self.head + 1) % capacity
        self.count -= 1
        return value

fn demonstrate_ring_buffer():
    # Each creates a distinct compiled type
    var float_buf = RingBuffer[Float32, 64]()
    var int_buf = RingBuffer[Int32, 256]()

    _ = float_buf.push(1.5)
    _ = float_buf.push(2.5)
    print(float_buf.pop())

Writing One Function for All SIMD Widths

The real payoff of metaprogramming is hardware-adaptive code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from algorithm import vectorize
from sys.info import simdwidthof

fn softmax[dtype: DType](
    logits: DTypePointer[dtype],
    output: DTypePointer[dtype],
    size: Int,
):
    """
    Softmax that automatically adapts to the SIMD width of the target hardware.
    On AVX-512 with float32, this uses 16-wide SIMD. On AVX2, 8-wide. On NEON, 4-wide.
    No runtime dispatch — the right code is emitted at compile time.
    """
    alias simd_width = simdwidthof[dtype]()

    # Step 1: find max for numerical stability
    var max_val = logits[0]
    for i in range(1, size):
        if logits[i] > max_val:
            max_val = logits[i]

    # Step 2: compute exp(x - max) and accumulate sum
    var sum_exp = Scalar[dtype](0)

    @parameter
    fn exp_and_sum[width: Int](i: Int):
        let chunk = logits.load[width=width](i) - SIMD[dtype, width](max_val)
        let exp_chunk = exp(chunk)
        output.store[width=width](i, exp_chunk)
        for j in range(width):
            sum_exp += exp_chunk[j]

    vectorize[exp_and_sum, simd_width](size)

    # Step 3: normalize
    @parameter
    fn normalize_chunk[width: Int](i: Int):
        output.store[width=width](
            i, output.load[width=width](i) / SIMD[dtype, width](sum_exp)
        )

    vectorize[normalize_chunk, simd_width](size)

# Call for float32 — compiler emits AVX2 or AVX-512 vectorized code automatically
# softmax[DType.float32](logit_ptr, output_ptr, vocab_size)

The Variant Type

Variant is Mojo’s tagged union — a value that can be one of several types, where the compiler tracks which type is active:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from utils.variant import Variant

alias Number = Variant[Int, Float64]

fn process_number(n: Number) -> Float64:
    if n.isa[Int]():
        return n.get[Int]()[].cast[Float64]()
    else:
        return n.get[Float64]()[]

fn main():
    var a: Number = 42
    var b: Number = 3.14

    print(process_number(a))   # 42.0
    print(process_number(b))   # 3.14

7. Python Interoperability

One of Mojo’s most important practical features is that it can call Python. You can use NumPy, PyTorch, matplotlib, pandas, scikit-learn — any Python package in your environment — from Mojo code. This is the migration path: start with Python, rewrite hot paths in Mojo, keep the rest.

Importing Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from python import Python

fn use_numpy():
    let np = Python.import_module("numpy")

    # Create a NumPy array
    let arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
    print(arr)

    # Call NumPy operations
    let mean = np.mean(arr)
    let std = np.std(arr)
    print("Mean:", mean, "Std:", std)

    # Matrix multiplication
    let a = np.random.randn(100, 200)
    let b = np.random.randn(200, 100)
    let c = np.matmul(a, b)
    print("Matrix product shape:", c.shape)

fn use_torch():
    let torch = Python.import_module("torch")

    let x = torch.randn(3, 4)
    let weight = torch.randn(4, 5)
    let output = torch.matmul(x, weight)

    print("Output shape:", output.shape)
    print("Output dtype:", output.dtype)

fn use_pandas():
    let pd = Python.import_module("pandas")
    let np = Python.import_module("numpy")

    let data = np.random.randn(100, 4)
    let df = pd.DataFrame(data, columns=Python.list(["a", "b", "c", "d"]))

    print(df.describe())
    print("Correlation:\n", df.corr())

The PythonObject Type

Python values in Mojo have the type PythonObject. It is dynamically typed and supports the same attribute access, indexing, and method calls you’d use in Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from python import Python, PythonObject

fn manipulate_python_objects():
    let py = Python()

    # Python list
    var py_list: PythonObject = py.evaluate("[1, 2, 3, 4, 5]")
    py_list.append(6)
    print(py_list)   # [1, 2, 3, 4, 5, 6]

    # Python dict
    var config: PythonObject = py.evaluate("{}")
    config["learning_rate"] = 0.001
    config["batch_size"] = 32
    config["epochs"] = 100
    print(config["learning_rate"])

    # Calling Python functions
    let builtins = Python.import_module("builtins")
    let result = builtins.sum(py_list)
    print("Sum:", result)

fn mixed_mojo_python_pipeline(size: Int) -> Float64:
    """
    Mix fast Mojo computation with Python data handling.
    Real use case: generate data in Mojo, pass to Python for visualization.
    """
    let np = Python.import_module("numpy")

    # Generate data in Mojo (fast)
    var mojo_data = List[Float64](capacity=size)
    for i in range(size):
        let x = Float64(i) / Float64(size) * 6.28318   # 2*pi
        mojo_data.append(sin(x) * cos(x * 2.0))

    # Convert to Python/NumPy for downstream processing
    var py_list = Python.list()
    for i in range(len(mojo_data)):
        _ = py_list.append(mojo_data[i])

    let np_array = np.array(py_list)
    let rms = Float64(np.sqrt(np.mean(np.power(np_array, 2))))
    return rms

Hybrid Architecture Pattern

The practical pattern for a performance-critical application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from python import Python

# The hot path — pure Mojo, compiles to native SIMD code
fn fast_normalize(
    data: DTypePointer[DType.float32],
    output: DTypePointer[DType.float32],
    size: Int,
):
    alias simd_width = simdwidthof[DType.float32]()

    # Compute mean via SIMD
    var sum_vec = SIMD[DType.float32, simd_width](0.0)
    let chunks = size // simd_width

    for i in range(chunks):
        sum_vec += data.load[width=simd_width](i * simd_width)

    var total: Float32 = 0.0
    for i in range(simd_width):
        total += sum_vec[i]
    for i in range(chunks * simd_width, size):
        total += data[i]

    let mean = total / Float32(size)

    # Compute variance via SIMD
    var var_sum = SIMD[DType.float32, simd_width](0.0)
    let mean_vec = SIMD[DType.float32, simd_width](mean)

    for i in range(chunks):
        let diff = data.load[width=simd_width](i * simd_width) - mean_vec
        var_sum += diff * diff

    var var_total: Float32 = 0.0
    for i in range(simd_width):
        var_total += var_sum[i]
    for i in range(chunks * simd_width, size):
        let diff = data[i] - mean
        var_total += diff * diff

    let std_dev = sqrt(var_total / Float32(size)) + Float32(1e-8)
    let std_vec = SIMD[DType.float32, simd_width](std_dev)

    # Normalize
    for i in range(chunks):
        let chunk = (data.load[width=simd_width](i * simd_width) - mean_vec) / std_vec
        output.store[width=simd_width](i * simd_width, chunk)
    for i in range(chunks * simd_width, size):
        output[i] = (data[i] - mean) / std_dev

# The glue — use Python for loading, preprocessing, and visualization
fn main():
    let torch = Python.import_module("torch")
    let np = Python.import_module("numpy")

    # Load data via PyTorch (Python)
    let raw_tensor = torch.randn(1, 512)
    let np_array = raw_tensor.numpy().flatten()

    print("Loaded tensor shape:", np_array.shape)
    # In a real application, you'd extract a DTypePointer here
    # and pass it to fast_normalize above

Current Limitations of Python Interop

The interop is real and functional, but has genuine constraints:

  1. Performance crossing the boundary: Calling into Python from Mojo is slow — you’re going through the CPython interpreter. The boundary should be coarse-grained (pass a whole array, not individual elements in a loop).

  2. Type conversion: Converting between Mojo types and Python types requires explicit work. There is no automatic zero-copy bridge between a Mojo DTypePointer[DType.float32] and a NumPy array yet (you’d need to share the underlying memory pointer, which requires careful handling).

  3. GIL: Python’s Global Interpreter Lock applies to Python calls from Mojo. You can’t parallelize Python calls across Mojo threads.

  4. No Python magic in Mojo structs: You cannot make a Mojo struct act as a Python object. The boundary is one-directional: Mojo calls Python, not the other way around (directly).

  5. Import performance: Python.import_module() at function call time imports the module every call. Cache it at module scope in practice.


8. The MAX Platform and Modular’s Strategy

Understanding Mojo without understanding MAX is like understanding Swift without understanding iOS — the language was designed to serve the platform.

What MAX Is

MAX is Modular’s commercial AI inference platform with three main components:

  • MAX Engine: A model serving engine that competes with TensorRT, ONNX Runtime, and TFLite. It takes trained models (ONNX, PyTorch, others) and serves them with high throughput and low latency. The claim is best-in-class performance on both CPU and GPU.
  • MAX Serve: HTTP serving infrastructure for MAX Engine, with batching, scheduling, and a LiteLLM-compatible API.
  • MAX Graph: An API for writing custom model graphs and ops, using Mojo as the kernel language.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Install MAX via the modular CLI
modular install max

# Or via magic
magic add max

# Serve a model with MAX Serve
max serve --model-path ./llama-3-8b.onnx --port 8080

# Or use the Python API
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# MAX Engine Python API
from max.engine import InferenceSession

# Load a model
session = InferenceSession()
model = session.load("./my_model.onnx")

# Run inference
import numpy as np
inputs = {"input_ids": np.array([[1, 2, 3, 4]], dtype=np.int64)}
outputs = model.execute(**inputs)
print(outputs["logits"].shape)

Writing Custom Ops in MAX Graph

This is where Mojo’s systems-programming capabilities feed directly into the ML stack. Instead of writing a CUDA C++ extension and compiling it with nvcc, you write a Mojo kernel and register it with MAX Graph:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Custom Mojo kernel for MAX Graph
from max.graph import TensorType, Type
from algorithm import vectorize
from sys.info import simdwidthof

fn gelu_kernel(
    input: DTypePointer[DType.float32],
    output: DTypePointer[DType.float32],
    size: Int,
):
    """
    GELU activation: x * 0.5 * (1 + erf(x / sqrt(2)))
    Vectorized implementation for CPU.
    """
    alias simd_width = simdwidthof[DType.float32]()
    alias sqrt2_inv = Float32(0.7071067811865476)   # 1/sqrt(2)
    alias half = SIMD[DType.float32, simd_width](0.5)
    alias one = SIMD[DType.float32, simd_width](1.0)
    let sqrt2_inv_vec = SIMD[DType.float32, simd_width](sqrt2_inv)

    @parameter
    fn gelu_chunk[width: Int](i: Int):
        let x = input.load[width=width](i)
        # erf approximation: tanh(sqrt(2/pi) * (x + 0.044715 * x^3))
        let x3 = x * x * x
        let inner = SIMD[DType.float32, width](0.7978845608) * (
            x + SIMD[DType.float32, width](0.044715) * x3
        )
        let tanh_val = tanh(inner)
        output.store[width=width](
            i, x * SIMD[DType.float32, width](0.5) * (SIMD[DType.float32, width](1.0) + tanh_val)
        )

    vectorize[gelu_chunk, simd_width](size)

The “Replace PyTorch C++ Extensions” Use Case

Today, ML researchers who need custom operations write:

  1. A Python binding layer (torch.autograd.Function or torch.nn.Module)
  2. A C++/CUDA kernel (kernel.cu, compiled with nvcc or torch.utils.cpp_extension)
  3. A CMake or setup.py build system to tie it together

This is a significant engineering burden. Modular’s pitch is: write the kernel in Mojo instead. The toolchain is simpler, the language is safer, the code is portable across CPU and GPU targets, and you don’t need a CUDA toolkit.

Whether this pitch has fully landed depends on where you’re targeting. For CPU inference, Mojo’s story is compelling. For GPU, the CUDA interop path is more mature but the native Mojo GPU programming support (through MAX) is developing rapidly.

Modular’s Business Model

Modular makes money through MAX — the commercial enterprise offering that includes support, priority access to new features, and managed serving infrastructure. Mojo (the language) and its standard library are either open source or freely available. The strategy is the same as HashiCorp’s was: build community on the open tool, monetize through the enterprise platform built on top of it.

This matters for the “should I bet on Mojo” calculation. Modular has VC backing (Greylock, GV, General Catalyst — raised over $130M in publicly disclosed funding). The commercial product is real and has paying customers. But the language’s fate is tied to the company’s commercial success in a way that Python, Rust, or Go are not.


9. Performance: What the Numbers Actually Say

Modular publishes benchmarks claiming 35,000x speedup over Python for specific workloads. These claims require unpacking carefully.

The Mandelbrot Benchmark

Modular’s most-cited benchmark is a Mandelbrot set computation:

Python implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def mandelbrot_kernel(c, max_iters):
    z = complex(0, 0)
    for i in range(max_iters):
        if abs(z) > 2:
            return i
        z = z * z + c
    return max_iters

def compute_mandelbrot(width, height, max_iters=1000):
    result = [[0]*width for _ in range(height)]
    for y in range(height):
        for x in range(width):
            c = complex(
                -2.0 + 3.0 * x / width,
                -1.5 + 3.0 * y / height
            )
            result[y][x] = mandelbrot_kernel(c, max_iters)
    return result

Mojo vectorized implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from algorithm import vectorize, parallelize
from sys.info import simdwidthof
from math import iota

alias simd_width = simdwidthof[DType.float64]()

fn mandelbrot_kernel_simd[
    simd_width: Int
](c_re: SIMD[DType.float64, simd_width], c_im: SIMD[DType.float64, simd_width], max_iters: Int) -> SIMD[DType.int32, simd_width]:
    """Compute mandelbrot for a SIMD-wide batch of complex numbers simultaneously."""
    var z_re = SIMD[DType.float64, simd_width](0)
    var z_im = SIMD[DType.float64, simd_width](0)
    var iters = SIMD[DType.int32, simd_width](0)
    let two = SIMD[DType.float64, simd_width](2.0)
    let four = SIMD[DType.float64, simd_width](4.0)
    let one = SIMD[DType.int32, simd_width](1)
    let max = SIMD[DType.int32, simd_width](max_iters)

    for _ in range(max_iters):
        let mask = (z_re * z_re + z_im * z_im) < four
        iters = (iters + mask.cast[DType.int32]()).min(max)
        if not mask.reduce_or():
            break
        let new_re = z_re * z_re - z_im * z_im + c_re
        z_im = two * z_re * z_im + c_im
        z_re = new_re

    return iters

fn compute_mandelbrot_mojo(
    output: DTypePointer[DType.int32],
    width: Int,
    height: Int,
    max_iters: Int,
):
    let x_scale = 3.0 / Float64(width)
    let y_scale = 3.0 / Float64(height)

    @parameter
    fn compute_row(row: Int):
        let c_im_val = -1.5 + y_scale * Float64(row)
        let c_im = SIMD[DType.float64, simd_width](c_im_val)
        let row_ptr = output.offset(row * width)

        @parameter
        fn process_cols[width: Int](col: Int):
            # Build a SIMD-wide set of c_re values
            var c_re = iota[DType.float64, width]()
            c_re = -2.0 + x_scale * (c_re + SIMD[DType.float64, width](Float64(col)))
            let result = mandelbrot_kernel_simd[width](c_re, c_im, max_iters)
            row_ptr.store[width=width](col, result)

        vectorize[process_cols, simd_width](width)

    parallelize[compute_row](height)

The Python version runs roughly 400-500ms for a 768x768 image at 1000 iterations. The Mojo version with SIMD + parallelization runs around 2-5ms on the same machine — a real 100-200x speedup. This is not 35,000x.

The 35,000x number comes from comparing a specific naive Python loop against Mojo with SIMD and parallelism enabled. When you compare Mojo against NumPy (compiled C/Fortran), the gap narrows considerably. When you compare Mojo against a well-written C++ implementation, Mojo is competitive but rarely dramatically faster.

Realistic Benchmark Ranges

Based on benchmarks from the community and Modular’s own data (which should be read critically, as all vendor benchmarks should):

Workload Python NumPy Mojo C++ Notes
Scalar math in tight loop 1x ~100x ~150x Mojo close to C++
SIMD float32 element-wise 1x 20-40x 40-80x 50-100x Mojo comparable to optimized C
Matrix multiply (1024x1024) 1x 800x 900-1100x 1000-1200x Mojo near-optimal
Memory-bound operations 1x 15-25x 15-30x 20-30x Memory-bound, not compute-bound
String processing 1x 5-15x 10-20x Less mature than numeric code

The honest summary: for numerical CPU code, Mojo achieves C/C++ performance. This is not marketing — the generated assembly quality is high, SIMD vectorization works, and LLVM’s optimization passes over well-typed Mojo IR produce excellent code. But “C performance” is not “20,000x Python performance” — it is the same order-of-magnitude speedup you get from any compiled language over CPython for compute-bound code.

Matrix Multiply Benchmark

Here is a simplified matrix multiply in Mojo to illustrate the performance techniques:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from algorithm import vectorize, parallelize
from sys.info import simdwidthof

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

fn matmul_naive(
    a: DTypePointer[dtype],
    b: DTypePointer[dtype],
    c: DTypePointer[dtype],
    m: Int, n: Int, k: Int,
):
    """Naive O(M*N*K) matmul — for baseline comparison only."""
    for i in range(m):
        for j in range(n):
            var sum = Scalar[dtype](0)
            for l in range(k):
                sum += a[i * k + l] * b[l * n + j]
            c[i * n + j] = sum

fn matmul_tiled_vectorized(
    a: DTypePointer[dtype],
    b: DTypePointer[dtype],
    c: DTypePointer[dtype],
    m: Int, n: Int, k: Int,
):
    """
    Tiled, vectorized matmul.
    Tile size chosen to fit L1 cache (32KB typical).
    SIMD on the inner loop for max throughput.
    """
    alias tile_m = 32
    alias tile_n = 32
    alias tile_k = 32

    @parameter
    fn compute_tile_row(tile_row: Int):
        let row_start = tile_row * tile_m
        let row_end = min(row_start + tile_m, m)

        for col_tile in range((n + tile_n - 1) // tile_n):
            let col_start = col_tile * tile_n
            let col_end = min(col_start + tile_n, n)

            for k_tile in range((k + tile_k - 1) // tile_k):
                let k_start = k_tile * tile_k
                let k_end = min(k_start + tile_k, k)

                for i in range(row_start, row_end):
                    @parameter
                    fn inner_simd[width: Int](j: Int):
                        var acc = c.load[width=width](i * n + col_start + j)
                        for l in range(k_start, k_end):
                            acc += SIMD[dtype, width](a[i * k + l]) * b.load[width=width](l * n + col_start + j)
                        c.store[width=width](i * n + col_start + j, acc)

                    vectorize[inner_simd, simd_width](col_end - col_start)

    parallelize[compute_tile_row]((m + tile_m - 1) // tile_m)

This is still not as fast as OpenBLAS or MKL for large matrices, because those libraries have highly tuned assembly kernels written over decades. Mojo can get close to 80-90% of BLAS performance with reasonable effort, which is genuinely useful.


10. Practical Use Cases Today

Custom CPU Inference Kernels

The clearest win for Mojo today is writing CPU inference kernels that need to be fast without the complexity of C++:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from algorithm import vectorize
from sys.info import simdwidthof

alias float32_simd_width = simdwidthof[DType.float32]()

fn layer_norm(
    input: DTypePointer[DType.float32],
    weight: DTypePointer[DType.float32],
    bias: DTypePointer[DType.float32],
    output: DTypePointer[DType.float32],
    hidden_size: Int,
    eps: Float32 = 1e-5,
):
    """
    Layer normalization kernel — a core operation in transformer models.
    This runs on CPU with SIMD vectorization.
    """
    # Step 1: compute mean
    var sum = Float32(0.0)
    for i in range(hidden_size):
        sum += input[i]
    let mean = sum / Float32(hidden_size)
    let mean_vec = SIMD[DType.float32, float32_simd_width](mean)

    # Step 2: compute variance
    var var_sum = Float32(0.0)

    @parameter
    fn variance_chunk[width: Int](i: Int):
        let diff = input.load[width=width](i) - SIMD[DType.float32, width](mean)
        # accumulate into var_sum (serialized — parallel reduction would be better for large sizes)
        for j in range(width):
            var_sum += diff[j] * diff[j]

    vectorize[variance_chunk, float32_simd_width](hidden_size)

    let variance = var_sum / Float32(hidden_size)
    let inv_std = 1.0 / sqrt(variance + eps)
    let inv_std_vec = SIMD[DType.float32, float32_simd_width](inv_std)

    # Step 3: normalize, scale, shift
    @parameter
    fn normalize_chunk[width: Int](i: Int):
        let norm = (input.load[width=width](i) - SIMD[DType.float32, width](mean)) * inv_std_vec
        let scaled = norm * weight.load[width=width](i) + bias.load[width=width](i)
        output.store[width=width](i, scaled)

    vectorize[normalize_chunk, float32_simd_width](hidden_size)

fn multi_head_attention_score(
    query: DTypePointer[DType.float32],
    key: DTypePointer[DType.float32],
    scores: DTypePointer[DType.float32],
    seq_len: Int,
    head_dim: Int,
):
    """
    Compute attention scores: Q @ K^T / sqrt(head_dim)
    For a single head, batched over sequence positions.
    """
    let scale = 1.0 / sqrt(Float32(head_dim))

    for q_pos in range(seq_len):
        for k_pos in range(seq_len):
            var dot = Float32(0.0)

            @parameter
            fn dot_chunk[width: Int](i: Int):
                dot += (
                    query.load[width=width](q_pos * head_dim + i) *
                    key.load[width=width](k_pos * head_dim + i)
                ).reduce_add()

            vectorize[dot_chunk, float32_simd_width](head_dim)
            scores[q_pos * seq_len + k_pos] = dot * scale

High-Performance Data Preprocessing

Data preprocessing is often a CPU-bound bottleneck, especially for text tokenization and feature engineering:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
fn tokenize_bpe_fast(
    text: String,
    vocab: Dict[String, Int],
    output_ids: DTypePointer[DType.int32],
    max_tokens: Int,
) -> Int:
    """
    Byte-pair encoding tokenization — a common bottleneck in ML pipelines.
    This is a simplified sketch; production BPE needs a full merge table.
    """
    var token_count = 0
    var pos = 0
    let text_len = len(text)

    while pos < text_len and token_count < max_tokens:
        # Try to match the longest token starting at pos
        var best_len = 0
        var best_id = -1

        for token_len in range(min(text_len - pos, 32), 0, -1):
            let candidate = text[pos:pos+token_len]
            if candidate in vocab:
                best_len = token_len
                best_id = vocab[candidate]
                break

        if best_id == -1:
            # Unknown character — use byte-level fallback
            output_ids[token_count] = Int32(ord(text[pos]))
            token_count += 1
            pos += 1
        else:
            output_ids[token_count] = Int32(best_id)
            token_count += 1
            pos += best_len

    return token_count

Replacing Cython/Numba Extensions

If you maintain a Python library with Cython or Numba for performance, Mojo is a viable alternative with some advantages:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# This kind of code replaces a Cython .pyx file
# Advantage over Cython: cleaner syntax, better hardware targeting
# Advantage over Numba: AOT compilation, no JIT warmup, predictable performance

fn rolling_mean(
    data: DTypePointer[DType.float64],
    output: DTypePointer[DType.float64],
    size: Int,
    window: Int,
):
    """Rolling window mean — the kind of thing you'd write in Cython for pandas."""
    var window_sum: Float64 = 0.0

    # Fill first window
    for i in range(window):
        window_sum += data[i]
    output[window - 1] = window_sum / Float64(window)

    # Slide the window
    for i in range(window, size):
        window_sum += data[i] - data[i - window]
        output[i] = window_sum / Float64(window)

    # Zero-fill positions before window is full
    for i in range(window - 1):
        output[i] = Float64(0)  # Or NaN in a real implementation

fn exponential_weighted_mean(
    data: DTypePointer[DType.float64],
    output: DTypePointer[DType.float64],
    size: Int,
    alpha: Float64,
):
    """EWM — another common Cython target in pandas internals."""
    let one_minus_alpha = 1.0 - alpha
    output[0] = data[0]

    for i in range(1, size):
        output[i] = alpha * data[i] + one_minus_alpha * output[i - 1]

Writing a Simple Inference Engine from Scratch

The most ambitious Mojo use case: building a complete neural network inference engine:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from algorithm import vectorize
from sys.info import simdwidthof

alias dtype = DType.float32
alias simd_w = simdwidthof[dtype]()

struct LinearLayer:
    var weight: DTypePointer[dtype]   # [out_features, in_features]
    var bias: DTypePointer[dtype]     # [out_features]
    var in_features: Int
    var out_features: Int

    fn __init__(
        inout self,
        in_features: Int,
        out_features: Int,
        weight: DTypePointer[dtype],
        bias: DTypePointer[dtype],
    ):
        self.weight = weight
        self.bias = bias
        self.in_features = in_features
        self.out_features = out_features

    fn forward(
        self,
        input: DTypePointer[dtype],
        output: DTypePointer[dtype],
    ):
        for out_i in range(self.out_features):
            var acc = self.bias[out_i]
            let row_ptr = self.weight.offset(out_i * self.in_features)

            @parameter
            fn dot_chunk[width: Int](j: Int):
                acc += (
                    input.load[width=width](j) * row_ptr.load[width=width](j)
                ).reduce_add()

            vectorize[dot_chunk, simd_w](self.in_features)
            output[out_i] = acc

struct ReLULayer:
    fn forward(
        self,
        input: DTypePointer[dtype],
        output: DTypePointer[dtype],
        size: Int,
    ):
        let zero = SIMD[dtype, simd_w](0.0)

        @parameter
        fn relu_chunk[width: Int](i: Int):
            output.store[width=width](
                i, max(input.load[width=width](i), SIMD[dtype, width](0.0))
            )

        vectorize[relu_chunk, simd_w](size)

fn run_mlp_inference(
    input: DTypePointer[dtype],
    layer1: LinearLayer,
    layer2: LinearLayer,
    hidden_buf: DTypePointer[dtype],
    output: DTypePointer[dtype],
):
    """Two-layer MLP: input -> linear -> relu -> linear -> output."""
    var relu = ReLULayer()
    var scratch = DTypePointer[dtype].alloc(layer1.out_features)

    layer1.forward(input, scratch)
    relu.forward(scratch, hidden_buf, layer1.out_features)
    layer2.forward(hidden_buf, output)

    scratch.free()

11. Honest Assessment

What Mojo Genuinely Does Well Today

Numerical CPU performance: If you write typed Mojo with fn, explicit SIMD, and vectorize, you get C-level performance. This is real and measurable. For inference serving engines, data processing pipelines, and custom ML kernels on CPU, Mojo is a credible tool.

Developer experience for systems code: Compared to C++, writing Mojo is dramatically more pleasant. The syntax is familiar (Python-like), error messages are clearer, there’s no header/source file split, no obscure template error cascades. Compared to Cython, you get more control and better hardware targeting. Compared to Numba, you get AOT compilation and no JIT warmup.

The MLIR foundation: This is a genuine long-term architectural advantage. MLIR’s progressive lowering is the right infrastructure for targeting heterogeneous AI hardware. As GPUs, NPUs, and custom accelerators proliferate, a compiler that can express operations at multiple levels of abstraction will matter.

Hardware intrinsics ergonomics: SIMD[DType, width] as a first-class type is better than C++ SIMD intrinsics (which are verbose and platform-specific), better than Rust’s SIMD story (improving but historically messy), and far better than anything you can do in Python or Cython.

What Mojo Does Not Do Well Yet

Ecosystem: As of early 2026, there is no mojo.toml-based package registry equivalent to crates.io or PyPI. Third-party Mojo libraries exist but are scattered. The magic package manager is good for distributing Modular-produced packages but not a general package index. If you write a useful Mojo library, distributing it is more involved than pip install.

Python compatibility gaps: Mojo’s def functions cover a lot of Python idioms, but large bodies of Python code will not work as-is. Decorators, metaclasses, generators, context managers with complex protocols, **kwargs patterns — many of these either do not work or work differently. The migration path from Python to Mojo is not “rename .py to .mojo and it runs fast.”

Tooling maturity: The language server (LSP), debugger, and IDE integrations are improving but not at the level of Python or Rust. mojo debug works, but stepping through vectorized Mojo code in a debugger is not as smooth as stepping through Python in VS Code.

GPU support maturity: Native Mojo GPU programming (not through the MAX runtime) is still developing. If your work is primarily GPU-bound, the toolchain for writing raw GPU kernels in Mojo is less mature than Triton + PyTorch, and far less mature than CUDA C++.

Documentation: Modular’s official docs are good and improving rapidly. The community knowledge base (Stack Overflow, blog posts, tutorials) is thin compared to any established language. Learning Mojo often means reading the standard library source code.

Comparison to Alternatives

vs. Cython: Mojo has better hardware targeting, cleaner syntax, and better potential. Cython has a massive existing user base, stable tooling, and deep integration with the Python C API. For maintaining existing Cython code, don’t rewrite. For new performance-critical Python extensions, Mojo is worth considering.

vs. Numba: Numba’s JIT compilation is zero-configuration magic — you add @jit and it often works. But the JIT has startup cost, the behavior is sometimes surprising, and debugging is difficult. Mojo requires more explicit types but gives you predictable, AOT-compiled performance. For production code that must have consistent latency, Mojo is preferable.

vs. JAX: JAX is primarily for automatic differentiation and accelerator compilation for ML research. If you’re doing ML research with backprop, JAX is excellent and Mojo does not compete directly. Mojo’s niche is inference, kernel authorship, and systems programming.

vs. Triton: OpenAI’s Triton is specifically for writing GPU kernels in Python-like syntax. It is more mature for pure GPU work today. Mojo’s GPU story is developing and aims to be broader (CPU + GPU + other accelerators) but is not yet ahead of Triton for GPU kernel development specifically.

vs. Rust: Rust is a more mature language with a larger ecosystem, a proven safety model, and excellent tooling. If your team already knows Rust, using it for ML systems code (with burn, candle, or other ML Rust crates) is a reasonable choice. Mojo’s advantage over Rust is Python familiarity and the MLIR/AI hardware story. Rust’s advantage over Mojo is ecosystem maturity, community size, and open governance.

vs. Go: Go is not a serious contender for numerical compute. Its lack of generics (until recently) and GC make it a poor fit for ML kernels. They solve different problems.

The Vendor Lock-In Question

This is the most important strategic concern for anyone considering adopting Mojo. Modular controls the compiler. If Modular pivots, shuts down, or changes its open source strategy, the language could stall. The Python standard library being open source helps, but the compiler being proprietary is a real risk.

Contrast with Rust (governed by the Rust Foundation), Go (Google, but with community governance), or Swift (Apple, but with open source compiler since 2015). These languages have more governance diversity, even if corporate sponsors are involved.

For production adoption decisions:

  • Use Mojo now if: You are writing CPU inference kernels or data preprocessing code where performance is critical, your team is Python-native, and you can accept ecosystem risk
  • Use MAX Engine now if: You are deploying model inference at scale and want to evaluate competitive alternatives to TensorRT/ONNX Runtime — the commercial product is more mature than the language
  • Wait and watch if: You need a general-purpose systems language for non-ML work, you need guaranteed long-term ecosystem stability, or your work is primarily GPU-bound
  • Stick with alternatives if: You already have working Rust/C++/CUDA code that is fast enough, your bottleneck is not numerical compute, or you cannot accept the dependency on a VC-backed proprietary compiler

Getting Started: A Real Example

To close with something concrete, here is a complete Mojo file that demonstrates the major features discussed: a small vector operations library with Python interop for visualization.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""
vec_ops.mojo — a demonstration of Mojo features for ML engineers.
Compile with: mojo build vec_ops.mojo -o vec_ops
"""

from algorithm import vectorize, parallelize
from sys.info import simdwidthof
from python import Python
from math import sqrt, exp, log

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

# ---- Trait definitions ----

trait VectorOp:
    fn apply(
        self,
        input: DTypePointer[dtype],
        output: DTypePointer[dtype],
        size: Int,
    ): ...

# ---- Activation functions ----

@value
struct ReLU(VectorOp):
    fn apply(
        self,
        input: DTypePointer[dtype],
        output: DTypePointer[dtype],
        size: Int,
    ):
        @parameter
        fn chunk[w: Int](i: Int):
            output.store[width=w](i, max(input.load[width=w](i), SIMD[dtype, w](0.0)))
        vectorize[chunk, simd_width](size)

@value
struct Sigmoid(VectorOp):
    fn apply(
        self,
        input: DTypePointer[dtype],
        output: DTypePointer[dtype],
        size: Int,
    ):
        @parameter
        fn chunk[w: Int](i: Int):
            let neg_x = -input.load[width=w](i)
            output.store[width=w](i, 1.0 / (1.0 + exp(neg_x)))
        vectorize[chunk, simd_width](size)

@value
struct LogSoftmax(VectorOp):
    fn apply(
        self,
        input: DTypePointer[dtype],
        output: DTypePointer[dtype],
        size: Int,
    ):
        # Find max for numerical stability
        var max_val = input[0]
        for i in range(1, size):
            if input[i] > max_val:
                max_val = input[i]

        let max_vec = SIMD[dtype, simd_width](max_val)
        var sum_exp = Float32(0.0)

        @parameter
        fn exp_chunk[w: Int](i: Int):
            let shifted = exp(input.load[width=w](i) - SIMD[dtype, w](max_val))
            output.store[width=w](i, shifted)
            for j in range(w):
                sum_exp += shifted[j]

        vectorize[exp_chunk, simd_width](size)

        let log_sum = log(sum_exp)

        @parameter
        fn log_norm_chunk[w: Int](i: Int):
            output.store[width=w](
                i,
                log(output.load[width=w](i)) - SIMD[dtype, w](log_sum)
            )

        vectorize[log_norm_chunk, simd_width](size)

# ---- Utilities ----

fn cosine_similarity(
    a: DTypePointer[dtype],
    b: DTypePointer[dtype],
    size: Int,
) -> Float32:
    """Cosine similarity between two vectors — useful for embedding lookup."""
    var dot = Float32(0.0)
    var norm_a = Float32(0.0)
    var norm_b = Float32(0.0)

    @parameter
    fn chunk[w: Int](i: Int):
        let chunk_a = a.load[width=w](i)
        let chunk_b = b.load[width=w](i)
        dot += (chunk_a * chunk_b).reduce_add()
        norm_a += (chunk_a * chunk_a).reduce_add()
        norm_b += (chunk_b * chunk_b).reduce_add()

    vectorize[chunk, simd_width](size)

    return dot / (sqrt(norm_a) * sqrt(norm_b) + 1e-8)

fn top_k_indices(
    scores: DTypePointer[dtype],
    indices: DTypePointer[DType.int32],
    size: Int,
    k: Int,
):
    """
    Find the top-k indices by score.
    Simple insertion-sort for small k (k < 100 typical in beam search).
    """
    for k_i in range(k):
        indices[k_i] = Int32(-1)

    for i in range(size):
        let score = scores[i]
        var insert_pos = k

        for j in range(k):
            let idx = Int32(indices[j])
            if idx == -1 or score > scores[int(idx)]:
                insert_pos = j
                break

        if insert_pos < k:
            # Shift elements right
            var j = k - 1
            while j > insert_pos:
                indices[j] = indices[j - 1]
                j -= 1
            indices[insert_pos] = Int32(i)

# ---- Python interop demo ----

fn benchmark_vs_numpy(size: Int):
    """Compare Mojo sigmoid vs NumPy sigmoid, using Python for timing."""
    let time_mod = Python.import_module("time")
    let np = Python.import_module("numpy")

    # NumPy benchmark
    let np_data = np.random.randn(size).astype(np.float32)
    let t0 = time_mod.perf_counter()
    for _ in range(100):
        let _ = 1.0 / (1.0 + np.exp(-np_data))
    let numpy_time = Float64(time_mod.perf_counter() - t0) / 100.0

    # Mojo benchmark
    var mojo_input = DTypePointer[dtype].alloc(size)
    var mojo_output = DTypePointer[dtype].alloc(size)

    # Fill with same data pattern
    for i in range(size):
        mojo_input[i] = Float32(i % 100) / 10.0 - 5.0

    let sigmoid = Sigmoid()

    # Warmup
    sigmoid.apply(mojo_input, mojo_output, size)

    let t1 = time_mod.perf_counter()
    for _ in range(100):
        sigmoid.apply(mojo_input, mojo_output, size)
    let mojo_time = Float64(time_mod.perf_counter() - t1) / 100.0

    print("Array size:", size)
    print("NumPy sigmoid time (avg):", numpy_time * 1000.0, "ms")
    print("Mojo sigmoid time (avg): ", mojo_time * 1000.0, "ms")
    print("Speedup:", numpy_time / mojo_time, "x")

    mojo_input.free()
    mojo_output.free()

fn main():
    print("=== Mojo Vector Ops Demo ===")
    print()

    # Demonstrate activation functions
    let size = 16
    var data = DTypePointer[dtype].alloc(size)
    var output = DTypePointer[dtype].alloc(size)

    for i in range(size):
        data[i] = Float32(i - 8)   # -8 to 7

    var relu = ReLU()
    relu.apply(data, output, size)
    print("ReLU of [-8, ..., 7]:")
    for i in range(size):
        print(" ", output[i], end="")
    print()

    var sigmoid = Sigmoid()
    sigmoid.apply(data, output, size)
    print("Sigmoid of [-8, ..., 7]:")
    for i in range(size):
        print(" ", output[i], end="")
    print()

    # Cosine similarity
    var a = DTypePointer[dtype].alloc(4)
    var b = DTypePointer[dtype].alloc(4)
    a[0] = 1.0; a[1] = 0.0; a[2] = 0.0; a[3] = 0.0
    b[0] = 0.707; b[1] = 0.707; b[2] = 0.0; b[3] = 0.0
    print("Cosine similarity:", cosine_similarity(a, b, 4))   # ~0.707

    # Benchmark vs NumPy
    print()
    print("=== Benchmarking vs NumPy ===")
    benchmark_vs_numpy(1_000_000)

    data.free()
    output.free()
    a.free()
    b.free()

Running this:

1
2
mojo build vec_ops.mojo -o vec_ops
./vec_ops

Expected output (machine-dependent):

=== Mojo Vector Ops Demo ===

ReLU of [-8, ..., 7]:
  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  1.0  2.0  3.0  4.0  5.0  6.0  7.0
Sigmoid of [-8, ..., 7]:
  0.000335  0.000912  ...  0.5  ...  0.9975  0.9993

Cosine similarity: 0.70710677

=== Benchmarking vs NumPy ===
Array size: 1000000
NumPy sigmoid time (avg): 4.21 ms
Mojo sigmoid time (avg):  1.87 ms
Speedup: 2.25 x

The NumPy sigmoid is already compiled C code — the Mojo speedup over NumPy for this kind of operation is typically in the 1.5-3x range for simple element-wise operations, growing for operations where better cache behavior or fewer intermediate allocations help.


Where Mojo is Going

The roadmap Modular has discussed publicly includes:

  • Full Python superset: The goal is that valid Python code becomes valid Mojo code. This requires implementing Python’s dynamic semantics in the Mojo runtime for def functions — an ambitious target.
  • Lifetime annotations: Rust-style lifetime tracking for more formal safety guarantees in fn functions.
  • GPU programming model: Native Mojo GPU programming (without going through the MAX runtime), targeting NVIDIA CUDA, AMD ROCm, and potentially custom accelerators.
  • Package registry: A mojo.toml ecosystem for distributing Mojo libraries, analogous to Cargo for Rust.
  • Debugger improvements: Better DWARF debug info and IDE integration for debugging compiled Mojo programs.
  • Standard library expansion: More of the Python standard library implemented natively in Mojo.

The velocity of development has been high. Features present in mid-2024 look significantly different in early 2026. This is a strength (rapid improvement) and a risk (APIs change, code written six months ago may need updates).


The Verdict

Mojo is a technically impressive language built on a sound architectural foundation. MLIR is the right infrastructure. The type system design is good. The performance numbers for numerical code are real. Chris Lattner has a track record that warrants taking this seriously.

The ecosystem limitations are real too. You cannot pip install mojo-package-of-your-choice. The Python compatibility story is “works well for numerical code, gaps elsewhere.” The compiler is not fully open source. The future is tied to a company’s commercial success.

For ML engineers today, the practical calculus is:

  • Writing a custom inference engine or preprocessing pipeline on CPU? Mojo is worth trying seriously.
  • Using MAX for model serving? Evaluate it against TensorRT and ONNX Runtime on your workload — the results are competitive.
  • Looking for a general-purpose fast language? Rust has better ecosystem and governance guarantees today.
  • Writing GPU kernels? Triton + PyTorch is more mature; keep watching Mojo’s GPU support.
  • Doing ML research with backprop? JAX or PyTorch. Mojo does not compete here.

The most interesting question about Mojo is not whether the language is good — it is. The question is whether Modular can build the ecosystem, open up the compiler, and execute on the Python superset vision while also running a successful commercial AI platform. If they pull both off, Mojo becomes a significant language. If the commercial pressure wins and the language becomes a moat rather than a commons, it becomes a footnote.

Check back in two years.

Comments