Qwen3.8-27B Kernel-Panicked My Mac Three Times. Here's the Number Apple Doesn't Print.
Your Mac has 24GB. Metal gives the GPU 17.76 GiB. Every guide comparing model size against total RAM is doing the wrong arithmetic — and that 6.25GB gap is where the crashes live. Measured on one machine, with the runs that failed.
Three kernel panics before it ran. — And the fix every forum gives you makes it worse.
TL;DR
Problem: Qwen3.8-27B at Q4_K_M needs ~19.5GB live. My 24GB M4 Pro panicked three times — IOGPUGroupMemory.cpp:528, WindowServer, hard reboot.
It was not out of memory. The panic log says Compressor Info: 4% of compressed pages limit (OK). It is a bug in Apple’s GPU driver, tripped when the working set is driven near full.
The number that decides everything: your Mac has 24GB, but Metal hands the GPU 17.76 GiB. Not 24. Every guide comparing a GGUF’s file size against total RAM is doing arithmetic against a number the GPU never sees.
The fix everyone gives you is backwards. Raising iogpu.wired_limit_mb made my crashes worse, and there is a reason.
What works: UD-Q3_K_XL (12GB) + llama.cpp at 16k context with q4_0 KV — 12.87 tok/s, 21/21 objective tasks, 6/6 tool calling.
- Part 1: the Gemma 4 26B version of this problem
- Video: (YouTube link)
The setup
- Hardware: MacBook Pro M4 Pro, 24GB unified memory, 273 GB/s
- Model: Qwen3.8-27B (dense, 27B params, native vision)
- Quant: Unsloth
UD-Q3_K_XL(12GB) —UD-IQ4_XS(13GB) does not fit with the vision projector - Runtime: llama.cpp
0.2.0
llama-server -m Qwen3.8-27B-UD-Q3_K_XL.gguf \
--mmproj mmproj-F16.gguf \
-ngl 99 -c 16384 -fa on -ctk q4_0 -ctv q4_0 --jinja
17.76 GiB, not 24
Run this on your own Mac:
ioreg -l | grep recommendedMaxWorkingSetSize
Divide by 2^30. On a 24GB M4 Pro it comes back 17.76 GiB — about 74% of unified RAM. That is the budget your model, your KV cache and your compute buffers all share.
| your Mac | what Metal actually gives the GPU |
|---|---|
| 16 GB | 11.8 GiB |
| 24 GB | 17.8 GiB |
| 36 GB | 26.6 GiB |
| 48 GB | 35.5 GiB |
Q4_K_M at ~19.5GB live is 110% of that. It cannot run. It does not fail politely — it panics the machine.
Why raising the wired limit makes it worse
The advice everywhere is sudo sysctl iogpu.wired_limit_mb=.... It is backwards.
Normal memory is a car in a car park: if the system needs the space, it tows it. Wired memory is bolted to the ground — the kernel is not allowed to move it. Raising the wired limit does not make the car park bigger. It bolts down more cars, so there is less room to shuffle, not more.
I raised it. It panicked again.
The KV cache is the part that grows
The weights are fixed the moment you pick a quant. The thing that grows while you talk is the KV cache — one key and one value per token, per layer. Sixteen thousand tokens of context is not free.
-ctk q4_0 -ctv q4_0 quantises the cache itself and hands most of that space back. It is the difference between 16k context fitting and not.
What it actually scored
Everything measured on the one machine, scripts published:
| Decode | 12.87 tok/s (273 GB/s bandwidth predicts 12.2–15.2) |
| Prefix cache | 14.2× — 47.9s → 3.4s on 4,826 tokens |
| Objective tasks | 21/21 |
| Tool calling | 6/6 |
| Metal working set | 92% of the 17.76 GiB ceiling |
The grader was wrong, not the model
One test scored 5/6 because the model answered "os\.system" where my fixture expected "os.system". The tool’s own description says the argument is a regex — so the escaped dot is correct and the bare one is the sloppy answer. The model was right. The score is 6/6.
It can drive a television
I gave it three tools over MCP — read what’s playing, screenshot the screen, force-stop the app — plus a plain-English house-rules file, and pointed it at a Sony Bravia over adb.
It read the title, looked at the frame, and caught the metadata lying twice in one run: a video titled “clips that made iShowSpeed famous” was a Fortnite lobby, and one titled “TOM HOLLAND LAUGHING #meme” was something else entirely. Both blocked.
One block does not hold. Measured: +4s app dead and the TV on its launcher, +8s the app relaunches itself, +20s playback restored. It has to keep checking. A check costs 23–718 seconds depending on how many tool calls it makes, so it is a patrol, not a guard.
llama.cpp can do video — with a trick
Qwen3.8-27B supports video natively. llama-server rejects it:
content[].type = "video_url" -> 400 unsupported content[].type
image_url = data:video/mp4;… -> 500 Invalid uri format
But the decoder is in the build — libmtmd has mtmd_helper_video_init and shells out to ffmpeg. Only the server never routes that content type to it, and the URI parser gates on the MIME string while mtmd sniffs the bytes. So:
{"type": "image_url",
"image_url": {"url": "data:image/png;base64," + base64_of_mp4}}
It decodes. Measured ceiling on this laptop: ~240 tokens per second of video, ~3.3s of compute per second of video, and 60s of video eats 85% of a 16k context. About 70 seconds is the wall. The model card advertises hour-scale — that needs 224k video tokens, roughly 14× more context than fits here.
This is a workaround against a parser gap, not an API. It will stop working the day the server routes video_url properly, which is the real fix.
Local vs cloud, on one clip
Same 4 seconds of Big Buck Bunny (CC-BY, Blender), same question: what animal is in the burrow?
| model | where | the answer |
|---|---|---|
| Qwen3.8-27B Q3 | my laptop | a gorilla ✗ |
| Molmo2-8B 6-bit | my laptop | a mole ✗ |
| Gemini 3.1 Pro | cloud | a bear ✗ |
| Gemini 3.7 Flash | cloud | a large rabbit ✓ |
It is a rabbit. Only Flash got it. Everyone else saw a dark shape in a hole and guessed a furry mammal — a sensible prior, and wrong. All of them got the camera push-in right; identifying a shape in shadow is the hard part, not watching.
Two traps I fell into first, and both flattered the local model:
- I left thinking on for Gemini and off for Qwen.
gemini-flash-latestresolves togemini-3.7-flashand spent 272 thought tokens.thinking_budget=0does not stop it either. - Gemini was silently on low
media_resolution— 4 frames at 66 tokens each, against Qwen’s 8 frames at 448px for 1,070 tokens. A quarter of the pixels. Low is the default on that path.
Check both before you compare anything to anything.
And run a no-input control
I published a Molmo2-8B result — 1.7s, plausible answer — before discovering the model had received no frames at all. The same prompt with no video returned the identical sentence, word for word. “Badger” came from the word burrow in my question.
mlx_vlm 0.6.14 builds the prompt as a bare string, so Molmo2’s chat template never emits <|video|>, and its processor cannot encode video regardless. generate() accepts video=[...] and drops it silently. Use the image path with pre-sampled frames instead — that works, and costs 15.9s, not 1.7s.
Any evaluation that does not run a no-input control is measuring the language prior, not the vision.
Will it fit on your machine
pip install localfit
localfit qwen3.8-27b --gpu-gb 12
--gpu-gb scores every quant against whatever budget you give it instead of mine. On a Mac, use the 74% figure from the table above — not the number on the box.
What I’d actually run
UD-Q3_K_XL, 16k context, -fa on, q4_0 KV. 12.87 tok/s, everything fits, nothing panics. IQ4_XS is a better quant on paper and it will not run here once the vision projector is loaded — 101% of budget.
If your Mac has 16GB, the honest answer is that 27B is not your model. Use the table.