Skip to content

qwen3_5_moe: load mixed-precision compressed-tensors checkpoints (unsloth NVFP4+FP8) - #208

Open
chrisqianz wants to merge 3 commits into
FlashML-org:mainfrom
chrisqianz:fix-qwen-mixed-fp8-weights
Open

qwen3_5_moe: load mixed-precision compressed-tensors checkpoints (unsloth NVFP4+FP8)#208
chrisqianz wants to merge 3 commits into
FlashML-org:mainfrom
chrisqianz:fix-qwen-mixed-fp8-weights

Conversation

@chrisqianz

Copy link
Copy Markdown

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, and lm_head as FP8. Raw e4m3 tensors die in the bf16 fusion (torch.cat fp8/bf16 promotion), and bf16 dequant of lm_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_storage per-layer override on ModelConfig
  • models/loader.py: ShardReader.has() for scale-sibling detection
  • qwen3_5_moe/config.py: _compressed_linear_storage sniffs the safetensors index weight_map (order-independent per-layer classification: weight_packed → nvfp4, scaled → fp8, plain → bf16) + FP8 lm_head detection
  • qwen3_5_moe/moe.py + model.py: overridden layers build shared-expert / dense-MLP / lm_head linears as native W8A16
  • qwen3_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: add unsloth/Qwen3.8-27B-NVFP4 to the known-good list

Verification (RTX 5090 D 32GB, driver 595.84, torch 2.11 cu130)

  • Before: torch.cat fp8/bf16 promotion crash on the mixed layout
  • After: native resident 21.809 GiB (vs ~54 GB if all dequantized to bf16)
  • config tests 8/8, weight tests 14/14
  • full-pass key/shape/dtype vs model state dict: 0 mismatch / 0 stray / 0 missing
  • ft serve e2e: 21.8 GB load, KV 16K, CUDA graph capture, /health ok, real chat generation

Usage 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).

chrisqianz and others added 3 commits August 26, 2026 13:43
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant