~/satyajit

Mojo and MAX source release: the whole stack is readable, and two licences cover it

mdjsonmcp

2026-10-24 · 20 min · compilers · kernels · licensing · gpu · hardware

A 1:56 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Hawser! Modular just published the whole Mojo and MAX stack. Here is how it works. The new part is the middle: graph compiler, runtime and plugins. You can read and patch them, not fork them. The parser skips the syntax tree and writes Mojo straight into an MLIR dialect called lit. That lowers to kgen and pop, still generic over things like vector width. The elaborator fills in every parameter. Here, the width became sixteen lanes. Then LLVM emits code for the CPU or the GPU. A model enters as RMO, which accepts unknown shapes. It lowers to strictly typed MO for shape inference and folding. In MAP, each op is a loop nest, and elementwise chains fuse into their neighbours. MOGG then generates code from the Mojo kernel library. Below, the runtime runs the graph, and its driver moves memory and launches kernels. The driver reaches hardware only through a small C interface. Each plugin maps those calls to a vendor driver. Four are public. Modular's own diagram: the graph compiler in the middle. The runtime at the bottom, above the hardware. Mojo, its compiler and the kernel library are Apache. The new internals allow binaries, services and upstream fixes, but no public forks. A forty line Mojo kernel came close to NumPy on one core. Readable is not forkable. Mojo is open source; MAX's core is source you may read and patch upstream. Mojo lowers through MLIR. MAX fuses in its graph compiler. Read and contribute, not fork. Every source is in the full article. I'm Hawser. Bye!

Why read this

Notabletop 60%

Sorts the 3.35M-line repo by each file's licence header, reads the MAX License clause by clause, and runs a Mojo matmul against NumPy.

  • Original analysis
  • Runs on a laptop CPU
  • Widely used

GPUs, kernels & systemsMixed licencesPractitioner teardown

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 66 of 100, ranked 208 of 607 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

On 23 October Chris Lattner posted that "Mojo and MAX are now fully open - for development, extension, and commercial use", including "the full Mojo programming language, but also the MAX graph compiler, MLRT runtime internals, and hardware abstraction logic." The launch post from the day before, modular.com/blog/max-license, uses a different word in its own title: "MAX is now source available and open to everyone."

Both are accurate about something. The source is there: I cloned github.com/modular/modular at da0db31 (2026-10-23) and the graph compiler, the runtime and the hardware plugins are all in it, with their design docs and 68,568 commits of history going back to January 2022. What "fully open" skips is that the repo now carries two licences, and the one on the newly opened parts is not an open-source licence. This piece measures which code is under which, reads the new licence clause by clause, explains what the stack actually does from Mojo source to a GPU, and ends with a Mojo program I compiled and ran.

modular/modular@da0db31 · snapshot 2026-10-24
tracked files
14,360
license
custom
branch
main
tests
6592 files
source
64.4 MB
commit date
2026-10-23
source by language
Python41.1 MB(3971)C++17.2 MB(1057)C4.5 MB(774)HTML885.1 kB(5)Rust447.0 kB(32)Shell236.6 kB(76)TypeScript82.2 kB(18)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-24 at da0db31 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

Two licences, sorted by file

The top-level LICENSE is now seven lines. Mojo "and its contributions" are under Apache License v2.0 with LLVM Exceptions; "portions of the MAX source code and its binary distribution" are under the MAX License; "the remainder of the source" is Apache again. Which portion is which is settled per file: the launch post says "each source file includes a header identifying the license that applies to it."

So I counted that way. For every tracked .mojo, .py, .cpp, .h, .c, .td, .mlir, .rs and .cu file I read the first 2.5 KB and classified it by the licence its header names. All counts below are measured from the clone.

Licence in the file headerFilesLines
Apache-2.0 with LLVM Exceptions9,3462,966,860
MAX License974266,760
No header (almost all MLIR lit tests)1,231121,295
Total11,5513,354,915

That is about 8% of the code under the MAX License (reasoned). By language the repo is 1,493,818 lines of Mojo, 1,117,978 of Python, 553,707 of C++, 121,833 of MLIR, 52,335 of TableGen and 11,758 of Rust (measured). There are no prebuilt compiler or runtime binaries in the tree: no .so, .a or .mojopkg files at all. The binary files that are there are documentation images, PDFs and videos, small test checkpoints and goldens, and three vec_add .cubin files of 3 to 6 KB that a GPU test loads (measured). MODULE.bazel says the rest plainly: "the prebuilt path is gone ... the whole tree builds from source."

The MAX-License code is concentrated in exactly the pieces Lattner named:

Everything else a developer touches is Apache: the Mojo compiler (330,903 lines across Mojo/lib, Mojo/include and Mojo/tools), the Mojo standard library (196,851), the MAX kernel library (1,078,917 lines, almost all Mojo), the Mojo GPU-programming library (59,338), the Python framework and server (512,284) and AsyncRT (14,838).

One wrinkle, measured: the repo has 32 directories literally named max_license/, and 115 of the source files inside them carry an Apache header, all in the Mojo tree (Mojo/lib/KGENToLLVM/Target/max_license/AMDGPU/AMDGPULowering.cpp is one). By the launch post's own rule the header wins, so those are Apache. The directory names look like a build partition that predates the licence split, not a statement about terms.

What the MAX License allows, clause by clause

The full text is at modular.com/legal/max-license, Last Modified 21 October 2026, and is Licenses/MAX_LICENSE in the repo. These are reported terms, quoted from it; the reading is mine and is not legal advice.

By the Open Source Definition, any licence that forbids distributing modified source outside one vendor's fork is not open source. The MAX License does not pretend otherwise; the launch post calls it "source available."

What actually changed, and when

The history is in the repo, which is the best thing about publishing it. Measured from git:

So the hardware cap people remember went two months ago. What is new this week is source for the MAX internals, and section 1.3, which lets you fork it to send a pull request.

The alliance

modular.com/alliance describes a program, not a foundation, and it has not launched. The FAQ says Modular is "registering interest now, ahead of a formal launch in early 2027." The page's pitch is "LLVM, but for AI", with "a conformance testing ecosystem". The launch post lists what partners get: "a voice in governance and conformance standards, input into the roadmap, priority for upstream contributions, early access to model checkpoints, ownership of dedicated hardware backends, and more permissive licensing." The FAQ is specific about who needs it: organisations that want to keep "support for proprietary hardware without upstreaming those changes."

That is the commercial shape (reasoned). The public licence forbids a vendor from publishing a modified MAX for its own chip, and the alliance is where a vendor gets that right. Modular is now "A Qualcomm Company" (the licence names the licensor as "Modular Inc, a Qualcomm Company"), and the thread says "Modular and Qualcomm are releasing this to help catalyze an open + unified future." Whether a program run by one chipmaker reads as neutral to its rivals is a governance question nobody can answer until the alliance's rules exist.

The stack, from source to silicon

With the internals readable, the design docs answer what each named part does. What follows is reported from GraphCompiler/docs/GraphCompiler.md, MLRT/docs/README.md, Target/README.md and Mojo/docs/compiler/MojoCompilerWalkthrough.md.

A layered block diagram. At the top, framework op graphs from TensorFlow, PyTorch and XGBoost feed framework dialects. The Graph Compiler box holds a graph importer, the RMO and MO representations, optimization passes, the MOGG representation and fusion and kernel selection. Below it a Kernel Compiler box holds KGEN generated kernels and hand-written kernels. Then a Kernel Graph layer with the MGP representation, a graph exporter called MLIR2MEF, and MEF. Then a Runtime layer with GraphRT, AsyncRT and the Driver. At the bottom, CPU, GPU and ASIC hardware.
The graph compiler's own architecture diagram, still the first figure in its design doc. It predates the current pipeline: today fusion happens in a MAP dialect between MO and MOGG, which this drawing does not show (High-Level_Modular_Stack_Diagram.png, GraphCompiler/docs in modular/modular).

Mojo to LLVM. The Mojo compiler is "built entirely on top of MLIR", and it has no traditional AST. The parser emits MLIR operations straight into a source-level dialect called lit. Lifetime and borrow checking run on that, and LowerLIT turns it into kgen, which the walkthrough calls the "canonical Mojo IR". Alongside sits pop, for SIMD and memory operations. Both are still parametric at this point: a function generic over a SIMD width is one generator, not many copies. Then the elaborator instantiates every generator with concrete parameters and runs compile-time code through an interpreter. That step is how comptime W = simd_width_of[DType.float32]() in my program below became the number 16 on this machine. After elaboration, LowerKGENToLLVM and LowerPOPToLLVM hand off to LLVM, with upstream nvvm and rocdl dialects for NVIDIA and AMD GPU intrinsics. The compiler code for this is Apache: the parser alone is 70,358 lines, the KGEN dialect 19,847 and the KGEN-to-LLVM lowering 12,900 (measured).

