Qwen 3.8 27B Benchmark on a MacBook Pro M5 Max vs RTX 5090

We ran the same Qwen 3.8 27B sweep on a MacBook Pro M5 Max and an RTX 5090. The desktop decodes about four times faster, but the draft head wins on both, and only one machine stalls.

Sep 4, 202616 min readFollow

Topics You Will Master

How a MacBook Pro M5 Max and an RTX 5090 compare on the same Qwen 3.8 27B sweep
Which numbers carry across the two machines, and which ones never will
Why the draft head is worth 1.71x on the Mac and 1.81x on the card
Why one repeat in three stalled on the laptop and none did on the desktop

We ran Qwen 3.8 27B on an RTX 5090 in an earlier post. Here we run the same model, the same quantization and the same llama.cpp sweep on a MacBook Pro M5 Max with 36GB of unified memory.

In this blog, we will learn what changes between the two machines and what does not. We will see where the laptop lands against the card, which settings still earn their place on Apple Silicon, and why the honest number to report on a laptop is not the median.

Bestseller

Fine Tuning LLM with Hugging Face Transformers for NLP

Learn transformer architecture fundamentals and fine-tune LLMs with custom datasets.

Enroll on Udemy 30 day refund, lifetime access

Our MacBook Pro M5 Max Test Setup

One MacBook Pro with an Apple M5 Max, 32 GPU cores, and 36GB of unified memory, on macOS 26.6.2 and wall power. The model is Qwen 3.8 27B in Q4_K_M, the same blob Ollama had already pulled. The runtime is llama.cpp build b10793 with the Metal backend.

We kept the discipline from the desktop run. One llama-server per configuration, the server killed between every configuration, and the port polled until it was free. Every prompt carries a unique request id, so llama.cpp cannot answer the second repeat from the cache of the first.

Sampling is greedy, and each generation is capped at 256 tokens. That cap matters, and we come back to it at the end.

How Much Memory Does llama.cpp Get on a 36GB Mac?

This is the first number to read on a Mac, and it is not 36GB. llama.cpp prints it at startup:

PLAINTEXT
cmn  common_param:   - MTL0    : Apple M5 Max (28753 MiB, 28753 MiB free)
cmn  common_param:   - CPU     : Apple M5 Max (36864 MiB, 36864 MiB free)

Here, we can see the machine has 36,864 MB in total, and the GPU may address 28,753 MB of it. That is 28.1 GB. The model, the KV cache, the recurrent state, and the scratch buffers all have to fit inside that number.

The weights alone take 15.4 GB. So the whole tuning question on this laptop is what we do with the twelve and a half gigabytes that are left.

Advertisement

What Is Inside the Model File?

Qwen 3.8 carries a multi token prediction head, called MTP, inside the GGUF file. It is one extra layer at index 64, sitting behind the 64 transformer layers, and it comes down with an ordinary Q4_K_M download.

We do not have to trust anyone about that. The loader reads the header out loud:

PYTHON
qwen35.block_count             = 65
qwen35.context_length          = 262144
qwen35.nextn_predict_layers    = 1
qwen35.full_attention_interval = 4
qwen35.ssm.state_size          = 128

Here, we can see 65 blocks where the model has 64 layers, one prediction layer, and a real context window of 262,144 tokens.

The next line is the one that makes this model usable on a laptop at all. full_attention_interval = 4 means only every fourth layer keeps a KV cache. The other 48 layers are state space layers, and they carry a small recurrent state whose size never changes.

llama.cpp says the same thing when it builds the cache at an 8K window:

PLAINTEXT
llama_kv_cache: size = 512.00 MiB (8192 cells, 16 layers, 1/1 seqs), K (f16): 256.00 MiB, V (f16): 256.00 MiB
llama_memory_recurrent: size = 149.62 MiB (1 cells, 64 layers, 1 seqs 0 rs_seq), R (f32): 5.62 MiB, S (f32): 144.00 MiB

Sixteen layers hold a cache, not 64. That works out to 64 KB per token, and the 150 MB of recurrent state is a flat cost that does not grow with the context at all.

There is one more line worth reading. With drafting switched off, the server tells us it is leaving the draft head on the floor:

PLAINTEXT
model has unused tensor blk.64.nextn.eh_proj.weight (size = 29491200 bytes) -- ignoring
model has unused tensor blk.64.nextn.enorm.weight (size = 20480 bytes) -- ignoring
model has unused tensor blk.64.nextn.hnorm.weight (size = 20480 bytes) -- ignoring

We paid for those tensors in the download. Until we pass one flag, they do nothing.

Advertisement

How Many Tokens Should We Draft on the M5 Max?

The head drafts a few tokens cheaply, and the full model checks all of them in one pass. Accepted drafts are free. Rejected ones cost us the drafting work. The only choice left is how many tokens to draft per step.

