fix(qwen3_5): re-verify 27B NVFP4 on the nvidia ModelOpt checkpoint — binding 0.72x, attributed (#164 follow-up) - #178
Conversation
fc549e4 to
ba43904
Compare
|
Rebased onto current main ( Why it grew. Binding grid (warm servers, one flock, greedy,
We are behind, flatly. Greedy continuations are identical between engines. SACRED gates on the production build: Two method defects are fixed here, both of which would have recurred:
Attribution. The dense-marlin routing fork is closed here (and the hang that blocked it before is named: mirroring the shared call sequence drops a per-call workspace memset that the shared helper compensates for by zeroing at allocation, leaving marlin's spin-locks uninitialized). It is worth +0.5% at c2-c4, +0.8% at c8 — real, small, and it retires the prime suspect. The next lever is the bf16 Known pre-existing preflight failure, not from this branch: |
|
Gap partially closed: 0.72x → 0.85x. The
This was never a missing capability.
Peak host RSS 24.2 → 21.0 GiB. Per-token 114 ms → ~96 ms, ~78% of the bandwidth roof against vLLM's ~95%. The 32-token greedy continuation is byte-identical to vLLM's, captured from the same warm process as the numbers. SACRED gates on this build: 235/235 and 315/315. Still behind. The remaining lever is the NVFP4 MLP marlin: 40% of the step, 192 calls, ~68% of roof. |
|
35B: the "correctness defect" was a bit-exact oracle tie, and a router lever now puts decode at 0.98x. Correctness — resolved, not fixed, because there was nothing to fix. One
vLLM's Speed — warp-shuffle MoE router top-k. The attribution put
Byte-identical by construction: top-k is an argmax reduction, and argmax under a total order is associative and commutative, so any reduction order gives the same (value, index). The softmax max/sum reductions in the same kernel are arithmetic and were deliberately left on their existing tree — reassociating those would move the denominator by an ulp. Gates: c2 and c8 remain the weak cells and both carry the wider spread, so the honest next step there is tightening the measurement before chasing it. |
e81f81e to
c666d04
Compare
…the gap The 27B-NVFP4 grid of record was measured on `unsloth/Qwen3.6-27B-NVFP4`, and that repo re-quantized in place: @890bdef7 is genuine NVFP4, @ccdaab7e is the same name turned into FP8 W8A8. `nvidia/Qwen3.6-27B-NVFP4` @0893e160 is the publisher's own single-revision NVFP4 build and is the reference from here on. Re-measuring against it found us far behind, and most of that is now closed. LOADING. Three defects, all the same shape: the dense loader assumed BF16 wherever the compressed-tensors NVFP4 probe did not match. One `MaterializeBf16Source` now sits under the BF16 loaders (a per-channel scale read as per-tensor is REJECTED rather than silently wrong); `IsNvfp4Projection` accepts ModelOpt naming (`weight`/`weight_scale_2`) alongside compressed-tensors; and W4A16 stays the default because consuming ModelOpt's `input_scale` flips `IsTrueW4A4()` into the fp4-activation GEMM, which produced incoherent text here (`VT_MODELOPT_W4A4=1` opts in). THE GAP WAS ALSO A LOADER BUG. `LoadAttnDense` and `LoadGdnDense`'s `out_proj` had only NVFP4-or-BF16 branches, so a `modelopt_mixed` FP8 tower fell through to the BF16 path, which DEQUANTIZES: ~3 GiB of FP8 became ~5.9 GiB of BF16 re-read every decode step and executed as cuBLAS `gemvx`. Never a missing capability -- the `*_fp8` slots and their `MatmulFp8Cutlass*` consumers already existed and the MoE loader has done exactly this since the 35B work, with its own comment calling it the DEFAULT. vLLM keeps the same weights fp8, so this is POL-MIRROR-VLLM. 27B, warm servers, greedy, ignore_eos, 128 tokens, medians of 3, vs vLLM 0.25.0 c | before | after | vs vLLM before -> after c1 | 8.76 | 10.41 | 0.713x -> 0.847x c8 | 62.13 | 72.49 | 0.722x -> 0.843x peak host RSS 24.2 -> 21.0 GiB TWO ROUTER KERNELS WERE BARRIER-BOUND, not compute-bound. The router grid is one block per token, so at decode a single block ran the k selection rounds with a block-wide tree each (~64 `__syncthreads` for k=8 over 256 experts); the GROUPED variant was worse, running group scoring, mask, top-k and renorm on ONE lane behind `if (threadIdx.x != 0) return;`, which its own comment deferred as "W9". Both now use a warp-shuffle argmax. Byte-identical by construction: top-k is an ARGMAX over a total order, which reassociates freely, while the softmax max and sum reductions in the same kernels are ARITHMETIC and are deliberately left on their original trees. 35B-A3B @491c2f1e, same harness: c1 70.58 -> 73.24 (0.945x -> 0.980x), c4 194.87 -> 199.90 (0.952x -> 0.977x), c2 0.826x -> 0.867x, c8 0.900x -> 0.919x THE 27B GATE WAS LETTING THE FILESYSTEM PICK THE MODEL. Five checkpoint-gated tests, SACRED `test_qwen27_paged_engine` among them, took the first entry `fs::directory_iterator` yielded under a repo with TWO revisions, so a token-exact pass against the FP8 revision would have been recorded as an NVFP4 pass. Green by luck. `tests/parity/hf_snapshot.h` pins the revision the goldens' own `oracle.model` field names, with `VT_QWEN27_SNAPSHOT` as the escape hatch. `max_num_seqs` also moves 8 -> 32: at 8 a c8 client sat exactly on our own batch ceiling. Not vLLM's 1024, because it also caps the padded decode-graph set. Correctness: 27B greedy continuation byte-identical to vLLM across 5 repeats. The 35B's continuation differs at ONE token, and that is NOT a defect -- vLLM's own top-2 there are the same float32 value (-1.2221027612686157, diff 0.0) and its `torch.argmax` breaks the tie by lower index; our `ArgReduce` implements the same rule, but our logits are not bit-identical, so the tie does not reproduce. Gates: test_qwen27_paged_engine 235/235, test_qwen36_paged_engine 315/315, test_ops_moe 33451/33451, test_ops_moe_grouped 440/440, grouped_bf16 19/19, test_deepseek_v4_moe 716/716. Still behind, and named rather than hidden: 27B at 0.85x with the NVFP4 MLP marlin at ~68% of the bandwidth roof, 35B c2/c8 the weak cells, `CastF32Kernel` 3.1% of the 35B step. Full method, the refuted hypotheses and the decode attributions are in `.agents/benchmark-record.md`. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [ClaudeCode]
c666d04 to
6603356
Compare
…message Main CI has been red on EVERY merged PR. Two independent defects, both landed by #178's push (run 31332846716); `cuda-fat-build` in that run was cancelled by concurrency, not failing. THE GATE REDDENED MAIN FOR OBEYING IT. `check-role-discipline.py` judges every commit in the `before..after` range on its own message. A PR landed with a REAL merge commit pushes the merge AND the branch commits under it: the merge names the PR, the branch commits were never required to, so each one read as a direct push. `6603356a` (#178), `e73cbbae` (#204) and `1a02ab4f` (#196) all failed this way, in both `documentation-checkpoint` and `agent-record`. Arrival is now judged ONCE, on the commit that lands the change: `merged_pr_content` exempts what a row/* PR merge brings in. Squash-merges are untouched -- their one commit carries "(#N)" and passes on its own message. NOT a weakening, and gated as such: only the SIDE parents count, so `--not parents[0]` keeps a commit pushed straight to main from being laundered by merging a PR on top, and a merge naming no row and no PR exempts nothing. Four unit checks build real git history for those cases, plus the exact `3bbee96e..0cf3dbb` range CI ran, pinned with `has_reached_main` forced TRUE -- from a `row/*` worktree everything reports as pending PR disposition, and the test would have passed against the defect it exists to catch. Suite 47/47, and 5/5 red without the fix. A STALE MESSAGE IN A TEST. `6603356a` taught `LoadMergedBf16RawNK` to accept F8_E4M3 shards and rewrote its rejection to name the supported dtypes; `test_qwen27_dense_forward.cpp:229` still asserted the old "expected BF16", so `build-test-cpu` and both sanitizer legs failed on it. The expectation now reads the message the loader raises, and the FP8 merge path that arrived WITHOUT a test in this file gets one: a mixed BF16+FP8 merged parameter, expectations hand- computed from E4M3 bytes and the scale (never re-derived through the same dequant helper the loader calls), plus the per-channel-scale rejection. 7/7, 333 assertions. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [ClaudeCode]
…message (#210) Main CI has been red on EVERY merged PR. Two independent defects, both landed by #178's push (run 31332846716); `cuda-fat-build` in that run was cancelled by concurrency, not failing. THE GATE REDDENED MAIN FOR OBEYING IT. `check-role-discipline.py` judges every commit in the `before..after` range on its own message. A PR landed with a REAL merge commit pushes the merge AND the branch commits under it: the merge names the PR, the branch commits were never required to, so each one read as a direct push. `6603356a` (#178), `e73cbbae` (#204) and `1a02ab4f` (#196) all failed this way, in both `documentation-checkpoint` and `agent-record`. Arrival is now judged ONCE, on the commit that lands the change: `merged_pr_content` exempts what a row/* PR merge brings in. Squash-merges are untouched -- their one commit carries "(#N)" and passes on its own message. NOT a weakening, and gated as such: only the SIDE parents count, so `--not parents[0]` keeps a commit pushed straight to main from being laundered by merging a PR on top, and a merge naming no row and no PR exempts nothing. Four unit checks build real git history for those cases, plus the exact `3bbee96e..0cf3dbb` range CI ran, pinned with `has_reached_main` forced TRUE -- from a `row/*` worktree everything reports as pending PR disposition, and the test would have passed against the defect it exists to catch. Suite 47/47, and 5/5 red without the fix. A STALE MESSAGE IN A TEST. `6603356a` taught `LoadMergedBf16RawNK` to accept F8_E4M3 shards and rewrote its rejection to name the supported dtypes; `test_qwen27_dense_forward.cpp:229` still asserted the old "expected BF16", so `build-test-cpu` and both sanitizer legs failed on it. The expectation now reads the message the loader raises, and the FP8 merge path that arrived WITHOUT a test in this file gets one: a mixed BF16+FP8 merged parameter, expectations hand- computed from E4M3 bytes and the scale (never re-derived through the same dequant helper the loader calls), plus the per-channel-scale rejection. 7/7, 333 assertions. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [ClaudeCode] Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Follow-up to #169. That PR fixed the reported symptom (BF16-only
lm_head); running it against the real checkpoints on the GB10 box then uncovered two more layers behind it. Same root cause each time: the dense loader assumed BF16 wherever the compressed-tensors NVFP4 probe did not match.Layer 2 — FP8 tower tensors
Both publishers quantize parts of the tower to FP8 and leave the rest BF16, and they disagree on which parts and on the scale layout:
nvidia/Qwen3.6-27B-NVFP4linear_attnin_proj_qkv / in_proj_z / out_projunsloth/...@ccdaab7eOne
MaterializeBf16Sourcenow sits underLoadBf16Direct,LoadBf16TransposedandLoadMergedBf16RawNK. BF16 stays zero-copy on the mmap (unchanged); FP8 is dequantized. Reading a per-channel scale as per-tensor would be silently wrong rather than loud, so the element count decides and anything else is rejected.Layer 3 — ModelOpt NVFP4 naming
Our probe was
has(<proj>.weight_packed)— compressed-tensors only. NVIDIA ships ModelOpt:<proj>.weightU8 +.weight_scaleF8 +.weight_scale_2F32. Every probe missed, so a genuinely NVFP4 tower fell through to the BF16 path and died at the first U8 tensor.IsNvfp4Projectionnow accepts both spellings;LoadNvfp4AnyNamingreads either, converting ModelOpt's direct scale to the CT divisor convention the shared dequant expects — the same conversion #169 made forlm_head.W4A16 is the default for ModelOpt
These checkpoints carry a per-tensor
input_scale. Consuming it setsalpha, flipsIsTrueW4A4()and routes to the fp4-activation GEMM — which produced incoherent output on nvidia's checkpoint. Leavingalphaat 0 takes the weight-only dispatcher and generates correctly.VT_MODELOPT_W4A4=1restores the other arm for A/B (documented indocs/ENVIRONMENT.md).Verified on real weights, dgx GB10 (sm_121)
Against the real 21 GB
nvidia/Qwen3.6-27B-NVFP4:dense loader: expected BF16 for ...linear_attn.in_proj_qkv.weight"The capital of France is"→" Paris."Clean CUDA build on the box; clean CPU build; full
scripts/check-*.pybattery green.This unblocks benchmarking the new NVFP4 publishes, which is the next step.