Qwen3.8-27B · oMLX · one Mac Studio
The engine
Part Three turned on the model's reasoning and found that everything before it had measured a model that wasn't thinking. But a thinking move is a few thousand tokens, not a few dozen — and at stock speed a single reasoned game ran close to two hours. Before any of the experiment was affordable, the model had to be made fast, on one machine, with two levers and a lot of things that fell over.
| The two machines | studio — M3 Ultra | claw — M4 Pro |
|---|---|---|
| Cores | 32C / 80G | 12 (8P + 4E) |
| Unified memory | 256 GB | 64 GB |
| Memory bandwidth | ~819 GB/s | ~273 GB/s |
| Decode — MTP off | ~35 tok/s | 12.9 tok/s |
| Decode — MTP on · k=3 | ~60 tok/s | 18.2 tok/s |
| MTP gain | 1.7× | 1.4× |
| ANE prefill | yes | none |
Fitting the model was never the problem. It's a dense 27-billion-parameter build, and with that much unified memory it loads with room to spare. Speed was the only question, and a reasoning turn costs about 221 seconds. Play a hundred games at that rate and the run is measured in days. So the whole serving stack became its own small project: how fast can one Apple-Silicon machine make a 27B think?
Lever one
Multi-token prediction is speculative decoding: a small draft head proposes the next
k=3 tokens, and the full model verifies all three in a single forward pass. When the
guesses are good you get two-to-three tokens for the price of one pass, and the measured rate —
~60 tok/s — legitimately beats the memory-bandwidth roofline of ~51, because the roofline assumes
one token per pass and MTP averages ~2.3–2.9.
The part I did not expect: the draft head gets more accurate as the context grows — acceptance rises from ~70% at 8K tokens to ~80% at 15.6K. Long agentic reasoning prompts, the ones I feared would be slowest, are its best case.
Installing the fancy engine and not turning MTP on is worse than never installing it. The 1.7× is the engine's speculative decoding, nothing else.
The numbers are almost rude about it: stock mlx-lm at 4-bit decodes 40.7 tok/s, and
oMLX with MTP switched off is slower — about 35. You pay to install the fancy
engine and go backward until you flip one setting. And it made the setup unforgiving. Stock
mlx-lm cannot do MTP for this model at all — there is no draft-head module for it — so
the speed lives entirely in oMLX, the
Apple-Silicon server that supports it. And oMLX only loads the engine author's own quantisations:
I learned that by downloading a 158 GB third-party build and watching it fail to load, not on bad
weights but on a parameter-naming convention (shard_0 where the parser wanted
shards.0). The pre-flight check is free — one look at the weight index — and I ran it
only after the download. Now I run it first.
And the lever is only as strong as the hardware under it — which is what the table up top is really saying. MTP earns its ~1.4× on the M4 Pro mini too, but its ceiling is ~18 tok/s, not ~60, because decode is bound by memory bandwidth and the mini has roughly a third of the Ultra's. Neither MTP nor the ANE lifts that roof. The trick did not get worse; the roof came down.
Lever two
Once decode was fast, the wait moved. At long context, prefill dominates — reading a 15.6K-token prompt costs about 39 seconds before the first token, against ~23 seconds of actually generating. Time-to-first-token is what you feel. So the second lever hands prefill to the Apple Neural Engine, the coprocessor that otherwise sits idle.
The trap that hid for days: the default Homebrew formula does
not build the native kernels, so the engine reports the ANE unavailable and silently skips it —
nothing but one warning line in the log. The config file still says true. You have to
rebuild from source --with-custom-kernel (a long Rust-plus-CMake compile that needs the
full Xcode toolchain), then verify build and runtime separately — because a setting
that reads true whether or not anything runs is exactly how the first attempt looked
like it worked and didn't.
| Context | prefill off → on | decode off → on |
|---|---|---|
| 4,096 | 431 → 459 | 65.5 → 67.1 |
| 16,384 | 430 → 520 (+21%) | 57.3 → 56.5 |
| 32,768 | 409 → 503 (+23%) | 53.2 → 53.0 |
Honest about the size of it: with thinking on, a 16K call is decode-dominated, so the
end-to-end win is about −13.5% wall time (51.0 s → 44.1 s), not the headline 21%.
And the 0.5 offload fraction I set is not what runs — the engine caps it to 0.375
because the recurrent path stays off the approximate kernel. Two small lies the numbers told me,
both worth catching.
None of the above arrives clean. The stack fell over in a dozen small ways, and each one looked, at first, like a result about the model.
omlx-server, so
the obvious pkill -f "omlx serve" misses it and you stack two servers on one port.http 000 from every request — the macOS firewall silently dropping
inbound connections, not a bug in anything I'd written.And you cannot trust your own numbers. The prefix cache is worth ~1.9×, so re-sending a prompt measures the cache, not the engine — every benchmark prompt needs a unique nonce first or the speedup is a mirage.
Bandwidth lies the same way: I once clocked the download link at "7 MB/s" and built every estimate on it — the sample was taken while my own downloads saturated the pipe. Real throughput was ~37. Measure nothing under contention, and never from the first minute.
Stacked, MTP and ANE took a reasoned game from close to two hours down to about twenty-six minutes, at zero cost to the answers — the golden outputs never changed. That is the only reason Part Three's hundreds of graded moves exist at all. None of it taught the model anything; it just made the model fast enough to be asked, honestly and often.
Every speedup here left the answers identical. The engine got faster; the model stayed exactly as smart, and exactly as limited.
Which is the wall the whole project keeps hitting from a new side each time. A faster engine gives you more moves per hour; it does not give the model more depth of search. That is the next thread, and a different repository: stop making the same model faster, and start making it better — teach the weights.
Companion to the Othello series · all figures measured on the M3 Ultra with oMLX (oQ4e + MTP, ANE prefill), split at the first streamed token so prefill and decode come from one request, each benchmark prompt nonce-prefixed to defeat the prefix cache. Speed is decode tokens/second; prefill is prompt tokens/second. ← Back to Part Three