quantprobe 1.4.0__tar.gz → 1.5.0__tar.gz

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.
Files changed (28) hide show
  1. {quantprobe-1.4.0/quantprobe.egg-info → quantprobe-1.5.0}/PKG-INFO +6 -4
  2. {quantprobe-1.4.0 → quantprobe-1.5.0}/README.md +5 -3
  3. {quantprobe-1.4.0 → quantprobe-1.5.0}/pyproject.toml +1 -1
  4. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/__init__.py +1 -1
  5. quantprobe-1.5.0/quantprobe/auto.py +106 -0
  6. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/cli.py +14 -0
  7. quantprobe-1.5.0/quantprobe/dashboard.py +384 -0
  8. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/detect.py +8 -4
  9. {quantprobe-1.4.0 → quantprobe-1.5.0/quantprobe.egg-info}/PKG-INFO +6 -4
  10. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe.egg-info/SOURCES.txt +1 -0
  11. quantprobe-1.4.0/quantprobe/dashboard.py +0 -179
  12. {quantprobe-1.4.0 → quantprobe-1.5.0}/LICENSE +0 -0
  13. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/fetch.py +0 -0
  14. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/optimize.py +0 -0
  15. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/plan.py +0 -0
  16. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/probe.py +0 -0
  17. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/runtime.py +0 -0
  18. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/spec.py +0 -0
  19. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe/target.py +0 -0
  20. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe.egg-info/dependency_links.txt +0 -0
  21. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe.egg-info/entry_points.txt +0 -0
  22. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe.egg-info/requires.txt +0 -0
  23. {quantprobe-1.4.0 → quantprobe-1.5.0}/quantprobe.egg-info/top_level.txt +0 -0
  24. {quantprobe-1.4.0 → quantprobe-1.5.0}/setup.cfg +0 -0
  25. {quantprobe-1.4.0 → quantprobe-1.5.0}/tests/test_engine.py +0 -0
  26. {quantprobe-1.4.0 → quantprobe-1.5.0}/tests/test_evaluator.py +0 -0
  27. {quantprobe-1.4.0 → quantprobe-1.5.0}/tests/test_pipeline.py +0 -0
  28. {quantprobe-1.4.0 → quantprobe-1.5.0}/tests/test_transforms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantprobe
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Probe-then-quantize for LLMs: measure a model's fragility curve, plan bit/tier placement by the tiered decode law, and emit ready-to-run llama.cpp recipes.
5
5
  Author: Federico Sciuca
6
6
  License: MIT
@@ -85,15 +85,17 @@ Full statements, each with its establishing measurement and a falsifiable predic
85
85
  <p align="center"><img src="weights/data/x_chart_I_kvdepth.png" width="700" alt="Decode speed versus context depth: measured 20.02 tok/s at depth zero falling to 16.12 at 16384, on the Law 4 v2 curve with eta_kv 0.70"></p>
86
86
 
87
87
 
88
- ## Install — and the ten commands
88
+ ## Install — and the eleven commands
89
89
 
90
90
  ```bash
91
91
  pip install quantprobe
92
+ quantprobe auto qwen3-coder --tps 15 --run # empty machine -> optimal quant chosen, fetched, chatting
92
93
  ```
93
94
 
