home lab · 1× DGX Spark · GB10 · all numbers measured

Two models. One Spark.
The full performance picture.

A co-located orchestrator + coder agent, tuned to roughly 2× on both ends — then benchmarked properly: decode curves, time-to-first-token vs context, concurrency scaling, and real speculative-decoding acceptance rates.

0
orchestrator tok/s peak
0
aggregate tok/s @ 16-way
0
coder tok/s on code
0
fastest TTFT
0
cloud cost
▾
benchmark 01

Decode speed, properly measured

Single-stream, temperature 0, on the co-located box. Hover the charts — every point is a real run.

decode tok/s vs output length · prose

Sustained generation, 128 → 1024 tokens
orchestrator · Qwen3.6-A3Bcoder · Qwen3.8-27B + MTP

time-to-first-token vs context length

Streaming; prompt from 64 → 16K tokens
orchestratorcoder

content matters: what MTP acceptance does to the coder

The same model, same flags — the draft head predicts code far better than prose.
code
30.4 tok/s · RateLimiter gen
prose
19–22 tok/s · tech explainer
no spec
11.6 tok/s · baseline
benchmark 02

One box, sixteen streams

Aggregate throughput scales hard on both models — while they share the same 121 GiB. All 16/16 requests succeeded at every level.

orchestrator · aggregate tok/s

Qwen3.6-A3B (MoE) · 256-tok generations

coder · aggregate tok/s

Qwen3.8-27B + MTP · 256-tok generations
benchmark 03

Why MTP works: measured acceptance

Straight from vLLM's Prometheus counters over the whole suite — how often the draft head's guesses survive verification.

0%
Orchestrator · Qwen3.6-A3B

31,309 / 43,927 draft tokens accepted. ≈ 2.14 bonus tokens per verified step — the engine that makes 105+ tok/s possible.

14,654 drafts · MTP n=3
0%
Coder · Qwen3.8-27B

7,372 / 13,503 draft tokens accepted on mixed work. On code it's higher — which is exactly why the coder hits 30+ tok/s there.

4,501 drafts · MTP n=3
tuning log · the honest kind

Two experiments: one flat, one big

Not every knob pays off — and the ones that do aren't always the ones you'd expect. Both measured on the box, temperature 0.

EXPERIMENT · CODER · ✗ no gain

Bigger batch budget didn't move decode

vLLM warned the MTP scheduler caps max_num_scheduled_tokens at 2048. So we raised --max-num-batched-tokens to 8192 and re-measured single-stream code decode. Result: statistical noise — and it ate memory headroom, so we reverted it.

27.0→27.7 tok/sflat · reverted

Lesson: single-stream MTP decode is already at the memory-bandwidth ceiling. The batch budget only helps prefill and concurrency, not one-request tok/s.

EXPERIMENT · ORCHESTRATOR · ✓ shipped

Turning the router's brain down made it better

The literature calls it the "reasoning trap": a verbose chain-of-thought steals the attention an orchestrator needs to read requests and route. We A/B'd our orchestrator's thinking on vs off across 3 plan-and-delegate tasks.

580 tok · 5.4s→299 tok · 2.9s2× faster

