Built from scratch · Apple M1 Max · zero dependencies

A compiler that writes its own machine code.

You describe a computation. KILN analyses it, decides how to schedule it, and emits ARM64 instructions one 32-bit word at a time — then writes them into executable memory and calls them. No LLVM. No assembler. No numpy. 3,176 lines of the Python standard library.

0x4E22CC20 —  one instruction KILN emitted, taken apart
Q = 1 — operate on all 128 bits, so four float32 numbers move at once.
sz = 0 — those lanes are 32-bit floats, not 64-bit.
opcode 110011 — multiply and add in a single rounding step.
v2, v1, v0 — which of the 32 vector registers to use.
KILN builds that number itself, bit by bit. Then the test suite hands the same instruction to Apple's own assembler and compares the result. 491 instructions, 491 matches, zero differences. That check found a real bug on its first run — one wrong bit in a load instruction's addressing field.
Resultmeasured, not claimed

What came out of it

Every number here was produced by running the code on this machine. Nothing is copied from a previous run.

2.9×
Faster than numpy
Median across 32 tests. Best case 7.9×.
97.6%
Of the chip's ceiling
Matrix multiply, against a limit KILN measured itself.
0
Wrong bits
Across 9,075,402 numbers compared one at a time.
1 ULP
Error in exp()
Hand-written, 21 instructions. 91.3% bit-identical to the C library.
60×
Loss reduction
A neural network trained entirely on emitted code.
0
Outside libraries
Checked by walking the source, not by promise.
Whyone pass, not four

The whole idea in one line

Ask numpy for (a*b + c)*a - b and it sees four separate operators. It walks all of memory four times and builds three throwaway arrays along the way, because a library has no way to know what you are going to ask for next.

KILN sees one expression. It compiles the whole thing into a single loop that touches each number once, keeps every intermediate value in a register, and allocates nothing at all. On 16 million numbers that is the difference between 14.0 milliseconds and 4.3.

The pattern below is the thesis: the more operators in the expression, the bigger the gap — because that is exactly how many trips through memory the fusion removes. a*2.5 + b has two operators and gains least. The nine-operator expression gains most.

vs normal numpy vs numpy tuned by an expert
16 million float32 elements, single core, best of many runs. The second bar gives numpy preallocated output arrays — the faster form someone who knows the library would write — so this is not a comparison against a strawman.
Ceilinghow fast is fast

Fast compared to what?

A speedup on its own means nothing. So before comparing against anything, KILN measures the two hard limits of the machine it is running on — using kernels it emits for the purpose. One loop of nothing but multiply-adds. One loop of nothing but reading memory. Whatever those hit is the ceiling.

Arithmetic ceiling

103.3 GFLOP/s
The most this core's vector units can do — exactly 4.00 multiply-adds per clock cycle, best of five kernel shapes.

Memory ceiling

61.2 GB/s
The most this core can pull from main memory, one core, streaming.

KILN's matrix multiply

97.6%
Of the arithmetic ceiling, at 128×128. No packing pass, no assembler.

The large sizes do not manage that on their own. At 2048×2048 the multiply runs at 16.2 GFLOP/s — 16% of the ceiling — because it re-reads the same slab of memory for every row of the answer. Working on cache-sized blocks instead takes it to 77.2. That 4.8× is the single largest improvement in the project, and the instructions in the inner loop are identical before and after. Only the order changed.

Getting the ceiling itself right took two attempts, and the first one was wrong in my favour. Measuring the chip's limit with only 8 independent chains of work reports 2.0 multiply-adds per cycle; with 12 it reports 3.0. Those are measurements of how long one instruction takes, not how many run at once — and the first version was also timed before the processor had raised its clock speed. It read 85.6 where the truth is 103.3. Every benchmark now warms the chip first and takes the best of five shapes.

What this does not beat. Apple's own Accelerate library reaches around 2,100 GFLOP/s here — roughly 25× more. Not by writing better instructions: it dispatches to AMX, a matrix coprocessor built into the chip whose instruction encoding Apple has never published. No sequence of documented ARM instructions can reach it. That is the real edge of “from scratch” on this hardware, and it is worth naming rather than hiding.
Trustfour standards

How you know it isn't lying

Different operations deserve different standards. Collapsing them into one number is how real errors get hidden.

Bit-exact, no tolerance

Kernels built from add, multiply, fused multiply-add, min, max and square root must match the reference on every single number, to the last bit. Seven kernel families, 77 configurations each: 0 error.

An honest reference

The comparison is not ordinary Python arithmetic — that rounds twice and disagrees with the hardware for reasons that have nothing to do with the compiler. KILN computes each reference value as an exact fraction and rounds it once. The first version of these tests reported an 86-in-1000 failure rate that turned out to be entirely the reference's fault.

