Repository navigation
fix(quant)!: keep nvfp4 global scales in fp32 - #739
Merged
Merged
Conversation
NVFP4 weight global scales (amax / 2688, 2e-5 to 4e-4 in the served checkpoints) sit at fp16's normal minimum; stored as fp16, up to 97% of a checkpoint's expert globals were subnormal, with up to 1.7e-3 relative error per row. BREAKING CHANGE: an FTW checkpoint that stores NVFP4 global scales as fp16 is refused at load; reconvert it with `ft checkpoint`.
jason-fxz
force-pushed
the
fix/nvfp4-global-fp32
branch
from
October 19, 2026 08:56
72dcb0b to
28fe6e9
Compare
pretor
pushed a commit
to pretor/FreeToken
that referenced
this pull request
Oct 19, 2026
…2) with backward-compat FTW support
|
Thanks for this fix. We had found the same fp16 cast downstream and carried an fp32 patch, so here are independent
|
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.
NVFP4 weight global scales (ModelOpt
weight_scale_2, the reciprocal of compressed-tensorsweight_global_scale) are fp32 scalars in the checkpoint, but FreeToken converted them to fp16 when it expanded them to one value per output row. A global is amax / (6 x 448), 2e-5 to 4e-4 in the checkpoints we serve, right at fp16's normal minimum (6.1e-5), so many of them became fp16 subnormals with only a few significant bits left. This PR keeps them in fp32 everywhere; the per-row layout stays as it is (gate and up, or fused dense parts, can carry different globals).Changes
models/nvfp4_banks.py), the per-row expansion (global_rows) and the Triton kernel's banks (gate_up_global,down_global) are fp32. Marlin and b12x fold the globals into their own alphas at pack time; they now start from the checkpoint values.weight_globalbuffer and the dense readers (qwen3_5_moe, glm4_moe, gemma4, compressed-tensors inmodels/loader.py) are fp32.kernel/aot_models.pyfor the prebuiltfast_index_copyspecs, the per-expert estimate inmoe/offload_cache.py) and theft bench bwdummy banks count 4 bytes per global.Breaking: an FTW checkpoint that stores NVFP4 global scales as fp16 is refused at load with an error that asks to reconvert it with
ft checkpoint.scripts/ftw_hotfix.pyreports the same problem and does not try to repair it (the fp16 rounding cannot be undone).docs/models.mdanddocs/ftw-hotfix.mdsay so.Results
fp16 rounding of the weight globals in local checkpoints (every global tensor of the model, one value each; the error is a fixed per-row scale error of every output of that row):
No global is below 1e-6 or rounds to zero in these checkpoints, so this is a precision fix, not a crash fix; a global below 6e-8 would have zeroed its rows.
Speed: the same kernels with fp16 vs fp32 globals (random banks, Qwen3.6-35B-A3B and GLM-4.7 shapes,
triton.testing.do_benchmedian, two runs), RTX PRO 6000: decode (T = 1, 8) 1.000 - 1.012x, prefill (T = 4096, 16384) 0.984 - 1.013x. The global banks grow from 0.12% to 0.24% of the expert bytes.Serving (greedy, 96 tokens): Qwen3.6-35B-A3B-NVFP4 with GPU offload and with hybrid CPU decode give coherent output that departs from main after a few lines; Qwen3.6-27B-NVFP4 and Muse-Glimmer-30B-NVFP4 give the same tokens as main.
Tests
pytest tests/moe tests/kernels tests/checkpoint tests/models tests/engineon the RTX PRO 6000: 1073 passed, 122 skipped, 27 failed; all 27 also fail on main here (19 intests/kernels/test_ggml.py, 5 intests/models/test_quant_config.py,test_offload.py::test_graph_capture_reuses_warm_offload_cache_before_capture, 2 intests/models/qwen4_exp/test_weight_ckpt.py). The tests that build or check globals now use fp32, and the Qwen3.8-Flash-Next bank-size constants are updated (63.46 -> 63.63 GiB experts, 111.14 -> 111.32 GiB pinned).