Cognithesis — an instrument for curiosity

cognithesis.← Return to the engine
← Systems

SILICON / 01

The Neural Engine Was Never a Place

How a stray assembly test on my desk, a five-way hardware fight club, and 12,000 lines of hand-written assembly answered what Apple's Neural Engine actually is.

In this entry

Series: Drivers From Scratch — Part 1 Part 1: The Apple Neural Engine (this post) · Part 2: AMD XDNA (NPU) · Part 3: NVIDIA Kepler (K80)

Status: Complete. The discovery, the proof, and the interpreter are public — github.com/joshmorgan1000/ane, MIT license. Everything in this post can be re-run from a single script on any M4.

(Codex note: file links below point at GitHub; swap for Torizo viewer links at publication.)

0. Cold open: the accident

The most important experiment in this project was an accident.

I was shopping for an ARM equivalent of Apple’s Accelerate framework — for some ASUS GX10 machines (ARM CPUs, unified memory, CUDA GPU), not for anything Apple. ARM’s documentation for the SME2 extension in ARMv9.2 looked like exactly the right primitive: matrix tiles in the CPU itself, an outer-product instruction called smopa, and a streaming mode you enter and exit around the work.

I had an agent write the first SME2 assembly probes, intending to run them on the GX10. The tests got built and executed on my M4 Max instead. Wrong machine, right architecture.

They ran. So I kept going.

That sentence — they ran — is where this story diverges from a normal ISA evaluation, because the M4 is not supposed to have interesting matrix hardware. It’s supposed to have a Neural Engine, which is a separate thing, on the other side of CoreML and BNNS, that you are not supposed to reach. What the probes kept finding, over the following weeks, was matrix hardware sitting in the CPU cores, undocumented, unadvertised, and faster to drive raw than through Apple’s own frameworks.

1. What you are told about the “Neural Engine”

The marketing is a destination: a “16-core Neural Engine,” a TOPS number on a slide, a row in the spec sheet between the GPU and the media engine.

The developer story is a door policy. The only documented ways in are CoreML and BNNS — Apple’s software stack. You hand over a model, the stack schedules it, and somewhere on the other side, math happens. There is no public instruction set, no register map, no DMA descriptor format. Nothing you could call a programming manual for the Neural Engine exists outside Cupertino.

Which raises the kind of question this series exists to ask: when a vendor’s whole story is an accelerator you can’t program, what is the hardware actually doing — and can we talk to it ourselves?

2. The probe: asking the silicon, not the docs

You cannot look up the M4’s SME2 behavior. Apple documents the extension not at all, and ARM’s architectural manuals will tell you what instructions may exist, not what a given chip does. So the probe does the only honest thing: it asks the silicon.

probes/probe.sh compiles and executes SME/SVE2/SSVE instructions one at a time — a few hundred of them — each in its own tiny assembly program with a C harness, and records a three-valued verdict per instruction:

WORKS        the instruction executed and returned
SIGILL       illegal instruction — the chip refused it
COMPILE_FAIL the assembler refused it

The results on M4 Max, which no manual would have told us:

  • bf16 fmopa runs at the exact same speed as fp32 fmopa. Unexpected — halved memory bandwidth should mean something.
  • Many basic za tile arithmetic instructions (add, mul, sub) raise SIGILL. The fused variants (fmla) work fine. The chip wants multiply-accumulate; it refuses anything that isn’t.
  • Every FP8 instruction fails. Zero exceptions.
  • int8/uint8 MOPA work correctly.
  • Four isolated 4096-byte za tile arrays are reachable, per core — real matrix capacity, per core, that Apple’s spec sheet never mentions.

Read that fingerprint again: matrix tiles on the CPU, INT8-first, BF16 support, no FP8, no documentation. That’s not just a list of quirks — I’ve heard that fingerprint before. It’s a behavioral match for a certain “16-core engine” we’ve all been told is a separate device.

A fingerprint is not a proof. For a proof, you need a fight — and every benchmark that matters is a fight, not a race.

3. The fight: five engines, every pairing, one winner-take-silent

If SME and the Neural Engine were the same silicon, then software paths that “use the Neural Engine” and hand-written SME code must contend — run them together and both slow down. If the Neural Engine is its own device, they won’t. There is no third option.

