← back to the article · Andrei Vogulkin

Home lab · spark (NVIDIA DGX Spark GB10, 121.7 GB unified) · 2026-09-03/04 · report by Claude, reviewed run log

Serving Qwen3.8-Flash-Next on Atlas: what it took, what it measured, what broke

Atlas main at 4df6e609 (the commit that merged our PR #17) serving RadixArk/Qwen3.8-Flash-Next-NVFP4, put through the same two-round tower-defense build benchmark as prod and the llama.cpp Flash-Next configs. Eleven serve attempts, one benchmark result, five engine defects, and a fix loop that was stopped on request.

16–17
tok/s decode, single stream, every round (prod: 48)
11
serve attempts to get one complete run
5
distinct engine defects, each reproduced with a log
2 + 2
rounds + fix rounds before the game loaded clean
~4.5 h
GPU wall time, prod stopped throughout

1 · Bottom line

Performance. Atlas decodes this model at 16–17 tok/s regardless of context length, with prefill around 430 tok/s (a 13.4K-token prompt took 31 s). That is a third of prod's speed on the same task and below both llama.cpp Flash-Next configs measured on 2026-08-28 (20–25 tok/s). The recipe's own claim (16.5 tok/s, 747 tok/s prefill) is consistent with what we saw, so this is the engine's current ceiling, not a misconfiguration.

Output quality. The model produced a full-featured game design ("IRONGATE", 47.6 KB) in round 1 but shipped it dead: four of six <script> blocks were unterminated. Round 2 thrashed, rewriting the file three times inside one response until it hit the 26K-token cap. Two targeted fix rounds later the game loads with zero console errors, towers place and a wave starts; whether it plays through to wave 20 was the check in progress when the run was stopped.

Engine. The dominant cost was not the model but Atlas: every long-form request path (rewinds, speculative verify, prompts over 8K tokens, generations past 32K positions) hit a separate failure. Section 5 lists them with the verbatim error lines and the workaround that got past each.

2 · Setup and parameters

What was compared

ConfigModelEngineServing
ATLAS (this run)RadixArk/Qwen3.8-Flash-Next-NVFP4, 125B MoE / 6B active + 51 GB FP8 n-gram table, 126 GB on diskAtlas main 4df6e609, release build, kernel target qwen3.8-flash-nextlocal :8001, prod stopped, single stream
P (prod, reference)Huihui-Qwen3.8-27B-abliterated, Q4_K_M GGUF, 17 GBllama.cpp DFlash build (PR #27342), DFlash2 speculative decoding, Sharp template:8000, live
E2 / A (2026-08-28)Qwen3.8-Flash-Next UD-Q3_K_XL / UD-Q4_K_XL (Unsloth GGUF)llama.cpp PR #27742, n-gram table on CPU via mmapprior TD shootout rows, not re-run

Benchmark protocol (unchanged from the 2026-08-28 shootout)

The serve line that finally worked

ATLAS_QSA_MAX_TOKENS=65536 ATLAS_PLE_CACHE_SLOTS=524288 ATLAS_PLE_MAX_TOKENS=20500 \
spark serve --model-from-path ~/models/radixark-fn-nvfp4 --model-name qwen \
  --kernel-target qwen3.8-flash-next --bind 127.0.0.1 --port 8001 \
  --max-seq-len 49152 --max-num-seqs 1 --max-batch-size 1 \
  --gpu-memory-utilization 0.95 --fast-load-prefetch-shards \
  --max-prefill-tokens 20480 --content-loop-watchdog false --request-timeout 3600

Every value above that is not a default exists because a default failed (section 3). No --speculative: the recipe's documented C=1 win (--num-drafts 1, +18%) breaks once the sparse-attention indexer activates at position 2051.

Memory at loadGBNote
Pre-KV footprint (weights, PLE scratch, arena, MTP head, vision)99.7of a 115.6 GB budget at 0.95 utilization
Buffer arena for a 20,481-token prefill chunk7.91.9 GB at 4,096-token chunks; this is the price of single-chunk prefill
PLE scratch delta, 32,768 vs 8,192 tokens+3.5why attempt 1 had no room left for KV at 0.90
KV cache (bf16, required by QSA)11.732,048 blocks × 16 tokens; far more than 49K context needs
System memory available while serving0–5touched 0 GB during a 17K-token round; survived, but there is no margin

3 · Step by step: the eleven attempts

Each row is one server launch. Times are spark local. Every attempt after the first changes exactly the parameter the previous failure named.

#TimeChange from previousWhat happenedVerdict
110:49PLE scratch 32,768 · util 0.90 · speculative (1 draft) · 64K ctxModel loaded, then refused: 103.8 GB pre-KV + 6.5 GB reserve exceeded the 109.5 GB budget. No KV cache possible.no start
210:53PLE 8,192 · util 0.95Round 1 died at 2,492 content tokens: the loop watchdog saw a period-2 repeat, rolled back 68 tokens, and the QSA indexer reported "pos 2864 but 2932 ingested". HTTP 500.defect 1
310:58--content-loop-watchdog falseRound 1 cut at exactly 300 s with finish_reason=timeout (5,641 tokens). Server-side default deadline.default
411:08--request-timeout 3600Round 1 ended after 1,679 tokens at position 2,051 with a clean stop; the log shows the MTP verify path erroring on QSA selection. Round 2 got a truncated file and produced 80 tokens.defect 2
511:13drop --speculative (recipe default)Round 1 complete: 18,027 tokens, 17.3 tok/s, 17.3 min, 47.6 KB. Round 2's 18,234-token prompt was refused: PLE cap 8,192 < chunk 8,193.R1 defect 3a
611:33PLE 8,200; round 2 only, reusing the round-1 fileSame prompt refused again: "NgramRowCache: every one of 65536 slots is pinned by the batch" (8,193 tokens × 16 heads).defect 3b
711:38ATLAS_PLE_CACHE_SLOTS=262144First 8,193-token chunk prefilled; the continuation chunk faulted at layer 0 with CUDA_ERROR_MISALIGNED_ADDRESS. Engine exited.defect 4
811:44--max-prefill-tokens 4095 (4,096-wide chunks)Prefill succeeded through all chunks. Decode ran 14.5K tokens, then "QSA: pos 32768 >= ATLAS_QSA_MAX_TOKENS". HTTP 500, output discarded.defect 5
912:02ATLAS_QSA_MAX_TOKENS=65536Round 2 complete: 24,174 tokens, 16.1 tok/s, 25 min, 62.8 KB. Fence unterminated, so the harness did not persist it (harness gap, fixed for the next run).R2 lost
1012:32harness persists raw outputRound 2 re-run: 26,624 tokens (hit the cap), 16.3 tok/s, 27 min. The response held three full rewrites; the longest closed one (35.2 KB) has a syntactically incomplete main script.broken file
1113:04–14:20persistent server; fix rounds fed the real console error / playtest bugsFix-1 prompt (13.4K tokens) faulted twice on continuation chunks even at 4,096 width. Moved to single-chunk prefill (20,480-token chunks, 48K ctx). Fix 1: 45.5 KB, loads clean. Fix 2: 47.8 KB, loads clean, towers place, wave starts. Stopped by request during the playtest.loads unverified

4 · Benchmark results

ConfigRoundTokensWalltok/sFinishCodeOutcome
ATLAS
Flash-Next NVFP4 on Atlas main
R1 build18,02717 m 19 s17.3stop47,611 chunterminated; 4 of 6 script blocks unclosed, dead on load
R2 fix/polish (att. 9)24,17424 m 57 s16.1stop62,826 chunterminated; not persisted
R2 fix/polish (att. 10)26,62427 m 18 s16.3length35,208 ch3 rewrites in one response; best closed block still broken
Fix 1 (console error)16,68716 m 48 s16.6stop45,479 chloads, 0 console errors; towers never fire, wave never ends, toast spam
Fix 2 (playtest bugs)17,47517 m 46 s16.4stop47,824 chloads, 0 console errors; towers place (gold 100→10), wave 1 starts; play-through unverified
P (prod, Aug 28)R112,3654 m 25 s46.6stop35,591 chworks first time; plainest design
R212,5504 m 16 s49.0stop35,762 ch
E2 (FN Q3, llama.cpp)R121,14614 m 21 s24.6stop55,956 chworks; most polished game of the set
R221,50417 m 55 s20.0stop56,057 ch
A (FN Q4, llama.cpp)R118,60813 m 16 s23.4stop48,934 chshipped with one fatal typo; self-fixed in one round
R219,19816 m 31 s19.4stop50,226 ch
R3 self-fix18,90816 m 13 s19.4stop50,225 ch
01020304050 tok/s ATLAS R1..fix216.1–17.3 E2 llama.cpp Q320.0–24.6 A llama.cpp Q419.4–23.4 P prod 27B46.6–49.0 P re-run (Sep 3)48.0–48.8
Decode tok/s on the tower-defense rounds (client-measured, completion tokens ÷ wall). Bars show the round-to-round range. The prod re-run is a consistency check only; prior configs were not re-benchmarked.

Prefill: the single-chunk 13,397-token fix-1 prompt showed TTFT 31.3 s, about 430 tok/s. That is in line with the llama.cpp Flash-Next configs (466–475 tok/s on 21K-token prompts) and well below prod (720 tok/s).

Speculative decoding could not be measured: with --speculative --num-drafts 1 the MTP-heavy regime (98% of steps) ran at 23.5 tok/s until it hit defect 2 at position 2,051, and a run where the auto gate parked in serial mode gave 18.8 tok/s. The +18% the recipe documents is real but currently unusable past 2K tokens.

5 · Engine defects, with evidence

All logs are kept on spark under ~/agentbench/atlas-td2-attempt*.log and the fix-loop logs; the consolidated write-up is ~/claude/atlas-flashnext-findings-2026-09-03.md.

D-1 Loop-watchdog rollback desyncs the QSA indexer cache

Trigger
Watchdog armed (default), speculative on. At ~2,492 content tokens it detected a period-2 repeat and rolled back 68 tokens.
Error
decode_batch error: QSA: decode at pos 2864 but 2932 tokens ingested — the indexer cache lost sync (prefix-cache skip or a rewound sequence) → HTTP 500
Reading
The QSA ingested-token counter is not rewound with the sequence. This is the user-visible form of the rewind-safety concern (D3) raised in the PR #16 review.
Workaround
--content-loop-watchdog false

D-2 Speculative verify with QSA active ends the response as a clean stop

Trigger
--speculative --num-drafts 1; generation reaches 372 + 1,679 = 2,051 tokens, exactly where the QSA indexer switches on.
Error
decode_verify_graphed: QSA selection active for row 1 on the batched ms path; the dispatch gate should have routed this batch per-seq, followed by Done: 1679 tokens (stop)
Reading
Two problems: the MTP verify dispatch gate does not re-route when QSA becomes active mid-sequence, and an internal error is surfaced to the client as a normal completion, so callers cannot distinguish truncation from success.
Workaround
Serve without --speculative.

D-3 Prompts over 8,192 tokens fail twice at the PLE layer with defaults

Trigger
An 18,234-token review prompt; chunked prefill uses chunk_size = max_prefill_tokens + 1 = 8193.
Errors
With ATLAS_PLE_MAX_TOKENS=8192: the load log itself warns it "is below the 8193 tokens a single forward can present", and the request 500s. With 8,200: Prefill chunk layer 1 failed: NgramRowCache: every one of 65536 slots is pinned by the batch in flight (8,193 tokens × 16 heads ≈ 131K rows).
Reading
Neither limit is derived from the serve configuration; both are fixed constants the operator must discover from a crash.
Workaround
ATLAS_PLE_MAX_TOKENS ≥ chunk width, ATLAS_PLE_CACHE_SLOTS ≥ chunk × 16 (we used 524,288, ~168 MB pinned).

D-4 Continuation prefill chunks fault with a misaligned CUDA address

Trigger
Any prompt needing more than one prefill chunk, intermittently: 8,193-wide chunks faulted on chunk 2; 4,096-wide chunks prefilled an 18,234-token prompt fine but faulted twice on a 13,397/13,398-token prompt (last chunk 1,117 / 1,118 wide). Chunk 0 never faulted.
Error
phase_continue_prefills::run_standard: Prefill chunk layer 0 failed: … CUDA_ERROR_MISALIGNED_ADDRESS (716), launch grid=[1024,ceil(width/16),1] block=[16,16,1]; then "the CUDA context is destroyed".
Reading
Not simply odd chunk width (parity padding did not help). Layer-0 kernel on the continuation slice. After the fault the process stays up with a dead context, still answers /tokenize, returns 500s, and holds ~100 GB until killed with SIGKILL.
Workaround
Single-chunk prefill: --max-prefill-tokens 20480 (costs ~8 GB of arena).

D-5 ATLAS_QSA_MAX_TOKENS default 32,768 aborts decode mid-stream

Trigger
Generation crossing position 32,768 with --max-seq-len 65536.
Error
decode_batch error: QSA: pos 32768 >= ATLAS_QSA_MAX_TOKENS → HTTP 500; 14.5K tokens of output discarded.
Reading
Silent at load, not tied to --max-seq-len, and surfaced as an error instead of finish_reason=length.
Workaround
ATLAS_QSA_MAX_TOKENS=65536
Two non-defect defaults also bit: --request-timeout is 300 s (any long generation returns finish_reason=timeout), and the recipe-documented --speculative --num-drafts 1 path is the one D-2 breaks.

6 · The fix loop (rounds 1–2 of "no budget limit")

A persistent server stayed up between rounds. Each round fed the model its own file plus what the browser actually reported; the prompt was even-padded via /tokenize while the chunk-parity hypothesis was being tested. Output budget was raised to 28K–40K tokens.

RoundInput givenPrompt tokResultBrowser verdict
Fix 1"Uncaught SyntaxError: Unexpected end of input"13,39845.5 KB, one script block, syntax-clean, complete </html>Loads, map renders, 0 console errors. Playtest: towers never fire (score 0), wave never completes, "LANE CLEAR" toast re-added every frame.
Fix 2Three playtest bugs, described in detail16,98247.8 KB, syntax-clean; model's summary claims all three fixed (shared coordinate mapping, projectile/bounty path, latched wave end)Loads, 0 errors, two towers placed (gold 100→10), wave 1 starts with 6 hostiles. Play-through check was in progress when stopped.

The model's fix-1 preamble diagnosed its own round-2 file accurately: "unclosed IIFE in the main script … no game loop, no rendering, no event handlers exist despite all UI elements being present". Given a precise symptom it repairs well; left to review its own 60 KB file it rewrites from scratch and runs out of budget. That matches the August finding for config A.

Playtesting note for anyone repeating this: a hidden Browser pane reports document.hidden = true and Chrome suspends requestAnimationFrame, so the game loop appears frozen. Test copies with a virtual-clock shim (test2.html, test3.html under /bench/atlas/) were used to drive the loop deterministically; they should be deleted before anything is published.

7 · sparkrun recipe check

@atlas/qwen3.8-flash-next-nvfp4 (registry Avarok-Cybersecurity/atlas-recipes) cannot reproduce this run: it pins the Inferact checkpoint (BF16 PLE table; its text says the FP8 layout is refused, which is no longer true on main), image avarok/atlas-gb10:5be3fc1 from before PR #16, max_model_len 2048, utilization 0.85, and none of the PLE/QSA environment pins or the request timeout. Its full serve line is spark serve Inferact/… --gpu-memory-utilization 0.85 --max-seq-len 2048 --kv-cache-dtype bf16 --max-batch-size 1. The recipe format supports the needed knobs (siblings pin ssm_cache_slots, dflash, etc.), so an update is mechanical once a configuration is agreed.

8 · Fix campaign and the second run (2026-09-04)

All seven plan items were implemented on assist/flashnext-longform-fixes (fork branch, seven commits on 4df6e609, pushed; no upstream PR yet). Executor: Flash-Next Q4 on llama.cpp for the edits with exact anchors; three items were finished by hand where the executor stalled in search loops. Every commit passed fmt, clippy (deny-all) and the full spark-server test targets.

ItemCommitVerified how
1 PLE caps derived from the serve confige623e010unit test; ATLAS2 loaded with the derived 262,144-slot cache, no env pin
2 QSA capacity from --max-seq-len561be215load log "indexer capacity 65536 (max_seq_len=65536)"; ATLAS2 decoded past 32K positions of context without the old cap
3 engine errors → finish_reason=error; cuBLASLt fallback logged16af4d33unit test; the fallback log was the instrument that showed the GEMM operands were aligned and the fault came from an earlier kernel
4 scheduler stops on a latched GPU fault35028544live: the reproduction run exited with "Exiting after a fatal GPU fault" instead of lingering
5 rollback restores QSA/PLE aux statef4e81291unit test; live: the rollback now restores and continues, but the restored cursor is one token behind the resume position ("pos 6050 but 6049 ingested"). Restore path works; the boundary accounting is being pinned with an instrumented run. Bench ran with the watchdog off.
6 MTP latched off before the QSA bound86bdf303unit table test; live log "mtp declined: qsa_active seq_len=2050"
7 aligned mHC prefill scratch (the misaligned-address fault)97aa9fa4the 13,397-token prompt prefills at 4,096 and default 8,192 chunking; ATLAS2's 10K-token round-2 prompt went through an 8,193-wide chunk

D-4 root cause. compute-sanitizer was ~100× too slow to reach the failing chunk, so the reproduction used CUDA_LAUNCH_BLOCKING=1, which attributed the fault to dense_gemm_bf16_pipelined launched as grid=[80,9]: the up_pre = low × up_wt^T GEMM inside hc_pre_gemm. Its scratch was packed as raw byte products and inj_pre is lay × 8 bytes, so an odd slab (1,117 tokens, or the default 8,193) placed up_wt 8 bytes off a 16-byte boundary. Even slabs (1,074 / 1,862 / 4,092) never faulted, which is why it looked random. The cuBLASLt "status 14" seen earlier was the sticky error from that kernel surfacing at the next API call, not a GEMM operand problem.

Second benchmark run (ATLAS2, fixed engine, default chunking, no env pins)

RoundTokensWalltok/sFinishCodeOutcome
R1 build9,6129 m 16 s17.3stop26,675 chclosed block
R2 fix/polish13,86613 m 51 s16.7stop38,163 chclosed block; zero server errors; the 10K-token prompt prefilled through the default 8,193-wide chunk

The engine side is now clean: one attempt, no workarounds, no 500s. The output is not: "BULWARK — Grid Defense Protocol" is a syntactically complete 38 KB file that defines the whole game and never boots — no newRun() call, no requestAnimationFrame loop, no event listeners. It loads with zero console errors and does nothing. Decode speed is unchanged by the fixes (16–17 tok/s single-stream; ~430 tok/s prefill): they remove failure modes, not cost.

Comparison verdict: Atlas serving Flash-Next is now reliable for long-form requests with default settings, but still ~3× slower than prod and 20–30% slower than llama.cpp on this model, and in both benchmark runs its output quality on this task was worse than llama.cpp's (dead or inert games versus playable ones). That gap is a sampling/serving-stack question worth its own investigation, separate from the engine defects fixed here.

9 · What is left

  1. Pin the fix-5 boundary accounting (restored QSA cursor one token behind the resume position) and re-run with the watchdog on.
  2. Decide on an upstream PR for assist/flashnext-longform-fixes (seven commits, CI-style checks green locally; the benchmark gate would need a re-measure).
  3. Investigate why Flash-Next's outputs through Atlas are worse than through llama.cpp at identical sampling (template/sampling presets, min_p/top_n_sigma pins, EOS handling).
  4. Recipe PR: post-fix image, RadixArk checkpoint, 32–64K context, longer request deadline.

Artifacts on spark: ~/tdbench/ATLAS/ (v1.trunc, v2, fix1, fix2, raw responses, rounds.jsonl), ~/driver_td2*.sh, ~/tdbench_r2.py, ~/tdfix3.py, ~/agentbench/atlas-td2-*.log. Prod (qwen38-dflash) was restored after every attempt and is running.

10 · 2026-09-04 afternoon: sampling, seven fix rounds, and the second engine batch

Sampling was the quality lever. Atlas fills unspecified request fields from the model card's [sampling.non_thinking] preset, including presence_penalty = 1.5. Over a 20K-token file that suppresses every token already emitted: ATLAS and ATLAS2 shipped dead/inert games. Pinning the request to llama.cpp's effective values (presence 0, top_k 40, min_p 0.05) on the same fixed engine gave ATLAS3: a complete, wired 51 KB game ("Ironline") with one typo.

RoundTokenstok/sOutcome
R1 / R218,954 / 18,95317.0 / 16.051.2 KB, one fatal typo (c.lineTo)
fix1 (console error)18,98015.5byte-identical except the one token; intro state never leaves idle
fix2 (runtime brief)19,01416.0state fix correct, stray } added
fix3 (console error)30,33414.1one statement changed, 11K tokens of extra output
fix4 (runtime brief)19,07315.6fixed the two lines named, ignored the other path consumers
fix5 (literal brief)19,31815.7playable; kill reward 0/0 → NaN gold
fix6 (runtime brief)19,25915.9NaN fixed; an unasked edit reintroduced the original typo
fix7 (console error)19,04615.7single-token diff; fully playable (3 waves, 20/20 lives)

Pattern: the model repairs exactly what the brief names, and both times it edited something unasked it introduced a regression. Config A on llama.cpp needed one round in August; with one sample per configuration that is suggestive, not proof.

Engine batch 2 (PR #23, commits 953820a0 … 7db64cca)

Throughput (item 7)

The investigation (config A as investigator, 129 tool calls, report in ~/claude/reports/): CUDA graphs are vetoed model-wide because a QSA layer (host top-k) and a PLE layer (host hash) both veto; every token is ~1,500–2,000 eager launches with 12 D2H syncs and 36 PLE stream barriers. The two zero-code levers, ATLAS_D2H_SPIN_SYNC=1 and ATLAS_PLE_FAULT_THREADS=128, measured neutral (15.4 tok/s vs 15.5). The real levers are code: device-side QSA top-k to lift the veto, an event instead of the PLE barrier, one launch per hyper-connection site.

ATLAS4: BF16 head and attention

Same request sampling as ATLAS3, watchdog off: R1 20,217 tokens at 15.2 tok/s, R2 22,432 at 14.0, a 57.7 KB game ("Bastion Defense") with zero syntax errors and one logic bug (killed and leaked enemies are never removed from the enemy list, so a wave never clears). Placement, road, combat all work. Against ATLAS3's typo plus four logic bugs that is better, at a ~10% speed cost; one sample each. With the watchdog on, the same build rolled back cleanly three times but the loop detector false-positived on a coordinate array and cut a coherent file at 8.4K tokens: keep it off for code.

Pages: bench round 2 · ATLAS3 final game · ATLAS4 game · PR #23.