Quick benchmark prompted by the question "how can E4B-it be only ~20 tok/s on MPS?"
Setup
- Hardware: Apple M4 Max, 128 GB unified memory
- Prompt: 52 tokens (system + 1 user turn asking for a 300-word explainer on virtual memory / TLBs / page faults / swap)
- Generation: greedy,
max_new_tokens=256, thinking disabled - transformers 5.8.0 / torch 2.11.0 / llama.cpp (homebrew, Metal)
| stack | model | precision | gen tok/s | prefill tok/s |
|---|---|---|---|---|
| transformers (CPU) | gemma-4-E4B-it | bf16 | 8.70 | — |
| transformers (MPS) | gemma-4-E4B-it | bf16 | 20.99 | — |
| llama.cpp (Metal) | gemma-4-E4B-it | bf16 | 21.52 | 499 |
| transformers (MPS) | gemma-4-E2B-it | bf16 | 26.77 | — |
| llama.cpp (Metal) | gemma-4-26B-A4B-it | Q4_K_XL | 41–44 | 300–345 |
Speculative decoding with the official MTP drafter (gemma-4-E{2,4}B-it-assistant) was slower on MPS in both cases (~15–19% regression) — verification overhead and Metal kernel-launch cost outweigh the drafting win at this model size. The README's "up to 2x" assumes a discrete GPU.
- At identical precision and identical model, transformers MPS ≈ llama.cpp Metal for token generation (20.99 vs 21.52 tok/s). The HF stack is not the bottleneck on Apple Silicon for decode.
- Quantization, not the framework, is the lever. The earlier ~2x gap I observed was llama.cpp running a Q4 model vs transformers running bf16. A Q4/Q8 E4B GGUF should land in the 50–80 tok/s range on this Mac.
- Prefill is where llama.cpp wins — ~500 tok/s vs (much slower) HF MPS. Matters a lot on long prompts; doesn't matter on short ones.
- MTP drafters need a real GPU. The drafter is ~78M params, but each verification step on Metal pays a non-trivial overhead that erases the speedup at E2B/E4B target size. They'd shine on the 26B/31B targets on CUDA.
- CPU bf16 is ~2.4× slower than MPS bf16 for the same model — fine for offline batch but not interactive.
transformers chat/transformers serverequirestransformers[serving]extras (openai,fastapi,uvicorn); without them the CLI errors on import. There is no--assistant-model/ speculative-decoding flag intransformers serve.Gemma4ProcessorandGemma4VideoProcessorrequirepillowandtorchvisioneven for text-only — install both up front.- The
hfCLI download (xet client) downloaded the BF16 GGUF at 2.1 MB/s. A directcurl -Lfrom the same CDN URL got 26 MB/s — ~12× faster. Thecurlprogress bar's100.0%was misleading; the file was actually ~14 MB short and llama.cpp errored withtensor 'per_layer_model_proj.weight' data is not within the file bounds.curl -C -resumed cleanly. device_map="auto"on a Mac withaccelerateis fiddly; explicit.to("mps")was simpler and worked first try.
uv init --python 3.12
uv add transformers torch torchvision accelerate pillow librosa
# bench script: see main.py / bench_cpu.py in the gist files
# llama.cpp side
brew install llama.cpp
curl -L -C - -o ~/models/gemma-4-E4B-it-BF16.gguf \
https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-BF16.gguf
llama-server -m ~/models/gemma-4-E4B-it-BF16.gguf -c 4096 -ngl 999 --port 8080