Opens in a new tab
← All writing

Six local AI models on my M1 Max: llama.cpp vs MLX

Six open-weights models on a 32 GB M1 Max. Power settings, speculative decoding and what the benchmark numbers leave out.

Six local AI models on my M1 Max: llama.cpp vs MLX

I spent today testing six open-weights models on my 2021 MacBook Pro. M1 Max, 32 GB of unified memory. I wanted to find a model and inference engine I could use for a fully local agent.

This is preparation for my talk, “Wonderful World of Open Weights AI”, at GDG Melbourne DevFest on 3 October 2026. I compared llama.cpp and MLX through mlx-lm, using builds labelled 4-bit for the same six models.

One setting changed how I read the results. Normally, a language model generates one token at a time. A token is a small piece of text, often part of a word. Multi-token prediction (MTP) uses an extra prediction head to propose several upcoming tokens. The main model checks the proposals together, accepts a valid continuation and discards the rest. This is a form of speculative decoding. Accept enough proposals, and generation gets faster.

With MTP enabled in llama-server, Qwen3.6-35B-A3B averaged 81.0 tokens per second. That is the model I am keeping for my local agent experiments. Getting there involved a power setting, a separate server setting, and a limitation I only found by trying it.

Check Low Power Mode first

My early numbers were well below what I expected from this chip. The Mac was in Low Power Mode.

Switching from Low Power to High Power roughly doubled prompt-processing speed on every model I checked. Qwen3.8-27B went from 68 to 136 tokens per second. Gemma 4 31B went from 64 to 130.

Generation speed changed unevenly: Gemma 4 31B went from 7.9 to 17.0 tokens per second, while Qwen3.6-35B-A3B went from 49.0 to 57.5. These were separate power-mode checks, not the MTP comparison below.

pmset -g | grep powermode

On this machine, 1 meant Low Power, 0 Automatic and 2 High Power. Apple describes the modes here. I measured Low versus High, so I cannot claim a separate gain from High over Automatic. Check the setting before blaming the model.

MTP is a llama-server setting

Downloading a model with an MTP head does not enable speculative decoding by itself. In my tested llama.cpp build, I had to load the matching draft-head file and select the mode in llama-server:

llama-server -m model.gguf -ngl 99 \
  --spec-type draft-mtp -md matching-mtp-head.gguf

The command shows the relevant setting; the filenames stand for the main model and its matching head. For the on/off test, I kept the other server settings unchanged and removed the two speculation options for the off run. llama-bench, which I used for the original comparison, does not exercise this path.

Which models supported MTP?

I found and tested draft heads for Gemma 4 E4B, Gemma 4 12B, Qwen3.8-27B, Gemma 4 31B and Qwen3.6-35B-A3B. They added about 0.06–1.68 GB of weights. Qwen3.5-9B was the exception: I had no published MTP head to use for it.

The six-model comparison

These are generation speeds in tokens per second. The five MTP-capable models each ran through llama-server twice per setting, on the same coding prompt, with a 300-token output limit, temperature 0 and thinking disabled.

Modelllama.cpp, MTP offllama.cpp, MTP onMTP gainMLX, speculation off
Gemma 4 E4B43.555.71.28×52.4
Qwen3.5-9B37.1*Not tested: no head57.1
Gemma 4 12B27.337.41.37×33.1
Qwen3.8-27B12.019.11.59×19.0
Gemma 4 31B13.618.11.34×14.9
Qwen3.6-35B-A3B53.381.01.52×67.8

The on/off pairs measure MTP’s effect within llama.cpp. The MLX column and the starred Qwen 9B value come from the separate 512-token prompt / 128-token generation benchmark. Cross-engine comparisons therefore differ in workload, harness and quantization. All figures are two-run averages.

MLX supports draft-model speculation, but it was blocked by the Qwen cache implementation tested here. Gemma speculation was untested.

MTP improved generation by 28–59% across the five eligible models. In the configurations measured, llama.cpp with MTP was ahead on four models and effectively tied with MLX on Qwen 27B. A 19.1-versus-19.0 result is too close to call a meaningful win. Qwen 9B remained faster on MLX.

