fitlab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fitlab/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """fitlab — will it run on my setup? Fit-check + benchmark wizard for local LLMs."""
2
+ try:
3
+ from importlib.metadata import version as _v
4
+ __version__ = _v("fitlab")
5
+ except Exception: # editable/dev
6
+ __version__ = "0.1.0"
fitlab/bench.py ADDED
@@ -0,0 +1,127 @@
1
+ """Real benchmark via Ollama — identical output schema to scripts/bench_gguf.py."""
2
+ import datetime
3
+ import json
4
+ import shutil
5
+ import statistics
6
+ import subprocess
7
+ import threading
8
+ import time
9
+ import os
10
+ from pathlib import Path
11
+ from urllib import request
12
+
13
+ OLLAMA = "http://127.0.0.1:11434"
14
+ PROMPT_ID = "fitlab-v1-256"
15
+ PROMPT = ("You are benchmarking. Write a precise 400-word technical explanation of how "
16
+ "KV-cache memory grows with context length in transformer decoding, with one worked example.")
17
+ INSTALL_CMD = "curl -fsSL https://ollama.com/install.sh | sh"
18
+
19
+
20
+ def api(path: str, payload: dict | None = None, timeout=600):
21
+ data = json.dumps(payload).encode() if payload else None
22
+ req = request.Request(OLLAMA + path, data=data, headers={"Content-Type": "application/json"})
23
+ with request.urlopen(req, timeout=timeout) as r:
24
+ return json.loads(r.read())
25
+
26
+
27
+ def ollama_installed() -> bool:
28
+ return shutil.which("ollama") is not None
29
+
30
+
31
+ def install_ollama() -> bool:
32
+ return subprocess.run(INSTALL_CMD, shell=True).returncode == 0
33
+
34
+
35
+ def ensure_server(wait_s: int = 60) -> str:
36
+ """Start `ollama serve` if needed; return its version."""
37
+ try:
38
+ return api("/api/version", timeout=3)["version"]
39
+ except Exception:
40
+ subprocess.Popen(["ollama", "serve"], stdout=subprocess.DEVNULL,
41
+ stderr=subprocess.DEVNULL, start_new_session=True)
42
+ for _ in range(wait_s):
43
+ time.sleep(1)
44
+ try:
45
+ return api("/api/version", timeout=3)["version"]
46
+ except Exception:
47
+ pass
48
+ raise RuntimeError("Ollama server did not start — try `ollama serve` in another terminal")
49
+
50
+
51
+ def pull(tag: str) -> None:
52
+ subprocess.run(["ollama", "pull", tag], check=True)
53
+
54
+
55
+ class VramPeak(threading.Thread):
56
+ def __init__(self):
57
+ super().__init__(daemon=True)
58
+ self.peak = 0.0
59
+ self.stop = threading.Event()
60
+
61
+ def run(self):
62
+ while not self.stop.is_set():
63
+ q = subprocess.run("nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits",
64
+ shell=True, text=True, capture_output=True)
65
+ if q.returncode == 0 and q.stdout.strip():
66
+ self.peak = max(self.peak, int(q.stdout.strip().splitlines()[0]) / 1024)
67
+ time.sleep(0.5)
68
+
69
+
70
+ def bench_model(tag: str, gpu: dict, *, model_id: str | None = None, reps: int = 3,
71
+ context: int = 4096, source: str = "community-local", submitter: str = "",
72
+ out_dir: str | Path = "fitlab-results", log=print) -> tuple[dict, Path]:
73
+ """Run the standard FitLab benchmark. `gpu` is hardware.schema_gpu(...) shaped."""
74
+ version = ensure_server()
75
+ t0 = time.time()
76
+ log(f" pulling {tag} …")
77
+ pull(tag)
78
+ mon = VramPeak()
79
+ mon.start()
80
+
81
+ load_t0 = time.time()
82
+ api("/api/generate", {"model": tag, "prompt": "hi", "stream": False,
83
+ "options": {"num_predict": 1}}) # cold load
84
+ load_s = time.time() - load_t0
85
+
86
+ tps_runs, ttfts, prompt_tps, out_tokens = [], [], [], 0
87
+ for i in range(reps):
88
+ r = api("/api/generate", {"model": tag, "prompt": PROMPT, "stream": False,
89
+ "options": {"num_predict": 256, "num_ctx": context,
90
+ "temperature": 0.0, "seed": 42}})
91
+ tps_runs.append(r["eval_count"] / (r["eval_duration"] / 1e9))
92
+ ttfts.append(r["prompt_eval_duration"] / 1e6)
93
+ prompt_tps.append(r["prompt_eval_count"] / (r["prompt_eval_duration"] / 1e9))
94
+ out_tokens += r["eval_count"]
95
+ log(f" run {i + 1}/{reps}: {tps_runs[-1]:.1f} tok/s")
96
+ mon.stop.set()
97
+ mon.join(timeout=2)
98
+
99
+ details = next((m for m in api("/api/tags")["models"]
100
+ if m["name"].startswith(tag.split(":")[0])), {})
101
+ quant = (details.get("details") or {}).get("quantization_level", "Q4_K_M")
102
+
103
+ result = {
104
+ "schema_version": "1.0",
105
+ "model_id": model_id or tag.replace(":", "-").replace(".", ""),
106
+ "ollama_tag": tag,
107
+ "runtime": {"engine": "ollama", "version": version},
108
+ "gpu": gpu,
109
+ "quant": quant,
110
+ "context": context,
111
+ "metrics": {"load_s": round(load_s, 2), "ttft_ms": round(statistics.median(ttfts), 1),
112
+ "prompt_tps": round(statistics.median(prompt_tps), 1),
113
+ "gen_tps": round(statistics.median(tps_runs), 2),
114
+ "gen_tps_runs": [round(t, 2) for t in tps_runs],
115
+ "peak_vram_gb": round(mon.peak, 2), "output_tokens": out_tokens},
116
+ "run": {"date": datetime.date.today().isoformat(), "prompt_id": PROMPT_ID,
117
+ "repetitions": reps, "duration_s": round(time.time() - t0, 1)},
118
+ "provenance": {"source": source, "submitter": submitter,
119
+ "environment": os.environ.get("FITLAB_ENV", "cli"),
120
+ "run_url": os.environ.get("RUN_URL", "")},
121
+ }
122
+ out = Path(out_dir)
123
+ out.mkdir(parents=True, exist_ok=True)
124
+ fname = f"{result['run']['date']}_{result['model_id']}_{gpu.get('profile_id') or 'unknown'}.json"
125
+ path = out / fname
126
+ path.write_text(json.dumps(result, indent=2))
127
+ return result, path
fitlab/cli.py ADDED
@@ -0,0 +1,303 @@
1
+ """fitlab CLI — `fitlab` with no arguments launches the wizard."""
2
+ import argparse
3
+ import json
4
+ import os
5
+ import sys
6
+ from urllib.parse import quote
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from . import __version__, bench, estimate, hardware, registry
12
+
13
+ console = Console(highlight=False)
14
+ REPO = os.environ.get("FITLAB_REPO", "OWNER/llm-fitlab")
15
+
16
+ VERDICT_STYLE = {"fits": "bold green", "tight": "bold yellow",
17
+ "offload": "dark_orange", "no": "bold red", "-": "dim"}
18
+
19
+ # reference profiles for `fitlab check --gpu <id>` (mirrors data/hardware.yaml)
20
+ PROFILES = {
21
+ "rtx3060-12": ("RTX 3060 12GB", 12, 360),
22
+ "t4-16": ("T4 · Colab free", 15, 320),
23
+ "rtx4070-12": ("RTX 4070 12GB", 12, 504),
24
+ "rtx4060ti-16": ("RTX 4060 Ti 16GB", 16, 288),
25
+ "apple-m-16": ("Apple M-series 16GB unified", 11, 120),
26
+ "cpu-only": ("CPU only", 0, 0),
27
+ }
28
+
29
+
30
+ def ask(prompt: str, default: str = "") -> str:
31
+ suffix = f" [dim]({default})[/]" if default else ""
32
+ console.print(f"[green]?[/] {prompt}{suffix} ", end="")
33
+ try:
34
+ val = input().strip()
35
+ except (EOFError, KeyboardInterrupt):
36
+ console.print("\n[dim]aborted[/]")
37
+ sys.exit(1)
38
+ if val.lower() in {"q", "quit", "exit"}:
39
+ console.print("[dim]bye — nothing was run[/]")
40
+ sys.exit(0)
41
+ return val or default
42
+
43
+
44
+ def banner():
45
+ console.print(f"[green]// llm fitlab[/] [dim]— fit-check console · v{__version__}[/]")
46
+
47
+
48
+ def load_registry(refresh: bool = False) -> dict:
49
+ reg, src = registry.load(refresh)
50
+ label = {"live": "live (weekly)", "cache": "cached <24h", "stale-cache": "stale cache — offline?",
51
+ "bundled-seed": "bundled seed — set FITLAB_REGISTRY_URL for weekly updates"}[src]
52
+ console.print(f"[dim]registry {reg.get('generated_at', '?')} · "
53
+ f"{len(reg.get('models', {}))} models · {label}[/]")
54
+ return reg
55
+
56
+
57
+ def hw_summary(hw: dict):
58
+ vram = f"{hw['vram_gb']} GB VRAM" if hw["vram_gb"] else "no VRAM"
59
+ console.print(f"[bold]your rig:[/] {hw['name']} · {vram} · ~{hw['bw']} GB/s"
60
+ + (f" [dim]{hw['note']}[/]" if hw.get("note") else ""))
61
+
62
+
63
+ def ordered_models(reg: dict, category: str):
64
+ cats = reg.get("categories", {})
65
+ if category == "all":
66
+ seen, out = set(), []
67
+ for c in cats.values():
68
+ for t in c.get("top", []):
69
+ if t["id"] not in seen and t["id"] in reg["models"]:
70
+ seen.add(t["id"])
71
+ out.append(t["id"])
72
+ return out
73
+ return [t["id"] for t in cats.get(category, {}).get("top", []) if t["id"] in reg["models"]]
74
+
75
+
76
+ def fit_table(reg: dict, ids, hw: dict, quant: str, ctx: int, limit: int | None = None):
77
+ tbl = Table(title=f"fit check · {hw['name']} · {quant} · {ctx // 1024}K ctx",
78
+ title_style="bold", header_style="dim")
79
+ for col in ("#", "model", "params", "est GB", "verdict", "~tok/s", "ollama tag"):
80
+ tbl.add_column(col)
81
+ rows = []
82
+ for i, mid in enumerate(ids[:limit] if limit else ids, 1):
83
+ m = reg["models"][mid]
84
+ if not m.get("arch"):
85
+ continue
86
+ f = estimate.fit(m, quant, ctx, hw["vram_gb"], hw["bw"])
87
+ rows.append((mid, m, f))
88
+ tps = "—" if f["verdict"] == "no" else (f"~{f['est_tps']}" if f["est_tps"] else "n/a")
89
+ tbl.add_row(str(i), m["hf_id"],
90
+ f"{m.get('active_params_b') or m['params_b']}B",
91
+ f"{f['est_gb']:.1f}",
92
+ f"[{VERDICT_STYLE[f['verdict']]}]{f['verdict'].upper()}[/]",
93
+ tps, m.get("ollama_tag") or "[dim]fit-only[/]")
94
+ console.print(tbl)
95
+ return rows
96
+
97
+
98
+ def submit_url(result: dict) -> str:
99
+ title = f"[bench] {result['ollama_tag']} on {result['gpu']['name']}"
100
+ body = "```json\n" + json.dumps(result, indent=2) + "\n```"
101
+ return (f"https://github.com/{REPO}/issues/new?labels=benchmark-submission"
102
+ f"&title={quote(title)}&body={quote(body)}")
103
+
104
+
105
+ def run_benchmarks(picked, hw, reps: int, ctx: int, submitter: str):
106
+ if not bench.ollama_installed():
107
+ console.print("[yellow]Ollama is not installed[/] — it is the engine FitLab benchmarks with.")
108
+ if ask("Install now via the official script? y/N", "n").lower() == "y":
109
+ if not bench.install_ollama():
110
+ console.print(f"[red]install failed[/] — run manually: {bench.INSTALL_CMD}")
111
+ return []
112
+ else:
113
+ console.print(f"[dim]install later with:[/] {bench.INSTALL_CMD}")
114
+ return []
115
+ results = []
116
+ for mid, m, _f in picked:
117
+ console.rule(f"[green]{m['ollama_tag']}[/]")
118
+ try:
119
+ result, path = bench.bench_model(
120
+ m["ollama_tag"], hardware.schema_gpu(hw), model_id=mid, reps=reps,
121
+ context=ctx, submitter=submitter, log=lambda s: console.print(f"[dim]{s}[/]"))
122
+ results.append((result, path))
123
+ console.print(f"[bold green]✓[/] {result['metrics']['gen_tps']} tok/s median · "
124
+ f"ttft {result['metrics']['ttft_ms']:.0f} ms · "
125
+ f"peak {result['metrics']['peak_vram_gb']} GB → [dim]{path}[/]")
126
+ except Exception as e:
127
+ console.print(f"[red]✗ {mid}: {e}[/]")
128
+ if results:
129
+ console.rule("[green]done")
130
+ tbl = Table(header_style="dim")
131
+ for col in ("model", "tok/s", "ttft ms", "load s", "peak GB"):
132
+ tbl.add_column(col)
133
+ for r, _ in results:
134
+ mm = r["metrics"]
135
+ tbl.add_row(r["ollama_tag"], str(mm["gen_tps"]), f"{mm['ttft_ms']:.0f}",
136
+ str(mm["load_s"]), str(mm["peak_vram_gb"]))
137
+ console.print(tbl)
138
+ console.print("\n[bold]share your numbers[/] — open to file a prefilled submission "
139
+ "(CI validates it and your rig joins the radar):")
140
+ for r, _ in results:
141
+ console.print(f" [green]↗[/] {submit_url(r)[:120]}…" if len(submit_url(r)) > 120
142
+ else f" [green]↗[/] {submit_url(r)}")
143
+ return results
144
+
145
+
146
+ def wizard():
147
+ banner()
148
+ hw = hardware.detect()
149
+ hw_summary(hw)
150
+ reg = load_registry()
151
+ console.print()
152
+
153
+ cats = list(reg.get("categories", {}).keys()) + ["all"]
154
+ for i, c in enumerate(cats, 1):
155
+ title = reg["categories"][c]["title"] if c in reg.get("categories", {}) else "everything, deduplicated"
156
+ console.print(f" [green]{i}[/] {c} [dim]— {title}[/]")
157
+ pick = ask(f"Category 1-{len(cats)}", "1")
158
+ try:
159
+ category = cats[int(pick) - 1]
160
+ except (ValueError, IndexError):
161
+ console.print("[red]not a valid choice[/]")
162
+ sys.exit(1)
163
+
164
+ ids = ordered_models(reg, category)
165
+ rows = fit_table(reg, ids, hw, "Q4_K_M", 8192)
166
+ benchable = [(mid, m, f) for mid, m, f in rows if m.get("ollama_tag") and f["verdict"] != "no"]
167
+
168
+ sel = ask("Benchmark which? — e.g. 1,3 · a = all that fit · q = quit", "a")
169
+ if sel.lower() == "a":
170
+ picked = benchable
171
+ else:
172
+ picked = []
173
+ for tok in sel.split(","):
174
+ tok = tok.strip()
175
+ if not tok.isdigit() or not (1 <= int(tok) <= len(rows)):
176
+ console.print(f"[red]'{tok}' is not a row number[/]")
177
+ sys.exit(1)
178
+ mid, m, f = rows[int(tok) - 1]
179
+ if not m.get("ollama_tag"):
180
+ console.print(f"[yellow]{mid} has no ollama tag — fit-only, skipping[/]")
181
+ continue
182
+ picked.append((mid, m, f))
183
+ if not picked:
184
+ console.print("[dim]nothing to benchmark[/]")
185
+ return
186
+ console.print("picked: " + ", ".join(m["ollama_tag"] for _, m, _f in picked))
187
+
188
+ reps = int(ask("Repetitions per model", "3") or 3)
189
+ submitter = ask("GitHub handle for credit (optional)", "")
190
+ est_pull = ", ".join(f"{f['weights_gb']:.0f} GB" for _, _m, f in picked[:4])
191
+ console.print(f"[dim]heads-up: models download once via ollama (~{est_pull}{' …' if len(picked) > 4 else ''})[/]")
192
+ if ask(f"Run {len(picked)} benchmark(s) now? Y/n", "y").lower() not in {"y", "yes"}:
193
+ console.print("[dim]bye — nothing was run[/]")
194
+ return
195
+ run_benchmarks(picked, hw, reps, 4096, submitter)
196
+
197
+
198
+ def resolve_gpu(spec: str) -> dict:
199
+ if spec == "auto":
200
+ return hardware.detect()
201
+ if spec in PROFILES:
202
+ name, vram, bw = PROFILES[spec]
203
+ return {"kind": "profile", "name": name, "vram_gb": vram, "bw": bw,
204
+ "profile_id": spec, "driver": "", "cuda": "", "note": ""}
205
+ console.print(f"[red]unknown gpu '{spec}'[/] — use auto or one of: {', '.join(PROFILES)}")
206
+ sys.exit(1)
207
+
208
+
209
+ def main(argv=None):
210
+ ap = argparse.ArgumentParser(prog="fitlab",
211
+ description="Will it run on my setup? Run `fitlab` with no arguments for the wizard.")
212
+ ap.add_argument("--version", action="version", version=f"fitlab {__version__}")
213
+ sub = ap.add_subparsers(dest="cmd")
214
+
215
+ sub.add_parser("detect", help="show detected hardware")
216
+ sub.add_parser("update", help="refresh the weekly model registry now")
217
+
218
+ p_check = sub.add_parser("check", help="fit table for a GPU (no benchmarking)")
219
+ p_check.add_argument("--gpu", default="auto", help="auto or profile id, e.g. rtx3060-12")
220
+ p_check.add_argument("--quant", default="Q4_K_M", choices=sorted(estimate.BPW))
221
+ p_check.add_argument("--ctx", type=int, default=8192, choices=estimate.CTX_CHOICES)
222
+ p_check.add_argument("--category", default="all")
223
+ p_check.add_argument("--limit", type=int, default=None)
224
+
225
+ p_bench = sub.add_parser("bench", help="benchmark one model non-interactively")
226
+ p_bench.add_argument("--model", required=True, help="ollama tag (qwen3:8b) or registry id (qwen3-8b)")
227
+ p_bench.add_argument("--reps", type=int, default=3)
228
+ p_bench.add_argument("--context", type=int, default=4096)
229
+ p_bench.add_argument("--out", default="fitlab-results")
230
+ p_bench.add_argument("--submitter", default="")
231
+ p_bench.add_argument("--source", default="community-local",
232
+ choices=["ci-kaggle", "ci-actions-cpu", "community-colab",
233
+ "community-local", "maintainer"])
234
+ p_bench.add_argument("--dry-run", action="store_true", help="show the fit + plan, don't run")
235
+
236
+ args = ap.parse_args(argv)
237
+
238
+ if args.cmd is None:
239
+ wizard()
240
+ return 0
241
+
242
+ if args.cmd == "detect":
243
+ banner()
244
+ hw_summary(hardware.detect())
245
+ return 0
246
+
247
+ if args.cmd == "update":
248
+ load_registry(refresh=True)
249
+ return 0
250
+
251
+ if args.cmd == "check":
252
+ banner()
253
+ hw = resolve_gpu(args.gpu)
254
+ hw_summary(hw)
255
+ reg = load_registry()
256
+ ids = ordered_models(reg, args.category)
257
+ if not ids:
258
+ console.print(f"[red]unknown category[/] — one of: "
259
+ f"{', '.join(reg.get('categories', {}))}, all")
260
+ return 1
261
+ fit_table(reg, ids, hw, args.quant, args.ctx, args.limit)
262
+ return 0
263
+
264
+ if args.cmd == "bench":
265
+ banner()
266
+ hw = hardware.detect()
267
+ hw_summary(hw)
268
+ reg = load_registry()
269
+ mid, m = None, None
270
+ if args.model in reg.get("models", {}):
271
+ mid, m = args.model, reg["models"][args.model]
272
+ tag = m.get("ollama_tag")
273
+ if not tag:
274
+ console.print(f"[red]{mid} has no ollama tag in the registry[/]")
275
+ return 1
276
+ else:
277
+ tag = args.model
278
+ if m and m.get("arch"):
279
+ f = estimate.fit(m, "Q4_K_M", args.context, hw["vram_gb"], hw["bw"])
280
+ console.print(f"fit: [{VERDICT_STYLE[f['verdict']]}]{f['verdict'].upper()}[/] · "
281
+ f"est {f['est_gb']} GB / {hw['vram_gb']} GB")
282
+ if args.dry_run:
283
+ console.print(f"[dim]dry-run: would pull {tag}, run {args.reps}× "
284
+ f"(num_predict 256, seed 42, ctx {args.context}) → {args.out}/[/]")
285
+ return 0
286
+ try:
287
+ result, path = bench.bench_model(tag, hardware.schema_gpu(hw), model_id=mid,
288
+ reps=args.reps, context=args.context,
289
+ source=args.source, submitter=args.submitter,
290
+ out_dir=args.out,
291
+ log=lambda s: console.print(f"[dim]{s}[/]"))
292
+ except Exception as e:
293
+ console.print(f"[red]benchmark failed: {e}[/]")
294
+ return 1
295
+ console.print(f"[bold green]✓[/] {result['metrics']['gen_tps']} tok/s median → {path}")
296
+ console.print(f"[green]↗ submit:[/] {submit_url(result)}")
297
+ return 0
298
+
299
+ return 0
300
+
301
+
302
+ if __name__ == "__main__":
303
+ sys.exit(main())