Initial release of AutoRound + ASHQ1 suite
Browse files- 00_SAFETENSORS-to-AutoRound-BF16-GGUF.py +1109 -0
- 00b_BF16-GGUF-MTP-extract.py +94 -0
- 01_create-calibration-dataset-and-imatrix.py +1025 -0
- 01b_BF16-GGUF-modules-fusion.py +145 -0
- 02_BF16-GGUF-to-ASHQ1.py +206 -0
- ASHQ1-mmproj.py +273 -0
- ASHQ1.py +1580 -0
- CHARTER.md +65 -0
- README.md +125 -0
00_SAFETENSORS-to-AutoRound-BF16-GGUF.py
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
r"""00_SAFETENSORS-to-AutoRound-BF16-GGUF.py — Universal AutoRound-infused GGUF builder.
|
| 3 |
+
|
| 4 |
+
Handles both:
|
| 5 |
+
1. Raw unquantized BF16 safetensors (runs AutoRound optimization, then builds GGUF)
|
| 6 |
+
2. Pre-AutoRounded safetensors (dequantizes streaming to BF16, then builds GGUF)
|
| 7 |
+
|
| 8 |
+
Always produces:
|
| 9 |
+
model-BF16.gguf — full model, MTP head included when present
|
| 10 |
+
model-no-mtp-BF16.gguf — trunk for imatrix (split via 00b when MTP present)
|
| 11 |
+
mtp-BF16.gguf — standalone speculative draft (if MTP present)
|
| 12 |
+
mmproj-BF16.gguf — vision projector (if multimodal)
|
| 13 |
+
Every GGUF gets a .provenance.json sidecar recording its AutoRound lineage.
|
| 14 |
+
|
| 15 |
+
Usage:
|
| 16 |
+
python 00_SAFETENSORS-to-AutoRound-BF16-GGUF.py # processes .\safetensors\
|
| 17 |
+
python 00_SAFETENSORS-to-AutoRound-BF16-GGUF.py <folder_path> # drag-and-drop
|
| 18 |
+
"""
|
| 19 |
+
import argparse,json,os,re,shutil,subprocess,sys,time,urllib.request
|
| 20 |
+
if os.name=="nt":os.environ.setdefault("KMP_AFFINITY","disabled")
|
| 21 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF","expandable_segments:True")
|
| 22 |
+
import torch
|
| 23 |
+
from safetensors.torch import load_file,save_file
|
| 24 |
+
from safetensors import safe_open
|
| 25 |
+
|
| 26 |
+
SCRIPT_DIR=os.path.dirname(os.path.abspath(__file__))
|
| 27 |
+
LLAMA_CPP=os.path.join(SCRIPT_DIR,"llama-cpp")
|
| 28 |
+
CONVERTER=os.path.join(LLAMA_CPP,"convert_hf_to_gguf.py")
|
| 29 |
+
EXTRACTOR=os.path.join(SCRIPT_DIR,"00b_BF16-GGUF-MTP-extract.py")
|
| 30 |
+
if not os.path.isfile(EXTRACTOR):
|
| 31 |
+
EXTRACTOR=os.path.join(SCRIPT_DIR,"00a_gguf-mtp-extract.py")
|
| 32 |
+
CALIB_SCRIPT=os.path.join(SCRIPT_DIR,"01_create-calibration-dataset.py")
|
| 33 |
+
if not os.path.isfile(CALIB_SCRIPT):
|
| 34 |
+
CALIB_SCRIPT=os.path.join(SCRIPT_DIR,"01_create-calibration-dataset-and-imatrix.py")
|
| 35 |
+
CALIB_DIR=os.path.join(SCRIPT_DIR,"01_create-calibration-dataset-and-imatrix")
|
| 36 |
+
DEFAULT_CALIB_NAME="bartowski-imatrix-v5-semantic.txt"
|
| 37 |
+
DEFAULT_CALIB_JSONL_NAME="bartowski-imatrix-v5-semantic.jsonl"
|
| 38 |
+
DEFAULT_CALIB_TXT=os.path.join(CALIB_DIR,DEFAULT_CALIB_NAME)
|
| 39 |
+
DEFAULT_CALIB_JSONL=os.path.join(CALIB_DIR,DEFAULT_CALIB_JSONL_NAME)
|
| 40 |
+
DEFAULT_CALIB_TXT_URL="https://huggingface.co/datasets/lemon07r/bartowski-imatrix-v5-semantic/resolve/main/bartowski-imatrix-v5-semantic.txt"
|
| 41 |
+
DEFAULT_CALIB_JSONL_URL="https://huggingface.co/datasets/lemon07r/bartowski-imatrix-v5-semantic/resolve/main/bartowski-imatrix-v5-semantic.jsonl"
|
| 42 |
+
DEFAULT_EXP_DATASET=os.path.join(CALIB_DIR,"experimental.txt")
|
| 43 |
+
_SIGLIP_NORM=[0.5,0.5,0.5]
|
| 44 |
+
LLAMA_CPP_REPO="https://github.com/ggml-org/llama.cpp.git"
|
| 45 |
+
|
| 46 |
+
# Per-architecture batch profiles: looped/compact architectures saturate at bs>=2
|
| 47 |
+
ARCH_BS_OVERRIDE={"nanbeige":{"bs":2,"min_vram":6,"max_gb":8}}
|
| 48 |
+
|
| 49 |
+
def _has_mod(name):
|
| 50 |
+
try:__import__(name);return True
|
| 51 |
+
except ImportError:return False
|
| 52 |
+
|
| 53 |
+
def _torch_whl_index():
|
| 54 |
+
ver=getattr(getattr(torch,"version",None),"cuda",None)
|
| 55 |
+
if not ver:return None
|
| 56 |
+
parts=str(ver).split(".")
|
| 57 |
+
if len(parts)<2:return None
|
| 58 |
+
return f"https://download.pytorch.org/whl/cu{parts[0]}{parts[1]}"
|
| 59 |
+
|
| 60 |
+
def ensure_pkg(pip_name,import_name=None,extra_args=None):
|
| 61 |
+
mod=import_name or pip_name.replace("-","_")
|
| 62 |
+
if _has_mod(mod):return True
|
| 63 |
+
print(f"\n[Setup] Installing missing package: {pip_name} …")
|
| 64 |
+
cmd=[sys.executable,"-m","pip","install","-U",pip_name]
|
| 65 |
+
if extra_args:cmd+=extra_args
|
| 66 |
+
if subprocess.run(cmd).returncode!=0:
|
| 67 |
+
print(f" ✗ pip install {pip_name} failed.")
|
| 68 |
+
return False
|
| 69 |
+
return _has_mod(mod)
|
| 70 |
+
|
| 71 |
+
def ensure_torchvision():
|
| 72 |
+
if _has_mod("torchvision"):return True
|
| 73 |
+
if ensure_pkg("torchvision"):return True
|
| 74 |
+
idx=_torch_whl_index()
|
| 75 |
+
if idx:
|
| 76 |
+
print(f" Retrying torchvision from {idx} …")
|
| 77 |
+
if ensure_pkg("torchvision",extra_args=["--index-url",idx]):return True
|
| 78 |
+
print(" ✗ torchvision is required for Qwen3.5 AutoProcessor.")
|
| 79 |
+
return False
|
| 80 |
+
|
| 81 |
+
def ensure_python_deps():
|
| 82 |
+
ok=ensure_pkg("auto-round","auto_round")
|
| 83 |
+
ok=ensure_torchvision() and ok
|
| 84 |
+
return ok
|
| 85 |
+
|
| 86 |
+
def ensure_llama_cpp():
|
| 87 |
+
if os.path.isfile(CONVERTER):return True
|
| 88 |
+
print(f"\n[Setup] llama.cpp converter missing — installing into {LLAMA_CPP}")
|
| 89 |
+
if shutil.which("git") is None:
|
| 90 |
+
print(" ✗ git is not on PATH. Install Git for Windows, then retry.")
|
| 91 |
+
return False
|
| 92 |
+
git_dir=os.path.join(LLAMA_CPP,".git")
|
| 93 |
+
if os.path.isdir(git_dir):
|
| 94 |
+
print(" Updating existing llama.cpp clone …")
|
| 95 |
+
rc=subprocess.run(["git","-C",LLAMA_CPP,"pull","--ff-only"]).returncode
|
| 96 |
+
elif os.path.isdir(LLAMA_CPP) and os.listdir(LLAMA_CPP):
|
| 97 |
+
print(f" ✗ {LLAMA_CPP} exists without convert_hf_to_gguf.py and without a git repo.")
|
| 98 |
+
return False
|
| 99 |
+
else:
|
| 100 |
+
rc=subprocess.run(["git","clone","--depth","1",LLAMA_CPP_REPO,LLAMA_CPP]).returncode
|
| 101 |
+
if rc!=0 or not os.path.isfile(CONVERTER):
|
| 102 |
+
print(" ✗ Unable to obtain convert_hf_to_gguf.py")
|
| 103 |
+
return False
|
| 104 |
+
req=os.path.join(LLAMA_CPP,"requirements.txt")
|
| 105 |
+
if os.path.isfile(req):
|
| 106 |
+
print(" Installing llama.cpp Python requirements …")
|
| 107 |
+
subprocess.run([sys.executable,"-m","pip","install","-q","-r",req])
|
| 108 |
+
print(f" ✓ llama.cpp ready: {CONVERTER}")
|
| 109 |
+
return True
|
| 110 |
+
|
| 111 |
+
def read_json(path):
|
| 112 |
+
if not os.path.isfile(path):return None
|
| 113 |
+
with open(path,"r",encoding="utf-8") as f:return json.load(f)
|
| 114 |
+
|
| 115 |
+
def read_safetensors_header(path):
|
| 116 |
+
import struct
|
| 117 |
+
with open(path,"rb") as f:
|
| 118 |
+
hlen=struct.unpack("<Q",f.read(8))[0]
|
| 119 |
+
return json.loads(f.read(hlen).decode("utf-8"))
|
| 120 |
+
|
| 121 |
+
_ROPE_RULES=(
|
| 122 |
+
(re.compile(r'if\s+(self\.config\.rope_scaling)\s+is\s+None\s*:'),
|
| 123 |
+
lambda m:f'if not {m.group(1)} or ({m.group(1)}.get("type") or {m.group(1)}.get("rope_type") or "default")=="default":'),
|
| 124 |
+
(re.compile(r'if\s+(self\.config\.rope_scaling)\s+is\s+not\s+None\s*:'),
|
| 125 |
+
lambda m:f'if {m.group(1)} and ({m.group(1)}.get("type") or {m.group(1)}.get("rope_type") or "default")!="default":'),
|
| 126 |
+
(re.compile(r'([A-Za-z_][\w\.]*rope_scaling)\["type"\]'),
|
| 127 |
+
lambda m:f'{m.group(1)}.get("type",{m.group(1)}.get("rope_type","default"))'),
|
| 128 |
+
(re.compile(r'([A-Za-z_][\w\.]*rope_scaling)\["factor"\]'),
|
| 129 |
+
lambda m:f'{m.group(1)}.get("factor",1.0)'),
|
| 130 |
+
# Recent transformers expects mapping {key: source}; remote code declared a list
|
| 131 |
+
(re.compile(r'_tied_weights_keys\s*=\s*\[[^\]]*\]'),
|
| 132 |
+
lambda m:'_tied_weights_keys={}'),
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def _clear_dynamic_module_cache(st_dir):
|
| 136 |
+
base=os.path.basename(os.path.abspath(st_dir).rstrip("\\/"))
|
| 137 |
+
sanitized=base.replace(".","_dot_").replace("-","_hyphen_")
|
| 138 |
+
root=os.environ.get("HF_MODULES_CACHE") or os.path.join(os.path.expanduser("~"),".cache","huggingface","modules")
|
| 139 |
+
root=os.path.join(root,"transformers_modules")
|
| 140 |
+
if os.path.isdir(root):
|
| 141 |
+
for name in os.listdir(root):
|
| 142 |
+
if name in(base,sanitized) or base.lower() in name.lower():
|
| 143 |
+
shutil.rmtree(os.path.join(root,name),ignore_errors=True)
|
| 144 |
+
for mod in [m for m in list(sys.modules) if "transformers_modules" in m]:
|
| 145 |
+
sys.modules.pop(mod,None)
|
| 146 |
+
|
| 147 |
+
def patch_remote_code_compat(st_dir):
|
| 148 |
+
# Match transformers rope_scaling default dict to remote code guard
|
| 149 |
+
patched=[]
|
| 150 |
+
for fn in sorted(os.listdir(st_dir)):
|
| 151 |
+
if not fn.endswith(".py"):continue
|
| 152 |
+
p=os.path.join(st_dir,fn)
|
| 153 |
+
try:
|
| 154 |
+
with open(p,"r",encoding="utf-8") as f:src=f.read()
|
| 155 |
+
except Exception:continue
|
| 156 |
+
new=src;n=0
|
| 157 |
+
for rx,fix in _ROPE_RULES:
|
| 158 |
+
new,c=rx.subn(fix,new)
|
| 159 |
+
n+=c
|
| 160 |
+
if n:
|
| 161 |
+
bak=p+".bak-00"
|
| 162 |
+
if not os.path.exists(bak):shutil.copy2(p,bak)
|
| 163 |
+
with open(p,"w",encoding="utf-8",newline="\n") as f:f.write(new)
|
| 164 |
+
patched.append(f"{fn}×{n}")
|
| 165 |
+
if patched:
|
| 166 |
+
_clear_dynamic_module_cache(st_dir)
|
| 167 |
+
print(f" ℹ Remote code compat (rope_scaling): {', '.join(patched)}")
|
| 168 |
+
return bool(patched)
|
| 169 |
+
|
| 170 |
+
def normalize_rope_config(st_dir):
|
| 171 |
+
p=os.path.join(st_dir,"config.json")
|
| 172 |
+
cfg=read_json(p)
|
| 173 |
+
if not isinstance(cfg,dict):return False
|
| 174 |
+
changed=[]
|
| 175 |
+
holders=[cfg]
|
| 176 |
+
if isinstance(cfg.get("text_config"),dict):holders.append(cfg["text_config"])
|
| 177 |
+
for holder in holders:
|
| 178 |
+
rs=holder.get("rope_scaling")
|
| 179 |
+
if not isinstance(rs,dict):continue
|
| 180 |
+
rtype=rs.get("type") or rs.get("rope_type")
|
| 181 |
+
if rtype in(None,"default"):
|
| 182 |
+
holder["rope_scaling"]=None
|
| 183 |
+
changed.append("rope_scaling=null (default)")
|
| 184 |
+
elif "type" not in rs:
|
| 185 |
+
rs["type"]=rtype
|
| 186 |
+
changed.append(f'rope_scaling.type="{rtype}"')
|
| 187 |
+
if rtype not in(None,"default","linear","dynamic"):
|
| 188 |
+
print(f' ℹ rope_scaling detected: {json.dumps(rs)}')
|
| 189 |
+
if changed:
|
| 190 |
+
bak=p+".bak-00"
|
| 191 |
+
if not os.path.exists(bak):shutil.copy2(p,bak)
|
| 192 |
+
with open(p,"w",encoding="utf-8") as f:json.dump(cfg,f,indent=2)
|
| 193 |
+
print(f" ℹ config.json normalized: {', '.join(changed)}")
|
| 194 |
+
return bool(changed)
|
| 195 |
+
|
| 196 |
+
def _scan_for_arch(arch):
|
| 197 |
+
roots=[CONVERTER,os.path.join(LLAMA_CPP,"conversion"),os.path.join(LLAMA_CPP,"gguf-py","gguf")]
|
| 198 |
+
for r in roots:
|
| 199 |
+
if os.path.isfile(r):files=[r]
|
| 200 |
+
elif os.path.isdir(r):files=[os.path.join(dp,f) for dp,_,fs in os.walk(r) for f in fs if f.endswith(".py")]
|
| 201 |
+
else:continue
|
| 202 |
+
for f in files:
|
| 203 |
+
try:
|
| 204 |
+
with open(f,"r",encoding="utf-8",errors="ignore") as fh:
|
| 205 |
+
if arch in fh.read():return True
|
| 206 |
+
except Exception:pass
|
| 207 |
+
return False
|
| 208 |
+
|
| 209 |
+
def ensure_arch_support(arch):
|
| 210 |
+
if not arch or arch=="?" or _scan_for_arch(arch):return True
|
| 211 |
+
print(f"\n[Setup] {arch} missing from llama.cpp — updating repository …")
|
| 212 |
+
if os.path.isdir(os.path.join(LLAMA_CPP,".git")):
|
| 213 |
+
subprocess.run(["git","-C",LLAMA_CPP,"pull","--ff-only"])
|
| 214 |
+
if _scan_for_arch(arch):
|
| 215 |
+
print(f" ✓ {arch} supported after update")
|
| 216 |
+
return True
|
| 217 |
+
print(f" ⚠ {arch} missing from master — support in review (llama.cpp PR #25994).")
|
| 218 |
+
print(f' PR branch: git -C "{LLAMA_CPP}" fetch origin pull/25994/head:nanbeige && git -C "{LLAMA_CPP}" checkout nanbeige')
|
| 219 |
+
return False
|
| 220 |
+
|
| 221 |
+
_MTP_PROJ_HINT=("eh_proj",".fc.","pre_fc_norm","enorm","hnorm","embed_proj")
|
| 222 |
+
|
| 223 |
+
def _is_mtp_key(k):
|
| 224 |
+
return k.startswith("mtp.") or "nextn." in k
|
| 225 |
+
|
| 226 |
+
def _has_mtp_projector(keys):
|
| 227 |
+
# Distinguishes complete MTP fusion head from bare attention blocks
|
| 228 |
+
return any(any(h in k for h in _MTP_PROJ_HINT) for k in keys if _is_mtp_key(k))
|
| 229 |
+
|
| 230 |
+
def _find_mtp_sidecar(st_dir):
|
| 231 |
+
cands=sorted(f for f in os.listdir(st_dir) if f.startswith("model-mtp") and f.endswith(".safetensors"))
|
| 232 |
+
if not cands:
|
| 233 |
+
for f in sorted(os.listdir(st_dir)):
|
| 234 |
+
if not f.endswith(".safetensors"):continue
|
| 235 |
+
try:hdr=read_safetensors_header(os.path.join(st_dir,f))
|
| 236 |
+
except Exception:continue
|
| 237 |
+
keys=[k for k in hdr if k!="__metadata__"]
|
| 238 |
+
if keys and all(_is_mtp_key(k) for k in keys):cands.append(f)
|
| 239 |
+
if not cands:return None
|
| 240 |
+
for f in cands:
|
| 241 |
+
if "restored" in f.lower():return os.path.join(st_dir,f)
|
| 242 |
+
return os.path.join(st_dir,cands[0])
|
| 243 |
+
|
| 244 |
+
def _dir_has_weights(st_dir):
|
| 245 |
+
return os.path.isdir(st_dir) and any(f.endswith(".safetensors") for f in os.listdir(st_dir))
|
| 246 |
+
|
| 247 |
+
def _iter_st_headers(st_dir):
|
| 248 |
+
idx=read_json(os.path.join(st_dir,"model.safetensors.index.json"))
|
| 249 |
+
names=sorted(set((idx or {}).get("weight_map",{}).values()))
|
| 250 |
+
if not names:
|
| 251 |
+
names=sorted(f for f in os.listdir(st_dir) if f.endswith(".safetensors"))
|
| 252 |
+
for fn in names:
|
| 253 |
+
p=fn if os.path.isabs(fn) else os.path.join(st_dir,fn)
|
| 254 |
+
if not os.path.isfile(p):continue
|
| 255 |
+
try:yield fn,read_safetensors_header(p)
|
| 256 |
+
except Exception:continue
|
| 257 |
+
|
| 258 |
+
def _dir_looks_quantized(st_dir):
|
| 259 |
+
if not os.path.isdir(st_dir):return False
|
| 260 |
+
for _,hdr in _iter_st_headers(st_dir):
|
| 261 |
+
if any(k.endswith((".qweight",".scales",".qzeros")) for k in hdr if k!="__metadata__"):
|
| 262 |
+
return True
|
| 263 |
+
return False
|
| 264 |
+
|
| 265 |
+
def _resolve_export_dir(path):
|
| 266 |
+
if not os.path.isdir(path):return path
|
| 267 |
+
if os.path.isfile(os.path.join(path,"config.json")) or _dir_has_weights(path):
|
| 268 |
+
return path
|
| 269 |
+
for name in sorted(os.listdir(path)):
|
| 270 |
+
sub=os.path.join(path,name)
|
| 271 |
+
if os.path.isdir(sub) and(os.path.isfile(os.path.join(sub,"config.json")) or _dir_has_weights(sub)):
|
| 272 |
+
return sub
|
| 273 |
+
return path
|
| 274 |
+
|
| 275 |
+
def _guess_source_dir(work_dir):
|
| 276 |
+
for suf in("_AutoRound_W4A16","_dequant_bf16"):
|
| 277 |
+
if work_dir.endswith(suf):
|
| 278 |
+
cand=work_dir[:-len(suf)]
|
| 279 |
+
if os.path.isdir(cand):return cand
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
def _ensure_export_sidecars(src_dir,dst_dir):
|
| 283 |
+
if not src_dir or not os.path.isdir(src_dir) or not os.path.isdir(dst_dir):return
|
| 284 |
+
copied=[]
|
| 285 |
+
for fn in os.listdir(src_dir):
|
| 286 |
+
if fn.endswith((".safetensors",".bin")) or fn=="model.safetensors.index.json":continue
|
| 287 |
+
low=fn.lower()
|
| 288 |
+
if not(low.endswith((".json",".model",".tiktoken",".txt",".py")) or "tokenizer" in low):continue
|
| 289 |
+
src,dst=os.path.join(src_dir,fn),os.path.join(dst_dir,fn)
|
| 290 |
+
if os.path.isfile(src) and not os.path.isfile(dst):
|
| 291 |
+
shutil.copy2(src,dst);copied.append(fn)
|
| 292 |
+
cfg_p=os.path.join(dst_dir,"config.json")
|
| 293 |
+
cfg=read_json(cfg_p)
|
| 294 |
+
qstandalone=read_json(os.path.join(dst_dir,"quantization_config.json"))
|
| 295 |
+
if isinstance(cfg,dict) and not cfg.get("quantization_config") and(_dir_looks_quantized(dst_dir) or qstandalone):
|
| 296 |
+
cfg["quantization_config"]=qstandalone or {"quant_method":"auto-round","bits":4,"group_size":128,"sym":True,"format":"auto_round"}
|
| 297 |
+
with open(cfg_p,"w",encoding="utf-8") as f:json.dump(cfg,f,indent=2)
|
| 298 |
+
copied.append("quantization_config")
|
| 299 |
+
if copied:
|
| 300 |
+
print(f" ℹ Restored export sidecars in {os.path.basename(dst_dir)}: {', '.join(copied)}")
|
| 301 |
+
|
| 302 |
+
def _export_mtime(path):
|
| 303 |
+
best=0.0
|
| 304 |
+
try:entries=os.listdir(path)
|
| 305 |
+
except OSError:return 0.0
|
| 306 |
+
for fn in entries:
|
| 307 |
+
if not fn.endswith(".safetensors"):continue
|
| 308 |
+
try:best=max(best,os.path.getmtime(os.path.join(path,fn)))
|
| 309 |
+
except OSError:pass
|
| 310 |
+
return best
|
| 311 |
+
|
| 312 |
+
def _autoround_name_hit(base,name):
|
| 313 |
+
low=name.lower()
|
| 314 |
+
pref=base.lower()
|
| 315 |
+
if low==pref:return False
|
| 316 |
+
if "dequant" in low:return False
|
| 317 |
+
if not(low.startswith(pref) or pref in low):return False
|
| 318 |
+
return any(tag in low for tag in("-w4g128","_w4g128","-w4a16","_autoround","-auto_round","autoround_w4"))
|
| 319 |
+
|
| 320 |
+
def find_autoround_export(st_dir,explicit=None):
|
| 321 |
+
cands=[]
|
| 322 |
+
if explicit:
|
| 323 |
+
p=os.path.abspath(explicit)
|
| 324 |
+
cands.append(_resolve_export_dir(p) if os.path.isdir(p) else p)
|
| 325 |
+
base=os.path.basename(st_dir.rstrip("\\/"))
|
| 326 |
+
fixed=[
|
| 327 |
+
os.path.join(SCRIPT_DIR,f"{base}_AutoRound_W4A16"),
|
| 328 |
+
os.path.join(SCRIPT_DIR,f"{base}-w4g128"),
|
| 329 |
+
os.path.join(SCRIPT_DIR,f"{base}-W4A16"),
|
| 330 |
+
os.path.join(os.path.dirname(st_dir),f"{base}-w4g128"),
|
| 331 |
+
]
|
| 332 |
+
try:names=os.listdir(SCRIPT_DIR)
|
| 333 |
+
except OSError:names=[]
|
| 334 |
+
for n in names:
|
| 335 |
+
if _autoround_name_hit(base,n):
|
| 336 |
+
fixed.append(os.path.join(SCRIPT_DIR,n))
|
| 337 |
+
seen=set()
|
| 338 |
+
for p in fixed:
|
| 339 |
+
p=_resolve_export_dir(p)
|
| 340 |
+
if p in seen or not os.path.isdir(p):continue
|
| 341 |
+
seen.add(p)
|
| 342 |
+
cands.append(p)
|
| 343 |
+
best=None
|
| 344 |
+
best_score=-1.0
|
| 345 |
+
for p in cands:
|
| 346 |
+
if not _dir_has_weights(p):continue
|
| 347 |
+
score=_export_mtime(p)
|
| 348 |
+
if _dir_looks_quantized(p):score+=1e15
|
| 349 |
+
if score>best_score:
|
| 350 |
+
best,best_score=p,score
|
| 351 |
+
return best
|
| 352 |
+
|
| 353 |
+
def detect_model(st_dir):
|
| 354 |
+
cfg=read_json(os.path.join(st_dir,"config.json"))
|
| 355 |
+
if not cfg:
|
| 356 |
+
if not _dir_has_weights(st_dir):return None
|
| 357 |
+
cfg={"architectures":["?"],"model_type":"?"}
|
| 358 |
+
idx=read_json(os.path.join(st_dir,"model.safetensors.index.json"))
|
| 359 |
+
qcfg=cfg.get("quantization_config",{})
|
| 360 |
+
info={
|
| 361 |
+
"arch":cfg.get("architectures",["?"])[0],
|
| 362 |
+
"type":cfg.get("model_type","?"),
|
| 363 |
+
"multimodal":"vision_config" in cfg or "audio_config" in cfg,
|
| 364 |
+
"mtp":False,
|
| 365 |
+
"mtp_declared":False,
|
| 366 |
+
"mtp_complete":False,
|
| 367 |
+
"mtp_in_index":False,
|
| 368 |
+
"mtp_shard":None,
|
| 369 |
+
"mtp_standalone":False,
|
| 370 |
+
"mtp_file":None,
|
| 371 |
+
"quantized":bool(qcfg),
|
| 372 |
+
"quant_method":qcfg.get("quant_method",qcfg.get("format","?")),
|
| 373 |
+
"quant_format":qcfg.get("format",qcfg.get("bits","?")),
|
| 374 |
+
"bits":qcfg.get("bits",qcfg.get("weight_bits",4)),
|
| 375 |
+
"group_size":qcfg.get("group_size",128),
|
| 376 |
+
"sym":qcfg.get("sym",True),
|
| 377 |
+
"shards":[],
|
| 378 |
+
"total_params":None,
|
| 379 |
+
"source_dir":st_dir,
|
| 380 |
+
"config":cfg,
|
| 381 |
+
}
|
| 382 |
+
text_cfg=cfg.get("text_config") if isinstance(cfg.get("text_config"),dict) else {}
|
| 383 |
+
if text_cfg.get("mtp_num_hidden_layers",0)>0 or cfg.get("mtp_num_hidden_layers",0)>0:
|
| 384 |
+
info["mtp_declared"]=True
|
| 385 |
+
if idx:
|
| 386 |
+
wm=idx.get("weight_map",{})
|
| 387 |
+
info["shards"]=sorted(set(wm.values()))
|
| 388 |
+
tp=idx.get("metadata",{}).get("total_parameters")
|
| 389 |
+
if isinstance(tp,(int,float)) and tp>1e8:
|
| 390 |
+
info["total_params"]=int(tp)
|
| 391 |
+
mtp_keys=[k for k in wm if _is_mtp_key(k)]
|
| 392 |
+
if mtp_keys:
|
| 393 |
+
info["mtp_declared"]=True
|
| 394 |
+
info["mtp_in_index"]=True
|
| 395 |
+
info["mtp_shard"]=wm[mtp_keys[0]]
|
| 396 |
+
if _has_mtp_projector(wm):
|
| 397 |
+
info["mtp_complete"]=True
|
| 398 |
+
if any("visual" in k or "vision" in k for k in wm):
|
| 399 |
+
info["multimodal"]=True
|
| 400 |
+
if not info["total_params"]:
|
| 401 |
+
info["total_params"]=_estimate_params(cfg)
|
| 402 |
+
if not info["total_params"]:
|
| 403 |
+
st_bytes=sum(os.path.getsize(os.path.join(st_dir,f)) for f in os.listdir(st_dir) if f.endswith(".safetensors"))
|
| 404 |
+
if st_bytes>0:info["total_params"]=st_bytes//2
|
| 405 |
+
sidecar=_find_mtp_sidecar(st_dir)
|
| 406 |
+
if sidecar:
|
| 407 |
+
hdr_keys=[k for k in read_safetensors_header(sidecar) if k!="__metadata__"]
|
| 408 |
+
info["mtp_file"]=os.path.basename(sidecar)
|
| 409 |
+
info["mtp_declared"]=True
|
| 410 |
+
if _has_mtp_projector(hdr_keys):
|
| 411 |
+
info["mtp_complete"]=True
|
| 412 |
+
if not info["mtp_in_index"]:
|
| 413 |
+
info["mtp_standalone"]=True
|
| 414 |
+
info["mtp"]=info["mtp_declared"] and info["mtp_complete"]
|
| 415 |
+
if not info["quantized"] and _dir_looks_quantized(st_dir):
|
| 416 |
+
info["quantized"]=True
|
| 417 |
+
info["quant_method"]="auto-round" if info["quant_method"]=="?" else info["quant_method"]
|
| 418 |
+
info["quant_format"]="auto_round" if info["quant_format"]=="?" else info["quant_format"]
|
| 419 |
+
info["bits"]=int(info.get("bits") or 4)
|
| 420 |
+
info["group_size"]=int(info.get("group_size") or 128)
|
| 421 |
+
return info
|
| 422 |
+
|
| 423 |
+
def unpack_gptq_autoround(qweight,scales,qzeros,g_idx,bits=4,group_size=128,sym=True):
|
| 424 |
+
dev=qweight.device
|
| 425 |
+
pf=32 // bits
|
| 426 |
+
in_feat=qweight.shape[0]*pf
|
| 427 |
+
out_feat=qweight.shape[1]
|
| 428 |
+
shifts=torch.arange(0,32,bits,device=dev,dtype=torch.int32)
|
| 429 |
+
qw=qweight.to(torch.int32)
|
| 430 |
+
q_unp=(qw.unsqueeze(1) >> shifts.view(1,pf,1)) & ((1 << bits)-1)
|
| 431 |
+
q_unp=q_unp.view(in_feat,out_feat).to(torch.float32)
|
| 432 |
+
if qzeros is not None and qzeros.numel()>0:
|
| 433 |
+
if qzeros.dtype in(torch.int32,torch.int64):
|
| 434 |
+
qz=qzeros.to(torch.int32)
|
| 435 |
+
z_unp=(qz.unsqueeze(2) >> shifts.view(1,1,pf)) & ((1 << bits)-1)
|
| 436 |
+
z_unp=z_unp.view(qz.shape[0],-1)[:,:out_feat].to(torch.float32)
|
| 437 |
+
z_unp=z_unp+1
|
| 438 |
+
else:
|
| 439 |
+
z_unp=qzeros.to(torch.float32)
|
| 440 |
+
else:
|
| 441 |
+
z_unp=torch.full((scales.shape[0],out_feat),2**(bits-1) if sym else 0.0,device=dev,dtype=torch.float32)
|
| 442 |
+
ng=scales.shape[0]
|
| 443 |
+
calc_gs=in_feat // ng if ng>0 else group_size
|
| 444 |
+
gs=group_size if group_size>0 else calc_gs
|
| 445 |
+
if g_idx is not None and g_idx.numel()>0 and not torch.all(g_idx==torch.arange(in_feat,device=dev) // gs):
|
| 446 |
+
scale_map=scales[g_idx].to(torch.float32)
|
| 447 |
+
zero_map=z_unp[g_idx].to(torch.float32)
|
| 448 |
+
else:
|
| 449 |
+
scale_map=scales.to(torch.float32).repeat_interleave(gs,dim=0)[:in_feat,:]
|
| 450 |
+
zero_map=z_unp.repeat_interleave(gs,dim=0)[:in_feat,:]
|
| 451 |
+
w=(q_unp-zero_map)*scale_map
|
| 452 |
+
return w.t().contiguous().to(torch.bfloat16)
|
| 453 |
+
|
| 454 |
+
def unpack_awq(qweight,scales,qzeros,bits=4,group_size=128):
|
| 455 |
+
dev=qweight.device
|
| 456 |
+
pf=32 // bits
|
| 457 |
+
in_feat=qweight.shape[0]
|
| 458 |
+
out_feat=qweight.shape[1]*pf
|
| 459 |
+
shifts=torch.arange(0,32,bits,device=dev,dtype=torch.int32)
|
| 460 |
+
qw=qweight.to(torch.int32)
|
| 461 |
+
q_unp=(qw.unsqueeze(2) >> shifts.view(1,1,pf)) & ((1 << bits)-1)
|
| 462 |
+
q_unp=q_unp.view(in_feat,out_feat).to(torch.float32)
|
| 463 |
+
if qzeros is not None and qzeros.numel()>0:
|
| 464 |
+
qz=qzeros.to(torch.int32)
|
| 465 |
+
z_unp=(qz.unsqueeze(2) >> shifts.view(1,1,pf)) & ((1 << bits)-1)
|
| 466 |
+
z_unp=z_unp.view(qz.shape[0],-1).to(torch.float32)
|
| 467 |
+
else:
|
| 468 |
+
z_unp=torch.full((scales.shape[0],out_feat),0.0,device=dev,dtype=torch.float32)
|
| 469 |
+
ng=scales.shape[0]
|
| 470 |
+
gs=group_size if group_size>0 else(in_feat // ng if ng>0 else 128)
|
| 471 |
+
scale_map=scales.to(torch.float32).repeat_interleave(gs,dim=0)[:in_feat,:]
|
| 472 |
+
zero_map=z_unp.repeat_interleave(gs,dim=0)[:in_feat,:]
|
| 473 |
+
w=(q_unp-zero_map)*scale_map
|
| 474 |
+
return w.t().contiguous().to(torch.bfloat16)
|
| 475 |
+
|
| 476 |
+
def dequantize_shard(shard_path,info):
|
| 477 |
+
tensors=load_file(shard_path,device="cpu")
|
| 478 |
+
out_tensors={}
|
| 479 |
+
consumed=set()
|
| 480 |
+
int4_keys=set()
|
| 481 |
+
bits=int(info.get("bits",4))
|
| 482 |
+
group_size=int(info.get("group_size",128))
|
| 483 |
+
sym=bool(info.get("sym",True))
|
| 484 |
+
qmethod=str(info.get("quant_method","")).lower()
|
| 485 |
+
for k,t in tensors.items():
|
| 486 |
+
if k in consumed:continue
|
| 487 |
+
if k.endswith(".qweight"):
|
| 488 |
+
base=k[:-8]
|
| 489 |
+
int4_keys.add(base)
|
| 490 |
+
qw=t
|
| 491 |
+
scales=tensors.get(f"{base}.scales",tensors.get(f"{base}.scale"))
|
| 492 |
+
qzeros=tensors.get(f"{base}.qzeros",tensors.get(f"{base}.zeros"))
|
| 493 |
+
g_idx=tensors.get(f"{base}.g_idx")
|
| 494 |
+
consumed.update([k,f"{base}.scales",f"{base}.scale",f"{base}.qzeros",f"{base}.zeros",f"{base}.g_idx"])
|
| 495 |
+
if scales is not None:
|
| 496 |
+
if "awq" in qmethod:
|
| 497 |
+
out_tensors[f"{base}.weight"]=unpack_awq(qw,scales,qzeros,bits,group_size)
|
| 498 |
+
else:
|
| 499 |
+
out_tensors[f"{base}.weight"]=unpack_gptq_autoround(qw,scales,qzeros,g_idx,bits,group_size,sym)
|
| 500 |
+
else:
|
| 501 |
+
out_tensors[k]=t
|
| 502 |
+
elif k.endswith(".weight") and(f"{k[:-7]}.scales" in tensors or f"{k[:-7]}.scale" in tensors):
|
| 503 |
+
base=k[:-7]
|
| 504 |
+
qw=t
|
| 505 |
+
scales=tensors.get(f"{base}.scales",tensors.get(f"{base}.scale"))
|
| 506 |
+
qzeros=tensors.get(f"{base}.qzeros",tensors.get(f"{base}.zeros"))
|
| 507 |
+
g_idx=tensors.get(f"{base}.g_idx")
|
| 508 |
+
consumed.update([k,f"{base}.scales",f"{base}.scale",f"{base}.qzeros",f"{base}.zeros",f"{base}.g_idx"])
|
| 509 |
+
if qw.dtype in(torch.int32,torch.int64,torch.int8,torch.uint8):
|
| 510 |
+
int4_keys.add(base)
|
| 511 |
+
out_tensors[f"{base}.weight"]=unpack_gptq_autoround(qw,scales,qzeros,g_idx,bits,group_size,sym)
|
| 512 |
+
else:
|
| 513 |
+
out_tensors[f"{base}.weight"]=qw.to(torch.bfloat16)
|
| 514 |
+
elif k.endswith(".weight") and f"{k[:-7]}.weight_scale_inv" in tensors:
|
| 515 |
+
base=k[:-7]
|
| 516 |
+
inv=tensors[f"{base}.weight_scale_inv"].to(torch.float32)
|
| 517 |
+
consumed.update([k,f"{base}.weight_scale_inv"])
|
| 518 |
+
out_tensors[f"{base}.weight"]=(t.to(torch.float32)*inv).to(torch.bfloat16)
|
| 519 |
+
else:
|
| 520 |
+
out_tensors[k]=t.to(torch.bfloat16) if t.dtype==torch.float16 else t
|
| 521 |
+
return out_tensors,int4_keys
|
| 522 |
+
|
| 523 |
+
def dequantize_model_to_dir(st_dir,out_dir,info):
|
| 524 |
+
os.makedirs(out_dir,exist_ok=True)
|
| 525 |
+
print(f"\n[Dequant] Streaming AutoRound ({info['quant_method']}/{info['bits']}bit) → BF16 safetensors")
|
| 526 |
+
st_files=sorted(f for f in os.listdir(st_dir) if f.endswith(".safetensors"))
|
| 527 |
+
idx_path=os.path.join(st_dir,"model.safetensors.index.json")
|
| 528 |
+
new_weight_map={}
|
| 529 |
+
int4_keys=set()
|
| 530 |
+
for i,fn in enumerate(st_files,1):
|
| 531 |
+
src_p=os.path.join(st_dir,fn)
|
| 532 |
+
dst_p=os.path.join(out_dir,fn)
|
| 533 |
+
print(f" [{i}/{len(st_files)}] Dequantizing shard: {fn} …")
|
| 534 |
+
unpacked,shard_int4=dequantize_shard(src_p,info)
|
| 535 |
+
int4_keys|=shard_int4
|
| 536 |
+
save_file(unpacked,dst_p,metadata={"format":"pt"})
|
| 537 |
+
for k in unpacked:new_weight_map[k]=fn
|
| 538 |
+
for fn in os.listdir(st_dir):
|
| 539 |
+
src=os.path.join(st_dir,fn)
|
| 540 |
+
dst=os.path.join(out_dir,fn)
|
| 541 |
+
if fn.endswith(".json") and fn!="model.safetensors.index.json":
|
| 542 |
+
cfg_data=read_json(src)
|
| 543 |
+
if fn=="config.json" and isinstance(cfg_data,dict):
|
| 544 |
+
cfg_data.pop("quantization_config",None)
|
| 545 |
+
with open(dst,"w",encoding="utf-8") as f:json.dump(cfg_data,f,indent=2)
|
| 546 |
+
elif fn.endswith((".model",".tiktoken",".txt",".py",".jinja")):
|
| 547 |
+
shutil.copy2(src,dst)
|
| 548 |
+
if os.path.isfile(idx_path):
|
| 549 |
+
orig_idx=read_json(idx_path)
|
| 550 |
+
orig_idx["weight_map"]=new_weight_map
|
| 551 |
+
with open(os.path.join(out_dir,"model.safetensors.index.json"),"w",encoding="utf-8") as f:
|
| 552 |
+
json.dump(orig_idx,f,indent=2)
|
| 553 |
+
if int4_keys:
|
| 554 |
+
layers=set();subs=set();globals_=[]
|
| 555 |
+
for k in sorted(int4_keys):
|
| 556 |
+
m=re.search(r"\.layers\.(\d+)\.(.+)$",k)
|
| 557 |
+
if m:
|
| 558 |
+
layers.add(int(m.group(1)));subs.add(m.group(2))
|
| 559 |
+
else:
|
| 560 |
+
globals_.append(k)
|
| 561 |
+
info["int4_layers"]=sorted(layers)
|
| 562 |
+
info["int4_submodules"]=sorted(subs)
|
| 563 |
+
if globals_:info["int4_globals"]=globals_
|
| 564 |
+
print(f" ℹ Source int4: {len(layers)} layer(s), {len(subs)} submodule(s)"+(f", {len(globals_)} global tensor(s)" if globals_ else ""))
|
| 565 |
+
print(f" ✓ Reconstructed pristine BF16 model in: {out_dir}\n")
|
| 566 |
+
return out_dir
|
| 567 |
+
|
| 568 |
+
def _estimate_params(cfg):
|
| 569 |
+
if not isinstance(cfg,dict):return None
|
| 570 |
+
c=cfg.get("text_config") if isinstance(cfg.get("text_config"),dict) else cfg
|
| 571 |
+
h=c.get("hidden_size") or cfg.get("hidden_size")
|
| 572 |
+
L=c.get("num_hidden_layers") or c.get("num_layers") or cfg.get("num_hidden_layers")
|
| 573 |
+
V=c.get("vocab_size") or cfg.get("vocab_size") or 152064
|
| 574 |
+
if not(h and L):return None
|
| 575 |
+
inter=c.get("intermediate_size") or cfg.get("intermediate_size") or 4*h
|
| 576 |
+
num_heads=c.get("num_attention_heads") or cfg.get("num_attention_heads") or 1
|
| 577 |
+
head=c.get("head_dim") or cfg.get("head_dim") or h//max(num_heads,1)
|
| 578 |
+
kv_heads=c.get("num_key_value_heads") or cfg.get("num_key_value_heads") or num_heads
|
| 579 |
+
kv=head*kv_heads
|
| 580 |
+
attn=2*h*h+2*h*kv
|
| 581 |
+
moe=c.get("num_experts") or c.get("n_routed_experts") or cfg.get("num_experts") or 0
|
| 582 |
+
mlp=3*h*inter*(moe if moe else 1)
|
| 583 |
+
return L*(attn+mlp)+V*h*(1 if(c.get("tie_word_embeddings") or cfg.get("tie_word_embeddings")) else 2)
|
| 584 |
+
|
| 585 |
+
def _fast_path_ready():
|
| 586 |
+
return _has_mod("fla") and _has_mod("causal_conv1d")
|
| 587 |
+
|
| 588 |
+
def _hybrid_linear_attn(cfg):
|
| 589 |
+
if not isinstance(cfg,dict):return False
|
| 590 |
+
blob=json.dumps(cfg).lower()
|
| 591 |
+
return any(t in blob for t in("linear_attention","gated_delta","linear_attn","mamba"))
|
| 592 |
+
|
| 593 |
+
def auto_tune_profile(info,device,vram):
|
| 594 |
+
p=(info or {}).get("total_params") or _estimate_params((info or {}).get("config") or {})
|
| 595 |
+
b=(p or 8e9)/1e9
|
| 596 |
+
if device=="cpu":iters=max(20,min(50,int(200/max(b,1))))
|
| 597 |
+
else:iters=50
|
| 598 |
+
return iters,p
|
| 599 |
+
|
| 600 |
+
def _autoround_device():
|
| 601 |
+
if torch.cuda.is_available():return "cuda"
|
| 602 |
+
xpu=getattr(torch,"xpu",None)
|
| 603 |
+
if xpu is not None and xpu.is_available():return "xpu"
|
| 604 |
+
return "cpu"
|
| 605 |
+
|
| 606 |
+
def _download_file(url,dest):
|
| 607 |
+
os.makedirs(os.path.dirname(dest) or ".",exist_ok=True)
|
| 608 |
+
print(f" [DL] {os.path.basename(dest)}")
|
| 609 |
+
urllib.request.urlretrieve(url,dest)
|
| 610 |
+
print(f" [DONE] {os.path.basename(dest)} ({os.path.getsize(dest)/1024/1024:.2f} MB)")
|
| 611 |
+
|
| 612 |
+
def _load_jsonl_texts(path):
|
| 613 |
+
out=[]
|
| 614 |
+
with open(path,"r",encoding="utf-8",errors="replace") as f:
|
| 615 |
+
for line in f:
|
| 616 |
+
line=line.strip()
|
| 617 |
+
if not line:continue
|
| 618 |
+
try:obj=json.loads(line)
|
| 619 |
+
except Exception:out.append(line);continue
|
| 620 |
+
if isinstance(obj,str):out.append(obj)
|
| 621 |
+
elif isinstance(obj,dict):
|
| 622 |
+
t=obj.get("text") or obj.get("content") or ""
|
| 623 |
+
if t:out.append(t)
|
| 624 |
+
return out
|
| 625 |
+
|
| 626 |
+
def _load_txt_texts(path):
|
| 627 |
+
with open(path,"r",encoding="utf-8",errors="replace") as f:text=f.read()
|
| 628 |
+
if "‗" in text:
|
| 629 |
+
return [b.strip() for b in re.split(r"\n(?=‗user‗)",text) if len(b.strip())>80]
|
| 630 |
+
samples=[]
|
| 631 |
+
for p in re.split(r"\n{2,}",text):
|
| 632 |
+
p=p.strip()
|
| 633 |
+
if not p:continue
|
| 634 |
+
if len(p)>50000 and p.count("\n")>100:
|
| 635 |
+
samples.extend(ln.strip() for ln in p.splitlines() if len(ln.strip())>=200)
|
| 636 |
+
elif len(p)>=200:
|
| 637 |
+
samples.append(p)
|
| 638 |
+
if not samples:
|
| 639 |
+
samples=[ln.strip() for ln in text.splitlines() if len(ln.strip())>=200]
|
| 640 |
+
return samples
|
| 641 |
+
|
| 642 |
+
def _load_calib_texts(path):
|
| 643 |
+
low=path.lower()
|
| 644 |
+
if low.endswith(".json") and not low.endswith(".jsonl"):
|
| 645 |
+
data=read_json(path)
|
| 646 |
+
if isinstance(data,list):
|
| 647 |
+
out=[]
|
| 648 |
+
for obj in data:
|
| 649 |
+
if isinstance(obj,str) and obj.strip():out.append(obj)
|
| 650 |
+
elif isinstance(obj,dict):
|
| 651 |
+
t=obj.get("text") or obj.get("content") or ""
|
| 652 |
+
if t:out.append(t)
|
| 653 |
+
return out
|
| 654 |
+
if isinstance(data,dict):
|
| 655 |
+
t=data.get("text") or data.get("content") or ""
|
| 656 |
+
return [t] if t else []
|
| 657 |
+
if low.endswith(".jsonl"):
|
| 658 |
+
return _load_jsonl_texts(path)
|
| 659 |
+
return _load_txt_texts(path)
|
| 660 |
+
|
| 661 |
+
def _stable_chunk(tokenizer,ids,seqlen):
|
| 662 |
+
# Enforce round-trip token count stability against edge BPE merges
|
| 663 |
+
for _ in range(4):
|
| 664 |
+
text=tokenizer.decode(ids,skip_special_tokens=False)
|
| 665 |
+
back=tokenizer(text,add_special_tokens=False,truncation=False)["input_ids"]
|
| 666 |
+
if back and isinstance(back[0],list):back=back[0]
|
| 667 |
+
n=len(back)
|
| 668 |
+
if n==seqlen:return text
|
| 669 |
+
if n<seqlen:return None
|
| 670 |
+
ids=back[:seqlen]
|
| 671 |
+
return None
|
| 672 |
+
|
| 673 |
+
def _pack_texts(tokenizer,texts,seqlen,nsamples):
|
| 674 |
+
chunks=[]
|
| 675 |
+
buf=[]
|
| 676 |
+
dropped=0
|
| 677 |
+
stride=max(seqlen//8,1)
|
| 678 |
+
for t in texts:
|
| 679 |
+
if not t:continue
|
| 680 |
+
ids=tokenizer(t,add_special_tokens=False,truncation=False)["input_ids"]
|
| 681 |
+
if ids and isinstance(ids[0],list):ids=ids[0]
|
| 682 |
+
buf.extend(ids)
|
| 683 |
+
while len(buf)>=seqlen+stride:
|
| 684 |
+
text=_stable_chunk(tokenizer,buf[:seqlen+stride],seqlen)
|
| 685 |
+
buf=buf[seqlen:]
|
| 686 |
+
if text is None:
|
| 687 |
+
dropped+=1
|
| 688 |
+
continue
|
| 689 |
+
chunks.append(text)
|
| 690 |
+
if len(chunks)>=nsamples:
|
| 691 |
+
_report_packing(len(chunks),dropped,nsamples)
|
| 692 |
+
return chunks
|
| 693 |
+
_report_packing(len(chunks),dropped,nsamples)
|
| 694 |
+
return chunks
|
| 695 |
+
|
| 696 |
+
def _report_packing(kept,dropped,nsamples):
|
| 697 |
+
if dropped:print(f" ℹ {dropped} block(s) dropped (unstable round-trip length)")
|
| 698 |
+
if kept<nsamples:
|
| 699 |
+
print(f" ⚠ {kept}/{nsamples} samples only — degraded calibration.")
|
| 700 |
+
print(" Expand corpus (--dataset) or reduce --seqlen to densify blocks.")
|
| 701 |
+
if dropped>kept:
|
| 702 |
+
print(" ⚠ Tokenizer rewrites more blocks than kept: unstable BPE round-trip.")
|
| 703 |
+
print(" A seqlen multiple of 512 stabilizes merge boundaries.")
|
| 704 |
+
|
| 705 |
+
def resolve_autoround_dataset(tokenizer,experimental=False,dataset_path=None,seqlen=2048,nsamples=128):
|
| 706 |
+
path=label=None
|
| 707 |
+
if dataset_path:
|
| 708 |
+
cand=os.path.abspath(dataset_path)
|
| 709 |
+
if os.path.isfile(cand):
|
| 710 |
+
path,label=cand,os.path.basename(cand)
|
| 711 |
+
else:
|
| 712 |
+
print(f" ✗ Custom dataset not found: {cand}")
|
| 713 |
+
if path is None and experimental:
|
| 714 |
+
if not os.path.isfile(DEFAULT_EXP_DATASET) and os.path.isfile(CALIB_SCRIPT):
|
| 715 |
+
print("\n[Prep] Building experimental.txt (same flag as 01 --experimental) …")
|
| 716 |
+
subprocess.run([sys.executable,CALIB_SCRIPT,"--dataset-only","--experimental"],cwd=SCRIPT_DIR)
|
| 717 |
+
if os.path.isfile(DEFAULT_EXP_DATASET):
|
| 718 |
+
path,label=DEFAULT_EXP_DATASET,"experimental.txt"
|
| 719 |
+
if path is None:
|
| 720 |
+
if not os.path.isfile(DEFAULT_CALIB_JSONL):
|
| 721 |
+
try:_download_file(DEFAULT_CALIB_JSONL_URL,DEFAULT_CALIB_JSONL)
|
| 722 |
+
except Exception as e:print(f" [WARN] JSONL download failed: {e}")
|
| 723 |
+
if not os.path.isfile(DEFAULT_CALIB_TXT):
|
| 724 |
+
try:_download_file(DEFAULT_CALIB_TXT_URL,DEFAULT_CALIB_TXT)
|
| 725 |
+
except Exception as e:print(f" [WARN] TXT download failed: {e}")
|
| 726 |
+
if os.path.isfile(DEFAULT_CALIB_JSONL):
|
| 727 |
+
path,label=DEFAULT_CALIB_JSONL,DEFAULT_CALIB_JSONL_NAME
|
| 728 |
+
elif os.path.isfile(DEFAULT_CALIB_TXT):
|
| 729 |
+
path,label=DEFAULT_CALIB_TXT,DEFAULT_CALIB_NAME
|
| 730 |
+
if path:
|
| 731 |
+
packed=_pack_texts(tokenizer,_load_calib_texts(path),seqlen,nsamples)
|
| 732 |
+
if packed:
|
| 733 |
+
print(f" Calib: {label} → {len(packed)} packed samples × {seqlen} tokens")
|
| 734 |
+
return packed
|
| 735 |
+
print(f" [WARN] {label} produced no seqlen-sized chunks")
|
| 736 |
+
print(" Fallback: NeelNanda/pile-10k:concat=True")
|
| 737 |
+
return "NeelNanda/pile-10k:concat=True"
|
| 738 |
+
|
| 739 |
+
def run_autoround_tune(st_dir,out_dir,iters=None,experimental=False,dataset_path=None,info=None,bs_override=None,seqlen_override=None):
|
| 740 |
+
if not ensure_python_deps():
|
| 741 |
+
raise SystemExit(1)
|
| 742 |
+
ensure_preprocessor_config(st_dir)
|
| 743 |
+
normalize_rope_config(st_dir)
|
| 744 |
+
patch_remote_code_compat(st_dir)
|
| 745 |
+
from auto_round import AutoRound
|
| 746 |
+
from transformers import AutoTokenizer
|
| 747 |
+
try:tokenizer=AutoTokenizer.from_pretrained(st_dir,trust_remote_code=True)
|
| 748 |
+
except Exception:tokenizer=AutoTokenizer.from_pretrained(st_dir,use_fast=False,trust_remote_code=True)
|
| 749 |
+
if torch.cuda.is_available():
|
| 750 |
+
device="cuda"
|
| 751 |
+
vram=torch.cuda.get_device_properties(0).total_memory/(1024**3)
|
| 752 |
+
gpu_name=torch.cuda.get_device_name(0)
|
| 753 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF","expandable_segments:True")
|
| 754 |
+
else:
|
| 755 |
+
xpu=getattr(torch,"xpu",None)
|
| 756 |
+
device="xpu" if xpu is not None and xpu.is_available() else "cpu"
|
| 757 |
+
vram=0.0
|
| 758 |
+
gpu_name="-"
|
| 759 |
+
if iters is None or iters<=0:
|
| 760 |
+
iters,est=auto_tune_profile(info,device,vram)
|
| 761 |
+
else:
|
| 762 |
+
est=(info or {}).get("total_params") or _estimate_params((info or {}).get("config") or {})
|
| 763 |
+
if not est or est<1e8:
|
| 764 |
+
st_bytes=sum(os.path.getsize(os.path.join(st_dir,f)) for f in os.listdir(st_dir) if f.endswith(".safetensors"))
|
| 765 |
+
est=(st_bytes//2) if st_bytes>0 else 8e9
|
| 766 |
+
gb=est/1e9
|
| 767 |
+
hybrid=_hybrid_linear_attn((info or {}).get("config"))
|
| 768 |
+
fast_ok=_fast_path_ready()
|
| 769 |
+
print(f" Auto profile: ~{gb:.2f}B params, {device} {vram:.1f} GiB → iters={iters}")
|
| 770 |
+
if device=="cuda" and gb>16:print(" ⚠ >16B on 8 GB: long tuning duration; --skip-tune with an existing export is recommended.")
|
| 771 |
+
print(f"\n[AutoRound] Optimizing raw BF16 weights ({iters} iters, W4A16) …")
|
| 772 |
+
# Maintain effective batch budget bs*acc=8; higher bs amortizes kernel launch overhead
|
| 773 |
+
if device=="cpu":
|
| 774 |
+
bs,acc,seqlen,nsamples=1,4,2048,128
|
| 775 |
+
print(" AutoRound device is CPU. A 9B W4A16 tune takes a very long time.")
|
| 776 |
+
print(" Install CUDA 13 torch+torchvision, then retry:")
|
| 777 |
+
print(" python -m pip install --force-reinstall torch torchvision --index-url https://download.pytorch.org/whl/cu130")
|
| 778 |
+
elif gb<=2 and vram>=6:
|
| 779 |
+
bs,acc,seqlen,nsamples=8,1,2048,128
|
| 780 |
+
elif gb<=4 and vram>=8:
|
| 781 |
+
bs,acc,seqlen,nsamples=4,2,2048,128
|
| 782 |
+
elif vram<10:
|
| 783 |
+
bs,acc,seqlen,nsamples=1,8,2048,128
|
| 784 |
+
elif vram<16:
|
| 785 |
+
bs,acc,seqlen,nsamples=1,4,2048,128
|
| 786 |
+
else:
|
| 787 |
+
bs,acc,seqlen,nsamples=4,2,2048,128
|
| 788 |
+
if hybrid and not fast_ok:
|
| 789 |
+
seqlen=min(seqlen,1024)
|
| 790 |
+
print(" ⚠ Hybrid linear attention using torch fallback (fla + causal-conv1d missing).")
|
| 791 |
+
print(f" seqlen capped at {seqlen} — enable fast path: pip install triton-windows flash-linear-attention")
|
| 792 |
+
arch_ov=ARCH_BS_OVERRIDE.get(str((info or {}).get("type","")).lower()) or ARCH_BS_OVERRIDE.get(str((info or {}).get("arch","")).lower())
|
| 793 |
+
if arch_ov and device=="cuda" and arch_ov["bs"]>bs and vram>=arch_ov.get("min_vram",6) and gb<=arch_ov.get("max_gb",8):
|
| 794 |
+
bs,acc=arch_ov["bs"],max(1,(bs*acc)//arch_ov["bs"])
|
| 795 |
+
print(f" ℹ Arch profile ({info['type']}): batch_size={bs} grad_acc={acc}")
|
| 796 |
+
if bs_override and bs_override>0:bs,acc=bs_override,max(1,(bs*acc)//bs_override)
|
| 797 |
+
if seqlen_override and seqlen_override>0:seqlen=seqlen_override
|
| 798 |
+
lr=5e-3 if 0<iters<=50 else None
|
| 799 |
+
print(f" Device: {device} GPU: {gpu_name} VRAM: {vram:.1f} GiB")
|
| 800 |
+
print(f" batch_size={bs} grad_acc={acc} seqlen={seqlen} nsamples={nsamples} iters={iters}")
|
| 801 |
+
dataset=resolve_autoround_dataset(tokenizer,experimental=experimental,dataset_path=dataset_path,seqlen=seqlen,nsamples=nsamples)
|
| 802 |
+
ar=AutoRound(
|
| 803 |
+
model=st_dir,tokenizer=tokenizer,bits=4,group_size=128,sym=True,
|
| 804 |
+
scheme="W4A16",dataset=dataset,iters=iters,seqlen=seqlen,nsamples=nsamples,
|
| 805 |
+
batch_size=bs,gradient_accumulate_steps=acc,low_gpu_mem_usage=True,
|
| 806 |
+
quant_nontext_module=False,device_map=device,enable_torch_compile=False,
|
| 807 |
+
**({} if lr is None else {"lr":lr}),
|
| 808 |
+
)
|
| 809 |
+
saved=ar.quantize_and_save(output_dir=out_dir,format="auto_round",inplace=False)
|
| 810 |
+
actual=out_dir
|
| 811 |
+
if isinstance(saved,(tuple,list)) and len(saved)>=2:
|
| 812 |
+
folders=saved[1]
|
| 813 |
+
if isinstance(folders,dict):
|
| 814 |
+
folders=[v for v in folders.values() if isinstance(v,str)]
|
| 815 |
+
elif isinstance(folders,str):
|
| 816 |
+
folders=[folders]
|
| 817 |
+
if isinstance(folders,(list,tuple)):
|
| 818 |
+
for p in folders:
|
| 819 |
+
if isinstance(p,str) and _dir_has_weights(p):
|
| 820 |
+
actual=p
|
| 821 |
+
break
|
| 822 |
+
actual=_resolve_export_dir(actual)
|
| 823 |
+
if not _dir_has_weights(actual):
|
| 824 |
+
found=find_autoround_export(st_dir)
|
| 825 |
+
if found:actual=found
|
| 826 |
+
print(f" ✓ AutoRound tuning complete → {actual}\n")
|
| 827 |
+
return actual
|
| 828 |
+
|
| 829 |
+
def patch_index_with_mtp(st_dir,mtp_name=None):
|
| 830 |
+
mtp_path=os.path.join(st_dir,mtp_name) if mtp_name else _find_mtp_sidecar(st_dir)
|
| 831 |
+
if not mtp_path or not os.path.isfile(mtp_path):return None
|
| 832 |
+
mtp_name=os.path.basename(mtp_path)
|
| 833 |
+
idx_path=os.path.join(st_dir,"model.safetensors.index.json")
|
| 834 |
+
idx=read_json(idx_path)
|
| 835 |
+
if idx is None:return None
|
| 836 |
+
header=read_safetensors_header(mtp_path)
|
| 837 |
+
patched=dict(idx)
|
| 838 |
+
wm=dict(idx.get("weight_map",{}))
|
| 839 |
+
for k in [k for k in header if k!="__metadata__"]:wm[k]=mtp_name
|
| 840 |
+
for k in list(wm):
|
| 841 |
+
if(k.startswith("mtp.") or ".nextn." in k) and k not in header:
|
| 842 |
+
del wm[k]
|
| 843 |
+
patched["weight_map"]=wm
|
| 844 |
+
return patched
|
| 845 |
+
|
| 846 |
+
def ensure_preprocessor_config(st_dir):
|
| 847 |
+
path=os.path.join(st_dir,"preprocessor_config.json")
|
| 848 |
+
cfg=read_json(path) or {}
|
| 849 |
+
cfg_main=read_json(os.path.join(st_dir,"config.json")) or {}
|
| 850 |
+
vis=cfg_main.get("vision_config",{})
|
| 851 |
+
if not vis:return True
|
| 852 |
+
added=[]
|
| 853 |
+
for key in("image_mean","image_std"):
|
| 854 |
+
if key not in cfg:
|
| 855 |
+
cfg[key]=list(_SIGLIP_NORM);added.append(key)
|
| 856 |
+
defaults={
|
| 857 |
+
"do_convert_rgb":True,"do_normalize":True,"do_rescale":True,"do_resize":True,
|
| 858 |
+
"rescale_factor":1/255,
|
| 859 |
+
"patch_size":vis.get("patch_size",16),
|
| 860 |
+
"merge_size":vis.get("spatial_merge_size",2),
|
| 861 |
+
"temporal_patch_size":vis.get("temporal_patch_size",2),
|
| 862 |
+
"processor_class":"Qwen3VLProcessor",
|
| 863 |
+
}
|
| 864 |
+
for k,v in defaults.items():
|
| 865 |
+
if k not in cfg:cfg[k]=v;added.append(k)
|
| 866 |
+
if added:
|
| 867 |
+
bak=path+".bak-00"
|
| 868 |
+
if os.path.isfile(path) and not os.path.exists(bak):
|
| 869 |
+
with open(bak,"w",encoding="utf-8") as f:json.dump(read_json(path),f,indent=2)
|
| 870 |
+
with open(path,"w",encoding="utf-8") as f:json.dump(cfg,f,indent=2)
|
| 871 |
+
print(f"\n ℹ preprocessor_config.json updated: {', '.join(added)}")
|
| 872 |
+
return True
|
| 873 |
+
|
| 874 |
+
def size_str(path):
|
| 875 |
+
mb=os.path.getsize(path)/(1024*1024)
|
| 876 |
+
return f"{mb/1024:.2f} GB" if mb>=1024 else f"{mb:.0f} MiB"
|
| 877 |
+
|
| 878 |
+
def _record_provenance(gguf_path,autoround,info):
|
| 879 |
+
rec={"autoround":bool(autoround),"quant_method":info.get("quant_method","bf16"),"bits":info.get("bits",16),"source":os.path.basename(info.get("source_dir","")),"created":time.strftime("%Y-%m-%dT%H:%M:%S")}
|
| 880 |
+
for k in("int4_layers","int4_submodules","int4_globals"):
|
| 881 |
+
if info.get(k):rec[k]=info[k]
|
| 882 |
+
try:
|
| 883 |
+
with open(os.path.splitext(gguf_path)[0]+".provenance.json","w",encoding="utf-8") as f:json.dump(rec,f,indent=2)
|
| 884 |
+
except Exception as e:
|
| 885 |
+
print(f" [WARN] Provenance sidecar not written: {e}")
|
| 886 |
+
fmap_path=os.path.join(SCRIPT_DIR,"quant-provenance.json")
|
| 887 |
+
lock=fmap_path+".lock"
|
| 888 |
+
# Exclusive lock file ensures concurrent instances merge records safely
|
| 889 |
+
for _ in range(50):
|
| 890 |
+
try:
|
| 891 |
+
fd=os.open(lock,os.O_CREAT|os.O_EXCL|os.O_WRONLY)
|
| 892 |
+
os.close(fd)
|
| 893 |
+
break
|
| 894 |
+
except FileExistsError:
|
| 895 |
+
time.sleep(0.1)
|
| 896 |
+
else:
|
| 897 |
+
print(" [WARN] Provenance lock busy — global map not updated")
|
| 898 |
+
return
|
| 899 |
+
try:
|
| 900 |
+
fmap=read_json(fmap_path) or {}
|
| 901 |
+
fmap[os.path.basename(gguf_path)]=rec
|
| 902 |
+
tmp=fmap_path+".tmp"
|
| 903 |
+
with open(tmp,"w",encoding="utf-8") as f:json.dump(fmap,f,indent=2)
|
| 904 |
+
os.replace(tmp,fmap_path)
|
| 905 |
+
except Exception as e:
|
| 906 |
+
print(f" [WARN] Provenance map not written: {e}")
|
| 907 |
+
finally:
|
| 908 |
+
try:os.remove(lock)
|
| 909 |
+
except OSError:pass
|
| 910 |
+
|
| 911 |
+
def run_converter(st_dir,outtype,outfile,no_mtp=False,mmproj_only=False):
|
| 912 |
+
env=os.environ.copy()
|
| 913 |
+
env["PYTHONPATH"]=LLAMA_CPP+os.pathsep+env.get("PYTHONPATH","")
|
| 914 |
+
cmd=[sys.executable,CONVERTER,st_dir,"--outtype",outtype,"--outfile",os.path.join(SCRIPT_DIR,outfile)]
|
| 915 |
+
if mmproj_only:cmd.append("--mmproj")
|
| 916 |
+
if no_mtp:cmd.append("--no-mtp")
|
| 917 |
+
print(f"\n CMD: convert_hf_to_gguf.py \"{st_dir}\" --outtype {outtype} --outfile {outfile}\n")
|
| 918 |
+
return subprocess.run(cmd,cwd=LLAMA_CPP,env=env).returncode==0
|
| 919 |
+
|
| 920 |
+
def split_mtp(st_dir,suffix,model_out,model_path,extract=False):
|
| 921 |
+
trunk_out=f"model-no-mtp-{suffix}.gguf"
|
| 922 |
+
trunk_path=os.path.join(SCRIPT_DIR,trunk_out)
|
| 923 |
+
mtp_out=f"mtp-{suffix}.gguf"
|
| 924 |
+
mtp_path=os.path.join(SCRIPT_DIR,mtp_out)
|
| 925 |
+
if os.path.exists(trunk_path):
|
| 926 |
+
print(f"\n Output exists: {trunk_out} ({size_str(trunk_path)})")
|
| 927 |
+
else:
|
| 928 |
+
print(f"\n[1b] Converting → {trunk_out}")
|
| 929 |
+
if not run_converter(st_dir,suffix.lower(),trunk_out,no_mtp=True):
|
| 930 |
+
print(f" ✗ no-mtp conversion failed.")
|
| 931 |
+
return False
|
| 932 |
+
print(f" ✓ {trunk_out} ({size_str(trunk_path)})")
|
| 933 |
+
if not extract:
|
| 934 |
+
print(" ℹ MTP extraction skipped — use --mtp-gguf or run 00b_BF16-GGUF-MTP-extract.py on demand.")
|
| 935 |
+
return True
|
| 936 |
+
if os.path.exists(mtp_path):
|
| 937 |
+
print(f"\n Output exists: {mtp_out} ({size_str(mtp_path)})")
|
| 938 |
+
return True
|
| 939 |
+
if not os.path.isfile(EXTRACTOR):
|
| 940 |
+
print(f" ✗ {os.path.basename(EXTRACTOR)} missing — MTP extraction skipped.")
|
| 941 |
+
return False
|
| 942 |
+
print(f"\n[1c] Extracting → {mtp_out}")
|
| 943 |
+
cmd=[sys.executable,EXTRACTOR,model_path,trunk_path,mtp_path,"--keep-index"]
|
| 944 |
+
if subprocess.run(cmd,cwd=SCRIPT_DIR).returncode!=0 or not os.path.exists(mtp_path):
|
| 945 |
+
print(f" ✗ MTP extraction failed.")
|
| 946 |
+
return False
|
| 947 |
+
print(f" ✓ {mtp_out} ({size_str(mtp_path)})")
|
| 948 |
+
return True
|
| 949 |
+
|
| 950 |
+
def main():
|
| 951 |
+
ap=argparse.ArgumentParser(description="Universal AutoRound-infused GGUF builder",formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 952 |
+
ap.add_argument("dir",nargs="?",default=None,help="safetensors directory (drag-drop supported)")
|
| 953 |
+
ap.add_argument("--iters",type=int,default=None,help="tuning iters for raw BF16 (default: auto from model size + hardware)")
|
| 954 |
+
ap.add_argument("--batch-size",type=int,default=None,help="override AutoRound batch size (effective batch bs×acc kept)")
|
| 955 |
+
ap.add_argument("--seqlen",type=int,default=None,help="override AutoRound calibration seqlen")
|
| 956 |
+
ap.add_argument("--skip-tune",action="store_true",help="skip AutoRound tuning on raw BF16")
|
| 957 |
+
ap.add_argument("--experimental","--exp",action="store_true",help="use experimental.txt (same flag as 01)")
|
| 958 |
+
ap.add_argument("--dataset",default=None,help="custom calib .txt/.json/.jsonl (overrides bartowski)")
|
| 959 |
+
ap.add_argument("--autoround-dir",default=None,help="reuse this AutoRound export (skips tuning)")
|
| 960 |
+
ap.add_argument("--outtype",default="bf16",choices=["bf16","f16"],help="GGUF precision (default: bf16)")
|
| 961 |
+
ap.add_argument("--force",action="store_true",help="overwrite existing GGUF outputs")
|
| 962 |
+
ap.add_argument("--mtp-gguf",action="store_true",help="also extract mtp-<SUF>.gguf (fusion-ready, via 00b)")
|
| 963 |
+
args=ap.parse_args()
|
| 964 |
+
SUF=args.outtype.upper()
|
| 965 |
+
st_dir=os.path.abspath(args.dir) if args.dir else os.path.join(SCRIPT_DIR,"safetensors")
|
| 966 |
+
if not os.path.isdir(st_dir):
|
| 967 |
+
print(f"Error: directory not found: {st_dir}")
|
| 968 |
+
return 1
|
| 969 |
+
st_files=[f for f in os.listdir(st_dir) if f.endswith((".safetensors",".bin"))]
|
| 970 |
+
if not st_files:
|
| 971 |
+
print(f"Error: no .safetensors files in {st_dir}")
|
| 972 |
+
return 1
|
| 973 |
+
st_dir=_resolve_export_dir(st_dir)
|
| 974 |
+
info=detect_model(st_dir)
|
| 975 |
+
if not info:
|
| 976 |
+
src=_guess_source_dir(st_dir)
|
| 977 |
+
if src:_ensure_export_sidecars(src,st_dir)
|
| 978 |
+
info=detect_model(st_dir)
|
| 979 |
+
if not info:
|
| 980 |
+
print(f"Error: config.json not found in {st_dir}")
|
| 981 |
+
return 1
|
| 982 |
+
print("="*68)
|
| 983 |
+
print(" 00 — Universal AutoRound-infused GGUF Builder")
|
| 984 |
+
print("="*68)
|
| 985 |
+
print(f" Source: {st_dir}")
|
| 986 |
+
print(f" Output dir: {SCRIPT_DIR}")
|
| 987 |
+
print(f" Architecture: {info['arch']}")
|
| 988 |
+
print(f" Model type: {info['type']}")
|
| 989 |
+
print(f" Quantized: {'yes ('+str(info['quant_method'])+'/'+str(info['quant_format'])+')' if info['quantized'] else 'no (raw BF16)'}")
|
| 990 |
+
mtp_str="PHANTOM" if(info["mtp_declared"] and not info["mtp_complete"]) else("yes" if info["mtp"] else "no")
|
| 991 |
+
print(f" MTP: {mtp_str}")
|
| 992 |
+
print(f" Multimodal: {'yes' if info['multimodal'] else 'no'}")
|
| 993 |
+
if args.dataset:calib_label=os.path.basename(args.dataset)
|
| 994 |
+
elif args.experimental:calib_label="experimental.txt"
|
| 995 |
+
else:calib_label=DEFAULT_CALIB_NAME
|
| 996 |
+
print(f" Calib: {calib_label}")
|
| 997 |
+
print("="*68)
|
| 998 |
+
work_st_dir=st_dir
|
| 999 |
+
temp_cleanup_dirs=[]
|
| 1000 |
+
used_autoround=False
|
| 1001 |
+
if not info["quantized"] and not args.skip_tune:
|
| 1002 |
+
tuned_dir=os.path.join(SCRIPT_DIR,f"{os.path.basename(st_dir)}_AutoRound_W4A16")
|
| 1003 |
+
existing=find_autoround_export(st_dir,explicit=args.autoround_dir)
|
| 1004 |
+
if existing and _dir_looks_quantized(existing):
|
| 1005 |
+
print(f"\n[AutoRound] Reusing existing W4A16 export: {existing}")
|
| 1006 |
+
print(" Previous tuning is kept. Conversion resumes from this folder.")
|
| 1007 |
+
tuned_dir=existing
|
| 1008 |
+
else:
|
| 1009 |
+
saved=run_autoround_tune(st_dir,tuned_dir,iters=args.iters,experimental=args.experimental,dataset_path=args.dataset,info=info,bs_override=args.batch_size,seqlen_override=args.seqlen)
|
| 1010 |
+
if saved:tuned_dir=saved
|
| 1011 |
+
tuned_dir=_resolve_export_dir(tuned_dir)
|
| 1012 |
+
_ensure_export_sidecars(st_dir,tuned_dir)
|
| 1013 |
+
work_st_dir=tuned_dir
|
| 1014 |
+
used_autoround=True
|
| 1015 |
+
info=detect_model(work_st_dir)
|
| 1016 |
+
if not info:
|
| 1017 |
+
print(f"Error: AutoRound output is not a readable HF folder: {tuned_dir}")
|
| 1018 |
+
return 1
|
| 1019 |
+
if info and info["quantized"]:
|
| 1020 |
+
target_dequant=os.path.join(SCRIPT_DIR,f"{os.path.basename(work_st_dir)}_dequant_bf16")
|
| 1021 |
+
dequantize_model_to_dir(work_st_dir,target_dequant,info)
|
| 1022 |
+
qm=(str(info["quant_method"])+str(info["quant_format"])).lower()
|
| 1023 |
+
used_autoround=used_autoround or "round" in qm
|
| 1024 |
+
src_method=info["quant_method"]
|
| 1025 |
+
int4_meta={k:info[k] for k in("int4_layers","int4_submodules","int4_globals") if k in info}
|
| 1026 |
+
work_st_dir=target_dequant
|
| 1027 |
+
temp_cleanup_dirs.append(target_dequant)
|
| 1028 |
+
info=detect_model(work_st_dir)
|
| 1029 |
+
if info:
|
| 1030 |
+
info["quant_method"]=src_method
|
| 1031 |
+
info.update(int4_meta)
|
| 1032 |
+
ensure_preprocessor_config(work_st_dir)
|
| 1033 |
+
if not ensure_llama_cpp():
|
| 1034 |
+
print(f"Error: {CONVERTER} not found")
|
| 1035 |
+
return 1
|
| 1036 |
+
phantom_head=info["mtp_declared"] and not info["mtp_complete"]
|
| 1037 |
+
model_out=f"model-no-mtp-{SUF}.gguf" if(phantom_head or not info["mtp"]) else f"model-{SUF}.gguf"
|
| 1038 |
+
model_path=os.path.join(SCRIPT_DIR,model_out)
|
| 1039 |
+
skip_convert=os.path.exists(model_path) and not args.force
|
| 1040 |
+
if skip_convert:
|
| 1041 |
+
print(f"\n Output exists: {model_out} ({size_str(model_path)}) — use --force to overwrite")
|
| 1042 |
+
if not(args.mtp_gguf and info["mtp"] and not phantom_head and "no-mtp" not in model_out):
|
| 1043 |
+
return 0
|
| 1044 |
+
before=set(f for f in os.listdir(SCRIPT_DIR) if f.endswith(".gguf"))
|
| 1045 |
+
idx_path=os.path.join(work_st_dir,"model.safetensors.index.json")
|
| 1046 |
+
idx_backup=None
|
| 1047 |
+
if info["mtp_standalone"] and not skip_convert:
|
| 1048 |
+
patched=patch_index_with_mtp(work_st_dir,info.get("mtp_file"))
|
| 1049 |
+
if patched:
|
| 1050 |
+
idx_backup=idx_path+".bak-00"
|
| 1051 |
+
os.replace(idx_path,idx_backup)
|
| 1052 |
+
with open(idx_path,"w",encoding="utf-8") as f:json.dump(patched,f,indent=2)
|
| 1053 |
+
no_mtp=phantom_head or not info["mtp"]
|
| 1054 |
+
if skip_convert:
|
| 1055 |
+
ok=True
|
| 1056 |
+
else:
|
| 1057 |
+
print(f"\n[1] Converting → {model_out}")
|
| 1058 |
+
try:
|
| 1059 |
+
ok=run_converter(work_st_dir,args.outtype,model_out,no_mtp=no_mtp and info["mtp_declared"])
|
| 1060 |
+
finally:
|
| 1061 |
+
if idx_backup and os.path.exists(idx_backup):
|
| 1062 |
+
os.replace(idx_backup,idx_path)
|
| 1063 |
+
if not ok and not no_mtp:
|
| 1064 |
+
print(" ⚠ Full conversion rejected by convert_hf_to_gguf — falling back without MTP head.")
|
| 1065 |
+
model_out=f"model-no-mtp-{SUF}.gguf"
|
| 1066 |
+
model_path=os.path.join(SCRIPT_DIR,model_out)
|
| 1067 |
+
no_mtp=True
|
| 1068 |
+
ok=run_converter(work_st_dir,args.outtype,model_out,no_mtp=True)
|
| 1069 |
+
if not ok or not os.path.exists(model_path):
|
| 1070 |
+
print(f"\n ✗ Conversion failed.")
|
| 1071 |
+
return 1
|
| 1072 |
+
print(f" ✓ {model_out} ({size_str(model_path)})")
|
| 1073 |
+
if info["mtp"] and not no_mtp:
|
| 1074 |
+
split_mtp(work_st_dir,SUF,model_out,model_path,extract=args.mtp_gguf)
|
| 1075 |
+
if info["multimodal"] and not(skip_convert and os.path.exists(os.path.join(SCRIPT_DIR,f"mmproj-{SUF}.gguf"))):
|
| 1076 |
+
mmproj_out=f"mmproj-{SUF}.gguf"
|
| 1077 |
+
mmproj_path=os.path.join(SCRIPT_DIR,mmproj_out)
|
| 1078 |
+
print(f"\n[2] Converting → {mmproj_out}")
|
| 1079 |
+
if run_converter(work_st_dir,args.outtype,mmproj_out,mmproj_only=True) and os.path.exists(mmproj_path):
|
| 1080 |
+
print(f" ✓ {mmproj_out} ({size_str(mmproj_path)})")
|
| 1081 |
+
after=set(f for f in os.listdir(SCRIPT_DIR) if f.endswith(".gguf"))
|
| 1082 |
+
new_files=sorted(after-before-{model_out,f"mmproj-{SUF}.gguf"})
|
| 1083 |
+
mmproj_renamed=os.path.exists(os.path.join(SCRIPT_DIR,f"mmproj-{SUF}.gguf"))
|
| 1084 |
+
for f in new_files:
|
| 1085 |
+
src=os.path.join(SCRIPT_DIR,f)
|
| 1086 |
+
if any(x in f.lower() for x in["mmproj","vision","proj","clip"]) and not mmproj_renamed:
|
| 1087 |
+
mmproj_path=os.path.join(SCRIPT_DIR,f"mmproj-{SUF}.gguf")
|
| 1088 |
+
os.replace(src,mmproj_path)
|
| 1089 |
+
f=f"mmproj-{SUF}.gguf"
|
| 1090 |
+
src=mmproj_path
|
| 1091 |
+
mmproj_renamed=True
|
| 1092 |
+
print(f" +{f} ({size_str(src)})")
|
| 1093 |
+
for gg in(model_out,f"model-no-mtp-{SUF}.gguf",f"mtp-{SUF}.gguf",f"mmproj-{SUF}.gguf"):
|
| 1094 |
+
gp=os.path.join(SCRIPT_DIR,gg)
|
| 1095 |
+
if os.path.isfile(gp):
|
| 1096 |
+
is_mm="mmproj" in gg
|
| 1097 |
+
mm_info={"quant_method":"bf16","bits":16,"source_dir":info.get("source_dir","")}
|
| 1098 |
+
_record_provenance(gp,False if is_mm else used_autoround,mm_info if is_mm else info)
|
| 1099 |
+
for p in temp_cleanup_dirs:
|
| 1100 |
+
if os.path.isdir(p):
|
| 1101 |
+
print(f" ℹ Cleaning up temporary workspace: {os.path.basename(p)}")
|
| 1102 |
+
shutil.rmtree(p,ignore_errors=True)
|
| 1103 |
+
print("\n"+"="*68)
|
| 1104 |
+
print(" Step 00 complete: AutoRound-infused GGUF files ready!")
|
| 1105 |
+
print("="*68)
|
| 1106 |
+
return 0
|
| 1107 |
+
|
| 1108 |
+
if __name__=="__main__":
|
| 1109 |
+
raise SystemExit(main())
|
00b_BF16-GGUF-MTP-extract.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
r"""00b_BF16-GGUF-MTP-extract.py — Extract the MTP head as a standalone GGUF.
|
| 3 |
+
|
| 4 |
+
Diffs a full GGUF against its no-mtp variant: tensors only present in the
|
| 5 |
+
full file (the MTP layer) plus shared embedding/output tensors needed by a
|
| 6 |
+
speculative draft are written to mtp-<suffix>.gguf.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python 00b_BF16-GGUF-MTP-extract.py model-BF16.gguf model-no-mtp-BF16.gguf
|
| 10 |
+
"""
|
| 11 |
+
import os,re,shutil,sys
|
| 12 |
+
|
| 13 |
+
SCRIPT_DIR=os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
sys.path.insert(0,os.path.join(SCRIPT_DIR,"llama-cpp","gguf-py"))
|
| 15 |
+
import gguf
|
| 16 |
+
|
| 17 |
+
SHARED_EXACT={"token_embd.weight","token_embd_norm.weight","output.weight","output_norm.weight"}
|
| 18 |
+
# Exact names avoid matching blk.N.attn_output.weight tensors
|
| 19 |
+
MTP_PROJ_HINT=("nextn.eh_proj","nextn.fc","nextn.enorm","nextn.hnorm","nextn.embed_tokens","nextn.shared_head")
|
| 20 |
+
|
| 21 |
+
SCALAR_WRITERS={
|
| 22 |
+
gguf.GGUFValueType.UINT8:"add_uint8",gguf.GGUFValueType.INT8:"add_int8",
|
| 23 |
+
gguf.GGUFValueType.UINT16:"add_uint16",gguf.GGUFValueType.INT16:"add_int16",
|
| 24 |
+
gguf.GGUFValueType.UINT32:"add_uint32",gguf.GGUFValueType.INT32:"add_int32",
|
| 25 |
+
gguf.GGUFValueType.FLOAT32:"add_float32",gguf.GGUFValueType.UINT64:"add_uint64",
|
| 26 |
+
gguf.GGUFValueType.INT64:"add_int64",gguf.GGUFValueType.FLOAT64:"add_float64",
|
| 27 |
+
gguf.GGUFValueType.BOOL:"add_bool",gguf.GGUFValueType.STRING:"add_string",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def _copy_field(writer,field):
|
| 31 |
+
vtype=field.types[0]
|
| 32 |
+
val=field.contents()
|
| 33 |
+
if vtype==gguf.GGUFValueType.ARRAY:
|
| 34 |
+
# Arrays without elements are skipped by the GGUF writer
|
| 35 |
+
if not val:
|
| 36 |
+
print(f" ! empty array skipped: {field.name}")
|
| 37 |
+
return
|
| 38 |
+
writer.add_array(field.name,val)
|
| 39 |
+
else:
|
| 40 |
+
getattr(writer,SCALAR_WRITERS[vtype])(field.name,val)
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
args=[a for a in sys.argv[1:] if not a.startswith("--")]
|
| 44 |
+
# Preserve blk.N indices to prevent collision with trunk blk.0 on fusion re-merge
|
| 45 |
+
keep_idx="--keep-index" in sys.argv
|
| 46 |
+
if len(args)<2:
|
| 47 |
+
print("Usage: python 00b_BF16-GGUF-MTP-extract.py <full.gguf> <no-mtp.gguf> [out.gguf] [--keep-index]")
|
| 48 |
+
return 1
|
| 49 |
+
full_path=os.path.abspath(args[0])
|
| 50 |
+
stripped_path=os.path.abspath(args[1])
|
| 51 |
+
m=re.search(r"-(BF16|F16|F32)",os.path.basename(full_path),re.I)
|
| 52 |
+
out_path=os.path.abspath(args[2]) if len(args)>2 else os.path.join(
|
| 53 |
+
SCRIPT_DIR,f"mtp-{m.group(1).upper() if m else 'BF16'}.gguf")
|
| 54 |
+
full=gguf.GGUFReader(full_path)
|
| 55 |
+
stripped_names={t.name for t in gguf.GGUFReader(stripped_path).tensors}
|
| 56 |
+
mtp_tensors=[t for t in full.tensors if t.name not in stripped_names]
|
| 57 |
+
if not mtp_tensors:
|
| 58 |
+
print("No extra tensors in the full GGUF — nothing to extract.")
|
| 59 |
+
return 1
|
| 60 |
+
if not any(any(h in t.name for h in MTP_PROJ_HINT) for t in mtp_tensors):
|
| 61 |
+
print("Extra tensors found without a nextn projector (eh_proj/fc/enorm/hnorm) —")
|
| 62 |
+
print("this represents an unprojected attention block rather than a complete MTP head.")
|
| 63 |
+
return 1
|
| 64 |
+
shared=[] if keep_idx else [t for t in full.tensors if t.name in stripped_names and t.name in SHARED_EXACT]
|
| 65 |
+
arch=str(full.fields["general.architecture"].parts[full.fields["general.architecture"].data[0]],encoding="utf-8")
|
| 66 |
+
writer=gguf.GGUFWriter(out_path,arch)
|
| 67 |
+
layers=sorted({int(m.group(1)) for t in mtp_tensors if(m:=re.match(r"blk\.(\d+)\.",t.name))})
|
| 68 |
+
for field in full.fields.values():
|
| 69 |
+
if field.name.startswith("GGUF.") or field.name=="general.architecture":continue
|
| 70 |
+
if field.name.endswith(".block_count"):
|
| 71 |
+
writer.add_key_value(field.name,(max(layers)+1) if keep_idx else len(layers),field.types[0])
|
| 72 |
+
continue
|
| 73 |
+
_copy_field(writer,field)
|
| 74 |
+
remap={} if keep_idx else {old:new for new,old in enumerate(layers)}
|
| 75 |
+
for t in mtp_tensors+shared:
|
| 76 |
+
name=t.name
|
| 77 |
+
if(m:=re.match(r"blk\.(\d+)\.",name)):
|
| 78 |
+
old=int(m.group(1))
|
| 79 |
+
if old in remap:name=f"blk.{remap[old]}."+name[m.end():]
|
| 80 |
+
writer.add_tensor(name,t.data,raw_shape=t.data.shape,raw_dtype=t.tensor_type)
|
| 81 |
+
writer.write_header_to_file()
|
| 82 |
+
writer.write_kv_data_to_file()
|
| 83 |
+
writer.write_tensors_to_file(progress=True)
|
| 84 |
+
writer.close()
|
| 85 |
+
prov_src=os.path.splitext(full_path)[0]+".provenance.json"
|
| 86 |
+
if os.path.isfile(prov_src):
|
| 87 |
+
shutil.copy2(prov_src,os.path.splitext(out_path)[0]+".provenance.json")
|
| 88 |
+
mb=os.path.getsize(out_path)/(1024*1024)
|
| 89 |
+
print(f" ✓ {os.path.basename(out_path)} ({mb:.0f} MiB) — {len(mtp_tensors)} MTP tensor(s)"
|
| 90 |
+
f" (layer(s) {layers}) + {len(shared)} shared tensor(s)")
|
| 91 |
+
return 0
|
| 92 |
+
|
| 93 |
+
if __name__=="__main__":
|
| 94 |
+
raise SystemExit(main())
|
01_create-calibration-dataset-and-imatrix.py
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""01_create-calibration-dataset-and-imatrix.py — End-to-end imatrix calibration pipeline.
|
| 3 |
+
|
| 4 |
+
1. Download & reassemble datasets → 01_create-calibration-dataset-and-imatrix/raw/
|
| 5 |
+
2. Normalize sources → 01_create-calibration-dataset-and-imatrix/normalized/
|
| 6 |
+
3. Build calibration text (Agentic / Frontier / Logic / Diversity quota targets)
|
| 7 |
+
4. Compute imatrix.dat via llama-imatrix (with GPU autotuning and PPL verification)
|
| 8 |
+
"""
|
| 9 |
+
import collections,concurrent.futures,itertools,json,os,re,struct,subprocess,sys,time,urllib.request
|
| 10 |
+
from huggingface_hub import hf_hub_download,list_repo_files
|
| 11 |
+
|
| 12 |
+
# Managed pages instead of rigid VMM pool to tolerate fragmentation across loaded VRAM
|
| 13 |
+
os.environ.setdefault("GGML_CUDA_ENABLE_UNIFIED_MEMORY","1")
|
| 14 |
+
|
| 15 |
+
LLAMA_GGUF_PY=os.path.join(os.path.dirname(os.path.abspath(__file__)),"llama-cpp","gguf-py")
|
| 16 |
+
if os.path.isdir(LLAMA_GGUF_PY):sys.path.insert(0,LLAMA_GGUF_PY)
|
| 17 |
+
|
| 18 |
+
ROOT=os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
DATA=os.path.join(ROOT,"01_create-calibration-dataset-and-imatrix")
|
| 20 |
+
RAW=os.path.join(DATA,"raw")
|
| 21 |
+
NORM=os.path.join(DATA,"normalized")
|
| 22 |
+
EXPERIMENTAL_CALIB=os.path.join(DATA,"experimental.txt")
|
| 23 |
+
DEFAULT_CALIB_NAME="bartowski-imatrix-v5-semantic.txt"
|
| 24 |
+
DEFAULT_CALIB_URL="https://huggingface.co/datasets/lemon07r/bartowski-imatrix-v5-semantic/blob/main/bartowski-imatrix-v5-semantic.txt"
|
| 25 |
+
DEFAULT_CALIB_PATH=os.path.join(DATA,DEFAULT_CALIB_NAME)
|
| 26 |
+
IMATRIX=os.path.join(ROOT,"imatrix.dat")
|
| 27 |
+
LLAMA=os.path.join(ROOT,"llama-cpp")
|
| 28 |
+
MODEL_CANDIDATES=[
|
| 29 |
+
"model-no-mtp-BF16.gguf",
|
| 30 |
+
"model-BF16-no-mtp.gguf",
|
| 31 |
+
"model-BF16.gguf",
|
| 32 |
+
"model-no-mtp-F16.gguf",
|
| 33 |
+
"model-F16-no-mtp.gguf",
|
| 34 |
+
"model-F16.gguf",
|
| 35 |
+
]
|
| 36 |
+
BF16=next(
|
| 37 |
+
(os.path.join(ROOT,name) for name in MODEL_CANDIDATES
|
| 38 |
+
if os.path.exists(os.path.join(ROOT,name))),
|
| 39 |
+
os.path.join(ROOT,MODEL_CANDIDATES[-1])
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 43 |
+
# STEP 1 — DOWNLOAD & REASSEMBLY
|
| 44 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 45 |
+
GISTS=[("calibration_data_v5_rc.txt","https://gist.github.com/tristandruyen/9e207a95c7d75ddf37525d353e00659c/raw/571fda718462de863e5a0171078c175420c7649a/calibration_data_v5_rc.txt"),]
|
| 46 |
+
DATASETS=[
|
| 47 |
+
("fable-5-claude-code","armand0e/claude-fable-5-claude-code","Frontier"),
|
| 48 |
+
("minimax-m3-traces","armand0e/minimax-m3-claude-code-traces","Frontier"),
|
| 49 |
+
("opus-4.8-pi-traces","armand0e/claude-opus-4.8-pi-traces","Frontier"),
|
| 50 |
+
("hermes-function-calling","NousResearch/hermes-function-calling-v1","Others"),
|
| 51 |
+
("python-codes-25k","flytech/python-codes-25k","Others"),
|
| 52 |
+
("ma-proofbench","openbmb/MA-ProofBench","Others"),
|
| 53 |
+
("multi-hop-websearch","lugman-madhiai/multi-hop-websearch-tool-calling","Agentic"),
|
| 54 |
+
]
|
| 55 |
+
SKIP_NAMES={"readme.md",".gitattributes"}
|
| 56 |
+
DATA_EXT=(".jsonl",".json")
|
| 57 |
+
CACHE=os.path.join(DATA,"_hf_cache")
|
| 58 |
+
|
| 59 |
+
def list_data_files(repo_id):
|
| 60 |
+
files=list_repo_files(repo_id,repo_type="dataset")
|
| 61 |
+
out=[]
|
| 62 |
+
for f in files:
|
| 63 |
+
low=f.lower()
|
| 64 |
+
if low in SKIP_NAMES:continue
|
| 65 |
+
if os.path.basename(low) in SKIP_NAMES:continue
|
| 66 |
+
if low.endswith(DATA_EXT):out.append(f)
|
| 67 |
+
jsonl_stems={f[:-6] for f in out if f.lower().endswith(".jsonl")}
|
| 68 |
+
deduped=[]
|
| 69 |
+
for f in out:
|
| 70 |
+
if f.lower().endswith(".json") and f[:-5] in jsonl_stems:
|
| 71 |
+
print(f" [DEDUP] Skipping duplicate: {f} (keeping .jsonl)")
|
| 72 |
+
continue
|
| 73 |
+
deduped.append(f)
|
| 74 |
+
return sorted(deduped)
|
| 75 |
+
|
| 76 |
+
def download_one(repo_id,filename):
|
| 77 |
+
return hf_hub_download(repo_id=repo_id,filename=filename,repo_type="dataset",cache_dir=CACHE)
|
| 78 |
+
|
| 79 |
+
def iter_records(path):
|
| 80 |
+
if path.lower().endswith(".jsonl"):
|
| 81 |
+
with open(path,"r",encoding="utf-8") as f:
|
| 82 |
+
for line in f:
|
| 83 |
+
line=line.strip()
|
| 84 |
+
if not line:continue
|
| 85 |
+
try:yield json.loads(line)
|
| 86 |
+
except Exception:continue
|
| 87 |
+
else:
|
| 88 |
+
try:
|
| 89 |
+
with open(path,"r",encoding="utf-8") as f:data=json.load(f)
|
| 90 |
+
except Exception:return
|
| 91 |
+
if isinstance(data,list):
|
| 92 |
+
for rec in data:yield rec
|
| 93 |
+
elif isinstance(data,dict):
|
| 94 |
+
yield data
|
| 95 |
+
|
| 96 |
+
def reassemble(out_name,repo_id):
|
| 97 |
+
out_path=os.path.join(RAW,out_name+".jsonl")
|
| 98 |
+
if os.path.exists(out_path):
|
| 99 |
+
print(f" [OK] Already assembled: {out_name}.jsonl")
|
| 100 |
+
return
|
| 101 |
+
try:files=list_data_files(repo_id)
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f" [ERR] Failed listing repo files for {repo_id}: {e}")
|
| 104 |
+
return
|
| 105 |
+
if not files:
|
| 106 |
+
print(f" [WARN] No data files found in {repo_id}")
|
| 107 |
+
return
|
| 108 |
+
print(f" [DL] {repo_id} → {len(files)} files")
|
| 109 |
+
local_paths=[]
|
| 110 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
|
| 111 |
+
futures={ex.submit(download_one,repo_id,f):f for f in files}
|
| 112 |
+
for fut in concurrent.futures.as_completed(futures):
|
| 113 |
+
f=futures[fut]
|
| 114 |
+
try:local_paths.append(fut.result())
|
| 115 |
+
except Exception as e:print(f" [ERR] {f}: {e}")
|
| 116 |
+
n_rec=0
|
| 117 |
+
with open(out_path,"w",encoding="utf-8") as out:
|
| 118 |
+
for lp in local_paths:
|
| 119 |
+
for rec in iter_records(lp):
|
| 120 |
+
out.write(json.dumps(rec,ensure_ascii=False)+"\n")
|
| 121 |
+
n_rec+=1
|
| 122 |
+
size_mb=os.path.getsize(out_path)/1024/1024
|
| 123 |
+
print(f" [DONE] {out_name}.jsonl → {n_rec} records ({size_mb:.1f} MB)")
|
| 124 |
+
|
| 125 |
+
def download_gists():
|
| 126 |
+
for fname,url in GISTS:
|
| 127 |
+
dest=os.path.join(RAW,fname)
|
| 128 |
+
if os.path.exists(dest):
|
| 129 |
+
print(f" [OK] Already present: {fname}")
|
| 130 |
+
continue
|
| 131 |
+
print(f" [DL] {fname}")
|
| 132 |
+
try:
|
| 133 |
+
urllib.request.urlretrieve(url,dest)
|
| 134 |
+
print(f" [DONE] {fname} ({os.path.getsize(dest)/1024/1024:.1f} MB)")
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f" [ERR] {fname}: {e}")
|
| 137 |
+
|
| 138 |
+
def step_download():
|
| 139 |
+
os.makedirs(RAW,exist_ok=True)
|
| 140 |
+
print("\n"+"="*72)
|
| 141 |
+
print(" 📥 STEP 1/4 — DOWNLOAD & REASSEMBLY")
|
| 142 |
+
print("="*72)
|
| 143 |
+
for out_name,repo_id,cat in DATASETS:
|
| 144 |
+
print(f"\n── [{cat}] {out_name} ──")
|
| 145 |
+
reassemble(out_name,repo_id)
|
| 146 |
+
print(f"\n── [Diversity] calibration gists ──")
|
| 147 |
+
download_gists()
|
| 148 |
+
|
| 149 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 150 |
+
# STEP 2 — NORMALIZATION
|
| 151 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 152 |
+
def extract_blocks(content):
|
| 153 |
+
if isinstance(content,str):
|
| 154 |
+
return [("text",content)] if content.strip() else []
|
| 155 |
+
out=[]
|
| 156 |
+
if isinstance(content,list):
|
| 157 |
+
for b in content:
|
| 158 |
+
if not isinstance(b,dict):continue
|
| 159 |
+
t=b.get("type")
|
| 160 |
+
if t=="text":out.append(("text",b.get("text","")))
|
| 161 |
+
elif t=="thinking":out.append(("think",b.get("thinking","")))
|
| 162 |
+
elif t in("tool_use","toolCall"):
|
| 163 |
+
name=b.get("name")
|
| 164 |
+
args=b.get("input") if b.get("input") is not None else b.get("arguments")
|
| 165 |
+
out.append(("toolcall",json.dumps({"name":name,"input":args},ensure_ascii=False)))
|
| 166 |
+
elif t in("tool_result","toolResult"):
|
| 167 |
+
c=b.get("content")
|
| 168 |
+
out.append(("tool",c if isinstance(c,str) else json.dumps(c,ensure_ascii=False)))
|
| 169 |
+
return out
|
| 170 |
+
|
| 171 |
+
BLOCK_ROLE={"text":None,"think":"think","toolcall":"toolcall","tool":"tool"}
|
| 172 |
+
|
| 173 |
+
def parse_hermes(row):
|
| 174 |
+
chat=[]
|
| 175 |
+
rolemap={"system":"system","human":"user","gpt":"assistant","tool":"tool"}
|
| 176 |
+
for msg in row.get("conversations",[]):
|
| 177 |
+
role=rolemap.get(msg.get("from"))
|
| 178 |
+
c=(msg.get("value") or "").strip()
|
| 179 |
+
if not role or not c:continue
|
| 180 |
+
if role=="assistant":
|
| 181 |
+
m=re.search(r"<think>\s*(.*?)\s*</think>",c,re.DOTALL)
|
| 182 |
+
if m:
|
| 183 |
+
chat.append({"role":"think","content":m.group(1).strip()})
|
| 184 |
+
c=re.sub(r"<think>.*?</think>","",c,flags=re.DOTALL).strip()
|
| 185 |
+
last=0
|
| 186 |
+
for tc in re.finditer(r"<tool_call>(.*?)</tool_call>",c,re.DOTALL):
|
| 187 |
+
chunk=c[last:tc.start()].strip()
|
| 188 |
+
if chunk:chat.append({"role":"assistant","content":chunk})
|
| 189 |
+
chat.append({"role":"toolcall","content":tc.group(1).strip()})
|
| 190 |
+
last=tc.end()
|
| 191 |
+
chunk=c[last:].strip()
|
| 192 |
+
if chunk:chat.append({"role":"assistant","content":chunk})
|
| 193 |
+
else:
|
| 194 |
+
chat.append({"role":role,"content":c})
|
| 195 |
+
return {"chat":chat} if len(chat)>=2 else None
|
| 196 |
+
|
| 197 |
+
def parse_python_codes(row):
|
| 198 |
+
instr=(row.get("instruction") or "").strip()
|
| 199 |
+
inp=(row.get("input") or "").strip()
|
| 200 |
+
out=(row.get("output") or "").strip()
|
| 201 |
+
if not instr or not out:return None
|
| 202 |
+
user=instr if not inp else f"{instr}\n{inp}"
|
| 203 |
+
return {"chat":[{"role":"user","content":user},{"role":"assistant","content":out}]}
|
| 204 |
+
|
| 205 |
+
def parse_proofbench(row):
|
| 206 |
+
inf=(row.get("informal_statement") or "").strip()
|
| 207 |
+
frm=(row.get("formal_statement") or "").strip()
|
| 208 |
+
proof=(row.get("formal_proof") or row.get("proof") or "").strip()
|
| 209 |
+
topic=(row.get("topic") or "").strip()
|
| 210 |
+
tag=(row.get("tag") or "").strip()
|
| 211 |
+
if not inf and not frm:return None
|
| 212 |
+
user=inf if inf else "Formalize and prove the following in Lean 4."
|
| 213 |
+
ctx=" / ".join(p for p in(topic,tag) if p)
|
| 214 |
+
if ctx:user+=f"\n(Domain: {ctx})"
|
| 215 |
+
answer="\n".join(p for p in(frm,proof) if p) or inf
|
| 216 |
+
return {"chat":[
|
| 217 |
+
{"role":"user","content":user},
|
| 218 |
+
{"role":"assistant","content":answer}
|
| 219 |
+
]}
|
| 220 |
+
|
| 221 |
+
def parse_multi_hop_websearch(row):
|
| 222 |
+
chat=[]
|
| 223 |
+
for msg in row.get("messages",[]):
|
| 224 |
+
if not isinstance(msg,dict):continue
|
| 225 |
+
role=msg.get("role")
|
| 226 |
+
content=(msg.get("content") or "").strip()
|
| 227 |
+
think=(msg.get("thinking") or "").strip()
|
| 228 |
+
if think:chat.append({"role":"think","content":think})
|
| 229 |
+
if role=="user":
|
| 230 |
+
if content:chat.append({"role":"user","content":content})
|
| 231 |
+
elif role=="assistant":
|
| 232 |
+
if content:chat.append({"role":"assistant","content":content})
|
| 233 |
+
for tc in(msg.get("tool_calls") or []):
|
| 234 |
+
if not isinstance(tc,dict):continue
|
| 235 |
+
name=tc.get("name","")
|
| 236 |
+
raw_args=tc.get("arguments","{}")
|
| 237 |
+
try:args=json.loads(raw_args) if isinstance(raw_args,str) else raw_args
|
| 238 |
+
except Exception:args=raw_args
|
| 239 |
+
chat.append({"role":"toolcall","content":json.dumps({"name":name,"input":args},ensure_ascii=False)})
|
| 240 |
+
elif role=="tool":
|
| 241 |
+
if content:chat.append({"role":"tool","content":content})
|
| 242 |
+
return {"chat":chat} if len(chat)>=2 else None
|
| 243 |
+
|
| 244 |
+
def merge_adjacent(turns):
|
| 245 |
+
out=[]
|
| 246 |
+
for t in turns:
|
| 247 |
+
if out and out[-1]["role"]==t["role"]:
|
| 248 |
+
out[-1]["content"]+="\n"+t["content"]
|
| 249 |
+
else:
|
| 250 |
+
out.append(dict(t))
|
| 251 |
+
return out
|
| 252 |
+
|
| 253 |
+
def parse_claude_events(rows):
|
| 254 |
+
sessions,order={},[]
|
| 255 |
+
cur,counter="__seed",0
|
| 256 |
+
for row in rows:
|
| 257 |
+
if not isinstance(row,dict):continue
|
| 258 |
+
t=row.get("type")
|
| 259 |
+
if t=="session":
|
| 260 |
+
counter+=1
|
| 261 |
+
cur=f"__pi{counter}"
|
| 262 |
+
continue
|
| 263 |
+
if t not in("user","assistant","message"):continue
|
| 264 |
+
msg=row.get("message")
|
| 265 |
+
if not isinstance(msg,dict):continue
|
| 266 |
+
sid=row.get("sessionId") or cur
|
| 267 |
+
raw_role=msg.get("role") or("assistant" if t=="assistant" else "user")
|
| 268 |
+
role={"toolResult":"tool","tool":"tool"}.get(raw_role,raw_role)
|
| 269 |
+
blocks=extract_blocks(msg.get("content"))
|
| 270 |
+
if not blocks:continue
|
| 271 |
+
if sid not in sessions:
|
| 272 |
+
sessions[sid]=[]
|
| 273 |
+
order.append(sid)
|
| 274 |
+
for kind,text in blocks:
|
| 275 |
+
text=(text or "").strip()
|
| 276 |
+
if not text:continue
|
| 277 |
+
r=("tool" if role=="tool" and kind=="text" else(BLOCK_ROLE[kind] or role))
|
| 278 |
+
sessions[sid].append({"role":r,"content":text})
|
| 279 |
+
chats=[]
|
| 280 |
+
for sid in order:
|
| 281 |
+
turns=merge_adjacent(sessions[sid])
|
| 282 |
+
has_tool=any(x["role"] in("toolcall","tool") for x in turns)
|
| 283 |
+
if len(turns)>=2 or(turns and has_tool):
|
| 284 |
+
chats.append({"chat":turns})
|
| 285 |
+
return chats
|
| 286 |
+
|
| 287 |
+
ROW_PARSERS={
|
| 288 |
+
"hermes-function-calling.jsonl":parse_hermes,
|
| 289 |
+
"python-codes-25k.jsonl":parse_python_codes,
|
| 290 |
+
"ma-proofbench.jsonl":parse_proofbench,
|
| 291 |
+
"multi-hop-websearch.jsonl":parse_multi_hop_websearch
|
| 292 |
+
}
|
| 293 |
+
FILE_PARSERS={
|
| 294 |
+
"fable-5-claude-code.jsonl":parse_claude_events,
|
| 295 |
+
"minimax-m3-traces.jsonl":parse_claude_events,
|
| 296 |
+
"opus-4.8-pi-traces.jsonl":parse_claude_events
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
def iter_rows(path):
|
| 300 |
+
with open(path,"r",encoding="utf-8") as f:
|
| 301 |
+
for line in f:
|
| 302 |
+
line=line.strip()
|
| 303 |
+
if not line:continue
|
| 304 |
+
try:yield json.loads(line)
|
| 305 |
+
except Exception:continue
|
| 306 |
+
|
| 307 |
+
def process_norm(path,filename):
|
| 308 |
+
os.makedirs(NORM,exist_ok=True)
|
| 309 |
+
out_path=os.path.join(NORM,filename)
|
| 310 |
+
count=0
|
| 311 |
+
with open(out_path,"w",encoding="utf-8") as fo:
|
| 312 |
+
if filename in FILE_PARSERS:
|
| 313 |
+
for chat in FILE_PARSERS[filename](list(iter_rows(path))):
|
| 314 |
+
fo.write(json.dumps(chat,ensure_ascii=False)+"\n")
|
| 315 |
+
count+=1
|
| 316 |
+
elif filename in ROW_PARSERS:
|
| 317 |
+
parser=ROW_PARSERS[filename]
|
| 318 |
+
for row in iter_rows(path):
|
| 319 |
+
parsed=parser(row)
|
| 320 |
+
if parsed:fo.write(json.dumps(parsed,ensure_ascii=False)+"\n")
|
| 321 |
+
count+=1
|
| 322 |
+
else:
|
| 323 |
+
print(f" [SKIP] No parser mapped: {filename}")
|
| 324 |
+
return
|
| 325 |
+
print(f" [OK] {filename:34s} → {count} chats")
|
| 326 |
+
|
| 327 |
+
def step_normalize():
|
| 328 |
+
print("\n"+"="*72)
|
| 329 |
+
print(" 🔄 STEP 2/4 — NORMALIZATION raw/ → normalized/")
|
| 330 |
+
print("="*72)
|
| 331 |
+
files=sorted(f for f in os.listdir(RAW) if f.lower().endswith(".jsonl"))
|
| 332 |
+
for fn in files:process_norm(os.path.join(RAW,fn),fn)
|
| 333 |
+
print("\n[DONE] normalized/ directory ready.")
|
| 334 |
+
|
| 335 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 336 |
+
# STEP 3 — CALIBRATION
|
| 337 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 338 |
+
CATEGORIES={
|
| 339 |
+
"Agentic":{"ratio":0.55,"files":[
|
| 340 |
+
"fable-5-claude-code.jsonl",
|
| 341 |
+
"minimax-m3-traces.jsonl",
|
| 342 |
+
"hermes-function-calling.jsonl",
|
| 343 |
+
"multi-hop-websearch.jsonl"
|
| 344 |
+
]},
|
| 345 |
+
"Frontier":{"ratio":0.10,"files":[
|
| 346 |
+
"opus-4.8-pi-traces.jsonl"
|
| 347 |
+
]},
|
| 348 |
+
"Logic":{"ratio":0.20,"files":[
|
| 349 |
+
"ma-proofbench.jsonl",
|
| 350 |
+
"python-codes-25k.jsonl"
|
| 351 |
+
]},
|
| 352 |
+
"Diversity":{"ratio":0.15,"raw":[
|
| 353 |
+
"calibration_data_v5_rc.txt"
|
| 354 |
+
]}
|
| 355 |
+
}
|
| 356 |
+
PRIORITY_SOURCES={"opus-4.8-pi-traces.jsonl","ma-proofbench.jsonl"}
|
| 357 |
+
HANDICAP={
|
| 358 |
+
"hermes-function-calling.jsonl":0.6,
|
| 359 |
+
"python-codes-25k.jsonl":0.7,
|
| 360 |
+
"multi-hop-websearch.jsonl":0.8
|
| 361 |
+
}
|
| 362 |
+
USER_MAX=6000
|
| 363 |
+
ASSISTANT_TRIM=5000
|
| 364 |
+
TARGET_MB=5
|
| 365 |
+
|
| 366 |
+
def load_source(fname):
|
| 367 |
+
path=os.path.join(NORM,fname)
|
| 368 |
+
blocks=[]
|
| 369 |
+
n_lines=n_drop=0
|
| 370 |
+
if not os.path.exists(path):
|
| 371 |
+
print(f" [ERR] Not found: {path}")
|
| 372 |
+
return fname,blocks
|
| 373 |
+
priority=fname in PRIORITY_SOURCES
|
| 374 |
+
with open(path,"r",encoding="utf-8") as f:
|
| 375 |
+
for line in f:
|
| 376 |
+
n_lines+=1
|
| 377 |
+
try:chat=json.loads(line).get("chat",[])
|
| 378 |
+
except Exception:continue
|
| 379 |
+
if not priority and any(m.get("role")=="user" and len(m.get("content",""))>USER_MAX for m in chat):
|
| 380 |
+
n_drop+=1
|
| 381 |
+
continue
|
| 382 |
+
merged=[]
|
| 383 |
+
for m in chat:
|
| 384 |
+
role=m.get("role")
|
| 385 |
+
text=(m.get("content") or "").strip()
|
| 386 |
+
if not role or not text:continue
|
| 387 |
+
if role=="assistant" and not priority and len(text)>ASSISTANT_TRIM:
|
| 388 |
+
text=text[:ASSISTANT_TRIM]
|
| 389 |
+
text=re.sub(r"\n{3,}","\n",text).strip()
|
| 390 |
+
if not text:continue
|
| 391 |
+
if merged and merged[-1]["role"]==role:
|
| 392 |
+
merged[-1]["content"]+="\n"+text
|
| 393 |
+
else:
|
| 394 |
+
merged.append({"role":role,"content":text})
|
| 395 |
+
if not merged:continue
|
| 396 |
+
parts=[f"‗{m['role']}‗\n{re.sub(chr(13),'',m['content']).strip()}" for m in merged]
|
| 397 |
+
blocks.append("\n".join(parts))
|
| 398 |
+
if fname in HANDICAP:
|
| 399 |
+
cap=HANDICAP[fname]
|
| 400 |
+
before=len(blocks)
|
| 401 |
+
blocks=blocks[:int(before*cap)]
|
| 402 |
+
print(f" [CAP] {fname:32s} {before}→{len(blocks)} blocks (~{int(cap*100)}%)")
|
| 403 |
+
tag="★100%" if priority else " "
|
| 404 |
+
print(f" {tag} {fname:32s} {n_lines:6d} lines (rejected {n_drop}) → {len(blocks)} blocks")
|
| 405 |
+
return fname,blocks
|
| 406 |
+
|
| 407 |
+
def load_raw_source(fname):
|
| 408 |
+
path=os.path.join(DATA,"raw",fname)
|
| 409 |
+
if not os.path.exists(path):
|
| 410 |
+
print(f" [ERR] Not found: {path}")
|
| 411 |
+
return fname,[]
|
| 412 |
+
with open(path,"r",encoding="utf-8",errors="replace") as f:
|
| 413 |
+
text=f.read()
|
| 414 |
+
if "===========" in text:
|
| 415 |
+
parts=re.split(r"\n=+\n",text)
|
| 416 |
+
else:
|
| 417 |
+
parts=re.split(r"\n{2,}",text)
|
| 418 |
+
blocks=[p.strip() for p in parts if len(p.strip())>200]
|
| 419 |
+
print(f" [RAW] {fname:32s} → {len(blocks)} raw blocks")
|
| 420 |
+
return fname,blocks
|
| 421 |
+
|
| 422 |
+
def load_category(name,cfg):
|
| 423 |
+
if "raw" in cfg:
|
| 424 |
+
print(f"\n📂 [{name}] {len(cfg['raw'])} raw source(s)…")
|
| 425 |
+
blocks=[b for fn in cfg["raw"] for b in load_raw_source(fn)[1]]
|
| 426 |
+
mb=sum(len(b.encode("utf-8")) for b in blocks)/1024/1024
|
| 427 |
+
print(f" → {name}: {mb:.1f} MB, {len(blocks)} blocks")
|
| 428 |
+
return name,blocks
|
| 429 |
+
print(f"\n📂 [{name}] {len(cfg['files'])} source(s)…")
|
| 430 |
+
with concurrent.futures.ThreadPoolExecutor() as ex:
|
| 431 |
+
results=list(ex.map(lambda fn:load_source(fn),cfg["files"]))
|
| 432 |
+
priority_bl=[b for fn,bl in results if fn in PRIORITY_SOURCES for b in bl]
|
| 433 |
+
rest=[bl for fn,bl in results if fn not in PRIORITY_SOURCES]
|
| 434 |
+
if not rest:inter=[]
|
| 435 |
+
elif len(rest)==1:inter=rest[0]
|
| 436 |
+
else:inter=[b for grp in itertools.zip_longest(*rest) for b in grp if b is not None]
|
| 437 |
+
combined=priority_bl+inter
|
| 438 |
+
mb=sum(len(b.encode("utf-8")) for b in combined)/1024/1024
|
| 439 |
+
print(f" → {name}: {mb:.1f} MB, {len(combined)} blocks ({len(priority_bl)} priority)")
|
| 440 |
+
return name,combined
|
| 441 |
+
|
| 442 |
+
def step_calibrate():
|
| 443 |
+
print("\n"+"="*72)
|
| 444 |
+
print(" 📊 STEP 3/4 — BUILDING experimental.txt")
|
| 445 |
+
print("="*72)
|
| 446 |
+
if os.path.exists(EXPERIMENTAL_CALIB):
|
| 447 |
+
print(f"[CALIB] Reusing existing {EXPERIMENTAL_CALIB}")
|
| 448 |
+
return {}
|
| 449 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(CATEGORIES)) as ex:
|
| 450 |
+
futures=[ex.submit(load_category,name,cfg) for name,cfg in CATEGORIES.items()]
|
| 451 |
+
results=[future.result() for future in futures]
|
| 452 |
+
avail={name:blocks for name,blocks in results}
|
| 453 |
+
sizes_av={n:sum(len(b.encode("utf-8")) for b in avail[n]) for n in CATEGORIES}
|
| 454 |
+
# Iterative saturation under global budget: exhaust small categories and reallocate remainder
|
| 455 |
+
targets={}
|
| 456 |
+
remaining={n:CATEGORIES[n]["ratio"] for n in CATEGORIES}
|
| 457 |
+
pool=dict(sizes_av)
|
| 458 |
+
budget_total=TARGET_MB*1024*1024
|
| 459 |
+
while remaining:
|
| 460 |
+
total_r=sum(remaining.values())
|
| 461 |
+
budget=min(sum(pool[n] for n in remaining),budget_total)
|
| 462 |
+
saturated=[n for n in remaining if pool[n]<budget*remaining[n]/total_r]
|
| 463 |
+
if not saturated:
|
| 464 |
+
for n in remaining:targets[n]=budget*remaining[n]/total_r
|
| 465 |
+
break
|
| 466 |
+
for n in saturated:
|
| 467 |
+
targets[n]=pool[n]
|
| 468 |
+
budget_total=max(0,budget_total-pool[n])
|
| 469 |
+
del remaining[n]
|
| 470 |
+
del pool[n]
|
| 471 |
+
print("\n"+"="*68)
|
| 472 |
+
print(" 📊 DIAGNOSTIC")
|
| 473 |
+
print("="*68)
|
| 474 |
+
for n in CATEGORIES:
|
| 475 |
+
t=targets.get(n,0)
|
| 476 |
+
flag=" ★saturated" if abs(t-sizes_av[n])<1 else ""
|
| 477 |
+
print(f" {n:10s} {sizes_av[n]/1024/1024:7.2f} MB available ({len(avail[n]):5d} blocks) → target {t/1024/1024:6.2f} MB{flag}")
|
| 478 |
+
print(f" Target corpus size: {sum(targets.values())/1024/1024:.2f} MB")
|
| 479 |
+
sizes={n:0 for n in CATEGORIES}
|
| 480 |
+
idx={n:0 for n in CATEGORIES}
|
| 481 |
+
contrib={}
|
| 482 |
+
out=[]
|
| 483 |
+
print("\n🔨 Packing blocks…")
|
| 484 |
+
while True:
|
| 485 |
+
deficit={n:targets[n]-sizes[n] for n in CATEGORIES}
|
| 486 |
+
cand=[n for n in CATEGORIES if idx[n]<len(avail[n]) and deficit[n]>0]
|
| 487 |
+
if not cand:break
|
| 488 |
+
pick=max(cand,key=lambda n:deficit[n])
|
| 489 |
+
blk=avail[pick][idx[pick]]
|
| 490 |
+
idx[pick]+=1
|
| 491 |
+
b=len(blk.encode("utf-8"))
|
| 492 |
+
sizes[pick]+=b
|
| 493 |
+
out.append(blk)
|
| 494 |
+
contrib[pick]=contrib.get(pick,0)+1
|
| 495 |
+
if not out:
|
| 496 |
+
print("\n [ERR] No blocks collected — experimental.txt not written.")
|
| 497 |
+
return {}
|
| 498 |
+
with open(EXPERIMENTAL_CALIB,"w",encoding="utf-8",newline="\n") as f:
|
| 499 |
+
f.write("\n".join(out))
|
| 500 |
+
total=sum(sizes.values())
|
| 501 |
+
print("\n"+"="*68)
|
| 502 |
+
print(f" ✅ experimental.txt : {total/1024/1024:.2f} MB, {len(out)} blocks")
|
| 503 |
+
print("="*68)
|
| 504 |
+
stats={}
|
| 505 |
+
for n in CATEGORIES:
|
| 506 |
+
r=sizes[n]/total if total else 0
|
| 507 |
+
print(f" {n:10s} {sizes[n]/1024/1024:6.2f} MB actual {r:5.1%} (target {CATEGORIES[n]['ratio']:.0%}) {contrib.get(n,0)} blocks")
|
| 508 |
+
stats[n]={"mb":round(sizes[n]/1024/1024,2),"ratio":round(r,4),"blocks":contrib.get(n,0)}
|
| 509 |
+
stats["total_mb"]=round(total/1024/1024,2)
|
| 510 |
+
stats["total_blocks"]=len(out)
|
| 511 |
+
return stats
|
| 512 |
+
|
| 513 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 514 |
+
# STEP 3b — STRIP MTP (GGUF copy without MTP heads for llama-imatrix)
|
| 515 |
+
# ═══════════════════════════════════��═══════════════════════════════════════
|
| 516 |
+
MTP_TENSOR_HINT=("mtp.","nextn.",".nextn_")
|
| 517 |
+
MTP_KV_HINT=("nextn_predict_layers","mtp_num")
|
| 518 |
+
|
| 519 |
+
def is_mtp_name(name):
|
| 520 |
+
low=name.lower()
|
| 521 |
+
return any(h in low for h in MTP_TENSOR_HINT)
|
| 522 |
+
|
| 523 |
+
def gguf_analyze_mtp(path):
|
| 524 |
+
"""Analyze blk.N layers structurally to locate the MTP head.
|
| 525 |
+
|
| 526 |
+
Returns (mtp_layers, n_keep):
|
| 527 |
+
mtp_layers — set of blk.N indices forming the MTP head (empty set if the
|
| 528 |
+
MTP lives only in name-matched tensors, None if no MTP found)
|
| 529 |
+
n_keep — corrected block_count for the stripped file
|
| 530 |
+
"""
|
| 531 |
+
import gguf
|
| 532 |
+
from collections import Counter
|
| 533 |
+
try:reader=gguf.GGUFReader(path)
|
| 534 |
+
except Exception as e:
|
| 535 |
+
print(f" [WARN] MTP scan failed ({e}) → using original GGUF directly")
|
| 536 |
+
return None,None
|
| 537 |
+
n_decl=None
|
| 538 |
+
mtp_kv=False
|
| 539 |
+
for field in reader.fields.values():
|
| 540 |
+
if field.name.endswith(".block_count"):
|
| 541 |
+
v=field.parts[field.data[0]]
|
| 542 |
+
n_decl=int(v.item() if hasattr(v,"item") else v)
|
| 543 |
+
if any(h in field.name.lower() for h in MTP_KV_HINT):mtp_kv=True
|
| 544 |
+
layers={}
|
| 545 |
+
named=False
|
| 546 |
+
named_layers=set()
|
| 547 |
+
for t in reader.tensors:
|
| 548 |
+
m=re.match(r"blk\.(\d+)\.(.+)$",t.name)
|
| 549 |
+
if m:
|
| 550 |
+
layers.setdefault(int(m.group(1)),set()).add(m.group(2))
|
| 551 |
+
if is_mtp_name(t.name):named_layers.add(int(m.group(1)))
|
| 552 |
+
elif is_mtp_name(t.name):
|
| 553 |
+
named=True
|
| 554 |
+
if not layers:
|
| 555 |
+
return(set() if(named or mtp_kv) else None),n_decl
|
| 556 |
+
idx=sorted(layers)
|
| 557 |
+
sig_count=Counter(frozenset(layers[i]) for i in idx)
|
| 558 |
+
legit={sig for sig,cnt in sig_count.items() if cnt>=2}
|
| 559 |
+
if not legit:
|
| 560 |
+
ref=max(sig_count,key=lambda s:len(s))
|
| 561 |
+
legit={ref}
|
| 562 |
+
mtp={i for i in idx if frozenset(layers[i]) not in legit}
|
| 563 |
+
if n_decl is not None:mtp|={i for i in idx if i>=n_decl}
|
| 564 |
+
mtp|=named_layers
|
| 565 |
+
if mtp and not named_layers:
|
| 566 |
+
has_eh=any("nextn.eh_proj" in t.name for t in reader.tensors)
|
| 567 |
+
if not has_eh and not mtp_kv:
|
| 568 |
+
print(" [MTP] Unique terminal block signature without nextn.eh_proj")
|
| 569 |
+
print(" → standard attention layer, strip not required")
|
| 570 |
+
return None,None
|
| 571 |
+
if not mtp and not named and not mtp_kv:return None,None
|
| 572 |
+
keep=[i for i in idx if i not in mtp]
|
| 573 |
+
n_keep=len(keep) if keep else n_decl
|
| 574 |
+
print(f" [MTP] Layer analysis: {len(idx)} blk.N layers, {len(sig_count)} distinct signature(s)")
|
| 575 |
+
for sig,cnt in sorted(sig_count.items(),key=lambda x:-x[1]):
|
| 576 |
+
tag="legitimate" if sig in legit else "MTP HEAD"
|
| 577 |
+
print(f" signature {len(sig)} tensor(s) × {cnt} layer(s) → {tag}")
|
| 578 |
+
for i in sorted(mtp):
|
| 579 |
+
print(f" layer blk.{i} : {len(layers[i])} tensor(s) → removed MTP head")
|
| 580 |
+
return mtp,n_keep
|
| 581 |
+
|
| 582 |
+
def strip_mtp_gguf(src,dst,mtp_layers,n_keep):
|
| 583 |
+
"""Copy a GGUF while removing whole MTP layers, name-matched MTP tensors and
|
| 584 |
+
|
| 585 |
+
their metadata keys. block_count is rewritten to n_keep."""
|
| 586 |
+
import gguf
|
| 587 |
+
reader=gguf.GGUFReader(src)
|
| 588 |
+
arch_field=reader.fields["general.architecture"]
|
| 589 |
+
arch=str(arch_field.parts[arch_field.data[0]],encoding="utf-8")
|
| 590 |
+
writer=gguf.GGUFWriter(dst,arch)
|
| 591 |
+
for field in reader.fields.values():
|
| 592 |
+
if field.name.startswith("GGUF.") or field.name=="general.architecture":continue
|
| 593 |
+
if any(h in field.name.lower() for h in MTP_KV_HINT):
|
| 594 |
+
print(f" [KV-SKIP] {field.name}")
|
| 595 |
+
continue
|
| 596 |
+
if n_keep is not None and field.name.endswith(".block_count"):
|
| 597 |
+
v=field.parts[field.data[0]]
|
| 598 |
+
old=int(v.item() if hasattr(v,"item") else v)
|
| 599 |
+
if old!=n_keep:
|
| 600 |
+
print(f" [KV-FIX] {field.name} : {old} → {n_keep}")
|
| 601 |
+
writer.add_key_value(field.name,n_keep,field.types[0])
|
| 602 |
+
continue
|
| 603 |
+
writer.add_key_value(field.name,field.contents(),field.types[0])
|
| 604 |
+
kept=dropped=0
|
| 605 |
+
for tensor in reader.tensors:
|
| 606 |
+
m=re.match(r"blk\.(\d+)\.",tensor.name)
|
| 607 |
+
if is_mtp_name(tensor.name) or(mtp_layers and m and int(m.group(1)) in mtp_layers):
|
| 608 |
+
dropped+=1
|
| 609 |
+
continue
|
| 610 |
+
writer.add_tensor(tensor.name,tensor.data,raw_shape=tensor.data.shape,raw_dtype=tensor.tensor_type)
|
| 611 |
+
kept+=1
|
| 612 |
+
writer.write_header_to_file()
|
| 613 |
+
writer.write_kv_data_to_file()
|
| 614 |
+
writer.write_tensors_to_file()
|
| 615 |
+
writer.close()
|
| 616 |
+
print(f" [STRIP] {kept} tensor(s) kept, {dropped} MTP tensor(s) stripped (layers {sorted(mtp_layers) if mtp_layers else 'named'})")
|
| 617 |
+
|
| 618 |
+
def step_strip_mtp():
|
| 619 |
+
"""Ensure a no-mtp GGUF exists for imatrix when the full model embeds MTP."""
|
| 620 |
+
global BF16
|
| 621 |
+
base_fn=os.path.basename(BF16)
|
| 622 |
+
if "-no-mtp" in base_fn or "no-mtp" in base_fn:return
|
| 623 |
+
dirname=os.path.dirname(BF16)
|
| 624 |
+
no_mtp_name=base_fn.replace("model-","model-no-mtp-") if base_fn.startswith("model-") else f"model-no-mtp-{base_fn}"
|
| 625 |
+
no_mtp=os.path.join(dirname,no_mtp_name)
|
| 626 |
+
legacy_no_mtp=os.path.join(dirname,base_fn.replace(".gguf","-no-mtp.gguf"))
|
| 627 |
+
if os.path.exists(no_mtp):
|
| 628 |
+
BF16=no_mtp
|
| 629 |
+
print(f" [OK] Stripped variant already exists: {os.path.basename(no_mtp)}")
|
| 630 |
+
return
|
| 631 |
+
if os.path.exists(legacy_no_mtp):
|
| 632 |
+
BF16=legacy_no_mtp
|
| 633 |
+
print(f" [OK] Stripped variant already exists: {os.path.basename(legacy_no_mtp)}")
|
| 634 |
+
return
|
| 635 |
+
mtp_layers,n_keep=gguf_analyze_mtp(BF16)
|
| 636 |
+
if mtp_layers is None:
|
| 637 |
+
print(" [OK] No MTP tensors detected → using full GGUF directly")
|
| 638 |
+
return
|
| 639 |
+
print(f" [MTP] MTP head detected → generating {os.path.basename(no_mtp)}…")
|
| 640 |
+
try:
|
| 641 |
+
strip_mtp_gguf(BF16,no_mtp,mtp_layers,n_keep)
|
| 642 |
+
BF16=no_mtp
|
| 643 |
+
except Exception as e:
|
| 644 |
+
if os.path.exists(no_mtp):
|
| 645 |
+
try:os.remove(no_mtp)
|
| 646 |
+
except OSError:pass
|
| 647 |
+
sys.exit(f"❌ MTP strip failed: {e}")
|
| 648 |
+
|
| 649 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 650 |
+
# STEP 4 — IMATRIX
|
| 651 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 652 |
+
RECURRENT_HINT=("delta","mamba","rwkv","lfm","gated","hybrid","recurrent","ssm","qwen3_next","qwen3.5","qwen35")
|
| 653 |
+
PPL_CEILING=50.0
|
| 654 |
+
|
| 655 |
+
def read_gguf_meta(path):
|
| 656 |
+
"""Read architecture and block_count via gguf-py (authoritative parser)."""
|
| 657 |
+
file_mb=os.path.getsize(path)/1024/1024
|
| 658 |
+
try:
|
| 659 |
+
import gguf
|
| 660 |
+
reader=gguf.GGUFReader(path)
|
| 661 |
+
arch=None
|
| 662 |
+
n_layers=0
|
| 663 |
+
af=reader.fields.get("general.architecture")
|
| 664 |
+
if af:arch=str(af.parts[af.data[0]],encoding="utf-8")
|
| 665 |
+
for field in reader.fields.values():
|
| 666 |
+
if field.name.endswith(".block_count"):
|
| 667 |
+
v=field.parts[field.data[0]]
|
| 668 |
+
n_layers=int(v.item() if hasattr(v,"item") else v)
|
| 669 |
+
break
|
| 670 |
+
if not(0<n_layers<10000):n_layers=0
|
| 671 |
+
return arch,n_layers,file_mb
|
| 672 |
+
except Exception as e:
|
| 673 |
+
print(f" [GGUF] Metadata parse failed: {e}")
|
| 674 |
+
return None,0,file_mb
|
| 675 |
+
|
| 676 |
+
def ensure_default_calibration():
|
| 677 |
+
if os.path.exists(DEFAULT_CALIB_PATH):
|
| 678 |
+
print(f" [OK] Default calibration file present: {DEFAULT_CALIB_NAME}")
|
| 679 |
+
return DEFAULT_CALIB_PATH
|
| 680 |
+
print(f"\n[DL] Downloading default calibration dataset: {DEFAULT_CALIB_NAME}…")
|
| 681 |
+
os.makedirs(DATA,exist_ok=True)
|
| 682 |
+
url=DEFAULT_CALIB_URL.replace("/blob/","/resolve/")
|
| 683 |
+
try:
|
| 684 |
+
urllib.request.urlretrieve(url,DEFAULT_CALIB_PATH)
|
| 685 |
+
print(f" [DONE] {DEFAULT_CALIB_NAME} ({os.path.getsize(DEFAULT_CALIB_PATH)/1024/1024:.2f} MB)")
|
| 686 |
+
except Exception as e:print(f" [ERR] Download failed: {e}")
|
| 687 |
+
return DEFAULT_CALIB_PATH
|
| 688 |
+
|
| 689 |
+
# Q8_0 by default: imatrix measures uncorrupted activations. Ladder extensible via IMATRIX_QUANT_LADDER
|
| 690 |
+
QUANT_LADDER=tuple(
|
| 691 |
+
q.strip() for q in os.environ.get("IMATRIX_QUANT_LADDER","Q8_0").split(",") if q.strip()
|
| 692 |
+
)
|
| 693 |
+
QUANT_BPW_RATIO={"Q8_0":0.53,"Q6_K":0.41,"Q5_K_M":0.35}
|
| 694 |
+
|
| 695 |
+
def ensure_gpu_sized_model(src,gpu_free,overhead,compute_mb):
|
| 696 |
+
"""Quantize the imatrix source down until it fits entirely into VRAM.
|
| 697 |
+
|
| 698 |
+
A partial -ngl split breaks fused GDN on hybrid archs, so a smaller
|
| 699 |
+
weight format preserves full GPU offload. Returns (path, created_this_run).
|
| 700 |
+
"""
|
| 701 |
+
budget=gpu_free-overhead-compute_mb
|
| 702 |
+
if budget<=0:return src,False
|
| 703 |
+
if os.path.getsize(src)/1024/1024<=budget:return src,False
|
| 704 |
+
quantize=os.path.join(LLAMA,"llama-quantize.exe")
|
| 705 |
+
if not os.path.exists(quantize):
|
| 706 |
+
print(" [QUANT] llama-quantize.exe missing → retaining source BF16")
|
| 707 |
+
return src,False
|
| 708 |
+
base=os.path.basename(src).replace(".gguf","")
|
| 709 |
+
for fmt in QUANT_LADDER:
|
| 710 |
+
dst=os.path.join(os.path.dirname(src),f"{base}-{fmt}.gguf")
|
| 711 |
+
if os.path.exists(dst):
|
| 712 |
+
mb=os.path.getsize(dst)/1024/1024
|
| 713 |
+
if mb<=budget:
|
| 714 |
+
print(f" [QUANT] Reusing {os.path.basename(dst)} ({mb/1024:.1f} GB)")
|
| 715 |
+
return dst,False
|
| 716 |
+
continue
|
| 717 |
+
est=os.path.getsize(src)/1024/1024*QUANT_BPW_RATIO.get(fmt,0.45)
|
| 718 |
+
if est>budget:continue
|
| 719 |
+
print(f" [QUANT] {fmt} estimated at {est/1024:.1f} GB ≤ budget {budget/1024:.1f} GB → converting…")
|
| 720 |
+
if fmt!="Q8_0":
|
| 721 |
+
print(f" ⚠ [QUANT] {fmt} < Q8_0: activation statistics will be sampled over lower precision weights.")
|
| 722 |
+
print(" Use CPU mode (--cpu-only) when numerical fidelity takes precedence over runtime.")
|
| 723 |
+
r=subprocess.run([quantize,src,dst,fmt],capture_output=True,text=True)
|
| 724 |
+
if r.returncode==0 and os.path.exists(dst):
|
| 725 |
+
mb=os.path.getsize(dst)/1024/1024
|
| 726 |
+
print(f" [QUANT] {os.path.basename(dst)} ready ({mb/1024:.1f} GB) → full GPU offload enabled")
|
| 727 |
+
return dst,True
|
| 728 |
+
if os.path.exists(dst):
|
| 729 |
+
try:os.remove(dst)
|
| 730 |
+
except OSError:pass
|
| 731 |
+
print(f" [QUANT] {fmt} failed → trying next quantization format")
|
| 732 |
+
print(" [QUANT] No quantized intermediate fit in budget → retaining original model")
|
| 733 |
+
return src,False
|
| 734 |
+
|
| 735 |
+
def run_imatrix_streaming(cmd):
|
| 736 |
+
"""Stream llama-imatrix with real-time progress, ETA, and stderr forwarding.
|
| 737 |
+
|
| 738 |
+
Returns (returncode, stdout_text, stderr_text).
|
| 739 |
+
"""
|
| 740 |
+
import threading
|
| 741 |
+
proc=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
| 742 |
+
out_parts=[];err_parts=[]
|
| 743 |
+
err_buf="";total=None;current=0;leftover=""
|
| 744 |
+
t0=time.perf_counter();last_disp=0.0
|
| 745 |
+
lock=threading.Lock();pending_stderr=[]
|
| 746 |
+
def on_stdout(text):
|
| 747 |
+
nonlocal current,leftover
|
| 748 |
+
buf=leftover+text
|
| 749 |
+
for m in re.finditer(r"\[(\d+)\]",buf):
|
| 750 |
+
n=int(m.group(1))
|
| 751 |
+
if n>current:current=n
|
| 752 |
+
leftover=buf[-8:]
|
| 753 |
+
def on_stderr(text):
|
| 754 |
+
nonlocal total,err_buf
|
| 755 |
+
err_buf+=text
|
| 756 |
+
if total is None:
|
| 757 |
+
m=re.search(r"computing over (\d+) chunks",err_buf)
|
| 758 |
+
if m:total=int(m.group(1))
|
| 759 |
+
while "\n" in err_buf:
|
| 760 |
+
line,err_buf=err_buf.split("\n",1)
|
| 761 |
+
line=line.strip()
|
| 762 |
+
if line:pending_stderr.append(line)
|
| 763 |
+
def drain(stream,bufs,cb=None):
|
| 764 |
+
while True:
|
| 765 |
+
try:chunk=stream.read1(128)
|
| 766 |
+
except Exception:break
|
| 767 |
+
if not chunk:break
|
| 768 |
+
text=chunk.decode("utf-8",errors="replace")
|
| 769 |
+
bufs.append(text)
|
| 770 |
+
if cb:cb(text)
|
| 771 |
+
th_out=threading.Thread(target=drain,args=(proc.stdout,out_parts,on_stdout),daemon=True)
|
| 772 |
+
th_err=threading.Thread(target=drain,args=(proc.stderr,err_parts,on_stderr),daemon=True)
|
| 773 |
+
th_out.start();th_err.start()
|
| 774 |
+
while proc.poll() is None:
|
| 775 |
+
time.sleep(1.0)
|
| 776 |
+
now=time.perf_counter()
|
| 777 |
+
with lock:
|
| 778 |
+
while pending_stderr:
|
| 779 |
+
sys.stdout.write("\r"+" "*100+"\r");sys.stdout.flush()
|
| 780 |
+
print(f" {pending_stderr.pop(0)}")
|
| 781 |
+
if now-last_disp>=1.0:
|
| 782 |
+
last_disp=now
|
| 783 |
+
el=now-t0
|
| 784 |
+
if total and current>0:
|
| 785 |
+
pct=current/total*100
|
| 786 |
+
speed=current/el if el>0 else 0
|
| 787 |
+
rem=(total-current)/speed if speed>0 else 0
|
| 788 |
+
msg=f" [{current}/{total}] {pct:5.1f}% · {el/60:.1f} min elapsed · ETA {rem/60:.1f} min "
|
| 789 |
+
elif current>0:
|
| 790 |
+
msg=f" [{current} chunks processed] · {el/60:.1f} min "
|
| 791 |
+
else:
|
| 792 |
+
msg=f" ⏳ Running... {el/60:.1f} min "
|
| 793 |
+
sys.stdout.write("\r"+msg);sys.stdout.flush()
|
| 794 |
+
proc.wait()
|
| 795 |
+
th_out.join(timeout=5);th_err.join(timeout=5)
|
| 796 |
+
with lock:
|
| 797 |
+
while pending_stderr:
|
| 798 |
+
print(f" {pending_stderr.pop(0)}")
|
| 799 |
+
sys.stdout.write("\r"+" "*100+"\r");sys.stdout.flush()
|
| 800 |
+
el=time.perf_counter()-t0
|
| 801 |
+
print(f" Finished in {el/60:.1f} min")
|
| 802 |
+
return proc.returncode,"".join(out_parts),"".join(err_parts)
|
| 803 |
+
|
| 804 |
+
def step_imatrix(calib_path):
|
| 805 |
+
print("\n"+"="*72)
|
| 806 |
+
print(" 🧮 STEP 4/4 — IMATRIX CALCULATION (imatrix.dat)")
|
| 807 |
+
print("="*72)
|
| 808 |
+
global BF16
|
| 809 |
+
print(f" Calibration source: {calib_path}")
|
| 810 |
+
if not os.path.exists(calib_path) or os.path.getsize(calib_path)<10240:
|
| 811 |
+
sys.exit(f"❌ Calibration dataset missing or undersized: {calib_path}")
|
| 812 |
+
if not os.path.exists(BF16):
|
| 813 |
+
sys.exit("❌ model-BF16.gguf (or -F16) not found in gguf root directory.")
|
| 814 |
+
imatrix_exe=os.path.join(LLAMA,"llama-imatrix.exe")
|
| 815 |
+
if not os.path.exists(imatrix_exe):
|
| 816 |
+
sys.exit(f"❌ Binary not found: {imatrix_exe}")
|
| 817 |
+
if os.path.exists(IMATRIX):
|
| 818 |
+
print(f"[IMATRIX] Reusing existing {IMATRIX}")
|
| 819 |
+
return
|
| 820 |
+
gpu_total=gpu_used=0
|
| 821 |
+
try:
|
| 822 |
+
r=subprocess.run(["nvidia-smi","--query-gpu=memory.total,memory.used","--format=csv,noheader,nounits"],capture_output=True,text=True,timeout=5)
|
| 823 |
+
parts=r.stdout.strip().split("\n")[0].split(",")
|
| 824 |
+
gpu_total=int(parts[0].strip());gpu_used=int(parts[1].strip())
|
| 825 |
+
except Exception:pass
|
| 826 |
+
gpu_free=max(0,gpu_total-gpu_used)
|
| 827 |
+
ngl=0
|
| 828 |
+
arch,n_layers,file_mb=read_gguf_meta(BF16)
|
| 829 |
+
recurrent=bool(arch) and any(h in arch.lower() for h in RECURRENT_HINT)
|
| 830 |
+
sized_new=None
|
| 831 |
+
if recurrent and gpu_free:
|
| 832 |
+
sized,sized_created=ensure_gpu_sized_model(BF16,gpu_free,900,1024)
|
| 833 |
+
if sized!=BF16:
|
| 834 |
+
if sized_created:sized_new=sized
|
| 835 |
+
BF16=sized
|
| 836 |
+
arch,n_layers,file_mb=read_gguf_meta(BF16)
|
| 837 |
+
print(f" Model: arch={arch or '?'} layers={n_layers or '?'} size={file_mb/1024:.1f} GB"+(" [recurrent/hybrid]" if recurrent else ""))
|
| 838 |
+
try:help_txt=subprocess.run([imatrix_exe,"-h"],capture_output=True,text=True,timeout=10).stdout
|
| 839 |
+
except Exception:help_txt=""
|
| 840 |
+
extra=[]
|
| 841 |
+
if "--flash-attn" in help_txt:extra+=["-fa","on"]
|
| 842 |
+
if "--process-output" in help_txt:extra+=["--process-output"]
|
| 843 |
+
if "--output-frequency" in help_txt or "-ofreq" in help_txt:extra+=["--output-frequency","100"]
|
| 844 |
+
extra_bench=list(extra)
|
| 845 |
+
if "--no-ppl" in help_txt:extra_bench+=["--no-ppl"]
|
| 846 |
+
OVERHEAD=900
|
| 847 |
+
def compute_est(ub):return max(384,ub)
|
| 848 |
+
candidates=[]
|
| 849 |
+
if gpu_free and n_layers:
|
| 850 |
+
headroom=gpu_free-file_mb-OVERHEAD
|
| 851 |
+
if headroom>0:
|
| 852 |
+
ngl=n_layers+1
|
| 853 |
+
ubs=[ub for ub in(2048,1024) if compute_est(ub)<=headroom] or [1024]
|
| 854 |
+
candidates+=[(ngl,ub) for ub in ubs]
|
| 855 |
+
for frac in(0.9,0.75,0.5):
|
| 856 |
+
lower=int(n_layers*frac)
|
| 857 |
+
if 0<lower<ngl:candidates.append((lower,1024))
|
| 858 |
+
print(f" Free VRAM {gpu_free} MiB → FULL GPU offload ({ngl} layers), margin {headroom:.0f} MiB → candidate ubatch {ubs}")
|
| 859 |
+
else:
|
| 860 |
+
avail_mb=max(0,gpu_free-OVERHEAD-compute_est(1024))
|
| 861 |
+
mb_per_layer=(file_mb/n_layers) if n_layers else 380
|
| 862 |
+
ngl=int(avail_mb/mb_per_layer) if mb_per_layer>0 else 0
|
| 863 |
+
if n_layers:ngl=min(ngl,n_layers)
|
| 864 |
+
print(f" Free VRAM {gpu_free} MiB → budget {avail_mb} MiB / {mb_per_layer:.0f} MiB per layer → max {ngl}/{n_layers or '?'} layers")
|
| 865 |
+
if ngl>0:
|
| 866 |
+
step=max(1,(ngl+5)//6)
|
| 867 |
+
for layers in range(ngl,0,-step):
|
| 868 |
+
ubs=(2048,1024) if layers==ngl else(1024,512)
|
| 869 |
+
for ub in ubs:candidates.append((layers,ub))
|
| 870 |
+
else:
|
| 871 |
+
print(" → Insufficient VRAM for GPU layers; defaulting to CPU mode")
|
| 872 |
+
else:
|
| 873 |
+
print(" VRAM unavailable → defaulting to CPU mode")
|
| 874 |
+
candidates.append((0,1024))
|
| 875 |
+
seen=set();candidates=[c for c in candidates if not(c in seen or seen.add(c))]
|
| 876 |
+
force_cpu=any(a in sys.argv[1:] for a in("--cpu-only","--no-hybrid-split"))
|
| 877 |
+
if recurrent and n_layers and any(0<l<n_layers for l,_ in candidates):
|
| 878 |
+
if force_cpu:
|
| 879 |
+
print(" ℹ --cpu-only: skipping GPU split; computing activation caches entirely on CPU")
|
| 880 |
+
candidates=[c for c in candidates if c[0]==0] or [(0,1024)]
|
| 881 |
+
else:
|
| 882 |
+
print(f" ℹ Hybrid/recurrent (GDN): testing {ngl}/{n_layers} layer split; verifying PPL at completion")
|
| 883 |
+
print(" Automatic CPU fallback triggers if PPL exceeds threshold. Force CPU with --cpu-only")
|
| 884 |
+
def build_cmd(layers,ubatch,out,flags=None):
|
| 885 |
+
cmd=[imatrix_exe,"-m",BF16,"-f",calib_path,"-o",out,"-t","12","-c","512","-b",str(max(2048,ubatch)),"-ub",str(ubatch)]+(flags if flags is not None else extra)
|
| 886 |
+
if layers>0:cmd+=["-ngl",str(layers)]
|
| 887 |
+
return cmd
|
| 888 |
+
benchmarks=[]
|
| 889 |
+
failed=set()
|
| 890 |
+
best_time=None
|
| 891 |
+
best_profile=None
|
| 892 |
+
gpu_cands=[c for c in candidates if c[0]>0]
|
| 893 |
+
if not gpu_cands:
|
| 894 |
+
print("\nSkipping GPU autotune (no eligible GPU candidates).")
|
| 895 |
+
else:
|
| 896 |
+
print("\nAutotuning GPU parameters over 12 chunks…")
|
| 897 |
+
for layers,ubatch in gpu_cands:
|
| 898 |
+
bench_out=os.path.join(DATA,f"_bench_ngl{layers}_ub{ubatch}.dat")
|
| 899 |
+
started=time.perf_counter()
|
| 900 |
+
result=subprocess.run(build_cmd(layers,ubatch,bench_out,extra_bench)+["--chunks","12"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
| 901 |
+
elapsed=time.perf_counter()-started
|
| 902 |
+
if os.path.exists(bench_out):
|
| 903 |
+
try:os.remove(bench_out)
|
| 904 |
+
except OSError:pass
|
| 905 |
+
if result.returncode!=0:
|
| 906 |
+
failed.add((layers,ubatch))
|
| 907 |
+
print(f" ngl={layers:2d} · ubatch={ubatch:4d} → FAILED (OOM/VRAM) — candidate discarded")
|
| 908 |
+
continue
|
| 909 |
+
benchmarks.append((elapsed,layers,ubatch))
|
| 910 |
+
if best_profile is None or elapsed<best_time:
|
| 911 |
+
best_time=elapsed
|
| 912 |
+
best_profile=(layers,ubatch)
|
| 913 |
+
print(f" ngl={layers:2d} · ubatch={ubatch:4d} → {elapsed:.1f} s ★ best candidate")
|
| 914 |
+
else:
|
| 915 |
+
print(f" ngl={layers:2d} · ubatch={ubatch:4d} → {elapsed:.1f} s")
|
| 916 |
+
if elapsed>best_time*1.02:
|
| 917 |
+
print(f" ↑ Regression > 2% vs {best_time:.1f} s → Stopping autotune; selected: ngl={best_profile[0]} · ubatch={best_profile[1]}")
|
| 918 |
+
break
|
| 919 |
+
if recurrent and best_profile is not None and best_time is not None and n_layers:
|
| 920 |
+
full=[b for b in benchmarks if b[1]>n_layers]
|
| 921 |
+
if full and best_profile[0]<=n_layers and full[0][0]<=best_time*1.05:
|
| 922 |
+
best_profile=(full[0][1],full[0][2])
|
| 923 |
+
print(f" ℹ Preferring full offload ({full[0][0]:.1f} s ≈ best) — avoids GDN split overhead")
|
| 924 |
+
if best_profile is None:
|
| 925 |
+
best_profile=(0,1024)
|
| 926 |
+
print(" No candidate fits VRAM cleanly → using CPU mode (ngl=0)")
|
| 927 |
+
ngl,best_ubatch=best_profile
|
| 928 |
+
print(f" Final profile: ngl={ngl} · batch={max(2048,best_ubatch)} · ubatch={best_ubatch}")
|
| 929 |
+
profiles=[best_profile]
|
| 930 |
+
for profile in candidates:
|
| 931 |
+
if profile not in profiles and profile not in failed:
|
| 932 |
+
profiles.append(profile)
|
| 933 |
+
for attempt,(layers,ubatch) in enumerate(profiles,1):
|
| 934 |
+
print(f" Attempt {attempt}/{len(profiles)}: {layers} GPU layers, ubatch {ubatch}")
|
| 935 |
+
rc,stdout_text,stderr_text=run_imatrix_streaming(build_cmd(layers,ubatch,IMATRIX))
|
| 936 |
+
if rc==0:
|
| 937 |
+
m=re.search(r"Final estimate:\s*PPL\s*=\s*([0-9.]+)",stdout_text+"\n"+stderr_text,re.I)
|
| 938 |
+
ppl=float(m.group(1)) if m else None
|
| 939 |
+
if ppl is None:
|
| 940 |
+
print(" Final PPL: unreported (binary compiled without --ppl support)")
|
| 941 |
+
elif ppl<PPL_CEILING or layers==0:
|
| 942 |
+
print(f" Final PPL: {ppl:.2f} ✓ imatrix healthy")
|
| 943 |
+
else:
|
| 944 |
+
print(f" Final PPL: {ppl:.2f} ⚠ > {PPL_CEILING} → GDN split issue suspected, restarting on CPU")
|
| 945 |
+
if os.path.exists(IMATRIX):
|
| 946 |
+
try:os.remove(IMATRIX)
|
| 947 |
+
except OSError:pass
|
| 948 |
+
profiles.append((0,1024))
|
| 949 |
+
continue
|
| 950 |
+
if sized_new and os.path.exists(sized_new):
|
| 951 |
+
try:
|
| 952 |
+
os.remove(sized_new)
|
| 953 |
+
print(f" 🧹 Temporary intermediate removed: {os.path.basename(sized_new)}")
|
| 954 |
+
except OSError:pass
|
| 955 |
+
print(f"[OK] {IMATRIX}")
|
| 956 |
+
break
|
| 957 |
+
if os.path.exists(IMATRIX):
|
| 958 |
+
try:os.remove(IMATRIX)
|
| 959 |
+
except OSError:pass
|
| 960 |
+
else:
|
| 961 |
+
sys.exit("❌ llama-imatrix failed across all candidate profiles.")
|
| 962 |
+
|
| 963 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 964 |
+
# MAIN
|
| 965 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 966 |
+
def main():
|
| 967 |
+
t0=time.perf_counter()
|
| 968 |
+
global BF16
|
| 969 |
+
print("="*72)
|
| 970 |
+
print(" 🚀 IMATRIX & CALIBRATION DATASET PIPELINE")
|
| 971 |
+
print("="*72)
|
| 972 |
+
force_rebuild=any(a in sys.argv[1:] for a in("--force-rebuild","--rebuild"))
|
| 973 |
+
explicit_dataset_only=any(a in sys.argv[1:] for a in("--dataset-only","--text-only","--build-dataset"))
|
| 974 |
+
use_experimental=any(a in sys.argv[1:] for a in("--experimental","--exp"))
|
| 975 |
+
calib_file=None
|
| 976 |
+
for arg in sys.argv[1:]:
|
| 977 |
+
if arg.lower().endswith(".gguf"):
|
| 978 |
+
if os.path.exists(arg):BF16=os.path.abspath(arg)
|
| 979 |
+
elif os.path.exists(os.path.join(ROOT,arg)):BF16=os.path.join(ROOT,arg)
|
| 980 |
+
elif arg.lower().endswith(".txt"):
|
| 981 |
+
for cand in(arg,os.path.join(DATA,arg),os.path.join(ROOT,arg)):
|
| 982 |
+
if os.path.exists(cand) and not force_rebuild:
|
| 983 |
+
calib_file=os.path.abspath(cand)
|
| 984 |
+
break
|
| 985 |
+
has_gguf=os.path.exists(BF16)
|
| 986 |
+
if use_experimental or(calib_file and os.path.abspath(calib_file)==os.path.abspath(EXPERIMENTAL_CALIB)):
|
| 987 |
+
if not os.path.exists(EXPERIMENTAL_CALIB) or force_rebuild:
|
| 988 |
+
if force_rebuild and os.path.exists(EXPERIMENTAL_CALIB):
|
| 989 |
+
try:os.remove(EXPERIMENTAL_CALIB)
|
| 990 |
+
except OSError:pass
|
| 991 |
+
step_download()
|
| 992 |
+
step_normalize()
|
| 993 |
+
step_calibrate()
|
| 994 |
+
if not calib_file:
|
| 995 |
+
calib_file=EXPERIMENTAL_CALIB
|
| 996 |
+
else:
|
| 997 |
+
ensure_default_calibration()
|
| 998 |
+
if not calib_file:
|
| 999 |
+
calib_file=DEFAULT_CALIB_PATH
|
| 1000 |
+
if not has_gguf or explicit_dataset_only:
|
| 1001 |
+
elapsed=time.perf_counter()-t0
|
| 1002 |
+
print("\n"+"="*72)
|
| 1003 |
+
print(" ✅ DATASET PREPARATION COMPLETE")
|
| 1004 |
+
print("="*72)
|
| 1005 |
+
print(f" Total time: {elapsed/60:.1f} min")
|
| 1006 |
+
if os.path.exists(DEFAULT_CALIB_PATH):
|
| 1007 |
+
print(f" Standard: {DEFAULT_CALIB_NAME} ({os.path.getsize(DEFAULT_CALIB_PATH)/1024/1024:.2f} MB)")
|
| 1008 |
+
if os.path.exists(EXPERIMENTAL_CALIB):
|
| 1009 |
+
print(f" Expert: experimental.txt ({os.path.getsize(EXPERIMENTAL_CALIB)/1024/1024:.2f} MB)")
|
| 1010 |
+
print("="*72)
|
| 1011 |
+
return
|
| 1012 |
+
step_strip_mtp()
|
| 1013 |
+
step_imatrix(calib_file)
|
| 1014 |
+
elapsed=time.perf_counter()-t0
|
| 1015 |
+
print("\n"+"="*72)
|
| 1016 |
+
print(" ✅ PIPELINE COMPLETE — SUMMARY")
|
| 1017 |
+
print("="*72)
|
| 1018 |
+
print(f" Total time: {elapsed/60:.1f} min")
|
| 1019 |
+
if os.path.exists(calib_file):
|
| 1020 |
+
print(f" Calib source: {os.path.basename(calib_file)} ({os.path.getsize(calib_file)/1024/1024:.2f} MB)")
|
| 1021 |
+
if os.path.exists(IMATRIX):
|
| 1022 |
+
print(f" imatrix.dat: {os.path.getsize(IMATRIX)/1024/1024:.2f} MB")
|
| 1023 |
+
|
| 1024 |
+
if __name__=="__main__":
|
| 1025 |
+
main()
|
01b_BF16-GGUF-modules-fusion.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
r"""01b_BF16-GGUF-modules-fusion.py — Merge multiple GGUF files into one.
|
| 3 |
+
|
| 4 |
+
Reads 2+ GGUF files (e.g. model + mmproj + mtp) via gguf-py, deduplicates
|
| 5 |
+
tensor names and metadata keys (first file wins), and writes a single
|
| 6 |
+
fused GGUF.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python 01b_BF16-GGUF-modules-fusion.py out.gguf main.gguf mmproj.gguf [extra.gguf ...]
|
| 10 |
+
# Or drag-and-drop 2+ .gguf files onto this script:
|
| 11 |
+
# -> writes fused-<first-file>.gguf next to the inputs.
|
| 12 |
+
"""
|
| 13 |
+
import os,re,shutil,sys
|
| 14 |
+
|
| 15 |
+
SCRIPT_DIR=os.path.dirname(os.path.abspath(__file__))
|
| 16 |
+
LLAMA_CPP=os.path.join(SCRIPT_DIR,"llama-cpp")
|
| 17 |
+
sys.path.insert(0,os.path.join(LLAMA_CPP,"gguf-py"))
|
| 18 |
+
import gguf
|
| 19 |
+
|
| 20 |
+
def size_str(path):
|
| 21 |
+
mb=os.path.getsize(path)/(1024*1024)
|
| 22 |
+
return f"{mb/1024:.2f} GB" if mb>=1024 else f"{mb:.0f} MiB"
|
| 23 |
+
|
| 24 |
+
def field_value(field):
|
| 25 |
+
if hasattr(field,"contents"):
|
| 26 |
+
return field.contents()
|
| 27 |
+
t=field.types[0]
|
| 28 |
+
if t==gguf.GGUFValueType.ARRAY:
|
| 29 |
+
sub=field.types[-1]
|
| 30 |
+
return [field.parts[i].tolist()[0] if sub!=gguf.GGUFValueType.STRING else bytes(field.parts[i].tolist()).decode() for i in field.data]
|
| 31 |
+
v=field.parts[-1].tolist()
|
| 32 |
+
if t==gguf.GGUFValueType.STRING:
|
| 33 |
+
return bytes(v).decode("utf-8")
|
| 34 |
+
return v[0] if isinstance(v,list) else v
|
| 35 |
+
|
| 36 |
+
SCALAR_WRITERS={
|
| 37 |
+
gguf.GGUFValueType.UINT8:"add_uint8",gguf.GGUFValueType.INT8:"add_int8",
|
| 38 |
+
gguf.GGUFValueType.UINT16:"add_uint16",gguf.GGUFValueType.INT16:"add_int16",
|
| 39 |
+
gguf.GGUFValueType.UINT32:"add_uint32",gguf.GGUFValueType.INT32:"add_int32",
|
| 40 |
+
gguf.GGUFValueType.FLOAT32:"add_float32",gguf.GGUFValueType.UINT64:"add_uint64",
|
| 41 |
+
gguf.GGUFValueType.INT64:"add_int64",gguf.GGUFValueType.FLOAT64:"add_float64",
|
| 42 |
+
gguf.GGUFValueType.BOOL:"add_bool",gguf.GGUFValueType.STRING:"add_string",
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
def copy_field(writer,key,field):
|
| 46 |
+
t=field.types[0]
|
| 47 |
+
val=field_value(field)
|
| 48 |
+
if t==gguf.GGUFValueType.ARRAY:
|
| 49 |
+
# add_array rejects empty lists — skip rather than abort fusion
|
| 50 |
+
if not val:
|
| 51 |
+
print(f" ! empty array skipped: {key}")
|
| 52 |
+
return
|
| 53 |
+
writer.add_array(key,val)
|
| 54 |
+
else:
|
| 55 |
+
getattr(writer,SCALAR_WRITERS[t])(key,val)
|
| 56 |
+
|
| 57 |
+
def parse_args(argv):
|
| 58 |
+
if len(argv)>=2 and all(p.lower().endswith(".gguf") for p in argv):
|
| 59 |
+
first=os.path.abspath(argv[0])
|
| 60 |
+
out=os.path.join(os.path.dirname(first),"fused-"+os.path.basename(first))
|
| 61 |
+
return out,argv
|
| 62 |
+
if len(argv)>=3:
|
| 63 |
+
return argv[0],argv[1:]
|
| 64 |
+
return None,None
|
| 65 |
+
|
| 66 |
+
def main():
|
| 67 |
+
out_arg,inputs=parse_args(sys.argv[1:])
|
| 68 |
+
if out_arg is None:
|
| 69 |
+
print("Usage: python 01b_BF16-GGUF-modules-fusion.py <out.gguf> <main.gguf> <extra.gguf> [...]")
|
| 70 |
+
print(" or: drag-and-drop 2+ .gguf files onto this script")
|
| 71 |
+
return 1
|
| 72 |
+
out_path=os.path.abspath(out_arg)
|
| 73 |
+
in_paths=[os.path.abspath(p) for p in inputs]
|
| 74 |
+
for p in in_paths:
|
| 75 |
+
if not os.path.isfile(p):
|
| 76 |
+
print(f"Error: file not found: {p}")
|
| 77 |
+
return 1
|
| 78 |
+
if out_path in in_paths:
|
| 79 |
+
print("Error: output must differ from inputs.")
|
| 80 |
+
return 1
|
| 81 |
+
print("="*64)
|
| 82 |
+
print(" GGUF fusion")
|
| 83 |
+
print("="*64)
|
| 84 |
+
for p in in_paths:
|
| 85 |
+
print(f" - {os.path.basename(p)} ({size_str(p)})")
|
| 86 |
+
readers=[]
|
| 87 |
+
for p in in_paths:
|
| 88 |
+
r=gguf.GGUFReader(p)
|
| 89 |
+
readers.append(r)
|
| 90 |
+
arch_field=r.fields.get("general.architecture")
|
| 91 |
+
print(f" arch={field_value(arch_field) if arch_field else '?'} "
|
| 92 |
+
f"tensors={len(r.tensors)} metadata={len(r.fields)} keys")
|
| 93 |
+
arch_field=readers[0].fields.get("general.architecture")
|
| 94 |
+
arch=field_value(arch_field) if arch_field else "llama"
|
| 95 |
+
for p,r in zip(in_paths,readers):
|
| 96 |
+
af=r.fields.get("general.architecture")
|
| 97 |
+
a=field_value(af) if af else "?"
|
| 98 |
+
if a!=arch:print(f" ! arch mismatch: {os.path.basename(p)} declares '{a}' (fusion base '{arch}')")
|
| 99 |
+
writer=gguf.GGUFWriter(None,arch)
|
| 100 |
+
WRITER_KEYS={"general.architecture","general.alignment"}
|
| 101 |
+
# Derive true layer count from union of blk.N indices to prevent truncating merged heads
|
| 102 |
+
blk_max=-1
|
| 103 |
+
for r in readers:
|
| 104 |
+
for t in r.tensors:
|
| 105 |
+
if(m:=re.match(r"blk\.(\d+)\.",t.name)):
|
| 106 |
+
blk_max=max(blk_max,int(m.group(1)))
|
| 107 |
+
seen_kv,kv_skipped=set(),0
|
| 108 |
+
for r in readers:
|
| 109 |
+
for key,field in r.fields.items():
|
| 110 |
+
if key in seen_kv or key in WRITER_KEYS or key.startswith("GGUF."):
|
| 111 |
+
kv_skipped+=1
|
| 112 |
+
continue
|
| 113 |
+
seen_kv.add(key)
|
| 114 |
+
if key.endswith(".block_count") and blk_max>=0:
|
| 115 |
+
declared=field_value(field)
|
| 116 |
+
if isinstance(declared,int) and declared<blk_max+1:
|
| 117 |
+
print(f" ! block_count {declared} → {blk_max+1} (MTP head merged)")
|
| 118 |
+
writer.add_uint32(key,blk_max+1)
|
| 119 |
+
continue
|
| 120 |
+
copy_field(writer,key,field)
|
| 121 |
+
seen_t,t_skipped=set(),0
|
| 122 |
+
for r in readers:
|
| 123 |
+
for t in r.tensors:
|
| 124 |
+
if t.name in seen_t:
|
| 125 |
+
t_skipped+=1
|
| 126 |
+
print(f" ! duplicate tensor skipped: {t.name}")
|
| 127 |
+
continue
|
| 128 |
+
seen_t.add(t.name)
|
| 129 |
+
writer.add_tensor(t.name,t.data,raw_dtype=t.tensor_type)
|
| 130 |
+
print(f"\n Merging: {len(seen_t)} tensors, {len(seen_kv)} metadata keys")
|
| 131 |
+
if kv_skipped or t_skipped:
|
| 132 |
+
print(f" Skipped duplicates: {kv_skipped} metadata, {t_skipped} tensors")
|
| 133 |
+
writer.write_header_to_file(path=out_path)
|
| 134 |
+
writer.write_kv_data_to_file()
|
| 135 |
+
writer.write_tensors_to_file(progress=True)
|
| 136 |
+
writer.close()
|
| 137 |
+
sidecar=next((os.path.splitext(p)[0]+".provenance.json" for p in in_paths if os.path.isfile(os.path.splitext(p)[0]+".provenance.json")),None)
|
| 138 |
+
if sidecar:shutil.copy2(sidecar,os.path.splitext(out_path)[0]+".provenance.json")
|
| 139 |
+
print(f"\n ✓ {os.path.basename(out_path)} ({size_str(out_path)})")
|
| 140 |
+
print(" Tip: rename to model-<SUF>.gguf for auto-discovery by 01/02, or pass it as an argument.")
|
| 141 |
+
print("="*64)
|
| 142 |
+
return 0
|
| 143 |
+
|
| 144 |
+
if __name__=="__main__":
|
| 145 |
+
raise SystemExit(main())
|
02_BF16-GGUF-to-ASHQ1.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
r"""02_BF16-GGUF-to-ASHQ1.py — Automated ASHQ1 orchestrator.
|
| 3 |
+
|
| 4 |
+
Execution order: modules (mmproj, MTP…) → tiers Mini → Quality → Nano.
|
| 5 |
+
On AutoRound int4 lineage (provenance), Fidelity is skipped by default —
|
| 6 |
+
information ceiling reached at Quality — and modules use Quality
|
| 7 |
+
(override: ASHQ1_INCLUDE_FIDELITY=1).
|
| 8 |
+
The COMPLETE model (including MTP head) is preferred over the no-mtp trunk:
|
| 9 |
+
ASHQ1 keeps the nextn layer at Q8_0 (Q6_K on int4 lineage) and excludes it from the size budget.
|
| 10 |
+
Final naming: model-AutoRound-ASHQ1-Compact-30pc.gguf (AutoRound lineage detected
|
| 11 |
+
via step 00 sidecar), model-ASHQ1-…-NNpc.gguf otherwise; mmproj-ASHQ1-Balanced-NNpc.gguf
|
| 12 |
+
(pct relative to source mmproj).
|
| 13 |
+
Lineage override: ASHQ1_LINEAGE=autoround|plain.
|
| 14 |
+
"""
|
| 15 |
+
import os,re,sys,subprocess
|
| 16 |
+
|
| 17 |
+
SCRIPT_DIR=os.path.dirname(os.path.abspath(__file__))
|
| 18 |
+
sys.path.insert(0,SCRIPT_DIR)
|
| 19 |
+
sys.path.insert(0,os.path.join(SCRIPT_DIR,"llama-cpp","gguf-py"))
|
| 20 |
+
from ASHQ1 import ALL_RATIOS,int4_lineage_of,lineage_tag,tier_label
|
| 21 |
+
|
| 22 |
+
MAIN_TIERS=["mini","compact","quality","fidelity"]
|
| 23 |
+
# Longest running and least used: executed last
|
| 24 |
+
LAST_TIER="nano"
|
| 25 |
+
MODULE_PROFILES={"mtp":"fidelity","dspark":"fidelity"}
|
| 26 |
+
INT4_MODULE_PROFILES={"mtp":"quality","dspark":"quality"}
|
| 27 |
+
|
| 28 |
+
def find_file_case_insensitive(directory,filename):
|
| 29 |
+
for f in os.listdir(directory):
|
| 30 |
+
if f.lower()==filename.lower():return os.path.join(directory,f)
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
def _file_mib(path):
|
| 34 |
+
return os.path.getsize(path)/1024/1024
|
| 35 |
+
|
| 36 |
+
def _pick_main(base_dir):
|
| 37 |
+
for name in("model-BF16.gguf","model-F16.gguf","model-no-mtp-BF16.gguf","model-BF16-no-mtp.gguf","model-no-mtp-F16.gguf","model-F16-no-mtp.gguf"):
|
| 38 |
+
hit=find_file_case_insensitive(base_dir,name)
|
| 39 |
+
if hit:return hit
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def _gguf_has_nextn(path):
|
| 43 |
+
try:
|
| 44 |
+
import gguf
|
| 45 |
+
return any("nextn" in t.name or "eh_proj" in t.name for t in gguf.GGUFReader(path).tensors)
|
| 46 |
+
except Exception:
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
def _pick_imatrices(base_dir):
|
| 50 |
+
# All .dat/.imatrix files are merged via max reduction (cHunter789 recipe: mixing mradermacher+ubergarm > bartowski alone). imatrix.dat takes precedence; _bench drafts are excluded
|
| 51 |
+
out=[]
|
| 52 |
+
primary=find_file_case_insensitive(base_dir,"imatrix.dat")
|
| 53 |
+
if primary:out.append(primary)
|
| 54 |
+
for f in sorted(os.listdir(base_dir)):
|
| 55 |
+
fl=f.lower()
|
| 56 |
+
if fl.startswith("_bench") or not fl.endswith((".dat",".imatrix")):continue
|
| 57 |
+
p=os.path.join(base_dir,f)
|
| 58 |
+
if p not in out:out.append(p)
|
| 59 |
+
return out
|
| 60 |
+
|
| 61 |
+
def _clean_base(basename):
|
| 62 |
+
clean=basename
|
| 63 |
+
for s in("-no-mtp-BF16.gguf","-no-mtp-F16.gguf","-BF16-no-mtp.gguf","-F16-no-mtp.gguf","-BF16.gguf","-F16.gguf","-bf16.gguf","-f16.gguf",".gguf"):
|
| 64 |
+
if clean.lower().endswith(s.lower()):
|
| 65 |
+
clean=clean[:-len(s)]
|
| 66 |
+
break
|
| 67 |
+
return clean
|
| 68 |
+
|
| 69 |
+
def process_model(base_dir,ashq1_script,imatrix,model_path,tiers_list):
|
| 70 |
+
ok=True
|
| 71 |
+
basename=os.path.basename(model_path)
|
| 72 |
+
clean_base=_clean_base(basename)
|
| 73 |
+
lineage=os.environ.get("ASHQ1_LINEAGE","auto")
|
| 74 |
+
tag=lineage_tag(model_path,lineage)
|
| 75 |
+
print(f"\n{'='*60}")
|
| 76 |
+
print(f" Processing: {basename} [{tag}]")
|
| 77 |
+
print(f"{'='*60}")
|
| 78 |
+
for tier in tiers_list:
|
| 79 |
+
label=tier_label(tier)
|
| 80 |
+
pct=round(ALL_RATIOS.get(tier.lower(),0)*100)
|
| 81 |
+
new_name=f"{clean_base}-{tag}-{label}-{pct}pc.gguf"
|
| 82 |
+
new_file=os.path.join(base_dir,new_name)
|
| 83 |
+
if os.path.isfile(new_file):
|
| 84 |
+
print(f" -> Already exists, skipping: {new_name}")
|
| 85 |
+
continue
|
| 86 |
+
cmd=[sys.executable,ashq1_script,"--model",model_path,"--tier",tier.lower(),"--output-dir",base_dir,"--run","--lineage","autoround" if tag=="AutoRound-ASHQ1" else "plain"]
|
| 87 |
+
for p in imatrix or []:cmd+=["--imatrix",p]
|
| 88 |
+
if subprocess.run(cmd,cwd=base_dir).returncode or not os.path.isfile(new_file):
|
| 89 |
+
print(f" -> Tier {label} failed")
|
| 90 |
+
ok=False
|
| 91 |
+
continue
|
| 92 |
+
print(f" -> Ready: {new_name}")
|
| 93 |
+
return ok
|
| 94 |
+
|
| 95 |
+
def process_optional_modules(base_dir,ashq1_script,imatrix,main_basename,main_has_nextn,profiles=None):
|
| 96 |
+
if main_has_nextn:
|
| 97 |
+
print("\n [MTP] nextn head already embedded in the main model — separate quantization not needed.")
|
| 98 |
+
return
|
| 99 |
+
for opt_name,profile in(profiles or MODULE_PROFILES).items():
|
| 100 |
+
opt_file=None
|
| 101 |
+
for f in sorted(os.listdir(base_dir)):
|
| 102 |
+
fl=f.lower()
|
| 103 |
+
if fl==main_basename or "ashq1" in fl:continue
|
| 104 |
+
if opt_name in fl and "no-mtp" not in fl and ("bf16" in fl or "f16" in fl) and fl.endswith(".gguf"):
|
| 105 |
+
opt_file=os.path.join(base_dir,f)
|
| 106 |
+
break
|
| 107 |
+
if opt_file:process_model(base_dir,ashq1_script,imatrix,opt_file,[profile])
|
| 108 |
+
|
| 109 |
+
def process_mmproj(base_dir):
|
| 110 |
+
src=None
|
| 111 |
+
for name in("mmproj-BF16.gguf","mmproj-F16.gguf","mmproj-bf16.gguf","mmproj-f16.gguf"):
|
| 112 |
+
hit=find_file_case_insensitive(base_dir,name)
|
| 113 |
+
if hit:src=hit;break
|
| 114 |
+
if not src:
|
| 115 |
+
cands=[]
|
| 116 |
+
for f in os.listdir(base_dir):
|
| 117 |
+
fl=f.lower()
|
| 118 |
+
if "mmproj" not in fl or not fl.endswith(".gguf"):continue
|
| 119 |
+
if "ashq1" in fl or "-to-" in fl:continue
|
| 120 |
+
cands.append(f)
|
| 121 |
+
if cands:
|
| 122 |
+
cands.sort()
|
| 123 |
+
src=os.path.join(base_dir,cands[0])
|
| 124 |
+
if not src:return False
|
| 125 |
+
f=os.path.basename(src)
|
| 126 |
+
script=os.path.join(base_dir,"ASHQ1-mmproj.py")
|
| 127 |
+
if not os.path.isfile(script):script=os.path.join(base_dir,"ASHQ1-MMPROJ.py")
|
| 128 |
+
if not os.path.isfile(script):
|
| 129 |
+
print(f"\n [INFO] ASHQ1-mmproj.py not found — skipping {f}.")
|
| 130 |
+
return False
|
| 131 |
+
src_mib=_file_mib(src)
|
| 132 |
+
dry=subprocess.run([sys.executable,script,"--model",src,"--profile","balanced","--dry-run"],capture_output=True,text=True,cwd=base_dir)
|
| 133 |
+
m=re.search(r"(?:Target|Cible)\s*(?:\(tensors\)|\(tenseurs\))?\s*:\s*([0-9.]+)\s*MiB",dry.stdout,re.I)
|
| 134 |
+
dst_mib=float(m.group(1)) if m else src_mib*0.72
|
| 135 |
+
pct=round(dst_mib/src_mib*100) if src_mib>0 else 100
|
| 136 |
+
# The vision tower stays outside the AutoRound grid (quant_nontext_module=False): its tag follows its own sidecar, never the global text model override
|
| 137 |
+
tag=lineage_tag(src,"auto")
|
| 138 |
+
# Final naming template: mmproj-ASHQ1-Balanced-72pc.gguf (percentage relative to source mmproj)
|
| 139 |
+
out=os.path.join(base_dir,f"mmproj-{tag}-Balanced-{pct}pc.gguf")
|
| 140 |
+
if os.path.isfile(out):
|
| 141 |
+
print(f"\n -> Already exists, skipping: {os.path.basename(out)}")
|
| 142 |
+
return True
|
| 143 |
+
print(f"\n{'='*60}")
|
| 144 |
+
print(f" Processing mmproj: {f} [{tag}]")
|
| 145 |
+
print(f"{'='*60}")
|
| 146 |
+
if subprocess.run([sys.executable,script,"--model",src,"--profile","balanced","--output",out],cwd=base_dir).returncode==0:
|
| 147 |
+
print(f" -> mmproj quantized: {os.path.basename(out)}")
|
| 148 |
+
return True
|
| 149 |
+
return False
|
| 150 |
+
|
| 151 |
+
def main():
|
| 152 |
+
base_dir=SCRIPT_DIR
|
| 153 |
+
ashq1_script=os.path.join(base_dir,"ASHQ1.py")
|
| 154 |
+
if not os.path.isfile(ashq1_script):
|
| 155 |
+
print("Error: ASHQ1.py not found in the current directory.")
|
| 156 |
+
sys.exit(1)
|
| 157 |
+
if len(sys.argv)>1 and sys.argv[1].lower().endswith(".gguf"):
|
| 158 |
+
main_model=os.path.abspath(sys.argv[1])
|
| 159 |
+
else:
|
| 160 |
+
main_model=_pick_main(base_dir)
|
| 161 |
+
has_main_model=main_model and os.path.isfile(main_model)
|
| 162 |
+
if has_main_model:
|
| 163 |
+
from ASHQ1 import _find_binary
|
| 164 |
+
if not _find_binary("llama-quantize"):
|
| 165 |
+
print("Error: llama-quantize binary not found.")
|
| 166 |
+
sys.exit(1)
|
| 167 |
+
imatrix=_pick_imatrices(base_dir)
|
| 168 |
+
if has_main_model and not imatrix:
|
| 169 |
+
print("No imatrix (.dat) found. Run 01_create-calibration-dataset-and-imatrix.py first.")
|
| 170 |
+
sys.exit(1)
|
| 171 |
+
if len(imatrix)>1:
|
| 172 |
+
print(f" ℹ {len(imatrix)} imatrix file(s) merged (max method): {', '.join(os.path.basename(p) for p in imatrix)}")
|
| 173 |
+
|
| 174 |
+
int4_lineage=int4_lineage_of(main_model) if has_main_model else False
|
| 175 |
+
if int4_lineage and os.environ.get("ASHQ1_INCLUDE_FIDELITY")!="1":
|
| 176 |
+
main_tiers=[t for t in MAIN_TIERS if t!="fidelity"]
|
| 177 |
+
module_profiles=INT4_MODULE_PROFILES
|
| 178 |
+
print("\n ℹ AutoRound int4 lineage detected — Fidelity skipped (information ceiling reached), modules set to Quality. Set ASHQ1_INCLUDE_FIDELITY=1 to override.")
|
| 179 |
+
else:
|
| 180 |
+
main_tiers=MAIN_TIERS
|
| 181 |
+
module_profiles=MODULE_PROFILES
|
| 182 |
+
# 1) Modules first: mmproj, then standalone MTP/dspark heads
|
| 183 |
+
mmproj_processed=process_mmproj(base_dir)
|
| 184 |
+
main_has_nextn=_gguf_has_nextn(main_model) if has_main_model else False
|
| 185 |
+
success=True
|
| 186 |
+
if has_main_model:
|
| 187 |
+
process_optional_modules(base_dir,ashq1_script,imatrix,os.path.basename(main_model).lower(),main_has_nextn,module_profiles)
|
| 188 |
+
if not process_model(base_dir,ashq1_script,imatrix,main_model,main_tiers):success=False
|
| 189 |
+
if not process_model(base_dir,ashq1_script,imatrix,main_model,[LAST_TIER]):success=False
|
| 190 |
+
|
| 191 |
+
print("\n"+"="*60)
|
| 192 |
+
if has_main_model:
|
| 193 |
+
print(" ASHQ1 pipeline completed "+("successfully!" if success else "with ERRORS (see tier logs above).")+" "+("(exit 2)" if not success else ""))
|
| 194 |
+
if success:
|
| 195 |
+
print(" ℹ Recommended runtime settings: --cache-type-k q4_0 --cache-type-v q4_0 maximizes context length efficiency (cHunter789 recipe)")
|
| 196 |
+
if not success:sys.exit(2)
|
| 197 |
+
elif mmproj_processed:
|
| 198 |
+
print(" ASHQ1 mmproj generated.")
|
| 199 |
+
else:
|
| 200 |
+
print(" No model or mmproj found in current folder.")
|
| 201 |
+
print("="*60)
|
| 202 |
+
sys.exit(1)
|
| 203 |
+
print("="*60)
|
| 204 |
+
|
| 205 |
+
if __name__=="__main__":
|
| 206 |
+
main()
|
ASHQ1-mmproj.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""ASHQ1-MMPROJ — Non-uniform quantization for CLIP/mmproj GGUF files."""
|
| 3 |
+
import argparse,os,sys
|
| 4 |
+
import numpy as np
|
| 5 |
+
import gguf
|
| 6 |
+
from gguf.constants import GGMLQuantizationType as QT
|
| 7 |
+
|
| 8 |
+
BLOCK_ALIGN={
|
| 9 |
+
QT.Q4_K:256,QT.Q5_K:256,QT.Q6_K:256,
|
| 10 |
+
QT.Q8_0:32,QT.Q5_0:32,QT.Q4_0:32,
|
| 11 |
+
QT.F16:1,QT.F32:1,QT.BF16:1,
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
QT_BLOCK={
|
| 15 |
+
QT.Q4_K:(256,144),QT.Q5_K:(256,176),QT.Q6_K:(256,210),
|
| 16 |
+
QT.Q8_0:(32,34),QT.Q5_0:(32,22),QT.Q4_0:(32,18),
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
TIER_TO_QT={
|
| 20 |
+
"F32":QT.F32,"F16":QT.F16,"BF16":QT.BF16,
|
| 21 |
+
"Q8_0":QT.Q8_0,"Q6_K":QT.Q6_K,"Q5_K":QT.Q5_K,"Q4_K":QT.Q4_K,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
BPW={
|
| 25 |
+
"F32":32.0,"F16":16.0,"BF16":16.0,
|
| 26 |
+
"Q8_0":8.5,"Q6_K":6.5625,"Q5_K":5.5,"Q4_K":4.5,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
CRITICAL=(
|
| 30 |
+
"patch_embd","position_embd","mm.0","mm.1","mm.2",
|
| 31 |
+
"pre_ln","post_ln","ln1","ln2","ln_q","norm",
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
PROFILES={
|
| 35 |
+
"balanced":{
|
| 36 |
+
"attn_qkv":"Q6_K","attn_out":"Q6_K",
|
| 37 |
+
"ffn_up":"Q6_K","ffn_down":"Q6_K",
|
| 38 |
+
"critical":"F32","default":"Q8_0",
|
| 39 |
+
},
|
| 40 |
+
"compact":{
|
| 41 |
+
"attn_qkv":"Q5_K","attn_out":"Q5_K",
|
| 42 |
+
"ffn_up":"Q5_K","ffn_down":"Q5_K",
|
| 43 |
+
"critical":"F32","default":"Q8_0",
|
| 44 |
+
},
|
| 45 |
+
"fidelity":{
|
| 46 |
+
"attn_qkv":"Q8_0","attn_out":"Q8_0",
|
| 47 |
+
"ffn_up":"Q8_0","ffn_down":"Q8_0",
|
| 48 |
+
"critical":"F32","default":"F16",
|
| 49 |
+
},
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
UPGRADE_CHAIN={"Q4_K":"Q5_K","Q5_K":"Q6_K","Q6_K":"Q8_0","Q8_0":"F16"}
|
| 53 |
+
|
| 54 |
+
FILE_TYPE_NAME={
|
| 55 |
+
"F32":"ALL_F32","F16":"MOSTLY_F16","BF16":"MOSTLY_BF16",
|
| 56 |
+
"Q8_0":"MOSTLY_Q8_0","Q6_K":"MOSTLY_Q6_K","Q5_K":"MOSTLY_Q5_K_M","Q4_K":"MOSTLY_Q4_K_M",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
def is_critical(name:str)->bool:
|
| 60 |
+
low=name.lower()
|
| 61 |
+
return any(c in low for c in CRITICAL)
|
| 62 |
+
|
| 63 |
+
def tensor_role(name:str)->str:
|
| 64 |
+
parts=name.split(".")
|
| 65 |
+
if len(parts)>=4 and parts[0]=="v" and parts[1]=="blk":
|
| 66 |
+
return parts[3]
|
| 67 |
+
return name
|
| 68 |
+
|
| 69 |
+
def block_index(name:str):
|
| 70 |
+
parts=name.split(".")
|
| 71 |
+
if len(parts)>=3 and parts[0]=="v" and parts[1]=="blk":
|
| 72 |
+
try:return int(parts[2])
|
| 73 |
+
except ValueError:return None
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
def row_length(shape)->int:
|
| 77 |
+
return int(shape[0]) if len(shape)>0 else 1
|
| 78 |
+
|
| 79 |
+
def best_quant_fit(shape,tier:str)->str:
|
| 80 |
+
cur=tier
|
| 81 |
+
seen=set()
|
| 82 |
+
while cur not in seen:
|
| 83 |
+
seen.add(cur)
|
| 84 |
+
if fits_alignment(shape,cur):return cur
|
| 85 |
+
nxt=UPGRADE_CHAIN.get(cur)
|
| 86 |
+
if nxt is None:break
|
| 87 |
+
cur=nxt
|
| 88 |
+
return "F16"
|
| 89 |
+
|
| 90 |
+
def fits_alignment(shape,tier:str)->bool:
|
| 91 |
+
qt=TIER_TO_QT[tier]
|
| 92 |
+
align=BLOCK_ALIGN.get(qt,1)
|
| 93 |
+
if align<=1:return True
|
| 94 |
+
return row_length(shape) % align==0
|
| 95 |
+
|
| 96 |
+
def pick_tier(name:str,shape,n_blocks:int,profile:dict,deep_boost:int)->str:
|
| 97 |
+
if is_critical(name):return profile["critical"]
|
| 98 |
+
# Keep 1D vectors in F32 (layer scale, bias)
|
| 99 |
+
if len(shape)<2 or int(np.prod(shape))<4096:return "F32"
|
| 100 |
+
role=tensor_role(name)
|
| 101 |
+
tier=profile.get(role,profile["default"])
|
| 102 |
+
idx=block_index(name)
|
| 103 |
+
if idx is not None and deep_boost>0 and idx>=n_blocks-deep_boost:
|
| 104 |
+
tier=UPGRADE_CHAIN.get(tier,tier)
|
| 105 |
+
tier=best_quant_fit(shape,tier)
|
| 106 |
+
return "F16" if tier=="BF16" else tier
|
| 107 |
+
|
| 108 |
+
def _decode(t)->np.ndarray:
|
| 109 |
+
raw=np.asarray(t.data)
|
| 110 |
+
name=t.tensor_type.name
|
| 111 |
+
nshape=tuple(int(d) for d in reversed(t.shape))
|
| 112 |
+
flat=raw.reshape(-1)
|
| 113 |
+
if name=="BF16":
|
| 114 |
+
u=flat.view(np.uint16).astype(np.uint32)<<16
|
| 115 |
+
return u.view(np.float32).reshape(nshape)
|
| 116 |
+
if name=="F16":
|
| 117 |
+
return flat.view(np.float16).astype(np.float32).reshape(nshape)
|
| 118 |
+
if name=="F32":
|
| 119 |
+
return flat.view(np.float32).reshape(nshape)
|
| 120 |
+
return gguf.quants.dequantize(raw,t.tensor_type).astype(np.float32).reshape(nshape)
|
| 121 |
+
|
| 122 |
+
def quantize_mmproj(src:str,dst:str,profile_name:str,deep_boost:int,dry_run:bool)->bool:
|
| 123 |
+
profile=PROFILES[profile_name]
|
| 124 |
+
reader=gguf.GGUFReader(src)
|
| 125 |
+
arch=None
|
| 126 |
+
for key,field in reader.fields.items():
|
| 127 |
+
if key=="general.architecture":
|
| 128 |
+
arch=str(bytes(field.parts[field.data[0]]),"utf-8")
|
| 129 |
+
break
|
| 130 |
+
if arch!="clip":
|
| 131 |
+
print(f" [WARN] architecture='{arch}' (clip expected)")
|
| 132 |
+
n_blocks=0
|
| 133 |
+
for t in reader.tensors:
|
| 134 |
+
idx=block_index(t.name)
|
| 135 |
+
if idx is not None:n_blocks=max(n_blocks,idx+1)
|
| 136 |
+
print(f" ViT blocks detected: {n_blocks}")
|
| 137 |
+
plan=[]
|
| 138 |
+
src_bits=0.0
|
| 139 |
+
dst_bits=0.0
|
| 140 |
+
for t in reader.tensors:
|
| 141 |
+
shape=list(t.shape)
|
| 142 |
+
n_el=int(np.prod(shape))
|
| 143 |
+
tier=pick_tier(t.name,shape,n_blocks,profile,deep_boost)
|
| 144 |
+
cur_name=t.tensor_type.name
|
| 145 |
+
if cur_name in BPW and BPW[cur_name]<BPW[tier]:
|
| 146 |
+
tier=cur_name
|
| 147 |
+
if cur_name=="BF16" and tier=="BF16":
|
| 148 |
+
tier="F16"
|
| 149 |
+
plan.append((t,tier,n_el))
|
| 150 |
+
src_bits+=n_el*BPW.get(cur_name,32.0)
|
| 151 |
+
dst_bits+=n_el*BPW[tier]
|
| 152 |
+
src_mib=src_bits/8/1024/1024
|
| 153 |
+
dst_mib=dst_bits/8/1024/1024
|
| 154 |
+
by_tier={}
|
| 155 |
+
for _,tier,n_el in plan:
|
| 156 |
+
e=by_tier.setdefault(tier,[0,0.0])
|
| 157 |
+
e[0]+=1
|
| 158 |
+
e[1]+=n_el*BPW[tier]/8/1024/1024
|
| 159 |
+
print("\n Tier distribution:")
|
| 160 |
+
for tier in sorted(by_tier,key=lambda x:-BPW[x]):
|
| 161 |
+
cnt,mib=by_tier[tier]
|
| 162 |
+
print(f" {tier:<5}: {cnt:>4} tensor(s) ({mib:>8.1f} MiB)")
|
| 163 |
+
print(f"\n Source (tensors) : {src_mib:>8.1f} MiB")
|
| 164 |
+
print(f" Target (tensors) : {dst_mib:>8.1f} MiB")
|
| 165 |
+
print(f" Reduction : {(1-dst_mib/src_mib)*100:>7.1f}%")
|
| 166 |
+
heavy=[(t.name,tier,n*BPW[tier]/8/1024/1024,list(t.shape)) for t,tier,n in plan if BPW[tier]>=16.0 and n*BPW[tier]/8/1024/1024>1.0]
|
| 167 |
+
if heavy:
|
| 168 |
+
heavy.sort(key=lambda x:-x[2])
|
| 169 |
+
print(f"\n High-precision tensors kept in 16/32-bit ({len(heavy)}):")
|
| 170 |
+
for name,tier,mib,shape in heavy[:20]:
|
| 171 |
+
print(f" {name[:46]:46s} {tier:<5} {mib:>7.1f} MiB shape={shape}")
|
| 172 |
+
if dry_run:
|
| 173 |
+
print("\n Dry run complete — no file written.")
|
| 174 |
+
return True
|
| 175 |
+
writer=gguf.GGUFWriter(dst,arch or "clip")
|
| 176 |
+
skip={"GGUF.version","GGUF.tensor_count","GGUF.kv_count","general.architecture","general.quantization_version","general.file_type"}
|
| 177 |
+
for key,field in reader.fields.items():
|
| 178 |
+
if key in skip:continue
|
| 179 |
+
try:_copy_field(writer,key,field)
|
| 180 |
+
except Exception as e:print(f" [WARN] KV '{key}' omitted: {e}")
|
| 181 |
+
writer.add_quantization_version(2)
|
| 182 |
+
dominant_tier=max(by_tier,key=lambda t:by_tier[t][1])
|
| 183 |
+
dominant_name=FILE_TYPE_NAME.get(dominant_tier,"MOSTLY_Q8_0")
|
| 184 |
+
writer.add_file_type(getattr(gguf.LlamaFileType,dominant_name))
|
| 185 |
+
for t,tier,_ in plan:
|
| 186 |
+
data=_decode(t)
|
| 187 |
+
qt=TIER_TO_QT[tier]
|
| 188 |
+
if tier=="F32":
|
| 189 |
+
out=np.ascontiguousarray(data.astype(np.float32))
|
| 190 |
+
writer.add_tensor(t.name,out)
|
| 191 |
+
continue
|
| 192 |
+
if tier=="F16":
|
| 193 |
+
out=np.ascontiguousarray(data.astype(np.float16))
|
| 194 |
+
writer.add_tensor(t.name,out)
|
| 195 |
+
continue
|
| 196 |
+
if tier=="BF16":
|
| 197 |
+
out=data.astype(np.float16)
|
| 198 |
+
writer.add_tensor(t.name,out,raw_dtype=QT.F16)
|
| 199 |
+
continue
|
| 200 |
+
try:
|
| 201 |
+
out=gguf.quants.quantize(data.astype(np.float32),qt)
|
| 202 |
+
except NotImplementedError:
|
| 203 |
+
qt=TIER_TO_QT["Q8_0"]
|
| 204 |
+
out=gguf.quants.quantize(data.astype(np.float32),qt)
|
| 205 |
+
blk,bsz=QT_BLOCK[qt]
|
| 206 |
+
nshape=list(data.shape)
|
| 207 |
+
nshape[-1]=(nshape[-1]//blk)*bsz
|
| 208 |
+
writer.add_tensor(t.name,out,raw_dtype=qt,raw_shape=nshape)
|
| 209 |
+
writer.write_header_to_file()
|
| 210 |
+
writer.write_kv_data_to_file()
|
| 211 |
+
writer.write_tensors_to_file()
|
| 212 |
+
writer.close()
|
| 213 |
+
real=os.path.getsize(dst)/1024/1024
|
| 214 |
+
print(f"\n Output: {dst}")
|
| 215 |
+
print(f" Actual size: {real:.1f} MiB")
|
| 216 |
+
drift=real/dst_mib if dst_mib>0 else 0
|
| 217 |
+
if drift>1.05:
|
| 218 |
+
print(f" ⚠ Size divergence ×{drift:.2f}")
|
| 219 |
+
else:
|
| 220 |
+
print(f" ✓ Size matches plan (×{drift:.2f})")
|
| 221 |
+
return True
|
| 222 |
+
|
| 223 |
+
def _copy_field(writer,key:str,field):
|
| 224 |
+
from gguf.constants import GGUFValueType as VT
|
| 225 |
+
vtype=field.types[0]
|
| 226 |
+
if vtype==VT.ARRAY:
|
| 227 |
+
itype=field.types[1]
|
| 228 |
+
if itype==VT.STRING:
|
| 229 |
+
vals=[str(bytes(field.parts[i]),"utf-8") for i in field.data]
|
| 230 |
+
else:
|
| 231 |
+
vals=[field.parts[i].tolist()[0] for i in field.data]
|
| 232 |
+
writer.add_array(key,vals)
|
| 233 |
+
return
|
| 234 |
+
if vtype==VT.STRING:
|
| 235 |
+
writer.add_string(key,str(bytes(field.parts[field.data[0]]),"utf-8"))
|
| 236 |
+
return
|
| 237 |
+
val=field.parts[field.data[0]].tolist()[0]
|
| 238 |
+
emit={
|
| 239 |
+
VT.UINT8:writer.add_uint8,VT.INT8:writer.add_int8,
|
| 240 |
+
VT.UINT16:writer.add_uint16,VT.INT16:writer.add_int16,
|
| 241 |
+
VT.UINT32:writer.add_uint32,VT.INT32:writer.add_int32,
|
| 242 |
+
VT.UINT64:writer.add_uint64,VT.INT64:writer.add_int64,
|
| 243 |
+
VT.FLOAT32:writer.add_float32,VT.FLOAT64:writer.add_float64,
|
| 244 |
+
VT.BOOL:writer.add_bool,
|
| 245 |
+
}
|
| 246 |
+
fn=emit.get(vtype)
|
| 247 |
+
if fn is None:raise ValueError(f"Unhandled value type {vtype}")
|
| 248 |
+
fn(key,val)
|
| 249 |
+
|
| 250 |
+
def main(args_list=None):
|
| 251 |
+
p=argparse.ArgumentParser(description="ASHQ1-MMPROJ — Non-uniform quantization for mmproj/CLIP")
|
| 252 |
+
p.add_argument("--model",required=True,help="Source mmproj GGUF (BF16/F16)")
|
| 253 |
+
p.add_argument("--output",default=None,help="Output path")
|
| 254 |
+
p.add_argument("--profile",choices=list(PROFILES),default="balanced")
|
| 255 |
+
p.add_argument("--deep-boost",type=int,default=3,help="Number of trailing blocks boosted one tier")
|
| 256 |
+
p.add_argument("--dry-run",action="store_true",help="Show quantization plan without writing")
|
| 257 |
+
a=p.parse_args(args_list)
|
| 258 |
+
if not os.path.isfile(a.model):
|
| 259 |
+
print(f"ERROR: file not found: {a.model}")
|
| 260 |
+
return 1
|
| 261 |
+
if not a.output:
|
| 262 |
+
base=os.path.splitext(os.path.basename(a.model))[0]
|
| 263 |
+
mm_suffix="F16" if "-F16" in base or "-f16" in base else "BF16"
|
| 264 |
+
a.output=f"mmproj-{mm_suffix}-to-ASHQ1-{a.profile.capitalize()}.gguf"
|
| 265 |
+
print("=== ASHQ1-MMPROJ ===")
|
| 266 |
+
print(f"Source : {a.model}")
|
| 267 |
+
print(f"Profile : {a.profile} (deep-boost={a.deep_boost})")
|
| 268 |
+
print(f"Output : {a.output}\n")
|
| 269 |
+
ok=quantize_mmproj(a.model,a.output,a.profile,a.deep_boost,a.dry_run)
|
| 270 |
+
return 0 if ok else 1
|
| 271 |
+
|
| 272 |
+
if __name__=="__main__":
|
| 273 |
+
sys.exit(main())
|
ASHQ1.py
ADDED
|
@@ -0,0 +1,1580 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""ASHQ1 — All-in-One Imatrix-Driven Hybrid Quantization Engine."""
|
| 3 |
+
import argparse,heapq,json,math,os,re,subprocess,sys,warnings
|
| 4 |
+
from bisect import bisect_left
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
from dataclasses import dataclass,field
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from typing import Any,Dict,List,Set,Tuple
|
| 9 |
+
import gguf
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 13 |
+
# 1. CONSTANTS & TENSOR CLASSIFICATION HELPERS
|
| 14 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 15 |
+
|
| 16 |
+
GGUF_TYPE_NAMES={
|
| 17 |
+
0:"F32",
|
| 18 |
+
1:"F16",
|
| 19 |
+
2:"Q4_0",3:"Q4_1",6:"Q5_0",7:"Q5_1",
|
| 20 |
+
8:"Q8_0",
|
| 21 |
+
10:"Q2_K",11:"Q3_K",12:"Q4_K",13:"Q5_K",14:"Q6_K",
|
| 22 |
+
15:"Q8_K",16:"IQ2_XXS",
|
| 23 |
+
20:"IQ3_XXS",
|
| 24 |
+
24:"IQ2_XXS",
|
| 25 |
+
26:"IQ2_XS",
|
| 26 |
+
28:"IQ2_S",
|
| 27 |
+
30:"IQ1_S",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
GGUF_TYPE_NAMES_INV={v:k for k,v in GGUF_TYPE_NAMES.items()}
|
| 31 |
+
|
| 32 |
+
# Ordered from lowest to highest precision
|
| 33 |
+
TIER_ORDER=[
|
| 34 |
+
"IQ1_S","IQ2_XXS","IQ2_XS","IQ2_S",
|
| 35 |
+
"IQ3_XXS","Q3_K","IQ3_S",
|
| 36 |
+
"IQ4_XS","IQ4_NL","Q4_K","Q5_K","Q6_K","Q8_0","F16",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
# Exact bits per weight from ggml block structs (ggml_type_sizef * 8)
|
| 40 |
+
TIER_BPW={
|
| 41 |
+
"IQ1_S":1.5625,
|
| 42 |
+
"IQ2_XXS":2.0625,
|
| 43 |
+
"IQ2_XS":2.3125,
|
| 44 |
+
"IQ2_S":2.5,
|
| 45 |
+
"IQ3_XXS":3.0625,
|
| 46 |
+
"Q3_K":3.4375,
|
| 47 |
+
"IQ3_S":3.44,
|
| 48 |
+
"IQ4_XS":4.25,
|
| 49 |
+
"IQ4_NL":4.5,
|
| 50 |
+
"Q4_K":4.5,
|
| 51 |
+
"Q5_K":5.5,
|
| 52 |
+
"Q6_K":6.5625,
|
| 53 |
+
"Q8_0":8.5,
|
| 54 |
+
"F16":16.0,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
OVERHEAD_CACHE="ashq1-overhead.json"
|
| 58 |
+
|
| 59 |
+
def _overhead_cache_path(model_path:str)->str:
|
| 60 |
+
return os.path.join(os.path.dirname(os.path.abspath(model_path)),OVERHEAD_CACHE)
|
| 61 |
+
|
| 62 |
+
def load_overhead_factor(model_path:str)->float:
|
| 63 |
+
p=_overhead_cache_path(model_path)
|
| 64 |
+
if os.path.isfile(p):
|
| 65 |
+
try:
|
| 66 |
+
with open(p,"r",encoding="utf-8") as f:
|
| 67 |
+
v=float(json.load(f).get(os.path.basename(model_path),1.0))
|
| 68 |
+
if 0.9<=v<=1.1:return v
|
| 69 |
+
except Exception:pass
|
| 70 |
+
return 1.0
|
| 71 |
+
|
| 72 |
+
def save_overhead_factor(model_path:str,factor:float):
|
| 73 |
+
if not(0.9<=factor<=1.1):return
|
| 74 |
+
p=_overhead_cache_path(model_path)
|
| 75 |
+
data={}
|
| 76 |
+
if os.path.isfile(p):
|
| 77 |
+
try:
|
| 78 |
+
with open(p,"r",encoding="utf-8") as f:data=json.load(f)
|
| 79 |
+
except Exception:data={}
|
| 80 |
+
data[os.path.basename(model_path)]=round(factor,4)
|
| 81 |
+
try:
|
| 82 |
+
with open(p,"w",encoding="utf-8") as f:json.dump(data,f,indent=2)
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f" [WARN] Overhead cache write failed: {e}")
|
| 85 |
+
|
| 86 |
+
GGUF_OVERHEAD_FACTOR=1.000
|
| 87 |
+
|
| 88 |
+
QUANT_RANK={tier:i for i,tier in enumerate(TIER_ORDER)}
|
| 89 |
+
|
| 90 |
+
# Hard floors per tensor class to guarantee stability across long context
|
| 91 |
+
CLASS_HARD_FLOORS={
|
| 92 |
+
"gate":"Q5_K",
|
| 93 |
+
"attn_proj":"IQ3_S",
|
| 94 |
+
"ffn_gate_up":"IQ2_XXS",
|
| 95 |
+
"ffn_down":"IQ3_S",
|
| 96 |
+
"norms":"F16",
|
| 97 |
+
"ssm_params":"F16",
|
| 98 |
+
# Recurrent memory state requires Q8_0 to prevent state collapse in long contexts
|
| 99 |
+
"gdn_state":"Q8_0",
|
| 100 |
+
"mtp":"Q5_K",
|
| 101 |
+
"embd":"IQ4_XS",
|
| 102 |
+
"shexp":"Q4_K",
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
CLASS_MAX_TIER={
|
| 106 |
+
"gate":"F16",
|
| 107 |
+
"attn_proj":"Q8_0",
|
| 108 |
+
"ffn_gate_up":"F16",
|
| 109 |
+
"ffn_down":"F16",
|
| 110 |
+
"norms":"F16",
|
| 111 |
+
"ssm_params":"F16",
|
| 112 |
+
"gdn_state":"F16",
|
| 113 |
+
"mtp":"F16",
|
| 114 |
+
"embd":"F16",
|
| 115 |
+
"shexp":"F16",
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
CAN_Q3={"ffn_gate","ffn_up","ffn_down","attn_output","ssm_out"}
|
| 119 |
+
ALLOW_LOWER_FLOOR="IQ2_XXS"
|
| 120 |
+
DEFAULT_FLOOR="Q4_K"
|
| 121 |
+
|
| 122 |
+
TIER_FLOORS={
|
| 123 |
+
"nano":{
|
| 124 |
+
"gate":"Q6_K",
|
| 125 |
+
"attn_proj":"IQ3_S",
|
| 126 |
+
"ffn_gate_up":"IQ2_XXS",
|
| 127 |
+
"ffn_down":"IQ2_S",
|
| 128 |
+
"norms":"F16",
|
| 129 |
+
"ssm_params":"F16",
|
| 130 |
+
"gdn_state":"Q8_0",
|
| 131 |
+
"mtp":"IQ4_XS",
|
| 132 |
+
"embd":"IQ4_XS",
|
| 133 |
+
"shexp":"Q4_K",
|
| 134 |
+
"unknown":"IQ3_XXS",
|
| 135 |
+
},
|
| 136 |
+
"mini":{
|
| 137 |
+
"gate":"Q6_K",
|
| 138 |
+
"attn_proj":"IQ4_XS",
|
| 139 |
+
"ffn_gate_up":"IQ3_S",
|
| 140 |
+
"ffn_down":"IQ4_XS",
|
| 141 |
+
"norms":"F16",
|
| 142 |
+
"ssm_params":"F16",
|
| 143 |
+
"gdn_state":"Q8_0",
|
| 144 |
+
"mtp":"Q5_K",
|
| 145 |
+
"embd":"IQ4_XS",
|
| 146 |
+
"shexp":"Q5_K",
|
| 147 |
+
"unknown":"IQ4_XS",
|
| 148 |
+
},
|
| 149 |
+
"compact":{
|
| 150 |
+
"gate":"Q6_K",
|
| 151 |
+
"attn_proj":"IQ4_XS",
|
| 152 |
+
"ffn_gate_up":"IQ3_S",
|
| 153 |
+
"ffn_down":"IQ4_XS",
|
| 154 |
+
"norms":"F16",
|
| 155 |
+
"ssm_params":"F16",
|
| 156 |
+
"gdn_state":"Q8_0",
|
| 157 |
+
"mtp":"Q8_0",
|
| 158 |
+
"embd":"IQ4_XS",
|
| 159 |
+
"shexp":"Q5_K",
|
| 160 |
+
"unknown":"Q4_K",
|
| 161 |
+
},
|
| 162 |
+
"quality":{
|
| 163 |
+
"gate":"Q6_K",
|
| 164 |
+
"attn_proj":"Q4_K",
|
| 165 |
+
"ffn_gate_up":"Q4_K",
|
| 166 |
+
"ffn_down":"Q4_K",
|
| 167 |
+
"norms":"F16",
|
| 168 |
+
"ssm_params":"F16",
|
| 169 |
+
"gdn_state":"Q8_0",
|
| 170 |
+
"mtp":"Q8_0",
|
| 171 |
+
"embd":"Q5_K",
|
| 172 |
+
"shexp":"Q5_K",
|
| 173 |
+
"unknown":"Q4_K",
|
| 174 |
+
},
|
| 175 |
+
"fidelity":{
|
| 176 |
+
"gate":"Q8_0",
|
| 177 |
+
"attn_proj":"Q5_K",
|
| 178 |
+
"ffn_gate_up":"Q5_K",
|
| 179 |
+
"ffn_down":"Q5_K",
|
| 180 |
+
"norms":"F16",
|
| 181 |
+
"ssm_params":"F16",
|
| 182 |
+
"gdn_state":"Q8_0",
|
| 183 |
+
"mtp":"Q8_0",
|
| 184 |
+
"embd":"Q5_K",
|
| 185 |
+
"shexp":"Q6_K",
|
| 186 |
+
"unknown":"Q5_K",
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
# Monotonic tier progression for the speculative draft head
|
| 191 |
+
PROFILE_MTP_TIER={"nano":"Q5_K","mini":"Q6_K","compact":"Q8_0"}
|
| 192 |
+
MTP_DEPLOY_TIER="Q8_0"
|
| 193 |
+
|
| 194 |
+
# Ceilings applied when weights originate from an AutoRound int4 optimization
|
| 195 |
+
INT4_LINEAGE_CAP="Q5_K"
|
| 196 |
+
INT4_MTP_CAP="Q6_K"
|
| 197 |
+
INT4_GDN_CAP="Q8_0"
|
| 198 |
+
INT4_CAP_CLASSES={"attn_proj","ffn_gate_up","ffn_down","shexp"}
|
| 199 |
+
|
| 200 |
+
TENSOR_CLASS={
|
| 201 |
+
"attn_gate":"gate",
|
| 202 |
+
"ssm_alpha":"gdn_state",
|
| 203 |
+
"ssm_beta":"gdn_state",
|
| 204 |
+
"ssm_beta_alpha":"gdn_state",
|
| 205 |
+
"ssm_ba":"gdn_state",
|
| 206 |
+
"attn_q":"attn_proj",
|
| 207 |
+
"attn_k":"attn_proj",
|
| 208 |
+
"attn_v":"attn_proj",
|
| 209 |
+
"attn_qkv":"attn_proj",
|
| 210 |
+
"attn_output":"attn_proj",
|
| 211 |
+
"attn_out":"attn_proj",
|
| 212 |
+
"ffn_gate":"ffn_gate_up",
|
| 213 |
+
"ffn_up":"ffn_gate_up",
|
| 214 |
+
"ffn_down":"ffn_down",
|
| 215 |
+
"ssm_in":"attn_proj",
|
| 216 |
+
"ssm_out":"attn_proj",
|
| 217 |
+
"ssm_d":"ssm_params",
|
| 218 |
+
"ssm_norm":"norms",
|
| 219 |
+
"ssm_conv1d":"norms",
|
| 220 |
+
"router":"norms",
|
| 221 |
+
"ssm_dt":"ssm_params",
|
| 222 |
+
"ssm_a":"ssm_params",
|
| 223 |
+
"nextn":"mtp",
|
| 224 |
+
"ffn_gate_exps":"ffn_gate_up",
|
| 225 |
+
"ffn_up_exps":"ffn_gate_up",
|
| 226 |
+
"ffn_down_exps":"ffn_down",
|
| 227 |
+
"ffn_gate_inp":"norms",
|
| 228 |
+
"vocoder":"norms",
|
| 229 |
+
"decoder_wave":"norms",
|
| 230 |
+
"codec_decoder":"norms",
|
| 231 |
+
"patch_embd":"norms",
|
| 232 |
+
"patch_embedding":"norms",
|
| 233 |
+
"position_embd":"norms",
|
| 234 |
+
"post_ln":"norms",
|
| 235 |
+
"pre_ln":"norms",
|
| 236 |
+
"ln1":"norms",
|
| 237 |
+
"ln2":"norms",
|
| 238 |
+
"ln_q":"norms",
|
| 239 |
+
"mm_input_norm":"norms",
|
| 240 |
+
"mm_soft_emb_norm":"norms",
|
| 241 |
+
"mm_0":"embd",
|
| 242 |
+
"mm_1":"embd",
|
| 243 |
+
"mm_2":"embd",
|
| 244 |
+
"0":"embd",
|
| 245 |
+
"1":"embd",
|
| 246 |
+
"2":"embd",
|
| 247 |
+
"mm_proj":"embd",
|
| 248 |
+
"v_enc_embd":"norms",
|
| 249 |
+
"q_proj":"attn_proj",
|
| 250 |
+
"k_proj":"attn_proj",
|
| 251 |
+
"v_proj":"attn_proj",
|
| 252 |
+
"o_proj":"attn_proj",
|
| 253 |
+
"in_proj_a":"gdn_state",
|
| 254 |
+
"in_proj_b":"gdn_state",
|
| 255 |
+
"in_proj_qkv":"attn_proj",
|
| 256 |
+
"in_proj_z":"attn_proj",
|
| 257 |
+
"out_proj":"attn_proj",
|
| 258 |
+
"gate_proj":"ffn_gate_up",
|
| 259 |
+
"up_proj":"ffn_gate_up",
|
| 260 |
+
"down_proj":"ffn_down",
|
| 261 |
+
"attn_sinks":"norms",
|
| 262 |
+
"ffn_exp_probs_b":"norms",
|
| 263 |
+
"exp_probs_b":"norms",
|
| 264 |
+
"per_layer_token_embd":"embd",
|
| 265 |
+
"ffn_gate_shexp":"shexp",
|
| 266 |
+
"ffn_up_shexp":"shexp",
|
| 267 |
+
"ffn_down_shexp":"shexp",
|
| 268 |
+
"ffn_gate_inp_shexp":"norms",
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
ARCH_FEATURES={
|
| 272 |
+
"qwen35":{
|
| 273 |
+
"has_qkv":True,"has_ssm":True,"has_mtp":True,"has_moe":False,
|
| 274 |
+
"is_qat":False,"prefix":"blk","n_layers":32,
|
| 275 |
+
},
|
| 276 |
+
"mellum2":{
|
| 277 |
+
"has_qkv":False,"has_ssm":False,"has_mtp":False,"has_moe":True,
|
| 278 |
+
"is_qat":False,"prefix":"blk","n_layers":28,
|
| 279 |
+
},
|
| 280 |
+
"gemma4":{
|
| 281 |
+
"has_qkv":False,"has_ssm":False,"has_mtp":False,"has_moe":False,
|
| 282 |
+
"is_qat":True,"prefix":"blk","n_layers":48,
|
| 283 |
+
},
|
| 284 |
+
"dense":{
|
| 285 |
+
"has_qkv":False,"has_ssm":False,"has_mtp":False,"has_moe":False,
|
| 286 |
+
"is_qat":False,"prefix":"blk","n_layers":32,
|
| 287 |
+
},
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
def strip_weight(name:str)->str:
|
| 291 |
+
return name.lstrip(".").removesuffix(".weight").removesuffix(".bias")
|
| 292 |
+
|
| 293 |
+
def get_tensor_type(name:str)->str:
|
| 294 |
+
parts=strip_weight(name).split(".")
|
| 295 |
+
if len(parts)>=3 and parts[0] in("v","a") and parts[1] in("blk","BLK"):
|
| 296 |
+
return parts[3] if len(parts)>=4 else "unknown"
|
| 297 |
+
if len(parts)>=2 and parts[0] in("blk","BLK"):
|
| 298 |
+
return parts[2] if len(parts)>=3 else "unknown"
|
| 299 |
+
if "token_embd" in name:
|
| 300 |
+
return "token_embd"
|
| 301 |
+
if name.startswith("output") and "norm" not in name:
|
| 302 |
+
return "output"
|
| 303 |
+
return name
|
| 304 |
+
|
| 305 |
+
def get_tensor_class(ttype:str)->str:
|
| 306 |
+
if ttype in TENSOR_CLASS:
|
| 307 |
+
return TENSOR_CLASS[ttype]
|
| 308 |
+
if "." in ttype:
|
| 309 |
+
tail=ttype.split(".")[-1]
|
| 310 |
+
if tail in TENSOR_CLASS:
|
| 311 |
+
return TENSOR_CLASS[tail]
|
| 312 |
+
for part in reversed(ttype.split(".")):
|
| 313 |
+
if part in TENSOR_CLASS:
|
| 314 |
+
return TENSOR_CLASS[part]
|
| 315 |
+
if "norm" in ttype or "scale" in ttype or ttype.startswith(("ln","pre_ln","post_ln")):
|
| 316 |
+
return "norms"
|
| 317 |
+
if ttype.startswith("ssm_"):
|
| 318 |
+
return "ssm_params"
|
| 319 |
+
if ttype in("token_embd","output","embed_tokens","lm_head","vision_embedder","audio_embedder"):
|
| 320 |
+
return "embd"
|
| 321 |
+
return "unknown"
|
| 322 |
+
|
| 323 |
+
def is_mtp_tensor(name:str,n_layers:int=32,mtp_layers:set | None=None)->bool:
|
| 324 |
+
if "nextn" in name or name.startswith("mtp."):
|
| 325 |
+
return True
|
| 326 |
+
if mtp_layers is not None:
|
| 327 |
+
return get_layer_number(name) in mtp_layers
|
| 328 |
+
layer=get_layer_number(name)
|
| 329 |
+
return layer is not None and layer>=n_layers
|
| 330 |
+
|
| 331 |
+
def get_layer_number(name:str)->int | None:
|
| 332 |
+
parts=strip_weight(name).split(".")
|
| 333 |
+
if len(parts)>=3 and parts[0] in("v","a") and parts[1] in("blk","BLK"):
|
| 334 |
+
try:return int(parts[2])
|
| 335 |
+
except ValueError:return None
|
| 336 |
+
if len(parts)>=2 and parts[0] in("blk","BLK"):
|
| 337 |
+
try:return int(parts[1])
|
| 338 |
+
except ValueError:return None
|
| 339 |
+
return None
|
| 340 |
+
|
| 341 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 342 |
+
# 2. UTILITY FUNCTIONS
|
| 343 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 344 |
+
|
| 345 |
+
def type_name(type_id:int)->str:
|
| 346 |
+
return GGUF_TYPE_NAMES.get(type_id,f"UNKNOWN({type_id})")
|
| 347 |
+
|
| 348 |
+
def type_id(name:str)->int:
|
| 349 |
+
return GGUF_TYPE_NAMES_INV.get(name,-1)
|
| 350 |
+
|
| 351 |
+
def parse_size_line(line:str)->float | None:
|
| 352 |
+
m=re.search(r"quant size\s*=\s*([0-9.]+)\s*MiB",line)
|
| 353 |
+
if m:return float(m.group(1))
|
| 354 |
+
m=re.search(r"model size\s*=\s*([0-9.]+)\s*MiB",line)
|
| 355 |
+
if m:return float(m.group(1))
|
| 356 |
+
return None
|
| 357 |
+
|
| 358 |
+
def parse_quant_size(output:str)->float | None:
|
| 359 |
+
m=re.search(r"quant size\s*=\s*([0-9.]+)\s*MiB",output)
|
| 360 |
+
if m:return float(m.group(1))
|
| 361 |
+
m=re.search(r"model size\s*=\s*([0-9.]+)\s*MiB",output)
|
| 362 |
+
if m:return float(m.group(1))
|
| 363 |
+
return None
|
| 364 |
+
|
| 365 |
+
def parse_fallback_warnings(output:str)->int:
|
| 366 |
+
return len(re.findall(r"converting to\s+(q[0-9]_[0-9KMS]|iq[0-9])",output))
|
| 367 |
+
|
| 368 |
+
def format_size(mib:float)->str:
|
| 369 |
+
if mib>=1024:return f"{mib/1024:.2f} GB"
|
| 370 |
+
return f"{mib:.0f} MiB"
|
| 371 |
+
|
| 372 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 373 |
+
# 3. MODEL READER
|
| 374 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 375 |
+
|
| 376 |
+
def detect_architecture(tensors:dict)->str:
|
| 377 |
+
names=list(tensors.keys())
|
| 378 |
+
has_ssm=any("ssm_" in n for n in names)
|
| 379 |
+
has_qkv=any("attn_qkv" in n for n in names)
|
| 380 |
+
has_moe=any("exps" in n for n in names)
|
| 381 |
+
has_gemma_specific=any(t in n for t in("layer_output_scale","post_attention_norm","post_ffw_norm") for n in names)
|
| 382 |
+
if has_ssm and has_qkv:return "qwen35"
|
| 383 |
+
if has_moe:return "mellum2"
|
| 384 |
+
if has_gemma_specific:return "gemma4"
|
| 385 |
+
return "dense"
|
| 386 |
+
|
| 387 |
+
def _detect_prefix(tensors:dict)->str:
|
| 388 |
+
for name in tensors:
|
| 389 |
+
if name.startswith("BLK."):return "BLK"
|
| 390 |
+
return "blk"
|
| 391 |
+
|
| 392 |
+
def _estimate_layers(tensors:dict)->int:
|
| 393 |
+
max_layer=-1
|
| 394 |
+
for name in tensors:
|
| 395 |
+
parts=name.split(".")
|
| 396 |
+
if len(parts)>=3 and parts[0] in("v","a") and parts[1] in("blk","BLK"):
|
| 397 |
+
parts=parts[1:]
|
| 398 |
+
if len(parts)>=2 and parts[0] in("blk","BLK"):
|
| 399 |
+
try:
|
| 400 |
+
layer=int(parts[1])
|
| 401 |
+
if layer>max_layer:max_layer=layer
|
| 402 |
+
except ValueError:pass
|
| 403 |
+
return max_layer+1 if max_layer>=0 else 0
|
| 404 |
+
|
| 405 |
+
def read_model(path:str)->dict:
|
| 406 |
+
r=gguf.GGUFReader(path)
|
| 407 |
+
tensors={}
|
| 408 |
+
meta={}
|
| 409 |
+
for k,v in r.fields.items():
|
| 410 |
+
try:
|
| 411 |
+
data=v.data
|
| 412 |
+
if isinstance(data,np.ndarray):data=data.tolist()
|
| 413 |
+
elif isinstance(data,(np.generic,)):data=data.item()
|
| 414 |
+
meta[k]=data
|
| 415 |
+
except Exception:meta[k]=str(v)
|
| 416 |
+
for t in r.tensors:
|
| 417 |
+
shape=list(t.shape)
|
| 418 |
+
name=t.name
|
| 419 |
+
n_elements=int(np.prod(shape))
|
| 420 |
+
tensors[name]={"shape":shape,"n_elements":n_elements,"size_mib":n_elements*2/1024/1024}
|
| 421 |
+
arch=detect_architecture(tensors)
|
| 422 |
+
arch_features=ARCH_FEATURES.get(arch,{}).copy()
|
| 423 |
+
prefix=_detect_prefix(tensors)
|
| 424 |
+
n_layers=_estimate_layers(tensors)
|
| 425 |
+
NEXTN_HINT=("nextn","eh_proj","mtp.fc")
|
| 426 |
+
has_nextn=any(any(h in n for h in NEXTN_HINT) for n in tensors)
|
| 427 |
+
BODY=("attn_qkv","attn_q","attn_k","attn_v","attn_output","ssm_","ffn_","eh_proj","exps")
|
| 428 |
+
last=n_layers-1
|
| 429 |
+
last_names=[n for n in tensors if n.startswith(f"blk.{last}.") or n.startswith(f"BLK.{last}.")]
|
| 430 |
+
last_has_body=any(any(h in n for h in BODY) for n in last_names)
|
| 431 |
+
last_has_nextn=any(any(h in n for h in NEXTN_HINT) for n in last_names)
|
| 432 |
+
has_blk_mtp=bool(last_names) and last_has_body and last_has_nextn
|
| 433 |
+
mtp_layers=[last] if has_blk_mtp else []
|
| 434 |
+
has_mtp=has_nextn and(has_blk_mtp or any("nextn" in n for n in tensors))
|
| 435 |
+
has_moe=any("exps" in n for n in tensors)
|
| 436 |
+
if last_names and not last_has_body:
|
| 437 |
+
print(f" [WARN] blk.{last} contains only {len(last_names)} norm tensor(s) — truncated MTP head ignored")
|
| 438 |
+
n_layers-=1
|
| 439 |
+
elif has_blk_mtp:
|
| 440 |
+
n_layers-=1
|
| 441 |
+
if arch=="mellum2" and arch_features.get("moe_intermediate_size",0)==0:
|
| 442 |
+
arch_features["moe_intermediate_size"]=896
|
| 443 |
+
arch_features["prefix"]=prefix
|
| 444 |
+
if n_layers>0:arch_features["n_layers"]=n_layers
|
| 445 |
+
if has_moe:arch_features["has_moe"]=True
|
| 446 |
+
arch_features["has_mtp"]=has_mtp
|
| 447 |
+
arch_features["mtp_layers"]=mtp_layers
|
| 448 |
+
return{
|
| 449 |
+
"path":path,
|
| 450 |
+
"architecture":arch,
|
| 451 |
+
"features":arch_features,
|
| 452 |
+
"tensors":tensors,
|
| 453 |
+
"n_tensors":len(tensors),
|
| 454 |
+
"meta":meta,
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 458 |
+
# 4. IMATRIX READER
|
| 459 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 460 |
+
|
| 461 |
+
def read_imatrix(path:str)->dict:
|
| 462 |
+
r=gguf.GGUFReader(path)
|
| 463 |
+
raw={}
|
| 464 |
+
meta={}
|
| 465 |
+
for k,v in r.fields.items():
|
| 466 |
+
try:meta[k]=v.data
|
| 467 |
+
except Exception:meta[k]=str(v)
|
| 468 |
+
for t in r.tensors:
|
| 469 |
+
name=t.name
|
| 470 |
+
arr=np.array(t.data,dtype=np.float64)
|
| 471 |
+
if name.endswith(".in_sum2"):
|
| 472 |
+
base=name[:-8]
|
| 473 |
+
if base not in raw:raw[base]={}
|
| 474 |
+
raw[base]["in_sum2"]=arr
|
| 475 |
+
if base+".in_sum" in {t2.name for t2 in r.tensors}:
|
| 476 |
+
for t2 in r.tensors:
|
| 477 |
+
if t2.name==base+".in_sum":
|
| 478 |
+
raw[base]["in_sum"]=np.array(t2.data,dtype=np.float64)
|
| 479 |
+
break
|
| 480 |
+
elif name.endswith(".counts"):
|
| 481 |
+
base=name[:-7]
|
| 482 |
+
if base not in raw:raw[base]={}
|
| 483 |
+
raw[base]["counts"]=float(np.mean(arr))
|
| 484 |
+
result={}
|
| 485 |
+
for base,data in raw.items():
|
| 486 |
+
if "in_sum2" not in data:continue
|
| 487 |
+
arr=data["in_sum2"]
|
| 488 |
+
n=arr.size
|
| 489 |
+
imp_mean=float(np.mean(arr))
|
| 490 |
+
outlier_ratio=None
|
| 491 |
+
if "in_sum" in data and imp_mean>0:
|
| 492 |
+
in_sum_arr=data["in_sum"]
|
| 493 |
+
total_sum=float(np.sum(in_sum_arr))
|
| 494 |
+
if total_sum>0 and n>0:
|
| 495 |
+
total_sum2=float(np.sum(arr**2))
|
| 496 |
+
mean_sq=total_sum2/n
|
| 497 |
+
mean_x=total_sum/n
|
| 498 |
+
if mean_x>0:
|
| 499 |
+
outlier_ratio=math.sqrt(mean_sq /(mean_x**2)-1)
|
| 500 |
+
result[base]={
|
| 501 |
+
"importance_mean":imp_mean,
|
| 502 |
+
"importance_sum":float(np.sum(arr)),
|
| 503 |
+
"importance_max":float(np.max(arr)),
|
| 504 |
+
"importance_min":float(np.min(arr)),
|
| 505 |
+
"n_elements":n,
|
| 506 |
+
"in_sum2_raw":arr,
|
| 507 |
+
"outlier_ratio":outlier_ratio,
|
| 508 |
+
}
|
| 509 |
+
return{
|
| 510 |
+
"path":path,
|
| 511 |
+
"tensors":result,
|
| 512 |
+
"n_tensors":len(result),
|
| 513 |
+
"meta":meta,
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
def combine_imatrix(imatrix_list:List[dict],method:str="max")->dict:
|
| 517 |
+
if not imatrix_list:return{"path":"","tensors":{},"n_tensors":0,"meta":{}}
|
| 518 |
+
if len(imatrix_list)==1:return imatrix_list[0]
|
| 519 |
+
all_names=set()
|
| 520 |
+
for im in imatrix_list:all_names.update(im["tensors"].keys())
|
| 521 |
+
combined_tensors={}
|
| 522 |
+
for name in all_names:
|
| 523 |
+
vals=[]
|
| 524 |
+
in_sum2_raw=None
|
| 525 |
+
n_elements=0
|
| 526 |
+
for im in imatrix_list:
|
| 527 |
+
if name in im["tensors"]:
|
| 528 |
+
t=im["tensors"][name]
|
| 529 |
+
vals.append(t["importance_mean"])
|
| 530 |
+
if in_sum2_raw is None and "in_sum2_raw" in t:in_sum2_raw=t["in_sum2_raw"]
|
| 531 |
+
n_elements=max(n_elements,t["n_elements"])
|
| 532 |
+
if not vals:continue
|
| 533 |
+
if method=="max":imp_mean=max(vals)
|
| 534 |
+
elif method in("mean","weighted_mean"):imp_mean=sum(vals)/len(vals)
|
| 535 |
+
else:imp_mean=max(vals)
|
| 536 |
+
combined_tensors[name]={
|
| 537 |
+
"importance_mean":imp_mean,
|
| 538 |
+
"importance_sum":imp_mean*n_elements,
|
| 539 |
+
"importance_max":max(v.get("importance_max",0) for im in imatrix_list if name in im["tensors"] for v in[im["tensors"][name]]),
|
| 540 |
+
"importance_min":min(v.get("importance_min",float('inf')) for im in imatrix_list if name in im["tensors"] for v in[im["tensors"][name]]),
|
| 541 |
+
"n_elements":n_elements,
|
| 542 |
+
"in_sum2_raw":in_sum2_raw,
|
| 543 |
+
"outlier_ratio":max((t.get("outlier_ratio") for im in imatrix_list if name in im["tensors"] for t in[im["tensors"][name]] if t.get("outlier_ratio") is not None),default=None),
|
| 544 |
+
}
|
| 545 |
+
combined_meta={}
|
| 546 |
+
for im in imatrix_list:
|
| 547 |
+
for k,v in im["meta"].items():
|
| 548 |
+
if k not in combined_meta:combined_meta[k]=v
|
| 549 |
+
return{
|
| 550 |
+
"path":"+".join(im["path"] for im in imatrix_list),
|
| 551 |
+
"tensors":combined_tensors,
|
| 552 |
+
"n_tensors":len(combined_tensors),
|
| 553 |
+
"meta":combined_meta,
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
def detect_tied_groups(imatrix:dict,atol:float=1e-5)->list:
|
| 557 |
+
coarse={}
|
| 558 |
+
no_raw=[]
|
| 559 |
+
for name in sorted(imatrix["tensors"].keys()):
|
| 560 |
+
arr=imatrix["tensors"][name].get("in_sum2_raw")
|
| 561 |
+
if arr is None:
|
| 562 |
+
no_raw.append(name)
|
| 563 |
+
continue
|
| 564 |
+
key=(arr.shape,hash(np.round(arr/atol).astype(np.int64).tobytes()))
|
| 565 |
+
coarse.setdefault(key,[]).append(name)
|
| 566 |
+
buckets={}
|
| 567 |
+
for key,names in coarse.items():
|
| 568 |
+
if len(names)==1:
|
| 569 |
+
buckets[key]=names
|
| 570 |
+
continue
|
| 571 |
+
n=len(names)
|
| 572 |
+
groups=list(range(n))
|
| 573 |
+
def find(x):
|
| 574 |
+
while groups[x]!=x:
|
| 575 |
+
groups[x]=groups[groups[x]]
|
| 576 |
+
x=groups[x]
|
| 577 |
+
return x
|
| 578 |
+
def union(a,b):
|
| 579 |
+
ra,rb=find(a),find(b)
|
| 580 |
+
if ra!=rb:groups[ra]=rb
|
| 581 |
+
arrs=[imatrix["tensors"][n]["in_sum2_raw"] for n in names]
|
| 582 |
+
for i in range(n):
|
| 583 |
+
for j in range(i+1,n):
|
| 584 |
+
a,b=arrs[i],arrs[j]
|
| 585 |
+
if a.shape!=b.shape:continue
|
| 586 |
+
if np.allclose(a,b,rtol=1e-5,atol=1e-6):union(i,j)
|
| 587 |
+
comps=defaultdict(list)
|
| 588 |
+
for i,name in enumerate(names):comps[find(i)].append(name)
|
| 589 |
+
for comp in comps.values():buckets.setdefault(key +(id(comp),),[]).extend(comp)
|
| 590 |
+
tied_groups=list(buckets.values())
|
| 591 |
+
tied_groups.extend([[n] for n in no_raw])
|
| 592 |
+
return tied_groups
|
| 593 |
+
|
| 594 |
+
def _imatrix_type(name:str)->str:
|
| 595 |
+
parts=name.split(".")
|
| 596 |
+
if len(parts)>=3 and parts[0]=="blk":return parts[2]
|
| 597 |
+
return name
|
| 598 |
+
|
| 599 |
+
def build_importance_table(imatrix:dict,model:dict)->dict:
|
| 600 |
+
table={}
|
| 601 |
+
for tname,info in imatrix["tensors"].items():
|
| 602 |
+
ttype=_imatrix_type(tname)
|
| 603 |
+
table[tname]={
|
| 604 |
+
"importance_mean":info["importance_mean"],
|
| 605 |
+
"importance_sum":info["importance_sum"],
|
| 606 |
+
"importance_max":info["importance_max"],
|
| 607 |
+
"importance_min":info["importance_min"],
|
| 608 |
+
"n_elements":info["n_elements"],
|
| 609 |
+
"type":ttype,
|
| 610 |
+
}
|
| 611 |
+
for tname,info in list(table.items()):
|
| 612 |
+
if tname.endswith("."):
|
| 613 |
+
alt=tname.rstrip(".")
|
| 614 |
+
table[alt]=info
|
| 615 |
+
return table
|
| 616 |
+
|
| 617 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 618 |
+
# 5. CLASSIFIER & OPTIMIZER
|
| 619 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 620 |
+
|
| 621 |
+
BITS_IN_MIB=8*1024*1024.0
|
| 622 |
+
|
| 623 |
+
TIER_SIZE_MULTIPLIER={
|
| 624 |
+
tier:(bpw/BITS_IN_MIB)*GGUF_OVERHEAD_FACTOR
|
| 625 |
+
for tier,bpw in TIER_BPW.items()
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
def apply_overhead_factor(factor:float):
|
| 629 |
+
TIER_SIZE_MULTIPLIER.update({t:(b/BITS_IN_MIB)*factor for t,b in TIER_BPW.items()})
|
| 630 |
+
|
| 631 |
+
K_QUANTS={"Q3_K","Q4_K","Q5_K","Q6_K"}
|
| 632 |
+
MOE_PAD_TYPES={"ffn_gate_exps","ffn_up_exps","ffn_down_exps","ffn_down"}
|
| 633 |
+
MSE_BPW=dict(TIER_BPW)
|
| 634 |
+
|
| 635 |
+
def _percentile_rank(values:Dict[str,float])->Dict[str,float]:
|
| 636 |
+
if not values:return{}
|
| 637 |
+
sorted_vals=sorted(values.values())
|
| 638 |
+
n=len(sorted_vals)
|
| 639 |
+
return{name:bisect_left(sorted_vals,raw)/n for name,raw in values.items()}
|
| 640 |
+
|
| 641 |
+
def _normalize_by_class(
|
| 642 |
+
tensor_importance:Dict[str,float],
|
| 643 |
+
importance_table:Dict[str,Any],
|
| 644 |
+
)->Dict[str,float]:
|
| 645 |
+
class_groups:Dict[str,Dict[str,float]]={}
|
| 646 |
+
for name,raw_imp in tensor_importance.items():
|
| 647 |
+
rep_info=importance_table.get(name,{})
|
| 648 |
+
ttype=rep_info.get("type",get_tensor_type(name))
|
| 649 |
+
cls=get_tensor_class(ttype)
|
| 650 |
+
class_groups.setdefault(cls,{})[name]=raw_imp
|
| 651 |
+
normalized={}
|
| 652 |
+
for cls,group_vals in class_groups.items():
|
| 653 |
+
pct=_percentile_rank(group_vals)
|
| 654 |
+
normalized.update(pct)
|
| 655 |
+
for name in tensor_importance:
|
| 656 |
+
if name not in normalized:normalized[name]=0.5
|
| 657 |
+
return normalized
|
| 658 |
+
|
| 659 |
+
@dataclass(order=True,slots=True)
|
| 660 |
+
class TierMove:
|
| 661 |
+
neg_utility:float
|
| 662 |
+
group_id:int=field(compare=False)
|
| 663 |
+
target_tier:str=field(compare=False)
|
| 664 |
+
size_delta:float=field(compare=False)
|
| 665 |
+
is_downgrade:bool=field(compare=False,default=False)
|
| 666 |
+
from_tier:str=field(compare=False,default="")
|
| 667 |
+
|
| 668 |
+
def _tier_index(tier:str)->int:
|
| 669 |
+
if tier not in TIER_ORDER:raise ValueError(f"Unknown tier: {tier}")
|
| 670 |
+
return TIER_ORDER.index(tier)
|
| 671 |
+
|
| 672 |
+
def _tier_at(idx:int)->str:
|
| 673 |
+
if not(0<=idx<len(TIER_ORDER)):raise IndexError(f"Tier index {idx} out of range")
|
| 674 |
+
return TIER_ORDER[idx]
|
| 675 |
+
|
| 676 |
+
def _size_mib(tier:str,n_elements:int)->float:
|
| 677 |
+
if n_elements<=0:return 0.0
|
| 678 |
+
return n_elements*TIER_SIZE_MULTIPLIER.get(tier,0.0)
|
| 679 |
+
|
| 680 |
+
FREE_EMBD_TIER="Q8_0"
|
| 681 |
+
|
| 682 |
+
TIER_BLOCK_ALIGN={
|
| 683 |
+
"IQ1_S":256,"IQ2_XXS":256,"IQ2_XS":256,"IQ2_S":256,
|
| 684 |
+
"IQ3_XXS":256,"Q3_K":256,"IQ3_S":256,
|
| 685 |
+
"IQ4_XS":256,"Q4_K":256,"Q5_K":256,"Q6_K":256,
|
| 686 |
+
"IQ4_NL":32,"Q8_0":32,
|
| 687 |
+
"F16":1,
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
def _row_len(shape)->int:
|
| 691 |
+
return int(shape[0]) if shape else 1
|
| 692 |
+
|
| 693 |
+
def tier_fits(shape,tier:str)->bool:
|
| 694 |
+
return _row_len(shape) % TIER_BLOCK_ALIGN.get(tier,1)==0
|
| 695 |
+
|
| 696 |
+
def highest_fitting_tier(shape,tier:str)->str:
|
| 697 |
+
idx=_tier_index(tier)
|
| 698 |
+
while idx<len(TIER_ORDER):
|
| 699 |
+
if tier_fits(shape,TIER_ORDER[idx]):return TIER_ORDER[idx]
|
| 700 |
+
idx+=1
|
| 701 |
+
return "F16"
|
| 702 |
+
|
| 703 |
+
def _free_embd_assignments(model:dict,all_names:set,enabled:bool,embd_cap:str | None=None)->dict:
|
| 704 |
+
# Host CPU token embeddings live outside VRAM allocations unless tied to lm_head
|
| 705 |
+
if not enabled:return {}
|
| 706 |
+
types={get_tensor_type(n):n for n in all_names}
|
| 707 |
+
tok,out=types.get("token_embd"),types.get("output")
|
| 708 |
+
if not(tok and out):return {}
|
| 709 |
+
tier=FREE_EMBD_TIER
|
| 710 |
+
if embd_cap and _tier_index(embd_cap)<_tier_index(tier):tier=embd_cap
|
| 711 |
+
return {tok:tier}
|
| 712 |
+
|
| 713 |
+
def embd_int4_cap(model_path:str)->str | None:
|
| 714 |
+
globals_=provenance(model_path).get("int4_globals") or []
|
| 715 |
+
return INT4_LINEAGE_CAP if any("embed" in g or "token_embd" in g for g in globals_) else None
|
| 716 |
+
|
| 717 |
+
def _pinned_f32_names(model:dict)->set:
|
| 718 |
+
# Keep 1-D tensors and rows unaligned with ggml block boundaries in F32
|
| 719 |
+
out=set()
|
| 720 |
+
for n,info in model.get("tensors",{}).items():
|
| 721 |
+
sh=[int(d) for d in info.get("shape",[])]
|
| 722 |
+
if len([d for d in sh if d>1])<2 or _row_len(sh)%32!=0:
|
| 723 |
+
out.add(n)
|
| 724 |
+
return out
|
| 725 |
+
|
| 726 |
+
def _shape_map(model:dict)->dict:
|
| 727 |
+
return {n:[int(d) for d in i.get("shape",[])] for n,i in model.get("tensors",{}).items()}
|
| 728 |
+
|
| 729 |
+
def _clamp_to_alignment(assignments:dict,shape_map:dict)->int:
|
| 730 |
+
fixed=0
|
| 731 |
+
for name,tier in list(assignments.items()):
|
| 732 |
+
sh=shape_map.get(name)
|
| 733 |
+
if not sh or tier_fits(sh,tier):continue
|
| 734 |
+
assignments[name]=highest_fitting_tier(sh,tier)
|
| 735 |
+
fixed+=1
|
| 736 |
+
return fixed
|
| 737 |
+
|
| 738 |
+
# Relative efficiency bonus for learned codebook representations
|
| 739 |
+
FAMILY_EFF_BITS={
|
| 740 |
+
"IQ4_XS":0.45,"IQ4_NL":0.30,
|
| 741 |
+
"IQ3_S":0.40,"IQ3_XXS":0.35,
|
| 742 |
+
"IQ2_S":0.30,"IQ2_XS":0.25,"IQ2_XXS":0.20,
|
| 743 |
+
"IQ1_S":0.15,
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
def _eff_bpw(tier:str)->float:
|
| 747 |
+
return MSE_BPW[tier]+FAMILY_EFF_BITS.get(tier,0.0)
|
| 748 |
+
|
| 749 |
+
@lru_cache(maxsize=256)
|
| 750 |
+
def _mse_delta(cur_tier:str,next_tier:str)->float:
|
| 751 |
+
return(2**(-2*_eff_bpw(cur_tier))) -(2**(-2*_eff_bpw(next_tier)))
|
| 752 |
+
|
| 753 |
+
def _group_size(group_registry:Dict[int,Tuple[List[str],int,int]],group_id:int,tier:str)->float:
|
| 754 |
+
g_names,g_elements,g_elements_padded=group_registry[group_id]
|
| 755 |
+
elements=g_elements_padded if tier in K_QUANTS else g_elements
|
| 756 |
+
return _size_mib(tier,elements)
|
| 757 |
+
|
| 758 |
+
def _push_upgrade(group_id:int,group_registry:Dict[int,Tuple[List[str],int,int]],assignments:Dict[str,str],tensor_importance:Dict[str,float],upgrade_queue:List[TierMove],importance_table:Dict[str,Any],cap_table:dict | None=None):
|
| 759 |
+
g_names,_,_=group_registry[group_id]
|
| 760 |
+
rep_name=g_names[0]
|
| 761 |
+
cur_tier=assignments[rep_name]
|
| 762 |
+
cur_idx=_tier_index(cur_tier)
|
| 763 |
+
rep_info=importance_table.get(rep_name,{})
|
| 764 |
+
ttype=rep_info["type"] if "type" in rep_info else get_tensor_type(rep_name)
|
| 765 |
+
cls=get_tensor_class(ttype)
|
| 766 |
+
max_tier=CLASS_MAX_TIER.get(cls,"Q8_0")
|
| 767 |
+
if cap_table and cls in cap_table and _tier_index(cap_table[cls])<_tier_index(max_tier):
|
| 768 |
+
max_tier=cap_table[cls]
|
| 769 |
+
if cur_idx>=_tier_index(max_tier) or cur_idx>=len(TIER_ORDER)-1:
|
| 770 |
+
return
|
| 771 |
+
next_idx=cur_idx+1
|
| 772 |
+
max_idx=min(_tier_index(max_tier),len(TIER_ORDER)-1)
|
| 773 |
+
next_tier,cost_delta,quality_delta=None,0.0,0.0
|
| 774 |
+
while next_idx<=max_idx:
|
| 775 |
+
next_tier=_tier_at(next_idx)
|
| 776 |
+
cost_delta=_group_size(group_registry,group_id,next_tier)-_group_size(group_registry,group_id,cur_tier)
|
| 777 |
+
quality_delta=_mse_delta(cur_tier,next_tier)
|
| 778 |
+
if quality_delta>0:break
|
| 779 |
+
next_idx+=1
|
| 780 |
+
if next_tier is None or next_idx>max_idx or quality_delta<=0 or cost_delta<0:
|
| 781 |
+
return
|
| 782 |
+
if cost_delta==0:
|
| 783 |
+
utility_per_mb=float('inf')
|
| 784 |
+
else:
|
| 785 |
+
total_g_imp=sum(tensor_importance.get(n,0) for n in g_names)
|
| 786 |
+
utility_per_mb=(total_g_imp*quality_delta)/cost_delta
|
| 787 |
+
heapq.heappush(upgrade_queue,TierMove(-utility_per_mb,group_id,next_tier,cost_delta,from_tier=cur_tier))
|
| 788 |
+
|
| 789 |
+
def _push_downgrade(group_id:int,group_registry:Dict[int,Tuple[List[str],int,int]],assignments:Dict[str,str],tensor_importance:Dict[str,float],downgrade_queue:List[TierMove],importance_table:Dict[str,Any],floor_table:Dict[str,str]):
|
| 790 |
+
g_names,_,_=group_registry[group_id]
|
| 791 |
+
rep_name=g_names[0]
|
| 792 |
+
cur_tier=assignments[rep_name]
|
| 793 |
+
cur_idx=_tier_index(cur_tier)
|
| 794 |
+
rep_info=importance_table.get(rep_name,{})
|
| 795 |
+
ttype=rep_info["type"] if "type" in rep_info else get_tensor_type(rep_name)
|
| 796 |
+
cls=get_tensor_class(ttype)
|
| 797 |
+
floor=floor_table.get(cls,"Q4_K")
|
| 798 |
+
if cur_idx<=_tier_index(floor) or cur_idx<=0:
|
| 799 |
+
return
|
| 800 |
+
next_tier=_tier_at(cur_idx-1)
|
| 801 |
+
saved=_group_size(group_registry,group_id,cur_tier)-_group_size(group_registry,group_id,next_tier)
|
| 802 |
+
quality_loss=_mse_delta(next_tier,cur_tier)
|
| 803 |
+
if saved<=0 or quality_loss<=0:
|
| 804 |
+
return
|
| 805 |
+
total_g_imp=sum(tensor_importance.get(n,0) for n in g_names)
|
| 806 |
+
loss_per_mb=(total_g_imp*quality_loss)/saved
|
| 807 |
+
heapq.heappush(downgrade_queue,TierMove(loss_per_mb,group_id,next_tier,-saved,True,from_tier=cur_tier))
|
| 808 |
+
|
| 809 |
+
def converge_to_target(importance_table:dict,tied_groups:list,model:dict,target_size_mib:float,allow_q3:bool=False,tolerance_pct:float=2.0,max_iterations:int=5,profile:str="quality",int4_lineage:bool=False,free_embd:bool=True,embd_cap:str | None=None)->Tuple[dict,dict]:
|
| 810 |
+
effective_target=target_size_mib
|
| 811 |
+
ne_map={k:v["n_elements"] for k,v in model.get("tensors",{}).items()}
|
| 812 |
+
if not ne_map:
|
| 813 |
+
ne_map={k:v["n_elements"] for k,v in importance_table.items()}
|
| 814 |
+
f32_names=_pinned_f32_names(model)
|
| 815 |
+
best=None
|
| 816 |
+
best_diff=float("inf")
|
| 817 |
+
seen_targets=set()
|
| 818 |
+
for iteration in range(max(max_iterations,1)):
|
| 819 |
+
assignments,padded_ne_map=optimal_classify(importance_table,tied_groups,model,target_size_mib=effective_target,allow_q3=allow_q3,profile=profile,int4_lineage=int4_lineage,free_embd=free_embd,embd_cap=embd_cap)
|
| 820 |
+
current_size=compute_stats(assignments,ne_map,padded_ne_map,f32_names)["total_mib"]
|
| 821 |
+
diff_pct=abs(current_size-target_size_mib)/target_size_mib*100
|
| 822 |
+
if diff_pct<best_diff:
|
| 823 |
+
best_diff=diff_pct
|
| 824 |
+
best=(dict(assignments),dict(padded_ne_map),current_size)
|
| 825 |
+
if diff_pct<=tolerance_pct:
|
| 826 |
+
print(f" [converge] iter {iteration+1}: {current_size:.0f} MiB (diff {diff_pct:.2f}%) — within tolerance {tolerance_pct}%")
|
| 827 |
+
return assignments,padded_ne_map
|
| 828 |
+
print(f" [converge] iter {iteration+1}: {current_size:.0f} MiB (diff {diff_pct:.2f}%) — adjusting effective budget…")
|
| 829 |
+
if current_size<=0:break
|
| 830 |
+
correction=target_size_mib/current_size
|
| 831 |
+
effective_target=min(max(effective_target*correction,target_size_mib*0.95),target_size_mib*1.05)
|
| 832 |
+
key=round(effective_target,1)
|
| 833 |
+
if key in seen_targets:
|
| 834 |
+
print(" [converge] Effective budget stationary — keeping best result")
|
| 835 |
+
break
|
| 836 |
+
seen_targets.add(key)
|
| 837 |
+
print(f" [converge] Selected best result: {best[2]:.0f} MiB (diff {best_diff:.2f}%)")
|
| 838 |
+
return best[0],best[1]
|
| 839 |
+
|
| 840 |
+
def compute_mtp_assignments_check(non_mtp_names,mtp_names,importance_table,allow_q3,is_qat,profile,int4_lineage):
|
| 841 |
+
has_im=any("importance_mean" in importance_table.get(n,{}) for n in mtp_names)
|
| 842 |
+
mtp_tier=PROFILE_MTP_TIER.get(profile,MTP_DEPLOY_TIER) if has_im else CLASS_HARD_FLOORS["mtp"]
|
| 843 |
+
if int4_lineage and _tier_index(mtp_tier)>_tier_index(INT4_MTP_CAP):
|
| 844 |
+
mtp_tier=INT4_MTP_CAP
|
| 845 |
+
assignments={n:mtp_tier for n in mtp_names}
|
| 846 |
+
floors=TIER_FLOORS.get(profile,TIER_FLOORS["quality"])
|
| 847 |
+
for name in non_mtp_names:
|
| 848 |
+
rep_info=importance_table.get(name,{})
|
| 849 |
+
has_im="importance_mean" in rep_info
|
| 850 |
+
ttype=rep_info.get("type",get_tensor_type(name))
|
| 851 |
+
cls=get_tensor_class(ttype)
|
| 852 |
+
tier=floors.get(cls,floors.get("unknown",DEFAULT_FLOOR))
|
| 853 |
+
hard_floor=CLASS_HARD_FLOORS.get(cls)
|
| 854 |
+
if hard_floor and _tier_index(tier)<_tier_index(hard_floor) and not(allow_q3 and cls in CAN_Q3):
|
| 855 |
+
tier=hard_floor
|
| 856 |
+
if is_qat and cls not in("norms","ssm_params","gdn_state"):
|
| 857 |
+
cap="Q4_K" if cls=="attn_proj" else "IQ4_XS"
|
| 858 |
+
if _tier_index(cap)<_tier_index(tier):tier=cap
|
| 859 |
+
if int4_lineage and cls in INT4_CAP_CLASSES and _tier_index(tier)>_tier_index(INT4_LINEAGE_CAP):
|
| 860 |
+
tier=INT4_LINEAGE_CAP
|
| 861 |
+
if allow_q3 and cls in CAN_Q3 and has_im:
|
| 862 |
+
tier=ALLOW_LOWER_FLOOR
|
| 863 |
+
assignments[name]=tier
|
| 864 |
+
return assignments
|
| 865 |
+
|
| 866 |
+
def compute_initial_assignments(non_mtp_names:Set[str],mtp_names:Set[str],importance_table:Dict,allow_q3:bool,is_qat:bool=False,profile:str="quality",int4_lineage:bool=False)->Dict[str,str]:
|
| 867 |
+
return compute_mtp_assignments_check(non_mtp_names,mtp_names,importance_table,allow_q3,is_qat,profile,int4_lineage)
|
| 868 |
+
|
| 869 |
+
def _tie_family(name:str,importance_table:Dict)->str:
|
| 870 |
+
ttype=importance_table.get(name,{}).get("type",get_tensor_type(name))
|
| 871 |
+
if ttype in("ffn_gate","ffn_up"):return "ffn_gate_up"
|
| 872 |
+
return ttype
|
| 873 |
+
|
| 874 |
+
def build_groups(tied_groups:List[List[str]],non_mtp_names:Set[str],ne_map:Dict[str,int],padded_ne_map:Dict[str,int],importance_table:Dict)->Dict[int,Tuple[List[str],int,int]]:
|
| 875 |
+
group_registry={}
|
| 876 |
+
assigned_tensors=set()
|
| 877 |
+
next_group_id=0
|
| 878 |
+
for tied_group in tied_groups:
|
| 879 |
+
families={}
|
| 880 |
+
for name in tied_group:
|
| 881 |
+
if name in non_mtp_names:
|
| 882 |
+
families.setdefault(_tie_family(name,importance_table),[]).append(name)
|
| 883 |
+
for names in families.values():
|
| 884 |
+
group_registry[next_group_id]=(names,sum(ne_map.get(name,0) for name in names),sum(padded_ne_map.get(name,0) for name in names))
|
| 885 |
+
assigned_tensors.update(names)
|
| 886 |
+
next_group_id+=1
|
| 887 |
+
for name in sorted(non_mtp_names-assigned_tensors):
|
| 888 |
+
group_registry[next_group_id]=([name],ne_map.get(name,0),padded_ne_map.get(name,0))
|
| 889 |
+
next_group_id+=1
|
| 890 |
+
return group_registry
|
| 891 |
+
|
| 892 |
+
def optimal_classify(importance_table:dict,tied_groups:list,model:dict,target_size_mib:float,allow_q3:bool=False,profile:str="quality",int4_lineage:bool=False,free_embd:bool=True,embd_cap:str | None=None)->Tuple[dict,dict]:
|
| 893 |
+
if target_size_mib<=0:raise ValueError("target_size_mib must be positive")
|
| 894 |
+
cap_table=None
|
| 895 |
+
if int4_lineage:
|
| 896 |
+
cap_table={cls:INT4_LINEAGE_CAP for cls in INT4_CAP_CLASSES}
|
| 897 |
+
cap_table["gdn_state"]=INT4_GDN_CAP
|
| 898 |
+
features=model.get("features",{})
|
| 899 |
+
has_mtp=features.get("has_mtp",False)
|
| 900 |
+
n_layers=features.get("n_layers",31)
|
| 901 |
+
is_qat=features.get("is_qat",False)
|
| 902 |
+
model_tensors=model.get("tensors",{})
|
| 903 |
+
ne_map={k:v["n_elements"] for k,v in model_tensors.items()}
|
| 904 |
+
if not model_tensors:
|
| 905 |
+
for tname,info in importance_table.items():
|
| 906 |
+
ne_map[tname]=info["n_elements"]
|
| 907 |
+
moe_d_ff=features.get("moe_intermediate_size",0)
|
| 908 |
+
padded_ne_map=dict(ne_map)
|
| 909 |
+
if moe_d_ff>0 and moe_d_ff % 256!=0:
|
| 910 |
+
aligned_d_ff=((moe_d_ff+255) // 256)*256
|
| 911 |
+
for name,n_el in ne_map.items():
|
| 912 |
+
ttype=importance_table.get(name,{}).get("type",get_tensor_type(name))
|
| 913 |
+
if ttype in MOE_PAD_TYPES:
|
| 914 |
+
padded_ne_map[name]=(n_el // moe_d_ff)*aligned_d_ff
|
| 915 |
+
all_names=set(ne_map.keys())
|
| 916 |
+
mtp_layer_set=set(features.get("mtp_layers",[]))
|
| 917 |
+
mtp_names={n for n in all_names if is_mtp_tensor(n,n_layers,mtp_layer_set)} if has_mtp else set()
|
| 918 |
+
f32_names=_pinned_f32_names(model)
|
| 919 |
+
free_map=_free_embd_assignments(model,all_names,free_embd,embd_cap)
|
| 920 |
+
non_mtp_names=all_names-mtp_names-f32_names-set(free_map)
|
| 921 |
+
tensor_importance={}
|
| 922 |
+
for name in non_mtp_names:
|
| 923 |
+
info=importance_table.get(name,{})
|
| 924 |
+
tensor_importance[name]=info.get("importance_mean",0.0)
|
| 925 |
+
tensor_importance=_normalize_by_class(tensor_importance,importance_table)
|
| 926 |
+
for name in tensor_importance:
|
| 927 |
+
tensor_importance[name]*=max(ne_map.get(name,1),1)
|
| 928 |
+
assignments=compute_mtp_assignments_check(non_mtp_names,mtp_names,importance_table,allow_q3,is_qat,profile,int4_lineage)
|
| 929 |
+
assignments.update(free_map)
|
| 930 |
+
group_registry=build_groups(tied_groups,non_mtp_names,ne_map,padded_ne_map,importance_table)
|
| 931 |
+
profile_floors=TIER_FLOORS.get(profile,{})
|
| 932 |
+
floor_table={}
|
| 933 |
+
for cls in set(list(CLASS_HARD_FLOORS)+list(profile_floors)):
|
| 934 |
+
soft=profile_floors.get(cls,CLASS_HARD_FLOORS.get(cls,"Q4_K"))
|
| 935 |
+
hard=CLASS_HARD_FLOORS.get(cls,"IQ1_S")
|
| 936 |
+
floor_table[cls]=soft if _tier_index(soft)>=_tier_index(hard) else hard
|
| 937 |
+
if profile in("mini","compact","quality"):
|
| 938 |
+
if _tier_index(floor_table.get("embd","Q4_K"))>_tier_index("IQ4_XS"):
|
| 939 |
+
floor_table["embd"]="IQ4_XS"
|
| 940 |
+
mtp_cost=sum(_size_mib(assignments[n],ne_map.get(n,0)) for n in mtp_names)
|
| 941 |
+
pinned_cost=sum(ne_map.get(n,0)*32.0/BITS_IN_MIB for n in f32_names)
|
| 942 |
+
effective_target=target_size_mib-mtp_cost-pinned_cost
|
| 943 |
+
current_size=sum(
|
| 944 |
+
_size_mib(assignments[n],padded_ne_map.get(n,ne_map.get(n,0)) if assignments[n] in K_QUANTS else ne_map.get(n,0))
|
| 945 |
+
for n in non_mtp_names
|
| 946 |
+
)
|
| 947 |
+
if current_size>effective_target:
|
| 948 |
+
downgrade_queue=[]
|
| 949 |
+
for g_id in group_registry:
|
| 950 |
+
_push_downgrade(g_id,group_registry,assignments,tensor_importance,downgrade_queue,importance_table,floor_table)
|
| 951 |
+
while downgrade_queue and current_size>effective_target:
|
| 952 |
+
item=heapq.heappop(downgrade_queue)
|
| 953 |
+
if assignments[group_registry[item.group_id][0][0]]!=item.from_tier:
|
| 954 |
+
continue
|
| 955 |
+
for n in group_registry[item.group_id][0]:
|
| 956 |
+
assignments[n]=item.target_tier
|
| 957 |
+
current_size+=item.size_delta
|
| 958 |
+
_push_downgrade(item.group_id,group_registry,assignments,tensor_importance,downgrade_queue,importance_table,floor_table)
|
| 959 |
+
if current_size>effective_target:
|
| 960 |
+
warnings.warn(
|
| 961 |
+
f"Cannot reach target: floors hold size at {current_size:.1f} MiB vs target {effective_target:.1f} MiB. "
|
| 962 |
+
"Use --allow-q3-or-lower or increase --size.",RuntimeWarning)
|
| 963 |
+
has_signal=any("importance_mean" in importance_table.get(n,{}) for n in non_mtp_names)
|
| 964 |
+
upgrade_queue=[]
|
| 965 |
+
if has_signal:
|
| 966 |
+
for g_id in group_registry:
|
| 967 |
+
_push_upgrade(g_id,group_registry,assignments,tensor_importance,upgrade_queue,importance_table,cap_table)
|
| 968 |
+
while upgrade_queue:
|
| 969 |
+
item=heapq.heappop(upgrade_queue)
|
| 970 |
+
if assignments[group_registry[item.group_id][0][0]]!=item.from_tier:
|
| 971 |
+
continue
|
| 972 |
+
if item.size_delta>0 and current_size+item.size_delta>effective_target:
|
| 973 |
+
break
|
| 974 |
+
for n in group_registry[item.group_id][0]:
|
| 975 |
+
assignments[n]=item.target_tier
|
| 976 |
+
current_size+=item.size_delta
|
| 977 |
+
_push_upgrade(item.group_id,group_registry,assignments,tensor_importance,upgrade_queue,importance_table,cap_table)
|
| 978 |
+
fixed=_clamp_to_alignment(assignments,_shape_map(model))
|
| 979 |
+
if fixed:print(f" ℹ {fixed} tensor(s) promoted to closest aligned tier (ggml block)")
|
| 980 |
+
return assignments,padded_ne_map
|
| 981 |
+
|
| 982 |
+
def compute_stats(assignments:dict,ne_map:dict=None,padded_ne_map:dict=None,f32_names:set=None)->dict:
|
| 983 |
+
stats={"by_tier_count":{},"by_tier_mib":{},"total_mib":0.0,"tensor_count":0}
|
| 984 |
+
for name,tier in assignments.items():
|
| 985 |
+
if not isinstance(tier,str):continue
|
| 986 |
+
stats["tensor_count"]+=1
|
| 987 |
+
stats["by_tier_count"][tier]=stats["by_tier_count"].get(tier,0)+1
|
| 988 |
+
if ne_map:
|
| 989 |
+
if padded_ne_map and tier in K_QUANTS:
|
| 990 |
+
elements=padded_ne_map.get(name,ne_map.get(name,0))
|
| 991 |
+
else:
|
| 992 |
+
elements=ne_map.get(name,0)
|
| 993 |
+
size=_size_mib(tier,elements)
|
| 994 |
+
stats["by_tier_mib"][tier]=stats["by_tier_mib"].get(tier,0.0)+size
|
| 995 |
+
stats["total_mib"]+=size
|
| 996 |
+
if ne_map and f32_names:
|
| 997 |
+
for name in f32_names:
|
| 998 |
+
size=ne_map.get(name,0)*32.0/BITS_IN_MIB
|
| 999 |
+
stats["tensor_count"]+=1
|
| 1000 |
+
stats["by_tier_count"]["F32"]=stats["by_tier_count"].get("F32",0)+1
|
| 1001 |
+
stats["by_tier_mib"]["F32"]=stats["by_tier_mib"].get("F32",0.0)+size
|
| 1002 |
+
stats["total_mib"]+=size
|
| 1003 |
+
return stats
|
| 1004 |
+
|
| 1005 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1006 |
+
# 6. CONFIG GENERATOR
|
| 1007 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1008 |
+
|
| 1009 |
+
def get_regex_priority(regex:str)->int:
|
| 1010 |
+
score=0
|
| 1011 |
+
if "nextn" in regex:score+=200
|
| 1012 |
+
if re.search(r"(blk|BLK)\\.3[0-2]\\.",regex):score+=100
|
| 1013 |
+
if re.search(r"(blk|BLK)\\.0\\.",regex):score+=90
|
| 1014 |
+
if re.search(r"(blk|BLK)\\.31\\.",regex):score+=80
|
| 1015 |
+
if r"(blk|BLK)\.(" in regex:score+=50
|
| 1016 |
+
elif r"(blk|BLK)\.\d" in regex:score+=30
|
| 1017 |
+
if regex.startswith(".*"):score-=50
|
| 1018 |
+
if regex.endswith(r"\.weight"):score+=10
|
| 1019 |
+
return score
|
| 1020 |
+
|
| 1021 |
+
def _is_contiguous(lst,low,high):
|
| 1022 |
+
if not lst:return False
|
| 1023 |
+
return len(lst)==(high-low+1)
|
| 1024 |
+
|
| 1025 |
+
def _group_ranges(lst):
|
| 1026 |
+
if not lst:return
|
| 1027 |
+
start=lst[0]
|
| 1028 |
+
end=lst[0]
|
| 1029 |
+
for i in range(1,len(lst)):
|
| 1030 |
+
if lst[i]==end+1:
|
| 1031 |
+
end=lst[i]
|
| 1032 |
+
else:
|
| 1033 |
+
yield(start,end)
|
| 1034 |
+
start=end=lst[i]
|
| 1035 |
+
yield(start,end)
|
| 1036 |
+
|
| 1037 |
+
def _range_to_regex(start:int,end:int)->str:
|
| 1038 |
+
if start==end:return str(start)
|
| 1039 |
+
if end<=9:return f"[{start}-{end}]"
|
| 1040 |
+
alt="|".join(str(i) for i in range(start,end+1))
|
| 1041 |
+
return f"(?:{alt})"
|
| 1042 |
+
|
| 1043 |
+
def generate_flags(assignments:dict,model:dict,base_type:str,target_size_mib:float=None)->dict:
|
| 1044 |
+
is_qat=model.get("features",{}).get("is_qat",False)
|
| 1045 |
+
output_type="Q5_K"
|
| 1046 |
+
token_embd_type="Q4_K" if is_qat else "Q5_K"
|
| 1047 |
+
for tname,tier in assignments.items():
|
| 1048 |
+
ttype=get_tensor_type(tname)
|
| 1049 |
+
if ttype=="output":output_type=tier
|
| 1050 |
+
elif ttype=="token_embd":token_embd_type=tier
|
| 1051 |
+
max_layer=model.get("features",{}).get("n_layers",31)
|
| 1052 |
+
rules=[]
|
| 1053 |
+
type_tier_layers={}
|
| 1054 |
+
max_layer_seen=-1
|
| 1055 |
+
for tname,tier in assignments.items():
|
| 1056 |
+
parts=tname.split(".")
|
| 1057 |
+
tower=""
|
| 1058 |
+
if len(parts)>=4 and parts[0] in("v","a") and parts[1] in("blk","BLK"):
|
| 1059 |
+
tower=parts[0]+"."
|
| 1060 |
+
parts=parts[1:]
|
| 1061 |
+
if len(parts)>=3 and parts[0] in("blk","BLK"):
|
| 1062 |
+
try:layer=int(parts[1])
|
| 1063 |
+
except ValueError:continue
|
| 1064 |
+
ttype=parts[2]
|
| 1065 |
+
key=(tower,ttype,tier)
|
| 1066 |
+
if key not in type_tier_layers:type_tier_layers[key]=[]
|
| 1067 |
+
type_tier_layers[key].append(layer)
|
| 1068 |
+
max_layer_seen=max(max_layer_seen,layer)
|
| 1069 |
+
max_layer=max_layer_seen if max_layer_seen>=0 else max_layer
|
| 1070 |
+
for(tower,ttype,tier),layers in sorted(type_tier_layers.items(),key=lambda x:-QUANT_RANK.get(x[0][2],0)):
|
| 1071 |
+
layers=sorted(set(layers))
|
| 1072 |
+
if len(layers)>=8 and _is_contiguous(layers,0,max_layer):
|
| 1073 |
+
pattern=f"{tower}(blk|BLK)\\.\\d+\\.{ttype}\\.weight={tier}"
|
| 1074 |
+
else:
|
| 1075 |
+
parts=[]
|
| 1076 |
+
for start,end in _group_ranges(layers):
|
| 1077 |
+
if start==end:parts.append(str(start))
|
| 1078 |
+
else:parts.append(_range_to_regex(start,end))
|
| 1079 |
+
desc="|".join(parts)
|
| 1080 |
+
pattern=f"{tower}(blk|BLK)\\.({desc})\\.{ttype}\\.weight={tier}"
|
| 1081 |
+
prio=get_regex_priority(pattern) +(10 if tier=="Q8_0" else 5 if tier=="Q6_K" else 0) +(5 if len(layers)==1 else 0) +(3 if "ffn_down" in ttype else 0)
|
| 1082 |
+
rules.append((pattern,prio))
|
| 1083 |
+
prefix=model.get("features",{}).get("prefix","blk")
|
| 1084 |
+
for tname,tier in sorted(assignments.items(),key=lambda kv:-QUANT_RANK.get(kv[1],0)):
|
| 1085 |
+
parts=tname.split(".")
|
| 1086 |
+
if len(parts)>=2 and parts[0].lower()==prefix.lower():continue
|
| 1087 |
+
if len(parts)>=4 and parts[0] in("v","a") and parts[1] in("blk","BLK"):continue
|
| 1088 |
+
ttype=get_tensor_type(tname)
|
| 1089 |
+
if ttype==tname:ttype=tname.removesuffix(".weight").removesuffix(".bias")
|
| 1090 |
+
if ttype in("token_embd","output"):continue
|
| 1091 |
+
ttype=re.sub(r"\.(?:weight|bias)\.\d+$","",ttype)
|
| 1092 |
+
body=f".*{re.escape(ttype)}\\.weight"
|
| 1093 |
+
pattern=f"{body}={tier}"
|
| 1094 |
+
prio=get_regex_priority(pattern) +(5 if tier=="Q8_0" else 0)
|
| 1095 |
+
if not any(p.rsplit("=",1)[0]==body for p,_ in rules):rules.append((pattern,prio))
|
| 1096 |
+
rules.sort(key=lambda x:-x[1])
|
| 1097 |
+
has_output=any(get_tensor_type(t)=="output" for t in assignments)
|
| 1098 |
+
has_embd=any(get_tensor_type(t)=="token_embd" for t in assignments)
|
| 1099 |
+
flags={
|
| 1100 |
+
"imatrix":None,
|
| 1101 |
+
"output_tensor_type":output_type if has_output else None,
|
| 1102 |
+
"token_embedding_type":token_embd_type if has_embd else None,
|
| 1103 |
+
"tensor_type_rules":[r[0] for r in rules],
|
| 1104 |
+
"base_type":base_type,
|
| 1105 |
+
"target_size_mib":target_size_mib,
|
| 1106 |
+
}
|
| 1107 |
+
return flags
|
| 1108 |
+
|
| 1109 |
+
def format_flags(flags:dict)->str:
|
| 1110 |
+
lines=[]
|
| 1111 |
+
if flags.get("output_tensor_type"):lines.append(" --output-tensor-type "+flags["output_tensor_type"])
|
| 1112 |
+
if flags.get("token_embedding_type"):lines.append(" --token-embedding-type "+flags["token_embedding_type"])
|
| 1113 |
+
for pattern in flags["tensor_type_rules"]:lines.append(f' --tensor-type "{pattern}"')
|
| 1114 |
+
return "\n".join(lines)
|
| 1115 |
+
|
| 1116 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1117 |
+
# 7. QUANTIZER EXECUTOR
|
| 1118 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1119 |
+
|
| 1120 |
+
def _find_binary(name:str)->str:
|
| 1121 |
+
exe=name +(".exe" if sys.platform=="win32" else "")
|
| 1122 |
+
env_var=f"LLAMA_{name.upper().replace('-','_')}_PATH"
|
| 1123 |
+
env_path=os.environ.get(env_var)
|
| 1124 |
+
if env_path:
|
| 1125 |
+
if os.path.isfile(env_path):return env_path
|
| 1126 |
+
cand=os.path.join(env_path,exe)
|
| 1127 |
+
if os.path.isfile(cand):return cand
|
| 1128 |
+
script_dir=os.path.dirname(os.path.abspath(__file__))
|
| 1129 |
+
roots=[]
|
| 1130 |
+
env_root=os.environ.get("LLAMA_CPP_DIR")
|
| 1131 |
+
if env_root:roots.append(env_root)
|
| 1132 |
+
for base in(script_dir,os.getcwd()):
|
| 1133 |
+
roots.append(base)
|
| 1134 |
+
roots.append(os.path.normpath(os.path.join(base,"llama-cpp")))
|
| 1135 |
+
roots.append(os.path.normpath(os.path.join(base,"..","llama-cpp")))
|
| 1136 |
+
roots.append(os.path.normpath(os.path.join(base,"..","..","llama-cpp")))
|
| 1137 |
+
subdirs=("","bin",os.path.join("build","bin"),os.path.join("build","bin","Release"),os.path.join("build","Release"),"build")
|
| 1138 |
+
seen=set()
|
| 1139 |
+
for root in roots:
|
| 1140 |
+
for sub in subdirs:
|
| 1141 |
+
p=os.path.normpath(os.path.join(root,sub,exe))
|
| 1142 |
+
if p in seen:continue
|
| 1143 |
+
seen.add(p)
|
| 1144 |
+
if os.path.isfile(p):return p
|
| 1145 |
+
from shutil import which
|
| 1146 |
+
found=which(name) or which(exe)
|
| 1147 |
+
if found:return found
|
| 1148 |
+
return ""
|
| 1149 |
+
|
| 1150 |
+
def _build_cmd(flags:dict,model_in:str,model_out:str,dry_run:bool=False)->list:
|
| 1151 |
+
cmd=[_find_binary("llama-quantize")]
|
| 1152 |
+
if dry_run:cmd.append("--dry-run")
|
| 1153 |
+
if flags.get("imatrix"):
|
| 1154 |
+
imatrix=flags["imatrix"]
|
| 1155 |
+
if isinstance(imatrix,list):imatrix=imatrix[0]
|
| 1156 |
+
cmd.extend(["--imatrix",imatrix])
|
| 1157 |
+
if flags.get("output_tensor_type"):cmd.extend(["--output-tensor-type",flags["output_tensor_type"]])
|
| 1158 |
+
if flags.get("token_embedding_type"):cmd.extend(["--token-embedding-type",flags["token_embedding_type"]])
|
| 1159 |
+
for pattern in flags["tensor_type_rules"]:cmd.extend(["--tensor-type",pattern])
|
| 1160 |
+
cmd.append(model_in)
|
| 1161 |
+
cmd.append(model_out)
|
| 1162 |
+
cmd.append(flags["base_type"])
|
| 1163 |
+
return cmd
|
| 1164 |
+
|
| 1165 |
+
def _missing_binary_msg(name:str):
|
| 1166 |
+
exe=name +(".exe" if sys.platform=="win32" else "")
|
| 1167 |
+
print(f" ⚠ Binary '{exe}' not found.")
|
| 1168 |
+
print(f" Set LLAMA_CPP_DIR to your llama.cpp folder,")
|
| 1169 |
+
print(f" or set LLAMA_{name.upper().replace('-','_')}_PATH to the exact path,")
|
| 1170 |
+
print(f" or place '{exe}' next to this script.")
|
| 1171 |
+
|
| 1172 |
+
def run_dry_run(flags:dict,model_in:str)->float | None:
|
| 1173 |
+
binary=_find_binary("llama-quantize")
|
| 1174 |
+
if not binary:
|
| 1175 |
+
_missing_binary_msg("llama-quantize")
|
| 1176 |
+
return None
|
| 1177 |
+
cmd=_build_cmd(flags,model_in,os.devnull,dry_run=True)
|
| 1178 |
+
try:result=subprocess.run(cmd,capture_output=True,text=True,timeout=600)
|
| 1179 |
+
except FileNotFoundError:
|
| 1180 |
+
_missing_binary_msg("llama-quantize")
|
| 1181 |
+
return None
|
| 1182 |
+
except subprocess.TimeoutExpired:
|
| 1183 |
+
print(" ⚠ Dry run timed out after 600 s")
|
| 1184 |
+
return None
|
| 1185 |
+
output=(result.stdout or "") +(result.stderr or "")
|
| 1186 |
+
size=parse_quant_size(output)
|
| 1187 |
+
if size is not None:return size
|
| 1188 |
+
m=re.search(r"unsupported model architecture:'([^']+)'",output)
|
| 1189 |
+
if m:
|
| 1190 |
+
print(f" ⚠ llama-quantize rejected architecture '{m.group(1)}' — quantization aborted.")
|
| 1191 |
+
return None
|
| 1192 |
+
print("STDERR:",(result.stderr or "")[:2000])
|
| 1193 |
+
return None
|
| 1194 |
+
|
| 1195 |
+
def run_quantization(flags:dict,model_in:str,model_out:str)->bool:
|
| 1196 |
+
binary=_find_binary("llama-quantize")
|
| 1197 |
+
if not binary:
|
| 1198 |
+
_missing_binary_msg("llama-quantize")
|
| 1199 |
+
return False
|
| 1200 |
+
cmd=_build_cmd(flags,model_in,model_out)
|
| 1201 |
+
print("Running:"," ".join(cmd[:6])+" ...")
|
| 1202 |
+
try:result=subprocess.run(cmd)
|
| 1203 |
+
except FileNotFoundError:
|
| 1204 |
+
_missing_binary_msg("llama-quantize")
|
| 1205 |
+
return False
|
| 1206 |
+
success=result.returncode==0
|
| 1207 |
+
if success and os.path.isfile(model_out):
|
| 1208 |
+
size_mib=os.path.getsize(model_out)/1024/1024
|
| 1209 |
+
print(f"Done: {model_out} ({size_mib:.0f} MiB)")
|
| 1210 |
+
return success
|
| 1211 |
+
|
| 1212 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1213 |
+
# 8. MAIN SHQ ENGINE (SINGLE TARGET RUNNER)
|
| 1214 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1215 |
+
|
| 1216 |
+
def _get_base_type(model:dict,profile:str="quality")->str:
|
| 1217 |
+
if model.get("features",{}).get("is_qat",False):return "IQ4_XS"
|
| 1218 |
+
return{
|
| 1219 |
+
"nano":"IQ3_XXS","mini":"IQ4_XS","compact":"IQ4_XS","quality":"Q5_K_M",
|
| 1220 |
+
"fidelity":"Q6_K",
|
| 1221 |
+
}.get(profile,"Q5_K_M")
|
| 1222 |
+
|
| 1223 |
+
def run_main_cli(args_list=None)->int:
|
| 1224 |
+
parser=argparse.ArgumentParser(description="ASHQ1: AutoRound-infused imatrix hybrid quantization")
|
| 1225 |
+
parser.add_argument("--model",help="BF16 GGUF model path")
|
| 1226 |
+
parser.add_argument("--imatrix",action="append",default=[],help="Imatrix GGUF path (can be specified multiple times)")
|
| 1227 |
+
parser.add_argument("--imatrix-method",choices=["max","mean"],default="max",help="How to combine multiple imatrices: max or mean")
|
| 1228 |
+
parser.add_argument("--size",type=float,default=6800,help="Target file size in MiB")
|
| 1229 |
+
parser.add_argument("--profile",choices=["nano","mini","compact","quality","fidelity"],default="quality",help="Quantization profile")
|
| 1230 |
+
parser.add_argument("--lineage",choices=["auto","autoround","plain"],default="auto",help="Force output name lineage tag")
|
| 1231 |
+
parser.add_argument("--output",default=None,help="Output GGUF path")
|
| 1232 |
+
parser.add_argument("--run",action="store_true",help="Execute quantization")
|
| 1233 |
+
parser.add_argument("--show-config",action="store_true",help="Print config and exit")
|
| 1234 |
+
parser.add_argument("--verbose",action="store_true",help="Detailed output")
|
| 1235 |
+
parser.add_argument("--allow-q3-or-lower",action="store_true",help="Allow starting tiers down to IQ2_XXS")
|
| 1236 |
+
parser.add_argument("--no-free-embd",action="store_true",help="Keep token_embd inside the size budget (tied lm_head or strict file-size target)")
|
| 1237 |
+
parser.add_argument("--aggro",type=float,default=None,help="[deprecated] Use --size instead")
|
| 1238 |
+
parser.add_argument("--target-size",action="store_true",help="Iteratively converge to --size")
|
| 1239 |
+
parser.add_argument("--show-floors",action="store_true",help="Print class hard floors and exit")
|
| 1240 |
+
try:args=parser.parse_args(args_list)
|
| 1241 |
+
except SystemExit as e:return e.code if isinstance(e.code,int) else 1
|
| 1242 |
+
if args.show_floors:
|
| 1243 |
+
_show_floors()
|
| 1244 |
+
return 0
|
| 1245 |
+
if not args.model:
|
| 1246 |
+
parser.print_usage()
|
| 1247 |
+
print("main: error: --model is required")
|
| 1248 |
+
return 1
|
| 1249 |
+
if not args.imatrix:
|
| 1250 |
+
print("Note: no --imatrix provided, using profile defaults only.")
|
| 1251 |
+
target_mib=args.size
|
| 1252 |
+
print("=== ASHQ1 (AutoRound-infused) ===")
|
| 1253 |
+
print(f"Model: {args.model}")
|
| 1254 |
+
if not args.imatrix:
|
| 1255 |
+
print("Imatrix: none (floor-driven profile)")
|
| 1256 |
+
elif len(args.imatrix)==1:
|
| 1257 |
+
print(f"Imatrix: {args.imatrix[0]}")
|
| 1258 |
+
else:
|
| 1259 |
+
print(f"Imatrix: {len(args.imatrix)} files ({args.imatrix_method})")
|
| 1260 |
+
for p in args.imatrix:print(f" - {p}")
|
| 1261 |
+
print(f"Target: {target_mib:.0f} MiB ({target_mib/1024:.2f} GB)")
|
| 1262 |
+
int4_lineage=int4_lineage_of(args.model)
|
| 1263 |
+
if int4_lineage:
|
| 1264 |
+
print(f" Lineage: int4 AutoRound source — weight upgrades capped at {INT4_LINEAGE_CAP}, MTP at {INT4_MTP_CAP}")
|
| 1265 |
+
_ov=load_overhead_factor(args.model)
|
| 1266 |
+
if abs(_ov-1.0)>1e-4:
|
| 1267 |
+
apply_overhead_factor(_ov)
|
| 1268 |
+
print(f" Calibrated overhead (previous run): ×{_ov:.4f}")
|
| 1269 |
+
if args.allow_q3_or_lower:
|
| 1270 |
+
print(" --allow-q3-or-lower: low-importance tensors may go to Q3_K")
|
| 1271 |
+
print()
|
| 1272 |
+
print("[1/4] Reading model...")
|
| 1273 |
+
model=read_model(args.model)
|
| 1274 |
+
if not args.no_free_embd:
|
| 1275 |
+
_free=_free_embd_assignments(model,set(model.get("tensors",{})),True)
|
| 1276 |
+
_mib=sum(_size_mib(t,model["tensors"].get(n,{}).get("n_elements",0)) for n,t in _free.items())
|
| 1277 |
+
if _mib>max(target_mib*0.15,128):
|
| 1278 |
+
print(f" ⚠ Embedding {_mib:.0f} MiB > 15% of target — disabling free-embd to preserve accurate tier ratios")
|
| 1279 |
+
args.no_free_embd=True
|
| 1280 |
+
no_free=args.no_free_embd
|
| 1281 |
+
print(f" Architecture: {model['architecture']}")
|
| 1282 |
+
print(f" Tensors: {model['n_tensors']}")
|
| 1283 |
+
print(f" Features: {json.dumps(model['features'],indent=2)}")
|
| 1284 |
+
print("\n[2/4] Reading imatrix...")
|
| 1285 |
+
imatrix_list=[read_imatrix(p) for p in args.imatrix]
|
| 1286 |
+
for im in imatrix_list:
|
| 1287 |
+
print(f" {im['path']}: {im['n_tensors']} tensors, datasets={im['meta'].get('imatrix.datasets','?')}")
|
| 1288 |
+
imatrix=combine_imatrix(imatrix_list,method=args.imatrix_method)
|
| 1289 |
+
if imatrix_list:print(f" Combined: {imatrix['n_tensors']} tensors")
|
| 1290 |
+
else:print(" Skipped: profile floors drive every assignment")
|
| 1291 |
+
print("\n[3/4] Detecting tied groups...")
|
| 1292 |
+
imp_table=build_importance_table(imatrix,model)
|
| 1293 |
+
tied_groups=detect_tied_groups(imatrix)
|
| 1294 |
+
print(f" Found {len(tied_groups)} tied groups:")
|
| 1295 |
+
for g in tied_groups:
|
| 1296 |
+
if len(g)<2:continue
|
| 1297 |
+
fam={}
|
| 1298 |
+
for n in g:fam.setdefault(_tie_family(n,imp_table),[]).append(n)
|
| 1299 |
+
for names in fam.values():
|
| 1300 |
+
if len(names)<2:continue
|
| 1301 |
+
print(f" TIED({len(names)}): {names[0].replace('.weight','')} = {names[1].replace('.weight','')}")
|
| 1302 |
+
print("\n[4/4] Classifying tensors (greedy imatrix-driven)...")
|
| 1303 |
+
embd_cap=embd_int4_cap(args.model) if int4_lineage else None
|
| 1304 |
+
if embd_cap:
|
| 1305 |
+
print(f" Embeddings covered by int4 grid → host bump capped at {embd_cap}")
|
| 1306 |
+
if args.target_size:
|
| 1307 |
+
assignments,padded_ne_map=converge_to_target(
|
| 1308 |
+
imp_table,tied_groups,model,
|
| 1309 |
+
target_size_mib=target_mib,
|
| 1310 |
+
allow_q3=args.allow_q3_or_lower,
|
| 1311 |
+
profile=args.profile,
|
| 1312 |
+
int4_lineage=int4_lineage,
|
| 1313 |
+
free_embd=not no_free,
|
| 1314 |
+
embd_cap=embd_cap,
|
| 1315 |
+
)
|
| 1316 |
+
else:
|
| 1317 |
+
assignments,padded_ne_map=optimal_classify(
|
| 1318 |
+
imp_table,tied_groups,model,
|
| 1319 |
+
target_size_mib=target_mib,
|
| 1320 |
+
allow_q3=args.allow_q3_or_lower,
|
| 1321 |
+
profile=args.profile,
|
| 1322 |
+
int4_lineage=int4_lineage,
|
| 1323 |
+
free_embd=not no_free,
|
| 1324 |
+
embd_cap=embd_cap,
|
| 1325 |
+
)
|
| 1326 |
+
ne_map={k:v["n_elements"] for k,v in model.get("tensors",{}).items()}
|
| 1327 |
+
if not ne_map:ne_map={k:v["n_elements"] for k,v in imp_table.items()}
|
| 1328 |
+
free_mib=sum(_size_mib(t,ne_map.get(n,0)) for n,t in _free_embd_assignments(model,set(ne_map),not no_free,embd_cap).items())
|
| 1329 |
+
if free_mib:print(f" Host-side embedding bump: {free_mib:.0f} MiB outside VRAM budget")
|
| 1330 |
+
_show_tier_summary(assignments,imp_table,ne_map,padded_ne_map,_pinned_f32_names(model))
|
| 1331 |
+
base_type=_get_base_type(model,args.profile)
|
| 1332 |
+
flags=generate_flags(assignments,model,base_type,target_mib)
|
| 1333 |
+
flags["imatrix"]=args.imatrix
|
| 1334 |
+
print(f"\nConfig (base={flags['base_type']}):")
|
| 1335 |
+
print(format_flags(flags))
|
| 1336 |
+
if args.show_config:return 0
|
| 1337 |
+
print("\n--- Dry Run ---")
|
| 1338 |
+
estimated=compute_stats(assignments,ne_map,padded_ne_map,_pinned_f32_names(model))["total_mib"]
|
| 1339 |
+
dry_size=run_dry_run(flags,args.model)
|
| 1340 |
+
if dry_size and estimated>0:
|
| 1341 |
+
ratio=dry_size/estimated
|
| 1342 |
+
save_overhead_factor(args.model,_ov*ratio)
|
| 1343 |
+
if abs(ratio-1.0)>0.02:
|
| 1344 |
+
print(f" Calibration: actual/estimated overhead ×{ratio:.4f} — saved for subsequent runs")
|
| 1345 |
+
_show_size_result(dry_size,target_mib+free_mib)
|
| 1346 |
+
if not args.run:
|
| 1347 |
+
print("\nDry run only. Use --run to execute quantization.")
|
| 1348 |
+
return 0
|
| 1349 |
+
if not args.output:
|
| 1350 |
+
base=clean_name(args.model)
|
| 1351 |
+
pct=round(ALL_RATIOS.get(args.profile,0.39)*100)
|
| 1352 |
+
args.output=f"{base}-{lineage_tag(args.model,args.lineage)}-{tier_label(args.profile)}-{pct}pc.gguf"
|
| 1353 |
+
print(f"\n--- Running quantization: {args.output} ---")
|
| 1354 |
+
success=run_quantization(flags,args.model,args.output)
|
| 1355 |
+
if success:
|
| 1356 |
+
print("Done!")
|
| 1357 |
+
return 0
|
| 1358 |
+
else:
|
| 1359 |
+
print("Failed!")
|
| 1360 |
+
return 1
|
| 1361 |
+
|
| 1362 |
+
def _show_tier_summary(assignments,imp_table,ne_map,padded_ne_map=None,f32_names=None):
|
| 1363 |
+
stats=compute_stats(assignments,ne_map,padded_ne_map,f32_names)
|
| 1364 |
+
print("\n Tier distribution:")
|
| 1365 |
+
for tier in sorted(stats["by_tier_count"].keys()):
|
| 1366 |
+
count=stats["by_tier_count"][tier]
|
| 1367 |
+
mib=stats["by_tier_mib"].get(tier,0.0)
|
| 1368 |
+
print(f" {tier}: {count} tensors ({mib:.1f} MiB)")
|
| 1369 |
+
print(f" Total estimated size: {stats['total_mib']:.1f} MiB")
|
| 1370 |
+
unknown=[n for n in assignments if get_tensor_class(get_tensor_type(n))=="unknown"]
|
| 1371 |
+
if unknown:
|
| 1372 |
+
print(f"\n Unclassified tensors (floor-driven, {len(unknown)}) — extend TENSOR_CLASS if sensitive:")
|
| 1373 |
+
for n in unknown[:8]:print(f" {n}")
|
| 1374 |
+
if len(unknown)>8:print(f" … +{len(unknown)-8} more")
|
| 1375 |
+
ranked=sorted([(n,v) for n,v in imp_table.items()],key=lambda x:-x[1]["importance_mean"])
|
| 1376 |
+
print("\n Top 10 by importance:")
|
| 1377 |
+
for n,v in ranked[:10]:
|
| 1378 |
+
tier=assignments.get(n,"base")
|
| 1379 |
+
display=n.replace(".weight","").replace(".bias","")
|
| 1380 |
+
print(f" {display[:52]:52s} imp={v['importance_mean']:10.0f} tier={tier}")
|
| 1381 |
+
|
| 1382 |
+
# Metadata overhead (header + vocab) not tracked by tensor arrays
|
| 1383 |
+
META_OVERHEAD_MIB=12.0
|
| 1384 |
+
|
| 1385 |
+
def _show_size_result(dry_size,target_mib):
|
| 1386 |
+
if dry_size:
|
| 1387 |
+
on_disk=dry_size+META_OVERHEAD_MIB
|
| 1388 |
+
print(f" Estimated size: {dry_size:.0f} MiB ({dry_size/1024:.2f} GB)")
|
| 1389 |
+
print(f" Estimated on disk: {on_disk:.0f} MiB (+{META_OVERHEAD_MIB:.0f} MiB metadata)")
|
| 1390 |
+
diff=on_disk-target_mib
|
| 1391 |
+
if diff>0:print(f" ⚠ Over target by {diff:.0f} MiB")
|
| 1392 |
+
else:print(f" ✓ Under target by {-diff:.0f} MiB")
|
| 1393 |
+
else:
|
| 1394 |
+
print(" ⚠ Could not parse size from dry-run output")
|
| 1395 |
+
|
| 1396 |
+
def _show_floors():
|
| 1397 |
+
print(" Class hard floors (never below without --allow-q3-or-lower):\n")
|
| 1398 |
+
max_n=max(len(c) for c in CLASS_HARD_FLOORS)
|
| 1399 |
+
for cls,floor in sorted(CLASS_HARD_FLOORS.items()):
|
| 1400 |
+
print(f" {cls:<{max_n}} → {floor}")
|
| 1401 |
+
print(f"\n Default floor (unknown class): Q4_K")
|
| 1402 |
+
print(f" --allow-q3-or-lower enables IQ2_XXS start for: {','.join(sorted(CAN_Q3))}")
|
| 1403 |
+
|
| 1404 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1405 |
+
# 9. TIERS RUNNER & CLI ENTRYPOINT
|
| 1406 |
+
# ════════════════════════════════════════════════════���══════════════════════
|
| 1407 |
+
|
| 1408 |
+
TIER_RATIOS={
|
| 1409 |
+
"nano":0.21,
|
| 1410 |
+
"mini":0.27,
|
| 1411 |
+
"compact":0.30,
|
| 1412 |
+
"quality":0.39,
|
| 1413 |
+
"fidelity":0.48,
|
| 1414 |
+
}
|
| 1415 |
+
|
| 1416 |
+
ALL_RATIOS=dict(TIER_RATIOS)
|
| 1417 |
+
TIER_DISPLAY={"nano":"Nano"}
|
| 1418 |
+
|
| 1419 |
+
def tier_label(tier:str)->str:
|
| 1420 |
+
return TIER_DISPLAY.get(tier.lower(),tier.capitalize())
|
| 1421 |
+
|
| 1422 |
+
def file_size_mib(path:str)->float:
|
| 1423 |
+
return os.path.getsize(path)/1024/1024
|
| 1424 |
+
|
| 1425 |
+
def clean_name(path:str)->str:
|
| 1426 |
+
base=os.path.splitext(os.path.basename(path))[0]
|
| 1427 |
+
for suffix in("-no-mtp-BF16","-no-mtp-F16","-BF16-no-mtp","-F16-no-mtp","-BF16","-F16","-bf16","-f16"):
|
| 1428 |
+
if base.endswith(suffix):
|
| 1429 |
+
base=base[:-len(suffix)]
|
| 1430 |
+
break
|
| 1431 |
+
return base
|
| 1432 |
+
|
| 1433 |
+
PROVENANCE_MAP="quant-provenance.json"
|
| 1434 |
+
|
| 1435 |
+
def provenance(model_path:str)->dict:
|
| 1436 |
+
sidecar=os.path.splitext(model_path)[0]+".provenance.json"
|
| 1437 |
+
if os.path.isfile(sidecar):
|
| 1438 |
+
try:
|
| 1439 |
+
with open(sidecar,"r",encoding="utf-8") as f:return json.load(f)
|
| 1440 |
+
except Exception:pass
|
| 1441 |
+
fmap=os.path.join(os.path.dirname(os.path.abspath(model_path)),PROVENANCE_MAP)
|
| 1442 |
+
if os.path.isfile(fmap):
|
| 1443 |
+
try:
|
| 1444 |
+
with open(fmap,"r",encoding="utf-8") as f:return json.load(f).get(os.path.basename(model_path),{})
|
| 1445 |
+
except Exception:pass
|
| 1446 |
+
return {}
|
| 1447 |
+
|
| 1448 |
+
def lineage_tag(model_path:str,override:str="auto")->str:
|
| 1449 |
+
if override=="autoround":return "AutoRound-ASHQ1"
|
| 1450 |
+
if override=="plain":return "ASHQ1"
|
| 1451 |
+
return "AutoRound-ASHQ1" if provenance(model_path).get("autoround") else "ASHQ1"
|
| 1452 |
+
|
| 1453 |
+
def int4_lineage_of(model_path:str)->bool:
|
| 1454 |
+
prov=provenance(model_path)
|
| 1455 |
+
try:bits=int(prov.get("bits",16) or 16)
|
| 1456 |
+
except (TypeError,ValueError):bits=16
|
| 1457 |
+
return bool(prov.get("autoround")) and bits<=4
|
| 1458 |
+
|
| 1459 |
+
def gen_imatrix(model:str,data:str,output:str,chunks:int)->bool:
|
| 1460 |
+
bin_path=_find_binary("llama-imatrix")
|
| 1461 |
+
if not bin_path:
|
| 1462 |
+
_missing_binary_msg("llama-imatrix")
|
| 1463 |
+
return False
|
| 1464 |
+
cmd=[bin_path,"-m",model,"-f",data,"-o",output,"--chunks",str(chunks)]
|
| 1465 |
+
print("Running:"," ".join(cmd[:4])+" ...")
|
| 1466 |
+
try:result=subprocess.run(cmd)
|
| 1467 |
+
except FileNotFoundError:
|
| 1468 |
+
_missing_binary_msg("llama-imatrix")
|
| 1469 |
+
return False
|
| 1470 |
+
return result.returncode==0
|
| 1471 |
+
|
| 1472 |
+
def print_tier_table(bf16_mib:float,model_name:str):
|
| 1473 |
+
print(f"\n Model: {model_name}")
|
| 1474 |
+
print(f" BF16 source: {bf16_mib:.0f} MiB ({bf16_mib/1024:.2f} GB)\n")
|
| 1475 |
+
print(f" {'Tier':<12}{'Ratio':>6}{'Target(MiB)':>14}{'Target(GB)':>12}")
|
| 1476 |
+
print(f" {'-'*12}{'-'*6}{'-'*14}{'-'*12}")
|
| 1477 |
+
for tier,ratio in ALL_RATIOS.items():
|
| 1478 |
+
target=bf16_mib*ratio
|
| 1479 |
+
print(f" {tier_label(tier):<12}{ratio*100:>5.0f}%{target:>13.0f}{target/1024:>11.2f}")
|
| 1480 |
+
print()
|
| 1481 |
+
|
| 1482 |
+
def run_tier(model:str,imatrix_paths:list,tier:str,target_mib:float,output_dir:str,run:bool,extra_args:list,lineage:str="auto")->bool:
|
| 1483 |
+
name=clean_name(model)
|
| 1484 |
+
pct=round(ALL_RATIOS.get(tier,0)*100)
|
| 1485 |
+
output=os.path.join(output_dir,f"{name}-{lineage_tag(model,lineage)}-{tier_label(tier)}-{pct}pc.gguf")
|
| 1486 |
+
cmd_args=[
|
| 1487 |
+
"--model",model,
|
| 1488 |
+
"--size",f"{target_mib:.0f}",
|
| 1489 |
+
"--output",output,
|
| 1490 |
+
"--profile",tier,
|
| 1491 |
+
]
|
| 1492 |
+
for im in imatrix_paths:cmd_args.extend(["--imatrix",im])
|
| 1493 |
+
cmd_args.extend(extra_args)
|
| 1494 |
+
if run:cmd_args.append("--run")
|
| 1495 |
+
print(f"\n{'='*60}")
|
| 1496 |
+
print(f" [{tier.upper()}] Target: {target_mib:.0f} MiB ({target_mib/1024:.2f} GB)")
|
| 1497 |
+
print(f" Output: {output}")
|
| 1498 |
+
print(f"{'='*60}\n")
|
| 1499 |
+
return run_main_cli(cmd_args)==0
|
| 1500 |
+
|
| 1501 |
+
def tiers_main(args_list=None):
|
| 1502 |
+
parser=argparse.ArgumentParser(
|
| 1503 |
+
description="ASHQ1 Tier Runner — standardized quantization tiers",
|
| 1504 |
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
| 1505 |
+
)
|
| 1506 |
+
parser.add_argument("--model",help="BF16/F16 GGUF model path")
|
| 1507 |
+
parser.add_argument("--imatrix",action="append",default=[],help="Imatrix file path")
|
| 1508 |
+
parser.add_argument("--tier",choices=list(TIER_RATIOS.keys())+["all"],default="all",help="Which tier to run")
|
| 1509 |
+
parser.add_argument("--lineage",choices=["auto","autoround","plain"],default="auto",help="Force output name lineage tag")
|
| 1510 |
+
parser.add_argument("--output-dir",default=None,help="Output directory")
|
| 1511 |
+
parser.add_argument("--run",action="store_true",help="Execute quantization")
|
| 1512 |
+
parser.add_argument("--show-sizes",action="store_true",help="Only print size table and exit")
|
| 1513 |
+
parser.add_argument("--allow-q3-or-lower",action="store_true",help="Allow Q3 or lower")
|
| 1514 |
+
parser.add_argument("--imatrix-method",choices=["max","mean"],default="max",help="Imatrix combination method")
|
| 1515 |
+
parser.add_argument("--verbose",action="store_true",help="Detailed output")
|
| 1516 |
+
parser.add_argument("--gen-imatrix",action="store_true",help="Generate imatrix before quantization")
|
| 1517 |
+
parser.add_argument("--data",help="Calibration data file for imatrix")
|
| 1518 |
+
parser.add_argument("--chunks",type=int,default=100,help="Number of chunks for imatrix")
|
| 1519 |
+
args,remaining=parser.parse_known_args(args_list)
|
| 1520 |
+
if not args.model:
|
| 1521 |
+
parser.print_usage()
|
| 1522 |
+
print("ashq1: error: --model is required")
|
| 1523 |
+
sys.exit(1)
|
| 1524 |
+
if not os.path.isfile(args.model):
|
| 1525 |
+
print(f"ERROR: Model not found: {args.model}")
|
| 1526 |
+
sys.exit(1)
|
| 1527 |
+
bf16_mib=file_size_mib(args.model)
|
| 1528 |
+
model_name=clean_name(args.model)
|
| 1529 |
+
output_dir=args.output_dir or os.path.dirname(os.path.abspath(args.model))
|
| 1530 |
+
os.makedirs(output_dir,exist_ok=True)
|
| 1531 |
+
print("=== ASHQ1 Tier Runner ===")
|
| 1532 |
+
print_tier_table(bf16_mib,model_name)
|
| 1533 |
+
if args.show_sizes:return
|
| 1534 |
+
imatrix_paths=list(args.imatrix)
|
| 1535 |
+
if args.gen_imatrix:
|
| 1536 |
+
if not args.data:
|
| 1537 |
+
print("ERROR: --data required for --gen-imatrix")
|
| 1538 |
+
sys.exit(1)
|
| 1539 |
+
imatrix_path=os.path.join(output_dir,f"{model_name}.imatrix.dat")
|
| 1540 |
+
print(f"\n--- Generating imatrix: {imatrix_path} ---")
|
| 1541 |
+
if not gen_imatrix(args.model,args.data,imatrix_path,args.chunks):
|
| 1542 |
+
print("ERROR: Imatrix generation failed!")
|
| 1543 |
+
sys.exit(1)
|
| 1544 |
+
imatrix_paths.insert(0,imatrix_path)
|
| 1545 |
+
if not imatrix_paths:
|
| 1546 |
+
print("ERROR: --imatrix required (or use --gen-imatrix with --data)")
|
| 1547 |
+
sys.exit(1)
|
| 1548 |
+
for im in imatrix_paths:
|
| 1549 |
+
if not os.path.isfile(im):
|
| 1550 |
+
print(f"ERROR: Imatrix not found: {im}")
|
| 1551 |
+
sys.exit(1)
|
| 1552 |
+
tiers=["mini","compact","quality","fidelity","nano"] if args.tier=="all" else[args.tier]
|
| 1553 |
+
if args.tier=="all" and int4_lineage_of(args.model) and os.environ.get("ASHQ1_INCLUDE_FIDELITY")!="1":
|
| 1554 |
+
tiers.remove("fidelity")
|
| 1555 |
+
print(f" ℹ AutoRound int4 lineage detected — Fidelity skipped (e₂ ≪ e₁ beyond {INT4_LINEAGE_CAP} on int4 classes). Set ASHQ1_INCLUDE_FIDELITY=1 to override.")
|
| 1556 |
+
extra_args=[]
|
| 1557 |
+
if args.allow_q3_or_lower:extra_args.append("--allow-q3-or-lower")
|
| 1558 |
+
extra_args.extend(["--imatrix-method",args.imatrix_method,"--lineage",args.lineage])
|
| 1559 |
+
if args.verbose:extra_args.append("--verbose")
|
| 1560 |
+
results={}
|
| 1561 |
+
for tier in tiers:
|
| 1562 |
+
target=bf16_mib*ALL_RATIOS[tier]
|
| 1563 |
+
success=run_tier(args.model,imatrix_paths,tier,target,output_dir,args.run,extra_args,args.lineage)
|
| 1564 |
+
results[tier]=success
|
| 1565 |
+
print(f"\n{'='*60}")
|
| 1566 |
+
print(" SUMMARY")
|
| 1567 |
+
print(f"{'='*60}")
|
| 1568 |
+
mode="quantized" if args.run else "dry-run"
|
| 1569 |
+
for tier,success in results.items():
|
| 1570 |
+
target=bf16_mib*ALL_RATIOS[tier]
|
| 1571 |
+
status="✓ Done" if success else "✗ Failed"
|
| 1572 |
+
print(f" {tier_label(tier):<12}{target:>7.0f} MiB {status} ({mode})")
|
| 1573 |
+
print()
|
| 1574 |
+
|
| 1575 |
+
if __name__=="__main__":
|
| 1576 |
+
raw_args=sys.argv[1:]
|
| 1577 |
+
if any(arg in raw_args for arg in("--profile","--show-config","--show-floors","--size")) and "--tier" not in raw_args:
|
| 1578 |
+
sys.exit(run_main_cli(raw_args))
|
| 1579 |
+
else:
|
| 1580 |
+
tiers_main(raw_args)
|
CHARTER.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ASHQ1 Technical Charter & Precision Guarantees
|
| 2 |
+
|
| 3 |
+
This document establishes the architectural principles, mathematical formulation, and invariant guarantees governing the AutoRound + ASHQ1 quantization suite.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📐 1. Architectural Principles
|
| 8 |
+
|
| 9 |
+
### 1.1 Dual-Stage Optimization Model
|
| 10 |
+
The suite operates on a two-stage sequential precision boundary:
|
| 11 |
+
1. **Weight-Space Reconditioning (AutoRound)**:
|
| 12 |
+
Minimizes local layer reconstruction error over calibration activations:
|
| 13 |
+
$$\min_{\mathbf{W}_q} \|\mathbf{W}\mathbf{X} - \mathbf{W}_q\mathbf{X}\|_F^2 + \lambda \mathcal{R}(\mathbf{W}_q)$$
|
| 14 |
+
where $\mathbf{W}_q$ represents the discrete integer matrix with continuous scale and zero adjustments.
|
| 15 |
+
2. **Activation-Space Precision Allocation (ASHQ1)**:
|
| 16 |
+
Given an empirical Fisher Information Proxy $\mathbf{F} = \text{diag}(\mathbf{H})$ derived from activation variances in `imatrix.dat`, ASHQ1 solves a constrained knapsack problem:
|
| 17 |
+
$$\max_{\{t_i\}} \sum_{i} \mathbf{F}_i \cdot \Delta \mathcal{Q}(t_i) \quad \text{subject to} \quad \sum_{i} \text{Size}(t_i, \text{params}_i) \le \text{Budget}_{\text{VRAM}}$$
|
| 18 |
+
where $t_i \in \text{TIER\_ORDER}$ and $\Delta \mathcal{Q}(t_i) = 2^{-2 \cdot \text{eff\_bpw}(t_i)}$.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## 🛡️ 2. Invariant Precision Guarantees
|
| 23 |
+
|
| 24 |
+
To prevent numerical drift and safeguard autoregressive context integrity, ASHQ1 enforces absolute tensor classification floors:
|
| 25 |
+
|
| 26 |
+
```
|
| 27 |
+
┌──────────────────────────────┬───────────────┬────────────────────────────────────────────┐
|
| 28 |
+
│ Tensor Class │ Minimum Floor │ Rationale │
|
| 29 |
+
├──────────────────────────────┼───────────────┼────────────────────────────────────────────┤
|
| 30 |
+
│ Norms & Scales (ln, rms) │ F16 / F32 │ Preserves dynamic scale variance │
|
| 31 |
+
│ Attention Gates (gate_proj) │ Q6_K │ Prevents non-linear token routing collapse │
|
| 32 |
+
│ Recurrent State (ssm_alpha) │ Q8_0 │ Guarantees stable hidden state accumulation│
|
| 33 |
+
│ Speculative Heads (MTP/NextN)│ Q6_K / Q8_0 │ Maximizes draft token acceptance rate │
|
| 34 |
+
│ Vision Projectors (mmproj) │ F32 (critical)│ Protects high-resolution visual grounding │
|
| 35 |
+
└──────────────────────────────┴───────────────┴────────────────────────────────────────────┘
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### 2.1 Pinned Structural Layers
|
| 39 |
+
* **1D Vectors & Bias Tensors**: All vectors with fewer than 4096 elements or non-matrix shapes remain strictly in `F32` or `F16`.
|
| 40 |
+
* **Block Alignment Rules**: Tensors must adhere to integer multiples of the GGML block size (32 or 256 elements). Unaligned shapes are automatically promoted to the nearest valid container (`F16`/`Q8_0`).
|
| 41 |
+
|
| 42 |
+
### 2.2 Tied-Weight Group Preservation
|
| 43 |
+
When tied layers are detected (such as shared input/output embeddings or symmetric MoE gates), ASHQ1 binds the corresponding parameters into unified equivalence sets. The optimizer updates their quantization tiers simultaneously to prevent directional divergence.
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## 🔬 3. AutoRound Lineage & Saturation Bounds
|
| 48 |
+
|
| 49 |
+
When weights are derived from an AutoRound W4A16 source (recorded via `.provenance.json` sidecars):
|
| 50 |
+
* **Information Saturation**: Because the underlying weights have been pre-conditioned for 4-bit representations, assigning container formats above `Q5_K` yields negligible signal recovery ($\Delta \mathcal{Q} \to 0$).
|
| 51 |
+
* **Automated Capping**: The ASHQ1 optimizer caps attention and FFN blocks at `Q5_K`, reallocating spare bit-budget toward recurrent memory states, routing gates, and structural normalization.
|
| 52 |
+
* **Tier Pruning**: On AutoRound int4 lineage, generation of the 48% `Fidelity` tier is skipped by default, focusing compute resources on optimal `Quality` (39%), `Compact` (30%), `Mini` (27%), and `Nano` (21%) variants.
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## 📈 4. Calibration Dataset Standard
|
| 57 |
+
|
| 58 |
+
Calibration quality directly determines activation matrix fidelity. The suite implements an exact multi-domain quota:
|
| 59 |
+
|
| 60 |
+
* **55% Agentic Execution**: Tool-use traces, JSON schema parameters, and multi-turn function call loops.
|
| 61 |
+
* **20% Formal Logic & Code**: Formal mathematical proofs (Lean 4), algorithmic Python implementations, and syntactic structures.
|
| 62 |
+
* **15% Semantic Diversity**: Multilingual sequences, encyclopedic knowledge, and conversational discourse.
|
| 63 |
+
* **10% Frontier Reasoning**: Extended thinking traces and complex reasoning chains.
|
| 64 |
+
|
| 65 |
+
All samples undergo token-count round-trip verification to ensure consistent sequence boundaries during `llama-imatrix` evaluation.
|
README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- quantization
|
| 5 |
+
- gguf
|
| 6 |
+
- autoround
|
| 7 |
+
- imatrix
|
| 8 |
+
- hybrid-quantization
|
| 9 |
+
- llama.cpp
|
| 10 |
+
- text-generation
|
| 11 |
---
|
| 12 |
+
|
| 13 |
+
# AutoRound + ASHQ1 Double-Quantization Suite
|
| 14 |
+
|
| 15 |
+
The **AutoRound + ASHQ1 Suite** delivers a complete pipeline for creating ultra-high-fidelity GGUF models. By combining gradient-guided weight reorganization (**AutoRound W4A16**) with fine-grained activation-aware tensor assignment (**ASHQ1 Imatrix Engine**), this suite establishes a new standard for low-bit LLM compression.
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 🌟 Key Architecture & Highlights
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
┌─────────────────────────┐
|
| 23 |
+
│ Safetensors (Raw / BF16)│
|
| 24 |
+
└────────────┬────────────┘
|
| 25 |
+
│ 00_SAFETENSORS-to-AutoRound-BF16-GGUF.py
|
| 26 |
+
▼
|
| 27 |
+
┌─────────────────────────┐
|
| 28 |
+
│ AutoRound Optimization │ ──► Iterative sign-rounding & Hessian estimation
|
| 29 |
+
└────────────┬────────────┘
|
| 30 |
+
│ Streaming dequantization + GGUF encapsulation
|
| 31 |
+
▼
|
| 32 |
+
┌─────────────────────────┐
|
| 33 |
+
│ AutoRound-Infused BF16 │ ──► Lineage recorded in sidecar metadata
|
| 34 |
+
└────────────┬────────────┘
|
| 35 |
+
│ 01_create-calibration-dataset-and-imatrix.py
|
| 36 |
+
▼
|
| 37 |
+
┌─────────────────────────┐
|
| 38 |
+
│ Multi-Source Imatrix │ ──► Agentic, Frontier, Logic & Diversity corpus
|
| 39 |
+
└────────────┬────────────┘
|
| 40 |
+
│ 02_BF16-GGUF-to-ASHQ1.py (ASHQ1 Engine)
|
| 41 |
+
▼
|
| 42 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 43 |
+
│ Standardized ASHQ1 Tiers: Nano • Mini • Compact • Quality │
|
| 44 |
+
└─────────────────────────────────────────────────────────────┘
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### 1. Dual-Phase Quantization Synergy
|
| 48 |
+
* **Phase 1 (AutoRound W4A16)**: Reconditions original full-precision matrices via small-sample Hessian compensation. The optimized rounding directions remain intact when converted into BF16 GGUF containers.
|
| 49 |
+
* **Phase 2 (ASHQ1 Engine)**: Dissects individual layer activations using multi-source importance matrices (`imatrix.dat`). Assigns precision tiers (`IQ2_XXS` through `Q8_0` and `F32`) dynamically based on layer sensitivity and tensor class.
|
| 50 |
+
|
| 51 |
+
### 2. Comprehensive Model Architecture Support
|
| 52 |
+
* **Dense & MoE Transformers**: Precise expert protection with token-routing stabilization.
|
| 53 |
+
* **Recurrent & Hybrid Models (GDN / Mamba / RWKV / Qwen3.5)**: Guaranteed `Q8_0` memory state retention to ensure long-context recurrence stability.
|
| 54 |
+
* **Multimodal Towers (CLIP / Vision Encoders)**: Dedicated `ASHQ1-mmproj.py` engine preserving spatial embeddings and layer-normalization vectors in `F32`/`F16`.
|
| 55 |
+
* **Multi-Token Prediction (MTP / NextN)**: Automatic extraction, isolation, and high-precision encoding (`Q6_K`/`Q8_0`) of speculative decoding heads.
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## 📊 Standardized ASHQ1 Tiers
|
| 60 |
+
|
| 61 |
+
All tiers maintain strict byte-budget percentages relative to the original unquantized BF16 model:
|
| 62 |
+
|
| 63 |
+
| Tier | File Ratio | Base Type | Typical Use Case | Target Preservation |
|
| 64 |
+
| :--- | :---: | :---: | :--- | :--- |
|
| 65 |
+
| **Nano** | **21%** | `IQ3_XXS` | Maximum compression, edge & mobile VRAM | Core gates `Q6_K`, Down-proj `IQ2_S` |
|
| 66 |
+
| **Mini** | **27%** | `IQ4_XS` | Efficient high-throughput serving | Balanced `IQ4_XS`/`IQ3_S` distribution |
|
| 67 |
+
| **Compact** | **30%** | `IQ4_XS` | Balanced daily-driver footprint | Down-proj `IQ4_XS`, Gate/Up `IQ3_S` |
|
| 68 |
+
| **Quality** | **39%** | `Q5_K_M` | Near-lossless general deployment | Full `Q4_K`/`Q5_K` attention coverage |
|
| 69 |
+
| **Fidelity** | **48%** | `Q6_K` | Maximum analytical precision (raw BF16 lineage) | High-precision `Q5_K`/`Q6_K`/`Q8_0` mix |
|
| 70 |
+
|
| 71 |
+
*Note: Models originating from an AutoRound int4 lineage cap their weight allocations at the Quality tier (`Q5_K`/`Q4_K` mix), as theoretical information saturation is fully realized.*
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## 🛠️ Suite Components
|
| 76 |
+
|
| 77 |
+
| Script | Purpose |
|
| 78 |
+
| :--- | :--- |
|
| 79 |
+
| `00_SAFETENSORS-to-AutoRound-BF16-GGUF.py` | AutoRound tuner & streaming GGUF builder. Emits lineage provenance sidecars. |
|
| 80 |
+
| `00b_BF16-GGUF-MTP-extract.py` | Standalone speculative draft extractor for Multi-Token Prediction layers. |
|
| 81 |
+
| `01_create-calibration-dataset-and-imatrix.py` | End-to-end dataset builder (Agentic/Frontier/Logic) and GPU-autotuned `llama-imatrix` runner. |
|
| 82 |
+
| `01b_BF16-GGUF-modules-fusion.py` | Lossless merger combining base models, vision projectors (`mmproj`), and MTP heads. |
|
| 83 |
+
| `02_BF16-GGUF-to-ASHQ1.py` | Automated orchestrator executing batch quantization across all target tiers. |
|
| 84 |
+
| `ASHQ1.py` | Core hybrid quantization optimizer with greedy knapsack utility scheduling and tied-weight detection. |
|
| 85 |
+
| `ASHQ1-mmproj.py` | Vision projector quantizer applying selective deep-block boosting and critical layer pinning. |
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## ⚡ Quick Start
|
| 90 |
+
|
| 91 |
+
### 1. Requirements
|
| 92 |
+
Ensure CUDA, PyTorch, and `auto-round` are installed:
|
| 93 |
+
```bash
|
| 94 |
+
pip install auto-round torchvision safetensors gguf numpy huggingface_hub
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### 2. End-to-End Workflow
|
| 98 |
+
```bash
|
| 99 |
+
# Step 0: Optimize safetensors and produce pristine AutoRound BF16 GGUF
|
| 100 |
+
python 00_SAFETENSORS-to-AutoRound-BF16-GGUF.py ./safetensors/
|
| 101 |
+
|
| 102 |
+
# Step 1: Compute calibration activation statistics (imatrix)
|
| 103 |
+
python 01_create-calibration-dataset-and-imatrix.py
|
| 104 |
+
|
| 105 |
+
# Step 2: Generate all ASHQ1 standardized tiers
|
| 106 |
+
python 02_BF16-GGUF-to-ASHQ1.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### 3. Recommended Inference Parameters
|
| 110 |
+
When serving ASHQ1 quantized models with `llama.cpp`, enable 4-bit KV cache quantization for optimal memory efficiency across extended context lengths:
|
| 111 |
+
```bash
|
| 112 |
+
llama-server -m model-AutoRound-ASHQ1-Quality-39pc.gguf -c 32768 --cache-type-k q4_0 --cache-type-v q4_0 -ngl 99
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 📜 Citation & Credits
|
| 118 |
+
|
| 119 |
+
The AutoRound + ASHQ1 suite builds directly upon fundamental research and tooling across the open-source ecosystem:
|
| 120 |
+
|
| 121 |
+
* **ASHQ1 (Autonomous Selective Hybrid Quantization)** by **[wepiqx](https://huggingface.co/wepiqx/ASHQ1)**:
|
| 122 |
+
Original mathematical formulation of the priority-queue-driven knapsack optimizer, tied-group detection using numerical activation hashes, and theoretical MSE reduction scheduling.
|
| 123 |
+
* **Intel AutoRound**:
|
| 124 |
+
Sign-gradient-based optimization framework for low-bit weight reorganization with Hessian compensation.
|
| 125 |
+
* **llama.cpp** by **[Georgi Gerganov & ggml contributors](https://github.com/ggml-org/llama.cpp)**:
|
| 126 |
+
Core GGML/GGUF format definitions, runtime execution kernels, and quantization tools (`llama-quantize`, `llama-imatrix`).
|
| 127 |
+
* **Calibration Methodology & Recipes**:
|
| 128 |
+
Activation corpus curation inspired by **[Bartowski](https://huggingface.co/bartowski)** and multi-matrix combination techniques by **cHunter789**.
|