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 ADDED
@@ -0,0 +1,11 @@
1
+ """quantprobe — probe-then-quantize for LLMs on commodity hardware.
2
+
3
+ Three commands, all grounded in measured laws (see LAWS.md in the repo):
4
+ quantprobe probe — measure a GGUF's depth-fragility curve, emit the depth-aware recipe (Law 3)
5
+ quantprobe plan — evaluate every bit/tier placement for a model on your machine, predict tok/s (Law 4)
6
+ quantprobe fetch — robust HuggingFace downloader (Range-resume, retry)
7
+
8
+ Every constant in `plan` is fitted from measurements published in the repo, including two
9
+ pre-registered hardware predictions confirmed within 8%.
10
+ """
11
+ __version__ = "1.3.0"
quantprobe/cli.py ADDED
@@ -0,0 +1,124 @@
1
+ """quantprobe CLI — probe / plan / fetch."""
2
+ from __future__ import annotations
3
+ import argparse
4
+
5
+ from .plan import numlist
6
+
7
+
8
+ def main():
9
+ import sys
10
+ try:
11
+ sys.stdout.reconfigure(errors="replace")
12
+ except Exception:
13
+ pass
14
+ ap = argparse.ArgumentParser(
15
+ prog="quantprobe",
16
+ description="Probe-then-quantize for LLMs: fragility curves, placement plans, recipes. "
17
+ "Laws + evidence: github.com/FedericoTs/quantprobe")
18
+ sub = ap.add_subparsers(dest="cmd", required=True)
19
+
20
+ p = sub.add_parser("probe", help="measure a GGUF's depth-fragility curve, emit the recipe (Law 3)")
21
+ p.add_argument("--gguf", required=True, help="f16/bf16 (or high-precision) source GGUF")
22
+ p.add_argument("--bands", type=int, default=4)
23
+ p.add_argument("--chunks", type=int, default=32)
24
+ p.add_argument("--eval", required=True, help="raw text eval file (e.g. wikitext-2 test)")
25
+ p.add_argument("--ngl", type=int, default=99, help="GPU layers for perplexity (0 for CPU)")
26
+ p.add_argument("--workdir", default=None)
27
+ p.add_argument("--llama-dir", default=None, help="dir containing llama-quantize/llama-perplexity")
28
+ p.add_argument("--apply", action="store_true", help="after probing, BUILD the recommended depth-aware GGUF")
29
+ p.add_argument("--out", default=None, help="output path for --apply (default: <model>-depthaware.gguf)")
30
+ p.add_argument("--dry-run", action="store_true")
31
+
32
+ q = sub.add_parser("plan", help="evaluate every bit/tier placement, predict tok/s (Law 4)")
33
+ q.add_argument("--model", default=None, help="preset: qwen3-30b deepseek-16b gemma-12b mistral-7b glm-air glm-744b")
34
+ q.add_argument("--machine", default=None, help="preset: 2016-xmp 2016 gaming ddr5 colibri")
35
+ q.add_argument("--bits", type=float, default=None, help="effective bits/weight (default: read from --gguf, else 2.5)")
36
+ q.add_argument("--total", type=float, help="total params (B)")
37
+ q.add_argument("--active", type=float, help="active params per token (B)")
38
+ q.add_argument("--always-active", type=float, help="always-active (attention/embed) params (B)")
39
+ q.add_argument("--vram", type=numlist, help="GB; comma-list for multi-GPU: 24,24"); q.add_argument("--vram-bw", type=numlist, help="GB/s; comma-list aggregates x0.85")
40
+ q.add_argument("--ram", type=float); q.add_argument("--ram-bw", type=float)
41
+ q.add_argument("--disk-bw", type=numlist, help="GB/s; comma-list (RAID) aggregates x0.75")
42
+ q.add_argument("--gguf", default=None, help="optional: read total/active/bits/KV exactly from this GGUF")
43
+ q.add_argument("--ctx", type=int, default=0, help="context depth: adds KV reads/token + KV memory to the prediction (Law 4 v2)")
44
+ q.add_argument("--kv-per-pos", type=float, default=None, help="KV bytes per position in KB (default: model preset, or 96)")
45
+
46
+ def hwargs(sp):
47
+ sp.add_argument("--model", default=None); sp.add_argument("--machine", default=None)
48
+ sp.add_argument("--bits", type=float, default=None, help="effective bits/weight (default: read from --gguf, else 2.5)")
49
+ sp.add_argument("--total", type=float); sp.add_argument("--active", type=float)
50
+ sp.add_argument("--always-active", type=float)
51
+ sp.add_argument("--vram", type=numlist); sp.add_argument("--vram-bw", type=numlist)
52
+ sp.add_argument("--ram", type=float); sp.add_argument("--ram-bw", type=float)
53
+ sp.add_argument("--disk-bw", type=numlist)
54
+ sp.add_argument("--ctx", type=int, default=0, help="context depth for the prediction (Law 4 v2)")
55
+ sp.add_argument("--kv-per-pos", type=float, default=None, help="KV bytes per position in KB")
56
+ sp.add_argument("--llama-dir", default=None); sp.add_argument("--dry", action="store_true")
57
+
58
+ r = sub.add_parser("run", help="plan the best placement, then launch llama.cpp chat with those flags")
59
+ r.add_argument("--gguf", required=True); hwargs(r)
60
+ r.add_argument("--serve", action="store_true", help="launch llama-server instead of interactive chat")
61
+ r.add_argument("--extra", default=None, help="extra flags passed through to llama.cpp")
62
+
63
+ b = sub.add_parser("bench", help="measure real tok/s with the planned flags; print predicted vs measured")
64
+ b.add_argument("--gguf", required=True); hwargs(b)
65
+ b.add_argument("--reps", type=int, default=3)
66
+ b.add_argument("--depth", type=int, default=None, help="bench at KV depth N (llama-bench -d): measures the Law 4 v2 context term on YOUR box")
67
+ b.add_argument("--contribute", action="store_true", help="print a pre-filled, opt-in GitHub issue with your predicted-vs-measured point (you review before submitting; nothing auto-sent)")
68
+
69
+ d = sub.add_parser("dashboard", help="launch llama-server with planned flags + a live predicted-vs-measured chat page")
70
+ d.add_argument("--gguf", required=True); hwargs(d)
71
+ d.add_argument("--port", type=int, default=8077); d.add_argument("--server-port", type=int, default=8090)
72
+ d.add_argument("--no-open", action="store_true")
73
+
74
+ g = sub.add_parser("target", help="INVERSE planning: give a tok/s target, get the smartest feasible model + the speed-intelligence ladder")
75
+ g.add_argument("--tps", type=float, required=True, help="minimum tok/s you need")
76
+ hwargs(g)
77
+ g.add_argument("--ladder", action="store_true", help="print the full speed-vs-intelligence ladder")
78
+
79
+ z = sub.add_parser("quantize", help="COMPRESS: build a depth-aware GGUF from a model (protect the fragile band, rest 2-bit)")
80
+ z.add_argument("--gguf", required=True, help="high-precision source GGUF (f16/bf16/Q8)")
81
+ z.add_argument("--out", default=None, help="output path (default: <model>-depthaware.gguf)")
82
+ z.add_argument("--protect-late", type=int, default=12, help="protect the last N layers at 4-bit (default 12)")
83
+ z.add_argument("--protect", default=None, help="protect an explicit band LO-HI (e.g. 36-47) instead of --protect-late")
84
+ z.add_argument("--llama-dir", default=None)
85
+ z.add_argument("--dry", action="store_true")
86
+
87
+ hwp = sub.add_parser("hw", help="detect THIS machine's memory tiers (no flags needed); every value tagged with its source")
88
+ hwp.add_argument("--measure", default=None, metavar="FILE", help="also MEASURE sequential disk read on a real file (e.g. a GGUF)")
89
+
90
+ f = sub.add_parser("fetch", help="robust HF download (Range-resume, retry)")
91
+ f.add_argument("repo", help="HF repo, or a preset: qwen3-30b, glm-air, deepseek-16b, qwen3-0.6b"); f.add_argument("dest"); f.add_argument("files", nargs="*")
92
+
93
+ a = ap.parse_args()
94
+ if a.cmd == "probe":
95
+ from . import probe
96
+ probe.run(a)
97
+ elif a.cmd == "plan":
98
+ from . import plan
99
+ plan.run(a)
100
+ elif a.cmd == "run":
101
+ from . import runtime
102
+ runtime.run(a)
103
+ elif a.cmd == "bench":
104
+ from . import runtime
105
+ runtime.bench(a)
106
+ elif a.cmd == "dashboard":
107
+ from . import dashboard
108
+ dashboard.dashboard(a)
109
+ elif a.cmd == "target":
110
+ from . import target
111
+ target.run(a)
112
+ elif a.cmd == "hw":
113
+ from . import detect
114
+ detect.run(a)
115
+ elif a.cmd == "quantize":
116
+ from . import probe
117
+ probe.quantize(a)
118
+ else:
119
+ from . import fetch
120
+ fetch.run(a)
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
@@ -0,0 +1,179 @@
1
+ """quantprobe dashboard — the law, live.
2
+
3
+ One command: plans the best placement, launches llama-server with those flags, and serves a local
4
+ page where you chat with the model while every reply is scored against the law's prediction —
5
+ predicted vs measured tok/s, live. Colibri-style dashboard energy, but the star is falsifiability:
6
+ the page is a running experiment, not a status screen.
7
+ """
8
+ from __future__ import annotations
9
+ import json, os, subprocess, sys, threading, time, urllib.request, webbrowser
10
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
11
+
12
+ from . import runtime
13
+
14
+ DASH_HTML = """<!doctype html><html><head><meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width,initial-scale=1">
16
+ <title>quantprobe — the law, live</title>
17
+ <style>
18
+ :root{--g:#fafaf7;--p:#fff;--ink:#16181d;--sub:#5c6066;--line:#e5e4df;--acc:#0f766e;
19
+ --accsoft:#e6f2f0;--warn:#b45309;--mono:Consolas,ui-monospace,monospace}
20
+ @media(prefers-color-scheme:dark){:root{--g:#14161a;--p:#1c1f24;--ink:#e8e7e2;--sub:#9aa0a8;
21
+ --line:#2e3238;--acc:#35b8a6;--accsoft:#12332f;--warn:#e0a458}}
22
+ body{margin:0;background:var(--g);color:var(--ink);font-family:"Segoe UI",system-ui,sans-serif}
23
+ .wrap{max-width:980px;margin:0 auto;padding:22px 16px}
24
+ h1{font-size:21px;margin:0 0 4px}.sub{color:var(--sub);font-size:13px;margin-bottom:16px}
25
+ .law{font-family:var(--mono);font-size:12.5px;color:var(--acc);margin-bottom:14px}
26
+ .cols{display:grid;grid-template-columns:340px 1fr;gap:16px}
27
+ @media(max-width:760px){.cols{grid-template-columns:1fr}}
28
+ .panel{background:var(--p);border:1px solid var(--line);border-radius:10px;padding:14px}
29
+ .k{font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--sub);font-weight:600;margin-bottom:8px}
30
+ .gauge{display:flex;gap:18px;align-items:baseline}
31
+ .gauge .n{font-family:var(--mono);font-size:44px;font-weight:700;color:var(--acc);font-variant-numeric:tabular-nums}
32
+ .gauge .lab{font-size:11px;color:var(--sub)}
33
+ .delta{font-family:var(--mono);font-size:15px;font-weight:600}
34
+ .tier{margin:8px 0}.tier .l{display:flex;justify-content:space-between;font-family:var(--mono);font-size:11px;color:var(--sub);margin-bottom:3px}
35
+ .bar{height:11px;background:var(--g);border:1px solid var(--line);border-radius:6px;overflow:hidden}
36
+ .fill{height:100%;background:var(--acc)}
37
+ .log{font-family:var(--mono);font-size:11.5px;color:var(--sub);max-height:120px;overflow-y:auto;margin-top:8px}
38
+ .chat{display:flex;flex-direction:column;height:520px}
39
+ .msgs{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding-bottom:8px}
40
+ .m{max-width:85%;padding:8px 12px;border-radius:10px;font-size:14px;line-height:1.5;white-space:pre-wrap}
41
+ .m.u{align-self:flex-end;background:var(--accsoft);color:var(--ink)}
42
+ .m.a{align-self:flex-start;background:var(--g);border:1px solid var(--line)}
43
+ .m .t{display:block;margin-top:6px;font-family:var(--mono);font-size:10.5px;color:var(--acc)}
44
+ .row{display:flex;gap:8px}
45
+ textarea{flex:1;resize:none;height:54px;padding:9px;font-size:14px;font-family:inherit;
46
+ background:var(--g);color:var(--ink);border:1px solid var(--line);border-radius:8px}
47
+ button{padding:0 18px;background:var(--acc);color:#fff;border:0;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer}
48
+ button:disabled{opacity:.5}
49
+ .foot{margin-top:14px;font-size:11.5px;color:var(--sub)}.foot a{color:var(--acc)}
50
+ </style></head><body><div class="wrap">
51
+ <h1>The law, live</h1>
52
+ <div class="sub">{{MODEL}} · {{PLACEMENT}} · every reply below is scored against the pre-registered prediction</div>
53
+ <div class="law">tok/s = η(tier) × bandwidth ÷ active-bytes-per-token &nbsp;→&nbsp; predicted {{PRED}} tok/s for this box</div>
54
+ <div class="cols">
55
+ <div>
56
+ <div class="panel"><div class="k">Prediction vs reality</div>
57
+ <div class="gauge">
58
+ <div><div class="n" id="pred">{{PRED}}</div><div class="lab">predicted tok/s</div></div>
59
+ <div><div class="n" id="meas">—</div><div class="lab">measured (running avg)</div></div>
60
+ <div class="delta" id="delta"></div>
61
+ </div>
62
+ <div class="log" id="log">each reply appends a data point…</div>
63
+ </div>
64
+ <div class="panel" style="margin-top:14px"><div class="k">Placement (planned)</div>{{TIERS}}
65
+ <div class="foot">Flags: <span style="font-family:var(--mono)">{{FLAGS}}</span></div></div>
66
+ <div class="panel" style="margin-top:14px"><div class="k">About</div>
67
+ <div class="foot">This page is a running experiment: the tiered decode law predicted this machine's speed
68
+ before the model loaded. Laws, probes, evidence: <a href="https://github.com/FedericoTs/quantprobe">repo</a>
69
+ · <a href="https://x.com/federico_sciuca">@federico_sciuca</a></div></div>
70
+ </div>
71
+ <div class="panel chat"><div class="k">Chat — each reply is a measurement</div>
72
+ <div class="msgs" id="msgs"></div>
73
+ <div class="row"><textarea id="inp" placeholder="Say something… (Enter to send)"></textarea>
74
+ <button id="send">Send</button></div>
75
+ </div>
76
+ </div></div>
77
+ <script>
78
+ const hist=[],pts=[];const $=id=>document.getElementById(id);
79
+ function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;")}
80
+ async function send(){
81
+ const t=$("inp").value.trim(); if(!t)return;
82
+ $("inp").value=""; $("send").disabled=true;
83
+ hist.push({role:"user",content:t});
84
+ $("msgs").insertAdjacentHTML("beforeend",'<div class="m u">'+esc(t)+'</div>');
85
+ const hold=document.createElement("div");hold.className="m a";hold.textContent="…";
86
+ $("msgs").appendChild(hold);$("msgs").scrollTop=1e9;
87
+ try{
88
+ const r=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},
89
+ body:JSON.stringify({messages:hist.slice(-12),max_tokens:256})});
90
+ const j=await r.json();
91
+ const msg=(j.choices&&j.choices[0].message.content)||"(no reply)";
92
+ const tps=j.timings?j.timings.predicted_per_second:null;
93
+ hist.push({role:"assistant",content:msg});
94
+ hold.innerHTML=esc(msg)+(tps?'<span class="t">'+tps.toFixed(1)+' tok/s · predicted {{PRED}}</span>':'');
95
+ if(tps){pts.push(tps);
96
+ const avg=pts.reduce((a,b)=>a+b,0)/pts.length,pred=parseFloat("{{PRED}}");
97
+ $("meas").textContent=avg.toFixed(1);
98
+ const d=(avg/pred-1)*100;
99
+ $("delta").textContent=(d>=0?"+":"")+d.toFixed(0)+"%";
100
+ $("delta").style.color=Math.abs(d)<25?"var(--acc)":"var(--warn)";
101
+ $("log").insertAdjacentHTML("afterbegin","#"+pts.length+": "+tps.toFixed(1)+" tok/s<br>");}
102
+ }catch(e){hold.textContent="error: "+e}
103
+ $("send").disabled=false;$("msgs").scrollTop=1e9;
104
+ }
105
+ $("send").onclick=send;
106
+ $("inp").addEventListener("keydown",e=>{if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();send();}});
107
+ </script></body></html>"""
108
+
109
+
110
+ def make_handler(upstream, page):
111
+ class H(BaseHTTPRequestHandler):
112
+ def log_message(self, *a):
113
+ pass
114
+
115
+ def do_GET(self):
116
+ self.send_response(200)
117
+ self.send_header("Content-Type", "text/html; charset=utf-8")
118
+ self.end_headers()
119
+ self.wfile.write(page.encode("utf-8"))
120
+
121
+ def do_POST(self):
122
+ if self.path != "/api/chat":
123
+ self.send_response(404); self.end_headers(); return
124
+ n = int(self.headers.get("Content-Length", 0))
125
+ body = json.loads(self.rfile.read(n) or b"{}")
126
+ body.setdefault("timings_per_token", True)
127
+ req = urllib.request.Request(upstream + "/v1/chat/completions",
128
+ data=json.dumps(body).encode(),
129
+ headers={"Content-Type": "application/json"})
130
+ try:
131
+ with urllib.request.urlopen(req, timeout=600) as r:
132
+ data = r.read()
133
+ self.send_response(200)
134
+ self.send_header("Content-Type", "application/json")
135
+ self.end_headers()
136
+ self.wfile.write(data)
137
+ except Exception as e:
138
+ self.send_response(502); self.end_headers()
139
+ self.wfile.write(json.dumps({"error": str(e)}).encode())
140
+ return H
141
+
142
+
143
+ def dashboard(a):
144
+ best, flags = runtime.best_flags(a)
145
+ binp = runtime.find_llama(a.llama_dir, "llama-server")
146
+ sport = a.server_port
147
+ cmd = [binp, "-m", a.gguf, "--port", str(sport)] + flags
148
+ print(f"[quantprobe] placement: {best[0]} (predicted {best[1]:.1f} tok/s)")
149
+ print("[quantprobe] llama-server:", " ".join(cmd))
150
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
151
+ upstream = f"http://127.0.0.1:{sport}"
152
+ for _ in range(120):
153
+ try:
154
+ urllib.request.urlopen(upstream + "/health", timeout=2)
155
+ break
156
+ except Exception:
157
+ if proc.poll() is not None:
158
+ raise SystemExit("llama-server exited during startup — check the model path/flags")
159
+ time.sleep(1)
160
+ tiers = ""
161
+ for name, cap, used in runtime.tier_view(a, best):
162
+ pct = min(100, used / cap * 100) if cap else 0
163
+ tiers += (f'<div class="tier"><div class="l"><span>{name}</span>'
164
+ f'<span>{used:.1f} / {cap:.0f} GB</span></div>'
165
+ f'<div class="bar"><div class="fill" style="width:{pct:.0f}%"></div></div></div>')
166
+ page = (DASH_HTML.replace("{{MODEL}}", os.path.basename(a.gguf))
167
+ .replace("{{PLACEMENT}}", best[0]).replace("{{PRED}}", f"{best[1]:.1f}")
168
+ .replace("{{FLAGS}}", " ".join(flags)).replace("{{TIERS}}", tiers))
169
+ srv = ThreadingHTTPServer(("127.0.0.1", a.port), make_handler(upstream, page))
170
+ url = f"http://127.0.0.1:{a.port}"
171
+ print(f"[quantprobe] dashboard: {url} (Ctrl-C stops both)")
172
+ if not a.no_open:
173
+ threading.Timer(1.0, lambda: webbrowser.open(url)).start()
174
+ try:
175
+ srv.serve_forever()
176
+ except KeyboardInterrupt:
177
+ pass
178
+ finally:
179
+ proc.terminate()
quantprobe/detect.py ADDED
@@ -0,0 +1,163 @@
1
+ """quantprobe hw — read the local machine's memory tiers, no questions asked.
2
+
3
+ Every printed value is tagged with its source:
4
+ [os] read from the operating system / driver (capacity, stick speed, GPU name)
5
+ [table] looked up from the device name (bandwidth spec, eta class)
6
+ [default] a conservative fallback — override it with flags
7
+ Bandwidths are THEORETICAL peaks (the law's eta absorbs realism, same convention as the presets).
8
+ Nothing is sent anywhere; this only reads local OS interfaces.
9
+ """
10
+ from __future__ import annotations
11
+ import os, platform, re, shutil, subprocess
12
+
13
+
14
+ def _run(cmd, timeout=10):
15
+ try:
16
+ return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout
17
+ except Exception:
18
+ return ""
19
+
20
+
21
+ # name-fragment -> (VRAM bandwidth GB/s, geta, gl). 1060 measured on the reference box; rest spec-sheet [table].
22
+ GPU_TABLE = [
23
+ ("5090", 1792, 0.62, 0.42), ("4090", 1008, 0.62, 0.42), ("4080", 717, 0.6, 0.4),
24
+ ("4070", 504, 0.55, 0.35), ("4060", 272, 0.5, 0.3),
25
+ ("3090", 936, 0.6, 0.4), ("3080", 760, 0.58, 0.38), ("3070", 448, 0.52, 0.32),
26
+ ("3060 ti", 448, 0.5, 0.3), ("3060", 360, 0.5, 0.3), ("3050", 224, 0.45, 0.28),
27
+ ("2080", 448, 0.45, 0.25), ("2070", 448, 0.45, 0.25), ("2060", 336, 0.42, 0.22),
28
+ ("1080", 320, 0.38, 0.06), ("1070", 256, 0.36, 0.05), ("1060", 192, 0.35, 0.04),
29
+ ("a100", 1935, 0.7, 0.55), ("h100", 3350, 0.75, 0.6), ("rtx 6000", 960, 0.62, 0.42),
30
+ ]
31
+ MAC_BW = {"m1 ultra": 800, "m1 max": 400, "m1 pro": 200, "m1": 68,
32
+ "m2 ultra": 800, "m2 max": 400, "m2 pro": 200, "m2": 100,
33
+ "m3 ultra": 819, "m3 max": 400, "m3 pro": 150, "m3": 100,
34
+ "m4 max": 546, "m4 pro": 273, "m4": 120}
35
+
36
+
37
+ def gpus():
38
+ """[(name, vram_gb)] via nvidia-smi; empty list if none/AMD (AMD: pass flags for now)."""
39
+ out = _run(["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
40
+ gs = []
41
+ for line in out.strip().splitlines():
42
+ if "," in line:
43
+ name, mem = line.rsplit(",", 1)
44
+ try:
45
+ gs.append((name.strip(), float(mem) / 1024))
46
+ except ValueError:
47
+ pass
48
+ return gs
49
+
50
+
51
+ def gpu_lookup(name):
52
+ n = name.lower()
53
+ for frag, bw, geta, gl in GPU_TABLE:
54
+ if frag in n:
55
+ return bw, geta, gl, "[table]"
56
+ return 300, 0.45, 0.27, "[default: unknown GPU, pass --vram-bw]"
57
+
58
+
59
+ def ram_windows():
60
+ """(total_gb[os], mts[os], channels[os]) via CIM (wmic fallback)."""
61
+ ps = _run(["powershell", "-NoProfile", "-c",
62
+ "$m=Get-CimInstance Win32_PhysicalMemory; "
63
+ "($m|Measure-Object Capacity -Sum).Sum; "
64
+ "($m|Select-Object -First 1).ConfiguredClockSpeed; ($m|Measure-Object).Count"])
65
+ vals = [v.strip() for v in ps.strip().splitlines() if v.strip()]
66
+ if len(vals) >= 3:
67
+ try:
68
+ return float(vals[0]) / 2**30, float(vals[1]), int(vals[2])
69
+ except ValueError:
70
+ pass
71
+ return None, None, None
72
+
73
+
74
+ def detect():
75
+ """Return the machine as quantprobe hardware kwargs + a provenance report."""
76
+ sysname = platform.system()
77
+ hw, notes = {}, []
78
+
79
+ if sysname == "Darwin":
80
+ mem = _run(["sysctl", "-n", "hw.memsize"]).strip()
81
+ chip = _run(["sysctl", "-n", "machdep.cpu.brand_string"]).lower()
82
+ total = float(mem) / 2**30 if mem else 16.0
83
+ bw = next((b for frag, b in sorted(MAC_BW.items(), key=lambda x: -len(x[0])) if frag in chip), 100)
84
+ hw = dict(vram=round(total * 0.8), vram_bw=bw, ram=8, ram_bw=bw, disk_bw=3.5,
85
+ geta=0.26, gl=0.24)
86
+ notes.append(f"Apple unified memory: {total:.0f} GB [os], {bw} GB/s [table, est eta 0.26 - unvalidated: bench me]")
87
+ return hw, notes
88
+
89
+ # RAM
90
+ total, mts, sticks = (ram_windows() if os.name == "nt" else (None, None, None))
91
+ if total is None and os.path.exists("/proc/meminfo"):
92
+ with open("/proc/meminfo") as f:
93
+ kb = int(re.search(r"MemTotal:\s*(\d+)", f.read()).group(1))
94
+ total, mts, sticks = kb / 2**20, None, None
95
+ if total is None:
96
+ total = 16.0; notes.append("RAM capacity: 16 GB [default - detection failed]")
97
+ channels = max(1, min(sticks or 2, 8)) if sticks else 2
98
+ if mts:
99
+ ram_bw = round(channels * mts * 8 / 1000) # theoretical peak, preset convention
100
+ notes.append(f"RAM: {total:.0f} GB, {sticks} stick(s) @ {mts:.0f} MT/s [os] -> {ram_bw} GB/s peak "
101
+ f"(assumes {channels}-channel)")
102
+ else:
103
+ ram_bw = 48
104
+ notes.append(f"RAM: {total:.0f} GB [os]; speed unknown -> 48 GB/s [default: DDR4-3000 dual, pass --ram-bw]")
105
+
106
+ # GPU(s)
107
+ gs = gpus()
108
+ if gs:
109
+ vram = sum(g[1] for g in gs)
110
+ bw0, geta, gl, src = gpu_lookup(gs[0][0])
111
+ vram_bw = bw0 * len(gs) * (1.0 if len(gs) == 1 else 0.85) # aggregate w/ tensor-parallel loss
112
+ names = " + ".join(g[0] for g in gs)
113
+ notes.append(f"GPU: {names}, {vram:.0f} GB total [os], {vram_bw:.0f} GB/s {src}"
114
+ + (f" (x{len(gs)} aggregate, 0.85 TP efficiency [est])" if len(gs) > 1 else ""))
115
+ hw.update(vram=vram, vram_bw=round(vram_bw), geta=geta, gl=gl)
116
+ else:
117
+ hw.update(vram=0, vram_bw=0)
118
+ notes.append("GPU: none detected (nvidia-smi absent/empty; AMD/Intel: pass --vram/--vram-bw) [os]")
119
+
120
+ # disk: class default; a real measured number needs `quantprobe hw --measure` (reads a large file)
121
+ hw.update(ram=round(total), ram_bw=ram_bw, disk_bw=0.5)
122
+ notes.append("disk: 0.5 GB/s [default: SATA-class; NVMe ~3.5, Gen4 ~7 - pass --disk-bw or run hw --measure]")
123
+ return hw, notes
124
+
125
+
126
+ def measure_disk(path, mb=512):
127
+ """Sequential read of a real file region, uncached-ish: the streaming pattern that matters."""
128
+ import time
129
+ size = os.path.getsize(path)
130
+ span = min(mb * 1024 * 1024, size)
131
+ off = max(0, size - span - (os.urandom(1)[0] % 7) * 1024 * 1024) # tail region, jittered
132
+ t0 = time.perf_counter()
133
+ with open(path, "rb", buffering=0) as f:
134
+ f.seek(off)
135
+ left = span
136
+ while left > 0:
137
+ chunk = f.read(min(1 << 24, left))
138
+ if not chunk:
139
+ break
140
+ left -= len(chunk)
141
+ dt = time.perf_counter() - t0
142
+ return (span - left) / 1e9 / dt
143
+
144
+
145
+ def run(a):
146
+ hw, notes = detect()
147
+ print("quantprobe hw - this machine, as the law sees it\n")
148
+ for n in notes:
149
+ print(" " + n)
150
+ if getattr(a, "measure", None):
151
+ p = a.measure
152
+ if os.path.isfile(p):
153
+ bw = measure_disk(p)
154
+ hw["disk_bw"] = round(bw, 2)
155
+ print(f" disk MEASURED on {os.path.basename(p)}: {bw:.2f} GB/s sequential [measured]")
156
+ else:
157
+ print(f" --measure: file not found: {p}")
158
+ flags = (f"--vram {hw['vram']:g} --vram-bw {hw['vram_bw']:g} --ram {hw['ram']:g} "
159
+ f"--ram-bw {hw['ram_bw']:g} --disk-bw {hw['disk_bw']:g}")
160
+ print(f"\n equivalent flags (for sharing / estimating this box elsewhere):\n {flags}")
161
+ print("\n every command now uses these automatically when you pass no hardware flags;")
162
+ print(" pass --machine or explicit flags to estimate a DIFFERENT machine instead.")
163
+ return hw
quantprobe/fetch.py ADDED
@@ -0,0 +1,88 @@
1
+ """hf_fetch.py -- robust multi-file HF downloader (manual HTTP Range, retry-on-break), bypassing the
2
+ hf CLI's Xet-backend stalls. Usage: python -m weights.hf_fetch <repo_id> <dest_dir> <file1> [file2 ...]
3
+ Resumes partial .part files; token from HF_TOKEN env or ~/.cache/huggingface/token.
4
+ """
5
+ from __future__ import annotations
6
+ import os, sys, time
7
+ import requests
8
+
9
+
10
+ PRESETS = {
11
+ "qwen3-30b": ("unsloth/Qwen3-30B-A3B-GGUF", "Qwen3-30B-A3B-Q2_K.gguf"),
12
+ "glm-air": ("unsloth/GLM-4.5-Air-GGUF", "GLM-4.5-Air-UD-IQ2_XXS.gguf"),
13
+ "deepseek-16b": ("bartowski/DeepSeek-Coder-V2-Lite-Base-GGUF", "DeepSeek-Coder-V2-Lite-Base-IQ2_XS.gguf"),
14
+ "qwen3-0.6b": ("unsloth/Qwen3-0.6B-GGUF", "Qwen3-0.6B-Q8_0.gguf"),
15
+ }
16
+
17
+
18
+ def token():
19
+ t = os.environ.get("HF_TOKEN")
20
+ if t:
21
+ return t.strip()
22
+ p = os.path.expanduser("~/.cache/huggingface/token")
23
+ return open(p).read().strip() if os.path.exists(p) else None
24
+
25
+
26
+ def fetch(repo, dest, fname, tok, tries=100):
27
+ url = f"https://huggingface.co/{repo}/resolve/main/{fname}"
28
+ out = os.path.join(dest, fname)
29
+ part = out + ".part"
30
+ if os.path.exists(out):
31
+ print(f" {fname}: already complete", flush=True)
32
+ return True
33
+ hdr0 = {"Authorization": f"Bearer {tok}"} if tok else {}
34
+ r = requests.head(url, headers=hdr0, allow_redirects=True, timeout=60)
35
+ total = int(r.headers.get("Content-Length", 0))
36
+ print(f" {fname}: {total/1e9:.2f} GB", flush=True)
37
+ t = 0
38
+ while t < tries:
39
+ have = os.path.getsize(part) if os.path.exists(part) else 0
40
+ if total and have >= total:
41
+ break
42
+ try:
43
+ h = dict(hdr0)
44
+ if have:
45
+ h["Range"] = f"bytes={have}-"
46
+ r = requests.get(url, headers=h, stream=True, timeout=(30, 120), allow_redirects=True)
47
+ if r.status_code not in (200, 206):
48
+ print(f" status {r.status_code}, retry", flush=True); time.sleep(5); t += 1; continue
49
+ mode = "ab" if (have and r.status_code == 206) else "wb"
50
+ t0 = last = time.time(); base = have if mode == "ab" else 0
51
+ with open(part, mode) as f:
52
+ for chunk in r.iter_content(1 << 22):
53
+ if chunk:
54
+ f.write(chunk)
55
+ if time.time() - last > 20:
56
+ sz = os.path.getsize(part)
57
+ print(f" {sz/1e9:.2f}/{total/1e9:.2f} GB ({(sz-base)/1e6/max(1e-6,time.time()-t0):.1f} MB/s)", flush=True)
58
+ last = time.time()
59
+ except (requests.exceptions.ChunkedEncodingError, requests.exceptions.ConnectionError,
60
+ requests.exceptions.ReadTimeout, requests.exceptions.Timeout) as e:
61
+ print(f" break at {os.path.getsize(part) if os.path.exists(part) else 0:,}, retry {t+1}: {str(e)[:60]}", flush=True)
62
+ time.sleep(3); t += 1
63
+ if total and os.path.exists(part) and os.path.getsize(part) == total:
64
+ os.replace(part, out)
65
+ print(f" {fname}: DONE", flush=True)
66
+ return True
67
+ print(f" {fname}: INCOMPLETE", flush=True)
68
+ return False
69
+
70
+
71
+ def run(a):
72
+ import sys as _s
73
+ repo, files = a.repo, a.files
74
+ if repo in PRESETS and not files:
75
+ repo, f = PRESETS[repo]
76
+ files = [f]
77
+ print(f"[quantprobe] preset '{a.repo}' -> {repo}/{f}")
78
+ if not files:
79
+ _s.exit("no files given (or use a preset: " + ", ".join(PRESETS) + ")")
80
+ ok = all(fetch(repo, a.dest, fn, token()) for fn in files)
81
+ _s.exit(0 if ok else 1)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ repo, dest = sys.argv[1], sys.argv[2]
86
+ os.makedirs(dest, exist_ok=True)
87
+ ok = all(fetch(repo, dest, f, token()) for f in sys.argv[3:])
88
+ sys.exit(0 if ok else 1)