Measured, with a stated bound

exp, 1/x and 1/√x are approximations by construction, so there is no correct answer to demand. The tests report the worst error instead: exp 1, reciprocal 1, inverse-square-root 2. tanh reaches 32 — that is the formula's own cancellation, and it is reported rather than tuned away.

Whole programs, not just parts

Checking instructions one at a time cannot catch a jump that lands in the wrong place. So 159 complete kernels — 16,518 instructions — get printed as assembly, handed back to Apple's compiler, and compared. Zero mismatches.

Tradepublished, not buried

The one place accuracy costs speed

Adding up millions of numbers loses precision: once the running total grows large, the smallest digits of each new number fall off the end. There is a fix — carry the lost part in a second register and feed it back — but it costs three extra instructions per number, roughly 3× the time.

So KILN doesn't just pick one. It switches the fix on only once the drift would actually matter. Both halves of that trade are measured:

Numbers addedPlain errorPlain Corrected errorCorrectednumpy errornumpy
65,5361.7e-076.8 µs9.6e-08 13.4 µs7.0e-0924.7 µs
1,048,5764.3e-06120 µs4.4e-08 204 µs4.5e-08576 µs
16,777,2162.8e-042,153 µs6.0e-08 3,303 µs6.0e-0811,170 µs

At 16 million numbers the corrected version matches numpy's accuracy to three significant figures while running 3.4× faster. Left uncorrected it would have been four thousand times less accurate — which is exactly why the switch exists.

Proofit actually works

A neural network, trained on nothing but this

The forward pass, the backward pass and the optimiser all run on machine code this project generated. To check it, the identical network — same starting weights, same data, same settings — is trained again in slow, high-precision numpy, and the two loss curves are compared step by step.

Mistakes in backpropagation compound. A transpose off by one row, a gradient with the wrong sign, a bias added to the wrong axis — any of those would pull the curves apart in the first few steps and never let them rejoin.

float64 reference (thick) KILN, float32 (thin)
1.4e-4
Largest gap
Relative difference between the two curves, over all 300 steps.
298/299
Steps that improved
The loss went down almost every single step.
0.33 ms
Per training step
67.7 GFLOP/s sustained, on one core.
Learning40 ranked, 5 timed

It learns the machine it runs on

There are forty ways to arrange each loop, and the best one shifts with the size of the data and the shape of the expression. The gap between the best and worst arrangement reaches 16× on the worst kernel, so choosing badly is expensive. Timing all forty is accurate and slow.

So KILN fits a small model on measurements from this machine — instructions per number, memory traffic, register pressure, loop length — and uses it to rank all forty candidates without running any of them. Only the top five get timed.

1.019×
Of the true best
Median, on kernels the model had never seen.
7 ms
To rank all 40
Compiles every option, times none of them.
1.38×
Worst case
Where it misses. It shortlists; it does not decide.

Scored by holding out an entire kernel: the model is trained on every other kernel, then asked to schedule one it has never encountered. Training on the thing you then measure would prove only that it memorised.

Caughta test that lied

The bug the test kept missing

Late on, one check failed that had passed a dozen times before: tanh was off by 254 units in the last place — not a rounding difference, a real loss of accuracy.

Two separate faults, and the second one is worse. The first: the textbook formula for tanh subtracts two nearly identical numbers when its input is near zero, and almost every meaningful digit cancels. The second: the test was seeding its random inputs from Python's built-in hash function, which is deliberately scrambled differently every time the program starts. The test data was changing from run to run, so the bug surfaced only when the dice happened to land on it.

A test whose inputs move is not a test. The seeds are now derived from a fixed checksum, and tanh was rebuilt: an exact polynomial for small inputs, the exponential formula for large ones, and a comparison that picks between them without ever branching — because four numbers travel through the processor together and can disagree about which formula they need.

254 → 2
tanh error, ULP
127× more accurate after the rewrite.
1.5×
What it cost
Both formulas now run for every number. The fast one is still available, with its 254 printed next to it.
1,386
Kernels re-checked
All passing, and now on data that cannot drift.
Limitsstated plainly

What it can't do

  • Apple Silicon only. The instruction encoder is specific to ARM64. The compiler above it is not, but it has only ever run here.
  • One core. Nothing in this project uses more than a single thread.
  • float32 only. No double precision, no float16, no integers.
  • Matrix multiply loses to Apple by 25× on large matrices, and will keep losing, for the hardware reason above.
  • The scheduling model misses by up to 62% in its worst case. It narrows the field; measurement still decides.
  • Awkward matrix shapes need padding, which costs a copy.
  • gelu's own formula loses precision where its output is near zero — a property of the approximation transformers use, not of the compiler. Reported against the function's scale, where the error is 8×10-8.