mlx-serve blog

Engineering · Release 26.9.5

Why mlx-serve is so fast: 239 tokens a second on a Mac, no Python

mlx-serve is a native inference server for Apple silicon, written in Zig on top of MLX's C API. One binary, no Python, more than 20 language model architectures. In release 26.9.5 on an M4 Max it decodes Qwen3.6-35B-A3B at 239 tokens a second, answers a continued chat in 56 ms, and runs four Qwen3.8-27B chats at 119 tokens a second combined, up from 72 before this release. On the same Mac it decodes 1.1 to 3.2 times faster than llama.cpp without speculation on either side, and 3.2 times faster on Qwen3.8-27B with MTP on both.

David Dalcu22 Sep 2026M4 Max · 128 GBv26.9.5
239tok/s, one streamQwen3.6-35B-A3B 4-bit with its MTP head. Median of three llmprobe runs
119tok/s, four chatsQwen3.8-27B 4-bit, combined. Was 72. One chat alone: 70
56 msto the first tokenContinued chat, Gemma 4 26B-A4B, 1,431 tokens from cache. Cold: 929 ms
1.1–3.2×faster than llama.cppDecode, same Mac, no speculation on either side. 3.2× on Qwen3.8-27B with MTP on both

Every number on this page comes from one Mac, an M4 Max with 128 GB. Decoding a token means streaming the model's weights from unified memory through the GPU, and no engine streams them faster than the memory bus allows. So the whole game on Apple silicon is how many tokens you get out of each read. mlx-serve gets more than one in three ways: it guesses ahead and checks the guesses in one pass, it checks several conversations in that same pass, and it never reads a prompt twice if it already has the answer cached. Everything else in the engine exists to make those three cheap.

One read of the weights

More tokens out of every read

A 27B model at 4 bits is about 15 GB. Reading it once takes tens of milliseconds on an M4 Max, and one token per read tops out near 29 tokens a second in practice. mlx-serve verifies 16 rows per read: four chats, each with the next token plus three guesses.

plain decode · 1 row per read
read weights · 15 GB
reads 0 · tokens 0~29 tok/s
mlx-serve · Qwen3.8-27B-4bit · 4 slots, MTP
read weights · 15 GB · verify 16 rows
reads 0 · tokens 0119 tok/s
same clock on both sides, slowed 10× · click to pausetokens out, mlx-serve vs plain …
FIG 1On the left, a plain engine reads the weights and gets one token for one chat, about every 34 ms. On the right, mlx-serve reads them once for a 16-row verify: four chats, each row holding a committed token (t) and three drafts from the model's own multi-token-prediction head (d1 to d3). The model keeps the drafts it agrees with, about 3.2 per chat. A 16-row read costs about three plain reads (roughly 106 ms a round) and keeps about 12.6 tokens. Measured on Qwen3.8-27B 4-bit: plain decode ~29 tok/s, one chat with MTP 70, four chats 119.

Results

Five models from the release benchmark, one M4 Max, the fastest configuration each ships with, speculation named where it engaged. Decode is llmprobe's own code-completion prompt, warmup discarded, median of three runs. Prefill is a 2k-token prompt. First-token times are a 1.4k-token conversation sent cold, then sent again with one more message.

Decode, one stream

tokens / s
llama.cppmlx-serve 26.9.5
FIG 2Grey bars are llama.cpp on the same Mac with a GGUF of the same model: Q4_0 for both Qwen3.8 models, the same size as our packs, and Q4_K_M for the Gemmas and the 35B, a little bigger (method below). The label under each model is mlx-serve's speculation mode. llama.cpp runs MTP only on the 27B: the 35B GGUF ships without its head and llama.cpp has no MTP for Flash-Next yet, so those grey bars run without it.

Time to the first token

log scale
warm, from the prefix cachecold
FIG 3The continued request carries 1,431 to 1,508 tokens the prefix cache already holds, so only the new message is prefilled. The dense 27B shows it best: 6.8 seconds cold, 202 ms warm, 34× sooner.

Decode by context length

tokens / s
FIG 4Every model stays above half its short-context speed at 16k. The 35B MoE reads 186 at 8k against 171 at 4k: that is speculation, not noise, drafts landed 3.2 tokens a round on that prompt.
Model, 4-bitSpecDecodePrefillColdWarmSooner
Qwen3.6 35B-A3BMTP2391,871850 ms68 ms12.5×
Gemma 4 E4BPLD1161,467990 ms53 ms18.7×
Gemma 4 26B-A4BPLD1151,596929 ms56 ms16.6×
Qwen3.8 Flash-Next*MTP837272,127 ms145 ms14.7×
Qwen3.8 27BMTP702576,835 ms202 ms33.8×

