Repository navigation
feat(gguf): serve gpt-oss gguf checkpoints on gpt_oss - #838
void-mckenzie wants to merge 2 commits into
Conversation
jason-fxz
left a comment
There was a problem hiding this comment.
Thanks for adding gpt-oss GGUF. Four things to fix before merge:
-
YaRN
truncate(models/gpt_oss/gguf.py): the HF gpt-oss config sets"truncate": false, and transformers and FreeToken's HF gpt-oss path both honor it. The GGUFrope_scalingleaves it out, so GGUF files get the truncated correction range (8..18 instead of 8.09..17.40). Frequency dims 9-17 then differ from the HF path, and the two rope tables are already up to 0.23 apart at position 1024. llama.cpp always truncates, so a parity check against it does not catch this. Please add"truncate": Falseas a family constant next to alpha / limit. -
bf16 cast of float experts (
models/gguf/experts.py,_bank_spec): the cast also hits mixed float/quant layers in every family, which main keeps in their own dtype since #702. HauhauCS Qwen3.6-35B Q8_K_P (13 layers of F16 gate_up + Q8_0 down) serves on main with--moe-strategy hybrid. With this PR it fails at startup withValueError: MoE layer 20 gate_up bank (256, 1024, 2048) torch.bfloat16 is not F16 rows of 2048 values, becausecpu_specstill sends those layers to theggufCPU format as F16. On GPU their F16 GEMM also drops to bf16. Please cast only when both gate_up and down are float (the layer runs the bf16 fused MoE) and keep mixed layers in their own dtype. If_expert_rowsstays a view andbanks[...].copy_(r)does the conversion,--load-format dummyalso stops reading the expert data (three F16 layers now peak at +7.9 GiB RSS, vs +3.9 GiB on main). -
NVFP4 down scale vs. down bias (
layers/quantization/moe/gguf.py, non-MMVQ down branch): abovemmvq_max_tokens,weight_scale_2is folded intotopk_weightsand the bias is then added to the unscaledy, so the bias is scaled too:sum w*s*(y + b)instead ofsum w*(s*y + b). No public gpt-oss file has NVFP4 experts yet, but with synthetic NVFP4 experts throughGgmlMoEKernel.apply, prefill (T=256) is 94% off the reference while decode (T=4) is within 0.4%. With a bias, scaleyin place instead; prefill then lands within 0.4% too:if down_scales is not None and down_bias is not None: # the bias adds after weight_scale_2, so the scale cannot fold into the routing weights y.mul_(down_scales[ids, 2].unsqueeze(-1)) elif down_scales is not None: topk_weights = topk_weights * down_scales[ids, 2]
-
w2_biaswithapply_router_weight_on_input(moe/fused.py, both impls): the guard checks onlyw1_bias, butw2_biasis always multiplied by the routing weight. In that mode both impls are 66% off your test reference, which adds it unweighted. Please make the assert cover both:# both bias adds assume the router weight scales the expert output assert (w1_bias is None and w2_bias is None) or not apply_router_weight_on_input, "per-expert biases need the router weight on the output"
Smaller cleanups:
OffloadMoeCache.set_bank_sourceswidens the gguf schema at runtime when bias roles show up. The loader already knows the file is biased when it names the format (experts.py,ExpertBanks("gguf_scaled" if scaled else "gguf", ...)), so registering the biased layouts in_BANK_SCHEMASkeeps it the single place a format's bank layout is declared._LAYER_LEAVES:mlp.gate_proj/up_proj/down_projandmlp.gatematch no gpt-oss module (no dense MLP; the router ismlp.router).- The
pattern is Nonebranch repeats the scalar branch with p = 2;md.get(key, 2)covers it.
Not for this PR: fusing the expert biases into the kernels (the [T, k, 2I] gathers on prefill, the MMVQ glu path on decode) and bias support in the CPU executor. We will follow up on those separately.
Serves llama.cpp
gpt-ossGGUFs (gpt-oss-20b, gpt-oss-120b) on the family's own model classes. Part of #671 (Model Support:gpt-oss). Rebased onto current main (e328cf6, clean).What it does
gpt_oss/gguf.py):parse_gguf_configfrom the GGUF KVs - yarn rope (factor 32, original 4096; the betas default to 32/1, which llama.cpp hardcodes for gpt-oss - its loader never reads the keys - when a file omits them; the unsloth UD and F16 releases omit them),attention.sliding_window, and the alternatingsliding_attention/full_attentionlayout from layer 0 (every release'slayer_types).attention.sliding_window.patternis read the way llama.cpp'sload_swa_patterndoes: the per-layer array verbatim, else the scalar'sil % p < p - 1, default 2 - no release writes the key, and a wrong-length array refuses. The swiglu's alpha 1.702 / limit 7 are family constants the GGUF does not store, as in llama.cpp. Attention sinks, router bias and q/k/v/o biases load with the dense state dict; the packed embedding takes the GGUF quant (model.pynow passesquant_configthrough, as the qwen3 model does). A GGUF missing any bias tensor llama.cpp requires (openai-moe.cppcreatesattn_output.bias,ffn_gate_inp.bias, the threeffn_*_exps.biasand a qkv bias as required tensors) refuses at config parse naming the file and tensors, instead of serving silently bias-free.swigluoai(the uninterleaved spelling ofgpt_oss_swiglu, same math; the ggmlswiglu_oaiepilogue and the cpu executor already know it) withinterleaved=False.ffn_{gate,up,down}_exps.bias). They ride the expert banks as two roles (gate_up_bias[E, 2I],down_bias[E, H] bf16), so each bias follows its expert through the slot cache on decode (where routing ids become slot ids) and the expert-ordered prefill/resident views. The ggml kernel adds gate/up before the gated activation and down before the routing weight, as llama.cpp'sbuild_moe_ffndoes; the NVFP4weight_scale_2multiply comes before the bias, as there too (no public gpt-oss file reaches that combination).bank_schemawidens for the two roles on the gguf formats. A file carrying the biases on only some layers refuses at bank load.cpu/hybriddecode refuses bias files (no CPU executor format), auto resolves to offload.cpu_specsends every float type to the bf16 executor format.Verification (RTX 5090 32 GB, driver 580.95.05, Ryzen 9 7950X3D, 187 GiB RAM)
UD-Q4_K_XL, ggml-orgMXFP4, unslothF16- 3/3 pass. tok_compare vsopenai/gpt-oss-20btokenizer.json: 0 differing texts (edges, full vocab, 20k combos) on the UD and MXFP4 files./v1/chat/completions: correct math (17*23+45/9 = 396 with steps) and SQL on each; harmony reasoning parses intoreasoning_content; 12 concurrent requests all complete; a repeated prompt hits the radix through the sliding/full mix (0 -> 268 cached; the SWA pool tracks both groups). UD-Q4_K_XL: prefill ~19.2k tok/s (11.8k prompt), decode 145 tok/s, experts offloaded (9.5 GiB host banks).kernel/triton/activation.py- 0.008 max rel err with the biases, 0.26 without (pinned by a test now).fetch_headers --search gpt-oss, 1,093 stubs / 128 repos, kernels probed on the 5090): 969gpt-osstrunks, 948 pass. Failures: 20 base/abliterated files with no chat template, 1 broken upload (notoken_embd). Nothing crashes and nothing mis-loads silently.tests/moe/test_gpt_oss.py,tests/moe/test_fused_moe.py,tests/models/test_models_registry.py,tests/models/test_models_loader.py- 60 passed, including a new per-expert-bias reference check over both fused impls (test_fused_moe.pyextends its existing reference helper with the swigluoai formula and bias params);tests/moe/test_offload.py(the rebased main's seams) passes too. No per-family GGUF test file, per the feat(gguf): serve GLM-5.3-Flash GGUF checkpoints on glm5-next #721 review direction.Not covered
gpt-oss-*-VISION*repos: mainlinegpt-osstext plus aninternvlprojector mmproj (clip.projector_type=internvl, 436v.*tensors) - a community graft no FreeToken tower implements; the text files of those repos serve.cpu/hybrid/--moe-cpu-layersfor gpt-oss files: the per-expert biases have no CPU executor format, refused at kernel selection.