howtospark
Recipes
Google logo

FP8-Dynamic256K ctx· model max73.93 tok/s@ 2KvLLMthinking offvanilla

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.

spark .1 · single nodepeer
76 GiB free
Expert planes21.5* GiBFP8
114 usable
38* / 114 GiB
34% of usable
Expert planesDense weightsDraft modelKV cacheActivations + graphsOS reserve

* 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. 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. 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-assistantbash

    Check for leftover *.incomplete files under .cache/huggingface/download/ before serving — a single-file checkpoint gives you no shard count to sanity-check against.

  3. 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'])
    PYbash

    Prints Counter({'sliding_attention': 25, 'full_attention': 5}), window 1024, 2 global KV heads, 262144 positions.

  4. 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}'bash

    No 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. 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.logbash

    On 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. 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. 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 Nbash

    Measured 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

ParameterValuePurpose
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}'bash

Troubleshooting

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.

  1. 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.

  2. 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.

gemma
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.