Repository navigation
tools: q4nx_to_gguf, the GGUF that matches the NPU weights - #541
Conversation
gguf_to_q4nx.py keeps Q4_0/Q4_1/Q4_K codes but rounds scale and zero to bf16, and requantizes every other type to 4-bit, so model.q4nx is not the model of the GGUF it came from. q4nx_to_gguf.py writes the GGUF of the Q4NX model itself: q4_1 tiles -> GGML Q4_1 blocks (same codes, d/m = the bf16 scale/zero as f16), norms F32, the embedding F16 when exact (else F32), lm_head as output.weight, metadata and tokenizer copied from the source GGUF. `verify` checks dequant(gguf) == dequant(q4nx) and, with --roundtrip, that gguf_to_q4nx.py pack of the new GGUF is byte-identical to model.q4nx. Measured on Qwen3-0.6B-Q4_K_M.gguf -> model.q4nx -> this tool: 0 of 192,831,488 scale, zero and embedding values change in bf16 -> f16; 311/311 tensors dequantize identically (max |diff| 0); the round trip is byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Reviewer Guide 🔍(Review updated until commit c1b93d6)Here are some key observations to aid the review process:
|
|
Measured on strixhalo with Qwen3-0.6B (hybrid KV gate: 10 prompts x {128, 512} tokens, 64 teacher-forced steps, 1280 positions, KLD(native || arm)). The NPU KV is the unchanged state files from the hybrid proof. The native side is the GGUF written by this tool, decoding along its own greedy continuation.
HRX decode speed (package 1bit-hrx-3092596, 2 x 6 runs of 128 tokens after a 512-token prompt, shared box):
Q4_1 runs on the GPU (it is not falling back to the CPU), but it is about 17% slower than Q4_K_M at decode and about 30% slower at prefill. ggml-hrx serves Q4_1 only through its generic quantized mul_mat, and the gpu-kernels add-on has no Q4_1 matcher. 🤖 Generated with Claude Code |
|
Persistent review updated to latest commit 0ce30d8 |
|
Persistent review updated to latest commit c1b93d6 |
What
tools/q4nx_to_gguf.pywrites a GGUF that holds exactly the weights in amodel.q4nx, so a GPU decoding the GGUF and the NPU running the Q4NX compute the same model. It is the inverse oftools/gguf_to_q4nx.py.write <model.q4nx> <source.gguf> <out.gguf>: q4_1 tiles become GGMLQ4_1blocks. They keep the same 4-bit codes, andd/mare the tile's bf16 scale and zero converted to f16. The tool stops if any value would change in that conversion. Norms are written as F32. The embedding is F16 when every value is exact in f16, otherwise F32. lm_head is written asoutput.weight(gguf_to_q4nx.py quantizes a tied head, so it is no longer the embedding). Metadata and tokenizer are copied from the source GGUF, andgeneral.file_typeis set to MOSTLY_Q4_1.verify <model.q4nx> <out.gguf> [--roundtrip <source.gguf>]: checksdequant(out.gguf) == dequant(model.q4nx)on every tensor, and with--roundtripchecks thatgguf_to_q4nx.py packof the new GGUF is byte-identical to the originalmodel.q4nx.Why
gguf_to_q4nx.pyrequantizes every non-Q4 type to 4-bit q4_1. In Qwen3-0.6B-Q4_K_M that covers 28 tensors:attn_vandffn_downin layers 0, 1, 2, 5, 8, 11, 14, 17, 20, 23, 24, 25, 26 and 27 are Q6_K. Hybrid prefill (NPU) feeding HRX decode on the source GGUF therefore mixes two different models.Measured (Qwen3-0.6B-Q4_K_M.gguf -> model.q4nx -> this tool)
Draft: the HRX gate and the Q4_1-vs-Q4_K_M decode speed on HRX are still to be measured.
🤖 Generated with Claude Code