Safe kernels are fast kernels: tile-rs next to Mojo 1.0

TL;DR — Mojo's compiler and toolchain went open source on 18 August 2026, which for the first time lets anyone put it side by side with something else on the same silicon. We did that with tile-rs, a Rust project for writing accelerator kernels that are type-, concurrency- and resource-safe by construction. On an Apple M1 Ultra, across sixteen kernels — five primitives and eleven from the decode path of DeepSeek-R1-Distill-Qwen-1.5B — the tile-rs-generated Metal kernel is faster than the Mojo one on 15 of 16 and ties on the last. Not by a lot on most of them: the median gap is 4.7%. The interesting claim isn't the margin, it's the direction — you do not have to spend safety to get speed.


Why this comparison is possible now

Chris Lattner has shipped the compiler infrastructure most of us build on: LLVM, Clang, Swift, MLIR. In June 2026 Qualcomm agreed to acquire Modular — the company behind Mojo — for about $3.9 billion in stock; the deal closed on 29 July, with Lattner becoming EVP of Advanced AI Software and Platforms. On 11 August Modular shipped Mojo 1.0 with stability guarantees, and on 18 August they open-sourced the compiler and toolchain under Apache 2.

That last step is what makes this post possible. Before it, Mojo's numbers were Modular's to publish. Now anyone can build it, run it, and check.

tile-rs is a much smaller thing, open-sourced in late June 2026. It is a Rust dialect for accelerator kernels whose defining property is that the compiler refuses to emit a kernel that could race, overflow a buffer, or read a tile it does not own.

Both projects target Apple GPUs. That overlap is the whole experiment.


The head-to-head

Sixteen kernels, Apple M1 Ultra, timed on the GPU's own clock — MTLCommandBuffer.gpuStartTime/gpuEndTime, so the host's submit-and-wait sits outside the measurement. Median of 5 blocks of 40 launches, minimum within each block. Mojo built with optimisation on.

kerneltile-rs → Metal (µs)Mojo (µs)tile-rs is
vec_add44.145.41.03× faster
vec_mul44.145.41.03× faster
vec_sub44.145.31.03× faster
vec_exp34.235.51.04× faster
residual_add44.145.41.03× faster
rms_norm49.364.61.31× faster
rope29.933.11.10× faster
argmax274.7281.51.02× faster
matmul244.6272.01.11× faster
q_proj30.534.21.12× faster
k_proj30.232.21.07× faster
v_proj30.532.21.05× faster
o_proj30.633.61.10× faster
down_proj45.445.9tie (1.01×)
gate_up_silu147.6152.51.03× faster
attn_gqa8616.99801.11.14× faster

Read this honestly. The corpus total is 9.74 ms against 11.00 ms — 1.13× — but attn_gqa is 88% of that total, so the corpus number is very nearly a single kernel's result wearing a suit. The per-kernel view is the fair one, and there the story is consistent small wins: 15 of 16, median 4.7%, one tie, nothing lost.

A 4.7% median is not a rout. What it rules out is the thing people assume must be true — that memory safety has to be paid for in throughput.


How? Three claims

One: the kernels are safe, in three specific senses.

Two: hardware affinity is not optional, and not uniform.

GPUs, TPUs and NPUs have genuinely different architectures, and no single emitted implementation suits all of them. What they do share is enough structure to be captured by one type system and one set of invariants. tile-rs generates a different implementation per target, keeping the operator's meaning while satisfying that target's memory, concurrency, synchronisation and capacity constraints.

Three: the two directions compose.

Lowering goes from types and invariants to a hardware-affine implementation; it must preserve the safety properties while introducing hardware detail. Lifting goes the other way — from an optimised, target-specific kernel back to types and invariants — and it must re-establish safety. The payoff is what happens when you lift and then lower again to a different target: an optimisation discovered on one backend transfers to the others.

If you know Journey to the West: tile-rs is the Monkey King. Safety is the golden headband, hardware affinity is the staff that grows to any length, and the lift/lower cycle is his seventy-two transformations. The headband is usually read as a punishment, but it is the thing that makes the rest of him safe to let loose — and wearing it he still goes everywhere, heaven and the dragon king's palace included.

In practice this has been a magpie exercise. tile-rs borrows from NVIDIA's cuTile, Huawei's PTO and Ascend C, and Mojo, then lifts and lowers repeatedly. Adding safety to those ideas has, repeatedly, also made them faster.


Two objections worth answering

"Rust only draws level with C++, and it cheats with unsafe"

The old complaint: Rust and C++ share an LLVM backend, so Rust merely ties, and C++ has decades of high-performance template libraries. And Rust has unsafe, so the optimisations sneak in through a back door anyway — which lets a C++ advocate say "I'm unsafe, but so are you, and I'm better at poking the hardware."

