zeroquantz 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.
- zeroquantz/__init__.py +14 -0
- zeroquantz/__main__.py +8 -0
- zeroquantz/agent/__init__.py +16 -0
- zeroquantz/agent/dispatcher.py +520 -0
- zeroquantz/agent/intents.py +46 -0
- zeroquantz/agent/parser.py +255 -0
- zeroquantz/benchmark/__init__.py +7 -0
- zeroquantz/benchmark/latency.py +66 -0
- zeroquantz/benchmark/memory.py +41 -0
- zeroquantz/benchmark/quality.py +38 -0
- zeroquantz/benchmark/runner.py +151 -0
- zeroquantz/cli/__init__.py +7 -0
- zeroquantz/cli/app.py +98 -0
- zeroquantz/cli/commands.py +459 -0
- zeroquantz/cli/interactive.py +56 -0
- zeroquantz/core/__init__.py +7 -0
- zeroquantz/core/artifacts.py +179 -0
- zeroquantz/core/context.py +127 -0
- zeroquantz/core/events.py +30 -0
- zeroquantz/core/exceptions.py +105 -0
- zeroquantz/core/session.py +202 -0
- zeroquantz/core/subenv.py +202 -0
- zeroquantz/deploy/__init__.py +25 -0
- zeroquantz/deploy/assets.py +161 -0
- zeroquantz/deploy/launcher.py +80 -0
- zeroquantz/deploy/runtime_env.py +66 -0
- zeroquantz/deploy/targets.py +154 -0
- zeroquantz/export/__init__.py +8 -0
- zeroquantz/export/exporter.py +68 -0
- zeroquantz/export/report.py +203 -0
- zeroquantz/hardware/__init__.py +15 -0
- zeroquantz/hardware/capabilities.py +152 -0
- zeroquantz/hardware/detector.py +200 -0
- zeroquantz/hardware/gpu.py +31 -0
- zeroquantz/models/__init__.py +8 -0
- zeroquantz/models/architecture.py +168 -0
- zeroquantz/models/downloader.py +161 -0
- zeroquantz/models/hf_auth.py +105 -0
- zeroquantz/models/inspector.py +249 -0
- zeroquantz/models/metadata.py +108 -0
- zeroquantz/models/search.py +71 -0
- zeroquantz/optimization/__init__.py +22 -0
- zeroquantz/optimization/candidate.py +272 -0
- zeroquantz/optimization/constraints.py +70 -0
- zeroquantz/optimization/fit.py +203 -0
- zeroquantz/optimization/pareto.py +66 -0
- zeroquantz/optimization/planner.py +297 -0
- zeroquantz/optimization/recommender.py +149 -0
- zeroquantz/profiling/__init__.py +18 -0
- zeroquantz/profiling/calibration.py +74 -0
- zeroquantz/profiling/sensitivity.py +234 -0
- zeroquantz/quantization/__init__.py +17 -0
- zeroquantz/quantization/backends/__init__.py +8 -0
- zeroquantz/quantization/backends/bitsandbytes.py +210 -0
- zeroquantz/quantization/backends/torchao.py +198 -0
- zeroquantz/quantization/base.py +136 -0
- zeroquantz/quantization/catalog.py +321 -0
- zeroquantz/quantization/config.py +106 -0
- zeroquantz/quantization/gguf_pipeline.py +210 -0
- zeroquantz/quantization/isolated.py +248 -0
- zeroquantz/quantization/memory.py +133 -0
- zeroquantz/quantization/native.py +91 -0
- zeroquantz/quantization/registry.py +101 -0
- zeroquantz/render.py +341 -0
- zeroquantz/runtimes/__init__.py +18 -0
- zeroquantz/runtimes/base.py +64 -0
- zeroquantz/runtimes/compatibility.py +91 -0
- zeroquantz/runtimes/registry.py +70 -0
- zeroquantz/runtimes/transformers.py +53 -0
- zeroquantz/runtimes/vllm.py +83 -0
- zeroquantz/tui/__init__.py +13 -0
- zeroquantz/tui/app.py +77 -0
- zeroquantz/tui/banner.py +47 -0
- zeroquantz/tui/screens/__init__.py +25 -0
- zeroquantz/tui/screens/confirm.py +41 -0
- zeroquantz/tui/screens/execute.py +194 -0
- zeroquantz/tui/screens/model_select.py +206 -0
- zeroquantz/tui/screens/plan.py +177 -0
- zeroquantz/tui/screens/quantize_select.py +272 -0
- zeroquantz/tui/screens/settings.py +219 -0
- zeroquantz/tui/screens/token.py +94 -0
- zeroquantz/tui/screens/welcome.py +128 -0
- zeroquantz/tui/screens/workspace.py +175 -0
- zeroquantz/tui/styles/app.tcss +424 -0
- zeroquantz/tui/widgets/__init__.py +9 -0
- zeroquantz/tui/widgets/chip.py +36 -0
- zeroquantz/tui/widgets/sidebar.py +107 -0
- zeroquantz/tui/widgets/status_bar.py +43 -0
- zeroquantz/utils/__init__.py +8 -0
- zeroquantz/utils/config.py +46 -0
- zeroquantz/utils/env.py +78 -0
- zeroquantz/utils/logging.py +73 -0
- zeroquantz/utils/metrics.py +98 -0
- zeroquantz/utils/paths.py +57 -0
- zeroquantz/utils/units.py +134 -0
- zeroquantz/verification/__init__.py +17 -0
- zeroquantz/verification/logits.py +55 -0
- zeroquantz/verification/report.py +186 -0
- zeroquantz/verification/weights.py +44 -0
- zeroquantz/version.py +8 -0
- zeroquantz-0.1.0.dist-info/METADATA +72 -0
- zeroquantz-0.1.0.dist-info/RECORD +105 -0
- zeroquantz-0.1.0.dist-info/WHEEL +4 -0
- zeroquantz-0.1.0.dist-info/entry_points.txt +2 -0
- zeroquantz-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Parse user text (slash commands *or* natural language) into an :class:`Intent`.
|
|
2
|
+
|
|
3
|
+
Natural-language understanding here is deliberately rule-based (no LLM): the core
|
|
4
|
+
must work fully offline. The important, well-tested piece is :func:`parse_goal`,
|
|
5
|
+
which turns phrases like "fit this under 8GB and target vLLM, quality matters"
|
|
6
|
+
into structured :class:`OptimizationGoal` updates.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from zeroquantz.agent.intents import Intent, IntentKind
|
|
14
|
+
|
|
15
|
+
# ---- slash command table ---------------------------------------------------
|
|
16
|
+
|
|
17
|
+
_SLASH: dict[str, IntentKind] = {
|
|
18
|
+
"model": IntentKind.LOAD_MODEL,
|
|
19
|
+
"load": IntentKind.LOAD_MODEL, # /load MODEL (ambiguous with sessions; see parse)
|
|
20
|
+
"inspect": IntentKind.INSPECT,
|
|
21
|
+
"hardware": IntentKind.HARDWARE,
|
|
22
|
+
"hw": IntentKind.HARDWARE,
|
|
23
|
+
"goal": IntentKind.SET_GOAL,
|
|
24
|
+
"recommend": IntentKind.RECOMMEND,
|
|
25
|
+
"rec": IntentKind.RECOMMEND,
|
|
26
|
+
"profile": IntentKind.PROFILE,
|
|
27
|
+
"plan": IntentKind.PLAN,
|
|
28
|
+
"quantize": IntentKind.QUANTIZE,
|
|
29
|
+
"benchmark": IntentKind.BENCHMARK,
|
|
30
|
+
"bench": IntentKind.BENCHMARK,
|
|
31
|
+
"compare": IntentKind.COMPARE,
|
|
32
|
+
"verify": IntentKind.VERIFY,
|
|
33
|
+
"export": IntentKind.EXPORT,
|
|
34
|
+
"history": IntentKind.HISTORY,
|
|
35
|
+
"undo": IntentKind.UNDO,
|
|
36
|
+
"checkout": IntentKind.CHECKOUT,
|
|
37
|
+
"sessions": IntentKind.SESSIONS,
|
|
38
|
+
"save": IntentKind.SAVE,
|
|
39
|
+
"help": IntentKind.HELP,
|
|
40
|
+
"exit": IntentKind.EXIT,
|
|
41
|
+
"quit": IntentKind.EXIT,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ---- natural-language command verbs (ordered; first match wins) ------------
|
|
45
|
+
|
|
46
|
+
_NL_VERBS: tuple[tuple[IntentKind, tuple[str, ...]], ...] = (
|
|
47
|
+
(IntentKind.RECOMMEND, ("recommend", "suggest", "what should i", "which quant", "advise", "best strategy", "best way")),
|
|
48
|
+
(IntentKind.HARDWARE, ("my gpu", "my hardware", "what gpu", "hardware", "my card", "my vram")),
|
|
49
|
+
(IntentKind.PLAN, ("mixed precision", "precision plan", "make a plan", "build a plan", "plan it")),
|
|
50
|
+
(IntentKind.PROFILE, ("sensitivity", "profile the", "which layers", "sensitive layers")),
|
|
51
|
+
(IntentKind.BENCHMARK, ("benchmark", "measure speed", "how fast", "throughput test")),
|
|
52
|
+
(IntentKind.VERIFY, ("verify", "validate quality", "check quality", "how lossy")),
|
|
53
|
+
(IntentKind.COMPARE, ("compare", "side by side", "side-by-side")),
|
|
54
|
+
(IntentKind.QUANTIZE, ("quantize", "compress", "shrink the model", "make it smaller")),
|
|
55
|
+
(IntentKind.EXPORT, ("export", "write the report", "save the model")),
|
|
56
|
+
(IntentKind.INSPECT, ("inspect", "tell me about", "details about", "info on", "describe")),
|
|
57
|
+
(IntentKind.HISTORY, ("show history", "history")),
|
|
58
|
+
(IntentKind.UNDO, ("undo", "go back")),
|
|
59
|
+
(IntentKind.SESSIONS, ("list sessions", "my sessions")),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_LOAD_VERBS = ("load", "open", "use", "optimize", "model", "quantize", "compress")
|
|
63
|
+
|
|
64
|
+
_MODEL_ID_RE = re.compile(r"\b([A-Za-z0-9][\w.\-]*/[\w.\-]+)\b")
|
|
65
|
+
_GB_RE = re.compile(r"(\d+(?:\.\d+)?)\s*(gib|gb|g)\b")
|
|
66
|
+
_PCT_RE = re.compile(r"(\d+(?:\.\d+)?)\s*%")
|
|
67
|
+
|
|
68
|
+
_KNOWN_RUNTIMES = ("vllm", "transformers", "sglang", "tensorrt-llm", "tensorrt", "llama.cpp", "ollama", "tgi")
|
|
69
|
+
|
|
70
|
+
_OBJECTIVE_KEYWORDS: dict[str, tuple[str, ...]] = {
|
|
71
|
+
"quality": ("quality", "accuracy", "accurate", "preserve", "lossless", "faithful", "precise", "minimal loss", "without losing", "keep quality"),
|
|
72
|
+
"speed": ("fastest", "faster", "speed", "throughput", "low latency", "latency", "quick", "tokens/sec", "tok/s", "high throughput"),
|
|
73
|
+
"memory": ("smallest", "as small as possible", "minimize memory", "tiniest", "most compact", "smallest footprint", "least vram"),
|
|
74
|
+
"balanced": ("balanced", "good balance", "trade off", "tradeoff"),
|
|
75
|
+
}
|
|
76
|
+
_OBJECTIVE_PRECEDENCE = ("quality", "speed", "memory", "balanced")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_goal(text: str) -> dict:
|
|
80
|
+
"""Extract structured :class:`OptimizationGoal` updates from free text.
|
|
81
|
+
|
|
82
|
+
Returns only the keys it actually detected, so callers can merge with
|
|
83
|
+
``goal.with_updates(**parse_goal(text))``.
|
|
84
|
+
"""
|
|
85
|
+
t = text.lower()
|
|
86
|
+
out: dict = {}
|
|
87
|
+
|
|
88
|
+
m = _GB_RE.search(t)
|
|
89
|
+
if m:
|
|
90
|
+
val = float(m.group(1))
|
|
91
|
+
preceding = t[max(0, m.start() - 28) : m.start()]
|
|
92
|
+
if any(w in preceding for w in ("size", "disk", "file", "weights")):
|
|
93
|
+
out["max_model_size_gb"] = val
|
|
94
|
+
else:
|
|
95
|
+
out["max_vram_gb"] = val
|
|
96
|
+
|
|
97
|
+
for rt in _KNOWN_RUNTIMES:
|
|
98
|
+
if rt in t:
|
|
99
|
+
out["runtime"] = rt
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
pct = _PCT_RE.search(t)
|
|
103
|
+
if pct and any(w in t for w in ("loss", "degrad", "accuracy", "quality", "within", "perplexity", "drop")):
|
|
104
|
+
out["max_quality_loss"] = round(float(pct.group(1)) / 100.0, 4)
|
|
105
|
+
|
|
106
|
+
objective = _detect_objective(t)
|
|
107
|
+
if objective:
|
|
108
|
+
out["objective"] = objective
|
|
109
|
+
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _detect_objective(t: str) -> str | None:
|
|
114
|
+
scores = {
|
|
115
|
+
obj: sum(1 for kw in kws if kw in t) for obj, kws in _OBJECTIVE_KEYWORDS.items()
|
|
116
|
+
}
|
|
117
|
+
best = max(scores.values())
|
|
118
|
+
if best == 0:
|
|
119
|
+
return None
|
|
120
|
+
for obj in _OBJECTIVE_PRECEDENCE: # deterministic tie-break
|
|
121
|
+
if scores[obj] == best:
|
|
122
|
+
return obj
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _detect_model_id(text: str) -> str | None:
|
|
127
|
+
m = _MODEL_ID_RE.search(text)
|
|
128
|
+
if m:
|
|
129
|
+
return m.group(1)
|
|
130
|
+
# local path heuristic
|
|
131
|
+
for token in text.split():
|
|
132
|
+
if token.startswith(("./", "~/", "../")) or "\\" in token:
|
|
133
|
+
return token
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class IntentParser:
|
|
138
|
+
"""Turn a raw input line into an :class:`Intent`."""
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def parse(text: str) -> Intent:
|
|
142
|
+
raw = text.strip()
|
|
143
|
+
if not raw:
|
|
144
|
+
return Intent(IntentKind.UNKNOWN, raw=raw)
|
|
145
|
+
|
|
146
|
+
if raw.startswith("/"):
|
|
147
|
+
return IntentParser._parse_slash(raw)
|
|
148
|
+
return IntentParser._parse_natural(raw)
|
|
149
|
+
|
|
150
|
+
# ---- slash --------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _parse_slash(raw: str) -> Intent:
|
|
154
|
+
parts = raw[1:].split()
|
|
155
|
+
if not parts:
|
|
156
|
+
return Intent(IntentKind.UNKNOWN, raw=raw)
|
|
157
|
+
cmd = parts[0].lower()
|
|
158
|
+
rest = parts[1:]
|
|
159
|
+
rest_str = " ".join(rest)
|
|
160
|
+
kind = _SLASH.get(cmd)
|
|
161
|
+
if kind is None:
|
|
162
|
+
return Intent(IntentKind.UNKNOWN, args={"command": cmd}, raw=raw)
|
|
163
|
+
|
|
164
|
+
# /load is a session load when it names a saved session, but /model is the
|
|
165
|
+
# canonical way to load a model; treat bare "/load X" as model load only if
|
|
166
|
+
# X looks like a model id/path.
|
|
167
|
+
if cmd == "load":
|
|
168
|
+
if rest and _looks_like_model(rest_str):
|
|
169
|
+
return Intent(IntentKind.LOAD_MODEL, {"model_id": rest_str}, raw)
|
|
170
|
+
return Intent(IntentKind.LOAD_SESSION, {"name": rest_str}, raw)
|
|
171
|
+
|
|
172
|
+
if kind is IntentKind.LOAD_MODEL:
|
|
173
|
+
return Intent(kind, {"model_id": rest_str}, raw)
|
|
174
|
+
if kind is IntentKind.SET_GOAL:
|
|
175
|
+
return Intent(kind, {"goal_updates": parse_goal(rest_str)}, raw)
|
|
176
|
+
if kind is IntentKind.CHECKOUT:
|
|
177
|
+
return Intent(kind, {"index": _safe_int(rest_str)}, raw)
|
|
178
|
+
if kind is IntentKind.SAVE:
|
|
179
|
+
return Intent(kind, {"name": rest_str or None}, raw)
|
|
180
|
+
if kind is IntentKind.EXPORT:
|
|
181
|
+
return Intent(kind, {"path": rest_str or None}, raw)
|
|
182
|
+
if kind is IntentKind.QUANTIZE:
|
|
183
|
+
return Intent(kind, {"method": rest[0] if rest else None}, raw)
|
|
184
|
+
if kind is IntentKind.VERIFY:
|
|
185
|
+
args = {}
|
|
186
|
+
if len(rest) >= 1:
|
|
187
|
+
args["base"] = rest[0]
|
|
188
|
+
if len(rest) >= 2:
|
|
189
|
+
args["quant"] = rest[1]
|
|
190
|
+
return Intent(kind, args, raw)
|
|
191
|
+
if kind in (IntentKind.BENCHMARK, IntentKind.INSPECT):
|
|
192
|
+
return Intent(kind, {"target": rest_str or None}, raw)
|
|
193
|
+
return Intent(kind, raw=raw)
|
|
194
|
+
|
|
195
|
+
# ---- natural language ---------------------------------------------------
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _parse_natural(raw: str) -> Intent:
|
|
199
|
+
t = raw.lower()
|
|
200
|
+
|
|
201
|
+
if t in ("exit", "quit", "q", ":q"):
|
|
202
|
+
return Intent(IntentKind.EXIT, raw=raw)
|
|
203
|
+
if t in ("help", "?", "commands"):
|
|
204
|
+
return Intent(IntentKind.HELP, raw=raw)
|
|
205
|
+
|
|
206
|
+
goal_updates = parse_goal(raw)
|
|
207
|
+
model_id = _detect_model_id(raw)
|
|
208
|
+
verb = IntentParser._detect_verb(t)
|
|
209
|
+
|
|
210
|
+
# explicit load ("load X", "use X", or a bare model id)
|
|
211
|
+
has_load_verb = any(re.search(rf"\b{v}\b", t) for v in _LOAD_VERBS)
|
|
212
|
+
if verb is None and model_id and (has_load_verb or _is_bare_id(raw)):
|
|
213
|
+
return Intent(IntentKind.LOAD_MODEL, {"model_id": model_id}, raw)
|
|
214
|
+
|
|
215
|
+
if verb is None:
|
|
216
|
+
if goal_updates:
|
|
217
|
+
return Intent(IntentKind.SET_GOAL, {"goal_updates": goal_updates}, raw)
|
|
218
|
+
return Intent(IntentKind.UNKNOWN, raw=raw)
|
|
219
|
+
|
|
220
|
+
args: dict = {}
|
|
221
|
+
if goal_updates:
|
|
222
|
+
args["goal_updates"] = goal_updates
|
|
223
|
+
if model_id and verb in (
|
|
224
|
+
IntentKind.INSPECT,
|
|
225
|
+
IntentKind.QUANTIZE,
|
|
226
|
+
IntentKind.BENCHMARK,
|
|
227
|
+
IntentKind.VERIFY,
|
|
228
|
+
IntentKind.RECOMMEND,
|
|
229
|
+
):
|
|
230
|
+
args["target"] = model_id
|
|
231
|
+
return Intent(verb, args, raw)
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _detect_verb(t: str) -> IntentKind | None:
|
|
235
|
+
for kind, keywords in _NL_VERBS:
|
|
236
|
+
if any(kw in t for kw in keywords):
|
|
237
|
+
return kind
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _looks_like_model(text: str) -> bool:
|
|
242
|
+
return bool(_MODEL_ID_RE.search(text)) or text.startswith((".", "~", "/")) or "\\" in text
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _is_bare_id(text: str) -> bool:
|
|
246
|
+
"""A message that is essentially just a model id (optionally 'load <id>')."""
|
|
247
|
+
tokens = text.split()
|
|
248
|
+
return len(tokens) <= 2 and _MODEL_ID_RE.search(text) is not None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _safe_int(text: str) -> int | None:
|
|
252
|
+
try:
|
|
253
|
+
return int(text.strip())
|
|
254
|
+
except (ValueError, AttributeError):
|
|
255
|
+
return None
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Latency and throughput measurement (torch/transformers, imported lazily)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class LatencyStats:
|
|
12
|
+
latency_ms_mean: float
|
|
13
|
+
latency_ms_p50: float
|
|
14
|
+
tokens_per_sec: float
|
|
15
|
+
generated_tokens: int
|
|
16
|
+
runs: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def measure_generation_latency(
|
|
20
|
+
model: Any,
|
|
21
|
+
tokenizer: Any,
|
|
22
|
+
prompt: str,
|
|
23
|
+
*,
|
|
24
|
+
max_new_tokens: int = 64,
|
|
25
|
+
warmup: int = 2,
|
|
26
|
+
runs: int = 5,
|
|
27
|
+
) -> LatencyStats:
|
|
28
|
+
"""Time ``model.generate`` over ``runs`` iterations (after ``warmup``)."""
|
|
29
|
+
import torch
|
|
30
|
+
|
|
31
|
+
device = next(model.parameters()).device
|
|
32
|
+
inputs = tokenizer(prompt, return_tensors="pt").to(device)
|
|
33
|
+
|
|
34
|
+
def _one() -> tuple[float, int]:
|
|
35
|
+
torch.cuda.synchronize() if torch.cuda.is_available() else None
|
|
36
|
+
start = time.perf_counter()
|
|
37
|
+
with torch.no_grad():
|
|
38
|
+
out = model.generate(
|
|
39
|
+
**inputs, max_new_tokens=max_new_tokens, do_sample=False
|
|
40
|
+
)
|
|
41
|
+
torch.cuda.synchronize() if torch.cuda.is_available() else None
|
|
42
|
+
elapsed = time.perf_counter() - start
|
|
43
|
+
new_tokens = int(out.shape[-1] - inputs["input_ids"].shape[-1])
|
|
44
|
+
return elapsed, new_tokens
|
|
45
|
+
|
|
46
|
+
for _ in range(max(warmup, 0)):
|
|
47
|
+
_one()
|
|
48
|
+
|
|
49
|
+
times: list[float] = []
|
|
50
|
+
total_tokens = 0
|
|
51
|
+
for _ in range(max(runs, 1)):
|
|
52
|
+
elapsed, new_tokens = _one()
|
|
53
|
+
times.append(elapsed)
|
|
54
|
+
total_tokens += new_tokens
|
|
55
|
+
|
|
56
|
+
times.sort()
|
|
57
|
+
mean = sum(times) / len(times)
|
|
58
|
+
p50 = times[len(times) // 2]
|
|
59
|
+
tps = (total_tokens / sum(times)) if sum(times) > 0 else 0.0
|
|
60
|
+
return LatencyStats(
|
|
61
|
+
latency_ms_mean=round(mean * 1000, 2),
|
|
62
|
+
latency_ms_p50=round(p50 * 1000, 2),
|
|
63
|
+
tokens_per_sec=round(tps, 2),
|
|
64
|
+
generated_tokens=total_tokens,
|
|
65
|
+
runs=len(times),
|
|
66
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Measured VRAM helpers (torch-backed, imported lazily)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from zeroquantz.utils import units
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def reset_peak_memory() -> None:
|
|
9
|
+
try:
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
if torch.cuda.is_available():
|
|
13
|
+
torch.cuda.reset_peak_memory_stats()
|
|
14
|
+
torch.cuda.empty_cache()
|
|
15
|
+
except Exception: # pragma: no cover - torch/CUDA optional
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def peak_vram_gb() -> float | None:
|
|
20
|
+
"""Peak allocated VRAM since the last reset, in GiB, or None if unavailable."""
|
|
21
|
+
try:
|
|
22
|
+
import torch
|
|
23
|
+
|
|
24
|
+
if torch.cuda.is_available():
|
|
25
|
+
return round(units.bytes_to_gb(torch.cuda.max_memory_allocated()), 3)
|
|
26
|
+
except Exception: # pragma: no cover
|
|
27
|
+
pass
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def module_size_gb(module) -> float | None: # noqa: ANN001
|
|
32
|
+
"""On-device size of a live module's parameters + buffers, in GiB."""
|
|
33
|
+
try:
|
|
34
|
+
total = 0
|
|
35
|
+
for p in module.parameters():
|
|
36
|
+
total += p.numel() * p.element_size()
|
|
37
|
+
for b in module.buffers():
|
|
38
|
+
total += b.numel() * b.element_size()
|
|
39
|
+
return round(units.bytes_to_gb(total), 3)
|
|
40
|
+
except Exception: # pragma: no cover
|
|
41
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Quality measurement: perplexity over calibration/eval text (lazy torch)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def measure_perplexity(
|
|
9
|
+
model: Any,
|
|
10
|
+
tokenizer: Any,
|
|
11
|
+
texts: list[str],
|
|
12
|
+
*,
|
|
13
|
+
max_length: int = 512,
|
|
14
|
+
) -> float:
|
|
15
|
+
"""Token-level perplexity of ``model`` over ``texts`` (teacher forcing)."""
|
|
16
|
+
import torch
|
|
17
|
+
|
|
18
|
+
device = next(model.parameters()).device
|
|
19
|
+
total_nll = 0.0
|
|
20
|
+
total_tokens = 0
|
|
21
|
+
with torch.no_grad():
|
|
22
|
+
for text in texts:
|
|
23
|
+
enc = tokenizer(
|
|
24
|
+
text, return_tensors="pt", truncation=True, max_length=max_length
|
|
25
|
+
).to(device)
|
|
26
|
+
input_ids = enc["input_ids"]
|
|
27
|
+
if input_ids.shape[-1] < 2:
|
|
28
|
+
continue
|
|
29
|
+
out = model(**enc, labels=input_ids)
|
|
30
|
+
# HF returns mean NLL over tokens; weight by token count.
|
|
31
|
+
n_tokens = int(input_ids.shape[-1] - 1)
|
|
32
|
+
total_nll += float(out.loss) * n_tokens
|
|
33
|
+
total_tokens += n_tokens
|
|
34
|
+
if total_tokens == 0:
|
|
35
|
+
return float("nan")
|
|
36
|
+
import math
|
|
37
|
+
|
|
38
|
+
return float(math.exp(total_nll / total_tokens))
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""The common benchmark runner.
|
|
2
|
+
|
|
3
|
+
Loads a model (base or quantized directory), measures peak VRAM, load time,
|
|
4
|
+
generation latency, throughput, on-device size, and optional perplexity. Every
|
|
5
|
+
result records the hardware + software environment it was produced on. All
|
|
6
|
+
numbers here are *measured* (``measured=True``) — distinct from the planner's
|
|
7
|
+
estimates.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from zeroquantz.benchmark import latency as latency_mod
|
|
18
|
+
from zeroquantz.benchmark import memory as memory_mod
|
|
19
|
+
from zeroquantz.benchmark import quality as quality_mod
|
|
20
|
+
from zeroquantz.core.exceptions import DependencyError, ZeroQuantzError
|
|
21
|
+
from zeroquantz.utils import units
|
|
22
|
+
from zeroquantz.utils.env import capture_environment
|
|
23
|
+
from zeroquantz.utils.logging import get_logger
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
|
|
28
|
+
from zeroquantz.hardware.capabilities import HardwareProfile
|
|
29
|
+
|
|
30
|
+
log = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BenchmarkResult(BaseModel):
|
|
34
|
+
model_ref: str
|
|
35
|
+
measured: bool = True
|
|
36
|
+
peak_vram_gb: float | None = None
|
|
37
|
+
load_time_s: float | None = None
|
|
38
|
+
model_size_gb: float | None = None
|
|
39
|
+
latency_ms_mean: float | None = None
|
|
40
|
+
latency_ms_p50: float | None = None
|
|
41
|
+
tokens_per_sec: float | None = None
|
|
42
|
+
perplexity: float | None = None
|
|
43
|
+
num_prompts: int = 0
|
|
44
|
+
warmup: int = 0
|
|
45
|
+
runs: int = 0
|
|
46
|
+
max_new_tokens: int = 0
|
|
47
|
+
environment: dict = Field(default_factory=dict)
|
|
48
|
+
notes: list[str] = Field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
def summary_rows(self) -> list[tuple[str, str]]:
|
|
51
|
+
def fmt(v: float | None, unit: str = "") -> str:
|
|
52
|
+
return f"{v:g}{unit}" if v is not None else "n/a"
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
("Peak VRAM", fmt(self.peak_vram_gb, " GB")),
|
|
56
|
+
("Load time", fmt(self.load_time_s, " s")),
|
|
57
|
+
("Model size", fmt(self.model_size_gb, " GB")),
|
|
58
|
+
("Latency (mean)", fmt(self.latency_ms_mean, " ms")),
|
|
59
|
+
("Throughput", fmt(self.tokens_per_sec, " tok/s")),
|
|
60
|
+
("Perplexity", fmt(self.perplexity)),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BenchmarkRunner:
|
|
65
|
+
"""Load and benchmark a model end-to-end."""
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def run(
|
|
69
|
+
model_ref: str,
|
|
70
|
+
*,
|
|
71
|
+
prompts: list[str] | None = None,
|
|
72
|
+
eval_texts: list[str] | None = None,
|
|
73
|
+
warmup: int = 2,
|
|
74
|
+
runs: int = 5,
|
|
75
|
+
max_new_tokens: int = 64,
|
|
76
|
+
measure_quality: bool = False,
|
|
77
|
+
hardware: HardwareProfile | None = None,
|
|
78
|
+
progress: Callable[[str, float], None] | None = None,
|
|
79
|
+
) -> BenchmarkResult:
|
|
80
|
+
try:
|
|
81
|
+
import torch
|
|
82
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
83
|
+
except ImportError as exc:
|
|
84
|
+
raise DependencyError.for_extra(
|
|
85
|
+
"transformers", "torch", purpose="benchmark a model"
|
|
86
|
+
) from exc
|
|
87
|
+
|
|
88
|
+
prompts = prompts or ["Explain the theory of relativity in simple terms."]
|
|
89
|
+
|
|
90
|
+
if progress:
|
|
91
|
+
progress("Loading model", 0.05)
|
|
92
|
+
memory_mod.reset_peak_memory()
|
|
93
|
+
load_start = time.perf_counter()
|
|
94
|
+
try:
|
|
95
|
+
tokenizer = AutoTokenizer.from_pretrained(model_ref)
|
|
96
|
+
model = AutoModelForCausalLM.from_pretrained(model_ref, device_map="auto")
|
|
97
|
+
model.eval()
|
|
98
|
+
except Exception as exc: # pragma: no cover - requires weights
|
|
99
|
+
raise ZeroQuantzError(
|
|
100
|
+
f"Could not load '{model_ref}' for benchmarking.",
|
|
101
|
+
detail=str(exc),
|
|
102
|
+
suggestions=["zeroquantz inspect " + model_ref, "check available VRAM"],
|
|
103
|
+
) from exc
|
|
104
|
+
load_time = time.perf_counter() - load_start
|
|
105
|
+
|
|
106
|
+
if tokenizer.pad_token is None and tokenizer.eos_token is not None:
|
|
107
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
108
|
+
|
|
109
|
+
result = BenchmarkResult(
|
|
110
|
+
model_ref=model_ref,
|
|
111
|
+
num_prompts=len(prompts),
|
|
112
|
+
warmup=warmup,
|
|
113
|
+
runs=runs,
|
|
114
|
+
max_new_tokens=max_new_tokens,
|
|
115
|
+
load_time_s=round(load_time, 2),
|
|
116
|
+
model_size_gb=memory_mod.module_size_gb(model),
|
|
117
|
+
environment=capture_environment(hardware),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if progress:
|
|
121
|
+
progress("Measuring latency", 0.4)
|
|
122
|
+
try:
|
|
123
|
+
stats = latency_mod.measure_generation_latency(
|
|
124
|
+
model, tokenizer, prompts[0],
|
|
125
|
+
max_new_tokens=max_new_tokens, warmup=warmup, runs=runs,
|
|
126
|
+
)
|
|
127
|
+
result.latency_ms_mean = stats.latency_ms_mean
|
|
128
|
+
result.latency_ms_p50 = stats.latency_ms_p50
|
|
129
|
+
result.tokens_per_sec = stats.tokens_per_sec
|
|
130
|
+
except Exception as exc: # pragma: no cover
|
|
131
|
+
result.notes.append(f"latency measurement failed: {exc}")
|
|
132
|
+
|
|
133
|
+
if measure_quality:
|
|
134
|
+
if progress:
|
|
135
|
+
progress("Measuring perplexity", 0.7)
|
|
136
|
+
try:
|
|
137
|
+
result.perplexity = round(
|
|
138
|
+
quality_mod.measure_perplexity(model, tokenizer, eval_texts or prompts), 4
|
|
139
|
+
)
|
|
140
|
+
except Exception as exc: # pragma: no cover
|
|
141
|
+
result.notes.append(f"perplexity measurement failed: {exc}")
|
|
142
|
+
|
|
143
|
+
result.peak_vram_gb = memory_mod.peak_vram_gb()
|
|
144
|
+
if progress:
|
|
145
|
+
progress("Benchmark complete", 1.0)
|
|
146
|
+
del model
|
|
147
|
+
try:
|
|
148
|
+
torch.cuda.empty_cache()
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
return result
|
zeroquantz/cli/app.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""The ``zeroquantz`` Typer application.
|
|
2
|
+
|
|
3
|
+
Running ``zeroquantz`` with no subcommand launches the interactive TUI (falling
|
|
4
|
+
back to a text REPL when stdout is not a TTY). Subcommands provide the scriptable,
|
|
5
|
+
non-interactive surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from zeroquantz import __version__
|
|
16
|
+
from zeroquantz.utils.logging import configure_logging
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
add_completion=False,
|
|
20
|
+
no_args_is_help=False,
|
|
21
|
+
rich_markup_mode="rich",
|
|
22
|
+
help="Interactive model optimization and quantization from your terminal.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _version_callback(value: bool) -> None:
|
|
27
|
+
if value:
|
|
28
|
+
typer.echo(f"zeroquantz {__version__}")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.callback(invoke_without_command=True)
|
|
33
|
+
def _main(
|
|
34
|
+
ctx: typer.Context,
|
|
35
|
+
version: bool = typer.Option(
|
|
36
|
+
False, "--version", callback=_version_callback, is_eager=True, help="Show version and exit."
|
|
37
|
+
),
|
|
38
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose (INFO) console logging."),
|
|
39
|
+
debug: bool = typer.Option(False, "--debug", help="Debug (DEBUG) console logging."),
|
|
40
|
+
session: str | None = typer.Option(None, "--session", help="Use a named session."),
|
|
41
|
+
continue_last: bool = typer.Option(
|
|
42
|
+
False, "--continue", help="Resume the most recent session."
|
|
43
|
+
),
|
|
44
|
+
no_tui: bool = typer.Option(False, "--no-tui", help="Use the plain text REPL instead of the TUI."),
|
|
45
|
+
) -> None:
|
|
46
|
+
configure_logging("debug" if debug else ("verbose" if verbose else "quiet"))
|
|
47
|
+
if ctx.invoked_subcommand is not None:
|
|
48
|
+
return
|
|
49
|
+
_launch_interactive(session=session, continue_last=continue_last, no_tui=no_tui)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _launch_interactive(*, session: str | None, continue_last: bool, no_tui: bool) -> None:
|
|
53
|
+
use_tui = (not no_tui) and sys.stdout.isatty() and sys.stdin.isatty()
|
|
54
|
+
if use_tui:
|
|
55
|
+
try:
|
|
56
|
+
from zeroquantz.tui.app import run_tui
|
|
57
|
+
|
|
58
|
+
run_tui(session_name=session, continue_last=continue_last)
|
|
59
|
+
return
|
|
60
|
+
except Exception as exc: # pragma: no cover - environment dependent
|
|
61
|
+
typer.echo(f"(TUI unavailable: {exc}; falling back to text mode)\n")
|
|
62
|
+
from zeroquantz.cli.interactive import run_repl
|
|
63
|
+
|
|
64
|
+
run_repl(session_name=session, continue_last=continue_last)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Register subcommands.
|
|
68
|
+
from zeroquantz.cli import commands as _commands # noqa: E402
|
|
69
|
+
|
|
70
|
+
_commands.register(app)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _setup_stdio() -> None:
|
|
74
|
+
"""Make Unicode output reliable (Windows consoles default to cp1252)."""
|
|
75
|
+
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
|
|
76
|
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
77
|
+
# Opt into the Rust hf_transfer download accelerator when it's installed
|
|
78
|
+
# (safe no-op otherwise; never sets the flag without the package present).
|
|
79
|
+
from zeroquantz.utils.env import enable_fast_downloads
|
|
80
|
+
|
|
81
|
+
enable_fast_downloads()
|
|
82
|
+
import contextlib
|
|
83
|
+
|
|
84
|
+
for stream in (sys.stdout, sys.stderr):
|
|
85
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
86
|
+
if reconfigure is not None:
|
|
87
|
+
with contextlib.suppress(Exception): # pragma: no cover
|
|
88
|
+
reconfigure(encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> None:
|
|
92
|
+
"""Console-script entry point."""
|
|
93
|
+
_setup_stdio()
|
|
94
|
+
app()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|