quantprobe 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
quantprobe/plan.py ADDED
@@ -0,0 +1,208 @@
1
+ """quantprobe plan - the tiered decode law as a CLI.
2
+
3
+ tok/s = eta(tier) x bandwidth / active-bytes-per-token.
4
+ Evaluates every placement (full-VRAM, hybrid attention->VRAM + experts->RAM, dense layer-split,
5
+ pure CPU, disk-stream), predicts speed for each, prints the winner WITH the llama.cpp command to
6
+ run it, plus an upgrade advisor. eta bands are fitted from published measurements (7B..744B),
7
+ validated by pre-registered predictions (30B hybrid: predicted 19, measured 19.30 +/- 0.88).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ # kvp = KV-cache bytes per position (K+V, f16, all layers). Exact where the architecture is known,
12
+ # [est] otherwise. MLA models (deepseek) cache the compressed latent -> ~10x smaller: placement's
13
+ # context story differs per architecture, which is why this is per-model, not a constant.
14
+ MODELS = {
15
+ "qwen3-30b": dict(t=30.5, a=3.3, ne=1.2, moe=True, kvp=98304, hint="Qwen3-30B-A3B"), # 48L x 4KV x 128d (exact; calibration anchor)
16
+ "deepseek-16b": dict(t=15.7, a=2.4, ne=1.3, moe=True, kvp=31104, hint="DeepSeek-V2-Lite"), # MLA: 27L x (512+64) latent (exact)
17
+ "gemma-12b": dict(t=11.9, a=11.9, ne=11.9, moe=False, kvp=65536, hint="Gemma 4 12B"), # [est] SWA: long-ctx slope from global layers only
18
+ "mistral-7b": dict(t=7.2, a=7.2, ne=7.2, moe=False, kvp=131072, hint="Mistral 7B"), # 32L x 8KV x 128d (exact)
19
+ "glm-air": dict(t=110, a=12, ne=2.7, moe=True, kvp=94208, hint="GLM-4.5-Air 106B"), # [est]
20
+ "glm-744b": dict(t=744, a=32, ne=8, moe=True, kvp=188416, hint="GLM-5.2 744B"), # [est]
21
+ }
22
+ DEFAULT_KVP = 98304 # custom models without --kv-per-pos: typical GQA mid-size (Qwen3-30B class)
23
+ ETA_KV = 0.70 # KV-read efficiency. Single-point calibration: measured tg32 d0->d16384
24
+ # 20.02 -> 16.12 on Qwen3-30B (+12.1 ms/token = 133 GB/s effective on the
25
+ # 192 GB/s tier). Falsify/refine it: quantprobe bench --depth N --contribute
26
+ # eta values: MEASURED on 2016-xmp/2016 (my box); ESTIMATED for the rest (help validate: run `quantprobe bench`).
27
+ # Mac uses unified memory -> modeled as one high-bandwidth pool the GPU serves from (the "all in VRAM" path).
28
+ MACHINES = {
29
+ # --- measured on my hardware ---
30
+ "2016-xmp": dict(vc=6, vb=192, rc=16, rb=48, db=0.45, geta=0.35, gl=0.04, hint="2016 desktop (GTX 1060 6GB), XMP on [measured]"),
31
+ "2016": dict(vc=6, vb=192, rc=16, rb=34, db=0.45, geta=0.35, gl=0.04, hint="2016 desktop, XMP off [measured]"),
32
+ # --- estimated: consumer GPUs ---
33
+ "rtx-3060": dict(vc=12, vb=360, rc=32, rb=51, db=3.5, geta=0.5, gl=0.3, hint="RTX 3060 12GB + DDR4-3200 [est]"),
34
+ "rtx-3090": dict(vc=24, vb=936, rc=64, rb=51, db=3.5, geta=0.6, gl=0.4, hint="RTX 3090 24GB + DDR4 [est]"),
35
+ "rtx-4090": dict(vc=24, vb=1008, rc=64, rb=83, db=5, geta=0.62, gl=0.42, hint="RTX 4090 24GB + DDR5 [est]"),
36
+ "rtx-5090": dict(vc=32, vb=1792, rc=64, rb=90, db=5, geta=0.62, gl=0.42, hint="RTX 5090 32GB + DDR5 [est]"),
37
+ "laptop-8gb": dict(vc=8, vb=256, rc=16, rb=45, db=2, geta=0.45, gl=0.28, hint="gaming laptop, 8GB GPU + DDR5 [est]"),
38
+ "gaming": dict(vc=12, vb=360, rc=32, rb=51, db=3.5, geta=0.5, gl=0.3, hint="RTX 3060 12GB + DDR4-3200 [est] (alias of rtx-3060)"),
39
+ # --- estimated: Apple Silicon (unified memory; I have NOT measured a Mac - these are predictions) ---
40
+ "mac-m2-max": dict(vc=64, vb=400, rc=8, rb=400, db=5, geta=0.26, gl=0.24, hint="Mac M2 Max, 400 GB/s unified [est, unvalidated]"),
41
+ "mac-m3-max": dict(vc=96, vb=400, rc=8, rb=400, db=5, geta=0.26, gl=0.24, hint="Mac M3 Max, 400 GB/s unified [est, unvalidated]"),
42
+ "mac-m4-max": dict(vc=128, vb=546, rc=8, rb=546, db=5, geta=0.26, gl=0.24, hint="Mac M4 Max, 546 GB/s unified [est, unvalidated]"),
43
+ "mac-m2-ultra":dict(vc=192, vb=800, rc=8, rb=800, db=5, geta=0.25, gl=0.23, hint="Mac M2 Ultra, 800 GB/s unified [est, unvalidated]"),
44
+ "mac-m3-ultra":dict(vc=512, vb=819, rc=8, rb=819, db=5, geta=0.25, gl=0.23, hint="Mac M3 Ultra 512GB, 819 GB/s [est, unvalidated]"),
45
+ # --- estimated: big-RAM / server ---
46
+ "ddr5": dict(vc=0, vb=0, rc=64, rb=80, db=5, geta=0.5, gl=0.3, hint="modern desktop DDR5, no GPU [est]"),
47
+ "colibri": dict(vc=0, vb=0, rc=128, rb=60, db=5, geta=0.5, gl=0.3, hint="128 GB DDR5 workstation [est]"),
48
+ "epyc-256": dict(vc=0, vb=0, rc=256, rb=200, db=5, geta=0.5, gl=0.3, hint="Epyc/Threadripper, 256GB, ~200 GB/s [est]"),
49
+ "dgx-spark": dict(vc=128,vb=273, rc=8, rb=273, db=5, geta=0.79, gl=0.6, hint="NVIDIA DGX Spark / GB10, 128GB unified [validated vs published]"),
50
+ }
51
+ def numlist(x):
52
+ """CLI type: '24,24' -> [24.0, 24.0]; '24' -> 24.0. Lists = multiple devices."""
53
+ if isinstance(x, (int, float)):
54
+ return float(x)
55
+ if "," in str(x):
56
+ return [float(v) for v in str(x).split(",") if v.strip()]
57
+ return float(x)
58
+
59
+
60
+ def agg_cap(v):
61
+ return sum(v) if isinstance(v, list) else v
62
+
63
+
64
+ def agg_bw(v, eff):
65
+ """Aggregate bandwidth of multiple devices: sum x efficiency (TP loss for GPUs [est],
66
+ stripe loss for disks [est from the RAID-0 Gen5 datapoint, eta 0.66 vs single ~0.88])."""
67
+ if isinstance(v, list):
68
+ return sum(v) * (eff if len(v) > 1 else 1.0)
69
+ return v
70
+
71
+
72
+ QUAL = {True: {2.0: 1.10, 2.5: 1.07, 3.0: 1.05, 4.5: 1.02, 6.5: 1.01, 8.5: 1.00},
73
+ False: {2.0: 1.45, 2.5: 1.30, 3.0: 1.12, 4.5: 1.03, 6.5: 1.01, 8.5: 1.00}}
74
+
75
+
76
+ def evaluate(t, a, ne, moe, bits, vc, vb, rc, rb, db, geta, act_scale=1.0, gl=None, ctx=0, kvp=0.0):
77
+ ab = max(bits, 4.5) # attention protected at ~4-bit (Law 3 recipes)
78
+ size = (ne * ab / 8 + (t - ne) * bits / 8) * 1.08 * act_scale
79
+ act_ne = ne * ab / 8 * 1.15 * act_scale
80
+ act_ex = (a - ne) * bits / 8 * 1.15 * act_scale
81
+ act = act_ne + act_ex
82
+ # Law 4 v2 (context term, v1.1): every generated token re-reads the whole KV cache from
83
+ # whichever tier KV lives on — kv_gb adds to BOTH the byte budget and that tier's capacity.
84
+ kv_gb = ctx * kvp / 1e9 if ctx > 0 else 0.0
85
+ ra = max(rc - 4, 1)
86
+ eta_r = 0.38 if moe else 0.62
87
+ if gl is None: gl = geta * 0.6
88
+ geta_w = geta if bits >= 4 else gl # decode-util law: low-bit GPU decode collapses on weak GPUs
89
+ out = []
90
+ if vc > 0 and size + kv_gb <= vc * 0.90:
91
+ out.append(("all in VRAM", 1 / (act / (geta_w * vb) + kv_gb / (ETA_KV * vb)), None,
92
+ "-ngl 99"))
93
+ if moe and vc > 0:
94
+ v_need = ne * ab / 8 * 1.08 + 1.2 + kv_gb # KV sits with attention in VRAM
95
+ r_need = size - ne * ab / 8 * 1.08
96
+ if v_need <= vc * 0.95 and r_need <= ra:
97
+ warn = "RAM boundary - needs --no-mmap; can be unstable" if r_need > ra * 0.85 else None
98
+ out.append(("hybrid: attention->VRAM, experts->RAM",
99
+ 1 / (act_ne / (geta * vb) + act_ex / (eta_r * rb) + kv_gb / (ETA_KV * vb)), warn,
100
+ '-ngl 99 -ot "exps=CPU" --no-mmap'))
101
+ if (not moe) and vc > 0 and size + kv_gb > vc * 0.90 and size + kv_gb <= ra + vc * 0.9:
102
+ g = min(0.95, vc * 0.9 / (size + kv_gb)) # KV splits with its layers
103
+ kv_t = g * kv_gb / (ETA_KV * vb) + (1 - g) * kv_gb / (ETA_KV * rb)
104
+ out.append((f"split: {g:.0%} layers->VRAM, rest->RAM",
105
+ 1 / (g * act / (geta_w * vb) + (1 - g) * act / (eta_r * rb) + kv_t), None,
106
+ f"-ngl {int(g * 99)}"))
107
+ if size + kv_gb <= ra:
108
+ warn = "RAM boundary - expect bimodal speed" if size + kv_gb > ra * 0.85 else None
109
+ out.append(("pure CPU (GPU idle)", 1 / (act / (eta_r * rb) + kv_gb / (ETA_KV * rb)), warn, "-ngl 0"))
110
+ if size + kv_gb > ra:
111
+ ra_eff = max(ra - kv_gb, 1) # KV crowds the expert cache
112
+ miss = max(0.0, 1 - (ra_eff * 0.9) / size)
113
+ hot = act_ne if moe else 0.0 # MoE attention stays LRU-hot; dense has no hot set
114
+ streamable = act - hot
115
+ tps = 0.95 / (streamable * miss / db + (streamable * (1 - miss) + hot) / (eta_r * rb)
116
+ + kv_gb / (ETA_KV * rb))
117
+ out.append(("stream from disk (cold experts)", tps, "exceeds RAM - capacity demo", "-ngl 0"))
118
+ if moe and vc > 0 and size + kv_gb > ra:
119
+ # three-tier expert cache (VRAM + RAM + disk): what expert-caching runtimes achieve.
120
+ # llama.cpp mainline cannot LRU experts in VRAM - its number is the row above.
121
+ v_res = ne * ab / 8 * 1.08 + 1.2 + kv_gb # attention + KV live in VRAM here
122
+ vfree = max(0.0, vc * 0.90 - v_res)
123
+ if vfree > 0.5:
124
+ cache = ra * 0.9 + vfree
125
+ miss3 = max(0.0, 1 - cache / size)
126
+ hot = act_ne
127
+ streamable = act - hot
128
+ vshare = vfree / cache if cache > 0 else 0.0
129
+ hit = streamable * (1 - miss3)
130
+ tps3 = 0.95 / (streamable * miss3 / db
131
+ + hit * (1 - vshare) / (eta_r * rb)
132
+ + hit * vshare / (geta * vb)
133
+ + hot / (geta * vb)
134
+ + kv_gb / (ETA_KV * vb))
135
+ out.append(("stream from disk (VRAM+RAM expert cache)", tps3,
136
+ "needs an expert-caching runtime (ktransformers/colibri-class) - stock llama.cpp gets the RAM-cache row",
137
+ "-ngl 99 + runtime-managed expert cache"))
138
+ out.sort(key=lambda x: -x[1])
139
+ return size, act, out
140
+
141
+
142
+ def qual_of(moe, bits):
143
+ keys = sorted(QUAL[moe])
144
+ return QUAL[moe][min(keys, key=lambda k: abs(k - bits))]
145
+
146
+
147
+ def run(args):
148
+ from . import spec as specmod
149
+ specmod.apply(args)
150
+ if getattr(args, "bits", None) is None:
151
+ args.bits = 2.5
152
+ m = dict(MODELS[args.model]) if args.model in MODELS else {}
153
+ t = args.total or m.get("t") or 13.0
154
+ a = args.active or m.get("a") or t
155
+ ne = args.always_active or m.get("ne") or (a if a >= t * 0.9 else a * 0.35)
156
+ moe = m.get("moe", a < t * 0.9)
157
+ hw = dict(MACHINES[args.machine]) if args.machine in MACHINES else {}
158
+ if not hw and all(getattr(args, k, None) is None for k in ("vram", "vram_bw", "ram", "ram_bw", "disk_bw")):
159
+ from . import detect as detmod
160
+ auto, _ = detmod.detect()
161
+ hw = dict(vc=auto["vram"], vb=auto["vram_bw"], rc=auto["ram"], rb=auto["ram_bw"],
162
+ db=auto["disk_bw"], geta=auto.get("geta", 0.45), gl=auto.get("gl"),
163
+ hint="THIS machine [auto-detected - run `quantprobe hw` for details]")
164
+ print("[quantprobe] no hardware flags: auto-detected this machine "
165
+ f"(vram {hw['vc']:g}GB@{hw['vb']:g} | ram {hw['rc']:g}GB@{hw['rb']:g} | disk {hw['db']:g} GB/s). "
166
+ "Pass --machine/flags to estimate a different box.")
167
+ vc = hw.get("vc", args.vram); vb = hw.get("vb", args.vram_bw)
168
+ rc = hw.get("rc", args.ram); rb = hw.get("rb", args.ram_bw)
169
+ db = hw.get("db", args.disk_bw); geta = hw.get("geta", 0.45); gl = hw.get("gl", None)
170
+ if args.vram is not None: vc = args.vram
171
+ if args.vram_bw is not None: vb = args.vram_bw
172
+ if args.ram is not None: rc = args.ram
173
+ if args.ram_bw is not None: rb = args.ram_bw
174
+ if args.disk_bw is not None: db = args.disk_bw
175
+ vc = agg_cap(vc) or 0; vb = agg_bw(vb, 0.85) or 0
176
+ rc = rc or 16; rb = rb or 40
177
+ db = agg_bw(db, 0.75) or 0.5
178
+ ctx = getattr(args, "ctx", 0) or 0
179
+ kvp = (args.kv_per_pos * 1024 if getattr(args, "kv_per_pos", None)
180
+ else m.get("kvp", DEFAULT_KVP))
181
+
182
+ size, act, cfgs = evaluate(t, a, ne, moe, args.bits, vc, vb, rc, rb, db, geta, gl=gl, ctx=ctx, kvp=kvp)
183
+ q = qual_of(moe, args.bits)
184
+ print(f"\nquantprobe plan - {m.get('hint', 'custom model')} @ {args.bits:g}-bit "
185
+ f"on {hw.get('hint', 'custom machine')}")
186
+ kvline = (f" | ctx {ctx}: +{ctx * kvp / 1e9:.2f} GB KV read/token"
187
+ if ctx > 0 else "")
188
+ print(f" model {size:.1f} GB | active {act:.2f} GB/token{kvline} | est. quality cost x{q:.2f} "
189
+ f"(depth-aware recipe)\n")
190
+ for i, (name, tps, warn, flags) in enumerate(cfgs):
191
+ star = "*" if i == 0 else " "
192
+ w = f" [{warn}]" if warn else ""
193
+ print(f" {star} {tps:6.1f} tok/s {name}{w}")
194
+ best = cfgs[0]
195
+ print(f"\n run it: llama-server -m model.gguf {best[3]}")
196
+ # upgrade advisor
197
+ alts = []
198
+ if rb < 40:
199
+ s2, _, c2 = evaluate(t, a, ne, moe, args.bits, vc, vb, rc, 48, db, geta, gl=gl, ctx=ctx, kvp=kvp)
200
+ if c2[0][1] > best[1] * 1.08: alts.append(("enable XMP (free)", c2[0][1]))
201
+ s2, _, c2 = evaluate(t, a, ne, moe, args.bits, vc, vb, rc + 16, rb, db, geta, gl=gl, ctx=ctx, kvp=kvp)
202
+ if c2[0][1] > best[1] * 1.08: alts.append(("+16 GB RAM", c2[0][1]))
203
+ s2, _, c2 = evaluate(t, a, ne, moe, args.bits, vc, vb, rc, rb, 3.5, geta, gl=gl, ctx=ctx, kvp=kvp)
204
+ if c2[0][1] > best[1] * 1.08: alts.append(("NVMe SSD", c2[0][1]))
205
+ if alts:
206
+ print(" upgrade advisor: " + " | ".join(f"{n} -> ~{v:.1f} tok/s" for n, v in alts))
207
+ print("\n (eta bands fitted from published measurements; estimates +/-25%. "
208
+ "Hybrid needs --no-mmap.)")
quantprobe/probe.py ADDED
@@ -0,0 +1,149 @@
1
+ """quantprobe probe — measure a GGUF's depth-fragility curve, emit the depth-aware recipe.
2
+ Adapted from the research script (weights/quant_probe.py); logic identical, llama.cpp located via
3
+ --llama-dir, QUANTPROBE_LLAMA_DIR, or PATH.
4
+ """
5
+ from __future__ import annotations
6
+ import os, re, shutil, subprocess
7
+
8
+
9
+ def find_llama(explicit):
10
+ for cand in ([explicit] if explicit else []) + [os.environ.get("QUANTPROBE_LLAMA_DIR")]:
11
+ if cand and os.path.isfile(os.path.join(cand, exe("llama-quantize"))):
12
+ return cand
13
+ w = shutil.which("llama-quantize") or shutil.which("llama-quantize.exe")
14
+ if w:
15
+ return os.path.dirname(w)
16
+ raise SystemExit("llama.cpp binaries not found: pass --llama-dir, set QUANTPROBE_LLAMA_DIR, or add to PATH")
17
+
18
+
19
+ def exe(name):
20
+ return name + (".exe" if os.name == "nt" else "")
21
+
22
+
23
+ def n_layers(gguf_path):
24
+ from gguf import GGUFReader
25
+ r = GGUFReader(gguf_path)
26
+ for field in r.fields.values():
27
+ if field.name.endswith(".block_count"):
28
+ return int(field.parts[field.data[0]][0])
29
+ raise RuntimeError("no .block_count key in GGUF metadata")
30
+
31
+
32
+ def band_regex(lo, hi):
33
+ return "blk\\.(" + "|".join(str(i) for i in range(lo, hi + 1)) + ")\\.ffn_.*"
34
+
35
+
36
+ def sh(cmd, dry, capture=False):
37
+ print(" $", " ".join(cmd), flush=True)
38
+ if dry:
39
+ return ""
40
+ if capture:
41
+ return subprocess.run(cmd, capture_output=True, text=True).stdout + \
42
+ subprocess.run(cmd, capture_output=True, text=True).stderr if False else \
43
+ subprocess.run(cmd, capture_output=True, text=True, errors="replace").stdout
44
+ subprocess.run(cmd, check=False)
45
+ return ""
46
+
47
+
48
+ def ppl(perp, gguf, eval_file, chunks, ngl, dry):
49
+ print(f" measuring perplexity on {chunks} chunks (this can take 1-3 min; llama.cpp is quiet while it works)...", flush=True)
50
+ if dry:
51
+ return None
52
+ p = subprocess.run([perp, "-m", gguf, "-f", eval_file, "--chunks", str(chunks), "-ngl", str(ngl)],
53
+ capture_output=True, text=True, errors="replace")
54
+ m = re.search(r"Final estimate: PPL = ([0-9.]+)", p.stdout + p.stderr)
55
+ return float(m.group(1)) if m else None
56
+
57
+
58
+ def run(a):
59
+ llama = find_llama(a.llama_dir)
60
+ quant = os.path.join(llama, exe("llama-quantize"))
61
+ perp = os.path.join(llama, exe("llama-perplexity"))
62
+ wd = a.workdir or os.path.dirname(os.path.abspath(a.gguf))
63
+ L = n_layers(a.gguf)
64
+ step = (L + a.bands - 1) // a.bands
65
+ bands = [(i, min(i + step - 1, L - 1)) for i in range(0, L, step)]
66
+ print(f"quant-probe: {os.path.basename(a.gguf)} | {L} layers -> {len(bands)} bands {bands}\n", flush=True)
67
+
68
+ ref = os.path.join(wd, "_probe_ref_q6k.gguf")
69
+ print("[1/3] reference Q6_K", flush=True)
70
+ sh([quant, "--allow-requantize", a.gguf, ref, "Q6_K", "8"], a.dry_run)
71
+ p_ref = ppl(perp, ref, a.eval, a.chunks, a.ngl, a.dry_run)
72
+ print(f" ref PPL = {p_ref}\n", flush=True)
73
+
74
+ print("[2/3] band probe (one band's FFNs -> Q2_K at a time)", flush=True)
75
+ deltas = []
76
+ for lo, hi in bands:
77
+ out = os.path.join(wd, f"_probe_b{lo}_{hi}.gguf")
78
+ sh([quant, "--allow-requantize", "--tensor-type", f"{band_regex(lo, hi)}=q2_k", a.gguf, out, "Q6_K", "8"], a.dry_run)
79
+ p = ppl(perp, out, a.eval, a.chunks, a.ngl, a.dry_run)
80
+ d = None if (p is None or p_ref is None) else p - p_ref
81
+ deltas.append(d)
82
+ print(f" layers {lo}-{hi}: PPL {p} (delta {d})", flush=True)
83
+ if not a.dry_run and os.path.exists(out):
84
+ os.remove(out)
85
+ if not a.dry_run and os.path.exists(ref):
86
+ os.remove(ref)
87
+
88
+ print("\n[3/3] recipe", flush=True)
89
+ if a.dry_run or any(d is None for d in deltas):
90
+ print(" (dry-run / incomplete: curve unavailable)", flush=True)
91
+ return
92
+ worst = max(range(len(bands)), key=lambda i: deltas[i])
93
+ lo, hi = bands[worst]
94
+ others = [f"{band_regex(b[0], b[1])}=q2_k" for i, b in enumerate(bands) if i != worst]
95
+ print(f" fragile band: layers {lo}-{hi} (delta +{deltas[worst]:.2f} vs "
96
+ f"median {sorted(deltas)[len(deltas)//2]:.2f}) -> protect at Q4_K:\n", flush=True)
97
+ flags = " ".join(f'--tensor-type "{o}"' for o in others)
98
+ print(f' llama-quantize {flags} --tensor-type "{band_regex(lo, hi)}=q4_k" '
99
+ f'--tensor-type "attn_.*=q4_k" --token-embedding-type q4_k \\\n'
100
+ f' {os.path.basename(a.gguf)} out-depthaware.gguf Q2_K 8', flush=True)
101
+ if getattr(a, "apply", False):
102
+ out = a.out or os.path.splitext(a.gguf)[0] + "-depthaware.gguf"
103
+ print("\n[quantprobe] --apply: building the recommended GGUF now...", flush=True)
104
+ build_depthaware(a.llama_dir, a.gguf, out, lo, hi, bands[-1][1] + 1, dry=a.dry_run)
105
+ else:
106
+ print("\n (re-run with --apply --out model-2bit.gguf to BUILD this GGUF automatically)", flush=True)
107
+
108
+
109
+ def _band_re(lo, hi):
110
+ return "blk\\.(" + "|".join(str(i) for i in range(lo, hi + 1)) + ")\\.ffn_.*"
111
+
112
+
113
+ def build_depthaware(llama_dir, src, out, protect_lo, protect_hi, n_lay,
114
+ base="Q2_K", protect="q4_k", dry=False):
115
+ """Actually PRODUCE the compressed GGUF: base bits everywhere, fragile band + attention + embed protected."""
116
+ # --dry previews the exact command WITHOUT requiring llama.cpp installed
117
+ q = exe("llama-quantize") if dry else os.path.join(find_llama(llama_dir), exe("llama-quantize"))
118
+ cmd = [q, "--allow-requantize"]
119
+ if protect_lo > 0:
120
+ cmd += ["--tensor-type", f"{_band_re(0, protect_lo - 1)}=q2_k"]
121
+ if protect_hi < n_lay - 1:
122
+ cmd += ["--tensor-type", f"{_band_re(protect_hi + 1, n_lay - 1)}=q2_k"]
123
+ cmd += ["--tensor-type", f"{_band_re(protect_lo, protect_hi)}={protect}",
124
+ "--tensor-type", "attn_.*=q4_k", "--token-embedding-type", "q4_k",
125
+ src, out, base, "8"]
126
+ print(f"[quantprobe] building depth-aware GGUF: protect layers {protect_lo}-{protect_hi} @ {protect}")
127
+ print(" $ " + " ".join(cmd))
128
+ if dry:
129
+ return out
130
+ rc = subprocess.call(cmd)
131
+ if rc == 0 and os.path.exists(out):
132
+ print(f"[quantprobe] done -> {out} ({os.path.getsize(out)/1e9:.2f} GB). "
133
+ f"Run it: quantprobe run --gguf {out} --model <preset> --machine <preset>")
134
+ else:
135
+ print(f"[quantprobe] quantize failed (exit {rc}).")
136
+ return out
137
+
138
+
139
+ def quantize(a):
140
+ """Standalone compress: build a depth-aware GGUF from an explicit band (no probing)."""
141
+ if not os.path.isfile(a.gguf):
142
+ raise SystemExit(f"GGUF not found: {a.gguf} (point --gguf at a real high-precision GGUF: f16/bf16/Q8)")
143
+ n_lay = n_layers(a.gguf)
144
+ if a.protect:
145
+ lo, hi = (int(x) for x in a.protect.split("-"))
146
+ else:
147
+ lo, hi = n_lay - a.protect_late, n_lay - 1
148
+ out = a.out or os.path.splitext(a.gguf)[0] + "-depthaware.gguf"
149
+ build_depthaware(a.llama_dir, a.gguf, out, lo, hi, n_lay, dry=getattr(a, "dry", False))
quantprobe/runtime.py ADDED
@@ -0,0 +1,187 @@
1
+ """quantprobe run / bench — the runtime layer.
2
+
3
+ run: plan the best placement for your model+machine, then LAUNCH llama.cpp with those exact
4
+ flags (chat via llama-cli, or --serve for llama-server). Colibri-style one-command UX,
5
+ riding stock llama.cpp instead of a custom engine.
6
+ bench: measure real decode tok/s with the planned flags and print predicted vs measured —
7
+ every user becomes a validation point for the tiered decode law.
8
+ """
9
+ from __future__ import annotations
10
+ import os, re, shutil, subprocess, sys
11
+
12
+ from . import plan as planmod
13
+
14
+
15
+ def exe(name):
16
+ return name + (".exe" if os.name == "nt" else "")
17
+
18
+
19
+ def find_llama(explicit, tool):
20
+ for cand in ([explicit] if explicit else []) + [os.environ.get("QUANTPROBE_LLAMA_DIR")]:
21
+ if cand and os.path.isfile(os.path.join(cand, exe(tool))):
22
+ return os.path.join(cand, exe(tool))
23
+ w = shutil.which(tool) or shutil.which(exe(tool))
24
+ if w:
25
+ return w
26
+ raise SystemExit(f"{tool} not found: pass --llama-dir, set QUANTPROBE_LLAMA_DIR, or add to PATH")
27
+
28
+
29
+ def best_flags(a):
30
+ """Run the planner, return (best_config, flags_list) for the winning placement."""
31
+ from . import spec as specmod
32
+ specmod.apply(a)
33
+ if getattr(a, "bits", None) is None:
34
+ a.bits = 2.5
35
+ m = dict(planmod.MODELS[a.model]) if getattr(a, "model", None) in planmod.MODELS else {}
36
+ t = getattr(a, "total", None) or m.get("t") or 13.0
37
+ ac = getattr(a, "active", None) or m.get("a") or t
38
+ ne = getattr(a, "always_active", None) or m.get("ne") or (ac if ac >= t * 0.9 else ac * 0.35)
39
+ moe = m.get("moe", ac < t * 0.9)
40
+ hw = dict(planmod.MACHINES[a.machine]) if getattr(a, "machine", None) in planmod.MACHINES else {}
41
+ if not hw and all(getattr(a, k, None) is None for k in ("vram", "vram_bw", "ram", "ram_bw", "disk_bw")):
42
+ from . import detect as detmod
43
+ auto, _ = detmod.detect()
44
+ hw = dict(vc=auto["vram"], vb=auto["vram_bw"], rc=auto["ram"], rb=auto["ram_bw"],
45
+ db=auto["disk_bw"], geta=auto.get("geta", 0.45), gl=auto.get("gl"))
46
+ print("[quantprobe] hardware auto-detected (run `quantprobe hw` for details; "
47
+ "pass --machine/flags to estimate a different box)")
48
+ vc = planmod.agg_cap(a.vram) if a.vram is not None else hw.get("vc", 0)
49
+ vb = planmod.agg_bw(a.vram_bw, 0.85) if a.vram_bw is not None else hw.get("vb", 0)
50
+ rc = a.ram if a.ram is not None else hw.get("rc", 16)
51
+ rb = a.ram_bw if a.ram_bw is not None else hw.get("rb", 40)
52
+ db = planmod.agg_bw(a.disk_bw, 0.75) if a.disk_bw is not None else hw.get("db", 0.5)
53
+ geta = hw.get("geta", 0.45); gl = hw.get("gl", None)
54
+ act_scale = 1.0
55
+ gguf = getattr(a, "gguf", None)
56
+ if gguf and os.path.isfile(gguf):
57
+ ab = max(a.bits, 4.5)
58
+ size_pred = (ne * ab / 8 + (t - ne) * a.bits / 8) * 1.08
59
+ size_real = os.path.getsize(gguf) / 1e9
60
+ if size_pred > 0:
61
+ act_scale = size_real / size_pred
62
+ print(f"[quantprobe] calibrated to file: {size_real:.2f} GB on disk "
63
+ f"(preset assumed {size_pred:.2f} GB, scale {act_scale:.2f})")
64
+ ctx = getattr(a, "ctx", 0) or 0
65
+ kvp = (a.kv_per_pos * 1024 if getattr(a, "kv_per_pos", None)
66
+ else m.get("kvp", planmod.DEFAULT_KVP))
67
+ _, _, cfgs = planmod.evaluate(t, ac, ne, moe, a.bits, vc, vb, rc, rb, db, geta, act_scale, gl,
68
+ ctx=ctx, kvp=kvp)
69
+ best = cfgs[0]
70
+ return best, best[3].replace('"', "").split()
71
+
72
+
73
+ def run(a):
74
+ best, flags = best_flags(a)
75
+ tool = "llama-server" if a.serve else "llama-cli"
76
+ # --dry previews the plan + command WITHOUT requiring llama.cpp installed
77
+ binp = tool if a.dry else find_llama(a.llama_dir, tool)
78
+ cmd = [binp, "-m", a.gguf] + flags
79
+ if (getattr(a, "ctx", 0) or 0) > 0:
80
+ cmd += ["-c", str(a.ctx)] # launch with the context you planned for
81
+ if not a.serve:
82
+ cmd += ["-cnv"]
83
+ if a.extra:
84
+ cmd += a.extra.split()
85
+ print(f"[quantprobe] placement: {best[0]} (predicted {best[1]:.1f} tok/s"
86
+ + (f", {best[2]}" if best[2] else "") + ")")
87
+ print("[quantprobe] exec:", " ".join(cmd), "\n")
88
+ if a.dry:
89
+ return
90
+ sys.exit(subprocess.call(cmd))
91
+
92
+
93
+ def bench(a):
94
+ if getattr(a, "depth", None):
95
+ a.ctx = a.depth # prediction at the benched depth
96
+ best, flags = best_flags(a)
97
+ binp = "llama-bench" if getattr(a, "dry", False) else find_llama(a.llama_dir, "llama-bench")
98
+ # llama-bench uses --mmap 0 rather than --no-mmap
99
+ bflags = ["--mmap", "0" if "--no-mmap" in flags else "1"]
100
+ for i, f in enumerate(flags):
101
+ if f == "-ngl":
102
+ bflags += ["-ngl", flags[i + 1]]
103
+ if f == "-ot":
104
+ bflags += ["-ot", flags[i + 1]]
105
+ if flags and flags[0].startswith("-ngl") is False and "-ngl" not in flags:
106
+ pass
107
+ # normalize: flags like ['-ngl','99','-ot','exps=CPU','--no-mmap']
108
+ cmd = [binp, "-m", a.gguf, "-n", "32", "-p", "0", "-r", str(a.reps)] + bflags
109
+ if getattr(a, "depth", None):
110
+ cmd += ["-d", str(a.depth)]
111
+ print(f"[quantprobe] placement: {best[0]} | predicted {best[1]:.1f} tok/s")
112
+ print("[quantprobe] bench:", " ".join(cmd))
113
+ if a.dry:
114
+ return
115
+ print("[quantprobe] benchmarking (30-90s; llama-bench runs quietly, then prints the number)...", flush=True)
116
+ out = subprocess.run(cmd, capture_output=True, text=True, errors="replace")
117
+ txt = out.stdout + out.stderr
118
+ mm = re.findall(r"tg\d+(?:\s*@\s*d\d+)?\s*\|\s*([0-9.]+)\s*(?:Â?±|\+/-)\s*([0-9.]+)", txt)
119
+ if not mm:
120
+ mm = re.findall(r"\|\s*([0-9.]+)\s*(?:Â?±)\s*([0-9.]+)\s*\|\s*$", txt, re.M)
121
+ if mm:
122
+ meas, err = float(mm[-1][0]), float(mm[-1][1])
123
+ delta = (meas / best[1] - 1) * 100 if best[1] else 0
124
+ print(f"\n[quantprobe] measured: {meas:.2f} +/- {err:.2f} tok/s "
125
+ f"(predicted {best[1]:.1f}, {delta:+.0f}%)")
126
+ if getattr(a, "contribute", False):
127
+ _emit_contribution(a, best, meas, err, delta)
128
+ else:
129
+ print("[quantprobe] the tiered decode law just ran on your machine.")
130
+ print("[quantprobe] help grow the law: re-run with --contribute for a one-click, "
131
+ "pre-filled data point (you review it first; nothing is sent automatically).")
132
+ else:
133
+ print("\n[quantprobe] could not parse llama-bench output; raw tail:")
134
+ print("\n".join(txt.strip().splitlines()[-6:]))
135
+
136
+
137
+ def tier_view(a, best):
138
+ """Rough (capacity, used) per tier for the dashboard's placement panel."""
139
+ hw = dict(planmod.MACHINES[a.machine]) if getattr(a, "machine", None) in planmod.MACHINES else {}
140
+ if not hw and all(getattr(a, k, None) is None for k in ("vram", "vram_bw", "ram", "ram_bw", "disk_bw")):
141
+ from . import detect as detmod
142
+ auto, _ = detmod.detect()
143
+ hw = dict(vc=auto["vram"], vb=auto["vram_bw"], rc=auto["ram"], rb=auto["ram_bw"],
144
+ db=auto["disk_bw"], geta=auto.get("geta", 0.45), gl=auto.get("gl"))
145
+ print("[quantprobe] hardware auto-detected (run `quantprobe hw` for details; "
146
+ "pass --machine/flags to estimate a different box)")
147
+ vc = planmod.agg_cap(a.vram) if a.vram is not None else hw.get("vc", 0)
148
+ rc = a.ram if a.ram is not None else hw.get("rc", 16)
149
+ size = os.path.getsize(a.gguf) / 1e9 if a.gguf and os.path.isfile(a.gguf) else 0
150
+ name = best[0]
151
+ if name == "all in VRAM":
152
+ return [("VRAM", vc, size), ("RAM", rc, 1.0)]
153
+ if name.startswith("hybrid"):
154
+ v = min(size * 0.15 + 1.2, vc)
155
+ return [("VRAM (attention + ctx)", vc, v), ("RAM (experts)", rc, size - size * 0.15)]
156
+ if name.startswith("split"):
157
+ return [("VRAM", vc, vc * 0.9), ("RAM", rc, max(0.5, size - vc * 0.9))]
158
+ if name.startswith("pure CPU"):
159
+ return [("VRAM (idle)", vc, 0), ("RAM", rc, size)]
160
+ return [("RAM (cache)", rc, rc - 4), ("disk (streaming)", max(size * 1.2, 1), size)]
161
+
162
+
163
+ def _emit_contribution(a, best, meas, err, delta):
164
+ import urllib.parse
165
+ from . import __version__
166
+ hw = (dict(planmod.MACHINES.get(getattr(a, "machine", "") or "", {})).get("hint")
167
+ or f"vram={a.vram} vram_bw={a.vram_bw} ram={a.ram} ram_bw={a.ram_bw} disk_bw={a.disk_bw}")
168
+ model = getattr(a, "model", None) or f"total={a.total} active={a.active}"
169
+ lines = [
170
+ f"hardware: {hw}",
171
+ f"model: {model} @ {a.bits:g}-bit",
172
+ f"placement: {best[0]}",
173
+ f"predicted: {best[1]:.1f} tok/s",
174
+ f"measured: {meas:.2f} +/- {err:.2f} tok/s ({delta:+.0f}%)",
175
+ f"quantprobe: v{__version__}",
176
+ "",
177
+ "Notes (optional): ",
178
+ ]
179
+ body = "\n".join(lines)
180
+ title = f"[eta] {model} {a.bits:g}-bit on {str(hw)[:40]}"
181
+ url = ("https://github.com/FedericoTs/quantprobe/issues/new?labels=eta-datapoint"
182
+ f"&title={urllib.parse.quote(title)}&body={urllib.parse.quote(body)}")
183
+ print("\n[quantprobe] Contribute this data point (OPT-IN). It contains ONLY what you see below --")
184
+ print(" no system scan, no IP, nothing auto-collected. Review, then submit:\n")
185
+ print(body)
186
+ print("\n Open to submit (you can edit first):\n " + url + "\n")
187
+ print(" Points that land OUTSIDE the predicted bands are the most valuable -- they refine the law.")
quantprobe/spec.py ADDED
@@ -0,0 +1,89 @@
1
+ """quantprobe autospec — read the MODEL's law-parameters from the GGUF itself.
2
+
3
+ A GGUF already contains everything the decode law needs:
4
+ total params = sum of tensor element counts
5
+ routed/active = expert tensor split + expert_used/expert_count from metadata
6
+ effective bits = file bytes x 8 / total params (the real number, not the type name)
7
+ kv bytes/pos = exact, from layer count x KV heads x head dims (MLA-aware)
8
+ So `--gguf model.gguf` alone fully specifies the model; flags remain as overrides.
9
+ """
10
+ from __future__ import annotations
11
+ import os
12
+
13
+
14
+ def _field(r, *names):
15
+ for f in r.fields.values():
16
+ for n in names:
17
+ if f.name.endswith(n):
18
+ try:
19
+ return int(f.parts[f.data[0]][0])
20
+ except Exception:
21
+ pass
22
+ return None
23
+
24
+
25
+ def from_gguf(path):
26
+ from gguf import GGUFReader
27
+ r = GGUFReader(path)
28
+ n_layer = _field(r, ".block_count") or 32
29
+ total = 0
30
+ routed = 0
31
+ for t in r.tensors:
32
+ n = 1
33
+ for d in t.shape:
34
+ n *= int(d)
35
+ total += n
36
+ if "exps" in t.name or "_expert" in t.name:
37
+ routed += n
38
+ ne_params = total - routed
39
+
40
+ n_exp = _field(r, ".expert_count")
41
+ n_used = _field(r, ".expert_used_count")
42
+ if routed and n_exp and n_used:
43
+ active = ne_params + routed * n_used / n_exp
44
+ moe = True
45
+ else:
46
+ active, moe = total, False
47
+
48
+ # exact KV bytes/pos (f16): MLA caches the latent; GQA caches heads x dims, K+V
49
+ kv_lora = _field(r, ".attention.kv_lora_rank")
50
+ if kv_lora:
51
+ rope = _field(r, ".rope.dimension_count") or 64
52
+ kvp = n_layer * (kv_lora + rope) * 2
53
+ else:
54
+ kv_heads = _field(r, ".attention.head_count_kv") or 8
55
+ k_dim = _field(r, ".attention.key_length")
56
+ v_dim = _field(r, ".attention.value_length")
57
+ if not k_dim:
58
+ emb = _field(r, ".embedding_length") or 4096
59
+ heads = _field(r, ".attention.head_count") or 32
60
+ k_dim = v_dim = emb // heads
61
+ kvp = n_layer * kv_heads * ((k_dim or 128) + (v_dim or k_dim or 128)) * 2
62
+
63
+ bits = os.path.getsize(path) * 8 / total
64
+ return dict(t=total / 1e9, a=active / 1e9, ne=ne_params / 1e9, moe=moe,
65
+ bits=round(bits, 2), kvp=int(kvp), n_layer=n_layer)
66
+
67
+
68
+ def apply(a, quiet=False):
69
+ """Fill law-parameters from a.gguf for anything the user didn't set. Explicit flags win."""
70
+ g = getattr(a, "gguf", None)
71
+ if not g or not os.path.isfile(g):
72
+ return False
73
+ try:
74
+ s = from_gguf(g)
75
+ except Exception as e:
76
+ if not quiet:
77
+ print(f"[quantprobe] autospec skipped ({e}); using flags/presets")
78
+ return False
79
+ used = []
80
+ if getattr(a, "total", None) is None and getattr(a, "model", None) is None:
81
+ a.total = s["t"]; a.active = a.active or s["a"]; a.always_active = a.always_active or s["ne"]
82
+ used.append(f"{s['t']:.1f}B total, {s['a']:.1f}B active")
83
+ if getattr(a, "bits", None) is None:
84
+ a.bits = s["bits"]; used.append(f"{s['bits']:g} effective bits")
85
+ if getattr(a, "kv_per_pos", None) is None:
86
+ a.kv_per_pos = s["kvp"] / 1024; used.append(f"KV {s['kvp']/1024:.0f} KB/pos")
87
+ if used and not quiet:
88
+ print(f"[quantprobe] read from GGUF: " + ", ".join(used))
89
+ return True