We ran the whole ladder at an 8K context, from drafting off up to n=4.

Decode throughput at each draft depth on the MacBook, rising from 18 tokens a second with the draft head off to 31 at n equals 3

Let me tabulate the full ladder for your better understanding.

setting best of 3 median of 3 vs off accept rate tokens/step Metal GB
MTP off 18.2 9.2 1.00x n/a n/a 16.20
n=1 23.4 18.4 1.29x 0.821 1.82 16.75
n=2 20.8 19.5 1.14x 0.614 1.99 16.90
n=3 31.1 28.5 1.71x 0.575 2.10 17.04
n=4 24.7 20.3 1.36x 0.444 1.77 17.19

Three draft tokens is the peak here, which is where the RTX 5090 peaked too. One flag takes this laptop from 18 to 31 tokens a second, and the head costs about 550 MB of extra Metal memory at n=1, then roughly 150 MB per level after that.

Why is there a peak at all? Because two curves pull against each other, and llama.cpp prints both of them:

PLAINTEXT
spec common_specu: statistics draft-mtp: #mean acc len = 2.94, #acc rate/pos = (0.818, 0.636, 0.483)

Here, we can see the first drafted token survives 82% of the time, the second 64%, and the third only 48%. Draft deeper and each single guess is worth less. But each accepted step harvests more tokens, so tokens per step climbs. Throughput is the two multiplied together, and that product tops out in the middle.

Advertisement

Why Did One MacBook Give 31 and 0.3 Tokens a Second?

Now look at the gap between the two speed columns above. At n=3 the best run was 31.1 tokens a second and the median was 28.5. At drafting off the best was 18.2 and the median was 9.2.

A median is supposed to be the safe number. Here it is the misleading one. Let's plot every single generation instead of summing them up.

Every timed generation in the draft sweep on a log scale, with one repeat per setting collapsing below one token a second

Here, we can see the shape of the problem. Most repeats land in a tight band between 18 and 31 tokens a second. Then one repeat per setting falls off the chart, down to 0.3 or 0.7 tokens a second. That is the same server, the same flags, and the same prompt, a few seconds later.

Nothing about the model changed. The machine ran out of physical memory and started paging the weights back and forth. One generation of 256 tokens took 952 seconds.

So, here comes the rule this laptop taught us. On a Mac we report the stall rate next to the speed, because a median over three repeats can be dragged anywhere by a single page fault. The best of three is the closest thing we have to a clean measurement of the setting itself.

Warning

A stall does not look like an error. The server keeps streaming, the logs stay clean, and the model still answers correctly. It just answers sixty times slower, and only for a while.

Does Flash Attention Still Matter on Apple Silicon?

On the desktop card this was a quiet win. Flash attention saved 2.3GB and cost nothing, so we left it on and moved along. On a 36GB laptop it is not quiet at all.

Metal memory with flash attention on and off, showing the compute buffer growing from 257 MB to 1664 MB

-fa on -fa off
total Metal allocation 17.80 GB 19.17 GB
of which compute buffer 257 MB 1,664 MB
peak resident memory 19.44 GB 21.60 GB
decode 18.80 tok/s never finished
written to swap none 3.2 GB

The compute buffer is the whole story. With flash attention on, llama.cpp reserves 257 MB of scratch space. With it off, the unfused attention path builds the full attention matrix and asks for 1,664 MB instead.

That extra 1.4GB is what tips this machine over. We killed the run after 50 minutes. Before that, the server's own counter recorded what happened:

PLAINTEXT
0.47.708.033 slot print_timing: n_gen =  288, tg = 15.63 t/s, tg_3s = 15.55 t/s
13.33.811.336 slot print_timing: n_gen =  321, tg =  0.41 t/s, tg_3s =  0.04 t/s
13.36.823.502 slot print_timing: n_gen =  376, tg =  0.48 t/s, tg_3s = 18.26 t/s

Here, we can see the timestamps at the left. The model generated 33 tokens in twelve minutes and forty six seconds, then went straight back to 18 tokens a second as if nothing had happened.

That is the real difference between a graphics card and unified memory. Running out of VRAM is an allocation that fails and tells us so. Running out of unified memory is a soft collapse that looks like a slow model.

The advice does not change from the desktop post, but the reason gets much harder. Leave flash attention on. On this machine it is not a tuning choice, it is a requirement.

Advertisement

How Does the M5 Max Compare With the RTX 5090?

Speed does not carry across machines, and we should not pretend it does. A 27B model at Q4_K_M streams 15.4 GB of weights for every token, so decoding is a memory bandwidth problem first. A 5090 has fast dedicated memory and a 600 watt budget. This laptop shares LPDDR5 with the display and every open app.

Acceptance is different. It measures how predictable the model's own text is to its own draft head, and that depends on the weights and the prompt, not on the silicon.

Here is the ladder we measured on the card, from the earlier run:

