DGX Spark · GB10 / sm_121 · field notes

926 tok/s on one DGX Spark — and up to 21× of 262k context in the KV cache. Here's exactly how.

The unsloth Qwen3.6 NVFP4 checkpoints dropped last week. I spent four days benchmarking and optimizing them on a GB10 — testing what works and what doesn't for DGX Spark specifically — next to the uncensored AEON stack. Same box, same bench, one variable at a time. Full recipes below, including the failures.

Replicate the headline in three commands

Everything the 926 tok/s and the giant KV pool come from — no custom container, no patches:

# 1. the image: nightly vLLM main built for GB10 (by eugr)
docker pull eugr/spark-vllm:latest

# 2. serve unsloth's 35B MoE — fp8 KV + MTP; let vLLM pick kernels (auto beats forcing, measured below)
docker run -d --name unsloth35b --gpus all --ipc=host --net=host \
  -e CUTE_DSL_ARCH=sm_121a \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  -v ~/vllm-cache:/root/.cache/vllm \
  --entrypoint vllm eugr/spark-vllm:latest \
  serve unsloth/Qwen3.6-35B-A3B-NVFP4-Fast \
  --host 0.0.0.0 --port 8888 --trust-remote-code \
  --kv-cache-dtype fp8 --moe-backend auto \
  --gpu-memory-utilization 0.55 --max-model-len 262144 \
  --max-num-seqs 64 --max-num-batched-tokens 32768 \
  --enable-chunked-prefill --async-scheduling --enable-prefix-caching \
  --speculative-config '{"method":"mtp","num_speculative_tokens":3}'

# 3. after EVERY boot: send one real request before trusting anything (health lies — see below)
curl -s http://127.0.0.1:8888/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"unsloth/Qwen3.6-35B-A3B-NVFP4-Fast","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'

Notes that save you a day: the ~/vllm-cache mount persists compile/autotune caches across container rebuilds (cold boot 13–16 min → warm ~2 min on some images — h/t walterra). --max-num-seqs is your parallel-request ceiling; 64 was my sweep max. Utilization 0.55 already holds ~10 full-262k contexts; 0.65 held ~21× in an earlier run. Never go above 0.80 on unified memory — the OS lives in the same RAM, and things die ugly (documented below).

Two checkpoints: Qwen3.6-35B-A3B-NVFP4-Fast (MoE, 3B active) and Qwen3.6-27B-NVFP4 (dense). Unsloth's published numbers were not measured on a DGX Spark — they're real figures from other hardware. The point of this table is what changes when the hardware is a GB10.

·Their (non-Spark) claims vs this box

unsloth published (other hardware) vs measured on this GB10 · different benches — direction, not decimals
Figureunsloth publishedMeasured on this GB10Why the gap
35B-A3B decode295.2 tok/s (cute-DSL)89–95 tok/s c=1 · 336 agg c=8 · 926 agg c=64theirs is batch-aggregate on faster-memory hardware; mine crosses it at high concurrency
27B decode125.9 tok/s (cute-DSL)33.5–35.5 tok/sGB10 has 273 GB/s memory bandwidth — a dense 27B physically can't stream weights faster than ~26 tok/s without speculation here
"1.79× faster than FP8"b12x kernels measured: prefill flat, decode −11%doesn't transfer to GB10 (explained below)
MMLU-Pro 35B / 27B85.85 / 86.25(their data, not re-measured)decides the model choice in section 05

Aggregate @ 64 streams

926 tok/s

35B-A3B, MTP k=3, U=0.55 — no throughput wall found up to 64

KV pool @262k

10.65×

at U=0.55 — and 21.46× recorded at U=0.65. Never exceed 0.80 util

27B w/ DFlash graft

33.5–35.5

tok/s — +35% over stock, fastest 27B-class figure I've seen published for GB10

b12x, engaged properly

−11%

decode, and KV pool cut 10.65× → 3.62×

What I achieved / fixed — the short version
  • 926 tok/s aggregate at 64 concurrent streams — and still climbing at the ceiling I tested
  • 10.65× full-262k KV at U=0.55, up to 21.46× at U=0.65 — fp8 KV works because unsloth ships calibration scales. Hard rule: never above 0.80 util on unified memory
  • b12x + MTP together — documented nowhere: the speculative drafter needs its own backend ("moe_backend":"triton" inside --speculative-config)
  • Measured b12x as a net loss on GB10 — with the mechanism, not a shrug
  • 27B +35% via AEON's DFlash drafter grafted onto unsloth weights (hidden size & vocab match)
  • Prefill measured as a curve, not a number — the single-figure prefill quote is a myth