Release 26.9.5, Apple M4 Max 128 GB, ReleaseFast, llmprobe --bench-only, medians of three. PLD is prompt lookup, MTP is the model's own multi-token-prediction head. Decode and prefill in tokens per second; cold and warm are the time to the first token for the same 1.4k-token conversation. *Flash-Next is a 125B-A6B mixture of experts in a mixed 4/8-bit pack.

How it works

mlx-serve is general on purpose: one server runs Gemma, Qwen, Llama, Mistral, DeepSeek, LFM, Nemotron and more, next to image, video, music and speech models. The speed comes from the parts around the math.

The line nobody gets under

Qwen3.8-27B at 4 bits is about 15 GB. The M4 Max moves 546 GB a second, so reading it once takes about 28 ms, and one token per read can never beat about 36 tokens a second. Plain decode measures about 29. mlx-serve serves one chat at 70 and four at 119 on the same weights. None of that is a faster read. It is more tokens kept per read: about 2.4 for one chat with MTP, about 12.6 for four.

That is why this post is mostly about speculation and batching. The trunk forward on a 27B is at parity with every other MLX-based engine we have measured; nobody reads 15 GB faster than the bus. What separates engines is what they do with each read.

Four chats, one read

Before 26.9.5, four MTP chats on the 27B took turns. Each chat ran its own draft and its own verify, so four chats added up to 72 tokens a second, barely more than one. Now the four lanes draft as rows of one head forward and verify together in one 16-row pass, and every lane is drafted to the full width so the pass never runs half empty.

Combined decode, Qwen3.8-27B 4-bit

tokens / s
earlier mlx-serve26.9.5
FIG 5Greedy, repeated boots per arm. Top: short prompts. Bottom: four chats each 28k tokens deep, warm.

Three changes made it work. A matrix tile that Apple's M5 runs in hardware turned out to run well as plain shader code on the M4, 11.2 TFLOP/s at 8, 16 and 24 rows, so a 16-row verify costs about three plain reads, not sixteen. The draft head now runs once for all four lanes instead of twelve small serial steps. And past 1k tokens of context each chat attends over its own KV cache instead of the engine padding and stacking all four every step, which is the 26 to 64 at 28k.

One four-chat MTP round

milliseconds
16-row verifydraft headaccept, glue
FIG 6Roughly to scale, from round traces. Before, each lane drafted on its own: twelve small head steps, about 21 ms of a 125 ms round. After, the lanes draft as rows of one head forward and the next round's drafts are dispatched before this one ends. The 16-row verify is now nearly the whole round, which is where it should be.

8-bit KV, as fast as bf16 at long context

An 8-bit KV cache halves the memory a long conversation takes, but reading it used to cost speed: at 32k the 27B decoded at 37 tokens a second with 8-bit KV against 46 with bf16. The packed kernels were spilling registers once a verify step had more than a dozen query rows. The new kernel dequantizes each 32-token page into threadgroup memory and does both attention matmuls with Apple's matmul2d tensor op, straight from our affine cache.

Qwen3.8-27B, one stream, MTP

tokens / s
8-bit KV, beforebf16 KV8-bit KV, 26.9.5
FIG 78-bit KV went from 45 to 55 at 16k and from 37 to 51 at 32k, now ahead of bf16 KV at both. Per layer at 32k the new kernel takes 0.77 ms for a 4-row verify against 0.98 for bf16 attention, and 1.22 against 1.99 at 8 rows.

What's next

Method

Apple M4 Max, 128 GB, macOS 26. mlx-serve 26.9.5, ReleaseFast. Single-stream rows are tests/bench.sh, which runs llmprobe --bench-only: warmup discarded, median of three, each engine at its fastest shipped configuration with the speculative mode named. The earlier columns in Fig 2 come from the same harness in benchmarks.md. Concurrency and 8-bit KV numbers come from our own harness: greedy, one engine per boot, repeated boots per arm, same prompts on both engines; the 8-bit KV runs used a 36 GB GPU memory limit. llmprobe's own four-stream cell on the 27B reads 86, because it includes the four cold prefills. Speculative numbers vary by prompt, so we compare only same-session, same-methodology cells. The llama.cpp comparison is its own session on 22 September: llama.cpp master f46bc30 built with Metal, llama-server -ngl 99 -fa on -c 65536 -np 4 --kv-unified --jinja (plus --spec-type draft-mtp on the MTP row), against mlx-serve 26.9.5. Its Flash-Next cell is the best of two llama.cpp runs, the default one and one with a 32k context and the prompt cache off, because the 100 GB model left the default run short of memory.

Try it

It speaks the OpenAI, Anthropic and Ollama APIs, so Claude Code, Codex, pi and anything else that talks to those can run on your Mac at these speeds.

# the app brew install --cask mlx-core # or just the server brew install mlx-serve mlx-serve run gemma4 # point Claude Code at it mlx-serve launch claude
Download for MacGitHubCommunity benchmarks