94
- **Zero-config on your own box**: `quantprobe plan --gguf model.gguf` auto-detects the machine and reads the model from the file. Presets/flags estimate any *other* machine. `hw`/`plan`/`target`/`optimize` need nothing else installed; the rest drive stock [llama.cpp](https://github.com/ggml-org/llama.cpp/releases) (point at it with `--llama-dir`/`QUANTPROBE_LLAMA_DIR`/`PATH`; preview any command with `--dry`).
95
+ **Zero-config on your own box**: `quantprobe plan --gguf model.gguf` auto-detects the machine and reads the model from the file. Presets/flags estimate any *other* machine. `hw`/`plan`/`target`/`optimize` need nothing else installed (`auto` needs network for the fetch); the rest drive stock [llama.cpp](https://github.com/ggml-org/llama.cpp/releases) (point at it with `--llama-dir`/`QUANTPROBE_LLAMA_DIR`/`PATH`; preview any command with `--dry`).
95
96
 
96
97
  ```bash
98
+ quantprobe auto qwen3-30b --tps 15 # ONE command: optimizer picks bits, closest quant fetched, run command printed
97
99
  quantprobe hw # what the law sees on THIS machine (every value source-tagged)
98
100
  quantprobe plan --gguf model.gguf # zero-config prediction: placement + tok/s + the launch command
99
101
  quantprobe optimize --tps 20 # CHEAPEST PATH to a target: bits x placement x hardware, Pareto-ranked
@@ -103,7 +105,7 @@ quantprobe quantize --gguf f16.gguf --out 2bit.gguf # COMPRESS: depth-aware
103
105
  quantprobe probe --gguf f16.gguf --eval wiki.test.raw # measure YOUR model's fragile band (~30 min); --apply builds it
104
106
  quantprobe run --gguf 2bit.gguf # plan the placement, then LAUNCH llama.cpp chat
105
107
  quantprobe bench --gguf 2bit.gguf --contribute # predicted vs measured on your box; opt-in datapoint
106
- quantprobe dashboard --gguf 2bit.gguf # chat while every reply is scored against the prediction
108
+ quantprobe dashboard --gguf 2bit.gguf # the law LIVE: neuron galaxy + thinking toggle, every reply scored vs prediction
107
109
  ```
108
110
 
109
111
  The loop is self-validating: `plan` predicted 17.5 for a file we then measured at **18.32 ± 0.17**; the config months of research converged to is what `optimize` picks blind. A measured example of what that's worth: the same model, mis-specified vs law-routed, is **3.38 vs 18.32 tok/s (×5.4)** — [worked examples](docs/EXAMPLES.md).
@@ -67,15 +67,17 @@ Full statements, each with its establishing measurement and a falsifiable predic
67
67
  <p align="center"><img src="weights/data/x_chart_I_kvdepth.png" width="700" alt="Decode speed versus context depth: measured 20.02 tok/s at depth zero falling to 16.12 at 16384, on the Law 4 v2 curve with eta_kv 0.70"></p>
68
68
 
69
69
 
70
- ## Install — and the ten commands
70
+ ## Install — and the eleven commands
71
71
 
72
72
  ```bash
73
73
  pip install quantprobe
74
+ quantprobe auto qwen3-coder --tps 15 --run # empty machine -> optimal quant chosen, fetched, chatting
74
75
  ```
75
76
 
76
- **Zero-config on your own box**: `quantprobe plan --gguf model.gguf` auto-detects the machine and reads the model from the file. Presets/flags estimate any *other* machine. `hw`/`plan`/`target`/`optimize` need nothing else installed; the rest drive stock [llama.cpp](https://github.com/ggml-org/llama.cpp/releases) (point at it with `--llama-dir`/`QUANTPROBE_LLAMA_DIR`/`PATH`; preview any command with `--dry`).
77
+ **Zero-config on your own box**: `quantprobe plan --gguf model.gguf` auto-detects the machine and reads the model from the file. Presets/flags estimate any *other* machine. `hw`/`plan`/`target`/`optimize` need nothing else installed (`auto` needs network for the fetch); the rest drive stock [llama.cpp](https://github.com/ggml-org/llama.cpp/releases) (point at it with `--llama-dir`/`QUANTPROBE_LLAMA_DIR`/`PATH`; preview any command with `--dry`).
77
78
 
78
79
  ```bash
80
+ quantprobe auto qwen3-30b --tps 15 # ONE command: optimizer picks bits, closest quant fetched, run command printed
79
81
  quantprobe hw # what the law sees on THIS machine (every value source-tagged)
80
82
  quantprobe plan --gguf model.gguf # zero-config prediction: placement + tok/s + the launch command
81
83
  quantprobe optimize --tps 20 # CHEAPEST PATH to a target: bits x placement x hardware, Pareto-ranked
@@ -85,7 +87,7 @@ quantprobe quantize --gguf f16.gguf --out 2bit.gguf # COMPRESS: depth-aware
85
87
  quantprobe probe --gguf f16.gguf --eval wiki.test.raw # measure YOUR model's fragile band (~30 min); --apply builds it
86
88
  quantprobe run --gguf 2bit.gguf # plan the placement, then LAUNCH llama.cpp chat
87
89
  quantprobe bench --gguf 2bit.gguf --contribute # predicted vs measured on your box; opt-in datapoint
88
- quantprobe dashboard --gguf 2bit.gguf # chat while every reply is scored against the prediction
90
+ quantprobe dashboard --gguf 2bit.gguf # the law LIVE: neuron galaxy + thinking toggle, every reply scored vs prediction
89
91
  ```
90
92
 
91
93
  The loop is self-validating: `plan` predicted 17.5 for a file we then measured at **18.32 ± 0.17**; the config months of research converged to is what `optimize` picks blind. A measured example of what that's worth: the same model, mis-specified vs law-routed, is **3.38 vs 18.32 tok/s (×5.4)** — [worked examples](docs/EXAMPLES.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quantprobe"
7
- version = "1.4.0"
7
+ version = "1.5.0"
8
8
  description = "Probe-then-quantize for LLMs: measure a model's fragility curve, plan bit/tier placement by the tiered decode law, and emit ready-to-run llama.cpp recipes."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -8,4 +8,4 @@ Three commands, all grounded in measured laws (see LAWS.md in the repo):
8
8
  Every constant in `plan` is fitted from measurements published in the repo, including two
9
9
  pre-registered hardware predictions confirmed within 8%.
10
10
  """
11
- __version__ = "1.4.0"
11
+ __version__ = "1.5.0"
@@ -0,0 +1,106 @@
1
+ """quantprobe auto — one command: model in, running setup out.
2
+
3
+ quantprobe auto qwen3-30b --tps 15 # preset
4
+ quantprobe auto unsloth/Qwen3-30B-A3B-GGUF # any HF GGUF repo (params read from filenames' size)
5
+
6
+ The two-speed design:
7
+ FAST PATH (this command): detect the machine -> ask the optimizer for the best effective-bits ->
8
+ scan the HF repo's file list and pick the closest-matching quant BY SIZE (bits = size*8/params —
9
+ no fragile name parsing) -> fetch it -> print the prediction and the run command (--run launches).
10
+ CUSTOM PATH (the actual product, printed at the end): probe YOUR model's fragile band and build a
11
+ depth-aware GGUF at the same bits — better quality at the same bytes. `probe --apply` does it.
12
+ """
13
+ from __future__ import annotations
14
+ import json, urllib.request
15
+
16
+ from . import plan as planmod
17
+ from . import optimize as optmod
18
+
19
+ # preset -> (repo, total params B, active B, always-active B, moe)
20
+ MODEL_REPOS = {
21
+ "qwen3-30b": ("unsloth/Qwen3-30B-A3B-GGUF", 30.5, 3.3, 1.2, True),
22
+ "qwen3-coder": ("unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF", 30.5, 3.3, 1.2, True),
23
+ "glm-air": ("unsloth/GLM-4.5-Air-GGUF", 110, 12, 2.7, True),
24
+ "laguna-s": ("unsloth/Laguna-S-2.1-GGUF", 117.6, 8, 2.5, True),
25
+ "gemma-12b": ("unsloth/gemma-4-12b-it-GGUF", 11.9, 11.9, 11.9, False),
26
+ "mistral-7b": ("unsloth/Mistral-7B-Instruct-v0.3-GGUF", 7.2, 7.2, 7.2, False),
27
+ }
28
+
29
+
30
+ def list_ggufs(repo):
31
+ """[(path, size_bytes)] for a HF repo, via the public tree API."""
32
+ req = urllib.request.Request(f"https://huggingface.co/api/models/{repo}/tree/main",
33
+ headers={"User-Agent": "quantprobe-auto"})
34
+ with urllib.request.urlopen(req, timeout=30) as r:
35
+ return [(f["path"], f.get("size", 0)) for f in json.load(r)
36
+ if f["path"].endswith(".gguf") and f.get("size", 0) > 1e8
37
+ and "mmproj" not in f["path"].lower() and "draft" not in f["path"].lower()]
38
+
39
+
40
+ def run(a):
41
+ target = a.target
42
+ if target in MODEL_REPOS:
43
+ repo, t, ac, ne, moe = MODEL_REPOS[target]
44
+ else:
45
+ repo = target
46
+ if not getattr(a, "total", None):
47
+ raise SystemExit(f"'{target}' is not a preset ({', '.join(MODEL_REPOS)}) - for a raw HF "
48
+ "repo also pass --total (B params) and, for MoE, --active/--always-active")
49
+ t, ac = a.total, a.active or a.total
50
+ ne = a.always_active or (ac if ac >= t * 0.9 else ac * 0.35)
51
+ moe = ac < t * 0.9
52
+ a.total, a.active, a.always_active, a.model = t, ac, ne, None
53
+
54
+ # 1. what does the optimizer want on THIS machine?
55
+ a.gguf = None
56
+ ranked = optmod.run(a)
57
+ want_bits = ranked[0]["bits"]
58
+ _, _, _, _, _, (vc, vb, rc, rb, db, geta, gl), ctx, kvp = optmod.resolve(a)
59
+
60
+ # 2. pick the closest file BY SIZE (bits = size*8/params; honest, format-agnostic)
61
+ try:
62
+ files = list_ggufs(repo)
63
+ except Exception as e:
64
+ raise SystemExit(f"could not list {repo}: {e}")
65
+ if not files:
66
+ raise SystemExit(f"no GGUF files found in {repo}")
67
+ scored = []
68
+ for path, size in files:
69
+ bits = size * 8 / (t * 1e9)
70
+ if bits < 1.0 or bits > 9:
71
+ continue
72
+ _, _, cfgs = planmod.evaluate(t, ac, ne, moe, bits, vc, vb, rc, rb, db, geta, 1.0, gl,
73
+ ctx=ctx, kvp=kvp)
74
+ cfgs = [c for c in cfgs if "expert cache" not in c[0]]
75
+ if not cfgs:
76
+ continue
77
+ scored.append((abs(bits - want_bits), -cfgs[0][1], path, size, bits, cfgs[0]))
78
+ if not scored:
79
+ raise SystemExit("no usable quant in that repo for this machine")
80
+ scored.sort()
81
+ _, _, path, size, bits, best = scored[0]
82
+ print(f"\n[quantprobe auto] optimizer wants ~{want_bits:g}-bit; closest file in {repo}:")
83
+ print(f" {path} ({size/1e9:.1f} GB, {bits:.2f} effective bits)")
84
+ print(f" predicted on this machine: {best[1]:.1f} tok/s ({best[0]})")
85
+ if getattr(a, "dry", False):
86
+ print(" (--dry: nothing downloaded)")
87
+ return path
88
+ # 3. fetch it (resumable), then hand off
89
+ from . import fetch as fetchmod
90
+ dest = getattr(a, "dir", None) or "./models"
91
+ import os
92
+ os.makedirs(dest, exist_ok=True)
93
+ ok = fetchmod.fetch(repo, dest, path, fetchmod.token())
94
+ if not ok:
95
+ raise SystemExit("download failed (it resumes: re-run the same command)")
96
+ full = os.path.join(dest, os.path.basename(path))
97
+ print(f"\n[quantprobe auto] ready. Run it:")
98
+ print(f" quantprobe run --gguf {full}")
99
+ print(f"\n Want better quality at the SAME size? That's the custom path (the actual product):")
100
+ print(f" fetch the f16, then: quantprobe probe --gguf <f16>.gguf --eval wiki.test.raw --apply")
101
+ if getattr(a, "run", False):
102
+ from . import runtime
103
+ a.gguf = full
104
+ a.bits = None # let autospec read the real file
105
+ runtime.run(a)
106
+ return full
@@ -91,6 +91,17 @@ def main():
91
91
  o.add_argument("--allow-prune", action="store_true", help="include REAP-class pruned variants (domain-specialized: +39%% out-of-domain ppl measured)")
92
92
  hwargs(o)
93
93
 
94
+ au = sub.add_parser("auto", help="ONE command: model in, running setup out - optimizer picks the bits, the closest quant is fetched, run command printed")
95
+ au.add_argument("target", help="model preset (qwen3-30b, qwen3-coder, glm-air, laguna-s, gemma-12b, mistral-7b) or a HF GGUF repo id")
96
+ au.add_argument("--dir", default="./models", help="download directory (default ./models)")
97
+ au.add_argument("--run", action="store_true", help="launch chat immediately after the download")
98
+ hwargs(au)
99
+ au.add_argument("--tps", type=float, default=None, help="target tok/s for the optimizer")
100
+ au.add_argument("--max-quality", type=float, default=None)
101
+ au.add_argument("--allow-prune", action="store_true"); au.add_argument("--any-runtime", action="store_true")
102
+ au.add_argument("--serve", action="store_true", help="with --run: llama-server instead of chat")
103
+ au.add_argument("--extra", default=None)
104
+
94
105
  hwp = sub.add_parser("hw", help="detect THIS machine's memory tiers (no flags needed); every value tagged with its source")
95
106
  hwp.add_argument("--measure", default=None, metavar="FILE", help="also MEASURE sequential disk read on a real file (e.g. a GGUF)")
96
107
 
@@ -116,6 +127,9 @@ def main():
116
127
  elif a.cmd == "target":
117
128
  from . import target
118
129
  target.run(a)
130
+ elif a.cmd == "auto":
131
+ from . import auto
132
+ auto.run(a)
119
133
  elif a.cmd == "optimize":
120
134
  from . import optimize
121
135
  optimize.run(a)
@@ -0,0 +1,384 @@
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
+ single-viewport app (no page scrolling: fixed sidebar, ChatGPT-style internal chat scroll) where
5
+ every reply is scored against the law's prediction. The NEURON GALAXY renders every expert of every
6
+ layer as a dot, illuminated per generated token — thinking tokens in violet, answer tokens in the
7
+ color of the memory tier the experts live on.
8
+
9
+ Honesty rule, printed on the panel: stock llama.cpp exposes no per-token router telemetry, so
10
+ expert illumination is sampled uniformly — which Law 2's measured flat routing (Jaccard 1.00)
11
+ makes the statistically exact picture. Topic-affinity atlases require an engine that exports
12
+ router data; this page shows nothing it cannot back.
13
+ """
14
+ from __future__ import annotations
15
+ import json, os, subprocess, threading, time, urllib.request, webbrowser
16
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
+
18
+ from . import runtime
19
+
20
+ DASH_HTML = """<!doctype html><html><head><meta charset="utf-8">
21
+ <meta name="viewport" content="width=device-width,initial-scale=1">
22
+ <title>quantprobe — the law, live</title>
23
+ <style>
24
+ :root{--g:#fafaf7;--p:#fff;--ink:#16181d;--sub:#5c6066;--line:#e5e4df;--acc:#0f766e;
25
+ --accsoft:#e6f2f0;--warn:#b45309;--ram:#d97706;--disk:#dc2626;--think:#7c6bd1;
26
+ --mono:Consolas,ui-monospace,monospace}
27
+ @media(prefers-color-scheme:dark){:root{--g:#101216;--p:#181b20;--ink:#e8e7e2;--sub:#9aa0a8;
28
+ --line:#2b2f35;--acc:#35b8a6;--accsoft:#12332f;--warn:#e0a458;--ram:#f0a94b;--disk:#f26d6d;--think:#a99af0}}
29
+ *{box-sizing:border-box}
30
+ body{margin:0;height:100vh;overflow:hidden;background:var(--g);color:var(--ink);
31
+ font-family:"Segoe UI",system-ui,sans-serif;display:grid;grid-template-rows:auto 1fr}
32
+ header{display:flex;align-items:baseline;gap:14px;padding:10px 18px;border-bottom:1px solid var(--line)}
33
+ header h1{font-size:17px;margin:0}
34
+ header .sub{color:var(--sub);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
35
+ header .law{margin-left:auto;font-family:var(--mono);font-size:11.5px;color:var(--acc);white-space:nowrap}
36
+ main{display:grid;grid-template-columns:320px 1fr;gap:14px;padding:14px;min-height:0}
37
+ aside{overflow-y:auto;min-height:0;display:flex;flex-direction:column;gap:12px}
38
+ .panel{background:var(--p);border:1px solid var(--line);border-radius:10px;padding:12px;flex-shrink:0}
39
+ .k{font-size:10px;text-transform:uppercase;letter-spacing:.09em;color:var(--sub);font-weight:600;margin-bottom:7px}
40
+ .gauge{display:flex;gap:14px;align-items:baseline}
41
+ .gauge .n{font-family:var(--mono);font-size:34px;font-weight:700;color:var(--acc);font-variant-numeric:tabular-nums}
42
+ .gauge .lab{font-size:10.5px;color:var(--sub)}
43
+ .delta{font-family:var(--mono);font-size:14px;font-weight:600}
44
+ .log{font-family:var(--mono);font-size:11px;color:var(--sub);max-height:72px;overflow-y:auto;margin-top:6px}
45
+ .stat{display:flex;justify-content:space-between;font-family:var(--mono);font-size:11.5px;color:var(--sub);margin:3px 0}
46
+ .stat b{color:var(--ink);font-weight:600}
47
+ .tgl{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;user-select:none}
48
+ .tgl input{accent-color:var(--think);width:15px;height:15px}
49
+ .tier{margin:7px 0}.tier .l{display:flex;justify-content:space-between;font-family:var(--mono);font-size:10.5px;color:var(--sub);margin-bottom:3px}
50
+ .bar{height:10px;background:var(--g);border:1px solid var(--line);border-radius:6px;overflow:hidden}
51
+ .fill{height:100%;background:var(--acc)}
52
+ .foot{font-size:11px;color:var(--sub);line-height:1.5}.foot a{color:var(--acc)}
53
+ .right{display:grid;grid-template-rows:minmax(220px,37%) 1fr;gap:14px;min-height:0}
54
+ .brainp{position:relative;background:var(--p);border:1px solid var(--line);border-radius:10px;padding:10px 12px;
55
+ display:flex;flex-direction:column;min-height:0}
56
+ #brain{flex:1;width:100%;min-height:0;border-radius:6px}
57
+ .legend{display:flex;gap:13px;font-size:10.5px;color:var(--sub);margin-top:6px;flex-wrap:wrap;align-items:center}
58
+ .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px;vertical-align:-1px}
59
+ .chatp{display:flex;flex-direction:column;min-height:0;background:var(--p);border:1px solid var(--line);border-radius:10px;padding:12px}
60
+ .msgs{flex:1;overflow-y:auto;min-height:0;display:flex;flex-direction:column;gap:8px;padding-right:4px}
61
+ .m{max-width:85%;padding:8px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;white-space:pre-wrap}
62
+ .m.u{align-self:flex-end;background:var(--accsoft)}
63
+ .m.a{align-self:flex-start;background:var(--g);border:1px solid var(--line)}
64
+ .m .t{display:block;margin-top:6px;font-family:var(--mono);font-size:10px;color:var(--acc)}
65
+ .m pre{background:var(--p);border:1px solid var(--line);border-radius:6px;padding:8px;overflow-x:auto;font-family:var(--mono);font-size:12px;margin:6px 0;white-space:pre}
66
+ .thk{align-self:flex-start;max-width:85%;font-size:11.5px;color:var(--think);border-left:3px solid var(--think);
67
+ padding:3px 10px;opacity:.85;white-space:pre-wrap}
68
+ .thk span{display:block;max-height:80px;overflow-y:auto}
69
+ .thk summary{cursor:pointer;font-size:10.5px;letter-spacing:.05em}
70
+ .row{display:flex;gap:8px;margin-top:10px}
71
+ textarea{flex:1;resize:none;height:52px;padding:9px;font-size:13.5px;font-family:inherit;
72
+ background:var(--g);color:var(--ink);border:1px solid var(--line);border-radius:8px}
73
+ button{padding:0 18px;background:var(--acc);color:#fff;border:0;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer}
74
+ button:disabled{opacity:.5}
75
+ </style></head><body>
76
+ <header><h1>The law, live</h1>
77
+ <div class="sub">{{MODEL}} · {{PLACEMENT}}</div>
78
+ <div class="law">tok/s = η·BW ÷ active-bytes → predicted {{PRED}}</div></header>
79
+ <main>
80
+ <aside>
81
+ <div class="panel"><div class="k">Prediction vs reality</div>
82
+ <div class="gauge">
83
+ <div><div class="n">{{PRED}}</div><div class="lab">predicted tok/s</div></div>
84
+ <div><div class="n" id="meas">—</div><div class="lab">measured avg</div></div>
85
+ <div class="delta" id="delta"></div>
86
+ </div>
87
+ <div class="log" id="log">each reply appends a data point…</div>
88
+ </div>
89
+ <div class="panel"><div class="k">Thinking</div>
90
+ <label class="tgl"><input type="checkbox" id="think" checked> allow thinking <span style="color:var(--think)">●</span></label>
91
+ <div style="margin-top:8px">
92
+ <div class="stat"><span>time to first token</span><b id="ttft">—</b></div>
93
+ <div class="stat"><span>thinking</span><b id="sthink">—</b></div>
94
+ <div class="stat"><span>answer</span><b id="sans">—</b></div>
95
+ </div>
96
+ <div class="foot" style="margin-top:6px">Thinking improves hard answers but costs latency at
97
+ decode speed — toggle it off for quick tasks.</div>
98
+ </div>
99
+ <div class="panel"><div class="k">Placement (planned)</div>{{TIERS}}
100
+ <div class="foot" style="font-family:var(--mono);font-size:10px">{{FLAGS}}</div></div>
101
+ <div class="panel"><div class="k">About</div>
102
+ <div class="foot">A running experiment: the law predicted this machine's speed before the model
103
+ loaded. <a href="https://github.com/FedericoTs/quantprobe">repo</a> ·
104
+ <a href="https://x.com/federico_sciuca">@federico_sciuca</a></div></div>
105
+ </aside>
106
+ <div class="right">
107
+ <div class="brainp"><div class="k">Neuron galaxy — {{NL}} layers × {{NE}} experts · every token traverses all layers</div>
108
+ <canvas id="brain"></canvas>
109
+ <div class="legend">
110
+ <span><span class="dot" style="background:var(--acc)"></span>attention + KV → {{ATIER}}</span>
111
+ <span><span class="dot" style="background:var({{ECOLV}})"></span>experts ({{NK}} of {{NE}} fire per layer per token) → {{ETIER}}</span>
112
+ <span><span class="dot" style="background:var(--think)"></span>thinking</span>
113
+ <span style="opacity:.75">flashes sampled uniformly — statistically exact under measured flat routing (Jaccard 1.00); stock llama.cpp exposes no router telemetry</span>
114
+ </div>
115
+ </div>
116
+ <div class="chatp"><div class="k">Chat — each reply is a measurement</div>
117
+ <div class="msgs" id="msgs"></div>
118
+ <div class="row"><textarea id="inp" placeholder="Say something… (Enter to send)"></textarea>
119
+ <button id="send">Send</button></div>
120
+ </div>
121
+ </div>
122
+ </main>
123
+ <script>
124
+ const NL={{NL}},NE={{NE}},NK={{NK}},PRED=parseFloat("{{PRED}}");
125
+ const hist=[],pts=[];const $=id=>document.getElementById(id);
126
+ function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;")}
127
+ function md(s){return esc(s).replace(/```(\\w*)\\n?([\\s\\S]*?)```/g,'<pre>$2</pre>')}
128
+ function cssv(n){return getComputedStyle(document.body).getPropertyValue(n).trim()||"#888"}
129
+
130
+ /* ---- neuron galaxy ---- */
131
+ const cv=$("brain"),cx2=cv.getContext("2d");
132
+ const NDOT=Math.max(NE,1);let W,H,px,py,ax,ay,heat,tint,aheat;
133
+ function layout(){
134
+ W=cv.width=cv.clientWidth*devicePixelRatio;H=cv.height=cv.clientHeight*devicePixelRatio;
135
+ const CX=W/2,CY=H/2,MR=Math.min(W,H)*0.46;
136
+ px=new Float32Array(NL*NDOT);py=new Float32Array(NL*NDOT);
137
+ ax=new Float32Array(NL);ay=new Float32Array(NL);
138
+ heat=new Float32Array(NL*NDOT);tint=new Uint8Array(NL*NDOT);aheat=new Float32Array(NL);
139
+ for(let l=0;l<NL;l++){
140
+ const t=NL>1?l/(NL-1):0, ang=t*Math.PI*3.8+0.7, R=(0.18+0.78*t)*MR;
141
+ const cxl=CX+R*Math.cos(ang), cyl=CY+R*Math.sin(ang)*0.6;
142
+ ax[l]=cxl;ay[l]=cyl;
143
+ const sig=MR*(0.05+0.05*Math.sin(l*3.7)*Math.sin(l*3.7));
144
+ for(let e=0;e<NDOT;e++){
145
+ const r=sig*Math.sqrt((e+0.5)/NDOT)*2.1, a=e*2.39996+l*0.7;
146
+ px[l*NDOT+e]=cxl+r*Math.cos(a);
147
+ py[l*NDOT+e]=cyl+r*Math.sin(a)*0.85;
148
+ }
149
+ }
150
+ draw();
151
+ }
152
+ function draw(){
153
+ cx2.clearRect(0,0,W,H);
154
+ const ec=cssv("{{ECOLV}}"),tc=cssv("--think"),ac=cssv("--acc");
155
+ const s=2.1*devicePixelRatio, sa=3.4*devicePixelRatio;
156
+ for(let i=0;i<NL*NDOT;i++){
157
+ const h=heat[i];
158
+ cx2.globalAlpha=0.09+0.91*h;
159
+ cx2.fillStyle=(h>0.03&&tint[i])?tc:ec;
160
+ cx2.fillRect(px[i],py[i],s,s);
161
+ }
162
+ for(let l=0;l<NL;l++){
163
+ cx2.globalAlpha=0.35+0.65*aheat[l];
164
+ cx2.fillStyle=ac;
165
+ cx2.beginPath();cx2.arc(ax[l],ay[l],sa,0,7);cx2.fill();
166
+ }
167
+ cx2.globalAlpha=1;
168
+ }
169
+ let decaying=false;
170
+ function decay(){let live=false;
171
+ for(let i=0;i<heat.length;i++){heat[i]*=0.86;if(heat[i]>0.02)live=true}
172
+ for(let l=0;l<NL;l++){aheat[l]*=0.86;if(aheat[l]>0.02)live=true}
173
+ draw();
174
+ if(live)requestAnimationFrame(decay);else decaying=false}
175
+ function pulse(thinking){
176
+ for(let l=0;l<NL;l++){aheat[l]=1;
177
+ for(let k=0;k<Math.max(NK,1);k++){const i=l*NDOT+((Math.random()*NDOT)|0);
178
+ heat[i]=1;tint[i]=thinking?1:0}}
179
+ if(!decaying){decaying=true;requestAnimationFrame(decay)}}
180
+ addEventListener("resize",layout);layout();
181
+
182
+ /* ---- streaming chat with reply anatomy ---- */
183
+ function fmt(ms){return ms<1000?ms.toFixed(0)+" ms":(ms/1000).toFixed(1)+" s"}
184
+ async function send(){
185
+ const t=$("inp").value.trim(); if(!t)return;
186
+ $("inp").value=""; $("send").disabled=true;
187
+ hist.push({role:"user",content:t});
188
+ $("msgs").insertAdjacentHTML("beforeend",'<div class="m u">'+esc(t)+'</div>');
189
+ const thk=document.createElement("details");thk.className="thk";thk.open=true;
190
+ thk.innerHTML='<summary>thinking…</summary><span></span>';
191
+ const hold=document.createElement("div");hold.className="m a";hold.textContent="…";
192
+ $("msgs").appendChild(thk);$("msgs").appendChild(hold);$("msgs").scrollTop=1e9;
193
+ let content="",think="",nT=0,nA=0,t0=performance.now(),tFirst=0,tThinkEnd=0,timings=null;
194
+ try{
195
+ const r=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},
196
+ body:JSON.stringify({messages:hist.slice(-12),max_tokens:2048,stream:true,think:$("think").checked})});
197
+ const rd=r.body.getReader(),dec=new TextDecoder();let buf="";
198
+ while(true){
199
+ const {done,value}=await rd.read(); if(done)break;
200
+ buf+=dec.decode(value,{stream:true});
201
+ let i;while((i=buf.indexOf("\\n"))>=0){
202
+ const line=buf.slice(0,i).trim();buf=buf.slice(i+1);
203
+ if(!line.startsWith("data:"))continue;
204
+ const p=line.slice(5).trim(); if(p==="[DONE]")continue;
205
+ let j;try{j=JSON.parse(p)}catch(e){continue}
206
+ if(j.timings)timings=j.timings;
207
+ const d=(j.choices&&j.choices[0]&&j.choices[0].delta)||{};
208
+ if(d.reasoning_content){think+=d.reasoning_content;nT++;pulse(true);
209
+ if(!tFirst)tFirst=performance.now();
210
+ thk.lastElementChild.textContent=think.slice(-1500);
211
+ thk.lastElementChild.scrollTop=1e9}
212
+ if(d.content){content+=d.content;nA++;pulse(false);
213
+ if(!tFirst)tFirst=performance.now();
214
+ if(!tThinkEnd)tThinkEnd=performance.now();
215
+ hold.innerHTML=md(content)}
216
+ }
217
+ $("msgs").scrollTop=1e9;
218
+ }
219
+ }catch(e){hold.textContent="error: "+e}
220
+ const tEnd=performance.now();
221
+ if(think){thk.open=false;
222
+ thk.firstElementChild.textContent="thought for ~"+think.split(/\\s+/).length+" words (click to expand)"}
223
+ else thk.remove();
224
+ if(!content)content="(empty reply — try again)";
225
+ hist.push({role:"assistant",content:content});
226
+ const tps=timings?timings.predicted_per_second:((nT+nA)>0?(nT+nA)/((tEnd-t0)/1000):0);
227
+ hold.innerHTML=md(content)+'<span class="t">'+tps.toFixed(1)+' tok/s'+(timings?"":" (client est.)")+' · predicted '+PRED+'</span>';
228
+ $("ttft").textContent=tFirst?fmt(tFirst-t0):"—";
229
+ $("sthink").textContent=nT?fmt((tThinkEnd||tEnd)-(tFirst||t0))+" · "+nT+" tok":"off";
230
+ $("sans").textContent=nA?fmt(tEnd-(tThinkEnd||tFirst||t0))+" · "+nA+" tok":"—";
231
+ if(tps>0){pts.push(tps);
232
+ const avg=pts.reduce((a,b)=>a+b,0)/pts.length;
233
+ $("meas").textContent=avg.toFixed(1);
234
+ const dd=(avg/PRED-1)*100;
235
+ $("delta").textContent=(dd>=0?"+":"")+dd.toFixed(0)+"%";
236
+ $("delta").style.color=Math.abs(dd)<25?"var(--acc)":"var(--warn)";
237
+ $("log").insertAdjacentHTML("afterbegin","#"+pts.length+": "+tps.toFixed(1)+" tok/s<br>");}
238
+ $("send").disabled=false;$("msgs").scrollTop=1e9;
239
+ }
240
+ $("send").onclick=send;
241
+ $("inp").addEventListener("keydown",e=>{if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();send();}});
242
+ </script></body></html>"""
243
+
244
+
245
+ def make_handler(upstream, page):
246
+ class H(BaseHTTPRequestHandler):
247
+ protocol_version = "HTTP/1.1"
248
+
249
+ def log_message(self, *a):
250
+ pass
251
+
252
+ def do_GET(self):
253
+ body = page.encode("utf-8")
254
+ self.send_response(200)
255
+ self.send_header("Content-Type", "text/html; charset=utf-8")
256
+ self.send_header("Content-Length", str(len(body)))
257
+ self.end_headers()
258
+ self.wfile.write(body)
259
+
260
+ def do_POST(self):
261
+ if self.path != "/api/chat":
262
+ self.send_response(404)
263
+ self.send_header("Content-Length", "0")
264
+ self.end_headers()
265
+ return
266
+ n = int(self.headers.get("Content-Length", 0))
267
+ body = json.loads(self.rfile.read(n) or b"{}")
268
+ body.setdefault("timings_per_token", True)
269
+ body.setdefault("max_tokens", 2048)
270
+ if body.pop("think", True) is False: # UI toggle: skip thinking entirely
271
+ body["chat_template_kwargs"] = {"enable_thinking": False}
272
+ stream = bool(body.get("stream", False))
273
+ req = urllib.request.Request(upstream + "/v1/chat/completions",
274
+ data=json.dumps(body).encode(),
275
+ headers={"Content-Type": "application/json"})
276
+ try:
277
+ r = urllib.request.urlopen(req, timeout=600)
278
+ self.send_response(200)
279
+ if stream:
280
+ self.send_header("Content-Type", "text/event-stream")
281
+ self.send_header("Cache-Control", "no-cache")
282
+ self.send_header("Transfer-Encoding", "chunked")
283
+ self.end_headers()
284
+ while True:
285
+ chunk = r.read1(8192) if hasattr(r, "read1") else r.read(8192)
286
+ if not chunk:
287
+ break
288
+ self.wfile.write(b"%x\r\n" % len(chunk) + chunk + b"\r\n")
289
+ self.wfile.flush()
290
+ self.wfile.write(b"0\r\n\r\n")
291
+ else:
292
+ data = r.read()
293
+ self.send_header("Content-Type", "application/json")
294
+ self.send_header("Content-Length", str(len(data)))
295
+ self.end_headers()
296
+ self.wfile.write(data)
297
+ except Exception as e:
298
+ try:
299
+ err = json.dumps({"error": str(e)}).encode()
300
+ self.send_response(502)
301
+ self.send_header("Content-Length", str(len(err)))
302
+ self.end_headers()
303
+ self.wfile.write(err)
304
+ except Exception:
305
+ pass
306
+ return H
307
+
308
+
309
+ def _anatomy(gguf):
310
+ """Exact layer/expert counts from the file, for the galaxy."""
311
+ try:
312
+ from gguf import GGUFReader
313
+ r = GGUFReader(gguf)
314
+ nl = ne = nk = 0
315
+ for f in r.fields.values():
316
+ if f.name.endswith(".block_count"):
317
+ nl = int(f.parts[f.data[0]][0])
318
+ elif f.name.endswith(".expert_count"):
319
+ ne = int(f.parts[f.data[0]][0])
320
+ elif f.name.endswith(".expert_used_count"):
321
+ nk = int(f.parts[f.data[0]][0])
322
+ return (nl or 32), ne, (nk if ne else 0)
323
+ except Exception:
324
+ return 32, 0, 0
325
+
326
+
327
+ def dashboard(a):
328
+ best, flags = runtime.best_flags(a)
329
+ binp = runtime.find_llama(a.llama_dir, "llama-server")
330
+ sport = a.server_port
331
+ cmd = [binp, "-m", a.gguf, "--port", str(sport)] + flags
332
+ print(f"[quantprobe] placement: {best[0]} (predicted {best[1]:.1f} tok/s)")
333
+ print("[quantprobe] llama-server:", " ".join(cmd))
334
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
335
+ upstream = f"http://127.0.0.1:{sport}"
336
+ print("[quantprobe] waiting for the model to finish loading (big models on slow disks take minutes)...")
337
+ ready_req = json.dumps({"messages": [{"role": "user", "content": "hi"}], "max_tokens": 1}).encode()
338
+ for _ in range(600):
339
+ try:
340
+ urllib.request.urlopen(urllib.request.Request(
341
+ upstream + "/v1/chat/completions", data=ready_req,
342
+ headers={"Content-Type": "application/json"}), timeout=8)
343
+ break # a real completion answered: weights are up
344
+ except Exception:
345
+ if proc.poll() is not None:
346
+ raise SystemExit("llama-server exited during startup — check the model path/flags")
347
+ time.sleep(2)
348
+ tiers = ""
349
+ for name, cap, used in runtime.tier_view(a, best):
350
+ pct = min(100, used / cap * 100) if cap else 0
351
+ tiers += (f'<div class="tier"><div class="l"><span>{name}</span>'
352
+ f'<span>{used:.1f} / {cap:.0f} GB</span></div>'
353
+ f'<div class="bar"><div class="fill" style="width:{pct:.0f}%"></div></div></div>')
354
+ nl, ne, nk = _anatomy(a.gguf)
355
+ pname = best[0]
356
+ if "hybrid" in pname:
357
+ atier, etier, ecolv = "VRAM", "RAM", "--ram"
358
+ elif "all in VRAM" in pname:
359
+ atier, etier, ecolv = "VRAM", "VRAM", "--acc"
360
+ elif "disk" in pname:
361
+ atier, etier, ecolv = "RAM", "disk (streamed, RAM-cached)", "--disk"
362
+ else:
363
+ atier, etier, ecolv = "RAM", "RAM", "--ram"
364
+ page = (DASH_HTML.replace("{{MODEL}}", os.path.basename(a.gguf))
365
+ .replace("{{PLACEMENT}}", pname).replace("{{PRED}}", f"{best[1]:.1f}")
366
+ .replace("{{FLAGS}}", " ".join(flags)).replace("{{TIERS}}", tiers)
367
+ .replace("{{NL}}", str(nl)).replace("{{NE}}", str(ne)).replace("{{NK}}", str(nk))
368
+ .replace("{{ATIER}}", atier).replace("{{ETIER}}", etier).replace("{{ECOLV}}", ecolv))
369
+ ThreadingHTTPServer.allow_reuse_address = False # Windows: reuse allows silent double-binds
370
+ try:
371
+ srv = ThreadingHTTPServer(("127.0.0.1", a.port), make_handler(upstream, page))
372
+ except OSError:
373
+ proc.terminate()
374
+ raise SystemExit(f"port {a.port} is already in use - another dashboard running? (pass --port N)")
375
+ url = f"http://127.0.0.1:{a.port}"
376
+ print(f"[quantprobe] dashboard v2.1: {url} (Ctrl-C stops both)")
377
+ if not a.no_open:
378
+ threading.Timer(1.0, lambda: webbrowser.open(url)).start()
379
+ try:
380
+ srv.serve_forever()
381
+ except KeyboardInterrupt:
382
+ pass
383
+ finally:
384
+ proc.terminate()
@@ -20,7 +20,9 @@ def _run(cmd, timeout=10):
20
20
 
21
21
  # name-fragment -> (VRAM bandwidth GB/s, geta, gl). 1060 measured on the reference box; rest spec-sheet [table].
22
22
  GPU_TABLE = [
23
- ("5090", 1792, 0.62, 0.42), ("4090", 1008, 0.62, 0.42), ("4080", 717, 0.6, 0.4),
23
+ ("5090", 1792, 0.62, 0.42), ("5080", 960, 0.62, 0.42), ("5070 ti", 896, 0.62, 0.42),
24
+ ("5070", 672, 0.6, 0.4), ("5060 ti", 448, 0.58, 0.38), ("5060", 448, 0.58, 0.38),
25
+ ("4090", 1008, 0.62, 0.42), ("4080", 717, 0.6, 0.4),
24
26
  ("4070", 504, 0.55, 0.35), ("4060", 272, 0.5, 0.3),
25
27
  ("3090", 936, 0.6, 0.4), ("3080", 760, 0.58, 0.38), ("3070", 448, 0.52, 0.32),
26
28
  ("3060 ti", 448, 0.5, 0.3), ("3060", 360, 0.5, 0.3), ("3050", 224, 0.45, 0.28),
@@ -107,11 +109,13 @@ def detect():
107
109
  gs = gpus()
108
110
  if gs:
109
111
  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
+ per = [gpu_lookup(g[0]) for g in gs] # per-card lookup (mixed pairs differ)
113
+ bw_sum = sum(p[0] for p in per)
114
+ vram_bw = bw_sum * (1.0 if len(gs) == 1 else 0.85) # aggregate w/ tensor-parallel loss
115
+ geta, gl, src = per[0][1], per[0][2], per[0][3] # eta class from the primary card
112
116
  names = " + ".join(g[0] for g in gs)
113
117
  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 ""))
118
+ + (f" (x{len(gs)} per-card sum, 0.85 TP efficiency [est]; slower card gates its share)" if len(gs) > 1 else ""))
115
119
  hw.update(vram=vram, vram_bw=round(vram_bw), geta=geta, gl=gl)
116
120
  else:
117
121
  hw.update(vram=0, vram_bw=0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantprobe
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Probe-then-quantize for LLMs: measure a model's fragility curve, plan bit/tier placement by the tiered decode law, and emit ready-to-run llama.cpp recipes.
5
5
  Author: Federico Sciuca
6
6
  License: MIT
@@ -85,15 +85,17 @@ Full statements, each with its establishing measurement and a falsifiable predic
85
85
  <p align="center"><img src="weights/data/x_chart_I_kvdepth.png" width="700" alt="Decode speed versus context depth: measured 20.02 tok/s at depth zero falling to 16.12 at 16384, on the Law 4 v2 curve with eta_kv 0.70"></p>
86
86
 
87
87
 
88
- ## Install — and the ten commands
88
+ ## Install — and the eleven commands
89
89
 
90
90
  ```bash
91
91
  pip install quantprobe
92
+ quantprobe auto qwen3-coder --tps 15 --run # empty machine -> optimal quant chosen, fetched, chatting
92
93
  ```
93
94
 
94
- **Zero-config on your own box**: `quantprobe plan --gguf model.gguf` auto-detects the machine and reads the model from the file. Presets/flags estimate any *other* machine. `hw`/`plan`/`target`/`optimize` need nothing else installed; the rest drive stock [llama.cpp](https://github.com/ggml-org/llama.cpp/releases) (point at it with `--llama-dir`/`QUANTPROBE_LLAMA_DIR`/`PATH`; preview any command with `--dry`).
95
+ **Zero-config on your own box**: `quantprobe plan --gguf model.gguf` auto-detects the machine and reads the model from the file. Presets/flags estimate any *other* machine. `hw`/`plan`/`target`/`optimize` need nothing else installed (`auto` needs network for the fetch); the rest drive stock [llama.cpp](https://github.com/ggml-org/llama.cpp/releases) (point at it with `--llama-dir`/`QUANTPROBE_LLAMA_DIR`/`PATH`; preview any command with `--dry`).
95
96
 
96
97
  ```bash
98
+ quantprobe auto qwen3-30b --tps 15 # ONE command: optimizer picks bits, closest quant fetched, run command printed
97
99
  quantprobe hw # what the law sees on THIS machine (every value source-tagged)
98
100
  quantprobe plan --gguf model.gguf # zero-config prediction: placement + tok/s + the launch command
99
101
  quantprobe optimize --tps 20 # CHEAPEST PATH to a target: bits x placement x hardware, Pareto-ranked
@@ -103,7 +105,7 @@ quantprobe quantize --gguf f16.gguf --out 2bit.gguf # COMPRESS: depth-aware
103
105
  quantprobe probe --gguf f16.gguf --eval wiki.test.raw # measure YOUR model's fragile band (~30 min); --apply builds it
104
106
  quantprobe run --gguf 2bit.gguf # plan the placement, then LAUNCH llama.cpp chat
105
107
  quantprobe bench --gguf 2bit.gguf --contribute # predicted vs measured on your box; opt-in datapoint
106
- quantprobe dashboard --gguf 2bit.gguf # chat while every reply is scored against the prediction
108
+ quantprobe dashboard --gguf 2bit.gguf # the law LIVE: neuron galaxy + thinking toggle, every reply scored vs prediction
107
109
  ```
108
110
 
109
111
  The loop is self-validating: `plan` predicted 17.5 for a file we then measured at **18.32 ± 0.17**; the config months of research converged to is what `optimize` picks blind. A measured example of what that's worth: the same model, mis-specified vs law-routed, is **3.38 vs 18.32 tok/s (×5.4)** — [worked examples](docs/EXAMPLES.md).
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  quantprobe/__init__.py
5
+ quantprobe/auto.py
5
6
  quantprobe/cli.py
6
7
  quantprobe/dashboard.py
7
8
  quantprobe/detect.py
@@ -1,179 +0,0 @@
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()
File without changes
File without changes