01The b12x recipe — and what it's for

For DGX Spark, unsloth recommend --moe-backend flashinfer_b12x. Their concern is valid: on their quant scheme, falling back to Marlin kernels costs ~2.5×. But add MTP speculation and the flag kills the boot:

ValueError: moe_backend='flashinfer_b12x' is not supported for unquantized MoE.
Expected one of ['triton', 'flashinfer_trtllm', 'flashinfer_cutlass', 'aiter'].

The MTP draft head ships unquantized and the flag applies to every layer. The fix — implied by r0b0tlab's config, spelled out nowhere:

export CUTE_DSL_ARCH=sm_121a

vllm serve unsloth/Qwen3.6-35B-A3B-NVFP4-Fast \
  --moe-backend flashinfer_b12x \
  --speculative-config '{"method":"mtp","num_speculative_tokens":2,"moe_backend":"triton"}'

The log confirms Using 'FLASHINFER_B12X' NvFp4 MoE backend. It boots, it answers, it's real.

02Auto vs b12x — vLLM's own picks win on this box

To be precise about what's being compared: both columns are unsloth's checkpoint. The left column lets vLLM auto-select kernels (it picks Cutlass — on my image the Marlin fallback unsloth warn about never happens). The right column is unsloth's Spark recommendation (b12x), made bootable with my drafter fix. Same box, same benchmark, same hour:

35B-A3B · U=0.55 · N=8 · fp8 KV — my recipe (auto) vs the b12x path
Metricauto → Cutlass + MTP k=3 (mine)b12x + Triton draft + MTP k=2
Prefill pp81926,694 / 6,4886,645 / 6,775
Prefill pp327684,824 / 4,8504,968 / 4,993
Decode c=186.0 / 89.675.2 / 81.8
Decode c=8322 / 336184* / 337
KV pool @262k10.65×3.62×