There is also prompt processing, or prefill: how quickly the model reads its input before writing an answer. llama.cpp led on five of six models in the original benchmark; MLX led on Gemma E4B. Both speeds matter as an agent accumulates conversation and tool results.

Original prompt and generation results, with speculation off
ModelPrompt: llama.cppPrompt: MLXGeneration: llama.cppGeneration: MLX
Gemma 4 E4B877.51072.763.652.4
Qwen3.5-9B455.8376.837.157.1
Gemma 4 12B344.7242.936.733.1
Qwen3.8-27B134.5107.311.819.0
Gemma 4 31B127.591.616.514.9
Qwen3.6-35B-A3B891.7636.354.567.8

This benchmark used a 512-token prompt, 128-token generation and two repetitions. Its figures should not be used as the off half of the server MTP test. For example, Gemma E4B’s matched server comparison is 43.5 → 55.7, even though its separate llama-bench result was 63.6.

Qwen 27B also exposed a precision mismatch

The latest server test puts Qwen 27B at 19.1 tokens per second with MTP, up from 12.0 without it, with 78% draft acceptance. This is the result I would use for my serving setup. MLX’s separate baseline measured about 19.0.

But “4-bit” does not mean the same thing in both files. The Qwen 27B GGUF contains 7.16 GB of 8-bit tensors, including Gated-DeltaNet and attention projections, plus 1.46 GB of 6-bit attention-output tensors. MLX uses 4-bit quantization for the corresponding projections. The GGUF main model is 18.97 GB; the MLX download is 16.05 GB, including a vision tower unused for text generation.

Reading fewer bytes helps explain part of MLX’s raw speed advantage. Implementation differences on the Gated-DeltaNet path matter too, including support for rollback. The six-model family split alone cannot tell us which engine is better.

I have not measured perplexity or answer accuracy. Higher precision may preserve quality, but a coherent answer to one prompt cannot establish that. These are speed measurements at different precision choices.

Why the 35B model is my pick

Qwen3.6-35B-A3B has roughly 35 billion parameters, with about 3 billion active per token. Its full weights still occupy about 20.4 GB, plus a 1.06 GB MTP head, but each generated token uses a subset of the model.

At 81.0 tokens per second with MTP, it is the large model I want to keep testing on this 32 GB machine. Qwen3.5-9B on MLX is another useful option: around 6 GB of weights and 57.1 tokens per second, leaving more memory for everything else. This is a choice based on speed and memory, not a model-quality ranking.

The setup and the limits

Measurements were taken on 15–16 September 2026, one model at a time, with one stream and High Power Mode enabled for the engine comparisons.

ComponentTested configuration
HardwareM1 Max, 10 CPU cores, 32 GPU cores, 32 GB unified memory
SystemmacOS 26.6, build 25G5065a; AC power
llama.cpp0.4.1, build 10964, commit b29c606e2; Metal backend
MLXmlx 0.32.2; mlx-lm 0.32.0 from git, commit 872ae88d1fac
Python3.13.12
QuantizationQwen GGUF: Q4_K_M; Gemma GGUF: QAT q4_0; MLX: affine 4-bit, group size 64

The PyPI mlx-lm 0.31.3 build I tried could not load Gemma 12B or E4B. Both loaded with the git build above. All support and cache observations here refer to these tested versions.

There are limits: two repetitions, one machine, unequal precision and no answer-quality evaluation. The Gemma MLX results came from an earlier run under the same conditions and build, before I removed those weights to reclaim disk space. Gemma E4B also has a substantial file-size mismatch between formats. I did not benchmark long-context performance at 8k–32k or complete agent tasks. Gemma’s untested MLX draft-model speculation could change its comparison.

What I am taking to DevFest

I am keeping llama.cpp and Qwen3.6-35B-A3B for my local agent experiments. The prompt-processing results, MTP gains and working rollback support make that a practical choice on this machine. The lesson for my demo is to test the server configuration I will actually use, including speculation, and check what “4-bit” contains.

The next step is to connect these models to a local agent harness, with its tools, memory and inference all running on your machine, and build your own fully local agent.

← All writing