So tests/run_full_throughput_tests.sh builds a five-way fight club, with every path measured alone and in every combination:

Engine How it’s driven
GPU (Metal) char4 INT8 multiply-accumulate
SME (ours) hand-written smopa on za tiles
BNNS INT8 (Apple’s stack) BNNSFilterCreateLayerFullyConnected, BNNSDataTypeInt8
CBLAS SGEMM (Apple’s stack) Accelerate fp32
NEON fp32 plain FMA on the vector units

And because a concurrency benchmark is trivially wrong in the default way, the methodology is paranoid by design: every worker process self-times with nanosecond timestamps and emits heartbeats, the analyzer only counts throughput from the proven-concurrent window — the samples where every process in the test is confirmed running — and startup/shutdown artifacts are discarded. Thirty-second cooldowns between scenarios keep thermals from voting. It generates all its own source (C, Metal, assembly, Swift) inline and cleans up after itself.

Results on M4 Max:

Test GPU SME BNNS INT8 NEON Total
GPU alone 37.4 37.4
SME alone (4 threads) 8.5 8.5
BNNS INT8 alone (4 threads) 4.2 4.2
GPU + SME 37.4 8.5 45.9
GPU + BNNS 37.3 4.0 41.3
SME + BNNS 5.2 1.6 6.9
GPU + SME + BNNS 37.0 5.2 1.6 43.8
GPU + SME + BNNS + NEON 35.8 4.9 1.7 0.7 43.2

SME + BNNS is the row that ends the argument. Run them together and SME drops 8.5 → 5.2 TOPS while BNNS drops 4.2 → 1.6. Both fall — total throughput below either alone. Hardware doesn’t slow itself down for a stranger; they are contending for the same units, the same tiles, the same wires.

Meanwhile the GPU is a saint: pair it with anything and it doesn’t drop a single point (37.4 → 37.4 with SME). The GPU is genuinely separate silicon. That contrast is what makes the SME/BNNS result legible — the one engine that never fights is the one engine everyone already knew was its own device.

Five findings, then, in order:

  1. SME and BNNS contend for the same hardware.
  2. The GPU is independent — unaffected by any CPU-side workload.
  3. The “Neural Engine” has the same data-type profile as SME: INT8 yes, BF16 yes, FP8 no.
  4. Raw SME beats Apple’s own path by 2× on the same work — 8.5 vs 4.2 TOPS, same hardware, same types.
  5. Same official documentation: none.

4. The verdict, stated as carefully as it deserves

I want to be precise here, because this is the sentence that gets clipped:

The “Neural Engine” as marketed is a software story — CoreML and BNNS — running on top of SME2’s matrix hardware: the za tiles on the CPU itself.

This does not mean “Apple’s Neural Engine is fake,” and driving the tiles does not make my code “the Neural Engine.” The scheduling framework is real engineering; it dispatches, batches, and load-balances. The claim is about the hardware: the matrix silicon Apple’s stack targets is the SME2 tile array in the cores. It’s not that the Neural Engine doesn’t exist — it’s that it was never a separate place. A toll road on hardware you already own.

(And a small confession of a wrong prior: AMD’s XDNA NPU — Part 2 of this series — is an actual separate accelerator on its own fabric with firmware mailboxes and DMA. Everyone assumed Apple did it that way too, because that’s how discrete accelerators work. They didn’t. Being pattern-matched by the industry’s better-documented architecture is a very effective camouflage, and I fell for it too, briefly, until the probes wouldn’t let me.)

5. The door prize: the direct path is 2× faster

If the road is public, drive it. Raw smopa at 8.5 TOPS versus BNNS INT8 at 4.2 — same hardware, same data types, same chip — means Apple’s framework leaves half the hardware’s throughput on the table for INT8 work. Whatever CoreML/BNNS buys you in convenience, it costs you in duty cycle.

Honest ledger, both directions:

  • For fp32 matmul: just use cblas_sgemm. Accelerate’s fp32 path is excellent and almost certainly schedules fmopa better than any of us will by hand. Direct SME wins where the frameworks don’t go: INT8, exotic fused kernels, and quantized-inference tricks like the luti4 lookup instructions — which are a beautiful fit for running quantized models without ever leaving the table-register world.
  • Not production-ready. These instructions are undocumented by Apple, and generation-to-generation behavior can change — see §7, where the M5 is already keeping secrets.

