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
@@ -0,0 +1,101 @@
1
+ """The quantization backend registry.
2
+
3
+ Built-in backends are registered directly. Third-party backends register through
4
+ the ``zeroquantz.quantizers`` entry-point group, so a plugin package
5
+ (``zeroquantz-awq``, ``zeroquantz-gptq``, ...) becomes available with no change to
6
+ core. ``available_backends()`` reflects what is actually usable on this machine.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from zeroquantz.core.exceptions import BackendUnavailableError
12
+ from zeroquantz.quantization.base import QuantizationBackend
13
+ from zeroquantz.utils.logging import get_logger
14
+
15
+ log = get_logger(__name__)
16
+
17
+ _ENTRY_POINT_GROUP = "zeroquantz.quantizers"
18
+
19
+
20
+ class BackendRegistry:
21
+ """A collection of quantization backends keyed by name."""
22
+
23
+ def __init__(self) -> None:
24
+ self._backends: dict[str, QuantizationBackend] = {}
25
+
26
+ def register(self, backend: QuantizationBackend, *, replace: bool = False) -> None:
27
+ if backend.name in self._backends and not replace:
28
+ raise ValueError(f"backend '{backend.name}' is already registered")
29
+ self._backends[backend.name] = backend
30
+
31
+ def all_backends(self) -> list[QuantizationBackend]:
32
+ """Every registered backend, whether or not its deps are installed."""
33
+ return list(self._backends.values())
34
+
35
+ def available_backends(self) -> list[QuantizationBackend]:
36
+ """Only backends whose dependencies are importable right now."""
37
+ return [b for b in self._backends.values() if b.is_available()]
38
+
39
+ def names(self) -> list[str]:
40
+ return list(self._backends)
41
+
42
+ def get(self, name: str) -> QuantizationBackend:
43
+ try:
44
+ return self._backends[name]
45
+ except KeyError as exc:
46
+ known = ", ".join(sorted(self._backends)) or "none"
47
+ raise BackendUnavailableError(
48
+ f"No quantization backend named '{name}'.",
49
+ detail=f"Registered backends: {known}.",
50
+ suggestions=["zeroquantz recommend <model>"],
51
+ ) from exc
52
+
53
+ def get_for_method(self, method: str) -> QuantizationBackend:
54
+ for backend in self._backends.values():
55
+ if method in backend.methods:
56
+ return backend
57
+ raise BackendUnavailableError(
58
+ f"No registered backend provides method '{method}'.",
59
+ suggestions=["zeroquantz recommend <model>"],
60
+ )
61
+
62
+ def methods(self) -> dict[str, str]:
63
+ """Map every known method -> the backend that provides it."""
64
+ return {m: b.name for b in self._backends.values() for m in b.methods}
65
+
66
+ def load_plugins(self) -> None:
67
+ """Discover and register external backends via entry points."""
68
+ try:
69
+ from importlib.metadata import entry_points
70
+ except ImportError: # pragma: no cover - py<3.8, unreachable on 3.11+
71
+ return
72
+ try:
73
+ eps = entry_points(group=_ENTRY_POINT_GROUP)
74
+ except TypeError: # pragma: no cover - very old importlib.metadata
75
+ eps = entry_points().get(_ENTRY_POINT_GROUP, []) # type: ignore[attr-defined]
76
+ for ep in eps:
77
+ try:
78
+ factory = ep.load()
79
+ backend = factory() if isinstance(factory, type) else factory
80
+ if isinstance(backend, QuantizationBackend):
81
+ self.register(backend, replace=True)
82
+ log.info("loaded quantization plugin: %s", ep.name)
83
+ except Exception as exc: # pragma: no cover - plugin defensive
84
+ log.warning("failed to load quantization plugin '%s': %s", ep.name, exc)
85
+
86
+
87
+ _DEFAULT: BackendRegistry | None = None
88
+
89
+
90
+ def default_registry() -> BackendRegistry:
91
+ """Return the process-wide registry, populated with built-ins + plugins."""
92
+ global _DEFAULT
93
+ if _DEFAULT is None:
94
+ from zeroquantz.quantization.backends import BitsAndBytesBackend, TorchAOBackend
95
+
96
+ registry = BackendRegistry()
97
+ registry.register(BitsAndBytesBackend())
98
+ registry.register(TorchAOBackend())
99
+ registry.load_plugins()
100
+ _DEFAULT = registry
101
+ return _DEFAULT
zeroquantz/render.py ADDED
@@ -0,0 +1,341 @@
1
+ """Shared Rich renderers.
2
+
3
+ Both the CLI and the TUI turn a :class:`~zeroquantz.agent.dispatcher.CommandResult`
4
+ into Rich renderables through this module, so a ``/recommend`` looks the same
5
+ whichever surface you run it from. Kept UI-agnostic: functions return Rich
6
+ renderables and never print.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from rich.box import ROUNDED, SIMPLE
14
+ from rich.console import Group, RenderableType
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from rich.text import Text
18
+
19
+ from zeroquantz.hardware.capabilities import PRECISIONS, PrecisionStatus
20
+
21
+ if TYPE_CHECKING:
22
+ from zeroquantz.agent.dispatcher import CommandResult
23
+
24
+ # ---- palette (OpenCode-style: cyan/green accents, muted secondaries) --------
25
+ ACCENT = "cyan"
26
+ OK = "green"
27
+ WARN = "yellow"
28
+ ERR = "red"
29
+ MUTED = "grey62"
30
+ HEAD = "bold cyan"
31
+
32
+
33
+ def _status_text(status: PrecisionStatus) -> Text:
34
+ style = {
35
+ PrecisionStatus.SUPPORTED: OK,
36
+ PrecisionStatus.LIMITED: WARN,
37
+ PrecisionStatus.UNSUPPORTED: MUTED,
38
+ }[status]
39
+ return Text(status.label, style=style)
40
+
41
+
42
+ def _kv_table(rows: list[tuple[str, Any]], *, key_style: str = MUTED) -> Table:
43
+ t = Table.grid(padding=(0, 2))
44
+ t.add_column(style=key_style, justify="left")
45
+ t.add_column(style="white")
46
+ for key, value in rows:
47
+ t.add_row(key, str(value))
48
+ return t
49
+
50
+
51
+ # ---- individual renderers ---------------------------------------------------
52
+
53
+
54
+ def render_model(profile) -> Panel: # noqa: ANN001
55
+ rows = profile.summary_rows()
56
+ body: list[RenderableType] = [_kv_table(rows)]
57
+ if profile.is_estimated:
58
+ body.append(Text("\nparameter count estimated from config", style=f"italic {MUTED}"))
59
+ else:
60
+ body.append(Text("\nparameter count exact (safetensors headers)", style=f"italic {MUTED}"))
61
+ return Panel(
62
+ Group(*body),
63
+ title=f"[b]{profile.model_id}[/b]",
64
+ border_style=ACCENT,
65
+ box=ROUNDED,
66
+ )
67
+
68
+
69
+ def render_hardware(hw) -> Panel: # noqa: ANN001
70
+ rows = [("GPU", hw.gpu_name)]
71
+ if hw.gpu_count > 1:
72
+ rows.append(("GPUs", f"{hw.gpu_count} devices"))
73
+ rows.append(("VRAM", f"{hw.total_vram_gb:g} GB" if hw.total_vram_gb else "n/a"))
74
+ if hw.available_vram_gb is not None:
75
+ rows.append(("VRAM free", f"{hw.available_vram_gb:g} GB"))
76
+ if hw.sm:
77
+ rows.append(("Compute", hw.sm))
78
+ rows.append(("CUDA", hw.cuda_version or ("detected" if hw.cuda_available else "not detected")))
79
+ if hw.driver_version:
80
+ rows.append(("Driver", hw.driver_version))
81
+ if hw.torch_version:
82
+ rows.append(("Torch", hw.torch_version))
83
+ if hw.system_ram_gb:
84
+ rows.append(("System RAM", f"{hw.system_ram_gb:g} GB"))
85
+
86
+ prec = Table.grid(padding=(0, 2))
87
+ prec.add_column(style=MUTED)
88
+ prec.add_column()
89
+ for p in PRECISIONS:
90
+ prec.add_row(p.upper(), _status_text(hw.status(p)))
91
+
92
+ grid = Table.grid(padding=(0, 4))
93
+ grid.add_column()
94
+ grid.add_column()
95
+ grid.add_row(
96
+ Group(Text("Hardware", style=HEAD), _kv_table(rows)),
97
+ Group(Text("Supported", style=HEAD), prec),
98
+ )
99
+ body: list[RenderableType] = [grid]
100
+ for note in hw.notes:
101
+ body.append(Text(note, style=f"italic {MUTED}"))
102
+ return Panel(Group(*body), border_style=ACCENT, box=ROUNDED)
103
+
104
+
105
+ def render_goal(goal) -> Panel: # noqa: ANN001
106
+ return Panel(
107
+ _kv_table(goal.describe()),
108
+ title="[b]Optimization goal[/b]",
109
+ border_style=ACCENT,
110
+ box=ROUNDED,
111
+ )
112
+
113
+
114
+ def _risk_style(label: str) -> str:
115
+ return {
116
+ "very low": OK,
117
+ "low": OK,
118
+ "moderate": WARN,
119
+ "high": "red",
120
+ "very high": "bold red",
121
+ }.get(label, "white")
122
+
123
+
124
+ def render_candidates(payload: dict) -> RenderableType:
125
+ ranked = payload["ranked"]
126
+ best = payload.get("best")
127
+ compare = payload.get("compare", False)
128
+ pareto_ids = payload.get("pareto_ids", set())
129
+ best_id = best.candidate.format_id if best else None
130
+
131
+ table = Table(box=SIMPLE, expand=False, header_style=HEAD, pad_edge=False)
132
+ table.add_column("#", justify="right", style=MUTED)
133
+ if compare:
134
+ table.add_column("★", justify="center")
135
+ table.add_column("Strategy", no_wrap=True)
136
+ table.add_column("bpw", justify="right", no_wrap=True)
137
+ table.add_column("Size", justify="right", no_wrap=True)
138
+ table.add_column("VRAM", justify="right", no_wrap=True)
139
+ table.add_column("Fits", justify="center", no_wrap=True)
140
+ table.add_column("Max ctx", justify="right", no_wrap=True)
141
+ table.add_column("Quality", no_wrap=True)
142
+ table.add_column("Speed", justify="right", no_wrap=True)
143
+ table.add_column("Exec", no_wrap=True)
144
+ table.add_column("Notes", style=MUTED, max_width=22, overflow="ellipsis", no_wrap=True)
145
+
146
+ exec_label = {"in_env": "in-env", "isolated": "sub-env", "external": "produce", "planned": "roadmap"}
147
+ for rec in ranked:
148
+ c = rec.candidate
149
+ is_best = c.format_id == best_id and not compare
150
+ prefix = "» " if is_best else " "
151
+ name = Text(prefix + c.label, style=f"bold {OK}" if is_best else "white")
152
+ vram = Text(f"{c.estimated_vram_gb:.1f}", style="white" if c.fits_memory else ERR)
153
+ fits = Text("✓", style=OK) if c.fits_memory else Text("✗", style=ERR)
154
+ maxctx = f"{c.max_context/1000:.1f}k" if c.max_context else "—"
155
+ risk = Text(c.quality_risk_label, style=_risk_style(c.quality_risk_label))
156
+ row = [str(rec.rank)]
157
+ if compare:
158
+ row.append("★" if id(c) in pareto_ids else "")
159
+ row += [
160
+ name,
161
+ f"{c.bits_per_weight:.2f}",
162
+ f"{c.estimated_size_gb:.1f}",
163
+ vram,
164
+ fits,
165
+ maxctx,
166
+ risk,
167
+ f"{c.estimated_speedup:.1f}x",
168
+ exec_label.get(c.execution, c.execution),
169
+ "; ".join(c.notes) if c.notes else "",
170
+ ]
171
+ table.add_row(*row)
172
+
173
+ parts: list[RenderableType] = [table]
174
+ if best is not None and not compare:
175
+ rec_body = Group(
176
+ Text(best.candidate.label, style=f"bold {OK}"),
177
+ Text(best.reason, style=MUTED),
178
+ )
179
+ parts.append(Panel(rec_body, title="[b]Recommended[/b]", border_style=OK, box=ROUNDED))
180
+ parts.append(Text("estimates, not measurements — run /benchmark to measure", style=f"italic {MUTED}"))
181
+ return Group(*parts)
182
+
183
+
184
+ def render_plan(plan) -> Panel: # noqa: ANN001
185
+ table = Table(box=SIMPLE, header_style=HEAD)
186
+ table.add_column("Modules")
187
+ table.add_column("Precision", style=ACCENT)
188
+ for name, prec in plan.grouped_rows():
189
+ table.add_row(name, prec)
190
+
191
+ fit_style = OK if plan.fits_budget else WARN
192
+ summary = _kv_table(
193
+ [
194
+ ("Estimated size", f"{plan.estimated_size_gb:.2f} GB"),
195
+ ("Estimated VRAM", f"{plan.estimated_vram_gb:.2f} GB"),
196
+ ("Budget", f"{plan.budget_gb:g} GB" if plan.budget_gb else "n/a"),
197
+ ("Fits budget", "yes" if plan.fits_budget else "no"),
198
+ ]
199
+ )
200
+ body: list[RenderableType] = [table, Text(""), summary]
201
+ for note in plan.notes:
202
+ body.append(Text(note, style=f"italic {WARN}"))
203
+ return Panel(
204
+ Group(*body),
205
+ title=f"[b]Mixed-precision plan · {plan.low_precision}/{plan.high_precision}[/b]",
206
+ border_style=fit_style,
207
+ box=ROUNDED,
208
+ )
209
+
210
+
211
+ def render_profile(profile) -> Panel: # noqa: ANN001
212
+ table = Table(box=SIMPLE, header_style=HEAD)
213
+ table.add_column("Layer")
214
+ table.add_column("Sensitivity", justify="right")
215
+ table.add_column("", style=MUTED)
216
+ for ls in profile.top(12):
217
+ bar = "█" * max(1, round(ls.score * 12))
218
+ table.add_row(ls.layer, f"{ls.score:.2f}", bar)
219
+ return Panel(
220
+ table,
221
+ title=f"[b]Layer sensitivity · {profile.metric}[/b] (top 12)",
222
+ border_style=ACCENT,
223
+ box=ROUNDED,
224
+ )
225
+
226
+
227
+ def render_benchmark(bench) -> Panel: # noqa: ANN001
228
+ body: list[RenderableType] = [_kv_table(bench.summary_rows())]
229
+ for note in bench.notes:
230
+ body.append(Text(note, style=f"italic {WARN}"))
231
+ return Panel(
232
+ Group(*body),
233
+ title=f"[b]Benchmark · {bench.model_ref}[/b] (measured)",
234
+ border_style=ACCENT,
235
+ box=ROUNDED,
236
+ )
237
+
238
+
239
+ def render_verification(vr) -> Panel: # noqa: ANN001
240
+ status_style = {"PASS": OK, "FAIL": ERR, "INCONCLUSIVE": WARN}.get(vr.status, "white")
241
+ rows = vr.summary_rows()
242
+ table = _kv_table(rows[:-1])
243
+ body: list[RenderableType] = [
244
+ table,
245
+ Text(""),
246
+ Text(f"Status: {vr.status}", style=f"bold {status_style}"),
247
+ ]
248
+ if vr.reasons:
249
+ body.append(Text(" " + "; ".join(vr.reasons), style=MUTED))
250
+ return Panel(Group(*body), title="[b]Verification[/b]", border_style=status_style, box=ROUNDED)
251
+
252
+
253
+ def render_history(view: list[tuple[int, str, bool]]) -> Panel:
254
+ table = Table.grid(padding=(0, 2))
255
+ table.add_column(justify="right", style=MUTED)
256
+ table.add_column()
257
+ table.add_column()
258
+ for idx, label, current in view:
259
+ marker = Text("→" if current else " ", style=OK)
260
+ style = "bold white" if current else MUTED
261
+ table.add_row(f"v{idx}", marker, Text(label, style=style))
262
+ return Panel(table, title="[b]History[/b]", border_style=ACCENT, box=ROUNDED)
263
+
264
+
265
+ def render_sessions(names: list[str]) -> Panel:
266
+ if not names:
267
+ body: RenderableType = Text("No saved sessions yet. Use /save to create one.", style=MUTED)
268
+ else:
269
+ t = Table.grid(padding=(0, 1))
270
+ t.add_column(style=MUTED)
271
+ t.add_column(style="white")
272
+ for n in names:
273
+ t.add_row("•", n)
274
+ body = t
275
+ return Panel(body, title="[b]Sessions[/b]", border_style=ACCENT, box=ROUNDED)
276
+
277
+
278
+ def render_help(commands: tuple[tuple[str, str], ...]) -> Panel:
279
+ table = Table.grid(padding=(0, 3))
280
+ table.add_column(style=ACCENT, justify="left")
281
+ table.add_column(style=MUTED)
282
+ for cmd, desc in commands:
283
+ table.add_row(cmd, desc)
284
+ footer = Text(
285
+ "\nOr just type naturally: “fit Qwen/Qwen3-8B under 8GB for vLLM, quality first”.",
286
+ style=f"italic {MUTED}",
287
+ )
288
+ return Panel(Group(table, footer), title="[b]ZeroQuantz[/b]", border_style=ACCENT, box=ROUNDED)
289
+
290
+
291
+ def render_error(message: str) -> Panel:
292
+ return Panel(Text(message, style="white"), title="[b red]Error[/b red]", border_style=ERR, box=ROUNDED)
293
+
294
+
295
+ def render_message(message: str) -> Text:
296
+ return Text(message, style="white")
297
+
298
+
299
+ def render_quant(result) -> Panel: # noqa: ANN001
300
+ rows = [
301
+ ("Backend", result.backend),
302
+ ("Method", result.method),
303
+ ("Elapsed", f"{result.elapsed_s:.1f} s"),
304
+ ]
305
+ if result.quantized_size_gb is not None:
306
+ rows.append(("Quantized size", f"{result.quantized_size_gb:.2f} GB"))
307
+ if result.output_dir:
308
+ rows.append(("Output", result.output_dir))
309
+ return Panel(_kv_table(rows), title="[b]Quantization[/b]", border_style=OK, box=ROUNDED)
310
+
311
+
312
+ # ---- top-level dispatch -----------------------------------------------------
313
+
314
+ _RENDERERS = {
315
+ "model": lambda r: render_model(r.payload),
316
+ "hardware": lambda r: render_hardware(r.payload),
317
+ "goal": lambda r: render_goal(r.payload),
318
+ "candidates": lambda r: render_candidates(r.payload),
319
+ "plan": lambda r: render_plan(r.payload),
320
+ "profile": lambda r: render_profile(r.payload),
321
+ "benchmark": lambda r: render_benchmark(r.payload),
322
+ "verification": lambda r: render_verification(r.payload),
323
+ "history": lambda r: render_history(r.payload),
324
+ "sessions": lambda r: render_sessions(r.payload),
325
+ "help": lambda r: render_help(r.payload),
326
+ "quant": lambda r: render_quant(r.payload),
327
+ "error": lambda r: render_error(r.message),
328
+ "message": lambda r: render_message(r.message),
329
+ "exit": lambda r: render_message(r.message),
330
+ }
331
+
332
+
333
+ def render_result(result: CommandResult) -> RenderableType:
334
+ """Turn any :class:`CommandResult` into a Rich renderable."""
335
+ renderer = _RENDERERS.get(result.kind)
336
+ if renderer is None:
337
+ return render_message(result.message)
338
+ try:
339
+ return renderer(result)
340
+ except Exception: # pragma: no cover - never let rendering crash the UI
341
+ return render_message(result.message)
@@ -0,0 +1,18 @@
1
+ """Runtime adapters and the centralized runtime-compatibility matrix."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.runtimes.compatibility import (
6
+ RuntimeCompat,
7
+ compatibility,
8
+ known_runtimes,
9
+ )
10
+ from zeroquantz.runtimes.registry import RuntimeRegistry, default_runtime_registry
11
+
12
+ __all__ = [
13
+ "RuntimeCompat",
14
+ "RuntimeRegistry",
15
+ "compatibility",
16
+ "default_runtime_registry",
17
+ "known_runtimes",
18
+ ]
@@ -0,0 +1,64 @@
1
+ """Runtime adapter protocol.
2
+
3
+ A runtime adapter knows how a deployment target (transformers, vLLM, ...) relates
4
+ to quantized models: which schemes it can serve, whether a given model directory
5
+ looks loadable, and what command/config would run it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import TYPE_CHECKING
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+ from zeroquantz.runtimes.compatibility import RuntimeCompat, compatibility
16
+
17
+ if TYPE_CHECKING:
18
+ from zeroquantz.quantization.config import QuantizationConfig
19
+
20
+
21
+ class ValidationResult(BaseModel):
22
+ """Result of checking whether a model can run on a runtime."""
23
+
24
+ runtime: str
25
+ ok: bool
26
+ compat: RuntimeCompat = RuntimeCompat.UNKNOWN
27
+ messages: list[str] = Field(default_factory=list)
28
+ warnings: list[str] = Field(default_factory=list)
29
+ serve_command: str | None = None
30
+
31
+
32
+ class RuntimeBackend(ABC):
33
+ """Abstract base for runtime adapters."""
34
+
35
+ name: str = "base"
36
+ description: str = ""
37
+
38
+ def compatibility(self, method: str) -> RuntimeCompat:
39
+ return compatibility(method, self.name)
40
+
41
+ def supports_quantization(self, config: QuantizationConfig) -> bool:
42
+ """True if this runtime can serve a model quantized with ``config``."""
43
+ return bool(self.compatibility(config.method))
44
+
45
+ @abstractmethod
46
+ def validate_model(
47
+ self, path: str, config: QuantizationConfig | None = None
48
+ ) -> ValidationResult:
49
+ """Check whether ``path`` looks runnable on this runtime."""
50
+
51
+ def serve_command(
52
+ self, path: str, config: QuantizationConfig | None = None
53
+ ) -> str | None:
54
+ """A shell command that would serve the model, or None if not applicable."""
55
+ return None
56
+
57
+ def benchmark(self, path: str, config: QuantizationConfig | None = None): # noqa: ANN201
58
+ """Optional runtime-native benchmark. Not implemented for every runtime."""
59
+ raise NotImplementedError(
60
+ f"{self.name} does not implement a native benchmark yet."
61
+ )
62
+
63
+ def describe(self) -> dict:
64
+ return {"name": self.name, "description": self.description}
@@ -0,0 +1,91 @@
1
+ """The single source of truth for "does quantization method X work on runtime Y".
2
+
3
+ Keeping this as one data table (rather than scattering ``if runtime == "vllm"``
4
+ checks across the codebase) is an explicit design requirement: adding a runtime
5
+ or a method means editing one matrix.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+
12
+
13
+ class RuntimeCompat(str, Enum):
14
+ COMPATIBLE = "compatible"
15
+ LIMITED = "limited"
16
+ NOT_RECOMMENDED = "not_recommended"
17
+ UNSUPPORTED = "unsupported"
18
+ UNKNOWN = "unknown"
19
+
20
+ @property
21
+ def label(self) -> str:
22
+ return {
23
+ "compatible": "compatible",
24
+ "limited": "limited",
25
+ "not_recommended": "not recommended",
26
+ "unsupported": "unsupported",
27
+ "unknown": "unknown",
28
+ }[self.value]
29
+
30
+ @property
31
+ def rank(self) -> int:
32
+ """Higher is better — used when scoring candidates for a runtime."""
33
+ return {
34
+ "compatible": 3,
35
+ "limited": 2,
36
+ "not_recommended": 1,
37
+ "unknown": 1,
38
+ "unsupported": 0,
39
+ }[self.value]
40
+
41
+ def __bool__(self) -> bool:
42
+ return self not in (RuntimeCompat.UNSUPPORTED,)
43
+
44
+
45
+ _C = RuntimeCompat.COMPATIBLE
46
+ _L = RuntimeCompat.LIMITED
47
+ _N = RuntimeCompat.NOT_RECOMMENDED
48
+ _U = RuntimeCompat.UNSUPPORTED
49
+
50
+ # runtime -> method -> compatibility
51
+ _MATRIX: dict[str, dict[str, RuntimeCompat]] = {
52
+ "transformers": {
53
+ "nf4": _C,
54
+ "int8_bnb": _C,
55
+ "int8_torchao": _C,
56
+ "int4_torchao": _C,
57
+ "awq": _C,
58
+ "gptq": _C,
59
+ "fp8": _L,
60
+ "mixed": _C,
61
+ },
62
+ "vllm": {
63
+ "awq": _C,
64
+ "gptq": _C,
65
+ "fp8": _C,
66
+ "int8_torchao": _L,
67
+ "int4_torchao": _L,
68
+ "nf4": _N, # vLLM can load bitsandbytes but it is slow / limited
69
+ "int8_bnb": _N,
70
+ "mixed": _L,
71
+ },
72
+ }
73
+
74
+
75
+ def known_runtimes() -> list[str]:
76
+ return list(_MATRIX)
77
+
78
+
79
+ def compatibility(method: str, runtime: str | None) -> RuntimeCompat:
80
+ """Compatibility of ``method`` with ``runtime``.
81
+
82
+ * ``runtime is None`` -> no runtime constraint -> COMPATIBLE.
83
+ * unknown runtime -> UNKNOWN (do not penalize; we simply don't know).
84
+ * known runtime -> the matrix entry, defaulting to UNSUPPORTED.
85
+ """
86
+ if runtime is None:
87
+ return RuntimeCompat.COMPATIBLE
88
+ table = _MATRIX.get(runtime.strip().lower())
89
+ if table is None:
90
+ return RuntimeCompat.UNKNOWN
91
+ return table.get(method, RuntimeCompat.UNSUPPORTED)
@@ -0,0 +1,70 @@
1
+ """Runtime adapter registry (mirrors the quantization backend registry)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.core.exceptions import ZeroQuantzError
6
+ from zeroquantz.runtimes.base import RuntimeBackend
7
+ from zeroquantz.utils.logging import get_logger
8
+
9
+ log = get_logger(__name__)
10
+
11
+ _ENTRY_POINT_GROUP = "zeroquantz.runtimes"
12
+
13
+
14
+ class RuntimeRegistry:
15
+ def __init__(self) -> None:
16
+ self._runtimes: dict[str, RuntimeBackend] = {}
17
+
18
+ def register(self, runtime: RuntimeBackend, *, replace: bool = False) -> None:
19
+ if runtime.name in self._runtimes and not replace:
20
+ raise ValueError(f"runtime '{runtime.name}' already registered")
21
+ self._runtimes[runtime.name] = runtime
22
+
23
+ def names(self) -> list[str]:
24
+ return list(self._runtimes)
25
+
26
+ def all_runtimes(self) -> list[RuntimeBackend]:
27
+ return list(self._runtimes.values())
28
+
29
+ def get(self, name: str) -> RuntimeBackend:
30
+ try:
31
+ return self._runtimes[name.strip().lower()]
32
+ except KeyError as exc:
33
+ known = ", ".join(sorted(self._runtimes)) or "none"
34
+ raise ZeroQuantzError(
35
+ f"Unknown runtime '{name}'.",
36
+ detail=f"Known runtimes: {known}.",
37
+ ) from exc
38
+
39
+ def load_plugins(self) -> None:
40
+ from importlib.metadata import entry_points
41
+
42
+ try:
43
+ eps = entry_points(group=_ENTRY_POINT_GROUP)
44
+ except TypeError: # pragma: no cover
45
+ eps = entry_points().get(_ENTRY_POINT_GROUP, []) # type: ignore[attr-defined]
46
+ for ep in eps:
47
+ try:
48
+ factory = ep.load()
49
+ runtime = factory() if isinstance(factory, type) else factory
50
+ if isinstance(runtime, RuntimeBackend):
51
+ self.register(runtime, replace=True)
52
+ except Exception as exc: # pragma: no cover
53
+ log.warning("failed to load runtime plugin '%s': %s", ep.name, exc)
54
+
55
+
56
+ _DEFAULT: RuntimeRegistry | None = None
57
+
58
+
59
+ def default_runtime_registry() -> RuntimeRegistry:
60
+ global _DEFAULT
61
+ if _DEFAULT is None:
62
+ from zeroquantz.runtimes.transformers import TransformersRuntime
63
+ from zeroquantz.runtimes.vllm import VLLMRuntime
64
+
65
+ registry = RuntimeRegistry()
66
+ registry.register(TransformersRuntime())
67
+ registry.register(VLLMRuntime())
68
+ registry.load_plugins()
69
+ _DEFAULT = registry
70
+ return _DEFAULT