Run Qwen3.8-Flash-Next — a ~176B-parameter model (125B main + 51B n-gram, 6B active) — on one NVIDIA DGX Spark / ASUS GX10 with vLLM, at full prefill speed, with MTP speculative decoding, working prefix caching, deterministic greedy decoding, and up to 500k tokens of context.
The catch this repo solves: the NVFP4 checkpoint is 122 GiB, which does not fit
next to a usable KV cache in the Spark's 128 GB unified pool. 44 GiB of that is
the n-gram embedding ("PLE") table — a pure lookup that a token only touches 16 rows
of. This repo patches the official vLLM image to serve that table from NVMe via
mmap instead of keeping it resident. Weights drop to ~76 GiB, the rest of the
pool goes to KV, and everything runs on stock GB10 kernels.
Along the way it also fixes two things that were broken for this model on GB10 — prefix caching (a vLLM block-size bug silently restored an all-zero Mamba state on every cache hit) and non-deterministic top-k in the sparse attention (a GB10 kernel that drops candidates) — and offers an optional hybrid checkpoint layout (NVFP4 experts + fp8 side layers) that decodes ~20% faster at the same quality.
Independently reproduced on a DGX Spark by @jschmied — see issue #1 and their write-up.
If you cloned this before, here is the short version (details in the linked sections):
- Prefix caching works now —
--enable-prefix-cachingwas crashing, then silently returning wrong answers on cache hits. Root cause was a vLLM block-size bug that made every prefix hit restore an all-zero Mamba state; two-line fix in the image. Getting there took @Saren-Arterius's pointer to vllm#50729 and their state-copy guard, and @0xBakeer's attempt to reproduce it, which sharpened the write-up.PREFIX_CACHE=1is the new default. Repeated prefixes (system prompts, multi-turn, tool loops) skip the prefill: ~14 s → ~1.4 s TTFT on a 20k-token prefix. → Prefix caching now works - Greedy decoding is deterministic now — the GB10 sparse-attention top-k kernel was
non-deterministic and dropped candidates, diagnosed and reported upstream by
@k3dani (issue #3, vllm#51782). Exact top-k
is the new default (
EXACT_TOPK=1); identical outputs at temperature 0, same tournament score, costs some long-prefill speed. → Deterministic top-k - Two checkpoint modes —
MODE=nvfp4(as published) orMODE=hybrid(NVFP4 experts- fp8 side layers, one-time
scripts/prepare-hybrid.sh): +20% decode, +8% KV, same quality. Our box runs the hybrid. The fp8 side-layer conversion and the original int4+fp8 dispatch it is ported from are @Saren-Arterius's. → Two checkpoint modes
- fp8 side layers, one-time
- Also in the image: vllm#50729 (Mamba state-copy race, by @AndreasKaratzas) + a bounds guard, the GB10 FLA fixes and the faster PLE gather from @Saren-Arterius's fork.
- We benchmarked the int4 (Intel AutoRound) variant too with the same patches: fastest raw decode, but not deterministic and slowest cached-TTFT, so we did not adopt it. Numbers in docs/HOW-IT-WORKS.md.
- fp8 KV cache is available (
KV_DTYPE=fp8_e4m3), contributed by @Nanetnounou: ×1.9 KV, 1M context on one box — at a speed and quality cost, so it is opt-in. → fp8 KV cache scripts/smoke-test.shnow also checks the prefix-cache hit and determinism, and measures decode on a real answer instead ofignore_eos(which produces meaningless numbers with this model).scripts/download-weights.shnow forwardsHF_TOKEN(@wawimundo, PR #4).
Everything was measured on one ASUS GX10 with a 17-scenario agentic tournament (3 repeats each), single-request speed benches on real prompts, and state checksums for the prefix-caching work; nothing here is extrapolated.
| llama.cpp IQ4_XS | NVFP4 (this repo) | hybrid (this repo) | |
|---|---|---|---|
| Prefill | ~540 tok/s | ~1,500–2,000 tok/s (exact top-k; ~2,400 with the stock kernel) — warm page cache; a first pass over a cold region of the table reads from NVMe and can be 2–3× slower (see PREWARM) |
same |
| Decode, single stream | ~22 tok/s (no MTP) | ~26 tok/s with MTP=2 | ~31 tok/s |
| Prefix-cache hit, TTFT on a 20k-token prefix | n/a | ~1.4 s (vs ~14 s cold) | same |
| Context | 262k | 262k native, 500k with YaRN | same |
| KV cache @0.80 (500k YaRN, MTP) | — | ~580k tokens | ~630k tokens |
| Deterministic at temperature 0 | yes | yes (EXACT_TOPK=1) |
yes |
Measured on an ASUS GX10 (GB10, 128 GB), single request, real prompts, greedy. Quality (a 17-scenario agentic tournament, 3 repeats) is identical across NVFP4 and hybrid: 45/51 both, same two scenarios failed by every quantization we tried. Details and the full comparison tables are in docs/HOW-IT-WORKS.md.
- An NVIDIA DGX Spark or compatible GB10 (sm_121) box, 128 GB unified memory, aarch64, recent NVIDIA driver, Docker with the NVIDIA container runtime.
- ~130 GB free disk for the checkpoint (+13 GB for the hybrid variant), on reasonably fast storage (the table is read from it at runtime — NVMe strongly recommended; the Spark's onboard NVMe is ideal).
- The base image is multi-arch, so
docker buildalso works on x86 Blackwell (sm_120, e.g. RTX PRO 6000) for testing, though this is tuned for the Spark.
git clone https://github.com/blazux/qwen3.8-Flash-DGX.git
cd qwen3.8-Flash-DGX
docker build -t qwen38-flash-dgx . # ~1 min: official image + the patches
scripts/download-weights.sh # ~122 GiB, resumable (one-time)
scripts/serve.sh # NVFP4 mode, boots on :18300 (~8-13 min to load)
docker logs -f qwen38-flash # wait for "Application startup complete"
scripts/smoke-test.sh # health, coherence, prefix-cache hit, determinism, tok/sThen hit the OpenAI-compatible API:
curl http://localhost:18300/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model": "qwen3.8-flash-next",
"messages": [{"role":"user","content":"Write a haiku about a desktop supercomputer."}],
"max_tokens": 512
}'500k context (YaRN, validated with a needle-in-a-haystack at 414k tokens):
YARN=1 CTX=500000 GPU_MEM=0.80 scripts/serve.shscripts/serve.sh serves one of two layouts of the same RadixArk NVFP4 checkpoint;
pick with MODE=.
MODE=nvfp4 (default) |
MODE=hybrid |
|
|---|---|---|
| Routed experts (the bulk, ~63 GiB) | NVFP4 | NVFP4 (unchanged) |
| GDN in/out projections, QSA q/k/v/o, shared experts (~15 GiB) | bf16, as published | blockwise fp8-e4m3 (128×128 blocks, DeepSeek layout) |
| Extra preparation | none | scripts/prepare-hybrid.sh once (~10 min, +13 GB disk) |
| Decode (MTP=2, greedy, real answers) | ~26 tok/s | ~31 tok/s (+20%) |
| Prefill | same | same (±5%) |
| KV cache | ~580k tokens | ~630k tokens (+8%), weights ~7 GiB smaller |
| Tournament quality (17 agentic scenarios × 3) | 45/51 | 45/51 |
| Deterministic at T=0 | yes | yes |
| Behavioural difference we noticed | — | slightly "more careful" in tool loops: it sometimes checks state first (one extra tool call), which is the only place it scored differently before we raised the turn budget |
Why it works: those side layers are dense and read in full on every decoded token, so
they dominate decode bandwidth; the experts are sparse (10 of 512 active) and already
4-bit. Halving the dense part is where the tokens/s come from. The MoE path — where
the quality lives — is untouched. Conversion uses
@Saren-Arterius's fp8_convert.py (worst
per-tensor max relative error 3.5%), and a small dispatch shim
(src/vllm_fp8_hybrid_modelopt.py) that routes those layers to vLLM's blockwise-fp8
GEMM while the ModelOpt NVFP4 config keeps handling the experts.
scripts/prepare-hybrid.sh # builds <snapshot>-fp8hybrid/ next to the HF snapshot
MODE=hybrid YARN=1 CTX=500000 GPU_MEM=0.80 scripts/serve.shOur own box runs the hybrid. If you want the checkpoint exactly as published, stay on
MODE=nvfp4 — you lose ~5 tok/s and nothing else.
--enable-prefix-caching used to crash this model on GB10 (CUDA illegal memory access) and, with a bounds guard in place, to silently return different answers on
cache hits. We traced it (details in docs/HOW-IT-WORKS.md):
vLLM's engine core overwrites cache_config.block_size with the smallest KV-group
block size — 8 tokens here with MTP=2 (4 without), the QSA raw-key ring — while the Mamba
state block is 1600 tokens. Two places used the former as the latter, so on a prefix hit
the worker computed the state slot as (3200-1)//8 = 399 instead of 1, read past the
block table row, and restored an all-zero Mamba state. The image carries a two-line fix;
with it, cold and cache-hit outputs are bit-identical (state checksums and first-token
logprobs match exactly) and the tournament score is unchanged.
What you get: multi-turn chats, agent/tool loops and shared system prompts skip the
prefill of everything already seen. On a 20k-token prefix, TTFT goes from ~14 s to
~1.4 s; with 8 concurrent conversations, from ~80 s to ~4–6 s. PREFIX_CACHE=1 is the
default. Mamba states are cached at 1600-token boundaries, so the tail of a prefix is
recomputed — expect the benefit to start around a couple of thousand tokens.
The sparse attention (QSA) picks the top-k key blocks per query with a persistent_topk
kernel. On GB10 that kernel is non-deterministic — identical greedy requests produce
different outputs 2 times out of 4 — and can drop legitimate candidates
(vllm#51782); reported against
this repo by @k3dani in
issue #3. The GB10 is more
exposed than other GPUs: the cooperative kernel used elsewhere for decode is disabled on
sm_12x, so this kernel runs for both prefill and decode.
EXACT_TOPK=1 replaces it with an exact torch.topk over the visible columns:
4/4 prompts stable, first-token logprobs identical to the 4th decimal, tournament
score unchanged. Cost: nothing on decode, ~10% on 8k prefill, ~20–40% on 32k+ prefill.
EXACT_TOPK=0 gives the stock kernel back if you prefer speed over reproducibility.
(We also tried just masking the never-written columns before the stock kernel: still
unstable, so it is the kernel itself.)
@Nanetnounou contributed a patch
(issue #6,
vllm#54426) that lets the QSA
layers read an fp8_e4m3 KV cache — the model used to refuse anything but bf16. It is
in the image, inert unless you pass KV_DTYPE=fp8_e4m3. Measured on the GX10 (hybrid,
MTP=2, exact top-k, prefix caching, GPU_MEM=0.80):
| bf16 KV (default) | KV_DTYPE=fp8_e4m3 |
|
|---|---|---|
| KV pool | 634k tokens | 1,219k tokens (×1.9) |
| Max single request | 500k (YaRN) | 1,000k (YaRN) — boots and serves |
| Decode | 30.8 tok/s | 27.9 tok/s (−9%) |
| Prefill 32k | 1,794 tok/s | 1,254 tok/s (−30%) |
| Needle 92k | 69 s | 89 s |
| Tournament | 45/51 | 45/51, but b3_itinerary (long multi-constraint reasoning) drops from 6/6 to 2/6 passes and its successful run took 4× longer |
The slowdown comes from the in-kernel dequantization and the halved block_n needed to
fit the GB10's shared memory; the quality dip is the fp8 rounding of K/V in the sparse
attention. Our recommendation: keep bf16 unless you actually need > 500k tokens in one
request or 2× the concurrency at 500k, and re-check your own workload's quality if you
switch. Attention blocks become 3,184 tokens in this mode (page alignment), so prefix
caching captures Mamba states less often.
KV_DTYPE=fp8_e4m3 YARN=1 CTX=1000000 GPU_MEM=0.80 MODE=hybrid scripts/serve.sh| Var | Default | Notes |
|---|---|---|
MODE |
nvfp4 |
hybrid = fp8 side layers (see above; needs scripts/prepare-hybrid.sh). |
PREFIX_CACHE |
1 |
--enable-prefix-caching. Correct with this image (block_size fix). |
EXACT_TOPK |
1 |
Exact, deterministic QSA top-k. Costs prefill on long prompts: −8% at 8k, −20–40% at 32k+ (decode unchanged; with prefix caching only the new tokens pay it). 0 = stock kernel: full prefill speed, but non-deterministic and may drop attention candidates (issue #3). |
PORT |
18300 |
API port |
CTX |
262144 |
Max context. Native is 262144; with YARN=1 up to 500000 is validated. |
YARN |
0 |
1 = YaRN rope scaling (factor 4, Qwen's recipe) for CTX > 262144. |
SEQS |
8 |
Max concurrent sequences. Do not benchmark with 1–2: excess requests queue silently and aggregate tok/s flatlines (see below). |
GPU_MEM |
0.85 |
Fraction of the 128 GB pool for weights+KV. For long-running service use 0.80 (as in the Quickstart): 0.875 got OOM-killed on a 300k-token prefill with MTP, and after a day at 0.85 the box drifted into swap. The lower you set it, the more RAM the page cache has for the 48 GiB table — which is what your prefill speed depends on (below). Right after stopping another big container the first boot can fail with "13.5 GiB KV cache is needed, larger than available" — memory not yet released; the unless-stopped retry succeeds. |
MTP |
2 |
Speculative tokens from the model's MTP head (0 = off). |
KV_DTYPE |
auto |
auto = bf16 (recommended). fp8_e4m3 = ~1.9× KV pool, 1M context on one box, at −10% decode / −30% prefill and a measurable quality cost — see fp8 KV cache before using it. |
PREWARM |
0 |
1 streams the 48 GiB table once at boot to warm the page cache — steadier first-request latency, ~10 s extra startup. |
WORKERS |
32 |
Threads used for the mmap gather (only used above VLLM_PLE_MMAP_FAST_ROWS=512 unique rows; decode-sized gathers run inline). |
EXTRA |
Extra vLLM flags, passed verbatim. |
Single-stream numbers understate this model on a GB10. @jschmied traced one box under load (RadixArk NVFP4, 8k ctx, no speculative decoding, using vLLM's native PLE CPU offload rather than this repo's mmap — the table-serving cost behaves the same way) and found aggregate throughput scales far past single-stream:
| concurrent streams | aggregate tok/s | per stream | major faults / token | TTFT |
|---|---|---|---|---|
| 1 | 17.1 | 17.1 | 16.0 | 0.22 s |
| 8 | 87.5 | 10.9 | 7.0 | 0.53 s |
| 16 | 131.6 | 8.2 | 9.6 | 0.83 s |
| 32 | 212.0 | 6.6 | 4.3 | 1.19 s |
| 48 | 266.8 | 5.6 | 3.6 | 1.60 s |
Two things worth knowing (their words, lightly condensed):
- The paged table is an argument for concurrency, not against it. Page-fault cost per token falls 4.4× from c=1 to c=48: batched tokens share n-gram rows and the page cache keeps the hot set, so the marginal token is far cheaper than the first. The table gather itself never exceeded ~25% of one CPU core.
- A low
--max-num-seqsis indistinguishable from saturation if you only look at tok/s. With--max-num-seqs 2their sweep flatlined at ~33 tok/s whilevllm:request_queue_time_seconds_sumclimbed to 142 s. Checkmax-num-seqsbefore quoting an aggregate number — this repo's default is now8for that reason.
Method and harness: load-and-waits.md.
A token's n-gram lookup reads 16 rows × 160 bytes ≈ 2.5 KB. Over a 20k-token
prefill that's ~1.3 GB of small reads — under a second on NVMe, and the hot n-grams
stay in the page cache. So the 44 GiB table never needs to be in the unified pool:
we mmap the checkpoint's model-plefp8-*.safetensors shards and gather rows on
demand. Nothing else about the model changes — the hashing, dequant, and the sparse
attention all run stock.
Full details, including the GB10-specific bugs this works around and the long-context findings, are in docs/HOW-IT-WORKS.md.
From @Saren-Arterius's fork, merged here with thanks:
- FLA shared-memory gate — sm_121 reports 99 KiB of shared memory per block but the
flash-linear-attention gate asked for 100 KiB, so all 36 GDN layers silently ran on
small tiles. One
sedin the Dockerfile lowers the gate to 99 KiB. chunk_delta_hnum_warpspin — works around atl.dotrace on Blackwell (fla#953). Correctness, not speed.- PLE gather hot path — CPU dedup of row ids, a persistent pinned staging buffer with an
async H2D copy, GPU-side expansion through the inverse index, and an inline fast path
for decode-sized batches (
VLLM_PLE_MMAP_FAST_ROWS, default 512). Also: bf16/f16 tables,VLLM_PLE_MMAP_DIRto serve the table from another directory, and a periodicPLE mmap statslog line (VLLM_PLE_MMAP_STATS_SEC, default 30). - Mamba state-copy guard — with vllm#50729
(the overlapping-copy race fix by @AndreasKaratzas), a bounds check that turns an
out-of-range block id into a skipped copy plus a log counter instead of a dead CUDA
context. With the block_size fix above the counter stays at 0; if you ever see
mamba state-copy guard: N out-of-range, something upstream regressed — please report it. fp8_convert.py— the side-layer conversion behindMODE=hybrid.
Their fork goes further with an int4 (Intel AutoRound) + fp8 hybrid checkpoint: qwen3.8-Flash-DGX-AutoRound. We benchmarked it with the same patches: ~34 tok/s decode, 44/51 on the tournament, but it is not deterministic even with the exact top-k and Marlin's atomic adds off, and it has the slowest prefill of the three — we kept the NVFP4-based layouts.
vLLM ships its own path (VLLM_PLE_CPU_OFFLOAD=1) that keeps the table in pinned host
RAM in a separate worker process. On a Spark that RAM is the same pool as the GPU, so
it saves less than the mmap — but @jschmied got it running and documented two things
you will need if you go that way (neither applies to the mmap patch, which is a single
process):
_get_ple_embedding_quant_method()inple_layer.pyonly acceptsFp8Config; with the NVFP4 checkpoint the quant config ismodelopt_fp4, so the FP8 PLE shards are rejected and loading dies onngram_embedding.weight_scale. Acceptingmodelopt/modelopt_fp4there fixes it.- The worker hands CUDA tensors to the GPU process over IPC via
pidfd_getfd, whichkernel.yama.ptrace_scope=1(the Ubuntu/DGX OS default) forbids between sibling processes. In Docker:--cap-add=SYS_PTRACE. Under systemd:AmbientCapabilities=CAP_SYS_PTRACE. It fails ~10 minutes in, after all shards have loaded, with an unhelpfulEngine core initialization failed.
Details: results-radixark-vllm.md.
Dockerfile official vLLM Flash-Next image + the patches below
src/vllm_ple_mmap.py 1. mmap PLE table (opaque splitting op) VLLM_PLE_MMAP=1
src/mamba_utils_guarded.py 3. vllm#50729 + bounds guard (drop-in mamba_utils.py)
src/patch_mamba_block_size.py 4. prefix-caching block_size fix
src/patch_qsa_exact_topk.py 5. exact, deterministic QSA top-k VLLM_QSA_EXACT_TOPK=1
src/vllm_fp8_hybrid_modelopt.py 6. NVFP4 experts + fp8 side layers dispatch VLLM_FP8_HYBRID=1
src/patch_qsa_fp8_kv.py 7. fp8_e4m3 KV cache on the QSA path (by @Nanetnounou) --kv-cache-dtype fp8_e4m3
src/test_ple_mmap_cpu.py CPU unit test for the gather (no GPU needed)
src/test_qsa_exact_topk_cpu.py CPU unit test for the exact top-k (no GPU needed)
tools/fp8_convert.py side-layer bf16 -> blockwise fp8 (by @Saren-Arterius)
scripts/download-weights.sh
scripts/prepare-hybrid.sh one-time: build the -fp8hybrid snapshot
scripts/serve.sh MODE=nvfp4|hybrid, PREFIX_CACHE, EXACT_TOPK, YARN, ...
scripts/smoke-test.sh health, coherence, prefix-cache hit, determinism, tok/s
docs/HOW-IT-WORKS.md
Run the unit tests (no GPU):
docker run --rm -v "$PWD/src:/t" -w /t --entrypoint python3 qwen38-flash-dgx test_ple_mmap_cpu.py
docker run --rm -v "$PWD/src:/t" -w /t --entrypoint python3 qwen38-flash-dgx test_qsa_exact_topk_cpu.py- One big model at a time. At
GPU_MEM=0.85this uses most of the 128 GB pool; don't co-locate another large model (an 8B embedding model next to it already starves the KV cache — we moved ours to another machine). - Full
torch.compileis off (an Inductor int64-indexing assert on sm_121); the serve script uses PIECEWISE CUDA graphs with the PLE lookup as a splitting op. - 1M context needs the fp8 KV cache (
KV_DTYPE=fp8_e4m3, see above), which costs speed and some quality; in bf16 a single 1M request needs ~26 GiB of KV and 500k with YaRN is the validated ceiling (800k booted but got OOM-killed on a long prefill). - Exact top-k costs prefill on long prompts (see above). The implementation is a
plain
torch.topkover the full visible width per chunk; a fused kernel would recover most of it — PRs welcome. - Weights are not included and the checkpoint carries Qwen's license (with a MAU/revenue clause) — review it before production use.
- Model: Qwen team, Alibaba — Qwen3.8-Flash-Next.
- NVFP4 checkpoint: RadixArk/Qwen3.8-Flash-Next-NVFP4.
- Serving engine and base image: vLLM (
vllm/vllm-openai:qwen38-flash-next, therelease/qwen38nextrecipe / PR #53896); the Mamba state-copy race fix is vllm#50729 by @AndreasKaratzas. - GB10 FLA fixes, the faster PLE gather, the state-copy guard and the fp8 side-layer conversion: @Saren-Arterius (qwen3.8-Flash-DGX-AutoRound).
- The fp8_e4m3 KV cache patch for the QSA path: @Nanetnounou (issue #6, vllm#54426).
- The non-deterministic
persistent_topkdiagnosis and upstream report: @k3dani (issue #3, vllm#51782). - Independent reproduction on a DGX Spark, the native-offload fixes and the concurrency measurements: @jschmied (issue #1, qwen38-flash-next-gb10).
- The mmap-PLE patch, the hybrid dispatch for ModelOpt-NVFP4, the prefix-caching root cause and fix, the exact top-k path and the GB10 serving recipe in this repo: see LICENSE (Apache-2.0).