feat(qwen3_5_moe): read every checkpoint layout through the QuantConfig - #438
Conversation
NVFP4 dense layers now declare the input_scale their scheme carries, so an FTW written by an older build from an NVFP4 dense checkpoint needs scripts/ftw_hotfix.py before it loads again.
|
Ran #438 on sm_89 with MoE offload, since the table in the PR is H100. It works out of the box, RTX 4080 SUPER, 16 GiB, sm_89, TP=1,
The 8192-token prefill is the canary I care about on a 16 GiB card, because that is where the GDN chunked-prefill workspace has OOM'd before (#401). 8/8 at 3648 tok/s is the best I have recorded for this checkpoint. This is now my serving default rather than a one-off test, so it has continuous use on sm_89. At the model's full 262,144-token context it measures 122.4 tok/s decode and 3398 tok/s prefill at 8k, 8/8, which is at or slightly above what I was getting before this branch. I confirmed your point 1 directly rather than taking it on trust. You are right that the legacy detectors were never protecting those three. I had proposed keeping them as a fallback and that was wrong: they only ever loaded for me because of local int4 patches, not because of anything on Two findings from prototyping the same migration before you posted, in case either is useful. The Separately, the One more datapoint on this as a base to build on: I carry a per-tensor fp8 KV change of my own, and rebasing it onto this branch needed hand-work on only two hunks, both in #415 is closed, since this carries it. Thanks for crediting the |
…4 cross-shard scales, ct block-fp8 experts
…QuantConfig scheme reader This stack was cut at 86214a9 (FlashML-org#375). Upstream has since replaced the qwen3_5_moe dense reader with one road through the QuantConfig (FlashML-org#438, squashed at 0ffd5c8), so the vision work has to sit on top of that reader rather than the old cascade. Three files conflicted, and all three resolve the same way: take upstream's side whole, then put the vision line back. config.py: the quant-detection cascade collapses into _expert_quant(hf_config, text); the vision_config block above it stays, and vision_config=vision_cfg still reaches ModelConfig. The result is byte-identical to try/all's. model.py: ParallelLMHead now takes quant_config=config.quant instead of the lm_head_quant special case. Take that, keep `self.visual = ... if config.is_multimodal else None`. weight.py: took origin/main's file and re-applied the vision delta -- _rename regains include_vision, threaded from config.is_multimodal through _iter_shards, and visual.* is yielded straight rather than through the reader. That delta is byte-identical to the one on try/all: this branch and try/all had the same vision threading in weight.py before the merge (git diff over the vision lines is empty). Assisted-by: Claude Opus 5
Upstream FlashML-org#418/FlashML-org#427/FlashML-org#438 moved expert quantization into MoEMethod/MoEKernel (layers/quantization/moe): bf16, NVFP4 and fp8 experts are packed by build_expert_banks and applied through quant_method.apply; only GGUF still loads through a format provider. --moe-backend became --moe-strategy and EngineConfig.moe_backend is folded into moe_strategy and left None. Resolution keeps every feature of this branch on top of that design: - engine: take upstream's shared_offload_method / _check_pin_budget / PinFailed / layout+max_slots path; keep the resident tier, worker executors, in-place worker layers, shared banks and decode-frequency stats. load_expert_banks is called once, inside shared_banks(), with both method= and resident_layers=. Our helpers read moe_strategy. - expert_banks: upstream build_expert_banks plus ResidentUploader; the resident upload rides host_banks' PinPipeline sink, which upstream's builder already drives. The bf16/nvfp4/ds_fp4 providers are gone as upstream intended; one GGUF provider serves every native ggml quant. - layers/moe: the format-tag dispatch keeps only the GGUF branch; the split/worker decode path already goes through _expert_gemm and so through quant_method.apply. - models/weight: load_gguf_moe_expert_sources, with upstream's load_q4_0_moe_expert_sources kept as an alias. - moe/fused_q4_0: add upstream's fused_experts_gguf_q4_0 entry point. - moe/_worker_main: take NVFP4 banks by kernel role (gate_up, down, ...), falling back to the FTW names. - tests: fake configs in test_resident / test_worker_placement use moe_strategy. Checked on tm (ROCm 7.2, torch 2.11): no undefined names in the 180 changed files; tests/engine + tests/moe show no failure that is not also present on the pre-merge branch or on pure upstream main in the same environment (51 failed / 285 passed; test_nvfp4_moe_vec.py and test_warp_width.py excluded -- both segfault identically before the merge). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tt8JH1wvkbStVdTSzHNPE
What
qwen3_5_moepicked one of four weight readers by string flags (attn_quant,dense_quant,lm_head_quant) that its config derived from the HFquantization_configon its own. Since #418 every layer builds its buffers fromconfig.quant.scheme_for(prefix), so the flags and the readers disagreed with the model on every compressed-tensors export (#238, #263, #344, #381, #410, #437, and the PRs #208, #275, #296, #390, #413, #415).One reader now asks the same
scheme_forfor every Linear module and fills exactly the buffers the model declared:qwen3_5_moe/weight.py: one_DenseReaderreplaces the four readers and their fusion tables. Tensors are matched to the roles of the module's scheme, packed projections are concatenated per role, dtypes and scale shapes are checked against the scheme, per-tensor scales are broadcast per row (the shape handling from fix(qwen3_5_moe): accept a per-channel fp8 weight_scale in the dense loader #415). A module the config calls bf16 but whose tensors are quantized is rejected; a module the family serves bf16 (the routers) is dequantized.qwen3_5_moe/config.py: the string detectors are gone;expert_quantcomes from theQuantConfig, as inqwen4_exp.STORAGE, the checkpoint tensor behind every role of every kind it exports (weight_packed/weight_global_scalefor llm-compressor,weight/weight_scale_2for ModelOpt), with the reciprocal convention on the entry. The NVFP4 expert reader spec is derived from it.ct_set: a compressed-tensorstargets/ignorename covers that module alone, not its children, as in llm-compressor and vLLM. Exports list every unquantized module including containers (layers.N.linear_attn,mlp.experts.N), and the old ancestor match turned the quantized projections under them into bf16. Target class names other thanLinearfail closed.Nvfp4LinearMethoddeclaresinput_scalewhen the scheme carries it, so W4A4 checkpoints load in full; no kernel reads it yet. The gemma4, glm4_moe and muse_glimmer readers load it too.loader.safetensors_weight_mapreads the index or, without one, the shard headers; it replaces the hard index read innvfp4_banks.pythat failed on single-file checkpoints.glm5_nextkeeps its own copy of_fp8_block_quantinstead of importing it fromqwen3_5_moe.Compatibility
An FTW written by an older build from an NVFP4 dense checkpoint (for example nvidia/Qwen3.6-27B-NVFP4, nvidia/Qwen3.6-35B-A3B-NVFP4, RadixArk/Qwen3.8-27B-NVFP4, nvidia/Gemma-4-31B-IT-NVFP4, nvidia/GLM-4.7-NVFP4) lacks the
input_scaletensors the model now declares and fails with aKeyError.scripts/ftw_hotfix.py --ftw <dir> --source <hf dir>adds them.Tested
H100 80 GB, driver 580.95, CUDA 13.0, torch 2.11, on top of
main@ fb7f732.tests/e2e/test_aime.py(AIME25 problem 1, pass@3) withFREETOKEN_TEST_MODEL=<dir>, plusFREETOKEN_TEST_MOE_CACHE_AUTO=1for MoE.main:KeyError in_proj_qkvz)KeyError ... input_scaleas expected; pass afterftw_hotfix.py --sourcemainpytest tests/models tests/moe/test_offload.py tests/engine/test_cache_budget.py: 255 passed.Closes #238, closes #344, closes #381. Implements the migration proposed in #437, left open for the int4 discussion. Addresses the compressed-tensors NVFP4 side of #263 (sakamakismile/Huihui-Qwen3.6-35B-A3B-abliterated-NVFP4; the joshebbs export with the nested ModelOpt config is not covered) and of #410 (the sahilchachra, sakamakismile and Ttimms KAT-Coder exports have regular configs; doth4580's format-only config is not covered). Not #252: its ornith-ai/Ornith-1.5-35B-A3B-FP8 quantizes the routed experts per-channel fp8 too, and there is no fp8 per-tensor MoE method yet. Supersedes #208, #275, #296, #390, #415 (its per-channel scale handling is included) and the reader and detection parts of #413 (its KAT-Coder handling is not included).