--- license: mit base_model: Qwen/Qwen3.8-Flash-Next-FP8 tags: - dgx-spark - gb10 - sm121 - sglang - nvfp4 - field-notes - recipe --- # Qwen3.8-Flash-Next NVFP4 on 2× DGX Spark — field notes *Mirror of https://github.com/beastllama/dgx-spark-qwen38-flash-next-recipe — the base recipe is [MiaAI-Lab's](https://github.com/MiaAI-Lab/Qwen3.8-Flash-Next-Dual-DGX-Sparks); this is the config delta plus everything that went wrong on the way and the measurements nobody had published. From the same homelab as the [GLM-5.3-Flash + DFlash2 recipe](https://huggingface.co/randomllama/GLM-5.3-Flash-DFlash2-SGLang-2x-DGX-Spark).* Serving **Qwen3.8-Flash-Next-NVFP4** across **two DGX Sparks (GB10 / sm_121)** with SGLang, TP=2 over ConnectX-7 RoCE (**single rail** — dual-rail is measured fabric capability, untested under SGLang). Vision enabled. 262,144 context. MTP speculative decoding. **Start here, then read the findings.** The base stack is MiaAI-Lab's — this repo is the config delta on top of it plus everything that went wrong getting there and how it was fixed (see [Credit](#credit)). What follows was *not* in either published recipe: repeated node wedges (on unified memory, exhaustion does not error — it takes the whole box), a deadlock that only appears behind a default-deny firewall, the first speculative-decode acceptance measurements we're aware of for this model, and three optimisation avenues that turned out to be **dead ends** — documented as such, with numbers. Everything here was measured on real hardware. Where a number is contested or unproven, it says so. --- ## Status | | | |---|---| | Serving | ✅ TP=2 across 2 nodes, 262,144 context | | Vision | ✅ verified end-to-end (see below) | | Spec decode | ✅ NEXTN 3/1/4 (`num_steps`/`eagle_topk`/`num_draft_tokens`) — note the engine self-reports `speculative_algorithm: EAGLE` — **the architectural maximum, not a default** | | Decode | **~63 tok/s** single-stream on real generation | | Concurrency | **306 tok/s** aggregate at 6 streams | | Prefill | 3,050 tok/s (cache defeated) | | Thermals | 52 °C / 35 W peak under load | Conditions, because this repo insists on them: **decode ~63 tok/s** = real generation of a 10.7k-token HTML file, thinking off, temp 0.3, 2398 MHz idle / 2522 under load. **306 tok/s** = aggregate across 6 concurrent streams, code prompt, 400 `max_tokens`, `ignore_eos`. **Prefill 3,050 tok/s** = ~7,450-token unique prompt per run, `cached_tokens=0` asserted, n=6. **Thermals** = concurrency 4, 1 Hz sampling. A number without its prompt, token count and clock state is not comparable to anything — including these. --- ## Performance Two DGX Sparks. 180B params (125B backbone + 51B PLE), NVFP4, 262k context, vision on. | | tok/s | conditions | |---|---|---| | **Single stream, real work** | **~63** | 10.7k-token HTML page, natural stop, 2522 MHz | | Single stream, 400 tok | 63.7 | code prompt, `ignore_eos` | | 2 concurrent | **104.9** agg | 52.8/stream² | | 4 concurrent | **178.7** agg | 45.2/stream² | | **6 concurrent** | **306.6** agg | 51.7/stream² | | Prefill | **3,050** | ~7,450-token unique prompt, `cached_tokens=0` asserted, n=6 | | Stress floor | 47.6 | `ignore_eos` + hard prompt + 800 tok — a deliberate FLOOR, see below | ² Concurrency measured **before** the config was pinned — at `max_running_requests=12` and an unpinned KV pool of 850,816 tokens, not the 8 / 600,000 in the recipe below. Under the pinned config 8 is the cap, so 6 streams is near it rather than "still climbing". Re-measure before quoting these against the shipped config. **Power and heat, at concurrency 4:** **52 °C, 35.5 W** peak per node; 42 °C / 10.4 W idle. That is roughly **half the draw** of a comparably-sized dense-ish MoE we previously ran on the same boxes (88 °C / 65 W), at *higher* clocks. Cause: ~6B active params per token (10 of 512 experts) and only 12 of 48 layers are full attention — the rest are linear-attention GDN, so decode waits on memory rather than burning watts. Practical effect: thermal guard stages sized for the older model are unreachable by 43 °C, and two Sparks serve this at **~71 W combined under load**. **Why two numbers for "single stream".** `ignore_eos` benchmarks force generation past the model's natural stopping point into degenerate text. They're excellent for regression detection and useless as a headline. Real generation of a complete HTML page runs at **~63 tok/s**; the same stack under `ignore_eos` on a hard prompt reports **47.6**. Both are correct. Quote the one that matches what you're doing, and say which. **Speculative decoding is doing much of the work, but the delta is not isolated.** Our earlier vLLM deployment of the same checkpoint (no MTP, `--enforce-eager`) decoded at 20–21 tok/s; this SGLang stack with MTP runs ~3× that. **Engine, CUDA-graph mode and MTP all changed together** — we have no SGLang-with-MTP-off measurement, and neither published recipe ships one to compare against. See §3 for why 3/1/4 is the ceiling. --- ## Recipe **1. Base stack.** Clone [MiaAI-Lab's repo](https://github.com/MiaAI-Lab/Qwen3.8-Flash-Next-Dual-DGX-Sparks) and follow it — it builds the SM121 QSA patch onto the public SGLang image and handles fabric preflight, worker-first ordering and readiness waiting. Everything below is a delta on that. **2. Stage NCCL on both nodes.** Both published recipes treat host-staged NCCL as required for GB10 multi-node stability: ```bash mkdir -p ~/nccl-2.30.7 cp /usr/lib/aarch64-linux-gnu/libnccl.so.2.30.7 ~/nccl-2.30.7/ ln -sf libnccl.so.2.30.7 ~/nccl-2.30.7/libnccl.so.2 ``` **3. Apply the config delta** (table below) to the `.env`. **4. Pin the control plane to the fabric** — the single most important line if you run a default-deny firewall, and the one that cost us the longest debug: ```bash -e SGLANG_HOST_IP= ``` **5. Evict page cache immediately before launch, and keep it bounded during the load:** ```bash # before launch (no root needed — this is what scripts/cache-warden.py automates) python3 -c " import os,glob for p in glob.glob(os.path.expanduser('~/.cache/huggingface/hub/**/*.safetensors'),recursive=True): rp=os.path.realpath(p) if os.path.exists(rp): fd=os.open(rp,os.O_RDONLY); os.posix_fadvise(fd,0,0,os.POSIX_FADV_DONTNEED); os.close(fd)" # during the load, on BOTH nodes python3 scripts/cache-warden.py --model-dir ~/.cache/huggingface/hub \ --interval 20 --stop-below-gb 25 --max-runtime 86400 --log ~/warden.jsonl ``` **6. Verify it took — at the point of effect, not in your config file:** ```bash docker exec env | grep -E 'SGLANG_HOST_IP|NCCL_IB_HCA|NCHANNELS' curl -s localhost:8899/get_server_info | python3 -m json.tool | grep -E 'max_total|speculative|context' ``` A setting you did not confirm arrived is a setting you did not set. Ours silently disagreed with the `.env` more than once. --- ## Stack (pin these when reproducing) | | | |---|---| | Model | `RadixArk/Qwen3.8-Flash-Next-NVFP4` (206 shards, 135.2 GB) | | Engine | SGLang, image `lmsysorg/sglang:qwen38flashnext` + MiaAI-Lab's SM121 QSA patch | | Driver | NVIDIA 580.173.02 (open kernel module, aarch64) | | NCCL | 2.30.7, host-staged and `LD_PRELOAD`ed (both recipes treat this as required for GB10 multi-node) | | Hardware | 2× DGX Spark, GB10 / sm_121, 121 GB unified per node | ### Config delta vs. the MiaAI-Lab defaults Everything else is hers. These are the only changes, and why: | setting | value | why | |---|---|---| | `SGLANG_HOST_IP` | fabric IP, per node | the deadlock in §1 | | `NCCL_MIN/MAX_NCHANNELS` | 4 | default negotiated **64 channels and hung** during init; tonyd2wild pins 4 | | `CUDA_GRAPH_BS` | dense `1..8` | a ladder with **gaps** forces padding, and padded rows carry `decode_len=0`, which wedges the sparse indexer under spec decode. Dense inside range means nothing pads. tonyd2wild solves the same problem with `--disable-cuda-graph-padding`; we do both | | `MAX_RUNNING_REQUESTS` | 8 | matches the graph ladder — 9–16 ran eager with transient workspace | | `--max-total-tokens` | 600000 | pinned; unpinned "OOMs under sustained load" (tonyd2wild) | | `MEM_FRACTION_STATIC` | 0.80 | leaves ~23 GB headroom | | `NCCL_CROSS_NIC` | 0 | a multi-Spark report had `CROSS_NIC=1` **wedge within hours** under real traffic | --- ## The five things this repo adds ### 1. `SGLANG_HOST_IP` — multi-node SGLang deadlocks behind a default-deny firewall **Symptom:** both ranks hang forever after `CustomAllreduce is disabled`. No error, no timeout, no NCCL warning. NCCL itself completes fine — RoCE connects, channels build. **Diagnosis** (`py-spy dump` on both ranks — this is what made it solvable): ``` rank 0 (head): wait_until_ready (shm_broadcast.py:333) <- writer awaiting subscriptions rank 1 (worker): wait_until_ready (shm_broadcast.py:344) <- reader awaiting READY ``` A ZMQ PUB/SUB deadlock, not an NCCL problem. Root cause: SGLang's `get_local_ip_auto()` returns the **default-route interface** — the management LAN address — and binds the cross-rank XPUB control socket there. With `-P INPUT DROP` on that interface, the worker's subscription never arrives. **Fix** — pin the control plane to the fabric, per node: ```bash docker run ... -e SGLANG_HOST_IP=10.10.10.1 # head docker run ... -e SGLANG_HOST_IP=10.10.10.2 # worker ``` The bug requires two conditions together: the fabric is not the default route, and the default-route interface drops unsolicited inbound. Absent either, it never appears — which is presumably why the published recipes don't mention it. vLLM's equivalent is `VLLM_HOST_IP`, which is what confirmed the fix was legitimate rather than a workaround. ### 2. Page cache is half your memory budget — and it is the wedge On GB10 the GPU and host share **one** physical pool. `safetensors` mmaps each shard, so file pages and resident tensors compete for the same memory. Measured on an idle node: ``` reading 41.6 GB of shards -> MemFree 103.0 -> 64.1 GB (~1:1) ``` A full load reads far more than that. When it runs out, **the NVIDIA driver fails before the kernel reclaims** — from our own kernel log on a hung boot: ``` 18:48:40 NVRM: Out of memory [NV_ERR_NO_MEMORY] .. _memdescAllocInternal @ mem_desc.c:1359 18:48:54 systemd-journald: Under memory pressure, flushing caches. ``` NVRM failed **14 seconds before** the kernel registered any pressure of its own, and that boot contains no `page allocation failure`, no `order:` line, and no OOM-killer invocation. The cache was resident; the driver could not have it. On unified memory this does not raise — **it wedges the whole box**, and recovery is a physical power-cycle. Note: when it wedges this hard the **power button is dead too** — no lights, no fans, no response to a long hold. Unplug and replug is the only recovery we found. **Consequences, all measured:** - **Gate on `MemFree`, never `MemAvailable`.** MemAvailable counts reclaimable page cache the driver cannot use. Live example from a node in this state: `MemFree 1.9 GB` vs `MemAvailable 17.2 GB`. - **`drop_caches` before launch is necessary but not sufficient** — the cache regrows during the 135 GB read. See [`scripts/cache-warden.py`](scripts/cache-warden.py), which bounds it *during* and *after* load, needs **no root**, and requires no engine patch. - **The same pressure silently costs throughput**, not just stability: | MemFree | median tok/s | CV | min | |---|---|---|---| | 1.7 GB | 58.77 | 9.7% | 44.96 | | 21 GB | 60.69 | **2.0%** | 58.74 | One root cause, two symptoms. Every benchmark in this repo evicts cache first. The warden's own A/B, identical 41.6 GB read: | | MemFree before → after | |---|---| | control (no warden) | 103.0 → 63.9 GB (**−39.1**) | | with warden | 102.8 → 102.6 GB (**−0.2**) | **Safe against a running engine.** `posix_fadvise(DONTNEED)` drops only *clean, unmapped* pages: pages the live engine has mmap'd are skipped by the kernel, dirty pages are never discarded, and the shards are read-only anyway. Worst case is a re-read from NVMe — latency, never corruption. It carries a hard self-limit, exits when its target process dies, and reports a loud `FATAL` on a bad directory and `UNVERIFIED` when it cannot evict. All three exit paths were proven by execution, not by reading the code. - **`--max-total-tokens` is a ceiling, not a floor.** Requesting 600,000 with a warm cache silently yielded **557,120** — the engine under-fills the pool and does not warn. ### 3. Speculative decoding is at its architectural ceiling — and the drafter saturates it `3/1/4` is not a conservative default. Raising it is **refused by the engine**: ``` NotImplementedError: Qwen QSA requires speculative_num_draft_tokens <= the QSA compress ratio (4): the pending index-key ring holds one group; got 5 ``` Qwen Sparse Attention's pending index-key ring holds exactly one group of 4. `num_draft_tokens` can never exceed 4 **regardless of acceptance rate**. **Per-run acceptance measurements — the first we're aware of for this model** — n=16, hard code prompt, per-run: ``` spec_accept_length 3.300 – 3.850 median 3.500 (HARD CEILING 4.0) tok/s 52.55 – 62.56 correlation r = +0.786 ``` Two findings: **The throughput variance people see is acceptance, not noise.** It is not thermal, not clocks, not page cache — all were flat/controlled. It is inherent and cannot be removed by environmental control. **The drafter reaches 3.85 against a hard maximum of 4.0.** Net of the bonus token that is ~95% of draft slots accepted on the best runs. The model would benefit from a larger draft budget and QSA makes that impossible. **Cumulative figure — and the trap we fell into publishing it.** Over the eval campaign: **accept length 2.99, accept rate 0.66**, derived from lifetime counters only: ``` generation_tokens_total 601,124 + 49,010 = 650,134 (BOTH streaming series, summed by name) spec_verify_calls_total 217,242 650,134 / 217,242 = 2.99 ``` ⚠️ **The windowed-gauge trap — we published this mistake before catching it.** SGLang's `/metrics` gauges `sglang:spec_accept_length` and `spec_accept_rate` are **recomputed and reset every decode-log interval**. They describe the last few dozen forwards, not the lifetime. `spec_verify_calls_total` *is* lifetime. Pairing them silently labels a window as a campaign. Watch a single gauge over minutes on an idle-ish engine: ``` spec_accept_length: 1.45 → 3.425 → 2.05 → 3.325 (window) spec_verify_calls_total: 211,743 → 217,091 → 217,242 (lifetime, monotonic) ``` A cumulative average over *more* calls cannot fall from 3.425 to 2.05. **We published "3.425 over 211,743 verify calls" — a gauge read pinned to a counter — and it flattered us by ~15%.** The per-run figures above survive, because a per-run gauge read approximates that run's own window. Any dashboard reading of these gauges is a window too. **External anchor.** LMSYS reports this model on B200 TP4 NVFP4 at **accept length 3.3** (their workload is unstated). Our numbers **bracket** it — 2.99 cumulative on a mixed workload, 3.50 median on a hard code prompt. Given the ±40 pp prompt sensitivity below, "the same range" is the most anyone can honestly claim from a cross-workload acceptance comparison. LMSYS also names the mechanism behind the ceiling: **IndexShare MTP** reuses QSA selections across draft steps, which is precisely why the pending index-key ring holds a single group. ⚠️ **Acceptance swings ~40 pp on prompt alone.** Measured on the same engine within one hour: accept **length** `3.5–3.7` on one code prompt vs `2.475` on a chat+code mix — i.e. accept **rate** ~0.84–0.90 vs ~0.49, a ~40 **percentage-point** swing (length and rate are different units; rate = (length − 1) / draft_steps). All arithmetically self-consistent — they measure different prompt mixes. **Never quote an acceptance number without naming the prompt set.** **Is there a way past the ceiling?** Not today. As of 2026-08-27 no DFlash / DSpark / EAGLE3 drafter exists for Flash-Next — z-lab's DFlash repo lists Muse-Glimmer-30B and Qwen3.8-**27B** (a different model) and does not mention Flash-Next in supported models, roadmap or TODO. Two things look like hits and are not: a HuggingFace repo named `…-MTP-Drafter-GGUF` is a repackaging of the built-in MTP ("extracted … unmodified", 33 tensors), and SGLang's cookbook lists `--speculative-algorithm DFLASH` because that's the engine-wide picker on every page — it needs a `--speculative-draft-model-path` checkpoint that doesn't exist for this target. **The flag being selectable is a label; the weights are the evidence.** ### 4. Vision works, and the self-review loop closes Verified with a generated image of known content, not taken from the model card: ``` 224×224 PNG, quadrants TL red / TR blue / BL green / BR yellow answer: all four correct, image_tokens=64, 1.3 s ``` More useful — the full loop: ``` model writes HTML → headless Chrome renders at 1280px and 380px → model reads its own screenshots → critiques its own output ``` On a run truncated by too small a `max_tokens`, it reported *"the rendering is a complete failure… just a dark background with a subtle grid pattern"* — describing what was on screen, not what it had intended to write. On a complete run it found a font-size inconsistency and a checkmark-colour mismatch that required zooming in to confirm. **It contradicts its own prior output**, which is the property that makes self-review worth anything. ⚠️ **`max_tokens` ≥ 8000 for a full page.** At 2,600 the file truncated mid-CSS and produced a *valid-looking* file that rendered blank. No error. Only the screenshot caught it. ### 5. Thinking mode: binary, helps reasoning, and fails catastrophically 30% of the time There are **no effort levels**. The engine reports `ReasoningToggleConfig(toggle_param='enable_thinking', default_enabled=True, effort_kwarg=None)`. A/B on 8 reasoning problems with verifiable answers: | | thinking OFF | thinking ON | |---|---|---| | score | 6/8 | **8/8** | | time | 2.9 s | 14.8 s (5×) | | tokens | 78 | 733, of which 651 thinking (9×) | It fixes exactly the intuition traps: bat-and-ball `$0.10 → $0.05`, "Sally's sisters" `3 → 2`. ⚠️ **But do not default it on for code generation.** Same task, same config, temperature 0, `max_tokens=14000`, n=10 each: | | runaways (empty answer, budget exhausted) | completion tokens | |---|---|---| | thinking **ON** | **3 / 10** | 1,342 – 14,000 | | thinking **OFF** | **0 / 10** | 222 – 287 | **30% of thinking-on requests consumed the entire 14,000-token budget and returned zero characters of content**, with everything in `reasoning_content` and `finish_reason: length`. Thinking off solved the identical task in 222–287 tokens every single time — roughly **50× cheaper and completely stable**. Note the token range under thinking: 1,342 to 14,000, a **10× spread at temperature 0**. Greedy decoding is not bit-reproducible on this stack (NVFP4 GEMM variance on sm_121 is the usual explanation), and thinking amplifies that divergence into a coin-flip between "fine" and "produces nothing at all". **Practical guidance:** - **Reasoning problems, short outputs** → thinking ON is a real win (6/8 → 8/8 on classic intuition traps: bat-and-ball `$0.10 → $0.05`, "Sally's sisters" `3 → 2`). - **Code generation, long outputs** → thinking OFF. It is faster, ~50× cheaper in tokens, and does not silently return nothing. - **If you must run thinking on unattended**, you need a guard: treat `finish_reason == "length"` or empty `content` as a retryable failure, not as a model answer. A harness without that guard will book 30% of its thinking-arm results as task failures and conclude "thinking hurts on code" — which is not what is happening. ⚠️ **Wherever thinking is on, `max_tokens` must be ≥ 2000** regardless. Thinking consumes the *same* budget as the answer, so a small cap guarantees the empty-content outcome rather than merely risking it. Tool calling was **not** harmed by thinking in our testing (correct `tool_calls` at temp 0.0, 0.7 and 1.0). One recipe reports a token-0 `!!!!!` repetition loop for thinking+tools; we probed n=6 at temp 1.0 and saw none, on the *riskier* configuration (flashinfer sampling, radix cache on). **n=6 cannot prove absence of a rare probabilistic loop.** Keep it on the watch list. That watch-list item now has a confirmed sibling, below — and note *why* the n=6 probe found nothing: it ran at temperature 1.0, which is precisely the setting that does not loop. --- ## Sampling: a 1-in-5 repetition loop at temperature 0 — cause NOT established The engine ships `sampling_defaults='model'`, so a request that sends **no** sampling parameters gets the checkpoint's own `generation_config`: ``` temperature 1.0 top_k 20 top_p 0.95 ``` Passing `temperature: 0` overrides that. **On this stack, long greedy builds loop INTERMITTENTLY — measured at 1 of 5 runs.** **What was measured (2026-08-27).** Identical prompt — rebuild a home page from a structured brief — thinking off, `max_tokens 14000`, **one run per arm**: | sampling | tokens | finish | outcome | compliance audit | |---|---|---|---|---| | `temperature: 0` | 14,000 | `length` | **one CSS line emitted 507 times, never escaped** | 12/28 | | *(none sent — checkpoint default)* | 9,550 | `stop` | clean | 27/29 | | `temp 0.7 / top_p 0.8 / top_k 20` | 11,831 | `stop` | clean | 28/29 | The greedy run never reached the end of the document, so the page had no `
`, no footer and no links — which is why the compliance score collapses. At 800 tokens **neither** config repeats a line, so whatever this is, it is length-dependent. **It is rare, and our first write-up of it was wrong.** The identical greedy build was re-run four more times on the same prompt: ``` run 1 10,785 tok finish=stop max repeated content line 3 clean run 2 11,006 tok finish=stop max repeated content line 3 clean run 3 12,252 tok finish=stop max repeated content line 4 clean run 4 14,000 tok finish=length max repeated content line 1 clean (long, not looping) ``` **0 of 4.** Pooled with the original, the observed rate is **1 in 5** — not something greedy does, something greedy sometimes does. Greedy decoding is **not bit-reproducible on this stack** (NVFP4 GEMM variance on sm_121 — the same effect behind the 10× token spread documented in the thinking section above), so `temperature: 0` names a *distribution*, not one trajectory. A small slice of that distribution lands in a basin greedy cannot leave. A sampled decoder can land in the same basin and still escape by chance — that asymmetry, not the loop itself, is the finding. The two sampled arms are **one run each**. n=1 bounds nothing; treat their rate as unmeasured, merely lower. ### Why we are NOT claiming "temperature 0 causes this" ⚠️ **There is an uncontrolled confound, and it is a big one.** We run `--sampling-backend flashinfer`. [tonyd2wild's recipe](https://github.com/tonyd2wild/qwen3.8-flash-next-nvfp4-dgx-spark) for the same model and hardware documents a degenerate-output loop and attributes it to that exact kernel — his fix is `--sampling-backend pytorch`, described as ruling out "the FlashInfer kernel arg-maxing a stale row to token 0." With his four-part stack he reports the loop **clean at temp 0.0 / 0.2 / 0.7**, with a residual edge only at temp 1.0. We ship **two of his four** loop-fix elements (`enable_thinking: false`, `--disable-cuda-graph-padding`) and not the other two (`--sampling-backend pytorch`, `--disable-radix-cache`). So the honest statement is: > A long greedy generation looped **on a stack missing the sampling-backend fix that a published > recipe says prevents exactly this class of failure.** Temperature is correlated with the failure > in our three runs; it is not established as the cause. Our manifestation also differs from his — a whole CSS line repeated 507 times, not a token-0 `!` loop — so they may be different bugs. **Unresolved.** Testing it properly means restarting the engine with `--sampling-backend pytorch` and re-running all three arms, which we have not done. (Do not reach for `--disable-radix-cache` casually as the other half of his stack: his own 2026-08-27 update reports it *silently collapses the mamba/SSM state pool* to `max_running_requests`. Our workload is also prefill-dominated, which is precisely where a prefix cache pays.) ### And no, we cannot tell you greedy is faster An earlier version of this section claimed temperature 0 was **+6.9%** faster (48.2 vs 45.1 tok/s), citing higher speculative acceptance (58.3% vs 50.8%) as the mechanism. **That claim is withdrawn.** It came from **n=3 per arm**, against a measured inherent CV of **~6.3%** on this cluster — the "difference" was the same size as the noise, and the defaults arm contained a 42.3 outlier of exactly the shape this log has previously root-caused to page-cache pressure. This repo's own standard, set after an earlier bad call, is that **n=5 is not enough** to report a config win. n=3 is not close. The acceptance figures (2.75 vs 2.525 accept-length) are real per-run gauge reads and the direction is mechanically plausible — greedy tokens are more predictable, so the drafter hits more often. Plausible is not measured. If you want this number, it needs n≥16 with page cache evicted and a named prompt. **Practical guidance, as far as it is actually supported:** - **Long generation on a flashinfer-sampling stack** → send no sampling parameters, or cap temperature at ≤0.7 per tonyd2wild. Both completed cleanly here — *one run each*, so this is a completion, not a rate. The reason to prefer them is the escape asymmetry above, not a measured difference in loop frequency. - **A 1-in-5 chance of losing the whole document is worth engineering around** even though it is rare. If you run greedy on long output, treat `finish_reason == "length"` plus a high repeated-line count as a retryable failure. - **On the server-side default there is a real trade, and we have not resolved it.** We leave `sampling_defaults='model'`, which serves temp 1.0 to any client that sends nothing — and temp 1.0 is precisely where tonyd2wild reports his residual edge, with an explicit recommendation to cap agent temperature at ≤0.7. Keeping `model` preserves the diagnostic signal and honours the checkpoint's own config; it also defaults silent clients into the one regime the cited source calls risky. Pick deliberately rather than inheriting it, as we did. - **Benchmarks** → always say which sampling config produced the number. The direction is plausible; the size, and whether it exists at all, is unmeasured. --- ## Capability evaluation 13 tasks across backend Python, backend Node/TS, SQL schema design, debugging, three frontend stacks (vanilla, React, Next.js App Router), Sanity CMS schemas, and multi-file cross-file debugging. Two passes per arm, temp 0. **8 of the 13 are graded by executing held-out tests in a sandbox** (backend Python ×2, Node ×2, SQL, debugging ×2, and the cross-file task). The three frontend tasks, the Sanity schema and one large-codebase task are graded by **structural checks on the output text** — weaker, and the negative controls validate only the executing verifiers. | arm | scored PASS | INVALID | wall clock¹ | |---|---|---|---| | thinking **OFF**, pass 1 | **13 / 13** | 0 | **3 m 44 s** | | thinking **OFF**, pass 2 | **13 / 13** | 0 | **3 m 45 s** | | thinking **ON**, pass 1 | 12 / 12 | 1 | **81 min** | | thinking **ON**, pass 2 | 11 / 11 | 2 | **84 min** | ¹ Wall clock between arm-start markers in the run log — this is what you actually wait for. It is much larger than the sum of per-task `elapsed`, because retried attempts are not counted in the per-record figure and one frontend task alone burned ~3 × 400 s per thinking-ON arm. **Thinking off is ~22× faster in wall clock, with equal correctness.** Both INVALIDs in pass 2 were thinking-budget exhaustion at 20,000 tokens on long-output tasks. **A fourth independent measurement of the runaway, from the campaign itself:** retries fired in **10 of 30 thinking-ON cells and 0 of 32 thinking-OFF cells**. The thinking-ON scoreline is therefore *retry-dependent* — retries only fire on INVALID (truncation or empty output), never on FAIL, so they cannot turn a wrong answer into a pass, but they do resample a nondeterministic coin-flip. Without the retry policy the thinking arm would show ~30% failures that are not capability failures. ⚠️ **A clean sweep measures the suite, not the model.** 13/13 bounds the failure rate; it does not locate the ceiling. The 95% Wilson interval on 13/13 is roughly **77–100%** — wide, because n is small. The honest reading is "this suite sits below the model's capability", not "this model does not fail". These tasks were written by us and are not a public benchmark. **Disclosure:** first-pass results under two buggy verifiers were 12/13. `fe-01` (both OFF passes) and `dbg-02` (ON pass 1) were **re-run after the verifier fixes described below** — fresh generations, not re-grades. The headline includes those re-run cells. The hardest task — a four-file service with a cross-file contract bug (a heap negating priority while the constants documented the opposite convention) — passed in **18 s** with thinking on (under 4 s with it off), changing only the file that needed changing, fixing the misleading comment that caused it, and satisfying a held-out three-part test covering ordering, FIFO tie-break, and untouched retry semantics. ### Verifier validation Every run includes negative controls whose tests are **deliberately unsatisfiable**: a Python task asserting `2+2==5`, and a SQL task whose table is pre-created so the model's own DDL must collide. Both failed correctly in all four arms, and real tasks pass — so the verifiers genuinely execute and are not merely always-fail. ### ⚠️ Three of our own checks failed *correct* output This is the part worth copying if you build something similar. In the first pass, three verifiers produced confident, specific, **false** results: | check | what it did | reality | |---|---|---| | mutable-default fix | asserted the caller's list must not be mutated | the prompt never asked for a defensive copy; taking ownership is a normal contract | | self-contained HTML | banned the substring `http://` | flagged `xmlns="http://www.w3.org/2000/svg"` — a namespace URI browsers never fetch | | token budget | 6,000 max_tokens | thinking consumed it, so truncation looked like failure | Uncorrected, the writeup would have claimed this model fails the classic mutable-default bug and cannot produce self-contained HTML. Both are the opposite of true. **A verifier is a claim about the world and needs its own negative controls** — ours caught the model's failures fine; what they could not catch was themselves. The tell each time was a *surprising* failure that turned out, on reading the actual output, to be correct. --- ## Dead ends — documented so you don't spend the time **Clock headroom does not exist.** `clocks.max.sm` reports 3003 MHz; the GPU runs 2528 under load. Locking `-lgc 2800,3003` yields **2528 MHz** — the floor does not take — and prefill changes by **0.07%**: | | clock | prefill | |---|---|---| | default | 2528 MHz | 3,055 tok/s | | locked 2800–3003 | 2528 MHz | 3,053 tok/s | GB10 is **memory-bandwidth bound**, not clock bound, for both prefill and decode. This also explains the low power draw — the GPU is mostly waiting on memory. **Raising the draft budget is impossible.** See §3. **`--load-format dummy` should not be used on GB10** — the rule and the >150 GB transient figure are Mia's; our contribution is only that it explains one of our own wedges. A "safe rehearsal" is more dangerous than the real load. --- ## Thermals — much cooler than a comparable dense-ish MoE Measured at concurrency 4, 1 Hz telemetry: ``` idle 42.0 °C · 10.4 W · 2398 MHz peak (load) 52.0 °C · 35.5 W · 2522 MHz ``` For scale, a previous model on identical hardware peaked at **88 °C / 65 W** uncapped. Qwen runs ~36 °C cooler at ~45% the power — at *higher* clocks. Cause: ~6B active params/token (10 of 512 experts) and only 12 of 48 layers are full attention; the rest are linear-attention GDN. **Practical effect:** thermal guard stages sized for the older model are effectively unreachable (43 °C of margin), and a clock cap intended to control thermals has nothing left to control. --- ## Benchmark discipline Two spectacular false results were produced and caught during this work. Both were **prefix-cache artifacts**: ``` "72,000 tok/s prefill" -> identical prompt repeated, radix cache hit "46,388 tok/s prefill" -> a shell function that never passed its seed argument true prefill: 3,050 tok/s (unique prompt per run, cached_tokens=0 asserted) ``` **Always assert `usage.prompt_tokens_details.cached_tokens == 0`** when measuring prefill. A 24× speedup that appears without a config change is a cache hit, not a discovery. Likewise, **`ignore_eos` benchmarks are a floor, not real-world throughput.** They force generation past the natural stopping point into degenerate text: | measurement | tok/s | |---|---| | real generation (10.7k tokens of HTML) | **62.9** | | harness, `ignore_eos`, hard prompt, 800 tok | 47.6 | Both correct; they measure different things. Name the prompt, token count and clock state on every number, or it is not comparable to anything. --- ## Files | | | |---|---| | [`scripts/cache-warden.py`](scripts/cache-warden.py) | bounds page cache during and after load; no root, no engine patch | | [`bench/decode-bench.py`](bench/decode-bench.py) | decode benchmark that waits for idle, discards contended runs, reports medians, and names its conditions | ## Credit This work stands on two recipes published first, and would not exist without them: - **[MiaAI-Lab/Qwen3.8-Flash-Next-Dual-DGX-Sparks](https://github.com/MiaAI-Lab/Qwen3.8-Flash-Next-Dual-DGX-Sparks)** — the orchestration and the SM121 QSA Triton fallback kernel that makes this model run on sm_121 at all. Our deployment *is* this stack. - **[tonyd2wild/qwen3.8-flash-next-nvfp4-dgx-spark](https://github.com/tonyd2wild/qwen3.8-flash-next-nvfp4-dgx-spark)** — `--disable-cuda-graph-padding`, NCCL channel pinning (which fixed a 64-channel init hang for us), KV pinning, and the rule that any fix making the model text-only is off the table. - **[bird/GLM-spark](https://github.com/bird/GLM-spark)** — published the `posix_fadvise(DONTNEED)` page-cache mechanism first, as an in-loader vLLM patch. We arrived at it independently and measured it before finding theirs; `cache-warden.py` is an out-of-tree, no-root variant that also bounds cache *during* and *after* load. ## License MIT.