Soulfate24 commited on
Commit
cb4574f
·
verified ·
1 Parent(s): aac306a

Initial release of AutoRound + ASHQ1 suite

Browse files
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**.