zeroquantz 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zeroquantz/__init__.py +14 -0
- zeroquantz/__main__.py +8 -0
- zeroquantz/agent/__init__.py +16 -0
- zeroquantz/agent/dispatcher.py +520 -0
- zeroquantz/agent/intents.py +46 -0
- zeroquantz/agent/parser.py +255 -0
- zeroquantz/benchmark/__init__.py +7 -0
- zeroquantz/benchmark/latency.py +66 -0
- zeroquantz/benchmark/memory.py +41 -0
- zeroquantz/benchmark/quality.py +38 -0
- zeroquantz/benchmark/runner.py +151 -0
- zeroquantz/cli/__init__.py +7 -0
- zeroquantz/cli/app.py +98 -0
- zeroquantz/cli/commands.py +459 -0
- zeroquantz/cli/interactive.py +56 -0
- zeroquantz/core/__init__.py +7 -0
- zeroquantz/core/artifacts.py +179 -0
- zeroquantz/core/context.py +127 -0
- zeroquantz/core/events.py +30 -0
- zeroquantz/core/exceptions.py +105 -0
- zeroquantz/core/session.py +202 -0
- zeroquantz/core/subenv.py +202 -0
- zeroquantz/deploy/__init__.py +25 -0
- zeroquantz/deploy/assets.py +161 -0
- zeroquantz/deploy/launcher.py +80 -0
- zeroquantz/deploy/runtime_env.py +66 -0
- zeroquantz/deploy/targets.py +154 -0
- zeroquantz/export/__init__.py +8 -0
- zeroquantz/export/exporter.py +68 -0
- zeroquantz/export/report.py +203 -0
- zeroquantz/hardware/__init__.py +15 -0
- zeroquantz/hardware/capabilities.py +152 -0
- zeroquantz/hardware/detector.py +200 -0
- zeroquantz/hardware/gpu.py +31 -0
- zeroquantz/models/__init__.py +8 -0
- zeroquantz/models/architecture.py +168 -0
- zeroquantz/models/downloader.py +161 -0
- zeroquantz/models/hf_auth.py +105 -0
- zeroquantz/models/inspector.py +249 -0
- zeroquantz/models/metadata.py +108 -0
- zeroquantz/models/search.py +71 -0
- zeroquantz/optimization/__init__.py +22 -0
- zeroquantz/optimization/candidate.py +272 -0
- zeroquantz/optimization/constraints.py +70 -0
- zeroquantz/optimization/fit.py +203 -0
- zeroquantz/optimization/pareto.py +66 -0
- zeroquantz/optimization/planner.py +297 -0
- zeroquantz/optimization/recommender.py +149 -0
- zeroquantz/profiling/__init__.py +18 -0
- zeroquantz/profiling/calibration.py +74 -0
- zeroquantz/profiling/sensitivity.py +234 -0
- zeroquantz/quantization/__init__.py +17 -0
- zeroquantz/quantization/backends/__init__.py +8 -0
- zeroquantz/quantization/backends/bitsandbytes.py +210 -0
- zeroquantz/quantization/backends/torchao.py +198 -0
- zeroquantz/quantization/base.py +136 -0
- zeroquantz/quantization/catalog.py +321 -0
- zeroquantz/quantization/config.py +106 -0
- zeroquantz/quantization/gguf_pipeline.py +210 -0
- zeroquantz/quantization/isolated.py +248 -0
- zeroquantz/quantization/memory.py +133 -0
- zeroquantz/quantization/native.py +91 -0
- zeroquantz/quantization/registry.py +101 -0
- zeroquantz/render.py +341 -0
- zeroquantz/runtimes/__init__.py +18 -0
- zeroquantz/runtimes/base.py +64 -0
- zeroquantz/runtimes/compatibility.py +91 -0
- zeroquantz/runtimes/registry.py +70 -0
- zeroquantz/runtimes/transformers.py +53 -0
- zeroquantz/runtimes/vllm.py +83 -0
- zeroquantz/tui/__init__.py +13 -0
- zeroquantz/tui/app.py +77 -0
- zeroquantz/tui/banner.py +47 -0
- zeroquantz/tui/screens/__init__.py +25 -0
- zeroquantz/tui/screens/confirm.py +41 -0
- zeroquantz/tui/screens/execute.py +194 -0
- zeroquantz/tui/screens/model_select.py +206 -0
- zeroquantz/tui/screens/plan.py +177 -0
- zeroquantz/tui/screens/quantize_select.py +272 -0
- zeroquantz/tui/screens/settings.py +219 -0
- zeroquantz/tui/screens/token.py +94 -0
- zeroquantz/tui/screens/welcome.py +128 -0
- zeroquantz/tui/screens/workspace.py +175 -0
- zeroquantz/tui/styles/app.tcss +424 -0
- zeroquantz/tui/widgets/__init__.py +9 -0
- zeroquantz/tui/widgets/chip.py +36 -0
- zeroquantz/tui/widgets/sidebar.py +107 -0
- zeroquantz/tui/widgets/status_bar.py +43 -0
- zeroquantz/utils/__init__.py +8 -0
- zeroquantz/utils/config.py +46 -0
- zeroquantz/utils/env.py +78 -0
- zeroquantz/utils/logging.py +73 -0
- zeroquantz/utils/metrics.py +98 -0
- zeroquantz/utils/paths.py +57 -0
- zeroquantz/utils/units.py +134 -0
- zeroquantz/verification/__init__.py +17 -0
- zeroquantz/verification/logits.py +55 -0
- zeroquantz/verification/report.py +186 -0
- zeroquantz/verification/weights.py +44 -0
- zeroquantz/version.py +8 -0
- zeroquantz-0.1.0.dist-info/METADATA +72 -0
- zeroquantz-0.1.0.dist-info/RECORD +105 -0
- zeroquantz-0.1.0.dist-info/WHEEL +4 -0
- zeroquantz-0.1.0.dist-info/entry_points.txt +2 -0
- zeroquantz-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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
|