Instructions to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL # Run inference directly in the terminal: llama cli -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL # Run inference directly in the terminal: llama cli -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL # Run inference directly in the terminal: ./llama-cli -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL # Run inference directly in the terminal: ./build/bin/llama-cli -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Use Docker
docker model run hf.co/bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
- LM Studio
- Jan
- vLLM
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
- Ollama
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with Ollama:
ollama run hf.co/bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
- Unsloth Desktop
- Pi
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with Docker Model Runner:
docker model run hf.co/bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
- Lemonade
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Run and chat with the model
lemonade run user.Qwen3.8-Flash-Next-Q4_K_XL-DN4-Q4_K_XL
List all available models
lemonade list
- Hermes Agent
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4 with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "bitlamas/Qwen3.8-Flash-Next-Q4_K_XL-DN4:Q4_K_XL" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Model card, license and build method
Browse files- LICENSE +16 -0
- README.md +123 -0
- SHA256SUMS +4 -0
- method/METHOD.md +86 -0
- method/gguf_extract.py +113 -0
- method/gguf_splice.py +75 -0
- method/gguf_tensor_map.py +181 -0
LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Qwen Community License 1.0
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Qwen
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software, including the model weights, parameters, configuration files, inference code and associated documentation files (collectively, the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, sell, deploy, host, fine-tune, and create derivative works from (collectively, "Use" or "Using") copies of the Software; and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
| 6 |
+
|
| 7 |
+
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. If the Software (or any derivative works thereof) is Used for any of the licensee's commercial products or services that have more than 100,000,000 monthly active users or US$ 20,000,000 (or equivalent in other currencies) monthly revenue, respective model name must be prominently displayed on the user interface of such product or service; and,
|
| 8 |
+
|
| 9 |
+
2. If the licensee or any of its affiliates conducts a Model as a Service or AI Work Assistant business, the licensee shall obtain a separate license from Qwen before Using the Software or its derivative works for any commercial purpose. The foregoing requirement shall not apply to the licensee's internal Use of the Software, provided that such Use does not make the Software, its outputs, or its underlying model capabilities available to any third party.
|
| 10 |
+
|
| 11 |
+
"Model as a Service" means giving a third party access to language model inference or fine-tuning (e.g., via API or a hosted endpoint) in a manner that allows such third parties to exercise meaningful control over the inputs, parameters, or training data. This does not include the mere relaying of requests to models hosted by other third parties.
|
| 12 |
+
“AI Work Assistant” means an independent AI-powered product primarily designed for AI-assisted coding or office productivity (e.g., Qoder and QwenWork). It does not include: (a) a single-purpose AI tool (such as an AI translation tool); (b) an AI assistant primarily designed for a domain other than coding or office productivity (such as Taobao AI Shopping Assistant or AMap AI Chat); or (c) an AI assistant that is a feature of a product whose primary purpose is not AI-assisted coding or office productivity.
|
| 13 |
+
|
| 14 |
+
THE SOFTWARE AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL QWEN, ITS AFFILIATES OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE USE OF THE SOFTWARE MUST COMPLY WITH APPLICABLE LAWS AND REGULATIONS, AND MUST NOT INFRINGE THE INTELLECTUAL PROPERTY RIGHTS OF ANY THIRD PARTY.
|
| 15 |
+
|
| 16 |
+
For any questions regarding this license, please contact model-business@notice.qwencloud.com.
|
README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: qwen-community-1.0
|
| 4 |
+
license_link: LICENSE
|
| 5 |
+
base_model:
|
| 6 |
+
- Qwen/Qwen3.8-Flash-Next
|
| 7 |
+
base_model_relation: quantized
|
| 8 |
+
pipeline_tag: text-generation
|
| 9 |
+
library_name: llama.cpp
|
| 10 |
+
tags:
|
| 11 |
+
- gguf
|
| 12 |
+
- llama.cpp
|
| 13 |
+
- qwen4exp
|
| 14 |
+
- strix-halo
|
| 15 |
+
- unified-memory
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# Qwen3.8-Flash-Next-Q4_K_XL-DN4
|
| 19 |
+
|
| 20 |
+
This is unsloth's UD-Q4_K_XL with one change: the 48 expert down projections are IQ4_NL instead of Q5_1. That takes the file from 111.3 GB to 102.5 GB. On the same text it measures as the same model.
|
| 21 |
+
|
| 22 |
+
I built it because I run this model on a 128 GB AMD Strix Halo (Ryzen AI MAX+ 395, Radeon 8060S) under Windows with llama.cpp on Vulkan. On that machine one process can put about 78 GiB on the GPU. UD-Q4_K_XL does not fit that unless you page or move expert layers to the CPU, and the next size down, UD-IQ4_XS, is a worse model. Nobody ships the size in between, so I made it. If your machine has room for something between 94 GB and 111 GB, this is for you.
|
| 23 |
+
|
| 24 |
+
Every number below was measured on that machine. The logs are in the method notes.
|
| 25 |
+
|
| 26 |
+
## What changed
|
| 27 |
+
|
| 28 |
+
| tensor group | size | UD-Q4_K_XL | this file |
|
| 29 |
+
|---|---|---|---|
|
| 30 |
+
| expert gate and up (`ffn_gate_exps`, `ffn_up_exps`) | 44.4 GB | Q4_K, layer 2 at Q5_K | same |
|
| 31 |
+
| expert down (`ffn_down_exps`, 48 layers) | 27.1 GB to 22.6 GB | Q5_1, 5 layers at Q8_0 | IQ4_NL |
|
| 32 |
+
| n-gram table (per-layer embeddings) | 28.8 GB | IQ4_NL | same |
|
| 33 |
+
| attention, linear attention, hyper-connections, shared expert, embeddings, output | 5.5 GB | Q8_0 | same |
|
| 34 |
+
| total | | 111.3 GB | 102.5 GB |
|
| 35 |
+
|
| 36 |
+
1,224 tensors in four shards, same split layout as the source. 48 tensors differ. Every other byte is unsloth's.
|
| 37 |
+
|
| 38 |
+
Why the down projections, and why IQ4_NL: `ffn_down_exps` is 640 values wide. That is not a
|
| 39 |
+
multiple of 256, so none of the K-quants or I-quants with 256-value blocks can touch it. unsloth had to leave it at Q5_1, a 32-block type at 6 bits, which makes it the one big tensor group in the file with more bits than it needs. The 32-block family also has IQ4_NL at 4.5 bits. Moving the downs there saves 8.9 GB and changes nothing else. The same rule is why the 28.8 GB table (rows 160 wide) cannot get any smaller.
|
| 40 |
+
|
| 41 |
+
## Quality
|
| 42 |
+
|
| 43 |
+
Same engine, same text (wikitext-2 test, 24 chunks of 2048 tokens, `-b 2048 -ub 1024 -fa on`), same machine.
|
| 44 |
+
|
| 45 |
+
| file | perplexity | KL divergence vs UD-Q4_K_XL | same top-1 token |
|
| 46 |
+
|---|---|---|---|
|
| 47 |
+
| unsloth UD-Q4_K_XL, 111.3 GB | 2.9117 ± 0.0363 | reference | reference |
|
| 48 |
+
| this file, 102.5 GB | 2.9165 ± 0.0363 | 0.028 ± 0.001 | 94.5 % |
|
| 49 |
+
| unsloth UD-IQ4_XS, 93.7 GB | 2.998 ± 0.037 | 0.089 | 90.75 % |
|
| 50 |
+
|
| 51 |
+
Perplexity moves 0.17 %, which is inside the error bar. The KL divergence is the number that
|
| 52 |
+
matters: median 0.005, 99th percentile 0.34, one token in eighteen changes its top pick. IQ4_XS changes one in eleven and its tail is three times worse.
|
| 53 |
+
|
| 54 |
+
Two checks on that number. The reference had to run with eight expert layers on the CPU, because 111 GB does not fit my GPU. Scoring this file the same way gives 0.0277 and 94.8 %, so the CPU kernels add nothing and the 0.028 is the quantization itself. And for scale: the q4_0 KV cache that I run every day puts 0.026 between this file and itself, and I have never noticed it.
|
| 55 |
+
|
| 56 |
+
## The imatrix made no difference
|
| 57 |
+
|
| 58 |
+
The downs here were requantized from unsloth's Q5_1 without an importance matrix. I expected that to be the weak point, so I tried it: the same 48 tensors requantized with unsloth's published `imatrix_unsloth.gguf_file`, everything else identical.
|
| 59 |
+
|
| 60 |
+
| downs requantized | KL divergence vs UD-Q4_K_XL | same top-1 |
|
| 61 |
+
|---|---|---|
|
| 62 |
+
| without the imatrix (this file) | 0.0282 ± 0.0007 | 94.50 % |
|
| 63 |
+
| with the imatrix | 0.0277 ± 0.0007 | 94.60 % |
|
| 64 |
+
|
| 65 |
+
Inside one error bar on every statistic except the single worst token. From a 6-bit source the IQ4_NL grid sets the error and the importance weighting has nothing left to steer. I deleted the imatrix build and kept this one. I guess a rung quantized from the BF16 checkpoint with the imatrix would still be the right way to make this file, though.
|
| 66 |
+
|
| 67 |
+
## Running it on a 128 GB Strix Halo, Windows, Vulkan
|
| 68 |
+
|
| 69 |
+
This is what serves on my machine every day, at 262144 context, the whole transformer on the GPU, no expert layers on the CPU:
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
llama-server -m Qwen3.8-Flash-Next-Q4_K_XL-DN4-00001-of-00004.gguf \
|
| 73 |
+
-c 262144 -ngl 99 -fa on -ctk q4_0 -ctv q4_0 -ub 512 -np 1 \
|
| 74 |
+
-lm mmap --lazy-mode on --cache-ram 0 --ctx-checkpoints 8 --jinja \
|
| 75 |
+
-md mtp-Qwen3.8-Flash-Next-shared-Q4_K_M.gguf --spec-type draft-mtp,ngram-mod \
|
| 76 |
+
--spec-draft-n-max 4 --spec-draft-p-min 0.75 \
|
| 77 |
+
--spec-ngram-mod-n-min 16 --spec-ngram-mod-n-max 128 --spec-ngram-mod-n-match 48
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Memory mode: dynamic settings with the shared memory slider at its 124 GB maximum. The driver then lets one process address about 78 GiB on the GPU. This file lands at 77.9 to 78.5 GiB with the q4_0 cache and the Q4_K_M draft head. If it ever dies at warm-up, `-ub 256` frees 1.7 GiB. I tested that fallback and it works.
|
| 81 |
+
|
| 82 |
+
The table streams from the file. `-lm mmap --lazy-mode on` keeps the 28.8 GB n-gram table out of committed memory and reads rows as needed. It measured the same as a resident table. `mlock` cannot pin a region this big on Windows, so do not bother.
|
| 83 |
+
|
| 84 |
+
The draft head is unsloth's `mtp-Qwen3.8-Flash-Next-shared-Q4_K_M.gguf`. Those are Qwen's own MTP weights, exported by unsloth. The head borrows embeddings from the main model by shape, so it pairs with this file unchanged. It is worth about 50 % more speed on fresh text. The n-gram drafter adds about 13 % on edits.
|
| 85 |
+
|
| 86 |
+
The KV cache at q4_0 is the biggest quality cost in this setup: 0.026 of KL divergence against an f16 cache at 2048 tokens, 0.020 at 16k, and it does not grow with depth. The value side is the sensitive one. q8_0 on both sides costs 0.012 but needs the 1.7 GiB from `-ub 256`. I tried keys at q4_0 with values at q8_0. It fits, it scores 0.019, and it is a trap: on this Vulkan build the mixed types leave the fast attention path, and decode falls to 17.0 / 13.2 / 6.8 tokens per second in the under-25k, 25 to 50k and 50 to 100k bands, against 19.8 / 18.8 / 17.1 for q4_0 on both sides. A short bench cannot see this. Keep the cache types the same on both sides.
|
| 87 |
+
|
| 88 |
+
Speed on my machine. Prefill about 180 tokens per second at shallow depth, 120 with the MTP head on a 22k prompt. Decode 28 to 32 on fresh text with the head, 45 to 50 on edits with both drafters, 11 to 13 past 200k tokens of context. On fresh text this file runs about 20 % slower than the source did in a fixed 96 GB carve-out, because IQ4_NL kernels cost more per step on this driver than Q5_1. That is the one price.
|
| 89 |
+
|
| 90 |
+
Vision. unsloth's `mmproj` for this model works with it. On this driver run the projector on the CPU with `--no-mmproj-offload`.
|
| 91 |
+
|
| 92 |
+
Engine used throughout: unsloth's llama.cpp fork build `b10715` with PR 27836 and unslothai#142 for MTP. Mainline llama.cpp loads the file the same way, as long as your build knows the `qwen4exp` architecture.
|
| 93 |
+
|
| 94 |
+
## Files
|
| 95 |
+
|
| 96 |
+
| file | bytes | sha256 |
|
| 97 |
+
|---|---|---|
|
| 98 |
+
| `Qwen3.8-Flash-Next-Q4_K_XL-DN4-00001-of-00004.gguf` | 10,946,624 | 4448186216b3af4cc558bbce2c3213f01608f8f8b2e5267a9767971dd3ec8082 |
|
| 99 |
+
| `Qwen3.8-Flash-Next-Q4_K_XL-DN4-00002-of-00004.gguf` | 47,447,858,336 | 9904da25dd2dc97c1756ac88e0d56ff30012b65e890161b90b14b378a7d3d883 |
|
| 100 |
+
| `Qwen3.8-Flash-Next-Q4_K_XL-DN4-00003-of-00004.gguf` | 44,552,691,904 | 2685325f5dfab3e4abad9f9519d1236b31b112b2bd7e5a3ff0afefd34e91b4cd |
|
| 101 |
+
| `Qwen3.8-Flash-Next-Q4_K_XL-DN4-00004-of-00004.gguf` | 10,462,690,720 | e656c8d08713915c9005ee2a631e8c02efa6c5914cc5c492c97e2a1c5ff82f92 |
|
| 102 |
+
|
| 103 |
+
Total 102,474,187,584 bytes, 102.47 GB, 95.43 GiB. Shard 1 holds the metadata and the split
|
| 104 |
+
header only, as in the source.
|
| 105 |
+
|
| 106 |
+
## How I built it, and how to build the next one
|
| 107 |
+
|
| 108 |
+
`method/` next to this card has the three scripts (plain Python, no numpy) and the exact commands. The short version:
|
| 109 |
+
|
| 110 |
+
1. `gguf_tensor_map.py` reads a GGUF's real composition from its header alone, from a local file or straight from a Hugging Face URL. Judge a quant by this map, never by its name.
|
| 111 |
+
2. `gguf_extract.py` pulls the 48 down tensors out of the four source shards into one loadable GGUF. 29 GB, three minutes.
|
| 112 |
+
3. `llama-quantize --allow-requantize --tensor-type ffn_down_exps=iq4_nl downs.gguf downs-iq4nl.gguf IQ4_NL`, about 25 minutes. The explicit `--tensor-type` is not optional. With the `IQ4_NL` file type alone, llama-quantize picks q5_K for the downs, hits the 256-block rule, and silently falls back to q5_1. You get the same file back and a success message. Read the log.
|
| 113 |
+
4. `gguf_splice.py` writes a new shard set. Every tensor is copied byte for byte except the 48 replacements, with offsets recomputed. Eleven minutes. The originals are never touched.
|
| 114 |
+
|
| 115 |
+
The same recipe works on any tensor whose width is not a multiple of 256, on any model.
|
| 116 |
+
|
| 117 |
+
## Credits and license
|
| 118 |
+
|
| 119 |
+
Qwen made the model. It is under the Qwen Community License 1.0, included here as `LICENSE`, as the license asks for derivatives.
|
| 120 |
+
|
| 121 |
+
unsloth made UD-Q4_K_XL, which is every tensor in this file but 48, and exported the MTP head. Their choice to keep attention, the linear attention blocks and the hyper-connections at Q8_0 is what makes this model's 4-bit quants good. My measurements say those small tensors decide quality more than the expert bits do.
|
| 122 |
+
|
| 123 |
+
I built and measured this on my own machine. I am not affiliated with Qwen or unsloth.
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
4448186216b3af4cc558bbce2c3213f01608f8f8b2e5267a9767971dd3ec8082 *Qwen3.8-Flash-Next-Q4_K_XL-DN4-00001-of-00004.gguf
|
| 2 |
+
9904da25dd2dc97c1756ac88e0d56ff30012b65e890161b90b14b378a7d3d883 *Qwen3.8-Flash-Next-Q4_K_XL-DN4-00002-of-00004.gguf
|
| 3 |
+
2685325f5dfab3e4abad9f9519d1236b31b112b2bd7e5a3ff0afefd34e91b4cd *Qwen3.8-Flash-Next-Q4_K_XL-DN4-00003-of-00004.gguf
|
| 4 |
+
e656c8d08713915c9005ee2a631e8c02efa6c5914cc5c492c97e2a1c5ff82f92 *Qwen3.8-Flash-Next-Q4_K_XL-DN4-00004-of-00004.gguf
|
method/METHOD.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# How Qwen3.8-Flash-Next-Q4_K_XL-DN4 was built
|
| 2 |
+
|
| 3 |
+
Three pure-Python scripts (Python 3.10+, no numpy, no gguf package) and one llama.cpp tool.
|
| 4 |
+
Everything here is byte-exact and reversible: the source shards are never modified.
|
| 5 |
+
|
| 6 |
+
Requirements: `llama-quantize` from any llama.cpp build that knows the `qwen4exp` architecture
|
| 7 |
+
(unsloth's fork `b10715` was used), about 35 GB of scratch space, and the four
|
| 8 |
+
`unsloth/Qwen3.8-Flash-Next-GGUF` `UD-Q4_K_XL` shards.
|
| 9 |
+
|
| 10 |
+
## 0. Look before you touch: the tensor map
|
| 11 |
+
|
| 12 |
+
```
|
| 13 |
+
python gguf_tensor_map.py Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf
|
| 14 |
+
python gguf_tensor_map.py hf:unsloth/Qwen3.8-Flash-Next-GGUF/UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Reads the header only (about 32 MB), locally or over HTTP range requests, and prints every tensor
|
| 18 |
+
group with its type and size, the expert gate/up/down types per layer band, and the KV-cache cost
|
| 19 |
+
per token for this architecture. This is how the down projections were found to be Q5_1 while
|
| 20 |
+
everything around them was Q4_K, and how the 640-wide shape (not a multiple of 256) explained why.
|
| 21 |
+
|
| 22 |
+
## 1. Extract the tensors to requantize
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
python gguf_extract.py Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf downs.gguf "ffn_down_exps"
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Writes one loadable GGUF holding only the tensors whose name matches the regex, carrying the first
|
| 29 |
+
shard's full metadata with the `split.*` keys rewritten for a single file, so `llama-quantize`
|
| 30 |
+
accepts it. 48 tensors, 29 GB, three minutes on NVMe.
|
| 31 |
+
|
| 32 |
+
## 2. Requantize, with the explicit tensor type
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
llama-quantize --allow-requantize --tensor-type ffn_down_exps=iq4_nl downs.gguf downs-iq4nl.gguf IQ4_NL
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
20 to 26 minutes on 16 Zen 5 cores. **`--tensor-type ffn_down_exps=iq4_nl` is not optional.** With only
|
| 39 |
+
the `IQ4_NL` file type, llama-quantize's per-tensor heuristic chooses q5_K for down projections,
|
| 40 |
+
finds that 640 is not divisible by 256, and falls back to q5_1, the type the tensors already had.
|
| 41 |
+
The tool reports success and the output is byte-for-byte the input. Read the per-tensor lines in
|
| 42 |
+
the log: every `ffn_down_exps` line must end in `iq4_nl`, and the output must be about 22.6 GB.
|
| 43 |
+
|
| 44 |
+
`--allow-requantize` is needed because the source is already quantized. A 6-bit Q5_1 source is
|
| 45 |
+
close enough to lossless that the extra step costs little; do not use this recipe to requantize a
|
| 46 |
+
4-bit tensor into another 4-bit type, where the errors compound.
|
| 47 |
+
|
| 48 |
+
## 3. Splice the replacements into a new shard set
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
python gguf_splice.py Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf downs-iq4nl.gguf Qwen3.8-Flash-Next-Q4_K_XL-DN4
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Writes `Qwen3.8-Flash-Next-Q4_K_XL-DN4-0000N-of-00004.gguf` next to the source shards. Every tensor
|
| 55 |
+
is copied byte for byte unless a tensor of the same name exists in the replacement file, in which
|
| 56 |
+
case the replacement's shape, type and data are used and the offsets are recomputed; shard 1
|
| 57 |
+
(metadata only) is copied under the new name; `split.*` keys are regenerated. 11 minutes.
|
| 58 |
+
|
| 59 |
+
Verify with the tensor map on the new first shard: the tensor count must match
|
| 60 |
+
`split.tensors.count` (1,224), the down projections must read IQ4_NL on every layer, and nothing
|
| 61 |
+
else may have changed.
|
| 62 |
+
|
| 63 |
+
## 4. Measure before you believe it
|
| 64 |
+
|
| 65 |
+
```
|
| 66 |
+
llama-perplexity -m <REF>.gguf -f wiki.test.raw -c 2048 -b 2048 -ub 1024 --chunks 24 -ngl 99 -fa on --kl-divergence-base base.kld
|
| 67 |
+
llama-perplexity -m <CAND>.gguf -f wiki.test.raw -c 2048 -b 2048 -ub 1024 --chunks 24 -ngl 99 -fa on --kl-divergence-base base.kld --kl-divergence
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
The reference run writes every scored token's logits (12.2 GB for 24 × 1024 scored tokens); the
|
| 71 |
+
candidate run reads them back and reports perplexity, mean and percentile KL divergence, and the
|
| 72 |
+
share of tokens whose top-1 prediction is unchanged. Perplexity alone is too blunt for a change
|
| 73 |
+
this small; the KLD is the number that says whether the file is the same model. Corpus: wikitext-2
|
| 74 |
+
raw test set (`https://huggingface.co/datasets/ggml-org/ci/resolve/main/wikitext-2-raw-v1.zip`).
|
| 75 |
+
|
| 76 |
+
If the reference does not fit on your GPU, `--n-cpu-moe N --no-host -lm mmap -lzm on` runs it
|
| 77 |
+
with the first N expert layers on the CPU at roughly 30 % lower prefill speed; that is how the
|
| 78 |
+
111 GB reference was scored on the 128 GB build machine.
|
| 79 |
+
|
| 80 |
+
## Why this generalises
|
| 81 |
+
|
| 82 |
+
Any tensor whose row width is not a multiple of 256 is limited to the 32-block types (Q4_0, Q4_1,
|
| 83 |
+
Q5_0, Q5_1, Q8_0, IQ4_NL), and quantizers tend to leave such tensors at a fat 32-block type rather
|
| 84 |
+
than reason about them. Wherever a large tensor group sits at Q5_1 or Q5_0 for that reason alone,
|
| 85 |
+
IQ4_NL is a 25 % cut that costs about what a Q5 to Q4 step costs anywhere else. That is usually
|
| 86 |
+
nothing you can measure when the tensor feeds an addition rather than a gate.
|
method/gguf_extract.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Extract selected tensors from a (split) GGUF into a single, loadable GGUF. Pure Python, no numpy.
|
| 3 |
+
|
| 4 |
+
The output carries the full metadata of the first shard (so llama-quantize can load it), with the
|
| 5 |
+
split.* keys rewritten to describe a single file, followed by only the selected tensors.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python gguf_extract.py <first shard or single .gguf> <output.gguf> <regex on tensor name>
|
| 9 |
+
Example (the 2026-09-03 build, all expert down projections):
|
| 10 |
+
python gguf_extract.py Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf downs.gguf "ffn_down_exps"
|
| 11 |
+
|
| 12 |
+
Written 2026-09-03 for the "Q4_K_XL with IQ4_NL downs" build (see studies/). See gguf_splice.py for the way back.
|
| 13 |
+
"""
|
| 14 |
+
import glob, os, re, struct, sys
|
| 15 |
+
|
| 16 |
+
ALIGN = 32
|
| 17 |
+
T = {0: 'B', 1: 'b', 2: 'H', 3: 'h', 4: 'I', 5: 'i', 6: 'f', 7: '?', 10: 'Q', 11: 'q', 12: 'd'}
|
| 18 |
+
# bytes per block, elements per block (for exact tensor byte sizes)
|
| 19 |
+
BLOCK = {0: (4, 1), 1: (2, 1), 30: (2, 1), 2: (18, 32), 3: (20, 32), 6: (22, 32), 7: (24, 32), 8: (34, 32),
|
| 20 |
+
10: (84, 256), 11: (110, 256), 12: (144, 256), 13: (176, 256), 14: (210, 256), 20: (18, 32),
|
| 21 |
+
23: (136, 256), 21: (110, 256), 22: (82, 256), 18: (66, 256), 16: (66, 256), 17: (74, 256),
|
| 22 |
+
19: (50, 256), 29: (56, 256)}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def nbytes(shape, ty):
|
| 26 |
+
n = 1
|
| 27 |
+
for d in shape: n *= d
|
| 28 |
+
bs, be = BLOCK[ty]
|
| 29 |
+
assert n % be == 0, (shape, ty)
|
| 30 |
+
return n // be * bs
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Reader:
|
| 34 |
+
def __init__(self, path):
|
| 35 |
+
self.path = path; self.f = open(path, 'rb'); self.size = os.path.getsize(path)
|
| 36 |
+
assert self.f.read(4) == b'GGUF'
|
| 37 |
+
self.version = self.rd('I'); self.n_tensors = self.rd('Q'); self.n_kv = self.rd('Q')
|
| 38 |
+
self.kv = [] # (key, type, raw_bytes_of_value)
|
| 39 |
+
for _ in range(self.n_kv):
|
| 40 |
+
key = self.rstr(); ty = self.rd('I'); start = self.f.tell(); self.skip_val(ty); end = self.f.tell()
|
| 41 |
+
self.f.seek(start); raw = self.f.read(end - start); self.kv.append((key, ty, raw))
|
| 42 |
+
self.tensors = [] # (name, shape, type, offset)
|
| 43 |
+
for _ in range(self.n_tensors):
|
| 44 |
+
name = self.rstr(); nd = self.rd('I'); shape = [self.rd('Q') for _ in range(nd)]
|
| 45 |
+
ty = self.rd('I'); off = self.rd('Q'); self.tensors.append((name, shape, ty, off))
|
| 46 |
+
self.data_start = (self.f.tell() + ALIGN - 1) // ALIGN * ALIGN
|
| 47 |
+
|
| 48 |
+
def rd(self, fmt): return struct.unpack('<' + fmt, self.f.read(struct.calcsize(fmt)))[0]
|
| 49 |
+
def rstr(self): n = self.rd('Q'); return self.f.read(n).decode('utf-8', 'replace')
|
| 50 |
+
def skip_val(self, ty):
|
| 51 |
+
if ty == 8: n = self.rd('Q'); self.f.seek(n, 1)
|
| 52 |
+
elif ty == 9:
|
| 53 |
+
et = self.rd('I'); n = self.rd('Q')
|
| 54 |
+
for _ in range(n): self.skip_val(et)
|
| 55 |
+
else: self.f.seek(struct.calcsize(T[ty]), 1)
|
| 56 |
+
|
| 57 |
+
def read_tensor(self, name, shape, ty, off):
|
| 58 |
+
self.f.seek(self.data_start + off); return self.f.read(nbytes(shape, ty))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def wstr(s):
|
| 62 |
+
b = s.encode('utf-8'); return struct.pack('<Q', len(b)) + b
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def patched_kv(kv, n_tensors_out):
|
| 66 |
+
out = []
|
| 67 |
+
for key, ty, raw in kv:
|
| 68 |
+
if key == 'split.no': raw = struct.pack('<H', 0)
|
| 69 |
+
elif key == 'split.count': raw = struct.pack('<H', 1)
|
| 70 |
+
elif key == 'split.tensors.count': raw = struct.pack('<i', n_tensors_out)
|
| 71 |
+
out.append((key, ty, raw))
|
| 72 |
+
return out
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def shards_of(first):
|
| 76 |
+
m = re.match(r'(.*)-(\d{5})-of-(\d{5})\.gguf$', first)
|
| 77 |
+
if not m: return [first]
|
| 78 |
+
base, _, count = m.groups()
|
| 79 |
+
return [f'{base}-{i:05d}-of-{count}.gguf' for i in range(1, int(count) + 1)]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def main():
|
| 83 |
+
first, out, pattern = sys.argv[1], sys.argv[2], re.compile(sys.argv[3])
|
| 84 |
+
shards = [Reader(p) for p in shards_of(first)]
|
| 85 |
+
meta = shards[0].kv
|
| 86 |
+
selected = [(r, t) for r in shards for t in r.tensors if pattern.search(t[0])]
|
| 87 |
+
print(f'{len(selected)} tensors selected from {len(shards)} shard(s)')
|
| 88 |
+
kv = patched_kv(meta, len(selected))
|
| 89 |
+
# header
|
| 90 |
+
hdr = b'GGUF' + struct.pack('<IQQ', 3, len(selected), len(kv))
|
| 91 |
+
for key, ty, raw in kv: hdr += wstr(key) + struct.pack('<I', ty) + raw
|
| 92 |
+
infos = b''; off = 0; layout = []
|
| 93 |
+
for r, (name, shape, ty, src_off) in selected:
|
| 94 |
+
sz = nbytes(shape, ty); layout.append((r, name, shape, ty, src_off, off, sz))
|
| 95 |
+
infos += wstr(name) + struct.pack('<I', len(shape)) + b''.join(struct.pack('<Q', d) for d in shape) + struct.pack('<IQ', ty, off)
|
| 96 |
+
off += (sz + ALIGN - 1) // ALIGN * ALIGN
|
| 97 |
+
head = hdr + infos
|
| 98 |
+
pad = (ALIGN - len(head) % ALIGN) % ALIGN
|
| 99 |
+
total = 0
|
| 100 |
+
with open(out, 'wb') as o:
|
| 101 |
+
o.write(head + b'\x00' * pad)
|
| 102 |
+
for r, name, shape, ty, src_off, dst_off, sz in layout:
|
| 103 |
+
r.f.seek(r.data_start + src_off)
|
| 104 |
+
remaining = sz
|
| 105 |
+
while remaining:
|
| 106 |
+
chunk = r.f.read(min(remaining, 64 << 20)); o.write(chunk); remaining -= len(chunk)
|
| 107 |
+
o.write(b'\x00' * ((ALIGN - sz % ALIGN) % ALIGN)); total += sz
|
| 108 |
+
print(f' {name:40s} type {ty:2d} {sz / 1e6:9.1f} MB', file=sys.stderr)
|
| 109 |
+
print(f'wrote {out}: {total / 1e9:.2f} GB of tensor data')
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == '__main__':
|
| 113 |
+
main()
|
method/gguf_splice.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Splice replacement tensors into a split GGUF, writing a new shard set. Pure Python, no numpy.
|
| 3 |
+
|
| 4 |
+
Every tensor of the original shards is copied byte-for-byte unless a tensor of the same name exists
|
| 5 |
+
in the replacement GGUF, in which case the replacement's shape/type/data is used instead. Shard 1
|
| 6 |
+
(metadata only in gguf-split layouts) is copied verbatim under the new name; shards with tensors get
|
| 7 |
+
their split.* keys regenerated and tensor offsets recomputed. The originals are never modified.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python gguf_splice.py <original first shard> <replacement.gguf> <output base name>
|
| 11 |
+
-> writes <output base name>-0000N-of-0000M.gguf next to the original shards
|
| 12 |
+
Example (2026-09-03):
|
| 13 |
+
python gguf_splice.py Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf build-downs-iq4nl.gguf Qwen3.8-Flash-Next-UD-Q4_K_XL-dn4
|
| 14 |
+
|
| 15 |
+
Verify afterwards with gguf_tensor_map.py on the new first shard (tensor count must match split.tensors.count).
|
| 16 |
+
"""
|
| 17 |
+
import os, re, shutil, struct, sys
|
| 18 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
+
from gguf_extract import Reader, nbytes, wstr, shards_of, ALIGN
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def write_shard(out_path, kv, tensors, sources):
|
| 23 |
+
"""kv: list of (key, type, raw); tensors: list of (name, shape, type); sources: name -> (Reader, src_off)."""
|
| 24 |
+
hdr = b'GGUF' + struct.pack('<IQQ', 3, len(tensors), len(kv))
|
| 25 |
+
for key, ty, raw in kv: hdr += wstr(key) + struct.pack('<I', ty) + raw
|
| 26 |
+
infos = b''; off = 0; layout = []
|
| 27 |
+
for name, shape, ty in tensors:
|
| 28 |
+
sz = nbytes(shape, ty); layout.append((name, shape, ty, off, sz))
|
| 29 |
+
infos += wstr(name) + struct.pack('<I', len(shape)) + b''.join(struct.pack('<Q', d) for d in shape) + struct.pack('<IQ', ty, off)
|
| 30 |
+
off += (sz + ALIGN - 1) // ALIGN * ALIGN
|
| 31 |
+
head = hdr + infos
|
| 32 |
+
pad = (ALIGN - len(head) % ALIGN) % ALIGN
|
| 33 |
+
with open(out_path, 'wb') as o:
|
| 34 |
+
o.write(head + b'\x00' * pad)
|
| 35 |
+
for name, shape, ty, dst_off, sz in layout:
|
| 36 |
+
r, src_off = sources[name]
|
| 37 |
+
r.f.seek(r.data_start + src_off); remaining = sz
|
| 38 |
+
while remaining:
|
| 39 |
+
chunk = r.f.read(min(remaining, 64 << 20)); o.write(chunk); remaining -= len(chunk)
|
| 40 |
+
o.write(b'\x00' * ((ALIGN - sz % ALIGN) % ALIGN))
|
| 41 |
+
return off
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main():
|
| 45 |
+
first, repl_path, out_base = sys.argv[1], sys.argv[2], sys.argv[3]
|
| 46 |
+
shard_paths = shards_of(first); n = len(shard_paths)
|
| 47 |
+
repl = Reader(repl_path)
|
| 48 |
+
repl_map = {t[0]: t for t in repl.tensors}
|
| 49 |
+
print(f'replacement carries {len(repl_map)} tensors')
|
| 50 |
+
total = 0; replaced = 0; out_dir = os.path.dirname(first) or '.'
|
| 51 |
+
for i, p in enumerate(shard_paths, 1):
|
| 52 |
+
out_path = os.path.join(out_dir, f'{out_base}-{i:05d}-of-{n:05d}.gguf')
|
| 53 |
+
r = Reader(p)
|
| 54 |
+
if r.n_tensors == 0:
|
| 55 |
+
r.f.close(); shutil.copyfile(p, out_path); print(f'shard {i}: metadata only, copied'); continue
|
| 56 |
+
kv = []
|
| 57 |
+
for key, ty, raw in r.kv:
|
| 58 |
+
kv.append((key, ty, raw)) # split.no/count/tensors.count unchanged: same shard numbering, same total
|
| 59 |
+
tensors = []; sources = {}
|
| 60 |
+
for name, shape, ty, off in r.tensors:
|
| 61 |
+
if name in repl_map:
|
| 62 |
+
_, rshape, rty, roff = repl_map[name]
|
| 63 |
+
assert rshape == shape, (name, shape, rshape)
|
| 64 |
+
tensors.append((name, rshape, rty)); sources[name] = (repl, roff); replaced += 1
|
| 65 |
+
else:
|
| 66 |
+
tensors.append((name, shape, ty)); sources[name] = (r, off)
|
| 67 |
+
written = write_shard(out_path, kv, tensors, sources); total += written
|
| 68 |
+
print(f'shard {i}: {len(tensors)} tensors, {written / 1e9:.2f} GB -> {os.path.basename(out_path)}')
|
| 69 |
+
r.f.close()
|
| 70 |
+
print(f'done: {replaced} tensors replaced, {total / 1e9:.2f} GB of tensor data written')
|
| 71 |
+
if replaced != len(repl_map): print(f'WARNING: {len(repl_map) - replaced} replacement tensors were not found in the original')
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == '__main__':
|
| 75 |
+
main()
|
method/gguf_tensor_map.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Map where the bytes of a GGUF go, without loading it.
|
| 3 |
+
|
| 4 |
+
Reads only the header (KV metadata + tensor infos) of every shard, so it works on a local
|
| 5 |
+
file or on a Hugging Face repo by range-fetching the first MBs of each shard. Prints:
|
| 6 |
+
- bytes per category (experts, n-gram/PLE table, embeddings, attention, rest) by quant type
|
| 7 |
+
- expert gate/up/down quant type per layer band
|
| 8 |
+
- nextn (MTP) tensors and nextn_shared_target_tensors, if present
|
| 9 |
+
- the KV cache: which layers hold one (full attention / sliding window / recurrent), bytes per
|
| 10 |
+
token, and the cache size at 262k context for f16, q8_0 and q4_0 -- plus the GPU-resident sum
|
| 11 |
+
(weights excluding a host-side PLE table + KV), which is the number to hold against the memory
|
| 12 |
+
budget in platform.md before downloading anything
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
python gguf_tensor_map.py <local .gguf (any shard; all shards in the dir are found)>
|
| 16 |
+
python gguf_tensor_map.py hf:<org>/<repo> [<subfolder-or-name-filter>]
|
| 17 |
+
|
| 18 |
+
Written 2026-09-03 for the Qwen3.8-Flash-Next quant study (see studies/). Shard size for the
|
| 19 |
+
last tensor comes from the file size, so remote mode needs the HF tree API (no auth needed).
|
| 20 |
+
"""
|
| 21 |
+
import collections, glob, json, os, re, struct, sys, urllib.request
|
| 22 |
+
|
| 23 |
+
T = {0: 'B', 1: 'b', 2: 'H', 3: 'h', 4: 'I', 5: 'i', 6: 'f', 7: '?', 10: 'Q', 11: 'q', 12: 'd'}
|
| 24 |
+
TYPES = {0: 'F32', 1: 'F16', 2: 'Q4_0', 3: 'Q4_1', 6: 'Q5_0', 7: 'Q5_1', 8: 'Q8_0', 9: 'Q8_1', 10: 'Q2_K', 11: 'Q3_K',
|
| 25 |
+
12: 'Q4_K', 13: 'Q5_K', 14: 'Q6_K', 15: 'Q8_K', 16: 'IQ2_XXS', 17: 'IQ2_XS', 18: 'IQ3_XXS', 19: 'IQ1_S',
|
| 26 |
+
20: 'IQ4_NL', 21: 'IQ3_S', 22: 'IQ2_S', 23: 'IQ4_XS', 24: 'I8', 25: 'I16', 26: 'I32', 27: 'I64', 28: 'F64',
|
| 27 |
+
29: 'IQ1_M', 30: 'BF16', 34: 'TQ1_0', 35: 'TQ2_0', 39: 'MXFP4'}
|
| 28 |
+
HEAD_BYTES_FIRST, HEAD_BYTES_OTHER = 32 << 20, 2 << 20 # first shard carries the tokenizer
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def parse_header(blob, fsize):
|
| 32 |
+
f = memoryview(blob); pos = 0
|
| 33 |
+
def rd(fmt):
|
| 34 |
+
nonlocal pos
|
| 35 |
+
v = struct.unpack_from('<' + fmt, f, pos)[0]; pos += struct.calcsize(fmt); return v
|
| 36 |
+
def rstr():
|
| 37 |
+
nonlocal pos
|
| 38 |
+
n = rd('Q'); s = bytes(f[pos:pos + n]).decode('utf-8', 'replace'); pos += n; return s
|
| 39 |
+
def rval(t):
|
| 40 |
+
if t == 8: return rstr()
|
| 41 |
+
if t == 9:
|
| 42 |
+
et = rd('I'); n = rd('Q')
|
| 43 |
+
return [rval(et) for _ in range(n)] # full arrays: per-layer KV heads and SWA patterns live here
|
| 44 |
+
return rd(T[t])
|
| 45 |
+
assert bytes(f[0:4]) == b'GGUF', 'not a GGUF'
|
| 46 |
+
pos = 4; rd('I'); nt = rd('Q'); nkv = rd('Q')
|
| 47 |
+
kv = {}
|
| 48 |
+
for _ in range(nkv):
|
| 49 |
+
k = rstr(); t = rd('I'); kv[k] = rval(t)
|
| 50 |
+
ts = []
|
| 51 |
+
for _ in range(nt):
|
| 52 |
+
name = rstr(); nd = rd('I'); shape = [rd('Q') for _ in range(nd)]; ty = rd('I'); off = rd('Q')
|
| 53 |
+
ts.append((name, shape, TYPES.get(ty, str(ty)), off))
|
| 54 |
+
align = kv.get('general.alignment') or 32
|
| 55 |
+
data_start = (pos + align - 1) // align * align
|
| 56 |
+
ts.sort(key=lambda x: x[3])
|
| 57 |
+
out = []
|
| 58 |
+
for i, (name, shape, ty, off) in enumerate(ts):
|
| 59 |
+
nxt = ts[i + 1][3] if i + 1 < len(ts) else fsize - data_start
|
| 60 |
+
out.append((name, shape, ty, nxt - off))
|
| 61 |
+
return kv, out
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def local_shards(path):
|
| 65 |
+
d = os.path.dirname(path) or '.'
|
| 66 |
+
base = re.sub(r'-\d{5}-of-\d{5}\.gguf$', '', os.path.basename(path))
|
| 67 |
+
files = sorted(glob.glob(os.path.join(d, base + '-*-of-*.gguf'))) or [path]
|
| 68 |
+
for p in files:
|
| 69 |
+
with open(p, 'rb') as fh:
|
| 70 |
+
yield p, fh.read(HEAD_BYTES_FIRST), os.path.getsize(p)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def hf_shards(repo, flt):
|
| 74 |
+
tree = json.load(urllib.request.urlopen(f'https://huggingface.co/api/models/{repo}/tree/main?recursive=true'))
|
| 75 |
+
files = sorted(e['path'] for e in tree if e['type'] == 'file' and e['path'].endswith('.gguf') and (flt or '') in e['path'])
|
| 76 |
+
sizes = {e['path']: e['size'] for e in tree if e['type'] == 'file'}
|
| 77 |
+
for i, p in enumerate(files):
|
| 78 |
+
req = urllib.request.Request(f'https://huggingface.co/{repo}/resolve/main/{p}',
|
| 79 |
+
headers={'Range': f'bytes=0-{(HEAD_BYTES_FIRST if i == 0 else HEAD_BYTES_OTHER) - 1}'})
|
| 80 |
+
yield p, urllib.request.urlopen(req).read(), sizes[p]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def category(name):
|
| 84 |
+
if name == 'per_layer_token_embd.weight': return 'n-gram/PLE table'
|
| 85 |
+
if name in ('token_embd.weight', 'output.weight'): return name
|
| 86 |
+
if not name.startswith('blk.'): return 'other top-level'
|
| 87 |
+
if '_exps' in name: return 'experts'
|
| 88 |
+
if 'shexp' in name: return 'shared expert'
|
| 89 |
+
if 'attn_' in name or 'indexer' in name: return 'attention'
|
| 90 |
+
if 'ssm_' in name: return 'linear attention (ssm)'
|
| 91 |
+
if 'hc_' in name: return 'hyper-connections'
|
| 92 |
+
if 'nextn' in name: return 'nextn (MTP)'
|
| 93 |
+
return 'other per-layer'
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
KV_BYTES = {'f16': 2.0, 'q8_0': 34 / 32, 'q4_0': 18 / 32} # bytes per cached value (block = 32 values + scale)
|
| 97 |
+
KV_CTX = 262144
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def kv_cache_report(kv, arch, table_bytes, total_bytes):
|
| 101 |
+
"""Estimate the KV cache from the header: per token and at 262k, for the cache types that matter here.
|
| 102 |
+
|
| 103 |
+
Layer kinds: full attention (pays the whole context), sliding-window (llama.cpp iSWA keeps only
|
| 104 |
+
window + micro-batch tokens), recurrent / linear attention (a fixed state, no per-token cache).
|
| 105 |
+
Verified 2026-09-04 against qwen4exp: 12 full layers x 2 KV heads x K256/V256 at q4_0 = 1728 MiB,
|
| 106 |
+
exactly the 'Vulkan0 KV buffer size' the server reports at 262144 context.
|
| 107 |
+
"""
|
| 108 |
+
n = kv.get(f'{arch}.block_count')
|
| 109 |
+
if not n:
|
| 110 |
+
return
|
| 111 |
+
g = lambda k, d=None: kv.get(f'{arch}.{k}', d)
|
| 112 |
+
n_head, n_embd = g('attention.head_count'), g('embedding_length')
|
| 113 |
+
hd = n_embd // n_head if n_embd and n_head else None
|
| 114 |
+
heads = g('attention.head_count_kv', n_head)
|
| 115 |
+
heads = heads if isinstance(heads, list) else [heads] * n
|
| 116 |
+
k_len, v_len = g('attention.key_length', hd), g('attention.value_length', hd)
|
| 117 |
+
k_swa, v_swa = g('attention.key_length_swa', k_len), g('attention.value_length_swa', v_len)
|
| 118 |
+
swa, pat = g('attention.sliding_window', 0), g('attention.sliding_window_pattern')
|
| 119 |
+
if isinstance(pat, list):
|
| 120 |
+
is_swa = [bool(x) for x in pat]
|
| 121 |
+
elif isinstance(pat, int) and pat > 0 and swa:
|
| 122 |
+
is_swa = [(i + 1) % pat != 0 for i in range(n)]
|
| 123 |
+
else:
|
| 124 |
+
is_swa = [False] * n
|
| 125 |
+
interval = g('full_attention_interval') # hybrid families: every Nth layer is attention, the rest recurrent
|
| 126 |
+
is_rec = [bool(interval and (i + 1) % interval != 0) or not heads[i] for i in range(n)]
|
| 127 |
+
full = [i for i in range(n) if not is_rec[i] and not is_swa[i]]
|
| 128 |
+
win = [i for i in range(n) if not is_rec[i] and is_swa[i]]
|
| 129 |
+
if k_len is None or v_len is None:
|
| 130 |
+
print(" KV cache: cannot estimate (no head/embedding sizes in header)"); return
|
| 131 |
+
vals_full = sum((k_len + v_len) * heads[i] for i in full) # cached values per token, full layers
|
| 132 |
+
vals_win = sum((k_swa + v_swa) * heads[i] for i in win)
|
| 133 |
+
kinds = f"{len(full)} full-attention"
|
| 134 |
+
if win: kinds += f", {len(win)} sliding-window (window {swa})"
|
| 135 |
+
if n - len(full) - len(win): kinds += f", {n - len(full) - len(win)} recurrent/no-KV"
|
| 136 |
+
hs = sorted(set(heads[i] for i in full + win))
|
| 137 |
+
print(f" KV cache: {kinds} layers; KV heads {hs} x K{k_len}/V{v_len}"
|
| 138 |
+
+ (f" (window layers K{k_swa}/V{v_swa})" if win and (k_swa, v_swa) != (k_len, v_len) else ""))
|
| 139 |
+
weights = total_bytes - table_bytes
|
| 140 |
+
print(f" GPU-resident weights {weights / 2**30:.1f} GiB" + (f" (the {table_bytes / 2**30:.1f} GiB PLE table stays in host RAM)" if table_bytes else "")
|
| 141 |
+
+ f"; at {KV_CTX // 1024}k context the cache adds:")
|
| 142 |
+
for t, b in KV_BYTES.items():
|
| 143 |
+
cache = vals_full * b * KV_CTX + vals_win * b * min(KV_CTX, swa + 512)
|
| 144 |
+
print(f" {t:5s} {vals_full * b / 1024:6.1f} KiB/token -> {cache / 2**30:6.2f} GiB cache -> {(weights + cache) / 2**30:6.1f} GiB on the GPU before compute buffers and any draft head")
|
| 145 |
+
if g('attention.indexer.head_count'):
|
| 146 |
+
print(" note: this architecture also allocates an attention-indexer cache the estimate leaves out "
|
| 147 |
+
"(qwen4exp measured 648 MiB extra at q4_0, 262144 ctx)")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def main():
|
| 151 |
+
arg = sys.argv[1]
|
| 152 |
+
shards = hf_shards(arg[3:], sys.argv[2] if len(sys.argv) > 2 else '') if arg.startswith('hf:') else local_shards(arg)
|
| 153 |
+
agg = collections.defaultdict(collections.Counter); per = collections.defaultdict(dict); kvs = {}; total = 0
|
| 154 |
+
for p, blob, fsize in shards:
|
| 155 |
+
kv, ts = parse_header(blob, fsize); kvs.update(kv)
|
| 156 |
+
for name, shape, ty, sz in ts:
|
| 157 |
+
agg[category(name)][ty] += sz; total += sz
|
| 158 |
+
if name.startswith('blk.') and '_exps' in name:
|
| 159 |
+
per[int(name.split('.')[1])][name.split('.')[2].replace('ffn_', '').replace('_exps', '')] = ty
|
| 160 |
+
arch = kvs.get('general.architecture')
|
| 161 |
+
print(f"{arg}\n arch={arch} name={kvs.get('general.name')} blocks={kvs.get(f'{arch}.block_count')} "
|
| 162 |
+
f"nextn={kvs.get(f'{arch}.nextn_predict_layers', 0)} shared_target={kvs.get(f'{arch}.nextn_shared_target_tensors')}")
|
| 163 |
+
print(f" total {total / 1e9:.2f} GB ({total / 2**30:.2f} GiB)")
|
| 164 |
+
for c, cnt in sorted(agg.items(), key=lambda x: -sum(x[1].values())):
|
| 165 |
+
s = sum(cnt.values())
|
| 166 |
+
print(f" {s / 1e9:7.2f} GB {c:24s} " + ", ".join(f"{t}:{v / 1e9:.1f}" for t, v in cnt.most_common()))
|
| 167 |
+
if per:
|
| 168 |
+
print(" expert gate / up / down by layer band:")
|
| 169 |
+
runs, prev, start = [], None, 0
|
| 170 |
+
for b in sorted(per):
|
| 171 |
+
sig = (per[b].get('gate'), per[b].get('up'), per[b].get('down'))
|
| 172 |
+
if sig != prev:
|
| 173 |
+
if prev: runs.append((start, b - 1, prev))
|
| 174 |
+
prev, start = sig, b
|
| 175 |
+
runs.append((start, max(per), prev))
|
| 176 |
+
for a, b, (g, u, d) in runs: print(f" layers {a:2d}-{b:2d}: {g} / {u} / {d}")
|
| 177 |
+
kv_cache_report(kvs, arch, sum(agg['n-gram/PLE table'].values()), total)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == '__main__':
|
| 181 |
+
main()
|