6. The artifact: a bytecode interpreter, because streaming hates doors

So you can touch the tiles. Now what? Each smstart/smstop pair (entering/leaving streaming mode) has overhead, and the architecture is explicitly designed for many streaming operations to happen inside one session — that’s why they call it streaming. The answer to “how do I run a whole neural network inside one streaming session” is the repo’s namesake artifact:

A bytecode interpreter, hand-written in ARM64 assembly. 12,074 lines of it. C++ (or the little DSL) compiles down to a stream of opcodes; a single smstart/smstop pair wraps the entire execution; the interpreter decodes and dispatches on the metal. 127 opcodes and growing — everything from load/store on the z-vectors to smopa outer-product accumulations, INT8 GEMM, RoPE, RMS-norm, softmax, and a fused flash-attention. Register conventions are documented like an ABI should be: z0 is the mailbox for memory ops, z2+ are yours, x20 is the loop counter, ZA persists unless you zero it.

A complete working program — the DSL hello-world, vector add — reads like this:

x: ZVEC_F32;
y: ZVEC_F32;
_LOOP_:;
    x.load(params[0]);
    y.load(params[1]);
    x = x + y;
    x.save(params[2]);
    params[0]++;
    params[1]++;
    params[2]++;
goto _LOOP_ 64;

And because a matrix ISA without a workload is a guitar without an amp: the repo trains MNIST end-to-end through the interpreter — a 784 → 128 → 10 MLP, all dispatch flowing through the bytecode, multi-threaded Hogwild SGD across cores. Training through your own hand-written bytecode VM on undocumented matrix hardware is a specific kind of fun that I recommend to everyone once.

Full honesty on current limits, per the repo’s own README: a bytecode program can hold one loop, up to 255 iterations. They call it streaming for a reason; we’ll see how that goes.

7. Open thread: the M5 is keeping secrets

The M5 Max walked into the probe like a sequel villain. In certain SME throughput benchmarks, the M4 outperforms the M5 — the newer chip is slower at some of these instructions. Digging in: SMLALL (signed multiply-add long) runs roughly 5.3× faster than SMOPA on M5, where the two are near-peers on M4. That’s the kind of asymmetry that doesn’t happen by accident — it looks like Apple is re-weighting the SME microarchitecture toward different instruction paths across generations, possibly because they know which paths their own stack actually uses.

Investigation ongoing. If Apple’s compilers quietly re-target from SMOPA to SMLALL-style paths on M5, that’s the house telling on itself about where the performance lives — and the probe will keep asking until it answers.

8. Run all of it yourself

Everything above is reproducible from github.com/joshmorgan1000/ane:

git clone https://github.com/joshmorgan1000/ane
cd ane
bash probes/probe.sh                          # the instruction census
bash tests/run_full_throughput_tests.sh       # the fight club
mkdir build && cd build && cmake .. && make   # the interpreter
./bin/test_mnist                              # train through bytecode

The probe prints the WORKS/SIGILL table; the throughput script prints the contention matrix; the MNIST demo trains a network whose every multiply passes through 12,074 lines of hand-written assembly. There’s also a live React dashboard that runs the probes and benchmarks against your own silicon.

9. Next: a real NPU this time

Part 1 closes a cold case: Apple’s “Neural Engine” is a software façade over matrix hardware inside the CPU. Part 2 goes the other way — AMD’s XDNA NPU, an actual separate accelerator with 32 AIE2P tiles on the Infinity Fabric, firmware mailboxes, DMA engines, and an official stack (ONNX Runtime → Vitis AI → XRT) built so you never have to learn any of it. All of that hardware, behind NDA’d IP licensing and a Python smile.

Kepler, XDNA’s opposite in every way that matters, waits in Part 3 — a datacenter GPU the vendor retired from its own driver, on a mission statement the vendor wrote and then abandoned. Three vendors, three attitudes about doors. This series is about what’s behind all of them.

← Back to systems