Prefill flat, decode −11%, KV pool cut to a third (b12x's workspace buffers eat it). This isn't unsloth being wrong — it's hardware context. The boot log shows why: Using Triton/FLA GDN prefill kernel. This model is a GDN hybrid, so prompt processing never touches the MoE math b12x accelerates, and generation speed on GB10 is pinned by memory bandwidth, not compute. b12x's 1.79× is real where MoE compute is the bottleneck. On a GB10 with this architecture, it isn't — so vLLM's auto selection is the optimal Spark config, and now that's a measurement.

03What GPU utilization actually buys

gpu-memory-utilization is how much of the 121 GiB the engine reserves. People treat it as a speed dial. It isn't one: generation speed doesn't change with it at all. What it buys is KV cache — the working memory that holds conversations — which means more parallel sessions and longer contexts:

KV pool vs utilization — decode identical in every row · "10.65×" = ten full 262k-token conversations at once
ModelutilKV @262kDecode c=1
35B-A3B (fp8 KV)0.5510.65×86–90
35B-A3B (fp8 KV)0.6521.46×same
27B dense (fp8 KV)0.551.90×30–33
27B dense (fp8 KV)0.744.24×same

Two hard ceilings before you crank it: the operating system lives in the same 121 GiB (push past ~0.85 and the OS can't even spawn processes — SSH hangs, ports never open), and speculative-decoding scratch memory allocates outside this setting entirely (the AEON tab has the full story — it cost me a power-cycle). House rule: 0.80 max, and leave more if anything else runs on the box.

04Prefill is a curve, not a number

35B-A3B (MoE, fp8 KV) 27B dense (fp8 KV)
02,5005,0007,500 35B-A3B at 8k: 6,600 tok/s (TTFT 1.2 s)6,600 27B dense at 8k: 1,600 tok/s (TTFT 5.1 s)1,600 35B-A3B at 32k: 4,850 tok/s (TTFT 6.8 s)4,850 27B dense at 32k: 1,085 tok/s (TTFT 30 s)1,085 8k prompt32k prompt
Single request, prompt caching defeated. My own docs had "~7,000 tok/s" written down as the prefill number; it was an 8k-only figure.
35B-A3B (MoE) 27B dense
short 8k 32k 35B-A3B, short prompt: 0.10 s 0.10 s ← the bar is 3px wide. That's the point. 27B dense, short prompt: ~0.3 s 0.3 s 35B-A3B, 8k prompt: 1.2 s 1.2 s 27B dense, 8k prompt: 5.1 s 5.1 s 35B-A3B, 32k prompt: 6.8 s 6.8 s 27B dense, 32k prompt: 30.0 s 30.0 s 010 s20 s30 s time to first token (TTFT) · one linear scale, deliberately · lower is better
TTFT is the number you feel — and it spans 300× on the same box. A short question answers in 0.10 s on the 35B; the same model needs 6.8 s at 32k, and the dense 27B needs 30 s — paid on every message, before a single word appears. No setting moves it; only prefill throughput and prompt caching do. (In real agent use with vLLM's prefix cache at ~86% hits, measured mean TTFT was 2.2 s — the cache is why agent workloads survive.)

05Dense vs MoE — read this before picking "the smart one"

Speed measured here · accuracy from unsloth's published figures
35B-A3B27B denseratio
Prefill 8k / 32k6,600 / 4,8501,600 / 1,0854.1× / 4.5×
Decode c=1 / c=887.8 / 32930.5 / 1372.9× / 2.4×
KV @262k10.65×1.89×5.6×
MMLU-Pro85.8586.25−0.4

On paper: 3–4× the speed for 0.4 benchmark points. In practice, one honest nuance from daily use: for coding specifically, the dense 27B still writes noticeably better code than that 0.4-point gap suggests — benchmarks and real work don't fully agree. My split: the dense 27B stays as the dedicated coding model on quality grounds; the MoE 35B does everything else, where its speed and 5.6× KV pool win outright. Pick per workload, not per leaderboard.

AWhose shoulders

Nearly everything above started as someone else's work. Two of the biggest wins were just reading what they'd published, carefully.

Published a working sm_121 config listing "B12X routed experts · MTP K=2 · Triton draft MoE." That phrase is the entire b12x+MTP fix. He had it before I did.
The NVFP4 checkpoints, CUTE_DSL_ARCH=sm_121a, the b12x availability probe, and the accuracy figures. Their Spark advice didn't pan out on my workload — their checkpoints absolutely did.
The sm_121a images, the DFlash drafters (one of which I grafted onto unsloth's 27B for +35%), the uncensored checkpoints, and properly Spark-measured model cards — the only vendor numbers here that compare directly.
The persistent /root/.cache/vllm mount (cold 13–16 min → warm ~2 min) and the fork-starvation warning at high utilization — which I then reproduced the hard way.
The nightly vLLM-main image the unsloth stack runs on, and the honest README line — "NVFP4 performance on Spark not fully optimized in vLLM (any build)" — my data independently confirms it.

BHow to read my numbers (and everyone else's)

Three things you need to know before comparing any figure on this page to a figure from anywhere else.

1. Benchmarks measure the measuring tool, too

My benchmark asks the server to stream its answer word-by-word, the way a chat app does. Most benchmarks ask for the whole answer in one block. Streaming has overhead — and I measured exactly how much: the same model, on the same machine, on the same day, scores 68.6–71.9 tok/s with my streaming tool and 92.7 with my older non-streaming one. Nothing about the model changed; only the measuring method did. That's a 25% gap from methodology alone. So: numbers from the same tool can be compared; numbers from different tools can't — including mine against unsloth's or AEON's. Where this page puts them side by side anyway, it says "direction, not decimals" and means it.

2. I published my mistakes on purpose

In four days I reached two confident, wrong conclusions and nearly a third:

Wrong #1: "The new AEON image is 20% slower." It wasn't — I had switched benchmarking tools between the measurements (see above). Running both images through the same tool showed them identical. Wrong #2: "The b12x kernels can't work on this GPU." vLLM's own check said they were available the whole time; the actual blocker was one unquantized layer needing its own setting — the fix in section 01. Near-miss #3: I almost judged the new AEON image on a single model — the one model (of three) that happened to show zero difference. Testing the rest revealed +22%.

3. The habits that caught all three

Change one thing at a time. Re-measure the old setup with the new tool before declaring anything slower. After every server start, send one real chat message — the health check is a different program from the engine and will happily say "ready" over a corpse. And when two runs disagree with each other by more than the difference you're trying to detect, run a third instead of publishing. None of this is clever. All of it is the reason this page isn't three falsehoods with confident formatting.