Repository navigation
feat(gguf): apply the weight scales of nvfp4 gguf tensors - #732
Merged
Merged
Conversation
23 of 42 tasks
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.
Part of #671.
Summary
llama.cpp's converter turns a ModelOpt NVFP4 checkpoint into GGUF by repacking each NVFP4 tensor and writing its second-level scale (
weight_scale_2) as a separate fp32 tensor<name>.scale: one value for a dense tensor, one per expert forffn_{gate,up,down}_exps. llama.cpp multiplies every NVFP4 matmul result by it. FreeToken did not read these tensors: Gemma-4 26B-A4B NVFP4 GGUF loaded but generated only<pad>, the Qwen and Llama loaders refused such files, and the qwen4exp loader loaded them without the scales.This PR applies the scales on every path that computes NVFP4 GGUF weights (GPU dense linears, GPU MoE experts with
fused,offloadandhybrid, CPU MoE experts withhybridandcpu) and lets the gemma4, llama, qwen3, qwen3moe, qwen35 and qwen35moe loaders read them. On real NVFP4 GGUFs, greedy decode matches FreeToken's HF NVFP4 path token for token on Gemma-4 26B-A4B and Qwen3.6-35B-A3B, and on bothoffload,hybridandcpugive the same tokens asfused.Design
The scale multiplies a matmul result, so it goes where llama.cpp puts it (
build_lora_mm,build_moe_ffn):ggml_scale, one fp32 value per output row (each part's scale, 1 for parts without one), and the linear multiplies its result by it.globalbank,[E, 4]fp32 = (gate, up, down, 1), next togate_upanddown. It is copied into the slot with the expert, sooffloadandhybridread it like the weights. The vector kernel applies the three scales in its fp32 epilogue (the vendored ggml MMVQ now reads strided scale vectors, i.e. columns of the bank); the matrix kernel path multiplies afterwards.gguf_scaledCPU MoE format is the GGUF format of feat(cpu-moe): serve GGUF experts on the cpu and hybrid backends through ggml-cpu #704 plus theglobalbank: gate and up rows are scaled before the GLU, down rows before the routing weight.per_expert_scaleinto the down experts'.scale, so the router uses ones when the down experts carry a scale.check_scales()accepts a.scaleor.input_scaleonly next to an NVFP4 weight (.input_scalestays unused, as in llama.cpp) and refuses any other scale tensor. gemma4, llama, qwen3, qwen3_moe and qwen3_5_moe use it; qwen4exp now refuses every.scale; qwen2, minimax-m2, minimax-m3 and muse-glimmer keep refusing them.--dtype float16with a file whose NVFP4 tensors carry scales (results are stored in the activation dtype before the scale, which is about 1e-4, multiplies them, so fp16 overflows), as for MXFP8; per-expert scales on a mergedffn_gate_up_expsNVFP4 tensor (llama.cpp drops them too).Tested on
NVIDIA H100 80GB HBM3, Intel Xeon Platinum 8480+, driver 580.95.05, CUDA 13.0, torch 2.14.1+cu130, Triton 3.8.0.
mainis 11c1adb, the base of this branch.GGUFs: nvidia/Gemma-4-26B-A4B-NVFP4-GGUF as published; the others converted from the ModelOpt NVFP4 checkpoints below with llama.cpp e937b17a2, non-NVFP4 tensors kept in BF16:
Results
Greedy decode of AIME25 problem 1 through the chat template, 256 tokens (
LLM.generate, temperature 0, CUDA graphs). Each cell is the number of tokens identical to the reference before the first difference, out of 256. The llama.cpp reference isllama-serverf3363593f on the same GGUF (-ngl 999,/completionwith the same prompt token ids, temperature 0, top-k 1).hybridruns a slot cache of twice the expert count with--moe-hybrid-max-fetch 0, so most routes go to the CPU.fusedoffload/hybrid/cpuvsfusedchunk expects at least a 1-dimensional tensor), so they are compared with llama.cpp only.Other checks
gguf_scaledon real NVFP4 experts (Gemma-4 26B-A4B layer 3, 16 experts, gelu_tanh and silu, batch 1, 3 and 8) against an fp32 reference built from the dequantized rows and the scales: max |error| / max |reference| is 4.5e-3 to 1.0e-2. Theggufformat, which reads no scales, gives NaN on the same rows.fusedandhybridoutputs are identical to main, 256 of 256 tokens.check_scales()accepts the local gemma4 MXFP4 and BF16 GGUFs and the Qwen3.8-Flash-Next IQ3_XXS GGUF, which carry no NVFP4 scales.--dtype float16with Gemma-4 26B-A4B NVFP4 GGUF is refused at startup.PYTHONPATH=python pytest tests/kernels/test_ggml.py tests/models tests/moe/test_cpu_moe.py tests/moe/test_mixed_expert_formats.py tests/moe/test_offload.py tests/server/test_parser_auto_selection.py tests/layers -m "not slow" --deselect tests/moe/test_offload.py::test_graph_capture_reuses_warm_offload_cache_before_capture: 477 passed, 179 skipped, 1 failed (theapodex/Apodex-1.1-mini-NVFP4case oftest_quant_config.py). The deselected test and that case also fail on main.Not covered
.scaletensors:Can not map tensor 'model.layers.0.block_sparse_moe.experts.0.w1.weight'); its NVFP4 expert path expectsgate_proj/up_proj/down_projnames.attn_q_norm, so such a file does not load in llama.cpp either.token_embdis still refused: the vendoredget_rowshas no NVFP4 case.FREETOKEN_FUSED_COPY=0, a scaled GGUF fails at startup: the per-bank copy needs each bank's slot size to be a multiple of 128 bytes, and aglobalslot is 16 bytes. With--moe-prefill-hit-d2d,globalis copied as one small batch entry.