quantgap 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.
quantgap/cli.py ADDED
@@ -0,0 +1,521 @@
1
+ """quantgap command line: pack, packs, score, compare, submit, inspect, doctor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import importlib.util
7
+ import json
8
+ import os
9
+ import platform
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Annotated
13
+
14
+ import typer
15
+ from rich.console import Console
16
+ from rich.markup import escape
17
+ from rich.progress import BarColumn, Progress, TextColumn, TimeRemainingColumn
18
+
19
+ from quantgap import __version__
20
+ from quantgap.backends import (
21
+ BACKEND_NAMES,
22
+ BackendSettings,
23
+ ResolvedSource,
24
+ get_backend,
25
+ pick_backend_name,
26
+ )
27
+ from quantgap.config import Settings, load_settings
28
+ from quantgap.models import ScoreResult
29
+ from quantgap.util import parse_size
30
+
31
+ for _s in (sys.stdout, sys.stderr): # Windows consoles default to a legacy code page
32
+ with contextlib.suppress(AttributeError, ValueError):
33
+ _s.reconfigure(encoding="utf-8", errors="replace")
34
+
35
+ app = typer.Typer(
36
+ help="quantgap: how much did this quant lose? Measure how far a quantized model drifts "
37
+ "from the original, on your own machine, without running the original.",
38
+ no_args_is_help=True,
39
+ add_completion=False,
40
+ )
41
+ packs_app = typer.Typer(help="List and inspect reference packs.", no_args_is_help=True)
42
+ app.add_typer(packs_app, name="packs")
43
+ console = Console()
44
+ err = Console(stderr=True)
45
+
46
+ OLLAMA_HINT = (
47
+ "Ollama models cannot be scored by name (Ollama does not return prompt logprobs). "
48
+ "Find the GGUF file Ollama already downloaded with `ollama show --modelfile NAME` "
49
+ "(the FROM line is the path) and pass that file to quantgap."
50
+ )
51
+
52
+
53
+ def _fail(msg: str, code: int = 1) -> typer.Exit:
54
+ err.print(f"[red]error:[/red] {escape(msg)}")
55
+ return typer.Exit(code)
56
+
57
+
58
+ def _version(v: bool) -> None:
59
+ if v:
60
+ console.print(f"quantgap {__version__}")
61
+ raise typer.Exit()
62
+
63
+
64
+ @app.callback()
65
+ def _main(
66
+ version: Annotated[bool, typer.Option("--version", callback=_version, is_eager=True)] = False,
67
+ ) -> None:
68
+ pass
69
+
70
+
71
+ # ---------------------------------------------------------------------------- helpers
72
+
73
+
74
+ def _settings_for(
75
+ cfg: Settings, backend: str, ctx: int | None, n_gpu_layers: int | None,
76
+ base_url: str | None, model_name: str | None,
77
+ ) -> BackendSettings: # fmt: skip
78
+ return BackendSettings(
79
+ ctx=ctx or cfg.defaults.ctx,
80
+ n_gpu_layers=n_gpu_layers if n_gpu_layers is not None else cfg.llamacpp.n_gpu_layers,
81
+ n_threads=cfg.llamacpp.n_threads,
82
+ base_url=base_url or (cfg.openai.base_url if backend == "openai" else None),
83
+ model_name=model_name or cfg.openai.model_name,
84
+ max_prompt_logprobs=cfg.openai.max_prompt_logprobs,
85
+ )
86
+
87
+
88
+ def _progress() -> tuple[Progress, callable]:
89
+ prog = Progress(
90
+ TextColumn("{task.description}"), BarColumn(), TextColumn("{task.completed}/{task.total}"),
91
+ TimeRemainingColumn(), console=console, transient=True,
92
+ ) # fmt: skip
93
+ tasks: dict[str, int] = {}
94
+
95
+ def cb(phase: str, done: int, total: int) -> None:
96
+ if phase not in tasks:
97
+ tasks[phase] = prog.add_task(phase, total=total)
98
+ prog.update(tasks[phase], completed=done, total=total)
99
+
100
+ return prog, cb
101
+
102
+
103
+ def _score_source(
104
+ src: ResolvedSource, pack, cfg: Settings, *, backend_name: str, ctx, n_gpu_layers,
105
+ base_url, model_name, groups, quick: bool, do_damage: bool,
106
+ ) -> ScoreResult: # fmt: skip
107
+ from quantgap.hub.packs import fine_tune_warning
108
+ from quantgap.score.run import score_quant
109
+
110
+ name = backend_name if backend_name != "auto" else pick_backend_name(src, base_url)
111
+ settings = _settings_for(cfg, name, ctx, n_gpu_layers, base_url, model_name)
112
+ warns = []
113
+ if (w := fine_tune_warning(src.base_model, pack)) is not None:
114
+ warns.append(w)
115
+ backend = get_backend(name)
116
+ backend.load(src, settings)
117
+ prog, cb = _progress()
118
+ try:
119
+ with prog:
120
+ return score_quant(
121
+ pack, backend, src, settings, groups=groups, quick=quick,
122
+ do_damage=do_damage, warnings=warns, progress=cb,
123
+ ) # fmt: skip
124
+ finally:
125
+ backend.close()
126
+
127
+
128
+ def _get_pack(pack_ref: str | None, src: ResolvedSource, cfg: Settings):
129
+ from quantgap.hub.packs import auto_find_pack, load_pack
130
+
131
+ if pack_ref:
132
+ return load_pack(pack_ref)
133
+ return auto_find_pack(src.base_model, cfg)
134
+
135
+
136
+ def _resolve(quant: str, base_url, model_name) -> ResolvedSource:
137
+ from quantgap.hub.resolve import ResolveError, resolve_quant
138
+
139
+ try:
140
+ return resolve_quant(quant, base_url=base_url, model_name=model_name)
141
+ except ResolveError as e:
142
+ hint = OLLAMA_HINT if (":" in quant and "/" not in quant and "\\" not in quant) else ""
143
+ raise _fail(f"{e}\n{hint}".strip()) from e
144
+
145
+
146
+ # ---------------------------------------------------------------------------- score
147
+
148
+
149
+ @app.command()
150
+ def score(
151
+ quant: Annotated[
152
+ str, typer.Argument(help="Local path, hf://org/repo/file.gguf, or openai://NAME")
153
+ ],
154
+ pack: Annotated[
155
+ str | None,
156
+ typer.Option(help="Pack folder or Hub dataset id (found automatically if omitted)"),
157
+ ] = None,
158
+ backend: Annotated[str, typer.Option(help="auto, llamacpp, mlx, openai or hf")] = "auto",
159
+ base_url: Annotated[
160
+ str | None, typer.Option(help="OpenAI-compatible server, e.g. http://localhost:8000/v1")
161
+ ] = None,
162
+ model_name: Annotated[str | None, typer.Option(help="Model name the server serves")] = None,
163
+ n_gpu_layers: Annotated[
164
+ int | None, typer.Option(help="llama.cpp layers on GPU (-1 = all)")
165
+ ] = None,
166
+ ctx: Annotated[int | None, typer.Option(help="Context size, at least 600")] = None,
167
+ groups: Annotated[
168
+ str | None, typer.Option(help="Comma list: general,chat,code,math,multilingual,json")
169
+ ] = None,
170
+ quick: Annotated[
171
+ bool, typer.Option(help="First 25% of each group: a fast first look, not submittable")
172
+ ] = False,
173
+ json_out: Annotated[
174
+ Path | None, typer.Option("--json", help="Where to save the result JSON")
175
+ ] = None,
176
+ no_damage: Annotated[bool, typer.Option(help="Skip the visible-damage checks")] = False,
177
+ ) -> None:
178
+ """Score one quant against a pack."""
179
+ from quantgap.report.jsonout import safe_name, write_result
180
+ from quantgap.report.table import print_result
181
+ from quantgap.score.run import ScoreError
182
+
183
+ cfg = load_settings()
184
+ if backend not in ("auto", *BACKEND_NAMES):
185
+ raise _fail(f"Unknown backend {backend!r}. Choose auto, {', '.join(BACKEND_NAMES)}.")
186
+ src = _resolve(quant, base_url, model_name)
187
+ try:
188
+ pk = _get_pack(pack, src, cfg)
189
+ res = _score_source(
190
+ src, pk, cfg, backend_name=backend, ctx=ctx, n_gpu_layers=n_gpu_layers,
191
+ base_url=base_url, model_name=model_name,
192
+ groups=[g.strip() for g in groups.split(",")] if groups else None,
193
+ quick=quick, do_damage=not no_damage,
194
+ ) # fmt: skip
195
+ except (ScoreError, RuntimeError, ValueError, FileNotFoundError) as e:
196
+ raise _fail(str(e)) from e
197
+ print_result(console, res)
198
+ out = json_out or Path(cfg.defaults.results_dir) / f"{safe_name(res)}.json"
199
+ write_result(res, out)
200
+ console.print(f"Saved result: {out}")
201
+
202
+
203
+ # ---------------------------------------------------------------------------- compare
204
+
205
+
206
+ @app.command()
207
+ def compare(
208
+ quants: Annotated[
209
+ list[str] | None,
210
+ typer.Argument(help="Quants (paths, hf:// URLs) or saved result .json files"),
211
+ ] = None,
212
+ repo: Annotated[
213
+ str | None, typer.Option(help="Hub repo to take quants from, with --glob")
214
+ ] = None,
215
+ glob: Annotated[str, typer.Option(help="File pattern inside --repo")] = "*.gguf",
216
+ pack: Annotated[str | None, typer.Option()] = None,
217
+ backend: Annotated[str, typer.Option()] = "auto",
218
+ budget: Annotated[str | None, typer.Option(help="Memory you have, e.g. 16GB")] = None,
219
+ chart: Annotated[
220
+ Path | None, typer.Option(help="Write the quality-vs-size chart (.svg or .png)")
221
+ ] = None,
222
+ json_out: Annotated[Path | None, typer.Option("--json")] = None,
223
+ ctx: Annotated[int | None, typer.Option()] = None,
224
+ n_gpu_layers: Annotated[int | None, typer.Option()] = None,
225
+ quick: Annotated[bool, typer.Option()] = False,
226
+ no_damage: Annotated[bool, typer.Option()] = False,
227
+ ) -> None:
228
+ """Score many quants of one model, draw a chart, and say which one fits your memory."""
229
+ from quantgap.hub.resolve import ResolveError, expand_glob
230
+ from quantgap.hub.submit import load_result
231
+ from quantgap.report.chart import draw_chart
232
+ from quantgap.report.jsonout import safe_name, write_report, write_result
233
+ from quantgap.report.table import print_compare
234
+ from quantgap.score.compare import MixedPacksError, build_report
235
+ from quantgap.score.recommend import ASSUMPTION, recommend
236
+ from quantgap.score.run import ScoreError
237
+
238
+ cfg = load_settings()
239
+ items = list(quants or [])
240
+ if repo:
241
+ try:
242
+ items += [f"hf://{repo}/{f}" for f in expand_glob(repo, glob)]
243
+ except ResolveError as e:
244
+ raise _fail(str(e)) from e
245
+ if not items:
246
+ raise _fail("Give at least one quant, or --repo ORG/REPO with --glob.")
247
+ budget_bytes = None
248
+ if budget:
249
+ try:
250
+ budget_bytes = parse_size(budget)
251
+ except ValueError as e:
252
+ raise _fail(str(e)) from e
253
+
254
+ results: list[ScoreResult] = []
255
+ pk = None
256
+ try:
257
+ for q in items:
258
+ if q.endswith(".json") and Path(q).is_file():
259
+ results.append(load_result(q))
260
+ continue
261
+ src = _resolve(q, None, None)
262
+ if pk is None:
263
+ pk = _get_pack(pack, src, cfg)
264
+ console.print(f"[bold]{src.label}[/bold]")
265
+ res = _score_source(
266
+ src, pk, cfg, backend_name=backend, ctx=ctx, n_gpu_layers=n_gpu_layers,
267
+ base_url=None, model_name=None, groups=None, quick=quick, do_damage=not no_damage,
268
+ ) # fmt: skip
269
+ write_result(res, Path(cfg.defaults.results_dir) / f"{safe_name(res)}.json")
270
+ results.append(res)
271
+ report = build_report(results, budget_bytes)
272
+ except (ScoreError, MixedPacksError, RuntimeError, ValueError, FileNotFoundError) as e:
273
+ raise _fail(str(e)) from e
274
+
275
+ print_compare(console, results)
276
+ if budget_bytes:
277
+ rec = recommend(results, budget_bytes)
278
+ console.print(f"\n[bold]{rec.sentence}[/bold]")
279
+ for w in rec.warnings:
280
+ console.print(f"[yellow]{w}[/yellow]")
281
+ console.print(f"[dim]{ASSUMPTION}[/dim]")
282
+ if chart:
283
+ draw_chart(results, chart, budget_bytes=budget_bytes)
284
+ console.print(f"Chart: {chart}")
285
+ if json_out:
286
+ write_report(report, json_out)
287
+ console.print(f"Report: {json_out}")
288
+
289
+
290
+ # ---------------------------------------------------------------------------- pack
291
+
292
+
293
+ @app.command()
294
+ def pack(
295
+ model_repo: Annotated[
296
+ str, typer.Argument(help="Reference model on the Hub, e.g. Qwen/Qwen3-8B")
297
+ ],
298
+ revision: Annotated[
299
+ str | None, typer.Option(help="Commit sha (recommended: pins the model)")
300
+ ] = None,
301
+ probes: Annotated[str, typer.Option(help="Probe set version")] = "v1",
302
+ k: Annotated[int, typer.Option(help="Top-k guesses to keep per position")] = 32,
303
+ batch_size: Annotated[int, typer.Option()] = 4,
304
+ device: Annotated[str, typer.Option(help="auto, cuda or cpu")] = "auto",
305
+ out: Annotated[Path | None, typer.Option()] = None,
306
+ push: Annotated[
307
+ str | None, typer.Option(help="Upload to the Hub as ORG/NAME (needs HF_TOKEN)")
308
+ ] = None,
309
+ max_new_tokens: Annotated[int, typer.Option()] = 256,
310
+ license_: Annotated[
311
+ str | None, typer.Option("--license", help="Reference model license, for the dataset card")
312
+ ] = None,
313
+ logp_dtype: Annotated[
314
+ str, typer.Option(help="float32 (default) or float16 (smaller, noisier)")
315
+ ] = "float32",
316
+ ) -> None:
317
+ """Run the full-size reference model once and save a shareable pack."""
318
+ if (
319
+ importlib.util.find_spec("torch") is None
320
+ or importlib.util.find_spec("transformers") is None
321
+ ):
322
+ raise _fail('Making a pack needs torch and transformers. Run: pip install "quantgap[pack]"')
323
+ from quantgap.hub.packs import push_pack
324
+ from quantgap.pack.runner import build_pack_from_objects, load_reference
325
+
326
+ out = out or Path("packs") / model_repo.split("/")[-1]
327
+ console.print(f"Loading {model_repo} ...")
328
+ model, tok, dev, dtype, label = load_reference(model_repo, revision, device)
329
+ if revision is None:
330
+ err.print(
331
+ "[yellow]note:[/yellow] no --revision given. Pin one so the pack can be reproduced."
332
+ )
333
+ prog, cb = _progress()
334
+ with prog:
335
+ manifest = build_pack_from_objects(
336
+ model, tok, out, repo=model_repo, revision=revision, dtype=dtype,
337
+ probes_version=probes, k=k, batch_size=batch_size, max_new_tokens=max_new_tokens,
338
+ logp_dtype=logp_dtype, license_=license_, device=dev, device_label=label,
339
+ progress=lambda d, t: cb("running reference", d, t),
340
+ ) # fmt: skip
341
+ size = sum(f.stat().st_size for f in out.glob("*.parquet")) / 1e6
342
+ console.print(
343
+ f"Pack {manifest.short_name}: {manifest.probes.n_positions:,} positions, {size:.1f} MB -> {out}"
344
+ )
345
+ if push:
346
+ url = push_pack(out, push, os.environ.get("HF_TOKEN"))
347
+ console.print(f"Uploaded: {url}")
348
+
349
+
350
+ @packs_app.command("list")
351
+ def packs_list(
352
+ model: Annotated[str | None, typer.Option(help="Filter by model name")] = None,
353
+ ) -> None:
354
+ """List known packs on the Hub (tag: quantgap-pack)."""
355
+ from quantgap.hub.packs import list_packs
356
+
357
+ try:
358
+ found = list_packs(load_settings(), model)
359
+ except Exception as e:
360
+ raise _fail(f"Could not reach the Hub: {e}") from e
361
+ if not found:
362
+ console.print("No packs found. Make one with `quantgap pack MODEL` (docs/packs.md).")
363
+ for ds, m in found:
364
+ console.print(f"{ds} {m.model.repo} probes {m.probes.set} k={m.k} {m.short_name}")
365
+
366
+
367
+ @packs_app.command("show")
368
+ def packs_show(ref: Annotated[str, typer.Argument(help="Pack folder or Hub dataset id")]) -> None:
369
+ """Print a pack's manifest summary."""
370
+ from quantgap.hub.packs import PackNotFound, load_pack
371
+ from quantgap.pack.reader import PackIntegrityError
372
+
373
+ try:
374
+ p = load_pack(ref)
375
+ except (PackNotFound, PackIntegrityError, FileNotFoundError) as e:
376
+ raise _fail(str(e)) from e
377
+ m = p.manifest
378
+ console.print(f"[bold]pack:{m.short_name}[/bold]")
379
+ console.print(f"model {m.model.repo} @ {m.model.revision} ({m.model.dtype})")
380
+ console.print(
381
+ f"probes {m.probes.set}, {m.probes.n_sequences} sequences, {m.probes.n_positions:,} positions"
382
+ )
383
+ console.print(f"k {m.k} ({m.logp_dtype})")
384
+ console.print(f"greedy {m.greedy.n_prompts} prompts x {m.greedy.max_new_tokens} tokens")
385
+ console.print(f"made on {m.runner.device}, quantgap {m.runner.quantgap}, {m.created_at}")
386
+
387
+
388
+ # ---------------------------------------------------------------------------- submit / inspect
389
+
390
+
391
+ @app.command()
392
+ def submit(
393
+ result: Annotated[Path, typer.Argument(help="Result JSON from `quantgap score`")],
394
+ results_repo: Annotated[str | None, typer.Option(help="Hub dataset to send the PR to")] = None,
395
+ dry_run: Annotated[
396
+ bool, typer.Option(help="Show exactly what would be sent, send nothing")
397
+ ] = False,
398
+ ) -> None:
399
+ """Open a pull request with your result on the shared results dataset."""
400
+ from quantgap.hub.submit import SubmitError
401
+ from quantgap.hub.submit import submit as do_submit
402
+
403
+ repo = results_repo or load_settings().hub.results_repo
404
+ if not dry_run and not os.environ.get("HF_TOKEN"):
405
+ raise _fail(
406
+ "Set HF_TOKEN (a Hugging Face token with write access) to submit. Or use --dry-run."
407
+ )
408
+ try:
409
+ sub, url = do_submit(result, repo, dry_run=dry_run, token=os.environ.get("HF_TOKEN"))
410
+ except (SubmitError, FileNotFoundError, json.JSONDecodeError) as e:
411
+ raise _fail(str(e)) from e
412
+ console.print(f"[bold]File:[/bold] {sub.path} [bold]Repo:[/bold] {repo}")
413
+ console.print(f"[bold]PR title:[/bold] {sub.pr_title}\n")
414
+ console.print(sub.pr_body)
415
+ console.print("\n[dim](dry run: nothing was sent)[/dim]" if dry_run else f"\nOpened: {url}")
416
+
417
+
418
+ @app.command()
419
+ def inspect(result: Annotated[Path, typer.Argument()]) -> None:
420
+ """Pretty-print a saved result."""
421
+ from quantgap.hub.submit import SubmitError, load_result
422
+ from quantgap.report.table import print_result
423
+
424
+ try:
425
+ print_result(console, load_result(result))
426
+ except (SubmitError, FileNotFoundError, json.JSONDecodeError) as e:
427
+ raise _fail(str(e)) from e
428
+
429
+
430
+ # ---------------------------------------------------------------------------- doctor
431
+
432
+
433
+ @app.command()
434
+ def doctor(
435
+ selftest: Annotated[
436
+ bool, typer.Option(help="Score a tiny built-in model against itself (needs torch)")
437
+ ] = False,
438
+ ) -> None:
439
+ """Check which backends are installed, whether a GPU is found, and Hugging Face login."""
440
+ console.print(
441
+ f"quantgap {__version__} python {platform.python_version()} {platform.system()} {platform.machine()}"
442
+ )
443
+ rows = [
444
+ ("llamacpp", "llama_cpp", 'pip install "quantgap[llamacpp]"', "GGUF files"),
445
+ ("mlx", "mlx_lm", 'pip install "quantgap[mlx]"', "Apple silicon (MLX)"),
446
+ ("hf", "transformers", 'pip install "quantgap[hf]"', "Transformers / bitsandbytes"),
447
+ ("pack", "torch", 'pip install "quantgap[pack]"', "making packs"),
448
+ ]
449
+ missing = []
450
+ for name, mod, fix, what in rows:
451
+ ok = importlib.util.find_spec(mod) is not None
452
+ console.print(
453
+ f" {'[green]ok[/green] ' if ok else '[yellow]missing[/yellow]'} {name:9} {what}"
454
+ + ("" if ok else f" -> {escape(fix)}")
455
+ )
456
+ missing += [] if ok else [fix]
457
+ console.print(
458
+ " [green]ok[/green] openai any OpenAI-compatible server (needs --base-url)"
459
+ )
460
+ gpu = "none found"
461
+ if importlib.util.find_spec("torch"):
462
+ import torch
463
+
464
+ if torch.cuda.is_available():
465
+ gpu = f"CUDA: {torch.cuda.get_device_name(0)}"
466
+ elif getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
467
+ gpu = "Apple MPS"
468
+ console.print(f" GPU: {gpu}")
469
+ console.print(
470
+ f" HF_TOKEN: {'set' if os.environ.get('HF_TOKEN') else 'not set (only needed for pack --push and submit)'}"
471
+ )
472
+ if missing:
473
+ console.print("\nTo score GGUF files on most machines you need only the first line above.")
474
+ if selftest:
475
+ _selftest()
476
+
477
+
478
+ def _selftest() -> None:
479
+ if importlib.util.find_spec("torch") is None or importlib.util.find_spec("tokenizers") is None:
480
+ raise _fail('--selftest needs torch and transformers: pip install "quantgap[pack]"')
481
+ import tempfile
482
+ import time
483
+
484
+ root = Path(__file__).resolve().parents[2] / "scripts" / "make_tiny_model.py"
485
+ if not root.is_file():
486
+ raise _fail("--selftest needs a source checkout (scripts/make_tiny_model.py).")
487
+ spec = importlib.util.spec_from_file_location("make_tiny_model", root)
488
+ mod = importlib.util.module_from_spec(spec)
489
+ spec.loader.exec_module(mod)
490
+ from quantgap.backends.hf import HFBackend
491
+ from quantgap.pack import Pack
492
+ from quantgap.pack.runner import build_pack_from_objects
493
+ from quantgap.score.run import score_quant
494
+
495
+ t0 = time.time()
496
+ with tempfile.TemporaryDirectory() as d:
497
+ mod.build(Path(d), with_gguf=False)
498
+ from transformers import AutoModelForCausalLM, AutoTokenizer
499
+
500
+ tok = AutoTokenizer.from_pretrained(Path(d) / "hf")
501
+ model = AutoModelForCausalLM.from_pretrained(Path(d) / "hf")
502
+ probes = Path(__file__).parent / "probes" / "data" / "probes_v1.jsonl"
503
+ build_pack_from_objects(model, tok, Path(d) / "pack", repo="selftest/tiny", dtype="float32",
504
+ probes_file=probes, k=32, max_new_tokens=16, max_len=200) # fmt: skip
505
+ b = HFBackend()
506
+ src = ResolvedSource(source="selftest", label="reference", path=Path(d) / "hf", format="hf")
507
+ s = BackendSettings()
508
+ b.load(src, s)
509
+ r = score_quant(Pack.load(Path(d) / "pack"), b, src, s, do_damage=False)
510
+ m = r.metrics
511
+ ok = m.kld_mean < 1e-6 and m.top1_agree == 1.0 and abs(m.ppl_ratio - 1) < 1e-4
512
+ console.print(
513
+ f" selftest: KLD {m.kld_mean:.2e}, top-1 {m.top1_agree * 100:.1f}%, ppl ratio {m.ppl_ratio:.6f} "
514
+ f"({time.time() - t0:.0f}s) -> {'[green]PASS[/green]' if ok else '[red]FAIL[/red]'}"
515
+ )
516
+ if not ok:
517
+ raise typer.Exit(1)
518
+
519
+
520
+ if __name__ == "__main__":
521
+ app()
quantgap/config.py ADDED
@@ -0,0 +1,60 @@
1
+ """Optional `quantgap.toml` (current folder, then ~/.config/quantgap/) plus environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tomllib
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+
12
+ class Defaults(BaseModel):
13
+ model_config = ConfigDict(extra="ignore")
14
+ backend: str = "auto"
15
+ ctx: int = 2048
16
+ results_dir: str = "./quantgap-results"
17
+
18
+
19
+ class LlamaCppCfg(BaseModel):
20
+ model_config = ConfigDict(extra="ignore")
21
+ n_gpu_layers: int = -1
22
+ n_threads: int | None = None
23
+
24
+
25
+ class OpenAICfg(BaseModel):
26
+ model_config = ConfigDict(extra="ignore")
27
+ base_url: str = "http://localhost:8000/v1"
28
+ model_name: str | None = None
29
+ max_prompt_logprobs: int = 20
30
+
31
+
32
+ class HubCfg(BaseModel):
33
+ model_config = ConfigDict(extra="ignore")
34
+ results_repo: str = "quantgap/results"
35
+ packs_org: str = "quantgap-packs"
36
+
37
+
38
+ class Settings(BaseModel):
39
+ model_config = ConfigDict(extra="ignore")
40
+ defaults: Defaults = Defaults()
41
+ llamacpp: LlamaCppCfg = LlamaCppCfg()
42
+ openai: OpenAICfg = OpenAICfg()
43
+ hub: HubCfg = HubCfg()
44
+
45
+
46
+ def _candidate_paths() -> list[Path]:
47
+ return [Path.cwd() / "quantgap.toml", Path.home() / ".config" / "quantgap" / "quantgap.toml"]
48
+
49
+
50
+ def load_settings() -> Settings:
51
+ data: dict = {}
52
+ for p in _candidate_paths():
53
+ if p.is_file():
54
+ with open(p, "rb") as f:
55
+ data = tomllib.load(f)
56
+ break
57
+ s = Settings.model_validate(data)
58
+ if env := os.environ.get("QUANTGAP_RESULTS_REPO"):
59
+ s.hub.results_repo = env
60
+ return s
@@ -0,0 +1 @@
1
+ """Everything that talks to the Hugging Face Hub. Imports of `huggingface_hub` happen lazily."""