quantprobe 1.4.0__tar.gz → 1.5.1__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.
- {quantprobe-1.4.0/quantprobe.egg-info → quantprobe-1.5.1}/PKG-INFO +7 -4
- {quantprobe-1.4.0 → quantprobe-1.5.1}/README.md +6 -3
- {quantprobe-1.4.0 → quantprobe-1.5.1}/pyproject.toml +1 -1
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/__init__.py +1 -1
- quantprobe-1.5.1/quantprobe/auto.py +171 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/cli.py +15 -0
- quantprobe-1.5.1/quantprobe/dashboard.py +384 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/detect.py +8 -4
- {quantprobe-1.4.0 → quantprobe-1.5.1/quantprobe.egg-info}/PKG-INFO +7 -4
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe.egg-info/SOURCES.txt +1 -0
- quantprobe-1.4.0/quantprobe/dashboard.py +0 -179
- {quantprobe-1.4.0 → quantprobe-1.5.1}/LICENSE +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/fetch.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/optimize.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/plan.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/probe.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/runtime.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/spec.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe/target.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe.egg-info/dependency_links.txt +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe.egg-info/entry_points.txt +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe.egg-info/requires.txt +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/quantprobe.egg-info/top_level.txt +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/setup.cfg +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/tests/test_engine.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/tests/test_evaluator.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/tests/test_pipeline.py +0 -0
- {quantprobe-1.4.0 → quantprobe-1.5.1}/tests/test_transforms.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantprobe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
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,18 @@ 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
|
|
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
|
|
99
|
+
quantprobe auto qwen3-30b --custom # THE PRODUCT: probe YOUR model, build its personalized depth-aware GGUF
|
|
97
100
|
quantprobe hw # what the law sees on THIS machine (every value source-tagged)
|
|
98
101
|
quantprobe plan --gguf model.gguf # zero-config prediction: placement + tok/s + the launch command
|
|
99
102
|
quantprobe optimize --tps 20 # CHEAPEST PATH to a target: bits x placement x hardware, Pareto-ranked
|
|
@@ -103,7 +106,7 @@ quantprobe quantize --gguf f16.gguf --out 2bit.gguf # COMPRESS: depth-aware
|
|
|
103
106
|
quantprobe probe --gguf f16.gguf --eval wiki.test.raw # measure YOUR model's fragile band (~30 min); --apply builds it
|
|
104
107
|
quantprobe run --gguf 2bit.gguf # plan the placement, then LAUNCH llama.cpp chat
|
|
105
108
|
quantprobe bench --gguf 2bit.gguf --contribute # predicted vs measured on your box; opt-in datapoint
|
|
106
|
-
quantprobe dashboard --gguf 2bit.gguf #
|
|
109
|
+
quantprobe dashboard --gguf 2bit.gguf # the law LIVE: neuron galaxy + thinking toggle, every reply scored vs prediction
|
|
107
110
|
```
|
|
108
111
|
|
|
109
112
|
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,18 @@ 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
|
|
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
|
|
81
|
+
quantprobe auto qwen3-30b --custom # THE PRODUCT: probe YOUR model, build its personalized depth-aware GGUF
|
|
79
82
|
quantprobe hw # what the law sees on THIS machine (every value source-tagged)
|
|
80
83
|
quantprobe plan --gguf model.gguf # zero-config prediction: placement + tok/s + the launch command
|
|
81
84
|
quantprobe optimize --tps 20 # CHEAPEST PATH to a target: bits x placement x hardware, Pareto-ranked
|
|
@@ -85,7 +88,7 @@ quantprobe quantize --gguf f16.gguf --out 2bit.gguf # COMPRESS: depth-aware
|
|
|
85
88
|
quantprobe probe --gguf f16.gguf --eval wiki.test.raw # measure YOUR model's fragile band (~30 min); --apply builds it
|
|
86
89
|
quantprobe run --gguf 2bit.gguf # plan the placement, then LAUNCH llama.cpp chat
|
|
87
90
|
quantprobe bench --gguf 2bit.gguf --contribute # predicted vs measured on your box; opt-in datapoint
|
|
88
|
-
quantprobe dashboard --gguf 2bit.gguf #
|
|
91
|
+
quantprobe dashboard --gguf 2bit.gguf # the law LIVE: neuron galaxy + thinking toggle, every reply scored vs prediction
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
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.
|
|
7
|
+
version = "1.5.1"
|
|
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.
|
|
11
|
+
__version__ = "1.5.1"
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
WIKI_URL = "https://huggingface.co/datasets/ggml-org/ci/resolve/main/wikitext-2-raw-v1.zip"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ensure_eval(dest_dir):
|
|
44
|
+
"""The held-out corpus for probing, fetched once (1.3 MB)."""
|
|
45
|
+
import os, zipfile, io
|
|
46
|
+
path = os.path.join(dest_dir, "wiki.test.raw")
|
|
47
|
+
if os.path.isfile(path):
|
|
48
|
+
return path
|
|
49
|
+
print("[quantprobe auto] fetching the WikiText-2 eval corpus (1.3 MB, one time)...")
|
|
50
|
+
with urllib.request.urlopen(WIKI_URL, timeout=60) as r:
|
|
51
|
+
z = zipfile.ZipFile(io.BytesIO(r.read()))
|
|
52
|
+
with z.open("wikitext-2-raw/wiki.test.raw") as f, open(path, "wb") as out:
|
|
53
|
+
out.write(f.read())
|
|
54
|
+
return path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pick_source(files, t):
|
|
58
|
+
"""High-precision source for the custom build: prefer Q8-class (smallest requantizable), else max bits."""
|
|
59
|
+
cands = []
|
|
60
|
+
for path, size in files:
|
|
61
|
+
if "-of-" in path.lower():
|
|
62
|
+
continue # split multi-part files: out of scope for v1
|
|
63
|
+
bits = size * 8 / (t * 1e9)
|
|
64
|
+
cands.append((bits, size, path))
|
|
65
|
+
if not cands:
|
|
66
|
+
return None
|
|
67
|
+
hi = sorted([c for c in cands if c[0] >= 7.5], key=lambda c: c[1])
|
|
68
|
+
return hi[0] if hi else max(cands)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(a):
|
|
72
|
+
target = a.target
|
|
73
|
+
if target in MODEL_REPOS:
|
|
74
|
+
repo, t, ac, ne, moe = MODEL_REPOS[target]
|
|
75
|
+
else:
|
|
76
|
+
repo = target
|
|
77
|
+
if not getattr(a, "total", None):
|
|
78
|
+
raise SystemExit(f"'{target}' is not a preset ({', '.join(MODEL_REPOS)}) - for a raw HF "
|
|
79
|
+
"repo also pass --total (B params) and, for MoE, --active/--always-active")
|
|
80
|
+
t, ac = a.total, a.active or a.total
|
|
81
|
+
ne = a.always_active or (ac if ac >= t * 0.9 else ac * 0.35)
|
|
82
|
+
moe = ac < t * 0.9
|
|
83
|
+
a.total, a.active, a.always_active, a.model = t, ac, ne, None
|
|
84
|
+
|
|
85
|
+
# 1. what does the optimizer want on THIS machine?
|
|
86
|
+
a.gguf = None
|
|
87
|
+
ranked = optmod.run(a)
|
|
88
|
+
want_bits = ranked[0]["bits"]
|
|
89
|
+
_, _, _, _, _, (vc, vb, rc, rb, db, geta, gl), ctx, kvp = optmod.resolve(a)
|
|
90
|
+
|
|
91
|
+
# 2. pick the closest file BY SIZE (bits = size*8/params; honest, format-agnostic)
|
|
92
|
+
try:
|
|
93
|
+
files = list_ggufs(repo)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise SystemExit(f"could not list {repo}: {e}")
|
|
96
|
+
if not files:
|
|
97
|
+
raise SystemExit(f"no GGUF files found in {repo}")
|
|
98
|
+
scored = []
|
|
99
|
+
for path, size in files:
|
|
100
|
+
bits = size * 8 / (t * 1e9)
|
|
101
|
+
if bits < 1.0 or bits > 9:
|
|
102
|
+
continue
|
|
103
|
+
_, _, cfgs = planmod.evaluate(t, ac, ne, moe, bits, vc, vb, rc, rb, db, geta, 1.0, gl,
|
|
104
|
+
ctx=ctx, kvp=kvp)
|
|
105
|
+
cfgs = [c for c in cfgs if "expert cache" not in c[0]]
|
|
106
|
+
if not cfgs:
|
|
107
|
+
continue
|
|
108
|
+
scored.append((abs(bits - want_bits), -cfgs[0][1], path, size, bits, cfgs[0]))
|
|
109
|
+
if not scored:
|
|
110
|
+
raise SystemExit("no usable quant in that repo for this machine")
|
|
111
|
+
scored.sort()
|
|
112
|
+
_, _, path, size, bits, best = scored[0]
|
|
113
|
+
|
|
114
|
+
if getattr(a, "custom", False):
|
|
115
|
+
src = pick_source(files, t)
|
|
116
|
+
if not src:
|
|
117
|
+
raise SystemExit("no usable high-precision source in that repo (split files unsupported)")
|
|
118
|
+
sbits, ssize, spath = src
|
|
119
|
+
import os
|
|
120
|
+
dest = getattr(a, "dir", None) or "./models"
|
|
121
|
+
os.makedirs(dest, exist_ok=True)
|
|
122
|
+
print("\n[quantprobe auto --custom] source: " + spath + f" ({ssize/1e9:.1f} GB, {sbits:.1f}-bit)")
|
|
123
|
+
print(" pipeline: fetch source -> probe the fragile band (~30-60 min) -> build the")
|
|
124
|
+
print(" depth-aware GGUF. Interrupt anytime; the fetch resumes.")
|
|
125
|
+
if getattr(a, "dry", False):
|
|
126
|
+
print(" (--dry: nothing downloaded)")
|
|
127
|
+
return spath
|
|
128
|
+
from . import fetch as fetchmod
|
|
129
|
+
if not fetchmod.fetch(repo, dest, spath, fetchmod.token()):
|
|
130
|
+
raise SystemExit("source download failed (re-run: it resumes)")
|
|
131
|
+
srcfull = os.path.join(dest, os.path.basename(spath))
|
|
132
|
+
evalf = ensure_eval(dest)
|
|
133
|
+
from . import probe as probemod
|
|
134
|
+
import argparse
|
|
135
|
+
out = os.path.join(dest, os.path.basename(spath).rsplit(".gguf", 1)[0] + "-depthaware.gguf")
|
|
136
|
+
pa = argparse.Namespace(gguf=srcfull, bands=4, chunks=32, eval=evalf, ngl=99,
|
|
137
|
+
workdir=dest, llama_dir=getattr(a, "llama_dir", None),
|
|
138
|
+
apply=True, out=out, dry_run=False)
|
|
139
|
+
probemod.run(pa)
|
|
140
|
+
print("\n[quantprobe auto --custom] your personalized model:")
|
|
141
|
+
print(" quantprobe run --gguf " + out)
|
|
142
|
+
if getattr(a, "run", False):
|
|
143
|
+
from . import runtime
|
|
144
|
+
a.gguf = out; a.bits = None
|
|
145
|
+
runtime.run(a)
|
|
146
|
+
return out
|
|
147
|
+
print(f"\n[quantprobe auto] optimizer wants ~{want_bits:g}-bit; closest file in {repo}:")
|
|
148
|
+
print(f" {path} ({size/1e9:.1f} GB, {bits:.2f} effective bits)")
|
|
149
|
+
print(f" predicted on this machine: {best[1]:.1f} tok/s ({best[0]})")
|
|
150
|
+
if getattr(a, "dry", False):
|
|
151
|
+
print(" (--dry: nothing downloaded)")
|
|
152
|
+
return path
|
|
153
|
+
# 3. fetch it (resumable), then hand off
|
|
154
|
+
from . import fetch as fetchmod
|
|
155
|
+
dest = getattr(a, "dir", None) or "./models"
|
|
156
|
+
import os
|
|
157
|
+
os.makedirs(dest, exist_ok=True)
|
|
158
|
+
ok = fetchmod.fetch(repo, dest, path, fetchmod.token())
|
|
159
|
+
if not ok:
|
|
160
|
+
raise SystemExit("download failed (it resumes: re-run the same command)")
|
|
161
|
+
full = os.path.join(dest, os.path.basename(path))
|
|
162
|
+
print(f"\n[quantprobe auto] ready. Run it:")
|
|
163
|
+
print(f" quantprobe run --gguf {full}")
|
|
164
|
+
print("\n Better quality at the SAME size: rerun with --custom - it probes YOUR model's")
|
|
165
|
+
print(" fragile band (~30-60 min) and builds a depth-aware GGUF personalized to it.")
|
|
166
|
+
if getattr(a, "run", False):
|
|
167
|
+
from . import runtime
|
|
168
|
+
a.gguf = full
|
|
169
|
+
a.bits = None # let autospec read the real file
|
|
170
|
+
runtime.run(a)
|
|
171
|
+
return full
|
|
@@ -91,6 +91,18 @@ 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("--custom", action="store_true", help="THE PRODUCT: fetch a high-precision source, probe YOUR model (~30-60 min), build a depth-aware GGUF personalized to it")
|
|
103
|
+
au.add_argument("--serve", action="store_true", help="with --run: llama-server instead of chat")
|
|
104
|
+
au.add_argument("--extra", default=None)
|
|
105
|
+
|
|
94
106
|
hwp = sub.add_parser("hw", help="detect THIS machine's memory tiers (no flags needed); every value tagged with its source")
|
|
95
107
|
hwp.add_argument("--measure", default=None, metavar="FILE", help="also MEASURE sequential disk read on a real file (e.g. a GGUF)")
|
|
96
108
|
|
|
@@ -116,6 +128,9 @@ def main():
|
|
|
116
128
|
elif a.cmd == "target":
|
|
117
129
|
from . import target
|
|
118
130
|
target.run(a)
|
|
131
|
+
elif a.cmd == "auto":
|
|
132
|
+
from . import auto
|
|
133
|
+
auto.run(a)
|
|
119
134
|
elif a.cmd == "optimize":
|
|
120
135
|
from . import optimize
|
|
121
136
|
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,"&").replace(/</g,"<")}
|
|
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), ("
|
|
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
|
-
|
|
111
|
-
|
|
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)}
|
|
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.
|
|
3
|
+
Version: 1.5.1
|
|
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,18 @@ 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
|
|
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
|
|
99
|
+
quantprobe auto qwen3-30b --custom # THE PRODUCT: probe YOUR model, build its personalized depth-aware GGUF
|
|
97
100
|
quantprobe hw # what the law sees on THIS machine (every value source-tagged)
|
|
98
101
|
quantprobe plan --gguf model.gguf # zero-config prediction: placement + tok/s + the launch command
|
|
99
102
|
quantprobe optimize --tps 20 # CHEAPEST PATH to a target: bits x placement x hardware, Pareto-ranked
|
|
@@ -103,7 +106,7 @@ quantprobe quantize --gguf f16.gguf --out 2bit.gguf # COMPRESS: depth-aware
|
|
|
103
106
|
quantprobe probe --gguf f16.gguf --eval wiki.test.raw # measure YOUR model's fragile band (~30 min); --apply builds it
|
|
104
107
|
quantprobe run --gguf 2bit.gguf # plan the placement, then LAUNCH llama.cpp chat
|
|
105
108
|
quantprobe bench --gguf 2bit.gguf --contribute # predicted vs measured on your box; opt-in datapoint
|
|
106
|
-
quantprobe dashboard --gguf 2bit.gguf #
|
|
109
|
+
quantprobe dashboard --gguf 2bit.gguf # the law LIVE: neuron galaxy + thinking toggle, every reply scored vs prediction
|
|
107
110
|
```
|
|
108
111
|
|
|
109
112
|
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).
|
|
@@ -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 → 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,"&").replace(/</g,"<")}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|