Repository navigation
qwen4_exp: serve NVIDIA's Qwen3.8-Flash-Next FP4-Local on its native formats (one GB10) - #195
Conversation
CSF-compressed NVFP4 block scales (lossless, per the layout vLLM's csf_scales.py specifies), exact MXFP8 -> BF16 expansion, and NVFP4 rows -> BF16 for row-gathered tables. Pure arithmetic, no env lookups in the hot loops. Checked on nvidia/Qwen3.8-Flash-Next-FP4-Local with examples/fp4local_decode: CSF bytes identical to a numpy port of the vLLM reference decoder on four expert tensors (81-762 exceptions each); dequantized experts cos 0.994, MXFP8 projections cos 0.9996 and PLE rows cos 0.995 against the NVFP4 pack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4aUGgtiZX94pLr63HXQzn
…ssed tensors resident - U32 maps to a raw 4-byte container (CSF exception streams); the fast loader's header now defers to the shared dtype table instead of a copy. - The fast loader defers CSF streams and the weight/scale pair of every module in host_decode_modules, so the build expands them from disk. - is_ngram_table also takes the per-shard NVFP4 block scales of the PLE table, which are row data like the weight. - An index that names shards under a directory (tensors/model-*.safetensors) routed zero tensors to every shard; match shard paths, not file names. - DeferredTensor::read_bytes / WeightStore::undefer for build-time consumers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4aUGgtiZX94pLr63HXQzn
The pack differs from the NVFP4 one only in how it stores tensors: - weight_map::modelopt_local decodes the 73,728 CSF expert scales into plain E4M3, expands 438 MXFP8 projections and 56 vision NVFP4 projections to BF16, and rounds the 100 F32 norms (hc_norm, PLE norms) to the BF16 the kernels read. Left F32, those norms produced NaN logits: every request answered with one empty token. - The PLE table is NVFP4: a second row cache serves the block-scale rows and the gather decodes both on the host, the trellis path's contract. Verified on a GB10 (sparky): boots in 65 s, coherent chat, correct tool call, needle passes at 4.4k prompt tokens, 16.9 tok/s decode without MTP (the NVFP4 pack measures 16.14 without it). MTP experts are per-expert NVFP4 here and are not wired yet, so --speculative is not supported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4aUGgtiZX94pLr63HXQzn
nvidia/Qwen3.8-Flash-Next-FP4-Local quantizes mtp.layers.0.mlp.experts as W4A16 NVFP4 with plain block scales, the trunk experts' own format, so a PerExpertNvfp4 layout loads them through quantized_any as shipped. The load log now names the detected layout instead of always claiming a stacked BF16 requantization. mtp.rs's tests move to mtp_tests.rs. Verified on a GB10: --speculative --num-drafts 1 boots, K2 acceptance 73-75%, decode 18.9-19.8 tok/s (16.9 without), outputs coherent and the tool call unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4aUGgtiZX94pLr63HXQzn
…FP8 bytes nvidia/Qwen3.8-Flash-Next-FP4-Local ships the GDN in_proj_qkv, in_proj_z and out_proj (36 layers, 2.08 GB) as MXFP8: E4M3 plus one E8M0 scale per 32 inputs, QAD-trained that way. They were expanded to 4.15 GB of BF16 at load and read by dense_gemv_bf16 — 44% of a decode token's GPU time. - kernels: mxfp8_proj.cu — mxfp8_proj_gemv (1 row), mxfp8_proj_batchm (2..8 rows, one weight pass, every row bit-identical to the 1-row kernel) and mxfp8_proj_dequant (exact BF16 expansion for prefill). - ops::mxfp8_proj picks by row count; prefill expands one projection at a time into a single 84 MB scratch shared by all GDN layers, then bf16_gemm. - materialize uploads native_mxfp8() modules byte for byte; ATLAS_FP4LOCAL_EXPAND_MXFP8=1 restores the BF16 expansion for A/B. - qwen4_exp::gdn_mx8 builds the layer on those bytes; Qwen3SsmLayer::mx8 is the only copy of both projections and is checked first at every dispatch site (serial decode, batched verify, multi-seq decode, prefill). Measured on GB10 (sparky): - microtest: dequant byte-identical to modelopt_codec::mxfp8_to_bf16 over 42M values; GEMV vs f64 rel_l2 1.7e-3; batchm M=1..8 0 mismatches; 230 GB/s, 1.89x dense_gemv_bf16 on [16384, 2560]; M=8 costs 1.09x M=1. - serve: pre-KV 97.6 -> 95.0 GB; decode with --speculative --num-drafts 1 18.9-19.8 -> 24.2-28.6 tok/s; coherence prompts and tool call correct. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4aUGgtiZX94pLr63HXQzn
FP4-Local ships the 12 full-attention layers' q (with its interleaved
gate rows), k, v and o as MXFP8. The generic builder expanded them to BF16
and then requantized BF16 -> NVFP4 for decode: a second, lossy quantization
of weights NVIDIA's QAD trained in MXFP8, plus a BF16 copy for prefill.
- QuantWeight::Mxfp8(Mxfp8Proj); Mxfp8Proj moves to weight_map so the
variant carries its geometry. quant_gemv/quant_gemm refuse it by name.
- Qwen3AttentionLayer::set_mx8_projections installs q/k/v/o as the only
copies (BF16 slots NULL, no NVFP4/FP8 twin) with the kernels and the
shared prefill scratch; try_mx8 is checked first at every projection
site: serial decode Q (gated + ungated) / K,V / O, the multi-sequence and
verify path (qkv_mx8.rs: one weight pass per 8 rows, then the per-row
scatter, as qkv_fp8.rs does) and its O, and the three prefill
projections through try_unpacked_prefill (MXFP8, then packed Q2_0).
- ops::mxfp8_proj: up to 32 rows run as 8-row GEMV chunks (bit-identical
to single-token decode per row), more rows expand + bf16_gemm.
- qwen4_exp::attn_mx8 builds the layer; mx8_common holds the checked
weight/scale lookup and one scratch sized for the largest projection of
either layer kind. native_mxfp8() now covers self_attn.{q,k,v,o}_proj.
Measured on GB10 (sparky), --speculative --num-drafts 1: pre-KV 95.0 ->
93.4 GB; decode 24.5-26.3 tok/s (the MXFP8 q/k/v/o read 0.6 GB a token vs
0.3 GB for the lossy NVFP4 requant); coherence prompts, tool call, and the
~4.4k-token passcode recall all correct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4aUGgtiZX94pLr63HXQzn
The FP4-Local pack ships the 100 hyper-connection hc_norm and PLE norm_conv/norm_key/norm_query weights as F32 (the only release that does). modelopt_local rounded them to BF16 because every consumer kernel was declared __nv_bfloat16* — measured lossy on all 100 tensors. The kernels already compute (1 + w) in FP32, so change the norm-weight arguments to const float* (hyper_connection.cu: hc_pre, hc_head, hc_pre_stage, hc_pre_stage_bf16, hc_post_stage_bf16, hc_pre_stage_vec, hc_post_stage_vec; ple.cu: ple_gate; strix-hip mirror), load every site's norm through dense_keep_f32 — F32 native for FP4-Local, exact (bits << 16) widening for the NVFP4 pack — and delete the BF16 rounding. NVFP4 stays bit-identical; FP4-Local keeps its full precision.
The FP4-Local checkpoint ships every main-layer mlp.shared_expert
{gate,up,down}_proj as MXFP8 (E4M3 + E8M0 scales). The loader used to
expand them to BF16 and requantize them to NVFP4 for the fused routed
kernels -- an 8-bit -> 4-bit loss on the one expert every token goes
through.
Generalize the Laguna BF16 seam into a separate shared-expert enum
(Bf16 | Mxfp8): fused routed kernels run with a NULL shared expert and
the native chain (mxfp8_proj + silu_mul) fills shared_out afterward.
Gate and up are row-concatenated into one [2*inter, h] projection so
the <=32-row bands take one pass plus fused_silu_mul; larger prefills
use the unfused trio. Covers decode, MTP K=2 verify, C>1 batched decode
(forward_rows, qwen4exp_fast, atomic_c4) and prefill. The MTP block's
BF16 shared expert is installed through the BF16 arm.
Tracked weight memory: +0.15 GB vs the NVFP4 shared copy.
gate_proj and up_proj are now views into the halves of the [2*inter, h] gate|up weight (and scale) buffer the fused small-row arm already needed; the store originals are reclaimed at load. Tracked weight memory drops ~0.16 GB. Also drop a dead local in helpers_shared.
FP4-Local's PLE table ships as packed E2M1 rows + E4M3 block scales on NVMe. Every gather used to copy both to the host, decode to BF16 there, upload, and synchronize the stream. Now the block-scale rows fault into the same cache as the weight rows (new segmented scale source on NgramRowCache) and a new batched_embed_nvfp4 kernel decodes them in place -- bit-identical to the host oracle, no per-gather synchronize, no copy_slot. Bookkeeping scales to tens of millions of slots: u32 row ids, O(pins) end_batch, and ATLAS_PLE_CACHE_GB sizes the arena (slot count stays an env override, the span-derived floor still applies). Adds a 30 s residency/hit-rate/fault-time line plus a shutdown summary.
FP4-Local ships the ViT's attn.qkv/attn.proj as MXFP8 and the MLP/merger linears as NVFP4; materialize used to host-expand all of them to BF16. They now upload as shipped (NVFP4 tensors stay deferred, uploaded verbatim by the vision loader; visual MXFP8 joins the native list) and VitWeight expands them into an encoder-owned BF16 scratch per GEMM -- mxfp8_proj_dequant and dequant_nvfp4_to_bf16, both bit-identical to the host codec. Every other checkpoint keeps raw BF16 weights on the same call path. Tracked weight memory drops ~0.5 GB.
…e open speed gap Adds docs/checkpoints/qwen3.8-flash-next-fp4-local.md: what the checkpoint ships, what Atlas now runs natively, how exactness was proven, why the n-gram table stays disk-backed, the serve configuration that was benchmarked, the results with their run ids, a same-box comparison against NVIDIA's vLLM recipe, and the ranked list of what is left. The four scripts under bench/qwen4_exp/fp4local/ are the non-in-tree harnesses those results used. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
CI's fmt job runs the latest stable rustfmt (1.10), which lays out several of this branch's new expressions differently from the pinned 1.93 toolchain they were formatted with. Formatting only. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Overnight results (same build and settings as the doc, sparky GB10)Same-binary quality A/B (only the checkpoint differs): BFCL golden ST-995, no overrides, single run each.
This is an observation from one run per arm, not a significance claim. Re-runs with the adopted
End to end:
Next on this branch (in progress): make FP4-Local loading format-driven. The per-module formats will come from |
Replace the qwen4_exp-shaped name lists with ModeloptQuantPlan: the
checkpoint's quantized_layers map (config.json or hf_quant_config.json)
yields a per-module ModuleFormat { algo, group_size, scale_encoding },
accepting both group_size and weight_block_size spellings and the
FP8_PB_WO tile form. Which modules host-expand and which stay native now
come from the plan's algo intersected with a per-model NativeConsumers
registry; CSF streams decode only for weight_scale_encoding == "csf",
any other encoding fails the load naming it. A model with no registered
consumers gets the exact-but-unoptimized BF16 expansion with no names
needed. FP4-Local loads byte-identical to the name-driven path.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Pushed b2c031e: format-driven FP4-Local loadingFP4-Local handling is now keyed on what the checkpoint declares, not on qwen4_exp module names.
Gates (sparky):
Note: nvidia's only FP4-Local releases are Flash-Next and GLM-5.3-Flash; no Qwen3.8-27B FP4-Local exists. GLM-5.3-Flash FP4-Local enablement is owned by someone else; this change only makes its declared formats parse and fail loudly where a codec is missing. |
…FP4-Local record Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Happy to test this at TP=EP=2 if that's useful. Our GB10 pair has served this same checkpoint every day since 10-16, using Enntity's SparkQwen FP4-Local build. That gives a same-box reference for both output and speed. What we'd run on a build of this branch at TP=2:
If TP=2 isn't meant to work on this branch yet, say so and we'll wait. Otherwise, is there a head commit you'd like us to pin? |
…t ceiling The default K-vs-batch ladder pinned every n<=4 verify at 3 drafts, so qwen4_exp with ATLAS_QWEN4EXP_MTP_DEPTH=4..7 (K=5..8, exact-lane) still speculated only 3 deep. The rung exists to step depth down with concurrency, not to cap below the ceiling -- the n<=4 rung now passes num_drafts (already min(--num-drafts, verify_max_drafts)) through, while the 8/16/32 rungs are unchanged. Models capped at 3 see no change. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Speed step S1 (sparky, current single-user profile +
|
| config | exactness CHECK | decode-floor (accept len) | short chat decode / first token | ~17K agent turn decode / first token |
|---|---|---|---|---|
| control | — | 33.1 (3.29) | 25.5 / 0.38 s | 22.9 / 15.8 s |
MOE_UNITS=1 |
0 rows differ (64 steps / 256 rows) | 33.0 (3.29) | 25.5 / 0.38 s | 22.8 / 15.5 s |
MOE_TC=1 |
different numerics, so every draft was rejected | inconclusive (raw runs ~22) | 21.4 / 0.38 s | 20.6 / 15.4 s |
| depth 4 (after cap fix) | 0 rows differ (64 / 256) | 32.3 (3.29) | 24.9 / 0.38 s | 23.0 / 15.3 s |
| depth 5 (after cap fix) | — | 31.4 (3.29) | 24.3 / 0.38 s | 23.0 / 15.8 s |
- Pushed 171f532: the default MTP ladder's first rung held single-sequence speculation at 3 drafts, even when
ATLAS_QWEN4EXP_MTP_DEPTHallowed more. It now passes the model's ceiling through. The rungs for higher concurrency are unchanged. - Deeper drafts don't pay on this workload. Accept length stays at 3.29, and the extra verify rows cost time, so the profile stays at 3 drafts.
- Neither existing MoE lever helps at C=1.
MOE_UNITSis a wash.MOE_TCbreaks speculative exactness. - The gap to vLLM is in the routed-MoE decode kernels, which run at ~53% of bandwidth. That's the next step (S2).
The single-row gate_up and silu_down kernels issue one byte-wide load per thread per k step and sit at ~100 GB/s under a cold expert set (~45 GB of per-layer pointer tables, 216 GB/s ceiling). Both impls now double-buffer a 64-row packed tile plus its scale rows through shared memory via cooperative 16 B loads, then accumulate each thread's own output column in the identical FP32 order — byte-identical outputs, 1.1-1.26x on the cold bench, +3.6% decode-floor. The _e8m0 (GS=32) variants keep the scalar impl. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
On a cold ~17K prefill the fault wall is ~450K O_DIRECT row reads; at 64 in-flight preads the pool reaches the GB10 NVMe's ~66K IOPS random-4K ceiling (fio-measured), cutting the fault wall ~20% and agent TTFT ~6% (15.4s -> 14.5s). Same rows, same bytes, same slot/pin semantics -- only deeper queue depth. ATLAS_PLE_FAULT_THREADS still overrides. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Speed steps S2 and S4 (sparky, single-user profile, same-session A/Bs)S2: routed-MoE decode (pushed 40a01d3). The cause was cold-data streaming, not memory layout. Each decode step reads about 9 MB of experts that are not in L2, out of roughly 1 GB.
S4: prefill / first token (pushed c24ea26). In a profile of the ~17K agent turn, about 7 s of the ~15 s first-token time is the GPU waiting on first-touch n-gram (PLE) rows read from NVMe: about 455K misses per prompt.
Remaining prefill costs from the same profile: routed-MoE batched GEMMs 25% of busy time, QSA attention 18%, mHC 15%, projections 11%, GDN 11%. |
Cold faulting reads two IOPS-bound O_DIRECT reads per miss: the packed row and its E4M3 block-scale row (~2.03 reads/miss measured cold). With the flag on, attach_segmented_block_scales reads every shard's scale tensor into one flat host arena in shard order at boot (~3.2 GB for the 128-shard NVFP4 table, loaded shard-parallel); fetch_row then memcpys arena[id * stride..] using the same (shard, local) split as scale_loc, leaving packed rows on disk. ~1.02 reads/miss, fault wall 7.5s->5.3s and agent TTFT 14.4->13.7s on a cold 17K turn. Flag off is byte-identical; Segments/files are built the same either way. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Speed step S4f (pushed 5e18f77): opt-in resident n-gram block scalesMeasured cold (dropped page cache, fresh boot), each first-touch n-gram miss cost 2.03 device reads: the 80 B packed row plus its 10 B E4M3 scale row. The drive is limited by reads per second, not bytes: fio gives the same ~62–69K IOPS at 512 B, 1 KiB and 4 KiB. So halving reads is the lever.
Cold A/B, single-user profile +
The 4-request controls are byte-identical, flag off vs on, on FP4-Local and the NVFP4 pack. The flag stays opt-in: its 3.2 GB comes out of the KV budget, which matters for the 262K profile. |
The 171f532 shallow rung passes the model's draft ceiling through at n<=4, so slot 0 legitimately sizes at the full K-1 when num_drafts=7. The assertion encoded the retired cap. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…stfmt The format-driven load commit removed `native_mxfp8`; the module doc now links `native_consumers`. Includes cargo +stable fmt output. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Gate campaign status: preparing the run on sparky surfaced a regression in this branch. |
…ghts BF16-only vision trees no longer register mxfp8_proj_dequant or dequant_nvfp4_to_bf16 with the boot audit — the FP4-Local native-format work had made every non-flash-next vision model fail boot. A checkpoint that does carry MXFP8/NVFP4 projections fails hard naming the kernel and weight if the kernel is missing. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@AzeezIsh, as you asked: how this branch and Enntity's FP4-Local path ( They solve opposite halves:
Why they differ, as far as we can tell: Enntity built for two Sparks and for speed (rank-sliced twins, W8A8 prefill). This branch built for fidelity to the checkpoint on one box (native shared expert, F32 norms, no extra PLE copy). Neither has the other's half. The plan you suggested, as we'd carry it out with @data-angel:
We opened Enntity/sparkqwen#13 to ask Jason about step 1. If this matches what you have in mind, we start there once he answers. |
|
@maplepaladin73 thanks, this is a useful map. One update to the premise: the TP=2 half of this branch exists as #198, stacked on this PR. It serves FP4-Local on two GB10s at TP=EP=2 and keeps this branch's fidelity choices. So step 1 of the plan is largely done on the Atlas-Inf side. Same rows as your table, with #198 at TP=2:
Measured on one GB10 pair, rc16 TP2 profile, single runs unless noted (fingerprints in the PR):
We haven't run the Enntity / SparkQwen build on this pair, so there's no head-to-head yet. What the merged path would take from each side, as I see it:
Happy to run the SparkQwen build on our pair for a same-box comparison if that helps step 3. |
….93.1 Formatting only. CI's fmt job runs the rust-toolchain.toml pin (1.93.1), which these two spots from the resident-scale arena did not match. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
FP4-Local agentic soak (2.5 h shape) passed at d773b21. Head is now 5ddfabb (formatting only)agentic-webserver, 50 iterations:
In progress on sparky:
The |
ST-995 at head, plus main vs head on the two gates that failed on other modelsBFCL golden ST-995 at
main
video-fidelity on
|
…entic-webserver records NVIDIA's FP4-Local checkpoint now has its own non-default, measured entries next to the vendor pack's, so its evidence lives in the benchmark SSOT rather than only in PR comments: the golden ST-995 draw (87.44 / 87.32 in run-1792340347555552942 at a78d2fb and run-1792488042130537521 at 5ddfabb) and the 50-iteration agentic soak (50/50 + 50/50, run-1792475013132909319 at d773b21). Bars follow the pack entry's rules: measured less the inherited 0.4 MTP floor for BFCL, absolute measured counts for the soak, speed deliberately ungated. BENCH.toml is outside the closure hash, so no gate record is invalidated. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Concurrency clears: with the MTP gate out of the picture, head matches mainWhy the gate runs were bimodal (main as well): the MTP throughput gate.
Controlled A/B, gate bypassed (
Also on the branch (
The remaining red checks:
Per the merge condition agreed tonight, merging. |
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
Serve NVIDIA's
nvidia/Qwen3.8-Flash-Next-FP4-Local(ModelOpt QAD, ~96 GiB) on one GB10, with each tensor family running on the format the checkpoint ships: no widening copies and no 8→4-bit requant. The full record (formats, verification, memory analysis, benchmarks, and the open speed gap) is indocs/checkpoints/qwen3.8-flash-next-fp4-local.md.tensors/are routed.in_proj_qkv/z/out_proj, attention q/k/v/o, the shared expert and the vision attention.mxfp8_proj.cu: a 1-row GEMV and a batched GEMV whose rows are bit-identical to it, plus an exact dequant feeding a BF16 GEMM for prefill.SeparateSharedExpert::{Bf16,Mxfp8}) and is applied in every MoE arm. The previous BF16 → NVFP4 requant of an 8-bit shared expert is gone.const float*. BF16 packs are widened exactly.batched_embed_nvfp4, bit-identical to the host decoder). Rows and their block scales share one slot-parallel cache.ATLAS_PLE_CACHE_GBsizes the cache, the bookkeeping scales to tens of millions of slots, and a periodic hit-rate line is logged. Full residency was measured not to fit on one Spark: the table's payload entropy is 3.966 of 4 bits, so it doesn't compress (doc §4).Measured (sparky, GB10; single runs unless noted; run ids in the doc)
run-1792340347555552942). A same-binary NVFP4 control is still queued, so this PR makes no claim versus the NVFP4 pack.Notes for reviewers
spark-storagen-gram cache: new scale source;u32row ids;end_batchis O(pins).Test plan
EXACT_VERIFY_CHECK/BATCH_FAST_CHECK: 0 rows differ across MTP depth 3 and C=4 batchingcargo fmt --check, workspace clippy (ATLAS_SKIP_BUILD=1), license headers, typos, file-size cap at the rebased HEAD