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/__init__.py +11 -0
- quantprobe/cli.py +124 -0
- quantprobe/dashboard.py +179 -0
- quantprobe/detect.py +163 -0
- quantprobe/fetch.py +88 -0
- quantprobe/plan.py +208 -0
- quantprobe/probe.py +149 -0
- quantprobe/runtime.py +187 -0
- quantprobe/spec.py +89 -0
- quantprobe/target.py +88 -0
- quantprobe-1.3.0.dist-info/METADATA +296 -0
- quantprobe-1.3.0.dist-info/RECORD +16 -0
- quantprobe-1.3.0.dist-info/WHEEL +5 -0
- quantprobe-1.3.0.dist-info/entry_points.txt +2 -0
- quantprobe-1.3.0.dist-info/licenses/LICENSE +21 -0
- quantprobe-1.3.0.dist-info/top_level.txt +1 -0
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
|