A flow diagram. main.mojo goes through a parser to an AST, then through MLIR-Gen to frontend dialects. These feed a parametric region holding lit, zap, kgen and pop, and a non-parametric region holding index and scf. Arrows labelled LIT Lowering and KGEN Elaborator move code from the parametric to the non-parametric region, and LLVM CodeGen moves it to a target-dependent llvm box.
The Mojo compiler's dialect diagram, drawn in September 2022 (the file's own metadata). The parametric and non-parametric split, the LIT lowering and the elaborator still match the current walkthrough; the separate AST box does not, since today's parser emits lit directly (dialects.svg, Mojo/docs/compiler/img in modular/modular).

The graph compiler. MAX takes a whole model graph, not a single function. It enters as RMO, a deliberately loose dialect that accepts unknown shapes, and is lowered into MO, which is strictly typed and is where device-independent passes run: shape inference, constant folding, simplification. Device and layout assignment comes next. Then MO is lowered to MAP, one loop-nest kernel per op, and MAP is "the stage where advanced fusion runs". It fuses elementwise chains and prologue and epilogue work around structured kernels like matmul. Fusion is decided by kernel structure, not op name: a Mojo kernel is fusable if it uses the right Mojo types and implements the fusion interfaces. The fused kernels lower to MOGG for code generation, and the kernels themselves are the Apache Mojo library in max/kernels. This is Lattner's "automatic kernel fusion and generation", and the MAP dialect is 7,471 lines of it.

MLRT. The runtime has three libraries: Core (tensors, buffers, a wrapper for Mojo values in C++), GraphRT ("a low-level execution layer for precompiled dataflow graphs"), and Driver, "a low-level cross-vendor runtime" for device discovery, memory and kernel launch. It sits on AsyncRT, which provides the thread pool and is Apache. The Driver is 24,799 lines (measured).

Hardware abstraction. The Driver talks to hardware only through a C API in SDK/include/HAL/. I count 63 distinct M_driver_* functions in those headers (measured). Each vendor plugin in Target/ implements that API and nothing more. The README says plugins "must only translate the functionality required by each M_driver_ HAL API function to sequences of function calls to HW-vendor driver APIs," and that reference counting, caching and other "pan-HW intelligence" live above the HAL. The plugins are deliberately small (measured): CUDA 3,000 lines, HIP 2,567, Metal 2,495, CPU 1,566.

The same README says what is missing on purpose: "Plugins for confidential partnerships MUST NOT be placed here, or anywhere else in the mono-repo!" Lattner's thread lists portability across "NVIDIA, AMD, Apple, Qualcomm, TPUs and Trainium", then adds "Support for the first 3 are already public and more is coming." That matches the tree: no Qualcomm, TPU or Trainium plugin appears in it (measured, by file name and directory).

One more consequence (reasoned). The HAL headers a vendor builds against are themselves MAX-License files. A chipmaker can implement them and ship its plugin as a binary under any licence, since that is software that "runs on or interoperates with MAX". What it cannot do is publish a modified HAL, or a modified MLRT tuned for its part, outside a contribution fork. That is the fragmentation the licence is written to stop, and the gap the alliance is meant to fill.

Running it: a matmul in Mojo, against NumPy

This was the one part of the brief where I ran code. Everything here is measured on a shared 4-core cloud VM (Intel Xeon @ 2.10GHz with AVX-512), CPU only. Every process ran under nice -n 19, and the load average sat near 9.8 because other jobs were using the machine. Numbers will be noisier and lower than on an idle box.

Install was the documented pip path: pip install mojo into a venv pulled mojo 1.2.0, mojo-compiler 1.2.0 and its libraries. The venv came to 755 MB, almost all of it the compiler, LLDB and its libraries. The mojo wheel is the language and the standard library only. parallelize now lives in MAX's max/mojo/max/algorithm, not in std, so my kernels are single-threaded. I did not install the max package; disk on the shared machine was too tight.

The program has three kernels for the same 512x512 float32 product: the textbook triple loop, the i-k-j loop with the standard library's vectorize, and a register-blocked kernel that keeps a 4x32 tile of C in eight SIMD registers for the whole k loop.

# matmul.mojo: C = A @ B, float32, row-major, single thread.
from std.algorithm.functional import vectorize
from std.sys import simd_width_of
from std.time import perf_counter_ns
 
comptime N = 512
comptime W = simd_width_of[DType.float32]()
 
 
def naive(a: List[Float32], b: List[Float32], mut c: List[Float32]):
    for i in range(N):
        for j in range(N):
            var acc: Float32 = 0
            for k in range(N):
                acc += a[i * N + k] * b[k * N + j]
            c[i * N + j] = acc
 
 
