LuffyTheFox

Untitled

Apr 7th, 2026 (edited)
3,858
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 18.92 KB | None | 0 0
  1. # ============================================================================
  2. # Unsloth UD-Q4_K_L -> JSON profile -> delete ref -> quantize your BF16 GGUF
  3. #
  4. # Goal:
  5. # 1) Read the exact per-tensor quantization types used in:
  6. # Qwen3.5-35B-A3B-UD-Q4_K_L.gguf (Unsloth)
  7. # 2) Save them into a JSON profile (once)
  8. # 3) Delete Unsloth reference quant to save disk
  9. # 4) Quantize your GGUF -> Q4_K_L using that JSON profile
  10. #
  11. # Notes:
  12. # - If your input is already quantized, llama-quantize needs --allow-requantize.
  13. # - This script uses regex-based --tensor-type rules (supported by llama.cpp quantizer).
  14. # - Designed for Colab/Linux.
  15. # ============================================================================
  16.  
  17. import os
  18. import sys
  19. import re
  20. import gc
  21. import json
  22. import time
  23. import subprocess
  24. import numpy as np
  25.  
  26. # ============================================================================
  27. # USER CONFIG
  28. # ============================================================================
  29.  
  30. WORK_DIR = "/content/gguf_workspace"
  31. os.makedirs(WORK_DIR, exist_ok=True)
  32.  
  33. # --- YOUR MODEL (input) ------------------------------------------------------
  34. # Put your trained model GGUF here (BF16 in our case):
  35. INPUT_FILE = os.path.join(WORK_DIR, "Qwen3.5-35B-A3B-Uncensored-Wasserstein-BF16.gguf")
  36.  
  37. # --- Unsloth reference quant (used only to extract profile) ------------------
  38. REF_FILE = os.path.join(WORK_DIR, "Qwen3.5-35B-A3B-UD-Q4_K_L.gguf")
  39. REF_URL = ("https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-UD-Q4_K_L.gguf?download=true")
  40.  
  41. # --- Output ------------------------------------------------------------------
  42. TARGET_BASE_TYPE = "Q4_K_M"
  43. OUTPUT_TAG = "UD" # marker in output filename
  44.  
  45. # --- Store the extracted quant schema in JSON --------------------------------
  46. PROFILE_JSON = os.path.join(WORK_DIR, "unsloth_ud_profile.json")
  47.  
  48. # After JSON is built, delete Unsloth reference quant GGUF to save disk
  49. DELETE_REF_AFTER_PROFILE = True
  50.  
  51. # Keep JSON compact by default (do NOT store huge name->type map)
  52. # Turn on only if you really want the full audit dump (can be big).
  53. STORE_RAW_TENSOR_MAP = False
  54.  
  55. # --- llama.cpp binaries ------------------------------------------------------
  56. LLAMA_TAG = "b8192"
  57. LLAMA_URL = (
  58. "https://github.com/ggml-org/llama.cpp/releases/download/"
  59. f"{LLAMA_TAG}/llama-{LLAMA_TAG}-bin-ubuntu-x64.tar.gz"
  60. )
  61. LLAMA_TAR = f"llama-{LLAMA_TAG}-bin-ubuntu-x64.tar.gz"
  62. LLAMA_BIN_DIR = os.path.join(WORK_DIR, "llama-bin")
  63.  
  64. # Verbosity
  65. PRINT_PROFILE_SUMMARY = True
  66. VERIFY_AFTER_QUANT = True
  67.  
  68.  
  69. # ============================================================================
  70. # UTILS
  71. # ============================================================================
  72.  
  73. def ensure_python_pkg(pkg: str):
  74. subprocess.check_call(
  75. [sys.executable, "-m", "pip", "install", "-q", pkg],
  76. stdout=subprocess.DEVNULL,
  77. stderr=subprocess.DEVNULL,
  78. )
  79.  
  80. def file_size_gb(path):
  81. return os.path.getsize(path) / (1 << 30)
  82.  
  83. def disk_free_gb(path=WORK_DIR):
  84. st = os.statvfs(path)
  85. return (st.f_bavail * st.f_frsize) / (1 << 30)
  86.  
  87. def ram_used_gb():
  88. try:
  89. with open("/proc/self/status", "r") as f:
  90. for line in f:
  91. if line.startswith("VmRSS:"):
  92. return int(line.split()[1]) / (1 << 20)
  93. except Exception:
  94. pass
  95. return -1
  96.  
  97. def safe_remove(path):
  98. try:
  99. if os.path.isfile(path):
  100. os.remove(path)
  101. except Exception:
  102. pass
  103.  
  104. def download_file(url, out_path, label, timeout=6*3600):
  105. if os.path.isfile(out_path) and os.path.getsize(out_path) > 1024:
  106. print(f"[{label}] Found: {out_path} ({file_size_gb(out_path):.2f} GB)")
  107. return out_path
  108.  
  109. print(f"[{label}] Downloading -> {out_path}")
  110. os.makedirs(os.path.dirname(out_path), exist_ok=True)
  111.  
  112. for cmd in [
  113. ["wget", "-O", out_path, "-c", "--show-progress", url],
  114. ["curl", "-L", "-o", out_path, url],
  115. ]:
  116. try:
  117. subprocess.check_call(cmd, timeout=timeout)
  118. if os.path.isfile(out_path) and os.path.getsize(out_path) > 1024:
  119. print(f"[{label}] Downloaded: {out_path} ({file_size_gb(out_path):.2f} GB)")
  120. return out_path
  121. except Exception:
  122. continue
  123.  
  124. print(f"[{label}] ERROR: download failed")
  125. return None
  126.  
  127. def get_llama_quantize():
  128. # Try existing
  129. for root, _, files in os.walk(LLAMA_BIN_DIR):
  130. if "llama-quantize" in files:
  131. p = os.path.join(root, "llama-quantize")
  132. if os.access(p, os.X_OK):
  133. print(f"[BIN] Found: {p}")
  134. return p
  135.  
  136. # Download
  137. tar_path = os.path.join(WORK_DIR, LLAMA_TAR)
  138. ok = download_file(LLAMA_URL, tar_path, "BIN", timeout=1200)
  139. if not ok:
  140. return None
  141.  
  142. os.makedirs(LLAMA_BIN_DIR, exist_ok=True)
  143.  
  144. extracted = False
  145. for strip in [True, False]:
  146. try:
  147. cmd = ["tar", "xzf", tar_path, "-C", LLAMA_BIN_DIR]
  148. if strip:
  149. cmd.append("--strip-components=1")
  150. subprocess.check_call(cmd)
  151. extracted = True
  152. break
  153. except subprocess.CalledProcessError:
  154. continue
  155.  
  156. safe_remove(tar_path)
  157.  
  158. if not extracted:
  159. print("[BIN] ERROR: extraction failed.")
  160. return None
  161.  
  162. for root, _, files in os.walk(LLAMA_BIN_DIR):
  163. if "llama-quantize" in files:
  164. p = os.path.join(root, "llama-quantize")
  165. os.chmod(p, 0o755)
  166. print(f"[BIN] Ready: {p}")
  167. return p
  168.  
  169. print("[BIN] ERROR: llama-quantize not found.")
  170. return None
  171.  
  172. def normalize_output_basename(input_path):
  173. base = os.path.splitext(os.path.basename(input_path))[0]
  174. known_suffixes = [
  175. ".Q8_0", "-Q8_0", ".F16", "-F16", ".BF16", "-BF16", ".F32", "-F32",
  176. ".Q4_0", "-Q4_0", ".Q4_K_M", "-Q4_K_M", ".Q5_K_M", "-Q5_K_M",
  177. ".IQ4_XS", "-IQ4_XS", ".IQ4_NL", "-IQ4_NL",
  178. ]
  179. for suf in known_suffixes:
  180. if base.endswith(suf):
  181. base = base[:-len(suf)]
  182. break
  183. return base
  184.  
  185. def regex_escape_literal(s: str) -> str:
  186. return re.sub(r'([.^$*+?{}\[\]\\|()])', r'\\\1', s)
  187.  
  188. def normalize_qtype(s):
  189. if s is None:
  190. return None
  191. s = str(s).strip()
  192. s = s.replace("GGML_TYPE_", "").replace("ggml_type_", "").replace("GGUF_TYPE_", "")
  193. return s.upper()
  194.  
  195. def seems_quantized_from_types(type_list):
  196. # If any tensor type starts with Q or IQ, treat as already-quantized.
  197. for t in type_list:
  198. if not t:
  199. continue
  200. u = str(t).upper()
  201. if u.startswith("Q") or u.startswith("IQ") or u.startswith("TQ") or u.startswith("KQ"):
  202. return True
  203. return False
  204.  
  205. # ============================================================================
  206. # GGUF INSPECTION
  207. # ============================================================================
  208.  
  209. def read_gguf_tensors_basic(gguf_path):
  210. """
  211. Returns list of dicts: {name, type_str, n_bytes, data_offset}
  212. """
  213. ensure_python_pkg("gguf")
  214. from gguf import GGUFReader
  215.  
  216. r = GGUFReader(gguf_path)
  217. out = []
  218. for t in r.tensors:
  219. typ = None
  220. for a in ("tensor_type", "type", "ggml_type", "data_type"):
  221. if hasattr(t, a):
  222. typ = getattr(t, a)
  223. break
  224. typ_s = normalize_qtype(typ.name if hasattr(typ, "name") else typ)
  225.  
  226. n_bytes = getattr(t, "n_bytes", None)
  227. data_offset = getattr(t, "data_offset", None)
  228.  
  229. out.append({
  230. "name": t.name,
  231. "type": typ_s,
  232. "n_bytes": int(n_bytes) if n_bytes is not None else None,
  233. "data_offset": int(data_offset) if data_offset is not None else None,
  234. })
  235.  
  236. del r
  237. gc.collect()
  238. return out
  239.  
  240. def get_tensor_names(gguf_path):
  241. return [x["name"] for x in read_gguf_tensors_basic(gguf_path)]
  242.  
  243. def get_tensor_types_map(gguf_path):
  244. items = read_gguf_tensors_basic(gguf_path)
  245. return {x["name"]: x["type"] for x in items}
  246.  
  247. # ============================================================================
  248. # PROFILE EXTRACTION (from Unsloth ref GGUF)
  249. # ============================================================================
  250.  
  251. def extract_profile_from_ref(ref_path):
  252. """
  253. Build a compact rule-set that reproduces Unsloth per-tensor types.
  254.  
  255. Output JSON structure:
  256. {
  257. "ref_file": "...",
  258. "token_embd_type": "Q8_0",
  259. "output_type": "Q6_K",
  260. "rules": [
  261. {"pattern": "^blk\\.\\d+\\.attn_q\\.weight$", "type": "Q8_0", "kind": "blk_all_layers"},
  262. {"pattern": "^blk\\.(?:0|1|2)\\.ffn_down_exps\\.weight$", "type": "IQ4_NL", "kind": "blk_subset_layers"},
  263. ...
  264. ],
  265. "stats": {...}
  266. ["tensor_types_by_name": {...}] # optional
  267. }
  268. """
  269. types = get_tensor_types_map(ref_path)
  270.  
  271. token_embd_type = types.get("token_embd.weight")
  272. output_type = types.get("output.weight")
  273.  
  274. # group block tensors by (suffix,type)->layers
  275. blk_re = re.compile(r"^blk\.(\d+)\.(.+)$")
  276. by_suffix_type_layers = {} # (suffix, type) -> set(layers)
  277. globals_map = {} # name -> type
  278. max_layer = -1
  279.  
  280. for name, typ in types.items():
  281. m = blk_re.match(name)
  282. if not m:
  283. globals_map[name] = typ
  284. continue
  285. layer = int(m.group(1))
  286. suffix = m.group(2)
  287. max_layer = max(max_layer, layer)
  288. by_suffix_type_layers.setdefault((suffix, typ), set()).add(layer)
  289.  
  290. n_layers = max_layer + 1 if max_layer >= 0 else 0
  291.  
  292. rules = []
  293.  
  294. # Global exact rules (but skip token_embd/output since we’ll pass dedicated flags)
  295. for name, typ in sorted(globals_map.items()):
  296. if name in ("token_embd.weight", "output.weight"):
  297. continue
  298. if typ is None:
  299. continue
  300. rules.append({
  301. "pattern": "^" + regex_escape_literal(name) + "$",
  302. "type": typ,
  303. "kind": "global_exact",
  304. })
  305.  
  306. # Block rules
  307. for (suffix, typ), layers in sorted(by_suffix_type_layers.items(), key=lambda x: (x[0][0], str(x[0][1]))):
  308. if typ is None:
  309. continue
  310. layers = sorted(layers)
  311. suffix_esc = regex_escape_literal(suffix)
  312.  
  313. if n_layers and len(layers) == n_layers:
  314. pat = r"^blk\.\d+\." + suffix_esc + r"$"
  315. kind = "blk_all_layers"
  316. else:
  317. alt = "|".join(str(i) for i in layers)
  318. pat = r"^blk\.(?:" + alt + r")\." + suffix_esc + r"$"
  319. kind = "blk_subset_layers"
  320.  
  321. rules.append({
  322. "pattern": pat,
  323. "type": typ,
  324. "kind": kind,
  325. })
  326.  
  327. prof = {
  328. "ref_file": os.path.basename(ref_path),
  329. "created_unix": int(time.time()),
  330. "token_embd_type": token_embd_type,
  331. "output_type": output_type,
  332. "rules": rules,
  333. "stats": {
  334. "n_tensors_total": len(types),
  335. "n_layers_detected": n_layers,
  336. "n_rules": len(rules),
  337. },
  338. }
  339.  
  340. if STORE_RAW_TENSOR_MAP:
  341. prof["tensor_types_by_name"] = types
  342.  
  343. return prof
  344.  
  345. def load_or_build_profile_json():
  346. # If profile JSON exists, use it and do NOT download ref again.
  347. if os.path.isfile(PROFILE_JSON) and os.path.getsize(PROFILE_JSON) > 100:
  348. with open(PROFILE_JSON, "r", encoding="utf-8") as f:
  349. prof = json.load(f)
  350. print(f"[PROFILE] Loaded: {PROFILE_JSON}")
  351. return prof
  352.  
  353. print("[PROFILE] JSON not found; building from Unsloth reference GGUF...")
  354.  
  355. if not os.path.isfile(REF_FILE):
  356. download_file(REF_URL, REF_FILE, "REF", timeout=6*3600)
  357.  
  358. prof = extract_profile_from_ref(REF_FILE)
  359.  
  360. with open(PROFILE_JSON, "w", encoding="utf-8") as f:
  361. json.dump(prof, f, ensure_ascii=False, indent=2)
  362.  
  363. print(f"[PROFILE] Saved: {PROFILE_JSON}")
  364.  
  365. if DELETE_REF_AFTER_PROFILE:
  366. print(f"[PROFILE] Deleting Unsloth reference quant to save disk: {REF_FILE}")
  367. safe_remove(REF_FILE)
  368.  
  369. return prof
  370.  
  371. # ============================================================================
  372. # QUANTIZE
  373. # ============================================================================
  374.  
  375. def filter_rules_to_existing_tensors(rules, tensor_names):
  376. """
  377. Keep only rules whose regex matches at least one tensor name in your input model.
  378. This reduces command size and avoids useless rules.
  379. """
  380. kept = []
  381. names = tensor_names
  382.  
  383. for r in rules:
  384. pat = r.get("pattern")
  385. typ = r.get("type")
  386. if not pat or not typ:
  387. continue
  388. try:
  389. rx = re.compile(pat)
  390. except re.error:
  391. continue
  392. if any(rx.search(n) for n in names):
  393. kept.append(r)
  394.  
  395. return kept
  396.  
  397. def quantize_with_profile(input_path, output_path, profile):
  398. bin_path = get_llama_quantize()
  399. if not bin_path:
  400. raise RuntimeError("llama-quantize not available")
  401.  
  402. if not os.path.isfile(input_path):
  403. raise FileNotFoundError(f"Input not found: {input_path}")
  404.  
  405. # Disk check (quantization creates output + temp buffers)
  406. in_gb = file_size_gb(input_path)
  407. free_gb = disk_free_gb()
  408. need_gb = max(12.0, in_gb * 0.9)
  409.  
  410. # Decide if allow-requantize is needed (input already quantized)
  411. input_types_map = get_tensor_types_map(input_path)
  412. allow_requantize = seems_quantized_from_types(list(input_types_map.values()))
  413.  
  414. input_names = list(input_types_map.keys())
  415.  
  416. rules = profile.get("rules", [])
  417. rules = filter_rules_to_existing_tensors(rules, input_names)
  418.  
  419. env = os.environ.copy()
  420. bin_dir = os.path.dirname(bin_path)
  421. ld = env.get("LD_LIBRARY_PATH", "")
  422. env["LD_LIBRARY_PATH"] = f"{bin_dir}:{ld}" if ld else bin_dir
  423.  
  424. cmd = [bin_path]
  425.  
  426. if allow_requantize:
  427. cmd += ["--allow-requantize"]
  428.  
  429. # Apply dedicated types only if corresponding tensors exist in input
  430. if "token_embd.weight" in input_types_map and profile.get("token_embd_type"):
  431. cmd += ["--token-embedding-type", str(profile["token_embd_type"])]
  432. if "output.weight" in input_types_map and profile.get("output_type"):
  433. cmd += ["--output-tensor-type", str(profile["output_type"])]
  434.  
  435. for r in rules:
  436. cmd += ["--tensor-type", f"{r['pattern']}={r['type']}"]
  437.  
  438. cmd += [input_path, output_path, TARGET_BASE_TYPE]
  439.  
  440. print("\n" + "=" * 78)
  441. print("QUANTIZATION")
  442. print("=" * 78)
  443. print(f"[IN ] {input_path} ({in_gb:.2f} GB)")
  444. print(f"[OUT] {output_path}")
  445. print(f"[BASE] {TARGET_BASE_TYPE}")
  446. print(f"[RULES] {len(rules)} (filtered to your model)")
  447. print(f"[FLAG] allow_requantize = {allow_requantize}")
  448. print(f"[SYS] free disk = {free_gb:.1f} GB | ram used = {ram_used_gb():.2f} GB")
  449.  
  450. print("[CMD] (showing head)")
  451. print(" " + " \\\n ".join(cmd[:40]) + (" \\\n ... (many --tensor-type rules) ..." if len(cmd) > 40 else ""))
  452.  
  453. t0 = time.time()
  454. proc = subprocess.Popen(
  455. cmd,
  456. stdout=subprocess.PIPE,
  457. stderr=subprocess.STDOUT,
  458. text=True,
  459. env=env,
  460. bufsize=1,
  461. )
  462. for line in proc.stdout:
  463. line = line.rstrip()
  464. if line:
  465. print(" ", line)
  466. proc.wait()
  467.  
  468. if proc.returncode != 0:
  469. raise RuntimeError(f"llama-quantize failed (code={proc.returncode})")
  470.  
  471. dt = time.time() - t0
  472. out_gb = file_size_gb(output_path)
  473. print(f"[DONE] {dt/60:.1f} min | size {in_gb:.2f} GB -> {out_gb:.2f} GB")
  474.  
  475. def verify_output(output_path):
  476. print("[VERIFY] Checking output GGUF parse + quick zero-fill sample...")
  477. try:
  478. ensure_python_pkg("gguf")
  479. from gguf import GGUFReader
  480.  
  481. r = GGUFReader(output_path)
  482. tensors = list(r.tensors)
  483. print(f"[VERIFY] Tensors: {len(tensors)}")
  484.  
  485. checked = 0
  486. zeroish = 0
  487.  
  488. with open(output_path, "rb") as f:
  489. for t in tensors[:60]:
  490. n_bytes = getattr(t, "n_bytes", None)
  491. off = getattr(t, "data_offset", None)
  492. if n_bytes is None or off is None:
  493. continue
  494. n = min(4096, int(n_bytes))
  495. if n < 128:
  496. continue
  497. f.seek(int(off))
  498. raw = f.read(n)
  499. arr = np.frombuffer(raw, dtype=np.uint8)
  500. if arr.size and arr.sum() == 0:
  501. zeroish += 1
  502. checked += 1
  503.  
  504. del r
  505. gc.collect()
  506.  
  507. if zeroish:
  508. print(f"[VERIFY] WARNING: {zeroish}/{checked} sampled tensors looked zero-filled.")
  509. else:
  510. print(f"[VERIFY] OK: sampled {checked} tensors.")
  511. return True
  512. except Exception as e:
  513. print(f"[VERIFY] ERROR: {e}")
  514. return False
  515.  
  516.  
  517. # ============================================================================
  518. # MAIN
  519. # ============================================================================
  520.  
  521. def main():
  522. print("=" * 78)
  523. print("UNSLOTH UD-IQ4_XS PROFILE -> JSON -> DELETE REF -> QUANTIZE YOUR MODEL")
  524. print("=" * 78)
  525. print(f"[INFO] Work dir: {WORK_DIR}")
  526. print(f"[INFO] Input: {INPUT_FILE}")
  527. print(f"[INFO] Profile: {PROFILE_JSON}")
  528. print(f"[INFO] Target: {TARGET_BASE_TYPE}")
  529. print(f"[INFO] Disk free: {disk_free_gb():.2f} GB | RAM used: {ram_used_gb():.2f} GB")
  530.  
  531. if not os.path.isfile(INPUT_FILE):
  532. sys.exit(f"[ERROR] Your input GGUF not found: {INPUT_FILE}")
  533.  
  534. # Build/load JSON profile (this step deletes REF_FILE if it had to download it)
  535. profile = load_or_build_profile_json()
  536.  
  537. if PRINT_PROFILE_SUMMARY:
  538. print("[PROFILE] Summary:")
  539. print(json.dumps(profile.get("stats", {}), indent=2, ensure_ascii=False))
  540. print(f"[PROFILE] token_embd_type: {profile.get('token_embd_type')}")
  541. print(f"[PROFILE] output_type: {profile.get('output_type')}")
  542. print(f"[PROFILE] rules_total: {len(profile.get('rules', []))}")
  543.  
  544. base = normalize_output_basename(INPUT_FILE)
  545. output_path = os.path.join(WORK_DIR, f"{base}-{OUTPUT_TAG}-{TARGET_BASE_TYPE}.gguf")
  546.  
  547. quantize_with_profile(INPUT_FILE, output_path, profile)
  548.  
  549. if VERIFY_AFTER_QUANT:
  550. print("\n" + "=" * 78)
  551. verify_output(output_path)
  552.  
  553. print("\n" + "=" * 78)
  554. print("COMPLETE")
  555. print("=" * 78)
  556. print(f"[OUT] {output_path}")
  557. print(f"[OUT] Size: {file_size_gb(output_path):.2f} GB")
  558. print(f"[SYS] Free disk: {disk_free_gb():.2f} GB")
  559.  
  560. print("\n[FS]")
  561. for fn in sorted(os.listdir(WORK_DIR)):
  562. fp = os.path.join(WORK_DIR, fn)
  563. if os.path.isfile(fp):
  564. try:
  565. print(f" {fn} ({file_size_gb(fp):.2f} GB)")
  566. except Exception:
  567. print(f" {fn}")
  568.  
  569. if __name__ == "__main__":
  570. main()
Advertisement
Add Comment
Please, Sign In to add comment