qwen3_5_moe: support unsloth/Qwen3.8-27B-NVFP4 (mixed-precision NVFP4+FP8) - #208
qwen3_5_moe: support unsloth/Qwen3.8-27B-NVFP4 (mixed-precision NVFP4+FP8)#208chrisqianz wants to merge 3 commits into
Conversation
…loth NVFP4+FP8) unsloth's dynamic per-module quant exports (e.g. unsloth/Qwen3.8-27B-NVFP4) store dense Qwen3.x checkpoints as per-module mixed precision: FP8 attention / GDN output linears (per-row scale), NVFP4 dense-MLP layers, FP8 dense-MLP layers, bf16 in_proj_b/a and norms. The loader crashed on them (fp8/bf16 promotion in the bf16 fusion, missing packed weights for natively-built linears) or materialized everything to bf16 (54 GB on a 27B -- no launch parameter fits a 32 GB card). Keep every dense linear in the storage the checkpoint actually uses, sniffed from model.safetensors.index.json: - config: _compressed_linear_storage() now reports per-module storage (attention nvfp4/fp8/none, per-layer dense-MLP overrides, fp8 lm_head); ModelConfig gains dense_mlp_storage (per-layer override map) and routes attn_quant/lm_head_quant to native fp8_pertensor when the export says so - moe: _SharedExpert builds per-layer native linears (NVFP4 W4A16 / FP8 W8A16 / bf16) from the override map; layer_id threaded through the dense MLP - model: lm_head built as native FP8 (Fp8PerTensorLinear) when the checkpoint stores it fp8 (halves the ~2.5 GB bf16 lm_head) - weight: the dense pass keeps fp8 parts native (q/k/v -> qkv_proj, in_proj_qkv/z -> in_proj_qkvz, dense gate/up -> gate_up_proj fusions; o_proj/out_proj/down_proj/lm_head singletons, per-row fp32 scales); a part buffered into the fp8 fusion never also enters the bf16 buffer (incomplete fusion assert); fp8 dequant (for the unscaled remainder) is a bf16 broadcast multiply (no fp32 copy); ShardReader gains has() for sibling-scale lookups Verified on unsloth/Qwen3.8-27B-NVFP4 (RTX 5090 D, 32 GB): weights resident at ~21.8 GB native (vs 54 GB bf16), CUDA graph capture, and live chat-completion requests all succeed; official dense-NVFP4 and routed-MoE layouts keep their native assumptions (sniffer fallbacks) and are unchanged. Tests: tests/models/test_qwen3_5_moe_config.py (8) + test_qwen3_5_moe_weight.py (13) -- storage sniffing, per-layer construction gates, fp8 native fusions, per-row/block scale dequant semantics.
…ported models unsloth's per-module mixed-precision dense exports (NVFP4 MLP + FP8 attention/GDN/lm_head + bf16 residual parts) load natively end-to-end; list the known-good checkpoint and document the layout.
No behavior change; the mixed-NVFP4+FP8 load path (unsloth/Qwen3.8-27B-NVFP4) stays byte-identical: - _pt_fp8_fuse: rename the 'scalar' param to 'scale' (it now accepts modelopt scalars AND unsloth's per-row [O, 1] scales) and fix the sloppy return annotation (bare 'list' -> list[tuple[str, torch.Tensor]] | None) - _per_row_scale: fail loud with a clear message if a non-scalar scale has the wrong element count instead of a cryptic reshape error - document the unscaled-fp8 fallthrough assumption in the .weight handler (an unscaled fp8 q/k/v or in_proj_qkv/z in the fp8-split layout would fail at load with a missing key, not silently dequant -- no real export has this) - weight tests: refresh the module docstring (native W8A16 + dequant, not dequant-only) and pin the local fp8-fusion-map extension (dense-MLP gate/up -> gate_up_proj native, per-row fp32 scales) with a unit test Verified: config tests 8/8, weight tests 14/14, parse_config on the real checkpoint, full-pass key/shape/dtype check (0 stray / 0 missing / 0 never-yielded) all green on the remote (RTX 5090 D, editable install). Co-Authored-By: GooeyPi <gpt-5.2@openai.com>
e8bca65 to
71bf149
Compare
|
maybe fix #238 |
|
Second data point for this PR, on a different variant of the same family. Checkpoint: a third-party Qwen3.8-27B multimodal NVFP4 export — Env: freetoken Both failure modes you describe reproduce:
because Exact per-layer split, since the description says "most dense-MLP layers as NVFP4, a few MLP layers as FP8":
232 FP8 targets / 168 NVFP4. FP8 One variant difference worth handling: here On footprint: dequantizing the 232 FP8 tensors takes them from 8.71 GiB to 17.42 GiB, giving a 30.06 GiB FTW from a 23 GiB source. Consistent with your 21.8 vs ~54 GB figures. I can't run this one — 16 GB card, and 21.8 GiB resident won't fit — so I can't verify generation. Happy to test conversion and load-path behaviour on this variant if useful. |
|
Follow-up to my comment above — I applied this PR's sources onto the released build (dropping the branch Working:
The gap: the
With
A concrete fix, tested. The FTW already carries what is needed: So I ran exactly that as a monkeypatch over this branch, and a converted FTW then loads and generates correctly with no other changes. The alternative -- persisting the resolved Not a blocker for Environment note in case it is useful: this ran on a 16 GB card, which is under the ~21.4 GiB resident. It works on Windows only because WDDM spills into shared system memory, at roughly 1.3 tok/s, with |
Problem
Mixed-precision compressed-tensors exports (e.g.
unsloth/Qwen3.8-27B-NVFP4) cannot load: the dense pass assumes either pure NVFP4 (weight_packed) or pure bf16, but unsloth's layout stores attention linears as weight-only FP8 (per-row scale), most dense-MLP layers as NVFP4, a few MLP layers as FP8, andlm_headas FP8. Raw e4m3 tensors die in the bf16 fusion (torch.catfp8/bf16 promotion), and bf16 dequant oflm_head/MLP doubles memory.Fix (per-module native storage — no new kernels)
Reuses the existing
Fp8PerTensor*kernels; no shared kernel / modelopt-pass changes:models/config.py:dense_mlp_storageper-layer override onModelConfigmodels/loader.py:ShardReader.has()for scale-sibling detectionqwen3_5_moe/config.py:_compressed_linear_storagesniffs the safetensors indexweight_map(order-independent per-layer classification:weight_packed→ nvfp4, scaled → fp8, plain → bf16) + FP8lm_headdetectionqwen3_5_moe/moe.py+model.py: overridden layers build shared-expert / dense-MLP /lm_headlinears as native W8A16qwen3_5_moe/weight.py: dense pass keeps scaled FP8 linears native (fused q/k/v →qkv_proj, in_proj_qkv/z →in_proj_qkvz, gate/up →gate_up_proj, singletons o_proj / GDN out_proj / down_proj / lm_head with per-row fp32 scales); unscaled fp8 dequantizes to bf16 (per-tensor / per-row / block scale). Official pure-NVFP4 checkpoints are unaffected (pass is a no-op there).docs/models.md: addunsloth/Qwen3.8-27B-NVFP4to the known-good listVerification (RTX 5090 D 32GB, driver 595.84, torch 2.11 cu130)
torch.catfp8/bf16 promotion crash on the mixed layoutft servee2e: 21.8 GB load, KV 16K, CUDA graph capture,/healthok, real chat generationUsage note: on a 32 GB card use
ft serve --model unsloth/Qwen3.8-27B-NVFP4 --num-tokens 32768 --max-prefill-length 1024(the large-vocab logits buffer OOMs at the default 8192-token prefill).