diff --git a/.agents/NOW.md b/.agents/NOW.md index eefa55580..a82f4df8f 100644 --- a/.agents/NOW.md +++ b/.agents/NOW.md @@ -24,7 +24,7 @@ checkpoint on `upstream/main` at `59674cf1d`. | Qwen3.5-4B revalidation | 0.9971x @`59674cf1` (#35); TTFT/PSS pass, TPOT/ITL open | `docs/bench-evidence/` | | MXFP4 parity (Qwen3-8B) | **`MARLIN-STRUCT`: decode-graph + gate_up FUSION default-ON (180→144 GEMM/step); #44 3/3, 0.6B/4B 184/184, 32B 142/142** | residual = marlin CTA + flash | | ROW-SERVE-ASYNC-DENSE-MIRROR | **LANDED+dgx-VERIFIED** (`f9c969ae`): #31 async mirror on classic dense Qwen3; gate RED→GREEN, SACRED 184/184 | Residual: sibling scope one-liner | -| MXFP4 parity goal | graph+fuse default-ON; c8 dominant residual = marlin CTA 144 vs 48. **`KERNEL-MARLIN-DENSE-PORT` gated-OFF** (`VT_MARLIN_DENSE`): vLLM's own dense marlin = byte-preserving E=1; 3 dense `.cu` compile-clean dgx; unit WRITTEN | NEXT (dgx): strict dense-ON vs oracle + nsys + binding (state) | +| MXFP4 parity goal | **`VT_MARLIN_DENSE` FLIPPED DEFAULT-ON** (`row/KERNEL-MARLIN-DENSE-EXEC` `200b4b56`): dense E=1 marlin, 48-CTA vs 128. Ratified near-tie (32B razor gap 0.000, TIGHTER than MoE; goldens regen). Binding beats #51 every axis (c8 0.969, GPU mem 2.63x). Goal still <1.0x c2-c8 (state) | Merge row; next = decode-flash | In-flight branches (default-OFF, not pushed): `laguna-fp4proj-prod` (fp4), laguna bf16/legacy/pipeline-gemv, `ds4-hc-expand-fuse`. Records: diff --git a/.agents/state.md b/.agents/state.md index 03ef90e26..b8e635d4e 100644 --- a/.agents/state.md +++ b/.agents/state.md @@ -37446,3 +37446,62 @@ not attempted here): Default stays OFF until (b) proves byte-match vs the oracle and (d) beats the MoE route. SHA of this landing: `row/KERNEL-MARLIN-DENSE-PORT` (code commit + this records commit). +## KERNEL-MARLIN-DENSE-EXEC: the four scoped gates RAN on GB10 — VT_MARLIN_DENSE FLIPS DEFAULT-ON (dense reduce IS vLLM's numerics: teacher-forced near-tie gap 0.000 nats, TIGHTER than MoE; 48-CTA grid beats the MoE route on EVERY binding throughput axis) + + +Executed the operator dgx follow-up for `KERNEL-MARLIN-DENSE-PORT` (#55) on +`row/KERNEL-MARLIN-DENSE-EXEC` off `origin/main` `828f6420`. Clean git-archive tree at the +port sha built RelWithDebInfo on GB10 sm_121a (full production stack: marlin-nvfp4 [121a] + +CUTLASS sm120a NVFP4 + FA2 [121a] + Triton-AOT vendored). **DECISION: FLIP VT_MARLIN_DENSE +DEFAULT ON** (`200b4b56`; `=0` opts back out to the MoE route). Box left clean (gpu.lock free, +GPU idle, worker down, disk 36G, tmux gone). + +(a) UNIT EXEC. First-ever execution of the #55 dense unit battery (`test_ops_moe_grouped`, +NVFP4+MXFP4, M=1..8 × model shapes). **First run RED on NVFP4** — but the diagnostic proved it is a +TEST-AUTHORING artifact, NOT a kernel bug: `max|dense-moe| == 0` at EVERY shape/M (dense output is +BYTE-IDENTICAL to the validated single-expert MoE route), while the failing elements are all +catastrophic-cancellation outputs (true |ref| 0.04-5, summands ~O(1e3)) where the bf16 tensor-core +result parts from the naive fp32-accumulate CPU reference by O(1) — IDENTICALLY for the MoE route. +The MXFP4 arm (looser 4e-2 + no cancellation in its RNG) passed. FIX (`200b4b56`): the NVFP4 +vs-reference checks use the standard cancellation-robust L2-relative metric; per-element +byte-fidelity stays the EXACT dense==MoE gate; the row-shift RED injection still fires. Re-run +**2/2 cases, 263/263 GREEN**. MEMCHECK (`compute-sanitizer memcheck --leak-check full`): **0 +invalid-access/race errors**; the 42 leak allocations are the graph-safe c_tmp retire-pool + static +harness caches at process exit (same benign class as #54). + +(b) STRICT BATTERY dense-ON vs the ORACLE goldens. `test_qwen3_32b_nvfp4a16_paged_engine` (THE +decider): baseline dense-OFF 6/6·143 reproduced (marlin_gemms=1536, dense_gemms=0). Dense-ON shifts +the greedy anchor at two EXACT bf16 ties — prompt[2] tok0 264->304, prompt[3] tok3 374->702 — so the +:344 hard REQUIRE fired (as it did for the #54 par1 clamp). **Near-tie razor (ratified method):** +teacher-forcing the vLLM 0.25.0 oracle with OUR dense prefix, **every dense token == vLLM's +teacher-forced argmax, max gap 0.000000 nats, 0 out-of-topK** — TIGHTER than the MoE route (which +had 62 mnats at [2,1]). vLLM's greedy CAPTURE landed on 264/374; its teacher-forced run lands on +304/702 (vLLM's OWN bf16 non-determinism at the tie) and dense tracks the tf-argmax. So the "dense = +vLLM's own numerics" premise is CONFIRMED, and the shift is a RATIFIED tie -> regenerated the 32B +`our_ids.npy`+`neartie_gap_mnats.npy` (all-zero gaps) under dense-ON. Re-run dense-ON **6/6·144 +graphed AND eager (byte-identical, capture-safe)**, counters `dense_gemms=2048, marlin_gemms=0, +fused_gate_up=0, fallback_gemms=0` (gate-conditional counter assertion added, `d250893e`). Canaries +(dense inert on bf16): `test_qwen3_paged_engine` 0.6B/4B 2/2·184; `test_qwen3_dense_async_serving` +2/2·82. #44 MXFP4-8B smoke dense-ON **3/3 token-exact + near-tie coherent**. + +(c) NSYS c8 decode (same tool, MXFP4-8B, dense-OFF vs dense-ON A/B): the marlin kernel NAME changes +`marlin_moe_wna16::Marlin` -> `marlin::Marlin` (the vendored dense template RAN), the decode grid +drops **128x1x1 -> 48x1x1** (the designed sms-wide grid), and per-call median **117.8us -> 85.8us** +(Avg 111.2us), total decode-marlin GPU -5%. Confirms the 48-CTA mechanism, now byte-safe. + +(d) BINDING c1..c8 x3 (`mxfp4-online-serving-grid.sh`, oracle vLLM 0.25.0 +`VLLM_DISABLED_KERNELS=FlashInferMxFp4LinearKernel`, RelWithDebInfo strict-contract build in +`~/mxfp4-bench/src` at the flip sha, single-load/arm, drop_caches+mincore, free>=90 gated, +reps tight CoV~1%). total_token_throughput ours/vLLM **c1 1.020 / c2 0.962 / c4 0.966 / c8 0.969** +vs #51 [1.005/0.925/0.939/0.953] = **+0.015/+0.037/+0.027/+0.016 — dense-ON BEATS the MoE route on +EVERY axis, no regression**; median TPOT 1.019/0.960/0.958/0.961 (all improve vs #51); peak GPU mem +28055 vs 73795 MiB = **2.63x LESS**. Evidence +`dgx:~/work/vllm.cpp-online-gate/evidence/6a992a59.../summary-q3mxfp4/`. + +PARITY VERDICT (MXFP4 goal) = still BELOW-FLOOR <1.0x at c2-c8 (best c8 0.969, gate_pass NO) but +CLOSER than #51; c1 crosses to 1.02x. RESIDUAL MAP (per #54 fair graphed-vs-graphed): the marlin +term is now byte-safe-recovered; the next-dominant is decode-flash (+658-784us) then the norm/quant/ +act GLUE tail (+195-290us) + ~0.7ms host/sched. FLIP conditions (parity-enablers) MET: correctness +ratified + beats MoE every axis + no regression + memory win. Landing `row/KERNEL-MARLIN-DENSE-EXEC` +(`200b4b56`): flip `dense_nvfp4_gemm.h`, regen 32B goldens, gate-conditional counters, L2 unit fix. + diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 0eddcf295..1fc601468 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -309,7 +309,7 @@ built on it rather than keeping the flattering one. | Qwen3-dense decode CUDA-graph | Token-exact pass, ~4.3% e2e directional | Steady-state per-step tok/s | | Kimi-Linear-48B-A3B (KDA+MLA+MoE) | Full-model GB10 e2e RUNS (bf16-resident §13), NEAR-TIE 106/128, pool math CLOSES; default OFF | Full model RUNS on GB10 (bf16-resident, RSS peak 1.7 GiB, min-avail 21 GiB, no OOM). Token NEAR-TIE 106/128 (6/8 prompts exact, numerics vs deterministic oracle). 1.59 tok/s. Detail: spec §13 | | vLLM 0.26 re-benchmark | Pending | Re-run the binding grids on the advanced pin | -| MXFP4 Qwen3-8B (W4A16 Marlin) | #51 x3: c1 1.005, c2/c4/c8 0.925/0.939/0.953, mem 2.18x. `MARLIN-STRUCT`: decode-graph + gate_up FUSION default-ON, marlin 180->144 GEMM/step (vLLM-structural); #44 3/3, 32B-NVFP4A16 142/142 | nsys c8: marlin +1,177us (CTA 144 vs 48, dominant); `VT_MARLIN_E1_PAR1` E=1->48 CTAs near-parity but flips a strict 32B token (OFF). Byte-preserving `KERNEL-MARLIN-DENSE-PORT` landed gated-OFF; GPU binding pending | +| MXFP4 Qwen3-8B (W4A16 Marlin) | **`KERNEL-MARLIN-DENSE-EXEC` x3 (dense-ON default): c1 1.020, c2/c4/c8 0.962/0.966/0.969, GPU mem 2.63x less** (beats #51 1.005/0.925/0.939/0.953 EVERY axis); #44 3/3, 32B-NVFP4A16 6/6 | **VT_MARLIN_DENSE FLIPPED DEFAULT-ON**: dense marlin 48-CTA (vs MoE 128), nsys c8 117.8->85.8us/call; ratified 32B near-tie (razor gap 0.000, goldens regen). Goal <1.0x c2-c8, next decode-flash | | SGLang floor arms | Never ran | Both arms of the SGLang comparison | | cuBLAS invocation-parity guard | CI guard landed (CPU); `kGemvHeuristicAlgos` refactor build-verify owed | `nvcc` rebuild + SACRED gate on dgx | | Pre-Ampere breadth (Turing `sm_75` / Volta `sm_70` / Pascal) | **NO NUMBER OWED, nothing executes on these arches.** 2026-08-06 sm_75 compile audit (nvcc 13.0.88): 20 unconditional sm_80+ constructs enumerated; detail in .agents/benchmark-record.md | Port the llama.cpp `fattn-tile`/`fattn-vec` fp16 body. Perf floor when a card exists is **llama.cpp on the same card** (vLLM does not run there) | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index d44ac3418..cbd9f4461 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -65,7 +65,7 @@ are our reading of their documented behavior, not measurements. | GGUF k-quants and i-quants | ✅ | ☐ | ☐ | ✅ | | AWQ | ◐ CPU dequant | ✅ | ✅ | ☐ | | GPTQ | ◐ CPU dequant | ✅ | ✅ | ☐ | -| MXFP4 compressed-tensors | ◐ W4A16 Marlin, mem 2.6x less. gate_up FUSION + decode-graph default-ON (180->144 GEMM/step); #44 3/3, 32B 142/142. Residual = marlin CTA; byte-preserving `KERNEL-MARLIN-DENSE-PORT` landed gated-OFF, GPU gates pending | ✅ | ✅ | ☐ | +| MXFP4 compressed-tensors | ◐ W4A16 Marlin, mem 2.63x less. gate_up FUSION + decode-graph default-ON; #44 3/3, 32B 6/6. **`VT_MARLIN_DENSE` DEFAULT-ON** (`KERNEL-MARLIN-DENSE-EXEC`): dense marlin 48-CTA, byte-faithful, beats MoE (c8 0.969) | ✅ | ✅ | ☐ | | fp8 weights | ✅ | ✅ | ✅ | ☐ | | bf16 / fp16 | ✅ | ✅ | ✅ | ✅ | | Safetensors direct load, no conversion | ✅ | ✅ | ✅ | ☐ | diff --git a/docs/STATUS.md b/docs/STATUS.md index 9ff46e260..513f1c9ff 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -84,7 +84,7 @@ token-for-token correctness against the pinned oracle. | Safetensors loading | Supported | Both gate models plus every registered dense/MoE family | | GGUF loading (F32/F16/BF16/Q4_0/Q8_0/Q2_K/Q3_K/Q4_K/Q5_K/Q6_K/IQ2_XXS/IQ3_XXS/IQ2_S/MXFP4/NVFP4) | Supported; compute-in-quant (keep-quant) on CPU AND now CUDA for the six K-block encodings PLUS Q2_K/IQ2_XXS/IQ3_XXS (DeepSeek-V4 W8, 2026-07-29 - the FIRST CUDA keep-quant GGUF k-quant GEMM `KERNEL-QUANT-CIQ-GEMM-CUDA`, MMVQ-style dequant-in-kernel, GB10-gated 92401/92401 vs the CPU oracle, so a CUDA runner keeps blocks compressed and dots them on the GPU instead of the ARM cores); **NVFP4 now COMPUTES IN FP4 on CUDA for the dense-MLP and full-attention projections (2026-07-29, `CLAIM-GGUF-NVFP4-COMPUTE`), no longer materialize-only** | Weights in six block encodings stay compressed from file to matmul on CPU (no BF16 expansion). NVFP4 (ggml type 40) DEQUANTIZES, including the per-tensor (per-expert) `.scale` sidecar the container keeps outside the blocks; gated BIT-EXACT against the compressed-tensors NVFP4 path on real Qwen3.6-27B bytes from both containers. **It no longer expands to bf16 on CUDA:** an NVFP4 matmul/expert weight is REPACKED at load into the same (`weight_packed [N,K/2]`, `weight_scale [N,K/16]`) operand pair the compressed-tensors path produces - a pure byte permutation, gated BYTE-IDENTICAL against that container - and the existing `vt::MatmulNvfp4*` kernels run on it, so no new kernel exists and no numerics are re-derived. Covers the dense MLP + full-attention q/k/v/o and the MoE shared/routed experts; the GDN `in_proj_*` family and `ssm_out` still expand (the V-head reorder rewrites their layout) and a CPU build still expands everything - the documented `part` subset. **MEASURED on GB10 (2026-07-29), same-binary A/B, one `flock`, idle box, 2 reps per arm:** peak RSS **50.8 -> 25.7 GiB**, load-and-generate **1:58 -> 0:41**, and the 256 projections that move cost 35 840 MiB expanded against 10 080 MiB fp4-resident (3.56x). **The divergence against the safetensors sibling CLOSES:** the fp4 arm is token-IDENTICAL over 24 greedy tokens where the bf16 arm of the same binary diverges at index 4, which retires the reading that that divergence was permanent. It is REPORTED, not gated: the two containers are not the same model - the GGUF NVFP4-quantizes 192 GDN `in_proj` tensors the safetensors keeps BF16 (mean relative weight error ~0.18) and their activation global scales differ - so identity is not guaranteed and a cross-container throughput arm is not valid. SACRED gates unmoved: `test_qwen27_paged_engine` 235/235, `test_qwen36_paged_engine` 315/315. **The MoE (35B) stacked-expert arm is now HARDWARE-GATED too (2026-07-29)**, superseding the gap recorded here: the real 35B A3B NVFP4 GGUF loads and generates through the fp4 path, its 120 routed-expert stacks x 256 experts repack to the modelopt safetensors' own operands with ZERO differing bytes over 840 sampled (tensor, expert) slabs, and all 840 per-expert `.scale[e]` are bit-identical to that expert's `weight_scale_2` - the per-expert scale INDEXING, mutation-proved against both a `scales[0]`-for-every-expert and an expert-0-slab-for-every-expert mutant. Same-binary A/B: peak RSS 68.5 -> 22.7 GiB (3.01x), load-and-generate 1:51.9 -> 0:28.8, tokens IDENTICAL (correct here, since the 35B routed experts run the W4A16 grouped GEMM in both arms). Recorded as OPEN, not smoothed over: this case's 24-token greedy stream is NOT run-to-run stable (one of three `use_a16` runs and one of four safetensors-reference runs differed), so the binding results are the weight-level byte identity and the residency audit, not a token-exactness claim; `test_qwen36_paged_engine` is token-exact at ITS engine params, so the instability belongs to this case's configuration and attributing it is owed work. That run also found and FIXED a latent defect the MoE arm made reachable: the two fp4 fused MoE blocks issued the router GEMM assuming the safetensors `[K,N]` gate layout and threw `matmul: inner dims mismatch` on the GGUF's `[N,K]` one; `MoeRouterLogits` now branches on `nk` (inert for the safetensors path, SACRED gates unmoved). **Q2_K (id 10) + IQ2_XXS (id 16) DEQUANTIZE (2026-07-29, `CLAIM-DSV4-GGUF-LOADER`):** the ~2-bit types the single-Spark `DeepSeek-V4-Flash-GGUF UD-IQ2_XXS`/`UD-Q2_K_XL` vehicles use, ported 1:1 from llama.cpp `ggml-quants.c` (`iq2xxs_grid` codebook + signs; Q2_K nibble sub-scale/min), unit-gated on hand-derived bytes (`test_gguf_dequant` 15/15). Dequant-only (no vec_dot -> expand-bf16). A V4-GGUF model still cannot RUN: the V4-GGUF name map (tensor-manifest-blocked) + the V4 forward (W3-W8) remain. **Multi-shard split GGUF READING landed (2026-08-03, `CLAIM-GGUF-SPLIT-SHARDS`):** `GgufFile::Open` now transparently stitches llama.cpp `gguf-split` shards (`...-00001-of-00003.gguf`) — every shard mmap'd, tensor tables merged, KV metadata taken from shard `00001`, and the sibling shard mappings kept alive by the primary so keep-quant mmap-borrows stay valid across shards (`OwnsSpan` is shard-aware); `VT_GGUF_NO_SPLIT=1` opts out; unit-gated (`test_gguf` split-merge / no-split / count-mismatch cases, 33/33 local). This unblocks the real 3-shard `unsloth/DeepSeek-V4-Flash-0731 UD-IQ2_M` (~91 GiB), whose layout is the NATIVE `deepseek4` arch — per-block `ffn_gate_tid2eid` hash tables (hash layers 0/1/2) + `hc_*` MHC + DSA compressor/indexer are all PRESENT (name-map 1328/1328), `vocab_size` derives from `token_embd` — NOT a standard llama.cpp conversion, so no loader-layout change is owed. It now loads THROUGH 1324/1328 tensors; the sole remaining gap is 4 routed-expert slabs quantized with IQ2_S (id 22, ×2) + MXFP4 (id 39, ×2) — encodings we have GGUF block traits for but no keep-quant vec_dot, so they hit the expand→dequant path which lacks them. Dequant-expanding those 4 big expert tensors to bf16 would add ~17 GiB (~106 GiB total → GB10 OOM-reboot risk), so the memory-safe fix is an IQ2_S+MXFP4 keep-quant kernel (CPU dequant dispatch + the `iq2s_grid` codebook + a CUDA `DotSuperblock`), spec'd as the next brick **IQ2_S (id 22) + MXFP4 (id 39) DEQUANTIZE + KEEP-QUANT on CPU (2026-08-03, `CLAIM-DSV4-UDIQ2M-QUANT`, off-GPU):** the extra per-tensor "dynamic" encodings the `unsloth/DeepSeek-V4-Flash-GGUF UD-IQ2_M` checkpoint mixes into its last routed-expert slabs (IQ2_S `ffn_gate/up` dotting Q8_K, MXFP4 `ffn_down` dotting Q8_0) — ported 1:1 from llama.cpp `ggml-quants.c` @ 237ad9b96 (`iq2s_grid` 1024-entry codebook + DIRECT sign bytes; MXFP4 `kvalues_mxfp4` + `e8m0_to_fp32_half` micro-scaling, distinct from the compressed-tensors `E8M0ToF32` NVFP4 path). CPU dequant + keep-quant `vec_dot`, unit-gated on hand-derived golden bytes (`test_gguf_dequant` 17/17), an INDEPENDENT f64 dequant-then-dot + GEMM NMSE (`test_ops_quant_dot` 19/19), and keep-quant routing (`test_gguf_keep_quant` 37/37) — all CPU-green, so UD-IQ2_M's four previously-`unsupported ggml type 22/39` slabs now load COMPRESSED (no ~17 GiB bf16 expansion that OOM-reboots the box). CUDA: the IQ2_S device `DotSuperblock` is wired into the Q8_K grouped-MoE GEMM and now **CUDA-BUILT + LINKED on GB10 (sm_121a, CUDA 13.0, `-Werror`, 2026-08-03 integration)** — it compiles clean and the merged binary links; MXFP4's device dot (`DotMXFP4`) is written but NOT wired (Q8_0-activation needs a separate 32-block GEMM) so it is marked `[[maybe_unused]]` to keep the ready math without tripping nvcc #177-D, and on GPU MXFP4 CPU-fallbacks like Q4_0/Q8_0. The V4-GGUF forward + a real UD-IQ2_M GPU load/coherence run are owed | | AWQ / GPTQ quantization | W0 spike + W1 CPU INT4 dequant primitive; not yet loadable end to end | INT4 unpack+dequant-to-bf16 for BOTH community formats, mirroring vLLM 1:1 (AWQ reverse-order `awq_triton.py`; GPTQ `qdq_4.cuh` with zero_offset v1/v2 + act-order g_idx). Unit-gated RED-first (hand-computed known bytes + double-precision roundtrip). NOT wired to a loader, no GPU Marlin compute, no model run yet: config recognizer (W2), Marlin GPU GEMM riding the vendored NVFP4 Marlin (W4), CPU e2e (W3), GPTQ 8/2/3-bit (W5) and MoE (W6) are named next bricks. See [.agents/specs/awq-gptq-quant.md](../.agents/specs/awq-gptq-quant.md) | -| MXFP4 (compressed-tensors `mxfp4-pack-quantized`) | Compute PROVEN (#38); GQA-swap ON (#49). **`MARLIN-STRUCT`: decode-graph (`VLLM_CPP_QWEN3_DENSE_DECODE_GRAPH`) + gate_up FUSION (NVFP4 to MXFP4) DEFAULT-ON.** Decode marlin 180->144 GEMM/step (vLLM-structural), byte-token-exact (0.6B/4B 184/184 graph ON==OFF, async 82/82, #44 3/3, 32B-NVFP4A16 142/142). nsys c8: marlin CTA 144 vs vLLM 48 dominates (+1,177us). `VT_MARLIN_E1_PAR1` clamps E=1->48 CTAs (near-parity, 8B token-exact) but flips a strict 32B token so OFF. **`KERNEL-MARLIN-DENSE-PORT` (gated OFF `VT_MARLIN_DENSE`): vLLM's OWN dense marlin = byte-preserving E=1; 3 dense `.cu` compile-clean dgx; unit battery WRITTEN; GPU exec/strict/nsys/binding PENDING.** Detail in state.md | Shared with DeepSeek-V4-Flash + Kimi-K3 MXFP4 paths. CPU E8M0 dequant 5/5·1142. GPU W4A4 + MoE-expert e2e later | +| MXFP4 (compressed-tensors `mxfp4-pack-quantized`) | Compute PROVEN (#38); GQA-swap ON (#49). **`MARLIN-STRUCT`: decode-graph (`VLLM_CPP_QWEN3_DENSE_DECODE_GRAPH`) + gate_up FUSION (NVFP4 to MXFP4) DEFAULT-ON.** Decode marlin 180->144 GEMM/step (vLLM-structural), byte-token-exact (0.6B/4B 184/184 graph ON==OFF, async 82/82, #44 3/3, 32B-NVFP4A16 142/142). nsys c8: marlin CTA 144 vs vLLM 48 dominates (+1,177us). **`KERNEL-MARLIN-DENSE-EXEC`: `VT_MARLIN_DENSE` FLIPPED DEFAULT-ON** — dense marlin 48-CTA (vs MoE 128, nsys 117.8->85.8us/call), byte-faithful (ratified 32B near-tie gap 0.000; unit 263/263, 6/6 graphed+eager, goldens regen); binding beats #51 every axis (c8 0.969, mem 2.63x). Goal <1.0x c2-c8. state.md | Shared with DeepSeek-V4-Flash + Kimi-K3 MXFP4 paths. CPU E8M0 dequant 5/5·1142. GPU W4A4 + MoE-expert e2e later | | CPU backend vs llama.cpp | At or ahead on every axis (GGUF) | Prefill 1.18x ahead, decode at parity, peak memory 1.01x, byte-identical greedy tokens. Single-stream only; no concurrent-serving comparison has been measured | | Paged KV cache + prefix caching | Supported | Block-paged full attention, hybrid full-attention + GDN state groups, automatic prefix caching (APC) on by default for dense models (cache-ON gated end to end: token-identical output, cache hits, faster TTFT) | | fp8 KV cache (`cache_dtype=fp8`) | In progress (W1 CPU brick), not yet usable end-to-end | HIGH-priority memory/throughput lever (halves the KV footprint). W0 spike + W1 CPU brick landed (`KV-FP8` ACTIVE): fp8-e4m3 K/V STORE (`Quantize(hp/scale)`) + the paged-attention READ dequant (`Dequant(fp8)*scale`) + the `cache_dtype` config parse, all CPU-gated RED-first (`test_ops_fp8_kv_cache` 8/8·511; a wrong store direction fails 3/480). Storage is 1-byte fp8 (`DType::kI8`) + a `Fp8KVCacheDataType` interpretation enum, per-tensor k/v scales (mirroring vLLM `BaseKVCacheMethod`). The CUDA store + fp8 paged-attention read (the GPU memory-halving path, DGX-blocked), the runner/spec integration (half-sized KV blocks + checkpoint-scale threading + `--kv-cache-dtype`/`--calculate-kv-scales`), fp8_e5m2 and per-head scales are named W2-W5 in [.agents/specs/fp8-kv-cache.md](../.agents/specs/fp8-kv-cache.md). No model can run with an fp8 KV cache yet | diff --git a/include/vllm/model_executor/models/dense_nvfp4_gemm.h b/include/vllm/model_executor/models/dense_nvfp4_gemm.h index d7bd43d41..9414bc774 100644 --- a/include/vllm/model_executor/models/dense_nvfp4_gemm.h +++ b/include/vllm/model_executor/models/dense_nvfp4_gemm.h @@ -103,20 +103,25 @@ inline bool FusedGateUpEnabled() { return on; } -// VT_MARLIN_DENSE (default OFF): route the E=1 dense NVFP4/MXFP4 projections through -// vLLM's OWN dense marlin GEMM (vt::MarlinDenseGemm) instead of the single-expert -// MoE-marlin route. The dense kernel is direct-A + tile-per-CTA with vLLM's dense -// fp32-C_tmp reduce, so at M<=8 it naturally runs the 48-CTA (sms-wide) grid the MoE -// path only reaches with the VT_MARLIN_E1_PAR1 clamp — WITHOUT that clamp's par -// regrouping, which costs one bf16 ULP vs the oracle and flips a strict 32B token -// (row QUANT-CT-MXFP4-MARLIN-STRUCT / #50 / #54). Same resident weights + workspace; -// the repack permute is vLLM's shared marlin_permute for both dense and MoE. Default -// OFF until the strict-gate battery + binding prove it byte-matches the oracle -// everywhere and beats the MoE route; then flipped ON per parity-enablers. +// VT_MARLIN_DENSE (default ON; VT_MARLIN_DENSE=0 opts back out to the MoE route): +// route the E=1 dense NVFP4/MXFP4 projections through vLLM's OWN dense marlin GEMM +// (vt::MarlinDenseGemm) instead of the single-expert MoE-marlin route. The dense +// kernel is direct-A + tile-per-CTA with vLLM's dense fp32-C_tmp reduce, so at M<=8 +// it naturally runs the 48-CTA (sms-wide) grid the MoE path only reaches with the +// VT_MARLIN_E1_PAR1 clamp — WITHOUT that clamp's par regrouping, which costs one bf16 +// ULP vs the oracle and flips a strict 32B token (row QUANT-CT-MXFP4-MARLIN-STRUCT / +// #50 / #54). Same resident weights + workspace; the repack permute is vLLM's shared +// marlin_permute for both dense and MoE. FLIPPED ON (row KERNEL-MARLIN-DENSE-EXEC): +// the dense reduce IS vLLM's own numerics — the teacher-forced near-tie razor on the +// 32B-NVFP4A16 SACRED gate scores max gap 0.000 nats (every dense token == vLLM's +// teacher-forced argmax, TIGHTER than the MoE route's 62 mnats), and the c8 decode +// marlin runs the 48-CTA grid at ~86us/call vs the MoE route's 128-CTA ~118us/call. +// The MoE route's greedy anchor (our_ids) shifts at two exact bf16 ties, so the 32B +// goldens were regenerated under dense-ON per the ratified-tie regen rule. inline bool MarlinDenseEnabled() { static const bool on = [] { const char* e = std::getenv("VT_MARLIN_DENSE"); - return e != nullptr && e[0] == '1'; + return !(e != nullptr && e[0] == '0'); }(); return on; } diff --git a/scripts/check-public-doc-tables.py b/scripts/check-public-doc-tables.py index 29dd3b53d..5545058bf 100755 --- a/scripts/check-public-doc-tables.py +++ b/scripts/check-public-doc-tables.py @@ -321,7 +321,7 @@ def features_errors(text: str) -> list[str]: # Lowering these numbers as the page is compacted is the gate closing. STATUS = ROOT / "docs/STATUS.md" STATUS_RATCHET = { - "chars": 284_329, + "chars": 284_301, "h2_sections": 11, "long_paragraphs": 89, "oversized_cells": 47, diff --git a/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/neartie_gap_mnats.npy b/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/neartie_gap_mnats.npy index 10cea4697..592685ce9 100644 Binary files a/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/neartie_gap_mnats.npy and b/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/neartie_gap_mnats.npy differ diff --git a/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/our_ids.npy b/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/our_ids.npy index cc3f8c517..8eab798b3 100644 Binary files a/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/our_ids.npy and b/tests/parity/goldens/qwen3_32b_nvfp4a16_greedy/our_ids.npy differ diff --git a/tests/vllm/models/test_qwen3_32b_nvfp4a16_paged_engine.cpp b/tests/vllm/models/test_qwen3_32b_nvfp4a16_paged_engine.cpp index 0b2bcb879..ff7a9d656 100644 --- a/tests/vllm/models/test_qwen3_32b_nvfp4a16_paged_engine.cpp +++ b/tests/vllm/models/test_qwen3_32b_nvfp4a16_paged_engine.cpp @@ -406,13 +406,34 @@ TEST_CASE( vllm::dense_nvfp4::GetW4A16Stats(); MESSAGE("Qwen3-32B-NVFP4A16 W4A16 execution counters: marlin_gemms=" << st.marlin_gemms << " fused_gate_up=" << st.fused_gate_up + << " dense_gemms=" << st.dense_gemms << " fallback_gemms=" << st.fallback_gemms); - CHECK_MESSAGE(st.marlin_gemms > 0, - "the NVFP4 W4A16 Marlin dense GEMM never ran — the quantized " - "path was NOT exercised by this gate"); - CHECK_MESSAGE(st.fused_gate_up > 0, - "the fused gate_up Marlin GEMM never ran — the MLP did not take " - "vLLM's merged gate_up_proj layout"); + // Under VT_MARLIN_DENSE (row KERNEL-MARLIN-DENSE-*), BOTH MatmulNvfp4MarlinD and + // GateUpFusedMarlinD route through vLLM's OWN dense marlin GEMM (dense_gemms++), + // so the single-expert MoE-marlin counters (marlin_gemms / fused_gate_up) must + // stay at zero. Assert the dense route actually RAN and the MoE route did NOT — + // the "the path RAN" positive signal for the flip decider. + if (vllm::dense_nvfp4::MarlinDenseEnabled()) { + CHECK_MESSAGE(st.dense_gemms > 0, + "VT_MARLIN_DENSE set but the dense marlin GEMM never ran — the " + "dense route was NOT exercised by this gate"); + CHECK_MESSAGE(st.marlin_gemms == 0, + "VT_MARLIN_DENSE set but the single-expert MoE-marlin GEMM still " + "ran — the dense route did not fully replace it"); + CHECK_MESSAGE(st.fused_gate_up == 0, + "VT_MARLIN_DENSE set but the MoE fused gate_up still ran — the " + "dense route did not fully replace the fused MLP GEMM"); + } else { + CHECK_MESSAGE(st.marlin_gemms > 0, + "the NVFP4 W4A16 Marlin dense GEMM never ran — the quantized " + "path was NOT exercised by this gate"); + CHECK_MESSAGE(st.fused_gate_up > 0, + "the fused gate_up Marlin GEMM never ran — the MLP did not take " + "vLLM's merged gate_up_proj layout"); + } + CHECK_MESSAGE(st.fallback_gemms == 0, + "the naive NVFP4 fallback GEMM ran on CUDA — a Marlin route " + "silently fell back"); MESSAGE("Qwen3-32B-NVFP4A16 correctness gate: " << (strict_exact + neartie_only) << "/" << N << " prompts PASS " diff --git a/tests/vt/test_ops_moe_grouped.cpp b/tests/vt/test_ops_moe_grouped.cpp index a92f30824..8e87b9a00 100644 --- a/tests/vt/test_ops_moe_grouped.cpp +++ b/tests/vt/test_ops_moe_grouped.cpp @@ -117,6 +117,29 @@ void CheckClose(const std::vector& got, const std::vector& want, f CHECK(bad == 0); } +// L2-relative closeness: ||got-want||_2 <= rtol * ||want||_2. This is the standard +// GEMM-correctness metric and — unlike a per-element absolute tolerance — is robust +// to catastrophic cancellation. A bf16 tensor-core GEMM vs a naive fp32-accumulate +// reference legitimately diverges by O(accumulation-magnitude * eps_bf16) at output +// elements whose TRUE value is near zero (a large sum that cancels): the per-element +// |got-want| there (~1) dwarfs the tiny |want| yet is pure bf16 rounding, not a +// kernel defect (proven: the VALIDATED single-expert MoE route diverges identically, +// max|dense-moe|==0). Per-element byte-fidelity is still gated EXACTLY by the +// dense==MoE check below; a wrong stride still fails this L2 gate (the row-shifted +// reference is uncorrelated => ||got-shift|| ~ ||want||, ratio ~1 >> rtol). +void CheckCloseL2(const std::vector& got, const std::vector& want, float rtol) { + REQUIRE(got.size() == want.size()); + double num = 0.0, den = 0.0; + for (size_t i = 0; i < got.size(); ++i) { + const double d = static_cast(got[i]) - static_cast(want[i]); + num += d * d; + den += static_cast(want[i]) * static_cast(want[i]); + } + const double rel = std::sqrt(num) / (std::sqrt(den) + 1e-12); + CAPTURE(rel); + CHECK(rel <= rtol); +} + struct Nvfp4Weight { std::vector packed; // [N, K/2] std::vector scale; // [N, K/16] @@ -1352,7 +1375,13 @@ TEST_CASE("CUDA marlin DENSE gemm matches CPU-dequant ref AND the grouped route dout.Download(gq.q, h_dense.data()); std::vector got_dense(static_cast(M * N)); for (size_t i = 0; i < got_dense.size(); ++i) got_dense[i] = vt::BF16ToF32(h_dense[i]); - CheckClose(got_dense, ref, 3e-2f, 3e-2f); + // NVFP4 random data exercises catastrophic cancellation (some output rows are + // large sums that nearly cancel), where a bf16 tensor-core result legitimately + // parts from a naive fp32-accumulate reference by more than a per-element 3e-2 + // — identically for the VALIDATED MoE route (max|dense-moe|==0 below). Gate the + // vs-reference correctness with the cancellation-robust L2-relative metric; the + // byte-preserving per-element claim is the EXACT dense==MoE check. + CheckCloseL2(got_dense, ref, 2e-2f); // (2) GROUPED single-expert MoE route (all M tokens -> expert 0). const int block = (M <= 8) ? 8 : vt::cuda::MarlinMoeAlignBlockSizeSelect(static_cast(M), 1, 1); @@ -1381,7 +1410,7 @@ TEST_CASE("CUDA marlin DENSE gemm matches CPU-dequant ref AND the grouped route mout.Download(gq.q, h_moe.data()); std::vector got_moe(static_cast(M * N)); for (size_t i = 0; i < got_moe.size(); ++i) got_moe[i] = vt::BF16ToF32(h_moe[i]); - CheckClose(got_moe, ref, 3e-2f, 3e-2f); + CheckCloseL2(got_moe, ref, 2e-2f); // Dense and grouped agree to within a couple bf16 ULPs (both marlin; the // dense reduce differs from the par-regrouped grouped reduce by ~1 ULP — // exactly the point of the port).