[Models] Support the Muse Glimmer dflash draft model in GGUF - #115
Open
WhatGhost wants to merge 2 commits into
Open
[Models] Support the Muse Glimmer dflash draft model in GGUF#115WhatGhost wants to merge 2 commits into
WhatGhost wants to merge 2 commits into
Conversation
Converting this architecture to GGUF is not a pure requantization. Four things change on the way in, and each one loads without complaint and produces fluent but wrong output if it is not undone: the Q/K rows are re-laid out from the half-split NEOX order into llama.cpp's interleaved order, the per-layer norms have the architecture's `1 +` folded into the stored weight, the Q/K norms are synthesized from the config's scale factor rather than stored as learned parameters, and the vision patch embedding is reduced to the sum of its per-time-step blocks. The adapter reverses the first three exactly. The Q/K permutation is applied to the packed bytes directly, since GGUF splits super-blocks along the input dimension and so leaves each output row self-contained. The fourth is exact for still images, which depend on the sum alone because the encoder expands one patch to every time step. Video depends on the blocks individually and cannot be recovered, so it is declared unsupported and rejected during input validation rather than served from a reconstruction that is off by about 7% in the channel carrying motion. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: whatghost <yuyang.gao@amd.com>
WhatGhost
force-pushed
the
muse-glimmer-dflash-gguf
branch
from
August 20, 2026 08:02
88e4a0c to
fb44a7a
Compare
Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: whatghost <yuyang.gao@amd.com>
WhatGhost
force-pushed
the
muse-glimmer-dflash-gguf
branch
from
August 20, 2026 09:23
fb44a7a to
7e9ecd4
Compare
This was referenced Aug 21, 2026
Open
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.
Support the Muse Glimmer dflash draft model in GGUF
Stacked on #113 — the first commit belongs to that PR; the dflash change is the second commit
7e9ecd4alone. I'll rebase onto main once #113 lands.Summary
#113 left GGUF speculative decoding blocked before any adapter is consulted. This unblocks it and adds the Muse Glimmer dflash draft, so the Q4_K_XL backbone can be served with its 2.56B assistant head drafting for it.
The draft is the opposite of the backbone: none of the four conversions #113 undoes applies here. Its Q/K rows are already in NEOX order, its norms carry no folded offset, and its Q/K norms are learned rather than synthesized. Reusing the backbone's rules would rewrite correct weights and nothing would say so — the target verifies every token, so the output stays fluent while the draft's proposals quietly stop being accepted. The name map is therefore the whole conversion, and several tests assert that a transformation did not happen.
The three blockers
A draft has no
hf_config_path. The target gets one for free:create_model_configrewritesmodelto the config source and keeps the file inmodel_weights. A draft's.ggufpath reachesModelConfigintact, so its config is looked for in the file's own directory — which, for a draft shipped next to the target it drafts for, holds the target's config.speculative_confignow acceptshf_config_path, and a directory without aconfig.jsonfails with an explanation instead ofUnrecognized model. The redirect is idempotent:create_speculative_configruns twice, and without remembering the original path the second pass would keep the config directory as the weights source and silently load the unquantized checkpoint sitting there.EAGLEConfighides the model type. A dflash draft is wrapped on its way into the engine, reportingmodel_type == "eagle"with the real config on.model, and the wrap happens after the config parser runs. Soarchitecture()sees the bare type while the loader sees the wrapped one, and an adapter matching only on the bare type stops matching exactly when the weights are about to be mapped — the fallback adapter then fails on an architecture it has never heard of.The fused KV buffer needs dense weights. The head fuses every layer's KV projection at the end of loading and reads
qkv_proj.weight, which a quantized layer does not have. Only Q/K/V are unpacked, about a sixth of the draft, so it still loads at roughly a third of its unquantized size. Building the buffer from packed bytes instead is not an option anyway: K is Q4_K and V is Q6_K, so their rows differ in width.Supporting changes
Declarations reach a draft through the config dict. A target's layers are built against the very
GGUFConfigthe loader extends; a draft's are not, since it rebuilds its own fromhf_config.quantization_config. Everything the loader records on the shared object therefore reaches the target and never the draft. The loader now writes the declaration into that dict as well, andGGUFConfig.from_configreads it back.Exemptions can be declared by suffix.
unquantized_modulescannot express "wherever this occurs": a fused layer matches by asking whether a declared name contains the layer's full runtime path, so every declaration spells out a prefix and a layer index. Those are knowable for a target and not for a draft, whose layers vLLM numbers after the target's — the draft's five appear asmodel.layers.62..66.dense_module_suffixesdrops that coupling.ReplicatedLineargets a GGUF loader. It is the one linear layer with noweight_loader_v2, and its v1 loader asserts the parameter already has the loaded weight's shape. GGUF parameters start empty and take their shape from the packed bytes, so the draft'sfclayer tripped that assertion on its first tensor.Testing
46 synthetic tests in
test_muse_glimmer_dflash_gguf.py, none needing a checkpoint. They pin the name map as a bijection, the match through the EAGLE wrapper, the Q/K/V unpacking and the packed bytes everything else keeps, and — as negative controls — that the norms, the final norm, the Q/K norms and the Q/K row order all come through untouched. The rest covers the two structural differences that each produced a silent-wrong-weights bug while this was written: a declaration surviving the rebuild from the config dict, and a suffix declaration surviving the layer renumbering.End-to-end against the real 30B, GGUF target with GGUF draft, 3 speculative tokens: 60.8% acceptance at a mean accepted length of 2.82, against a BF16 baseline of 51.0–53.2% at 2.53 over the same prompts. Per position, 83.5/60.4/38.5% against 72.5/47.1/33.3%.
Also checked against the real weights outside CI: the name map is an exact 58↔58 bijection onto the assistant checkpoint, and the tensors are a plain requantization of it — no row permutation, no folded norm offset, and Q/K norms that differ from what the backbone's scale factor would synthesize.
Known limitations
A draft's quantization config is resolved by a path GGUF cannot take.
get_quant_configreadshf_config.quantization_configand falls back tohf_overrides; a GGUF file has neither, because vLLM always hands a draft a callablehf_overridesso config transforms applied to the target reach it too, and the fallback rejects anything that is not a dict. That rejection is the whole obstacle, and it looks like an oversight: the two keys read from hf_overrides there can only exist on a dict, so a callable means "neither key is present" rather than something being wrong, and raising makes the file-based lookup below it unreachable for every draft. This PR plants a {"quant_method": "gguf"} marker to select the first branch instead; it comes out once that check stops raising upstream.Q/K/V are dequantized rather than fused while packed. Keeping them packed needs
_build_fused_kv_buffersto accept quantized layers, which would cost the fused-GEMM fast path for every model that uses it.