Gemma 4 26B-A4B at FP8 serves its full 262,144-token context on a single DGX Spark in about 34 GiB — a quarter of the box — because 25 of its 30 layers are sliding-window attention and only 5 keep a global KV cache. Pair it with Google's 0.42B assistant draft and decode goes from 38.6 to 66.8 tok/s at a 2K prompt; quantize that draft to W4A16 (Sparkulator-Gemma-4-26B-A4B) and it goes to 73.9, for free — speculative decoding is verified, so a lossier draft cannot change a single output token. The draft must be run on vLLM 0.24.0: 0.26.0 crashes on it.
* segment sizes marked with an asterisk are estimates pending a measured run
Eval scores
(compare all)Bench card
No measured runs yet — be the first: install the spark-benchmark skill.
Overview
Gemma 4 26B-A4B is a 26B-parameter mixture of experts that activates about 4B per token, with a vision and an audio tower attached. On a DGX Spark the memory question that usually decides a recipe — how much context can I afford? — barely applies here, and the reason is in the architecture rather than the quantization. Of its 30 layers, 25 use sliding-window attention with a 1,024-token window, and the 5 that keep a global KV cache carry only 2 key/value heads apiece. The KV cost per token is therefore both small and nearly independent of how long the conversation gets, so the model's full 262,144-token context fits in roughly 7 GiB of cache. Weights, draft and cache together come to about 34 GiB, leaving most of the box free. The speed question is the one worth spending effort on. At 4B active parameters this decodes at 38.6 tok/s with no draft — respectable but not exciting on bandwidth-bound hardware. Google publishes a 0.42B "assistant" model built to speculate for it, and on this hardware it is the single biggest lever available: 63.4 tok/s at a 2,048-token prompt and 92.2 tok/s at 512, for 0.78 GiB of extra residency. The gain narrows as the prompt grows (40.7 tok/s at 8K), so it is at its best in the short-prompt, long-output conversational shape rather than on large-context retrieval. The one sharp edge is the engine version. Everything on this page was verified on 2026-07-27 against vLLM 0.24.0. vLLM 0.26.0 serves this checkpoint correctly with no draft and at identical speed, but it cannot load the assistant draft at all — it dies during compilation on a shape mismatch. Since the draft is most of why this recipe is worth running, 0.24.0 is the version to serve it on until that is fixed upstream.
- Full context, one node — 262,144 tokens in ~7 GiB of KV — 25 of 30 layers are sliding-window, so context is nearly free
- Draft is the whole story — 38.6 → 66.8 tok/s at a 2K prompt with the BF16 draft, → 73.9 with the W4A16 Sparkulator (+10.7% more), for 0.58 GiB
- Pin the engine at 0.24.0 — 0.26.0 crashes loading the assistant draft; no-draft speed is identical on both
- Room to spare — ~34 GiB total for weights, draft and full-context KV, on a 128 GB machine
Software requirements
- vLLM: 0.24.0 (0.26.0 works only without the draft — see troubleshooting)
- Target: RedHatAI/gemma-4-26B-A4B-it-FP8-Dynamic
- Draft: sapidlabs/Sparkulator-Gemma-4-26B-A4B (W4A16 of google/gemma-4-26B-A4B-it-assistant, 0.42B)
- Nodes: 1 × DGX Spark (GB10, sm_121, 128 GB unified)
Quick start
- 1
Free the node first
Anything still holding unified memory — a previous vLLM, a leftover Ray cluster — comes out of the same pool.
pgrep -af 'vllm serve|ray::' || echo 'node is clear' ray stop --force 2>/dev/null || true free -gbash - 2
Download the target and the draft
The target is a single ~27 GB model.safetensors, not a sharded export, so the download is one long file rather than many. The draft is small enough to be an afterthought.
hf download RedHatAI/gemma-4-26B-A4B-it-FP8-Dynamic \ --local-dir ~/models/hf/gemma-4-26B-A4B-it-FP8-Dynamic hf download google/gemma-4-26B-A4B-it-assistant \ --local-dir ~/models/hf/gemma-4-assistantbashCheck for leftover *.incomplete files under .cache/huggingface/download/ before serving — a single-file checkpoint gives you no shard count to sanity-check against.
- 3
Confirm the context is as cheap as it looks
Before budgeting memory, read the layer types out of the config. This is the fact the whole recipe rests on: only the full_attention layers keep a global KV cache, and here there are five of them.
python3 - <<'PY' import json, collections c = json.load(open('/home/joemuller/models/hf/gemma-4-26B-A4B-it-FP8-Dynamic/config.json')) t = c['text_config'] print(collections.Counter(t['layer_types'])) print('sliding window :', t['sliding_window']) print('global kv heads :', t['num_global_key_value_heads']) print('max position embeddings:', t['max_position_embeddings']) PYbashPrints Counter({'sliding_attention': 25, 'full_attention': 5}), window 1024, 2 global KV heads, 262144 positions.
- 4
Serve it, with the quantized draft, on vLLM 0.24.0
Two things need explaining. The KV pin: left to itself the engine takes 75 GiB of cache and reports 12x concurrency, which a single user cannot spend — 7 GiB still admits a full-length request and gives the rest of the machine back. And the draft: 0.24.0 cannot load a quantized Gemma 4 MTP draft as shipped, so the one-file patch below is not optional. Without it the draft loads silently with random projections and acceptance falls to ~3%.
python3 scripts/patch_gemma4_mtp_draft_quant.py --venv ~/venvs/vllm PATH="$HOME/venvs/vllm/bin:$PATH" \ vllm serve ~/models/hf/gemma-4-26B-A4B-it-FP8-Dynamic \ --served-model-name gemma-4-26b-a4b-it \ --max-model-len 262144 \ --gpu-memory-utilization 0.85 \ --kv-cache-memory-bytes 7516192768 \ --max-num-seqs 4 \ --port 8000 \ --speculative-config '{"model": "'$HOME'/models/hf/Sparkulator-Gemma-4-26B-A4B", "num_speculative_tokens": 4}'bashNo VLLM_USE_DEEP_GEMM=0 is needed for this checkpoint — vLLM selects the TRITON FP8 MoE backend on its own. The draft is sapidlabs/Sparkulator-Gemma-4-26B-A4B; hf download it first. Serving the stock BF16 draft instead is fine and needs no patch — it just costs 10% of decode.
- 5
Confirm what it actually chose
Three lines tell you the configuration came up as intended: the MoE backend, the resident weight size, and how much context the pinned cache actually admits.
grep -E "Fp8 MoE backend|Model loading took|GPU KV cache size|Maximum concurrency" /tmp/vllm.logbashOn vLLM 0.24.0 expect TRITON, 26.61 GiB, 323,358 tokens and 1.23x — all four confirmed 2026-07-27. 25.83 GiB instead of 26.61 means the draft did not load.
- 6
Warm it up before you trust any number
The first requests after load pay CUDA-graph and speculative-decode JIT costs. Three are enough here.
for i in 1 2 3; do curl -s http://127.0.0.1:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{"model":"gemma-4-26b-a4b-it", "messages":[{"role":"user","content":"Write an LRU cache in Python."}], "max_tokens":256,"temperature":0}' > /dev/null donebash - 7
Read the acceptance rate, not just the tok/s
vLLM does not put draft counts in the response stream, so they have to come from the Prometheus endpoint. The counters are cumulative for the life of the server — snapshot before and after a workload and subtract.
curl -s localhost:8000/metrics | grep -E '^vllm:spec_decode' | grep -v '^#' # accepted_tokens_total / draft_tokens_total = mean acceptance # accepted_tokens_per_pos_total{position=N} / drafts = acceptance at position NbashMeasured over the benchmark window on 2026-07-27: 5,100 drafted, 1,895 accepted = 37.2% mean, and 52.9 / 35.7 / 31.5 / 28.5% at positions 0-3. Position 3 still lands more than a quarter of the time, which is why k=4 is worth its cost here.
Key vLLM parameters
| Parameter | Value | Purpose |
|---|---|---|
| The model's full context. It is affordable here because only 5 of 30 layers keep a global KV cache — on a model with all-global attention this length would not fit. | ||
| Pins the KV pool at 7 GiB. Unpinned the engine takes 75 GiB and reports 12.22x concurrency at full context, which one user cannot use. 7 GiB gives 1.23x — still a full-length request — and decode is unchanged (63.76 vs 63.42 tok/s at 2K). | ||
| The draft, and the largest single lever on this recipe. Google's 0.42B assistant draft takes decode from 38.6 to 66.8 tok/s at a 2K prompt; the W4A16 Sparkulator build of that same draft takes it to 73.9 (+10.7%) for 0.58 GiB instead of 0.78. Requires vLLM 0.24.0, plus the draft-quantization patch for the W4A16 build. | ||
| Sets the CUDA-graph capture size for decode, which under speculation is max-num-seqs × (k+1) = 20 positions. Do not drop it to 1 or 2 on a spec-decode config — it is a decode knob, not just a scheduler limit. | ||
| Caps the engine's share of unified memory. With the KV pool explicitly pinned this mostly bounds the profiling headroom rather than the final footprint. |
API usage
curl -s http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"gemma-4-26b-a4b-it",
"messages":[{"role":"user","content":"Summarise the attached design doc in five bullets."}],
"max_tokens":512,"temperature":0.7}'bashTroubleshooting
- The quantized draft loads with no error at all, and then acceptance is ~3% and decode is worse than no draft
- vLLM 0.26.0 dies at startup with 'a and b must have same reduction dim, but got [s47, 3840] X [5632, 1024]'
- Is the speculative draft actually worth the memory?
- Should I pass VLLM_USE_DEEP_GEMM=0?
- The engine reports 12x concurrency at full context. Am I wasting memory?
- My decode numbers look implausibly high — hundreds of tok/s on a 26B model
- How does this compare to running it at a short context?
Revision history
What has changed on this page since it was published, and what it measured. Newest first.
- docs
Fixed the data shape of the highlights and software-requirements lists: they were authored as {label, value} objects where the schema is a list of strings, which crashed the recipe page server-side. Flattened each entry into one sentence; no serving flags, measurements or memory figures changed.
- performance
Switched the draft to sapidlabs/Sparkulator-Gemma-4-26B-A4B, a W4A16 quantization of Google's assistant draft, and added the vLLM patch it needs to load at all.
Decode at a 2,048-token prompt 66.8 → 73.9 tok/s (+10.7%) on Spark-1, and 66.5 → 73.0 (+9.9%) on Spark-2 running the same pair in the reverse order; 512-token prompt 94.5 → 99.1 and 92.1 → 100.0; 8,192-token prompt 42.2 → 43.7 and 40.8 → 45.0. Six of six counterbalanced arms favour the quantized draft. Draft on disk 0.782 → 0.585 GiB, resident weights 26.61 → 26.41 GiB. Acceptance on a fixed prompt set 62.8% vs 60.5–65.3% for the BF16 draft, i.e. unchanged — and speculative decoding is verified, so output is identical either way. The published 63.4 tok/s headline was an earlier session's BF16-draft measurement; the matched in-session BF16 baseline for this A/B was 66.8.
Memory budget
Weights load in a measured 25.83 GiB, or 26.61 GiB with the assistant draft resident — so the draft costs 0.78 GiB, which is cheap for the decode it buys. The interesting number is the KV cache. This model runs 25 sliding-window layers (window 1,024) against only 5 full-attention layers, and those 5 carry just 2 KV heads each, so the per-token KV cost is small and almost flat in context length: with the pool left to size itself at --gpu-memory-utilization 0.85 the engine reported 75.14 GiB of KV holding 3,202,672 tokens — 24.6 KB/token, and 12.22x concurrency at the full 262,144-token request length. For a single user that is 11x more KV than can ever be used, so the pool is worth pinning: at --kv-cache-memory-bytes 7516192768 (7 GiB) the engine reports 323,358 tokens = 1.23x concurrency at the full context and decode is unchanged (63.76 vs 63.42 tok/s at a 2K prompt, same day, same flags). That puts the whole working configuration — weights, draft, and enough KV for one maximum-length request — at about 34 GiB of the Spark's 128 GB. Every figure here is read from the engine log or the harness; the segment split below is apportioned from parameter counts, not measured per-tensor.