def simd_ikj(a: List[Float32], b: List[Float32], mut c: List[Float32]):
    var pa = a.unsafe_ptr()
    var pb = b.unsafe_ptr()
    var pc = c.unsafe_ptr()
    for i in range(N):
        for k in range(N):
            var aik = pa.unsafe_load(i * N + k)
            var crow = i * N
            var brow = k * N
 
            def row[width: Int](j: Int) {mut}:
                var cv = pc.unsafe_load[width=width](crow + j)
                var bv = pb.unsafe_load[width=width](brow + j)
                pc.unsafe_store[width=width](crow + j, cv + aik * bv)
 
            vectorize[W](N, row)
 
 
def blocked(a: List[Float32], b: List[Float32], mut c: List[Float32]):
    # 4 rows x 2 vectors of C held in registers across the whole k loop.
    var pa = a.unsafe_ptr()
    var pb = b.unsafe_ptr()
    var pc = c.unsafe_ptr()
    for i in range(0, N, 4):
        for j in range(0, N, 2 * W):
            var c00 = SIMD[DType.float32, W](0)
            var c01 = SIMD[DType.float32, W](0)
            var c10 = SIMD[DType.float32, W](0)
            var c11 = SIMD[DType.float32, W](0)
            var c20 = SIMD[DType.float32, W](0)
            var c21 = SIMD[DType.float32, W](0)
            var c30 = SIMD[DType.float32, W](0)
            var c31 = SIMD[DType.float32, W](0)
            for k in range(N):
                var b0 = pb.unsafe_load[width=W](k * N + j)
                var b1 = pb.unsafe_load[width=W](k * N + j + W)
                var a0 = SIMD[DType.float32, W](pa.unsafe_load(i * N + k))
                var a1 = SIMD[DType.float32, W](pa.unsafe_load((i + 1) * N + k))
                var a2 = SIMD[DType.float32, W](pa.unsafe_load((i + 2) * N + k))
                var a3 = SIMD[DType.float32, W](pa.unsafe_load((i + 3) * N + k))
                c00 = a0.fma(b0, c00)
                c01 = a0.fma(b1, c01)
                c10 = a1.fma(b0, c10)
                c11 = a1.fma(b1, c11)
                c20 = a2.fma(b0, c20)
                c21 = a2.fma(b1, c21)
                c30 = a3.fma(b0, c30)
                c31 = a3.fma(b1, c31)
            pc.unsafe_store[width=W](i * N + j, c00)
            pc.unsafe_store[width=W](i * N + j + W, c01)
            pc.unsafe_store[width=W]((i + 1) * N + j, c10)
            pc.unsafe_store[width=W]((i + 1) * N + j + W, c11)
            pc.unsafe_store[width=W]((i + 2) * N + j, c20)
            pc.unsafe_store[width=W]((i + 2) * N + j + W, c21)
            pc.unsafe_store[width=W]((i + 3) * N + j, c30)
            pc.unsafe_store[width=W]((i + 3) * N + j + W, c31)

main() (omitted here for length) fills A and B with ((i * 7) % 13) * 0.1 and ((i * 3) % 11) * 0.1, times each kernel with perf_counter_ns (the best of five calls for the two fast ones), and checks every element of the fast results against the naive one.

My first build failed. The closure captured the loop indices i and k by mut, and the compiler refused:

matmul.mojo:28:47: error: Cannot capture i by mut because it could be immutable
                var cv = pc.load[width=width](i * N + j)
                                              ^

Copying them into var crow and var brow fixed it. The next build warned that load/store on a pointer are deprecated in favour of unsafe_load/unsafe_store, which is what the listing uses. The final build is silent:

$ export MODULAR_TELEMETRY_ENABLED=0
$ mojo --version
Mojo 1.2.0 (2016d0cd)
$ nice -n 19 mojo build matmul.mojo -o matmul    # 6.05 s wall
$ ls -la matmul
-rwxr-xr-x 1 root root 56584 Oct 24 03:35 matmul

A 57 KB native executable. mojo build --emit asm shows that the blocked kernel's inner loop is eight vfmadd231ps instructions on zmm registers, 512-bit AVX-512, which is what simd_width_of picked when it returned 16 lanes of float32.

The Python side is the same product in NumPy (A @ B, float32, best of five) and as a pure Python triple loop. The pure Python loop runs at n = 128, because n = 512 would take minutes. I ran three rounds, interleaved, with OPENBLAS_NUM_THREADS at 1 and 2. Verbatim output:

