Repository navigation
fix(qwen3_5_moe)!: apply the zero-centred norm weights' +1 in fp32 at runtime - #755
Merged
Merged
Conversation
… runtime BREAKING CHANGE: an FTW of Qwen3.5 / Qwen3.6 / Qwen3.8-27B converted by an older build stores the norms with the 1 added and gives wrong output; reconvert it with `ft checkpoint`.
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Oct 20, 2026
- hybrid fetch split: upstream per-layer-kind bench rows (FlashML-org#734) first, our startup split now measured per layer kind without a row (before the CPU executor), then cap 1; dropped the profile-driven auto->hybrid pick and its cache-share guard with upstream - qwen3_5_moe: upstream fp32 runtime norm +1 (FlashML-org#755) under our DFlash hidden-layer return and LinearOProj - qwen4_exp: upstream quant-config reader and MXFP8 scale aliases (FlashML-org#759) replace our .weight_scale rename; our validating scan_ple_table and NVFP4 PLE tables gain the bf16 table support - nvfp4_banks: upstream W4A4 input scales (FlashML-org#736) combined with our TP sharding and disk-tier skip - ftw: fp16 nvfp4 global refusal (FlashML-org#739) on top of our eager index validation - kernel/nvfp4_moe: drop the e2m1 LUT argument our bit-placement decode removed - benchbw: upstream layer-kind rows plus our THP bench banks, decode-shaped gather and custom profile hint - tests: disk-tier spec test skips the quant-config-driven qwen4_exp hook Assisted-by: Claude Opus 5.5
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #747 (thanks @SUKUNA-AI for the analysis).
Qwen3.5 / Qwen3.6 / Qwen3.8-27B (the
qwen3_5_moefamily, dense variants included) store zero-centred RMSNorm weights: the multiplier is1 + w, which transformers computes asoutput * (1.0 + weight.float()), as do vLLM (GemmaRMSNorm) and SGLang (gemma_rmsnorm/gemma_fused_add_rmsnorm). FreeToken folded the 1 into the bf16 weight at load time, so it multiplied bybf16(1 + w). This PR keepswand adds the 1 in fp32 inside the norm kernel, for the input / post-attention / q / k / final norms. The GDN gated norm (linear_attn.norm) is a plainw * xnorm and is unchanged.Changes
models/qwen3_5_moe/weight.py: the safetensors loader yields the norm weights as stored (drops the+ 1.0fold).models/qwen3_5_moe/gguf.py: llama.cpp's converter stores these norms as F32w + 1; the GGUF loader now subtracts the 1 in fp32, as theqwen4expGGUF loader already does. Without this the GGUF path would multiply by2 + w. The GGUF path had the same rounding before: its F321 + wwas cast to the bf16 norm parameter.models/qwen3_5_moe/model.py,attention.py: the norms are the existingGemmaPlusOneRMSNormFused/GemmaPlusOneRMSNorm(flashinfergemma_rmsnorm/gemma_fused_add_rmsnorm, Triton fallback), the classes Qwen3.8-Flash-Next, Muse-Glimmer and MiniMax-M3 use.docs/models.md,docs/ftw-hotfix.md: old FTWs of this family must be reconverted.Breaking: an FTW of Qwen3.5 / Qwen3.6 / Qwen3.8-27B converted by an older build stores the norms with the 1 already added. It loads without an error but gives wrong output (the multiplier becomes
2 + w); reconvert it withft checkpoint. A converted FTW cannot be told apart by its values (the zero-centred weights of one checkpoint have per-tensor means from -0.27 to 1.63), so it is not detected.Results
Weights (every input / post-attention / q / k / final norm tensor): share of multipliers where
bf16(1 + w) != 1 + w, and the relative error of the folded value:1 + wcast to bf16)After the fix the stored value is
witself: the bf16 checkpoint value, and on GGUF(1 + w) - 1in fp32, which equals the bf16 checkpointwbitwise (checked on Qwen3.6-35B-A3B's output / attention / q norms).Norm op against an fp32 reference of
x * rsqrt(mean(x^2) + eps) * (1 + w)(bf16 inputs, H = 2048 and 5120, per-head q norm D = 256): the new norms give the correctly rounded bf16 result on every element (plain, fused add, Triton fallback, per-head); the folded weight missed on about 25% of the elements, by up to 3.1e-2. Same speed as the sgl_kernel norms they replace (CUDA graph): decode fused add 1.35 vs 1.37 us, 8192 x 2048 prefill fused add 23.8 vs 23.9 us, q norm over 8192 rows x 24 heads 130 vs 128 us.Model, Qwen3.6-27B bf16, last-position logits of 14 prompts (64 - 480 tokens) against transformers 5.18 bf16, RTX PRO 6000:
The PR is closer to transformers on 10 of the 14 prompts. The gain is small: KL(main || PR) is 2.6e-4, about the distance of either to transformers, so other bf16 differences in the stack dominate. No PPL or eval was run.
Serving (greedy, 96 tokens): Qwen3.5-0.8B GGUF, Qwen3.6-35B-A3B NVFP4 (GPU offload) and Qwen3.6-35B-A3B GGUF (IQ3_XXS) give coherent text with the same opening as main.
Tests
pytest tests/models/test_qwen3_5_moe_weight.py tests/models/test_quant_config.py tests/models/test_models_registry.py tests/checkpoint tests/models/qwen4_exp tests/layerson the RTX PRO 6000: 307 passed, 150 skipped, 5 failed; the 5 (tests/models/test_quant_config.py, from the local HF cache) also fail on main. The loader test now expects the norm weights as stored.