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
zeroquantz/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""ZeroQuantz — interactive model optimization and quantization for the terminal.
|
|
2
|
+
|
|
3
|
+
The top-level package deliberately imports *nothing* heavy at import time. The
|
|
4
|
+
planning/recommendation core, CLI, and TUI depend only on lightweight libraries;
|
|
5
|
+
torch/transformers/bitsandbytes/torchao are imported lazily by the components
|
|
6
|
+
that actually need them. This keeps ``import zeroquantz`` fast and side-effect
|
|
7
|
+
free, and lets the offline planning engine run on machines without a GPU.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from zeroquantz.version import __version__
|
|
13
|
+
|
|
14
|
+
__all__ = ["__version__"]
|
zeroquantz/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Intent parsing and dispatch: the funnel from user text to deterministic APIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from zeroquantz.agent.dispatcher import CommandResult, Dispatcher
|
|
6
|
+
from zeroquantz.agent.intents import Intent, IntentKind
|
|
7
|
+
from zeroquantz.agent.parser import IntentParser, parse_goal
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CommandResult",
|
|
11
|
+
"Dispatcher",
|
|
12
|
+
"Intent",
|
|
13
|
+
"IntentKind",
|
|
14
|
+
"IntentParser",
|
|
15
|
+
"parse_goal",
|
|
16
|
+
]
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""Dispatch intents to deterministic application services.
|
|
2
|
+
|
|
3
|
+
A single :class:`Dispatcher` maps each :class:`IntentKind` to a handler via a
|
|
4
|
+
registry (no ``if kind == ...`` ladder). Both the CLI REPL and the TUI feed
|
|
5
|
+
parsed intents through here, so natural language and slash commands always hit
|
|
6
|
+
identical code paths. Handlers return a UI-agnostic :class:`CommandResult`; the
|
|
7
|
+
rendering layer decides how to display it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from zeroquantz.agent.intents import Intent, IntentKind
|
|
17
|
+
from zeroquantz.core.events import ProgressCallback, null_progress
|
|
18
|
+
from zeroquantz.core.exceptions import CommandError, ZeroQuantzError
|
|
19
|
+
from zeroquantz.utils.logging import get_logger
|
|
20
|
+
|
|
21
|
+
# The optimization/profiling/inspection services (CandidateGenerator, Recommender,
|
|
22
|
+
# MixedPrecisionPlanner, SensitivityProfiler, ModelInspector, pareto_frontier,
|
|
23
|
+
# Objective) are imported lazily inside the handlers that use them. Importing the
|
|
24
|
+
# Dispatcher is on the hot path of every `zeroquantz` invocation — including
|
|
25
|
+
# `--version`/`--help` and shell completion — so keeping those (pydantic + the
|
|
26
|
+
# whole optimization graph) out of module import time is a large cold-start win.
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from zeroquantz.core.context import AppContext
|
|
30
|
+
from zeroquantz.models.metadata import ModelProfile
|
|
31
|
+
|
|
32
|
+
log = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CommandResult:
|
|
37
|
+
"""UI-agnostic outcome of dispatching one intent."""
|
|
38
|
+
|
|
39
|
+
kind: str
|
|
40
|
+
message: str = ""
|
|
41
|
+
payload: Any = None
|
|
42
|
+
success: bool = True
|
|
43
|
+
should_exit: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# (command, description) pairs for /help and autocomplete.
|
|
47
|
+
COMMANDS: tuple[tuple[str, str], ...] = (
|
|
48
|
+
("/model <id>", "Load & inspect a model (Hub id or local path)"),
|
|
49
|
+
("/inspect", "Show the current model's architecture and size"),
|
|
50
|
+
("/hardware", "Show detected GPU, VRAM, and precision support"),
|
|
51
|
+
("/goal <text>", "Set constraints, e.g. 'fit under 8GB target vllm quality'"),
|
|
52
|
+
("/recommend", "Rank quantization strategies for the current goal"),
|
|
53
|
+
("/profile", "Estimate per-layer sensitivity (heuristic)"),
|
|
54
|
+
("/plan", "Build a mixed-precision plan for the budget"),
|
|
55
|
+
("/quantize [method]", "Quantize the model (needs a backend + GPU)"),
|
|
56
|
+
("/benchmark [path]", "Measure VRAM, latency, throughput"),
|
|
57
|
+
("/compare", "Compare candidates and show the Pareto frontier"),
|
|
58
|
+
("/verify <base> <quant>", "Compare quantized vs baseline quality"),
|
|
59
|
+
("/export <dir>", "Write the ZeroQuantz report (and model if quantized)"),
|
|
60
|
+
("/history", "Show the configuration history"),
|
|
61
|
+
("/undo", "Revert the last configuration change"),
|
|
62
|
+
("/checkout <n>", "Restore a history entry"),
|
|
63
|
+
("/sessions", "List saved sessions"),
|
|
64
|
+
("/save [name]", "Save the current session"),
|
|
65
|
+
("/help", "Show this help"),
|
|
66
|
+
("/exit", "Leave ZeroQuantz"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
Handler = Callable[["AppContext", Intent, ProgressCallback], CommandResult]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Dispatcher:
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
self._handlers: dict[IntentKind, Handler] = {
|
|
75
|
+
IntentKind.LOAD_MODEL: self._load_model,
|
|
76
|
+
IntentKind.INSPECT: self._inspect,
|
|
77
|
+
IntentKind.HARDWARE: self._hardware,
|
|
78
|
+
IntentKind.SET_GOAL: self._set_goal,
|
|
79
|
+
IntentKind.RECOMMEND: self._recommend,
|
|
80
|
+
IntentKind.PROFILE: self._profile,
|
|
81
|
+
IntentKind.PLAN: self._plan,
|
|
82
|
+
IntentKind.QUANTIZE: self._quantize,
|
|
83
|
+
IntentKind.BENCHMARK: self._benchmark,
|
|
84
|
+
IntentKind.COMPARE: self._compare,
|
|
85
|
+
IntentKind.VERIFY: self._verify,
|
|
86
|
+
IntentKind.EXPORT: self._export,
|
|
87
|
+
IntentKind.HISTORY: self._history,
|
|
88
|
+
IntentKind.UNDO: self._undo,
|
|
89
|
+
IntentKind.CHECKOUT: self._checkout,
|
|
90
|
+
IntentKind.SESSIONS: self._sessions,
|
|
91
|
+
IntentKind.SAVE: self._save,
|
|
92
|
+
IntentKind.LOAD_SESSION: self._load_session,
|
|
93
|
+
IntentKind.HELP: self._help,
|
|
94
|
+
IntentKind.EXIT: self._exit,
|
|
95
|
+
IntentKind.UNKNOWN: self._unknown,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def dispatch(
|
|
99
|
+
self,
|
|
100
|
+
intent: Intent,
|
|
101
|
+
ctx: AppContext,
|
|
102
|
+
progress: ProgressCallback = null_progress,
|
|
103
|
+
) -> CommandResult:
|
|
104
|
+
handler = self._handlers.get(intent.kind, self._unknown)
|
|
105
|
+
try:
|
|
106
|
+
return handler(ctx, intent, progress)
|
|
107
|
+
except ZeroQuantzError as exc:
|
|
108
|
+
return CommandResult("error", exc.format(), payload=exc, success=False)
|
|
109
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
110
|
+
log.exception("unexpected error handling %s", intent.kind)
|
|
111
|
+
return CommandResult("error", f"Unexpected error: {exc}", success=False)
|
|
112
|
+
|
|
113
|
+
# ---- helpers ------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _require_model(ctx: AppContext) -> ModelProfile:
|
|
117
|
+
if ctx.session.model_profile is None:
|
|
118
|
+
raise CommandError(
|
|
119
|
+
"No model loaded yet.",
|
|
120
|
+
suggestions=["/model Qwen/Qwen3-8B"],
|
|
121
|
+
)
|
|
122
|
+
return ctx.session.model_profile
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _apply_goal_updates(ctx: AppContext, intent: Intent) -> list[str]:
|
|
126
|
+
updates = dict(intent.args.get("goal_updates") or {})
|
|
127
|
+
if not updates:
|
|
128
|
+
return []
|
|
129
|
+
if "objective" in updates:
|
|
130
|
+
from zeroquantz.optimization.constraints import Objective
|
|
131
|
+
|
|
132
|
+
updates["objective"] = Objective(updates["objective"])
|
|
133
|
+
ctx.session.goal = ctx.session.goal.with_updates(**updates)
|
|
134
|
+
ctx.candidates = [] # invalidate cached candidates
|
|
135
|
+
label = "set " + ", ".join(f"{k}={_fmt(v)}" for k, v in updates.items())
|
|
136
|
+
ctx.session.record(label)
|
|
137
|
+
return [label]
|
|
138
|
+
|
|
139
|
+
def _ensure_candidates(self, ctx: AppContext) -> list:
|
|
140
|
+
from zeroquantz.optimization.candidate import CandidateGenerator
|
|
141
|
+
|
|
142
|
+
model = self._require_model(ctx)
|
|
143
|
+
if not ctx.candidates:
|
|
144
|
+
ctx.candidates = CandidateGenerator.generate(
|
|
145
|
+
model, ctx.hardware, ctx.goal, registry=ctx.backends
|
|
146
|
+
)
|
|
147
|
+
return ctx.candidates
|
|
148
|
+
|
|
149
|
+
# ---- handlers -----------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def _load_model(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
152
|
+
from zeroquantz.models.inspector import ModelInspector
|
|
153
|
+
|
|
154
|
+
model_id = (intent.args.get("model_id") or "").strip()
|
|
155
|
+
if not model_id:
|
|
156
|
+
raise CommandError("Usage: /model <hf-id-or-path>", suggestions=["/model Qwen/Qwen3-8B"])
|
|
157
|
+
progress(f"Inspecting {model_id}", 0.2)
|
|
158
|
+
profile = ModelInspector.inspect(model_id)
|
|
159
|
+
progress("Loaded metadata", 1.0)
|
|
160
|
+
ctx.session.model_id = model_id
|
|
161
|
+
ctx.session.model_profile = profile
|
|
162
|
+
ctx.candidates = []
|
|
163
|
+
ctx.sensitivity = None
|
|
164
|
+
ctx.session.record(f"load {model_id}")
|
|
165
|
+
ctx.save()
|
|
166
|
+
return CommandResult("model", f"Model loaded: {model_id}", profile)
|
|
167
|
+
|
|
168
|
+
def _inspect(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
169
|
+
target = intent.args.get("target")
|
|
170
|
+
if target and target != ctx.session.model_id:
|
|
171
|
+
return self._load_model(ctx, Intent(IntentKind.LOAD_MODEL, {"model_id": target}), progress)
|
|
172
|
+
model = self._require_model(ctx)
|
|
173
|
+
return CommandResult("model", f"Model: {model.model_id}", model)
|
|
174
|
+
|
|
175
|
+
def _hardware(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
176
|
+
return CommandResult("hardware", f"Hardware: {ctx.hardware.gpu_name}", ctx.hardware)
|
|
177
|
+
|
|
178
|
+
def _set_goal(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
179
|
+
labels = self._apply_goal_updates(ctx, intent)
|
|
180
|
+
if not labels:
|
|
181
|
+
return CommandResult(
|
|
182
|
+
"message",
|
|
183
|
+
"No goal changes detected. Try: 'fit under 8GB', 'target vllm', 'optimize for quality'.",
|
|
184
|
+
ctx.goal,
|
|
185
|
+
)
|
|
186
|
+
ctx.save()
|
|
187
|
+
return CommandResult("goal", "Optimization goal updated.", ctx.goal)
|
|
188
|
+
|
|
189
|
+
def _recommend(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
190
|
+
from zeroquantz.optimization.recommender import Recommender
|
|
191
|
+
|
|
192
|
+
self._apply_goal_updates(ctx, intent)
|
|
193
|
+
cands = self._ensure_candidates(ctx)
|
|
194
|
+
ranked = Recommender.rank(cands, ctx.goal)
|
|
195
|
+
best = Recommender.recommend(cands, ctx.goal)
|
|
196
|
+
if best is not None:
|
|
197
|
+
ctx.session.selected_method = best.candidate.method
|
|
198
|
+
ctx.save()
|
|
199
|
+
return CommandResult(
|
|
200
|
+
"candidates",
|
|
201
|
+
"Ranked quantization strategies.",
|
|
202
|
+
{"ranked": ranked, "best": best, "goal": ctx.goal},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _profile(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
206
|
+
from zeroquantz.profiling.sensitivity import SensitivityProfiler
|
|
207
|
+
|
|
208
|
+
model = self._require_model(ctx)
|
|
209
|
+
progress("Estimating layer sensitivity", 0.5)
|
|
210
|
+
profile = SensitivityProfiler.heuristic(model)
|
|
211
|
+
ctx.sensitivity = profile
|
|
212
|
+
progress("Done", 1.0)
|
|
213
|
+
return CommandResult(
|
|
214
|
+
"profile",
|
|
215
|
+
"Estimated per-layer sensitivity (heuristic prior). "
|
|
216
|
+
"Use `zeroquantz profile <model> --measured` for a measured pass.",
|
|
217
|
+
profile,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _plan(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
221
|
+
from zeroquantz.optimization.planner import MixedPrecisionPlanner
|
|
222
|
+
|
|
223
|
+
self._apply_goal_updates(ctx, intent)
|
|
224
|
+
model = self._require_model(ctx)
|
|
225
|
+
progress("Planning precision assignment", 0.5)
|
|
226
|
+
plan = MixedPrecisionPlanner.plan(model, ctx.hardware, ctx.goal, ctx.sensitivity)
|
|
227
|
+
ctx.session.plan = plan
|
|
228
|
+
ctx.session.record("plan")
|
|
229
|
+
ctx.save()
|
|
230
|
+
return CommandResult("plan", "Mixed-precision plan ready.", plan)
|
|
231
|
+
|
|
232
|
+
def _quantize(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
233
|
+
from zeroquantz.quantization.catalog import Execution
|
|
234
|
+
|
|
235
|
+
model = self._require_model(ctx)
|
|
236
|
+
fmt = self._resolve_format(ctx, intent)
|
|
237
|
+
if fmt is None:
|
|
238
|
+
raise CommandError("No quantization strategy selected.", suggestions=["/recommend"])
|
|
239
|
+
ctx.session.selected_format_id = fmt.id
|
|
240
|
+
ctx.session.selected_method = fmt.method or fmt.id
|
|
241
|
+
# Prefer the locally-downloaded snapshot (from the download screen) if present.
|
|
242
|
+
model_ref = ctx.session.model_local_path or ctx.session.model_id or model.model_id
|
|
243
|
+
output = intent.args.get("output")
|
|
244
|
+
|
|
245
|
+
if fmt.execution is Execution.IN_ENV:
|
|
246
|
+
return self._quantize_in_env(ctx, fmt, model, model_ref, output, progress)
|
|
247
|
+
if fmt.execution is Execution.ISOLATED:
|
|
248
|
+
return self._quantize_isolated(ctx, fmt, model_ref, output, progress)
|
|
249
|
+
raise self._external_guidance(fmt, model_ref)
|
|
250
|
+
|
|
251
|
+
def _resolve_format(self, ctx: AppContext, intent: Intent):
|
|
252
|
+
from zeroquantz.optimization.recommender import Recommender
|
|
253
|
+
from zeroquantz.quantization import catalog
|
|
254
|
+
|
|
255
|
+
fid = intent.args.get("format_id") or ctx.session.selected_format_id
|
|
256
|
+
if fid and catalog.get_format(fid):
|
|
257
|
+
return catalog.get_format(fid)
|
|
258
|
+
method = intent.args.get("method") or ctx.session.selected_method
|
|
259
|
+
if method:
|
|
260
|
+
if catalog.get_format(method):
|
|
261
|
+
return catalog.get_format(method)
|
|
262
|
+
for f in catalog.ALL_FORMATS:
|
|
263
|
+
if f.method == method:
|
|
264
|
+
return f
|
|
265
|
+
cands = self._ensure_candidates(ctx)
|
|
266
|
+
best = Recommender.recommend(cands, ctx.goal)
|
|
267
|
+
return catalog.get_format(best.candidate.format_id) if best else None
|
|
268
|
+
|
|
269
|
+
def _quantize_in_env(self, ctx, fmt, model, model_ref, output, progress):
|
|
270
|
+
backend = ctx.backends.get(fmt.backend)
|
|
271
|
+
err = backend.availability_error()
|
|
272
|
+
if err is not None:
|
|
273
|
+
raise err
|
|
274
|
+
if not ctx.hardware.cuda_available:
|
|
275
|
+
raise CommandError(
|
|
276
|
+
"Quantization needs a CUDA GPU, which was not detected.",
|
|
277
|
+
suggestions=["/recommend (planning still works without a GPU)"],
|
|
278
|
+
)
|
|
279
|
+
config = backend.default_config(fmt.method, model, ctx.hardware, ctx.goal)
|
|
280
|
+
progress(f"Quantizing with {backend.name}:{fmt.method}", 0.1)
|
|
281
|
+
result = backend.quantize(model_ref, config, progress=progress)
|
|
282
|
+
ctx.last_quant_result = result
|
|
283
|
+
ctx.session.record(f"quantize {fmt.id}")
|
|
284
|
+
ctx.save()
|
|
285
|
+
if output:
|
|
286
|
+
from zeroquantz.export.exporter import Exporter
|
|
287
|
+
|
|
288
|
+
Exporter.export_model(result, output, backend)
|
|
289
|
+
result.output_dir = output
|
|
290
|
+
_record_quantized(output, ctx.session.model_id, fmt.id, fmt.method)
|
|
291
|
+
return CommandResult(
|
|
292
|
+
"quant", f"Quantized in-env with {fmt.label} in {result.elapsed_s:.1f}s.", result
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _quantize_isolated(self, ctx, fmt, model_ref, output, progress):
|
|
296
|
+
from pathlib import Path
|
|
297
|
+
|
|
298
|
+
from zeroquantz.quantization.base import QuantizationResult
|
|
299
|
+
from zeroquantz.quantization.config import QuantizationConfig
|
|
300
|
+
from zeroquantz.quantization.isolated import IsolatedEnvRunner
|
|
301
|
+
|
|
302
|
+
runner = IsolatedEnvRunner()
|
|
303
|
+
if not runner.supports(fmt.family):
|
|
304
|
+
raise self._external_guidance(fmt, model_ref)
|
|
305
|
+
name = model_ref.split("/")[-1]
|
|
306
|
+
out = output or str(Path.cwd() / f"zeroquantz-{name}-{fmt.id}")
|
|
307
|
+
progress(f"Building isolated {fmt.family} env (first run downloads the toolchain)", 0.02)
|
|
308
|
+
res = runner.quantize(
|
|
309
|
+
fmt, model_ref, out,
|
|
310
|
+
bits=max(2, round(fmt.bits_per_weight)),
|
|
311
|
+
cuda_version=ctx.hardware.cuda_version,
|
|
312
|
+
progress=progress,
|
|
313
|
+
)
|
|
314
|
+
config = QuantizationConfig(
|
|
315
|
+
backend=fmt.family, method=fmt.method or fmt.id,
|
|
316
|
+
weight_bits=max(1, min(16, round(fmt.bits_per_weight))),
|
|
317
|
+
activation_bits=fmt.activation_bits, extra={"format_id": fmt.id, "isolated": True},
|
|
318
|
+
)
|
|
319
|
+
result = QuantizationResult(
|
|
320
|
+
backend=fmt.family, method=fmt.id, config=config,
|
|
321
|
+
output_dir=res.output_dir, elapsed_s=res.elapsed_s,
|
|
322
|
+
)
|
|
323
|
+
ctx.last_quant_result = result
|
|
324
|
+
_record_quantized(res.output_dir, ctx.session.model_id, fmt.id, fmt.method or fmt.id)
|
|
325
|
+
ctx.session.record(f"quantize {fmt.id} (isolated)")
|
|
326
|
+
ctx.save()
|
|
327
|
+
return CommandResult(
|
|
328
|
+
"quant",
|
|
329
|
+
f"Quantized via isolated {fmt.family} env in {res.elapsed_s:.0f}s → {res.output_dir}",
|
|
330
|
+
result,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def _external_guidance(self, fmt, model_ref) -> CommandError:
|
|
334
|
+
if fmt.family == "bitnet":
|
|
335
|
+
return CommandError(
|
|
336
|
+
"BitNet b1.58 is a training-time (native-ternary) format, not a PTQ step.",
|
|
337
|
+
detail="Use a pre-trained BitNet checkpoint and run it with bitnet.cpp / llama.cpp.",
|
|
338
|
+
)
|
|
339
|
+
if "llamacpp" in fmt.runtimes: # GGUF (incl. Unsloth dynamic GGUF)
|
|
340
|
+
qtype = fmt.id.replace("gguf_", "").replace("unsloth_ud_", "UD-").upper()
|
|
341
|
+
return CommandError(
|
|
342
|
+
f"'{fmt.label}' is produced with llama.cpp (not auto-run yet).",
|
|
343
|
+
detail="Convert to GGUF, then quantize:",
|
|
344
|
+
suggestions=[
|
|
345
|
+
f"python convert_hf_to_gguf.py {model_ref} --outfile model.f16.gguf",
|
|
346
|
+
f"llama-quantize model.f16.gguf {model_ref.split('/')[-1]}-{qtype}.gguf {qtype}",
|
|
347
|
+
],
|
|
348
|
+
)
|
|
349
|
+
return CommandError(
|
|
350
|
+
f"'{fmt.label}' isn't auto-executed in this release.",
|
|
351
|
+
detail=f"Produced by {fmt.produced_by}. It is catalogued and estimated here; "
|
|
352
|
+
"execution is on the roadmap.",
|
|
353
|
+
suggestions=["/recommend (pick an in-env or isolated format)", "/plan (mixed precision)"],
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _benchmark(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
357
|
+
from zeroquantz.benchmark.runner import BenchmarkRunner
|
|
358
|
+
|
|
359
|
+
target = intent.args.get("target") or ctx.session.model_id
|
|
360
|
+
if not target:
|
|
361
|
+
raise CommandError("Usage: /benchmark <model-id-or-path>", suggestions=["/model <id>"])
|
|
362
|
+
result = BenchmarkRunner.run(
|
|
363
|
+
target,
|
|
364
|
+
warmup=ctx.config.benchmark.warmup_runs,
|
|
365
|
+
runs=ctx.config.benchmark.runs,
|
|
366
|
+
hardware=ctx.hardware,
|
|
367
|
+
progress=progress,
|
|
368
|
+
)
|
|
369
|
+
ctx.session.last_benchmark = result
|
|
370
|
+
ctx.session.record(f"benchmark {target}")
|
|
371
|
+
ctx.save()
|
|
372
|
+
return CommandResult("benchmark", f"Benchmarked {target}.", result)
|
|
373
|
+
|
|
374
|
+
def _compare(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
375
|
+
from zeroquantz.optimization.pareto import pareto_frontier
|
|
376
|
+
from zeroquantz.optimization.recommender import Recommender
|
|
377
|
+
|
|
378
|
+
cands = self._ensure_candidates(ctx)
|
|
379
|
+
ranked = Recommender.rank(cands, ctx.goal)[:20]
|
|
380
|
+
frontier = {id(c) for c in pareto_frontier(cands)}
|
|
381
|
+
return CommandResult(
|
|
382
|
+
"candidates",
|
|
383
|
+
"Candidate comparison (* = Pareto-optimal).",
|
|
384
|
+
{"ranked": ranked, "pareto_ids": frontier, "goal": ctx.goal, "compare": True},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def _verify(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
388
|
+
from zeroquantz.verification.report import Verifier
|
|
389
|
+
|
|
390
|
+
base = intent.args.get("base") or ctx.session.model_id
|
|
391
|
+
quant = intent.args.get("quant")
|
|
392
|
+
if not base or not quant:
|
|
393
|
+
raise CommandError(
|
|
394
|
+
"Usage: /verify <base-model> <quantized-path>",
|
|
395
|
+
suggestions=["/verify Qwen/Qwen3-8B ./qwen-nf4"],
|
|
396
|
+
)
|
|
397
|
+
report = Verifier.verify(base, quant, hardware=ctx.hardware, progress=progress)
|
|
398
|
+
ctx.session.last_verification = report
|
|
399
|
+
ctx.session.record(f"verify {quant}")
|
|
400
|
+
ctx.save()
|
|
401
|
+
return CommandResult("verification", f"Verification: {report.status}", report)
|
|
402
|
+
|
|
403
|
+
def _export(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
404
|
+
from zeroquantz.export.exporter import Exporter
|
|
405
|
+
from zeroquantz.export.report import build_report
|
|
406
|
+
|
|
407
|
+
path = intent.args.get("path")
|
|
408
|
+
if not path:
|
|
409
|
+
raise CommandError("Usage: /export <output-dir>", suggestions=["/export ./qwen-report"])
|
|
410
|
+
model = self._require_model(ctx)
|
|
411
|
+
config = None
|
|
412
|
+
if ctx.session.selected_method:
|
|
413
|
+
try:
|
|
414
|
+
backend = ctx.backends.get_for_method(ctx.session.selected_method)
|
|
415
|
+
config = backend.default_config(ctx.session.selected_method, model, ctx.hardware, ctx.goal)
|
|
416
|
+
except ZeroQuantzError:
|
|
417
|
+
config = None
|
|
418
|
+
report = build_report(
|
|
419
|
+
model,
|
|
420
|
+
config=config,
|
|
421
|
+
hardware=ctx.hardware,
|
|
422
|
+
goal=ctx.goal,
|
|
423
|
+
plan=ctx.session.plan,
|
|
424
|
+
benchmark=ctx.session.last_benchmark,
|
|
425
|
+
verification=ctx.session.last_verification,
|
|
426
|
+
)
|
|
427
|
+
if ctx.last_quant_result is not None and ctx.session.selected_method:
|
|
428
|
+
backend = ctx.backends.get_for_method(ctx.session.selected_method)
|
|
429
|
+
out = Exporter.export_model(ctx.last_quant_result, path, backend, report=report)
|
|
430
|
+
_record_quantized(str(out), ctx.session.model_id, ctx.session.selected_format_id, ctx.session.selected_method)
|
|
431
|
+
msg = f"Exported quantized model + report to {out}"
|
|
432
|
+
else:
|
|
433
|
+
json_path, md_path = Exporter.write_report(report, path)
|
|
434
|
+
msg = f"Wrote report:\n {json_path}\n {md_path}"
|
|
435
|
+
ctx.session.record(f"export {path}")
|
|
436
|
+
ctx.save()
|
|
437
|
+
return CommandResult("message", msg, report)
|
|
438
|
+
|
|
439
|
+
def _history(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
440
|
+
return CommandResult("history", "Configuration history.", ctx.session.history_view())
|
|
441
|
+
|
|
442
|
+
def _undo(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
443
|
+
label = ctx.session.undo()
|
|
444
|
+
if label is None:
|
|
445
|
+
return CommandResult("message", "Nothing to undo.")
|
|
446
|
+
# Model may have changed; drop a now-stale profile.
|
|
447
|
+
if ctx.session.model_profile and ctx.session.model_profile.model_id != ctx.session.model_id:
|
|
448
|
+
ctx.session.model_profile = None
|
|
449
|
+
ctx.candidates = []
|
|
450
|
+
ctx.save()
|
|
451
|
+
return CommandResult("message", f"Reverted '{label}'.", ctx.session)
|
|
452
|
+
|
|
453
|
+
def _checkout(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
454
|
+
index = intent.args.get("index")
|
|
455
|
+
if index is None:
|
|
456
|
+
raise CommandError("Usage: /checkout <history-index>", suggestions=["/history"])
|
|
457
|
+
snapshot = ctx.session.checkout(int(index))
|
|
458
|
+
ctx.candidates = []
|
|
459
|
+
ctx.save()
|
|
460
|
+
return CommandResult("message", f"Checked out #{index}: {snapshot.label}.", ctx.session)
|
|
461
|
+
|
|
462
|
+
def _sessions(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
463
|
+
return CommandResult("sessions", "Saved sessions.", ctx.repo.list())
|
|
464
|
+
|
|
465
|
+
def _save(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
466
|
+
name = intent.args.get("name")
|
|
467
|
+
if name:
|
|
468
|
+
ctx.session.name = name
|
|
469
|
+
elif ctx.session.name.startswith("scratch-"):
|
|
470
|
+
ctx.session.name = "default"
|
|
471
|
+
ctx.repo.save(ctx.session)
|
|
472
|
+
return CommandResult("message", f"Saved session '{ctx.session.name}'.")
|
|
473
|
+
|
|
474
|
+
def _load_session(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
475
|
+
name = (intent.args.get("name") or "").strip()
|
|
476
|
+
if not name:
|
|
477
|
+
raise CommandError("Usage: /load <session-name>", suggestions=["/sessions"])
|
|
478
|
+
session = ctx.repo.load(name)
|
|
479
|
+
session.hardware = ctx.hardware
|
|
480
|
+
ctx.session = session
|
|
481
|
+
ctx.candidates = []
|
|
482
|
+
return CommandResult("message", f"Loaded session '{name}'.", session)
|
|
483
|
+
|
|
484
|
+
def _help(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
485
|
+
return CommandResult("help", "ZeroQuantz commands.", COMMANDS)
|
|
486
|
+
|
|
487
|
+
def _exit(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
488
|
+
return CommandResult("exit", "Goodbye.", should_exit=True)
|
|
489
|
+
|
|
490
|
+
def _unknown(self, ctx: AppContext, intent: Intent, progress: ProgressCallback) -> CommandResult:
|
|
491
|
+
cmd = intent.args.get("command")
|
|
492
|
+
if cmd:
|
|
493
|
+
return CommandResult("error", f"Unknown command '/{cmd}'. Type /help.", success=False)
|
|
494
|
+
return CommandResult(
|
|
495
|
+
"message",
|
|
496
|
+
"I didn't catch a command there. Try /help, or say something like "
|
|
497
|
+
"'load Qwen/Qwen3-8B' or 'fit under 8GB for vllm'.",
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _record_quantized(
|
|
502
|
+
path: str, base_model: str | None, format_id: str | None, method: str | None
|
|
503
|
+
) -> None:
|
|
504
|
+
"""Register a quantized output so Settings can list/manage it (best-effort)."""
|
|
505
|
+
try:
|
|
506
|
+
from zeroquantz.core.artifacts import default_quantized_registry
|
|
507
|
+
|
|
508
|
+
default_quantized_registry().add(
|
|
509
|
+
path, base_model=base_model, format_id=format_id, method=method
|
|
510
|
+
)
|
|
511
|
+
except Exception: # pragma: no cover - never let bookkeeping break a quantize
|
|
512
|
+
log.debug("could not record quantized artifact at %s", path)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _fmt(value: Any) -> str:
|
|
516
|
+
from zeroquantz.optimization.constraints import Objective as _Obj
|
|
517
|
+
|
|
518
|
+
if isinstance(value, _Obj):
|
|
519
|
+
return value.value
|
|
520
|
+
return str(value)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Intent types.
|
|
2
|
+
|
|
3
|
+
Natural language and slash commands both parse into a single :class:`Intent`
|
|
4
|
+
(a ``kind`` plus a small ``args`` dict), which the dispatcher routes to a handler.
|
|
5
|
+
Keeping one flat type — rather than a subclass per command — keeps the parser and
|
|
6
|
+
the dispatch table simple and fully testable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IntentKind(str, Enum):
|
|
16
|
+
LOAD_MODEL = "load_model"
|
|
17
|
+
INSPECT = "inspect"
|
|
18
|
+
HARDWARE = "hardware"
|
|
19
|
+
SET_GOAL = "set_goal"
|
|
20
|
+
RECOMMEND = "recommend"
|
|
21
|
+
PROFILE = "profile"
|
|
22
|
+
PLAN = "plan"
|
|
23
|
+
QUANTIZE = "quantize"
|
|
24
|
+
BENCHMARK = "benchmark"
|
|
25
|
+
COMPARE = "compare"
|
|
26
|
+
VERIFY = "verify"
|
|
27
|
+
EXPORT = "export"
|
|
28
|
+
HISTORY = "history"
|
|
29
|
+
UNDO = "undo"
|
|
30
|
+
CHECKOUT = "checkout"
|
|
31
|
+
SESSIONS = "sessions"
|
|
32
|
+
SAVE = "save"
|
|
33
|
+
LOAD_SESSION = "load_session"
|
|
34
|
+
HELP = "help"
|
|
35
|
+
EXIT = "exit"
|
|
36
|
+
UNKNOWN = "unknown"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Intent:
|
|
41
|
+
kind: IntentKind
|
|
42
|
+
args: dict = field(default_factory=dict)
|
|
43
|
+
raw: str = ""
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str: # concise for tests
|
|
46
|
+
return f"Intent({self.kind.value}, {self.args})"
|