== round 1  load average: 9.84, 6.93, 3.51
$ ./matmul
N = 512  simd width (float32) = 16
naive  ms: 304.382368  GFLOP/s: 0.8819021212161672
simd   ms: 6.564135  GFLOP/s: 40.89426192483854
block  ms: 2.156174  GFLOP/s: 124.49619372091492
max |naive - simd|, |naive - block| = 0.0  checksum c[0], c[last] = 151.90997 155.06004
$ OPENBLAS_NUM_THREADS=1 python matmul_py.py
numpy  ms: 1.943  GFLOP/s: 138.18  c[0], c[last] = 151.91003 155.06006
python ms (n=128): 563.1  GFLOP/s: 0.0074
$ OPENBLAS_NUM_THREADS=2 python matmul_py.py
numpy  ms: 1.069  GFLOP/s: 251.03  c[0], c[last] = 151.91003 155.06006
python ms (n=128): 554.0  GFLOP/s: 0.0076
== round 2  load average: 9.84, 6.93, 3.51
$ ./matmul
N = 512  simd width (float32) = 16
naive  ms: 263.699274  GFLOP/s: 1.0179605424321343
simd   ms: 6.355364  GFLOP/s: 42.2376210080178
block  ms: 2.054937  GFLOP/s: 130.62953073500552
max |naive - simd|, |naive - block| = 0.0  checksum c[0], c[last] = 151.90997 155.06004
$ OPENBLAS_NUM_THREADS=1 python matmul_py.py
numpy  ms: 2.008  GFLOP/s: 133.66  c[0], c[last] = 151.91003 155.06006
python ms (n=128): 475.1  GFLOP/s: 0.0088
$ OPENBLAS_NUM_THREADS=2 python matmul_py.py
numpy  ms: 1.368  GFLOP/s: 196.28  c[0], c[last] = 151.91003 155.06006
python ms (n=128): 598.5  GFLOP/s: 0.0070
== round 3  load average: 9.84, 6.93, 3.51
$ ./matmul
N = 512  simd width (float32) = 16
naive  ms: 402.364981  GFLOP/s: 0.6671441817149589
simd   ms: 8.057109  GFLOP/s: 33.3165973055596
block  ms: 3.270955  GFLOP/s: 82.06638611659287
max |naive - simd|, |naive - block| = 0.0  checksum c[0], c[last] = 151.90997 155.06004
$ OPENBLAS_NUM_THREADS=1 python matmul_py.py
numpy  ms: 1.878  GFLOP/s: 142.90  c[0], c[last] = 151.91003 155.06006
python ms (n=128): 423.1  GFLOP/s: 0.0099
$ OPENBLAS_NUM_THREADS=2 python matmul_py.py
numpy  ms: 19.672  GFLOP/s: 13.65  c[0], c[last] = 151.91003 155.06006
python ms (n=128): 803.2  GFLOP/s: 0.0052

What the runs say, with the medians across rounds:

This is the language half of the release, Apache before this week. It shows how close a plain SIMD type gets to a tuned BLAS in code you can read in a minute. Packing, tiling, threads and every other shape are what max/kernels is for.

What I did not run

I did not build the compiler from source. The README's path is ./bazelw run //Mojo:mojo, and MODULE.bazel builds LLVM from a pinned commit with Modular's own patches applied. That means compiling LLVM, MLIR, the Mojo compiler and its dependencies with Bazel: many gigabytes of build output and, on four shared cores, far more than the twenty minutes and 2 GB of disk I had. I also did not install the max wheel or run the graph compiler, so nothing here measures MAX's fusion or its GPU path.

What to make of it

The release is real. The graph compiler's design docs, its passes, the HAL and the vendor plugins are readable, with the history that explains them. If a kernel misbehaves, you can now read the lowering instead of guessing.

It is not open source in the sense that LLVM is, and the post that called it "fully open" claimed more than the licence grants. Mojo is. The MAX kernels are. The parts that make MAX run across vendors are source you may read, change, ship as binaries and contribute back, but may not fork in public. Modular says that is to prevent fragmentation, and the FAQ points anyone who needs more, such as a chipmaker with a private backend, at an alliance that opens in 2027.

For the related ground on this site: Modular's LLM Inference Handbook is the same company's teaching material, checked formula by formula. CUDA Rust is another MLIR-shaped route from a language to GPU code. TorchTPU shows the other way to get portability, with the framework compiling down through someone else's compiler. DeepGEMM is what a production matmul kernel looks like once it grows past forty lines.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Mojo and MAX source release: the whole stack is readable, and two licences cover it", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026mojomaxopen,
  author = {Satyajit Ghana},
  title  = {Mojo and MAX source release: the whole stack is readable, and two licences cover it},
  url    = {https://ai.thesatyajit.com/articles/mojo-max-open},
  year   = {2026}
}
share