Field brief · Aug 20, 2026 · serving recipes
Two public recipes landed on DGX Spark in the same week. Neither is a new model. They are measured serve configs: image, checkpoint, a flag stack that survived A/B, and a harness that does not lie about tokens. I read both repos end to end, mapped them onto the Qwen card, and wrote down the playbook plus the architecture that makes 262K cheap.
The 27B weights are flour. The recipe is which oven (SGLang vs vLLM vs MLX),
what temperature (NVFP4 / FP8 / BF16), which intern drafts the next sentence
(MTP / DSpark / DFlash2), how many guesses before the senior checks
(k / block size), how much of the fridge is reserved for leftovers
(KV + GDN pool), and how you time the dish.
What they actually ship, in order of usefulness:
k that wins at one stream can lose 43% of fleet throughput at eight.
Repo: MiaAI-Lab/Qwen3.8-27B-SGLang-DGX-Spark.
Hardware: NVIDIA DGX Spark, GB10, 128 GB unified, ~273 GB/s.
Engine: cookbook-pinned lmsysorg/sglang:qwen38-27b, plus a derived image for DFlash2
because that merged upstream after every published tag.
Three swap-in scripts, same NVFP4 27B, only the drafter changes:
| Script | Drafter | Mem | Best for |
|---|---|---|---|
start.sh | EAGLE/MTP 3/1/4 | 0.95 | Long-form writing |
start-dspark.sh | DSpark block 7 | 0.90 | Agents, code, tools, default chat |
start-dflash.sh | DFlash2 block-diffusion | 0.90 | Code and essay; chat once counted right |
| Knob | Cookbook / trap | Pin | Why |
|---|---|---|---|
| GDN dtype | float32 | bf16 | float32 measured −3% |
| Radix | extra_buffer | extra_buffer_lazy | DFlash2 rejects lazy; force extra_buffer there |
| Mem fraction | 0.85 DSpark | 0.90 / 0.95 MTP | 0.95 + DFlash2 hard-rebooted at graph capture |
| Prefill chunk | 2048 | 8192 | TTFT, not decode tok/s |
| DSpark block | — | 7 (code peak) | Block 5 is +8% prose / −16% code |
| MTP EAGLE | — | 3/1/4 | Sweep peaked at 3 steps |
| KV | — | fp8_e4m3 | ~32.8 KB/token |
| CPU pin | none | X5 5-9,15-19 | GB10 is big.LITTLE; +2–7% |
| GDN pool | ratio 0.9 | concurrency × 4 | Verify window is a separate buffer |
| YaRN / 1M | optional | MTP only | Leaks into DSpark/DFlash2 draft config |
They also pin FlashInfer, torch.compile + decode graphs, prefill graphs off, thinking parser qwen3, tool parser qwen3_coder. NGRAM spec rejected (~30% under MTP). Accept threshold left at 1.0.
completion_tokens. DFlash2 batches ~3.75 tokens per SSE event at a fixed ~8 events/s.lm_head (~2.5 GB) at CUDA-graph capture and hard-rebooted GB10. Fix: in-place lm_head.quant_method.apply.
Repo: 0xBakeer/Qwen3.8-27B-FP8-on-a-single-DGX-Spark.
Same Spark, different oven: official vLLM 0.27.1 aarch64, target Qwen/Qwen3.8-27B-FP8.
This is the cleaner lab notebook. Speed only. No quality eval. Read LIMITATIONS.md before quoting.
| Config | Fresh gen | Edit-heavy | c8 aggregate |
|---|---|---|---|
| Stock, no spec, no prefix | 7.88 | 7.88 | — |
| MTP k=3 | 17.70 | 21.3 | — |
| MTP k=15 | 13.39 | 39.0 | — |
| DSpark k=7 | 20.05 | 46.8 | 208.7 |
| DSpark k=14 | 18.77 | 58.5 | 119.7 |
k=15 is a trap on mixed workloads: best MTP on edits, worst overall on fresh generation. No single k wins both latency and fleet throughput.
default = supported and not is_hybrid. Qwen3.8-27B is hybrid. You must pass --enable-prefix-caching. Worth 14–22× on long shared prefixes. Costs ~22% of KV capacity.enable_thinking: false and a max_tokens cap are measuring a reasoning novel.qwen3 for reasoning, qwen3_xml for tools on vLLM. A typo costs a full load.DFlash2 later, same device: generative 31.72 vs DSpark 19.12 vs MTP 17.99 vs stock 7.94 (accept 4.61). That win is single-stream. At c≥2, 4-bit plus free in-checkpoint MTP overtakes on their curve. On vLLM, FP8 is the only Qwen3.8-27B build that can serve DFlash2, because the selector wants an unquantized LM head.
Collapsing both cooks plus the SGLang cookbook into something you can run on any oven:
trim() like KV on partial reject. YaRN can leak into the draft config.From the Qwen3.8-27B card: dense 27B, hidden 5120, 64 layers, vocab padded to 248,320, FFN intermediate 17,408, native 262,144 context, YaRN to 1M, native MTP, native vision. Layout: 16 × (3 × GDN→FFN then 1 × gated attention→FFN). That is 48 Gated DeltaNet layers and 16 full-attention layers. 3:1.
As a back-of-envelope after embeddings, yes. Architecturally, no. Layers are not equal, and most of a layer is not attention.
3 × 17,408 × 5,120 = 267,386,880
Full-attention GQA (24 query heads / 4 KV heads, head dim 256) is about 73M plus gates. GDN uses a different split (48 value heads, 16 QK heads, head dim 128) and a recurrent state update. Different math, similar width, so layers stay similarly sized.
Classic transformers do full attention every layer. Attention cost and KV memory grow with sequence length. At 262K–1M that becomes the product. Linear / recurrent mixers (Mamba, DeltaNet, Gated DeltaNet) keep a fixed-size state. You fold the new token into a matrix of constant size. The weakness: the notepad forgets sharp token-to-token pointers. The 3:1 hybrid, also used in Qwen3-Next / 3.5 / 3.6, spends cheap long-range glue on 75% of depth and real attention every fourth layer so retrieval and copying still work. That is how a 27B dense model advertises 262K native context on a 128 GB box.
Only the 16 gated-attention layers do attention that needs keys and values from previous tokens.
The 48 GDN layers have a recurrent state of fixed size. Nothing there grows with t.
16 layers × 4 KV heads × 2 (K and V) × 256 dim × 2 bytes = 65,536 bytes ≈ 64 KB per token (BF16) FP8 KV (Mia): half of that ≈ 32.8 KB per token 1M sequence ≈ 33 GB
If all 64 layers were full attention, multiply by 4: ~131 KB/token FP8, ~131 GB for 1M tokens of KV alone. Mia’s GDN pool is extra and does not grow with sequence length (~78 MB/slot × concurrency × 4; default 10 concurrent → 40 slots ≈ 3.1 GB).
Autoregressive decode, no tricks: the 27B reads all its weights once per token. That is the 15–33 t/s floor depending on box and quant.
Speculative decoding: a cheap intern proposes the next k tokens. The senior (the 27B) checks them in one weight-read. If the intern got five right, you paid one senior pass for five tokens. The senior’s distribution still wins. Speculation cannot force a token. It can change text across configs because batch geometry changes floating-point order. 0xBakeer measured 8/20 greedy prompts byte-identical across tuned vs stock. Deterministic within a config, not across configs.
Extra heads live in the checkpoint. Sequential guesses. Each still pays a full lm_head over ~248k vocab. No extra download. Mia’s peak: EAGLE 3/1/4. Cost coefficient 0.153 per draft token.
Separate ~1–1.4B, 5-layer drafter (~2.6 GB) that emits a block (often 7) in one shot, with a lightweight Markov/confidence bias. Cost 0.046. Wins when the intern is cheaper and accurate enough (edit-heavy 98%+; fresh gen much worse).
Not a tiny AR model. A block-diffusion drafter: it paints a whole block in parallel, then a ~2M-parameter pairwise path selector plus a two-tap grouped dynamic conv pick a consistent path. Target still verifies. Published accept ~4.6–5.5 tokens/pass on their mixes. Mia: ties DSpark on code (~51), beats MTP on essay (~25 vs 24), and crushes short-chat once you count completion_tokens (the ~9 tok/s reading was SSE). 0xBakeer: 31.7 generative vs DSpark 19 on FP8 vLLM. v2 vs v1 is those extra tensors, not a new GPU. A Metal port is a model port, not a CUDA port.
The Spark recipe is four NVIDIA serving technologies plus a GDN state machine. They are not synonyms, and they are not sitting in MLX under different names.
SGLang is a CUDA-first serving engine: paged KV, radix prefix cache, overlap scheduler, speculative runners (EAGLE, DSpark, DFLASH), and a zoo of NVIDIA kernels. The Spark “recipe” is a cell in the SGLang cookbook that Mia started from and then pinned.
MLX is Apple’s array library. The GPU language is Metal. Graphs are lazy evaluation
plus optional mx.compile, not CUDA graph capture. Speculation is whatever mlx_vlm / mlx-dspark
implemented in Python plus those shaders.
SGLang has an experimental SGLANG_USE_MLX=1 backend. It is a partial port of the scheduler
onto MLX arrays. It does not give you FlashInfer, CUDA graphs, or Mia’s DFlash2 runner.
Running it on a Mac is SGLang Python overhead without the CUDA kernel zoo. Wrong runtime, incomplete port, slower.
A GPU is fast. Telling it what to do from the CPU is not, when each decode step is a few milliseconds of math. Normally: CPU launches kernel, GPU runs, CPU launches next. For 64 layers that is dozens of launches per token. Launch overhead can rival the math when the math is skinny.
CUDA graph capture records a sequence of launches once, then replays the whole sequence with one submit.
Pointers and shapes must be fixed. That is why batch size is capped
(--cuda-graph-max-bs-decode 4), why changing k can force a recapture, and why Mia’s
DFlash2 crash happened at graph capture when a 2.5 GB dense head suddenly materialized.
Analogy: teaching a kitchen line by shouting each step versus filming the service once and pressing play. Replay is faster until someone orders a dish that is not in the film.
GEMM is the matrix multiply behind almost every linear layer. FlashInfer is the kernel library behind SGLang and vLLM: attention, GEMM, MoE, FP8/FP4, CUDA-graph compatible workspaces. On Blackwell it includes NVFP4 GEMM that runs on Tensor Cores with weights and activations in 4-bit (W4A4), accumulate in FP32.
NVFP4 the codec (portable): E2M1 4-bit values, 16-wide E4M3 FP8 scale, optional FP32 tensor scale. MLX can store this packing.
NVFP4 the Tensor Core datapath (Spark): the matrix unit swallows FP4×FP4. That is W4A4.
NVFP4 on MLX Metal: a shader looks up the E2M1 nibble, multiplies the E4M3 scale, and dots against fp16/bf16 activations. That is W4A16. You saved weight bandwidth. You did not get Blackwell FP4 MMA.
Naive attention builds a huge score matrix, softmax, times values. That matrix does not fit in fast memory at long context. FlashAttention tiles the work so softmax is done online in SRAM. FA2 improved tiling. FA3 (Hopper-oriented, now a serving backend) overlaps math and memory better, unifies prefill and decode so the whole model including attention can sit inside one CUDA graph, and is a selectable SGLang/vLLM attention backend.
FlashInfer vs FA3: overlapping kitchens. FlashInfer is the broader serving library (paged KV, decode split-K, FP4 GEMM). FA3 is a specific attention algorithm that often wins throughput and graph-friendliness on NVIDIA. Mia pins FlashInfer on the Spark NVFP4 cell. MLX attention is neither.
Each of the 48 GDN layers holds a recurrent state per request. Speculation mutates state for tokens that might be rejected. Prefix cache wants to store a GDN snapshot in the radix tree without freezing the live request. Strategies:
no_buffer: tight, fights with cache/spec.extra_buffer: a second slot so the running request keeps mutating while a snapshot goes to the tree.extra_buffer_lazy: allocate that extra lazily. Mia’s DSpark/MTP pin. DFlash2 asserts and rejects it.
Mia’s formula: max-mamba-cache-size = MAX_CONCURRENT_REQUESTS × 4.
The spec verify window is a separate engine buffer. If you also multiply by draft tokens you over-provision ~2×.
ReplaySSM (Tri Dao, 2026; used on SGLang’s DSpark path) stores a few hundred bytes of raw inputs per draft step
and, after the sampler picks the accepted length, folds the accepted prefix from a checkpoint.
Bit-identical to running those tokens for real. That is what lets DSpark compose with radix prefix cache
on mutable GDN state.
Mia’s 56.6 / 227.6 is NVFP4 weights consumed as W4A4 on Tensor Cores, FlashInfer FP4 GEMMs, FA3-class attention option, decode CUDA graphs so the CPU is not the bottleneck, an FP8 KV cache so 16 attention layers stay cheap, a GDN pool and ReplaySSM so speculation and prefix cache do not fight the 48 notepads, and a DFlash2 runner fused into that graph. A Mac has the same weight codec and can run the same DFlash2 tensors in software. It does not have that kernel orchestra. SGLang-on-Mac does not import the orchestra. It imports a scheduler onto Metal without the instruments.
I ran this research because the Spark recipes look copy-pasteable onto an M4 Max 128 GB, which has roughly 2× the memory bandwidth of GB10. That bandwidth argument only sets the serial floor. Mia’s 56.6 is already ~3.4× Spark autoregressive. Doubling that on Metal is not a bandwidth identity.
| They do | Ports to Metal? |
|---|---|
| Measurement order, two-probe table, completion_tokens | Yes. Steal this. |
| Prefix-cache opt-in on hybrid | Yes. Highest-utility lever people skip. |
| k × concurrency rank-reversal | Yes. Metal’s verify cost moves the optimum (wide DSpark blocks lose on prose). |
| NVFP4 weight packing | Yes. Same E2M1 + E4M3 bytes. |
| W4A4 Tensor Cores | No. Metal is W4A16 dequant GEMM. |
| CUDA graphs, FlashInfer FP4 GEMM, FA3 | No. Different ISA. |
| FP8 E4M3 paged KV | Partial. No hardware FP8 KV. |
| GDN extra_buffer pool + ReplaySSM + overlap scheduler | Concept yes. That kernel no. |
| DFlash2 algorithm (selector + conv) | Yes, as a model port. Not as a CUDA verify graph. |
| 16-way batched spec verify (227 aggregate) | No. Different sport. Do not caption it as the same ladder. |
Steal their lab order, not their kernel list. Serve MLX if you are on this silicon. Compare two scoreboards honestly: single-stream speculation (the Mac can play) versus 16-way graph batching (Spark can play). Keep hard prose next to any structural fixture. Speculation that only wins on LRUCache is not a chat recipe.