Skip to content

convert: read GGUF (any ggml quant level) as a conversion source - #282

Open
giveen wants to merge 1 commit into
Neroued:masterfrom
giveen:gguf-source-conversion
Open

giveen wants to merge 1 commit into
Neroued:masterfrom
giveen:gguf-source-conversion

Conversation

@giveen

@giveen giveen commented Sep 18, 2026

Copy link
Copy Markdown

Summary

  • Adds tools/convert/sources/gguf.py: GGUFSource/HFAliasSource let a recipe source some or all logical parameters from a GGUF file (F16 through low-bit ggml/I-quants) instead of the full Safetensors checkpoint, via the existing "Read another source" recipe mechanism — no changes needed to model.py/methods.py/recipe.py.
  • Covers Qwen3.5's hybrid GDN layers (qwen35_linear_attention_name_map), the MTP extra layer (qwen35_mtp_name_map), and the vision tower + qwen3vl_merger projector exported as a separate mmproj-*.gguf (qwen35_vision_name_map), plus a generic standard_dense_name_map for ordinary attention/MLP/norm tensors. DFlash2 needs no new code — it already reads natively via the existing --source dflash2=PATH Safetensors path.
  • Every name/shape/reorder map was checked end-to-end against real checkpoint/GGUF pairs (dequantized GGUF values compared back to the original HF tensors, not just names/shapes) before landing. That caught two wrong assumptions along the way: the GDN head reorder needs swapped arguments to invert (calling it twice with the same arguments does not round-trip except in the square case), and llama.cpp's MTP export does not apply the same norm.weight + 1 shift the base model's layers get.
  • Fixes GGUFSource to decode only the GGUF-native row range a read actually needs, instead of dequantizing the whole tensor on every call. GGUF's raw tensor data is already row-structured (ReaderTensor.data.shape[0] always matches the tensor's outer dimension), so this is exact, not approximate. recipe.prepare()'s 1-element preflight probe was fully dequantizing every tensor once for the probe and once again for the real write; this was roughly doubling conversion time and is now near-free (a 1.27B-element tensor's preflight-style read went from ~3.5s to ~0.0002s in testing).
  • tools/convert/__main__.py: --source NAME=path.gguf now opens as a GGUFSource automatically (suffix-detected), same as any other named source.
  • docs/weight-conversion.md: new "Convert from a GGUF quant instead of the full checkpoint" section with a worked override-recipe example, plus a subsection on MTP/vision/DFlash2.
  • tests/convert/test_gguf.py: synthetic-fixture coverage (built with the gguf package's own writer, no external model files needed) for row-partial reads, name-map construction, the GDN reorder's swapped-argument inverse, the norm shift, and the vision patch-embed composite reconstruction.

Test plan

  • python3 -m pytest tests/convert/ — 42 passed (14 new)
  • Manually validated GGUFSource/HFAliasSource/all four name-map functions against real Qwen3.8-27B checkpoint/GGUF pairs (Q5_0, IQ2_M, Q8_0) across text, GDN, MTP, and vision tensors, comparing dequantized values back to the HF originals
  • End-to-end: converted a real checkpoint from Q5_0/IQ2_M/Q8_0 GGUF sources (text-only and full text,vision,mtp,dflash2) and loaded each resulting .ninfer in ninfer-serve, including a real image through the vision path and both --spec dflash2 and --spec mtp speculative decoding (draft acceptance and correct, baseline-matching output confirmed for both)

🤖 Generated with Claude Code

Adds tools/convert/sources/gguf.py so a recipe can source some or all
logical parameters from a GGUF file (F16 through Q2-class quants,
including Qwen3.5's GDN reorder, MTP's extra layer, and the mmproj
vision tower) instead of the full Safetensors checkpoint. Parsing and
dequantization delegate to the gguf package; the name/shape/reorder
maps for Qwen3.5 were checked against real checkpoint/GGUF pairs
end-to-end (dequantized values compared back to the HF originals, not
just names), which caught two wrong assumptions in the process: the
GDN head reorder needs swapped arguments to invert, not the same call
twice, and llama.cpp's MTP export does not apply the same norm.weight
+1 shift the base model's layers get.

Also fixes GGUFSource to decode only the GGUF-native row range a read
actually needs (GGUF's raw tensor data is already row-structured) --
recipe.prepare()'s 1-element preflight probe was fully dequantizing
every tensor twice (once for the probe, once for the real write), and
this was roughly doubling conversion time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wallawalla47 pushed a commit to Wallawalla47/ninfer-custom that referenced this pull request Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant