pocharlies/dgx-spark-moe-benchmarks
Four MoE models on a DGX Spark: speed, tool-calling, and what actually breaks Full measurement campaign on NVIDIA DGX Spark (GB10, 128 GB unified, ~273 GB/s), vLLM 0.23.1rc1.dev301+g04c2a8dea, arm64/sm121. Every number here is measured on this hardware, with the raw evidence included. The headline: on synthetic tool-calling benchmarks all four models score 91-95 %. In a real coding agent, three of them score 0-1 out of 14 and one scores 11 out of 14. If you pick a model from the… See the full description on the dataset page: https://huggingface.co/datasets/pocharlies/dgx-spark-moe-benchmarks.
Four MoE models on a DGX Spark: speed, tool-calling, and what actually breaks
Full measurement campaign on NVIDIA DGX Spark (GB10, 128 GB unified, ~273 GB/s), vLLM 0.23.1rc1.dev301+g04c2a8dea, arm64/sm121. Every number here is measured on this hardware, with the raw evidence included.
The headline: on synthetic tool-calling benchmarks all four models score 91-95 %. In a real coding agent, three of them score 0-1 out of 14 and one scores 11 out of 14. If you pick a model from the synthetic score, you will pick wrong.
1. Speed (single-stream decode, contention-gated)
Steady-state decode; prefill excluded from the denominator. Any sample taken while another request — or another pod on the same time-sliced GPU — was in flight is discarded.
Speculative acceptance, measured per workload:
Speculative decoding is workload-dependent, not a constant multiplier. EAGLE3 gives Qwen3-Coder +80 % on code editing and −26 % on prose — at 15 % acceptance you pay for the draft and get nothing. Quote the workload with the number or the number means nothing.
2. Tool calling — synthetic
`tooling_matrix`, 370 deterministic cases, seed 45739, six phases.
Run-to-run variance on the same model and config was 1.4 % (Ornith scored 345 and 340 on two runs), so treat gaps under ~1.5 points as noise.
Form matrix: 500 tool schemas exposed per call, across 10 shapes (chat vs responses API, tool_choice auto/required/named, streaming on/off, strict, catalog narrowing).
Only failure: Ornith on responses-auto.
3. Tool calling — a real agent
14 tasks driving [opencode](https://opencode.ai) 1.18.9 over a sandbox repo. Every task is graded by a script — a test that passes, a file containing X, a command exiting 0. No LLM judge. The graders were validated first: each one fails on the pristine repo and passes on a correct fix (14 ok, 0 bad).
What the three failures look like
Under opencode's ~9,300-token system prompt, the qwen3_5_moe models stop emitting API tool calls and start emitting invented XML in the response text:
Let me read the relevant files...
<think>
<read filePath="/…/config/settings.json" />grep
<think>
<Grep>
<pattern>LEGACY_RATE_TOKEN\s*=</pattern>
</Grep>An unterminated <think> block, then a hallucinated tool syntax. With no </think>, the reasoning parser never fires, everything stays in content, and the tool parser sees nothing it recognises.
This is not a client bug. We proxied and logged the raw HTTP: opencode sends its 9 tools correctly with tool_choice: auto. And the same endpoint, given a short prompt with a well-formed tools array, answers perfectly:
finish_reason: tool_calls
tool_calls: [{"function": {"name": "grep", "arguments": "{\"pattern\": \"LEGACY_RATE_TOKEN\"}"}}]The failure appears only under a large agent system prompt. Note the latencies: the models that fail do so in 5-7 s because they give up in one turn; the one that works takes 45 s because it actually does the work.
It is not an Ornith fine-tune regression either. Its base model (nvidia/Qwen3.6-35B-A3B) fails the same way, as does the 27B from the same family. qwen3_moe (Qwen3-Coder) is the only architecture here that holds its tool-call format under load.
Tried and rejected: switching to the qwen3_xml tool parser made it worse — 0/8 with a 900 s median latency (timeouts) versus 5 s.
4. Two optimizations that did not work
Both were plausible. Both were measured. Both are negative — and the negatives are the useful part of this repo.
FlashInfer autotuning: zero effect
vLLM logs an explicit warning:
No tuned config covers fp8_gemm … falling back to runner=CutlassFp8GemmRunner tactic=-1. This shape is outside the tuning bucket range … to avoid this perf cliff.Persisting the autotune cache (VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR) and running a warm pass first changed decode by 135.3 → 135.1 tok/s, and TTFT not at all.
Why: the shapes in the warning are 249 and 32,160 tokens — those are prefill. The decode gap was never there. Read which phase a warning refers to before optimizing for it.
Requantizing attention FP8 → NVFP4: catastrophic
NVIDIA's checkpoint is MIXED_PRECISION: MoE experts in NVFP4, attention in FP8. The byte math makes converting attention look very attractive:
Attention is 5.5 % of the checkpoint but 51 % of the bytes read per token — the classic MoE decode profile: experts are sparse, attention is dense. Converting it should cut 26 % of per-forward bytes and lift the ceiling from 96 to 112 forwards/s.
We did it: 90 linear_attn tensors, FP8 → NVFP4 W4A16, using vLLM's own scaled_fp4_quant. Round-trip L2 error 9.25-9.41 %, linear_attn 0.94 → 0.53 GiB. Clean conversion, model loads and serves.
Result:
Slower AND 62.7 points worse. Look at the pattern: freeform and negative — where the model writes prose or abstains — are untouched. Everything requiring exact structured output collapses. A 9.3 % error in attention is invisible in prose and fatal to a tool call.
Two things worth taking away:
- NVIDIA's FP8 attention is a deliberate, correct choice. Now measured rather than assumed.
- MTP acceptance is a free quality probe. It fell from 98-100 % to 81-87 % before we ran any quality battery. In MTP the draft and the verifier share the model: degrade the model and the draft's predictions stop matching. If acceptance drops after a change, the change hurt the model — you do not need a benchmark to know something is wrong, only to measure how much.
5. How to read a tok/s number
Three traps, each of which cost us hours:
- State the workload. The same model and config measured 52.8 to 138.7 tok/s depending only on whether the task was prose or code editing.
- A neighbour on a time-sliced GPU halves your numbers silently. We measured one config at 138.7 and at 26.8 tok/s. The contaminated figure looked entirely plausible. Gate on
/metricsand discard samples taken withnum_requests_running > 0. - Verify the config you think is running is running. We once concluded EAGLE3 "gave zero speedup" when a
kubectl applyhad silently reverted the flag to its manifest default. The tell isspec_decode_num_accepted_tokens_total— absent counters mean nothing is speculating. "On but useless" and "never enabled" are indistinguishable from throughput alone.
Contents
evidence/
tool370-{ornith,qwen3coder,dense27b,nv-base,nv-attnfp4}.jsonl 370 cases each, per-case verdicts
tooling500.jsonl 30 runs: 3 models x 10 shapes
{ornith,nvidia,sweep}-*.json decode measurements, per-run
agentic/
results-*.jsonl 14 tasks x 4 models, script-graded
scripts/
decode_speed.py contention-gated decode measurement
requant_attn_nvfp4.py FP8 -> NVFP4 attention requantizer (with round-trip validation)
agentic_suite/ 14 tasks, runner, scorer, sandbox generatorRelated
- `pocharlies/Qwen3-Coder-30B-A3B-NVFP4-EAGLE3-DGXSpark` — the 138.7 tok/s build, complete and reproducible
- `pocharlies/Ornith-1.0-35B-NVFP4-MTP-graft` — the 154.5 tok/s build
Recommendation
For an agentic coding CLI, Qwen3-Coder-30B-A3B, despite being 10 % slower than Ornith and tied on synthetic tooling. It is the only one of the four that reliably emits tool calls under a real agent's system prompt, and 11/14 versus 0/14 dwarfs a 16 tok/s difference.
For non-agentic serving, Ornith is the fastest here.
