Quick answer
On a MacBook Pro (M2 Pro, 32 GB), Qwen3.8-27B decodes 20.1 tokens/sec on code with TensorFold 1.0.8 + its DFlash2 drafter — up from ~11 tok/s in LM Studio. That’s 2.2× faster than the model’s own serial decoding and ~48% faster than the previous best (mlx-vlm + MTP). Drafted output is byte-identical to serial (lossless).
Qwen3.8-27B is a 27B-parameter hybrid model (64 layers, ~3:1 linear-to-full attention, with a vision tower). In MLX 4-bit the weights are ~15 GB, which fits in 32 GB of unified memory with room to spare. The catch: the model ships a Multi-Token Prediction (MTP) head that can act as a native speculative-decoding drafter — a small head that drafts several cheap candidate tokens, which the big model then verifies in parallel. Done right it is lossless: the output is byte-for-byte what serial decoding would produce, only faster.
The problem: LM Studio leaves speed on the table
LM Studio’s bundled MLX engine ignores the MTP head (upstream issue lmstudio-ai/mlx-engine#371), so it runs the model serially and tops out at ~11 tok/s. On paper an M2 Pro has ~200 GB/s of memory bandwidth, and a 15 GB 4-bit dense model reading its weights once per token has a hard serial ceiling of roughly 13 tok/s. At ~11 tok/s LM Studio is already ~80% of that ceiling, so no settings tweak can meaningfully help. The only real lever is speculative decoding.
Two speculative-decoding paths
| mlx-vlm + MTP | TensorFold + DFlash2 | |
|---|---|---|
| Engine | mlx-vlm 0.7.6 (MLX) | TensorFold 1.0.8 (Zig native) |
| Drafter | mlx-community/Qwen3.8-27B-MTP-4bit (~239 MB, the model’s own MTP head) |
z-lab/Qwen3.8-27B-DFlash2 (~3.6 GB) |
| Vision | Works | Text-only |
mlx-vlm + MTP uses the model’s own MTP head as the drafter (auto-detected as --draft-kind=mtp). It is a drop-in, keeps image input working, and is easy to run. TensorFold + DFlash2 is a younger, from-scratch Zig engine that serves the 27B with a dedicated DFlash2 drafter — faster, but text-only and (in 1.0.7) not qualified for M2 chips.
The TensorFold saga: from 0 tokens to the fastest option
My first TensorFold attempt (1.0.7) failed completely: the server started, then every request errored with RoundGpuFailure and produced 0 tokens. The root cause was memory — 1.0.7 kept the projection weights in memory twice (~32+ GB), which blew past the 32 GB machine and caused an out-of-memory failure.
I reported it as TensorFold issue #643. Another user dug in and found the exact cause and fix, and the maintainer confirmed and shipped both fixes in 1.0.8:
93a218dda— keep the projection weights once on Metal (the memory fix)b5056a511— a failed GPU round now ends only its own requests (and logs the Metal error)
After brew upgrade tensorfold, the same model loaded in ~19 GB instead of 32+, and generated tokens cleanly.
The benchmarks
All runs: --max-tokens 256 --temperature 0 (greedy), on a quiet machine, plugged in, median of 3 runs.
Prompt A — code-heavy
Write a detailed 400-word tutorial on quicksort in Python, then a short scene describing a rainstorm over a city.
| Config | Decode tok/s | Speedup vs own serial |
|---|---|---|
| LM Studio (baseline) | ~11 | — |
| mlx-vlm serial | 11.28 | — |
| mlx-vlm + MTP drafter | 13.54 | 1.20× |
| mlx-vlm server + MTP | 13.4 | — |
| TensorFold 1.0.8 + DFlash2 | 20.1 | 2.21× |
TensorFold "draft": false |
9.09 | — |
Prompt B — prose-heavy
Describe in vivid prose a rainstorm rolling over a sleeping city at night.
| Config | Decode tok/s | Draft acceptance |
|---|---|---|
| mlx-vlm + MTP | 11.30 | 53.7% |
| TensorFold + DFlash2 | 11.6 | 28.5% |
Key findings
- TensorFold 1.0.8 + DFlash2 wins for text: 20.1 tok/s on code — +48% over mlx-vlm+MTP (13.54) and 2.2× over its own serial (9.09).
- The gains are content-dependent. On code, TensorFold’s DFlash2 drafter accepts 56% of drafted tokens (~4.8 tokens/round). On fresh prose, acceptance drops to 28.5% and both engines roughly tie at ~11.5 tok/s.
- mlx-vlm + MTP is the safe, general pick: +20% on code, ~0% on prose, keeps vision working.
- Lossless: drafted output was byte-identical to
"draft": falsefor both engines on both prompts. TensorFold was even deterministic across runs (same sha256 every time).
Memory (the thing that nearly killed the whole idea)
| Config | Peak footprint |
|---|---|
| mlx-vlm serial | 16.4 GB |
| mlx-vlm + MTP | 17.1 GB |
| mlx-vlm + MTP + image | 19.4 GB |
| TensorFold 1.0.7 (broken) | 32+ GB (OOM) |
| TensorFold 1.0.8 | 19.2 GB |
That 32 GB → 19 GB difference is the whole story of whether a 32 GB Mac can run the model at all.
Vision
mlx-vlm + MTP still handles images (correctly described a screenshot in testing; image prefill ~55 s). TensorFold’s 27B row has no vision tower, so if you need image input, mlx-vlm is the one.
How to reproduce
mlx-vlm + MTP (keeps vision)
uv tool install mlx-vlm
mlx_vlm.generate --model "$HOME/.lmstudio/models/lmstudio-community/Qwen3.8-27B-MLX-4bit"
--draft-model mlx-community/Qwen3.8-27B-MTP-4bit
--max-tokens 256 --temperature 0 --verbose --prompt "Your prompt here"
TensorFold 1.0.8 + DFlash2 (fastest text)
brew install ashhart/tensorfold/tensorfold # or: brew upgrade tensorfold
tensorfold pull TensorFold/Qwen3.8-27B-MLX-4bit
tensorfold pull z-lab/Qwen3.8-27B-DFlash2
tensorfold serve TensorFold/Qwen3.8-27B-MLX-4bit
--drafter z-lab/Qwen3.8-27B-DFlash2 --context 4096 --temperature 0 --no-thinking --port 8081
Then POST to http://127.0.0.1:8081/v1/chat/completions like any OpenAI-compatible API.
Bottom line
| Use case | Best pick | Why |
|---|---|---|
| Fastest text generation | TensorFold 1.0.8 + DFlash2 | 20 tok/s code, 2.2× over serial |
| Need image input | mlx-vlm + MTP | +20% on code, vision works |
| Zero-hassle GUI | LM Studio | ~11 tok/s, no MTP support yet |
If you’re on an M-series Mac with 32 GB and want the fastest local text inference for Qwen3.8-27B today, upgrade to TensorFold 1.0.8 and add the DFlash2 drafter. And if you hit a bug, file it — the maintainer fixed #643 within a day.


0 Comments