Lesson: ~half the tokens, ~46% lower latency, and delegation quality held at 3/3 clean. Light, decisive routing beats heavy deliberation — we shipped thinking-off for the orchestrator, kept the coder at medium (it's the one that hallucinates).

what moved the needle

Four moves. Both models win.

MOVE 01 · CODER

MTP was hiding in the checkpoint

The NVFP4 weights ship a trained multi-token-prediction head. Enabling it — method:mtp, n=3 — nearly tripled code decode, with tool-calls intact. No new image needed.

11.6→30.4 tok/s2.6×
MOVE 02 · ORCHESTRATOR

Swap the harness to a fast MoE

Replaced a 30B reasoning model (forced-eager, ~50 tok/s) with a 3B-active MoE running CUDA graphs + MTP. Same clean tool-calls, half the latency per turn.

51.7→105.6 tok/s2.0×
MOVE 03 · WORKFLOW

Think where it's cheap

Reasoning lives on the 105 tok/s orchestrator (thinking: high); the 30 tok/s coder thinks medium and executes. The delegation turn collapsed.

6.85s→2.72s / turn2.5×
MOVE 04 · QUALITY

Fast hands, sharp eyes

The MoE drafts; the stronger 27B critiques & improves — complex code only. First trial: it caught 3 real bugs the draft missed (an unlocked __repr__ race, a bool-is-int gap, a liveness-only test).

draft → critiquequality loop
the exact box config

DGX Spark settings

Two NVFP4 models resident on one unified-memory GB10. The whole fight is memory + startup order.

GB10
Grace-Blackwell · sm_121a
121.7 GiB
unified LPDDR5X
~273 GB/s
memory bandwidth
vLLM 0.26
stock aarch64 image
orchestrator · :8002 · util 0.42
# Qwen3.6-35B-A3B · MoE (3B active) · NVFP4
vllm serve Qwen3.6-35B-A3B-NVFP4 \
  --quantization modelopt \
  --moe-backend marlin \
  --speculative-config '{"method":"mtp",
        "num_speculative_tokens":3}' \
  --tool-call-parser qwen3_coder \
  --reasoning-parser qwen3 \
  # enable_thinking: true — reasons hard
  --gpu-memory-utilization 0.42 \
  --max-model-len 131072
coder · :8000 · util 0.45
# Qwen3.8-27B · Qwen3_5 hybrid · NVFP4
vllm serve Qwen3.8-27B-NVFP4 \
  --speculative-config '{"method":"mtp",
        "num_speculative_tokens":3}' \
  --attention-backend flashinfer \
  --enable-prefix-caching \
  --tool-call-parser qwen3_coder \
  --reasoning-parser qwen3 \
  # enable_thinking: true — it critiques
  --gpu-memory-utilization 0.45 \
  --max-model-len 131072 --max-num-seqs 32
⚠ Co-location rules (learned the hard way)
  • Utils are per-process shares of the whole pool — must fit free memory and sum ≲ 0.88 (0.42 + 0.45 here). Not a device-wide ceiling.
  • Start sequentially, coder first — simultaneous starts race; the loser dies with "no available memory for the cache blocks."
  • MTP loads from the checkpoint — no draft model, no third-party build; stock v0.26 aarch64 registers the arch.
  • --restart no + one ordered boot script — Docker auto-restart re-introduces the race on reboot.
  • A third model doesn't fit — a 35B MoE + MTP fails engine-init below ~0.30 util. Two residents is the sweet spot.
reasoning budget · orchestrator
Thinks hard, talks fast
thinking: HIGH

At 105 tok/s, reasoning is nearly free. It plans, decomposes, writes the first draft, and hands the coder a precise spec — signatures, file paths, test expectations.

reasoning budget · coder
Thinks enough to catch bugs
thinking: MEDIUM

At 30 tok/s, tokens are precious. Medium thinking is the sweet spot for its critic role — deep enough to spot an unlocked __repr__, cheap enough to stay responsive.

how it fits together

Orchestrator drafts. Coder sharpens.

1× DGX Spark · GB10 · 121.7 GiB unified · both resident
orchestrator · :8002

Qwen3.6-35B-A3B

MoE · 3B active · MTP · thinking high
112 tok/s peak · 91 ms TTFT · 514 agg @16
draft + spec →
← critique + fixes
coder · :8000

Qwen3.8-27B

Qwen3_5 hybrid · MTP · thinking medium
30.4 tok/s on code · 202 agg @16 · finds the subtle bugs

Trivial code → the orchestrator just writes it. Complex code → fast draft, then a correctness/edge-case/perf critique from the 27B, then integration. Reasoning lives where it's cheap; depth is applied where it counts.