Look at the tile-rs kernels. The body a person writes contains no unsafe at all. What remains is confined to a macro-generated prelude that does two things no safe API can do for you: read the hardware block index, and construct the global-memory views from the raw device context. Everything above that line — every operator in both tables below — is safe Rust, and on this hardware it is faster than the alternatives.

"Python is AI-native; Rust isn't"

Where did that idea come from? When Python became popular, AI in the statistical sense barely existed. And look at what the "AI-native" stack is actually made of once you get below the API surface. OpenAI's tiktoken, Hugging Face's tokenizers, safetensors and candle, NVIDIA's cuTile-rs, and uv — the package manager for Python itself — are all Rust. Python serving stacks like vLLM and SGLang are Python and CUDA where the scheduling lives, but the tokenizer they load is somebody's Rust crate.

That is the actual division of labour: Python is where the interface is AI-native, and a systems language is where the throughput is. The question is only which systems language, and whether it hands you a compiler that objects before the chip does.

At minimum: if a large model is going to generate the code, I would rather have a Rust compiler checking it for hazards before it ships.


The wider table: five backends, one source

The Ascend numbers below deserve a precise reading, because it is easy to misread them as "tile-rs versus Triton". They are not. Every column is generated from tile-rs. The question they answer is which backend should tile-rs lower to for this kernel, not whose language is faster.

Lines of code

kerneltile-rsAscend CPTOTileLangTritonMetalMojo
all 16, total22312761605498283635606

tile-rs is the most concise of the seven, and it is the only one a human writes — the rest are emitted. (PTO's 1605 is inflated by two kernels, attn_gqa at 621 lines and argmax at 365, both of which are fully unrolled.)

Seven of the sixteen kernels, when an AI first generated the equivalent tile-rs operator, did not compile. Those compiler errors were real defects, not pedantry; fixing them is what produced the operators measured here.

Relative performance, Ascend 910B2, DeepSeek-R1-Distill-Qwen-1.5B

tile-rs (best-of)Ascend CPTOTileLangTriton
total-time ratio51.08×1.62×2.28×1.42×22.33×

Every one of these numbers needs its caveats stated, not buried.

It is a total-time ratio, not a geometric mean or a median of speedups. So the expensive kernels dominate. Triton reads 22.33× while being fastest on only 5 of the 16 — two kernels carry the whole figure. TileLang makes the same point from the other side: it is fastest on 8 of 16, more than any other backend, and has the largest total, because its launch path is the leanest on the part while its scans are single-core.

The tile-rs column is a best-of, and therefore theoretical. It takes the fastest generated kernel per row against the slowest. It is achievable in principle — each kernel is compiled independently, so a compiler may pick a different backend for each — but if a deployment requires one backend throughout, or fuses only within a backend, that headroom does not materialise.

Triton and TileLang reach the NPU through their own Ascend C generation. So those two columns measure Ascend C that was generated from tile-rs-generated Triton or TileLang — a longer path, and worth remembering when comparing them to the direct Ascend C column.

What the safety argument looks like in practice

The most concrete evidence for the "resource safety" claim came from the four Ascend backends. Each has a 192 KB vector unified buffer to respect. Three of the four got it wrong, each differently, and — this is the part that matters — each was caught at a different distance from the hardware:

backendhow the limit was missedwho caught it
PTObudget constant set above the hardware'sthe assembler
Tritongated on Triton's representational limit, not the bufferthe compiler
TileLangno check at allthe device, mid-run
Ascend Cit asks the hardware

A budget that is merely wrong gets caught by the next tool down. A budget that is absent does not get caught until the chip faults. That ordering is the argument for pushing the constraint into the type system, where it is checked before anything is emitted.


Credit where it is due

Mojo's abstraction level holds up well on safety, and here is the evidence for that from outside the timing tables. Every kernel in this corpus exists in two readings: the shapes as they were first captured, some of them degenerate and ill-formed, and the same kernels after those shapes were fixed. Ascend C, TileLang, Triton and PTO all emit different code for the two readings — they were sensitive to a malformation they had no business being sensitive to. The Mojo operator is byte-identical across both. It never had to care.

That is a real design win, and hats off to the Modular team for it.


An invitation

Measuring nine days after a 1.0 open-sourcing is not especially fair to Chris Lattner and his team. Nine days is not long.

So: consider this a challenge, warmly meant. The kernel corpus and the benchmark entries are published at pu-rs.org, and the measurement method is described above in enough detail to be argued with — which is the part that matters. If the next Mojo release takes these numbers back, everyone who runs a model on any of this hardware wins, and that is rather the point of doing it in the open.


All Ascend figures: Ascend 910B2, DeepSeek-R1-Distill-Qwen-1.5B, CANN 8.5.2. All Apple figures: M1 Ultra, 128 GB. Every kernel in both tables is verified against a host reference before it is timed.