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.
Files changed (105) hide show
  1. zeroquantz/__init__.py +14 -0
  2. zeroquantz/__main__.py +8 -0
  3. zeroquantz/agent/__init__.py +16 -0
  4. zeroquantz/agent/dispatcher.py +520 -0
  5. zeroquantz/agent/intents.py +46 -0
  6. zeroquantz/agent/parser.py +255 -0
  7. zeroquantz/benchmark/__init__.py +7 -0
  8. zeroquantz/benchmark/latency.py +66 -0
  9. zeroquantz/benchmark/memory.py +41 -0
  10. zeroquantz/benchmark/quality.py +38 -0
  11. zeroquantz/benchmark/runner.py +151 -0
  12. zeroquantz/cli/__init__.py +7 -0
  13. zeroquantz/cli/app.py +98 -0
  14. zeroquantz/cli/commands.py +459 -0
  15. zeroquantz/cli/interactive.py +56 -0
  16. zeroquantz/core/__init__.py +7 -0
  17. zeroquantz/core/artifacts.py +179 -0
  18. zeroquantz/core/context.py +127 -0
  19. zeroquantz/core/events.py +30 -0
  20. zeroquantz/core/exceptions.py +105 -0
  21. zeroquantz/core/session.py +202 -0
  22. zeroquantz/core/subenv.py +202 -0
  23. zeroquantz/deploy/__init__.py +25 -0
  24. zeroquantz/deploy/assets.py +161 -0
  25. zeroquantz/deploy/launcher.py +80 -0
  26. zeroquantz/deploy/runtime_env.py +66 -0
  27. zeroquantz/deploy/targets.py +154 -0
  28. zeroquantz/export/__init__.py +8 -0
  29. zeroquantz/export/exporter.py +68 -0
  30. zeroquantz/export/report.py +203 -0
  31. zeroquantz/hardware/__init__.py +15 -0
  32. zeroquantz/hardware/capabilities.py +152 -0
  33. zeroquantz/hardware/detector.py +200 -0
  34. zeroquantz/hardware/gpu.py +31 -0
  35. zeroquantz/models/__init__.py +8 -0
  36. zeroquantz/models/architecture.py +168 -0
  37. zeroquantz/models/downloader.py +161 -0
  38. zeroquantz/models/hf_auth.py +105 -0
  39. zeroquantz/models/inspector.py +249 -0
  40. zeroquantz/models/metadata.py +108 -0
  41. zeroquantz/models/search.py +71 -0
  42. zeroquantz/optimization/__init__.py +22 -0
  43. zeroquantz/optimization/candidate.py +272 -0
  44. zeroquantz/optimization/constraints.py +70 -0
  45. zeroquantz/optimization/fit.py +203 -0
  46. zeroquantz/optimization/pareto.py +66 -0
  47. zeroquantz/optimization/planner.py +297 -0
  48. zeroquantz/optimization/recommender.py +149 -0
  49. zeroquantz/profiling/__init__.py +18 -0
  50. zeroquantz/profiling/calibration.py +74 -0
  51. zeroquantz/profiling/sensitivity.py +234 -0
  52. zeroquantz/quantization/__init__.py +17 -0
  53. zeroquantz/quantization/backends/__init__.py +8 -0
  54. zeroquantz/quantization/backends/bitsandbytes.py +210 -0
  55. zeroquantz/quantization/backends/torchao.py +198 -0
  56. zeroquantz/quantization/base.py +136 -0
  57. zeroquantz/quantization/catalog.py +321 -0
  58. zeroquantz/quantization/config.py +106 -0
  59. zeroquantz/quantization/gguf_pipeline.py +210 -0
  60. zeroquantz/quantization/isolated.py +248 -0
  61. zeroquantz/quantization/memory.py +133 -0
  62. zeroquantz/quantization/native.py +91 -0
  63. zeroquantz/quantization/registry.py +101 -0
  64. zeroquantz/render.py +341 -0
  65. zeroquantz/runtimes/__init__.py +18 -0
  66. zeroquantz/runtimes/base.py +64 -0
  67. zeroquantz/runtimes/compatibility.py +91 -0
  68. zeroquantz/runtimes/registry.py +70 -0
  69. zeroquantz/runtimes/transformers.py +53 -0
  70. zeroquantz/runtimes/vllm.py +83 -0
  71. zeroquantz/tui/__init__.py +13 -0
  72. zeroquantz/tui/app.py +77 -0
  73. zeroquantz/tui/banner.py +47 -0
  74. zeroquantz/tui/screens/__init__.py +25 -0
  75. zeroquantz/tui/screens/confirm.py +41 -0
  76. zeroquantz/tui/screens/execute.py +194 -0
  77. zeroquantz/tui/screens/model_select.py +206 -0
  78. zeroquantz/tui/screens/plan.py +177 -0
  79. zeroquantz/tui/screens/quantize_select.py +272 -0
  80. zeroquantz/tui/screens/settings.py +219 -0
  81. zeroquantz/tui/screens/token.py +94 -0
  82. zeroquantz/tui/screens/welcome.py +128 -0
  83. zeroquantz/tui/screens/workspace.py +175 -0
  84. zeroquantz/tui/styles/app.tcss +424 -0
  85. zeroquantz/tui/widgets/__init__.py +9 -0
  86. zeroquantz/tui/widgets/chip.py +36 -0
  87. zeroquantz/tui/widgets/sidebar.py +107 -0
  88. zeroquantz/tui/widgets/status_bar.py +43 -0
  89. zeroquantz/utils/__init__.py +8 -0
  90. zeroquantz/utils/config.py +46 -0
  91. zeroquantz/utils/env.py +78 -0
  92. zeroquantz/utils/logging.py +73 -0
  93. zeroquantz/utils/metrics.py +98 -0
  94. zeroquantz/utils/paths.py +57 -0
  95. zeroquantz/utils/units.py +134 -0
  96. zeroquantz/verification/__init__.py +17 -0
  97. zeroquantz/verification/logits.py +55 -0
  98. zeroquantz/verification/report.py +186 -0
  99. zeroquantz/verification/weights.py +44 -0
  100. zeroquantz/version.py +8 -0
  101. zeroquantz-0.1.0.dist-info/METADATA +72 -0
  102. zeroquantz-0.1.0.dist-info/RECORD +105 -0
  103. zeroquantz-0.1.0.dist-info/WHEEL +4 -0
  104. zeroquantz-0.1.0.dist-info/entry_points.txt +2 -0
  105. 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,8 @@
1
+ """Enable ``python -m zeroquantz``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.cli.app import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -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})"