Decode throughput at each draft depth on the RTX 5090, rising from 74 tokens a second with MTP off to a peak of 134 at n equals 3

Put that next to the MacBook ladder further up and the two curves have the same silhouette at very different heights. Both start low with the draft head off, climb to a peak at n=3, and fall away at n=4.

Draft acceptance rate at each depth on the MacBook against the RTX 5090 values, both falling as the draft gets deeper

setting MacBook tok/s vs off RTX 5090 tok/s vs off Mac accept 5090 accept
MTP off 18.2 1.00x 73.6 1.00x n/a n/a
n=1 23.4 1.29x 104.8 1.42x 0.821 0.860
n=2 20.8 1.14x 125.5 1.70x 0.614 0.766
n=3 31.1 1.71x 133.6 1.81x 0.575 0.674
n=4 24.7 1.36x 119.5 1.62x 0.444 0.592

Let me tabulate what that table actually says. The absolute speeds are a different machine: the card decodes roughly four times faster at every depth, and no flag on the laptop closes that. The ratios and the acceptance rates are the same experiment twice, and those line up.

Both acceptance curves start high, fall at every step, and fall fastest after n=2. At n=1 the two agree closely, at 0.821 against 0.860. Past that the MacBook accepts less, and there is a plain reason for it: the desktop run averaged four different tasks, and this sweep ran one coding prompt. Acceptance depends on the text being generated, so a narrower prompt set gives a different level.

The shape held, the winner held, and the peak sat at n=3 on both machines. So the tuning we did on the card transfers to the laptop, even though not one of its numbers does.

There is one difference that no ratio captures. The RTX 5090 ran the whole sweep without a single stall. The MacBook stalled on one repeat in three, and that is the part of this comparison a benchmark table cannot show.

Advertisement

What Would We Actually Run on a 36GB Mac?

For everyday interactive work:

BASH
llama-server -m Qwen3.8-27B-Q4_K_M.gguf \
  -ngl 999 -fa on --jinja -np 1 \
  -c 8192 \
  --spec-type draft-mtp --spec-draft-n-max 3

That is 31 tokens a second at 17.0 GB of Metal memory, measured exactly as written.

Two of those flags are not preferences on this machine. -fa on saves 1.4GB of scratch memory that we do not have spare. The 8K window is what let the sweeps finish, because a 32K window adds 1.5GB of KV cache and pushed this laptop into swap.

There is one more setting worth knowing about, and it is not a speed flag. Qwen 3.8 reasons before it answers. At a 32K window with drafting off, we measured 18.8 tokens a second. The first token arrived after 0.555 seconds. The first word of the real answer took about 17 seconds, behind a median of 426 thinking tokens. Nobody waits on throughput. They wait through the thinking block. --reasoning-effort medium is the flag that shortens it.

Note

llama.cpp accepts minimal, high and max for that flag without complaining. Qwen 3.8's chat template defines only xhigh, medium and low, and it raises on anything else. The server starts cleanly and then returns HTTP 500 on every request.

What We Are Not Claiming

This is a spot check on one laptop, and a partial one.

The honest limits:

  • The sweep ran one coding prompt with a 256 token cap. The model spent that budget inside the thinking block, so this run measures decode speed only. It says nothing about answer quality on this machine.
  • The ladder ran forwards only, and n=5 never completed. On the desktop we ran every ladder backwards as well, and that check is missing here.
  • The KV cache, context window, and reasoning effort sweeps did not finish on this machine. Those numbers in the desktop post have not been replicated here.
  • Memory pressure is inside every number above. A browser and two other apps held 3 to 4GB throughout, which is a realistic laptop and not a clean bench. A Mac with 64GB would not hit any of this.
  • One quantization, one model. A different quantization would move the numbers, and we tested none.
Advertisement

Conclusion

This is how Qwen 3.8 27B compares on a MacBook Pro M5 Max and an RTX 5090. The card decodes about four times faster and never stalled. The laptop answers the same questions correctly, at 31 tokens a second instead of 134, and pages to swap on one repeat in three. Every tuning decision we made on the card held on the laptop.

Key takeaways:

  • Turn the draft head on with --spec-type draft-mtp. It is 1.71x at n=3 on this laptop, and it is already inside the model file.
  • Keep -fa on. It is worth 1.4GB of scratch memory here, and turning it off pushed this machine into swap.
  • Report the spread, not just the median. One repeat in three fell below one token a second, and a median hides that.
  • Speed does not transfer between machines, and acceptance does. Compare ratios and acceptance rates, and leave absolute tokens per second where they were measured.
  • Expect about a quarter of the card's decode speed on this laptop, and expect the settings that won on the card to keep winning.
  • Size the window against the memory you actually have free, not against the 36GB on the box.

Next steps:

Found this useful? Keep building with me.

New tutorials every week on YouTube: or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments