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,53 @@
|
|
|
1
|
+
"""transformers runtime adapter — the reference/default deployment target."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from zeroquantz.runtimes.base import RuntimeBackend, ValidationResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from zeroquantz.quantization.config import QuantizationConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TransformersRuntime(RuntimeBackend):
|
|
15
|
+
name = "transformers"
|
|
16
|
+
description = "Hugging Face Transformers (AutoModelForCausalLM)."
|
|
17
|
+
|
|
18
|
+
def validate_model(
|
|
19
|
+
self, path: str, config: QuantizationConfig | None = None
|
|
20
|
+
) -> ValidationResult:
|
|
21
|
+
p = Path(path)
|
|
22
|
+
messages: list[str] = []
|
|
23
|
+
warnings: list[str] = []
|
|
24
|
+
ok = True
|
|
25
|
+
|
|
26
|
+
if p.exists() and p.is_dir():
|
|
27
|
+
if not (p / "config.json").exists():
|
|
28
|
+
ok = False
|
|
29
|
+
messages.append("No config.json found in the model directory.")
|
|
30
|
+
else:
|
|
31
|
+
messages.append(f"Treating '{path}' as a Hub id (not validated offline).")
|
|
32
|
+
|
|
33
|
+
compat = self.compatibility(config.method) if config else None
|
|
34
|
+
if config and compat is not None:
|
|
35
|
+
messages.append(f"{config.method}: {compat.label} with transformers.")
|
|
36
|
+
|
|
37
|
+
return ValidationResult(
|
|
38
|
+
runtime=self.name,
|
|
39
|
+
ok=ok,
|
|
40
|
+
compat=compat or ValidationResult.model_fields["compat"].default,
|
|
41
|
+
messages=messages,
|
|
42
|
+
warnings=warnings,
|
|
43
|
+
serve_command=self.serve_command(path, config),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def serve_command(
|
|
47
|
+
self, path: str, config: QuantizationConfig | None = None
|
|
48
|
+
) -> str | None:
|
|
49
|
+
# transformers is a library, not a server; show the load snippet.
|
|
50
|
+
return (
|
|
51
|
+
"python -c \"from transformers import AutoModelForCausalLM; "
|
|
52
|
+
f"AutoModelForCausalLM.from_pretrained('{path}', device_map='auto')\""
|
|
53
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""vLLM runtime adapter.
|
|
2
|
+
|
|
3
|
+
For v0.1 this focuses on *compatibility validation and command generation* rather
|
|
4
|
+
than embedding vLLM internals: it tells you whether a quantized model will serve
|
|
5
|
+
well on vLLM and prints the ``vllm serve`` command to run it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from zeroquantz.runtimes.base import RuntimeBackend, ValidationResult
|
|
14
|
+
from zeroquantz.runtimes.compatibility import RuntimeCompat
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from zeroquantz.quantization.config import QuantizationConfig
|
|
18
|
+
|
|
19
|
+
# ZeroQuantz method -> vLLM --quantization argument.
|
|
20
|
+
_VLLM_QUANT_ARG = {
|
|
21
|
+
"awq": "awq",
|
|
22
|
+
"gptq": "gptq",
|
|
23
|
+
"fp8": "fp8",
|
|
24
|
+
"nf4": "bitsandbytes",
|
|
25
|
+
"int8_bnb": "bitsandbytes",
|
|
26
|
+
"int8_torchao": "torchao",
|
|
27
|
+
"int4_torchao": "torchao",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VLLMRuntime(RuntimeBackend):
|
|
32
|
+
name = "vllm"
|
|
33
|
+
description = "vLLM high-throughput inference server."
|
|
34
|
+
|
|
35
|
+
def validate_model(
|
|
36
|
+
self, path: str, config: QuantizationConfig | None = None
|
|
37
|
+
) -> ValidationResult:
|
|
38
|
+
p = Path(path)
|
|
39
|
+
messages: list[str] = []
|
|
40
|
+
warnings: list[str] = []
|
|
41
|
+
ok = True
|
|
42
|
+
|
|
43
|
+
if p.exists() and p.is_dir() and not (p / "config.json").exists():
|
|
44
|
+
ok = False
|
|
45
|
+
messages.append("No config.json found in the model directory.")
|
|
46
|
+
|
|
47
|
+
compat = RuntimeCompat.UNKNOWN
|
|
48
|
+
if config is not None:
|
|
49
|
+
compat = self.compatibility(config.method)
|
|
50
|
+
messages.append(f"{config.method}: {compat.label} on vLLM.")
|
|
51
|
+
if compat is RuntimeCompat.UNSUPPORTED:
|
|
52
|
+
ok = False
|
|
53
|
+
messages.append(
|
|
54
|
+
f"vLLM cannot serve '{config.method}'. Prefer AWQ, GPTQ, or FP8."
|
|
55
|
+
)
|
|
56
|
+
elif compat is RuntimeCompat.NOT_RECOMMENDED:
|
|
57
|
+
warnings.append(
|
|
58
|
+
f"'{config.method}' loads on vLLM but is slow; AWQ/GPTQ are preferred."
|
|
59
|
+
)
|
|
60
|
+
elif compat is RuntimeCompat.LIMITED:
|
|
61
|
+
warnings.append(
|
|
62
|
+
f"'{config.method}' support on vLLM is limited/experimental."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return ValidationResult(
|
|
66
|
+
runtime=self.name,
|
|
67
|
+
ok=ok,
|
|
68
|
+
compat=compat,
|
|
69
|
+
messages=messages,
|
|
70
|
+
warnings=warnings,
|
|
71
|
+
serve_command=self.serve_command(path, config),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def serve_command(
|
|
75
|
+
self, path: str, config: QuantizationConfig | None = None
|
|
76
|
+
) -> str | None:
|
|
77
|
+
parts = ["vllm", "serve", path]
|
|
78
|
+
if config is not None:
|
|
79
|
+
arg = _VLLM_QUANT_ARG.get(config.method)
|
|
80
|
+
if arg is not None:
|
|
81
|
+
parts.append(f"--quantization {arg}")
|
|
82
|
+
parts.append("--max-model-len 4096")
|
|
83
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""The Textual interactive terminal UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["ZeroQuantzApp", "run_tui"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name: str): # lazy so importing the package never imports textual
|
|
9
|
+
if name in ("ZeroQuantzApp", "run_tui"):
|
|
10
|
+
from zeroquantz.tui.app import ZeroQuantzApp, run_tui
|
|
11
|
+
|
|
12
|
+
return {"ZeroQuantzApp": ZeroQuantzApp, "run_tui": run_tui}[name]
|
|
13
|
+
raise AttributeError(name)
|
zeroquantz/tui/app.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""The ZeroQuantz Textual application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import App
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
|
|
8
|
+
from zeroquantz.agent.dispatcher import Dispatcher
|
|
9
|
+
from zeroquantz.core.context import AppContext
|
|
10
|
+
from zeroquantz.tui.screens.execute import ExecuteScreen
|
|
11
|
+
from zeroquantz.tui.screens.model_select import ModelSelectScreen
|
|
12
|
+
from zeroquantz.tui.screens.plan import PlanScreen
|
|
13
|
+
from zeroquantz.tui.screens.quantize_select import QuantizeSelectScreen
|
|
14
|
+
from zeroquantz.tui.screens.settings import SettingsScreen
|
|
15
|
+
from zeroquantz.tui.screens.welcome import WelcomeScreen
|
|
16
|
+
from zeroquantz.tui.screens.workspace import WorkspaceScreen
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ZeroQuantzApp(App):
|
|
20
|
+
"""Interactive TUI: welcome → (guided) model → quantization → plan → execute."""
|
|
21
|
+
|
|
22
|
+
CSS_PATH = "styles/app.tcss"
|
|
23
|
+
TITLE = "ZeroQuantz"
|
|
24
|
+
BINDINGS = [Binding("ctrl+q", "quit", "Quit", show=True)]
|
|
25
|
+
|
|
26
|
+
def __init__(self, ctx: AppContext | None = None) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.ctx = ctx or AppContext.create()
|
|
29
|
+
self.dispatcher = Dispatcher()
|
|
30
|
+
self.pending_command: str | None = None
|
|
31
|
+
self.selected_candidate = None
|
|
32
|
+
self.quant_fits_only = False
|
|
33
|
+
self.deploy_target: str | None = None
|
|
34
|
+
|
|
35
|
+
def on_mount(self) -> None:
|
|
36
|
+
self.push_screen(WelcomeScreen())
|
|
37
|
+
|
|
38
|
+
# ---- guided wizard ------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def start_wizard(self) -> None:
|
|
41
|
+
self.push_screen(ModelSelectScreen())
|
|
42
|
+
|
|
43
|
+
def push_quantize_select(self) -> None:
|
|
44
|
+
self.push_screen(QuantizeSelectScreen())
|
|
45
|
+
|
|
46
|
+
def push_plan(self, candidate) -> None: # noqa: ANN001
|
|
47
|
+
self.selected_candidate = candidate
|
|
48
|
+
self.push_screen(PlanScreen())
|
|
49
|
+
|
|
50
|
+
def push_execute(self) -> None:
|
|
51
|
+
self.push_screen(ExecuteScreen())
|
|
52
|
+
|
|
53
|
+
def go_home(self) -> None:
|
|
54
|
+
while len(self.screen_stack) > 1:
|
|
55
|
+
self.pop_screen()
|
|
56
|
+
|
|
57
|
+
# ---- settings + command mode -------------------------------------------
|
|
58
|
+
|
|
59
|
+
def push_settings(self) -> None:
|
|
60
|
+
self.push_screen(SettingsScreen())
|
|
61
|
+
|
|
62
|
+
def open_workspace(self, auto_command: str | None) -> None:
|
|
63
|
+
self.pending_command = auto_command
|
|
64
|
+
self.push_screen(WorkspaceScreen())
|
|
65
|
+
|
|
66
|
+
def resume_last_session(self) -> None:
|
|
67
|
+
latest = self.ctx.repo.latest()
|
|
68
|
+
if latest is not None:
|
|
69
|
+
latest.hardware = self.ctx.hardware
|
|
70
|
+
self.ctx.session = latest
|
|
71
|
+
self.open_workspace(None)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run_tui(*, session_name: str | None = None, continue_last: bool = False) -> None:
|
|
75
|
+
"""Build the context and launch the TUI (blocks until the user exits)."""
|
|
76
|
+
ctx = AppContext.create(session_name=session_name, continue_last=continue_last)
|
|
77
|
+
ZeroQuantzApp(ctx=ctx).run()
|
zeroquantz/tui/banner.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""The ``ZEROQUANTZ`` ASCII wordmark (ANSI-Shadow style), rendered responsively.
|
|
2
|
+
|
|
3
|
+
Composed from per-letter glyphs so the strings can't drift out of alignment.
|
|
4
|
+
Three widths: single-line (wide terminals), stacked ZERO / QUANT (typical), and a
|
|
5
|
+
plain bold fallback (narrow).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
_GLYPHS: dict[str, list[str]] = {
|
|
11
|
+
"Z": ["███████╗", "╚══███╔╝", " ███╔╝ ", " ███╔╝ ", "███████╗", "╚══════╝"],
|
|
12
|
+
"E": ["███████╗", "██╔════╝", "█████╗ ", "██╔══╝ ", "███████╗", "╚══════╝"],
|
|
13
|
+
"R": ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔══██╗", "██║ ██║", "╚═╝ ╚═╝"],
|
|
14
|
+
"O": [" ██████╗ ", "██╔═══██╗", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
|
|
15
|
+
"Q": [" ██████╗ ", "██╔═══██╗", "██║ ██║", "██║▄▄ ██║", "╚██████╔╝", " ╚══▀▀═╝ "],
|
|
16
|
+
"U": ["██╗ ██╗", "██║ ██║", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
|
|
17
|
+
"A": [" █████╗ ", "██╔══██╗", "███████║", "██╔══██║", "██║ ██║", "╚═╝ ╚═╝"],
|
|
18
|
+
"N": ["███╗ ██╗", "████╗ ██║", "██╔██╗ ██║", "██║╚██╗██║", "██║ ╚████║", "╚═╝ ╚═══╝"],
|
|
19
|
+
"T": ["████████╗", "╚══██╔══╝", " ██║ ", " ██║ ", " ██║ ", " ╚═╝ "],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _compose(word: str, sep: str = " ") -> list[str]:
|
|
24
|
+
rows = ["" for _ in range(6)]
|
|
25
|
+
for ch in word:
|
|
26
|
+
glyph = _GLYPHS[ch]
|
|
27
|
+
width = max(len(r) for r in glyph)
|
|
28
|
+
for i in range(6):
|
|
29
|
+
rows[i] += glyph[i].ljust(width) + sep
|
|
30
|
+
return [r.rstrip() for r in rows]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def banner_lines(available_width: int) -> list[str]:
|
|
34
|
+
"""Return the widest banner variant that fits ``available_width``."""
|
|
35
|
+
single = _compose("ZEROQUANTZ")
|
|
36
|
+
if available_width >= max(len(r) for r in single) + 2:
|
|
37
|
+
return single
|
|
38
|
+
zero = _compose("ZERO")
|
|
39
|
+
quantz = _compose("QUANTZ")
|
|
40
|
+
stacked = zero + [""] + quantz
|
|
41
|
+
if available_width >= max(len(r) for r in stacked) + 2:
|
|
42
|
+
return stacked
|
|
43
|
+
return ["ZEROQUANTZ"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def banner_text(available_width: int) -> str:
|
|
47
|
+
return "\n".join(banner_lines(available_width))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""ZeroQuantz TUI screens."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from zeroquantz.tui.screens.confirm import ConfirmScreen
|
|
6
|
+
from zeroquantz.tui.screens.execute import ExecuteScreen
|
|
7
|
+
from zeroquantz.tui.screens.model_select import ModelSelectScreen
|
|
8
|
+
from zeroquantz.tui.screens.plan import PlanScreen
|
|
9
|
+
from zeroquantz.tui.screens.quantize_select import QuantizeSelectScreen
|
|
10
|
+
from zeroquantz.tui.screens.settings import SettingsScreen
|
|
11
|
+
from zeroquantz.tui.screens.token import TokenScreen
|
|
12
|
+
from zeroquantz.tui.screens.welcome import WelcomeScreen
|
|
13
|
+
from zeroquantz.tui.screens.workspace import WorkspaceScreen
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ConfirmScreen",
|
|
17
|
+
"ExecuteScreen",
|
|
18
|
+
"ModelSelectScreen",
|
|
19
|
+
"PlanScreen",
|
|
20
|
+
"QuantizeSelectScreen",
|
|
21
|
+
"SettingsScreen",
|
|
22
|
+
"TokenScreen",
|
|
23
|
+
"WelcomeScreen",
|
|
24
|
+
"WorkspaceScreen",
|
|
25
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""A small yes/no confirmation modal (used before destructive actions)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.screen import ModalScreen
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfirmScreen(ModalScreen[bool]):
|
|
14
|
+
BINDINGS = [
|
|
15
|
+
Binding("y", "confirm", "Yes"),
|
|
16
|
+
Binding("enter", "confirm", "Yes", show=False),
|
|
17
|
+
Binding("n", "cancel", "No"),
|
|
18
|
+
Binding("escape", "cancel", "No", show=False),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, *, detail: str | None = None) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._message = message
|
|
24
|
+
self._detail = detail
|
|
25
|
+
|
|
26
|
+
def compose(self) -> ComposeResult:
|
|
27
|
+
with Vertical(id="confirm-dialog"):
|
|
28
|
+
yield Static(Text(self._message, style="bold white"), id="confirm-message")
|
|
29
|
+
if self._detail:
|
|
30
|
+
yield Static(Text(self._detail, style="#8b949e"), classes="dialog-sub")
|
|
31
|
+
yield Static(
|
|
32
|
+
Text.assemble(("y", "#f85149"), (" confirm ", "#8b949e"),
|
|
33
|
+
("n", "#3fb950"), (" cancel", "#8b949e")),
|
|
34
|
+
classes="page-hint",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def action_confirm(self) -> None:
|
|
38
|
+
self.dismiss(True)
|
|
39
|
+
|
|
40
|
+
def action_cancel(self) -> None:
|
|
41
|
+
self.dismiss(False)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Execute screen: run the decided plan as stages — download → quantize → deploy.
|
|
2
|
+
|
|
3
|
+
Each stage streams progress/logs and shows a status glyph. Failures are surfaced
|
|
4
|
+
(with the actionable message) but don't abort the pipeline — deploy assets are
|
|
5
|
+
still generated so the user has runnable commands for their server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from textual import work
|
|
14
|
+
from textual.app import ComposeResult
|
|
15
|
+
from textual.binding import Binding
|
|
16
|
+
from textual.containers import Vertical
|
|
17
|
+
from textual.screen import Screen
|
|
18
|
+
from textual.widgets import Footer, ProgressBar, RichLog, Static
|
|
19
|
+
|
|
20
|
+
from zeroquantz.agent.intents import Intent, IntentKind
|
|
21
|
+
from zeroquantz.quantization.catalog import get_format
|
|
22
|
+
from zeroquantz.utils import units
|
|
23
|
+
|
|
24
|
+
_GLYPH = {"pending": ("○", "#6e7681"), "running": ("●", "#d29922"),
|
|
25
|
+
"done": ("✓", "#3fb950"), "failed": ("✗", "#f85149"), "manual": ("⤼", "#d29922")}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExecuteScreen(Screen):
|
|
29
|
+
BINDINGS = [Binding("escape", "back", "Back"), Binding("h", "home", "Home")]
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self._stages = [
|
|
34
|
+
{"name": "Download weights", "status": "pending"},
|
|
35
|
+
{"name": "Quantize", "status": "pending"},
|
|
36
|
+
{"name": "Deploy", "status": "pending"},
|
|
37
|
+
]
|
|
38
|
+
self._total = 0
|
|
39
|
+
self._start = 0.0
|
|
40
|
+
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
with Vertical(id="exec-page"):
|
|
43
|
+
yield Static("Executing plan", classes="page-title")
|
|
44
|
+
yield Static("", id="exec-stages")
|
|
45
|
+
yield ProgressBar(id="exec-progress", total=100, show_eta=False)
|
|
46
|
+
yield RichLog(id="exec-log", wrap=True, markup=False, highlight=False)
|
|
47
|
+
yield Footer()
|
|
48
|
+
|
|
49
|
+
def on_mount(self) -> None:
|
|
50
|
+
self._render_stages()
|
|
51
|
+
self._run()
|
|
52
|
+
|
|
53
|
+
# ---- stage rendering ----------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _render_stages(self) -> None:
|
|
56
|
+
text = Text()
|
|
57
|
+
for i, st in enumerate(self._stages):
|
|
58
|
+
glyph, color = _GLYPH[st["status"]]
|
|
59
|
+
text.append(f" {glyph} ", style=color)
|
|
60
|
+
text.append(st["name"], style="bold white" if st["status"] == "running" else "white")
|
|
61
|
+
if i < len(self._stages) - 1:
|
|
62
|
+
text.append("\n")
|
|
63
|
+
self.query_one("#exec-stages", Static).update(text)
|
|
64
|
+
|
|
65
|
+
def _set_stage(self, index: int, status: str) -> None:
|
|
66
|
+
self._stages[index]["status"] = status
|
|
67
|
+
self._render_stages()
|
|
68
|
+
|
|
69
|
+
def _log(self, message: str, style: str = "#c9d1d9") -> None:
|
|
70
|
+
self.query_one("#exec-log", RichLog).write(Text(message, style=style))
|
|
71
|
+
|
|
72
|
+
def _progress(self, done: int, total: int) -> None:
|
|
73
|
+
pb = self.query_one("#exec-progress", ProgressBar)
|
|
74
|
+
pb.update(total=max(total, 1), progress=done)
|
|
75
|
+
|
|
76
|
+
def _indeterminate(self) -> None:
|
|
77
|
+
self.query_one("#exec-progress", ProgressBar).update(total=None)
|
|
78
|
+
|
|
79
|
+
# ---- pipeline -----------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
@work(thread=True, exclusive=True)
|
|
82
|
+
def _run(self) -> None:
|
|
83
|
+
app = self.app
|
|
84
|
+
ctx = app.ctx
|
|
85
|
+
candidate = app.selected_candidate
|
|
86
|
+
fmt = get_format(candidate.format_id)
|
|
87
|
+
model_id = ctx.session.model_id or (ctx.session.model_profile.model_id if ctx.session.model_profile else "")
|
|
88
|
+
|
|
89
|
+
# ---- stage 1: download ----
|
|
90
|
+
app.call_from_thread(self._set_stage, 0, "running")
|
|
91
|
+
try:
|
|
92
|
+
local = ctx.session.model_local_path
|
|
93
|
+
from pathlib import Path as _P
|
|
94
|
+
|
|
95
|
+
if local and _P(local).exists():
|
|
96
|
+
app.call_from_thread(self._log, f"Weights already present: {local}", "#6e7681")
|
|
97
|
+
else:
|
|
98
|
+
from zeroquantz.models import downloader as dl
|
|
99
|
+
from zeroquantz.models.hf_auth import current_token
|
|
100
|
+
|
|
101
|
+
token = current_token()
|
|
102
|
+
files, total = dl.plan_download(model_id, token=token)
|
|
103
|
+
self._total = total
|
|
104
|
+
self._start = time.perf_counter()
|
|
105
|
+
app.call_from_thread(self._log, f"Downloading {len(files)} files · {units.humanize_bytes(total)}")
|
|
106
|
+
last = [0.0]
|
|
107
|
+
|
|
108
|
+
def on_prog(done: int) -> None:
|
|
109
|
+
now = time.perf_counter()
|
|
110
|
+
if done >= self._total or now - last[0] >= 0.1:
|
|
111
|
+
last[0] = now
|
|
112
|
+
app.call_from_thread(self._progress, done, self._total)
|
|
113
|
+
|
|
114
|
+
local = dl.download_model(model_id, files, token=token, on_progress=on_prog)
|
|
115
|
+
ctx.session.model_local_path = local
|
|
116
|
+
app.call_from_thread(self._log, f"Downloaded to {local}", "#3fb950")
|
|
117
|
+
app.call_from_thread(self._set_stage, 0, "done")
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
app.call_from_thread(self._log, f"Download failed: {exc}", "#f85149")
|
|
120
|
+
app.call_from_thread(self._set_stage, 0, "failed")
|
|
121
|
+
|
|
122
|
+
# ---- stage 2: quantize ----
|
|
123
|
+
app.call_from_thread(self._set_stage, 1, "running")
|
|
124
|
+
app.call_from_thread(self._indeterminate)
|
|
125
|
+
quant_out = None
|
|
126
|
+
try:
|
|
127
|
+
def qprog(stage: str, frac: float) -> None:
|
|
128
|
+
app.call_from_thread(self._log, f" {stage} … {frac * 100:.0f}%", "#6e7681")
|
|
129
|
+
|
|
130
|
+
result = app.dispatcher.dispatch(
|
|
131
|
+
Intent(IntentKind.QUANTIZE, {"format_id": candidate.format_id}), ctx, qprog
|
|
132
|
+
)
|
|
133
|
+
if result.success:
|
|
134
|
+
quant_out = getattr(result.payload, "output_dir", None)
|
|
135
|
+
app.call_from_thread(self._log, result.message, "#3fb950")
|
|
136
|
+
app.call_from_thread(self._set_stage, 1, "done")
|
|
137
|
+
else:
|
|
138
|
+
app.call_from_thread(self._log, result.message, "#d29922")
|
|
139
|
+
app.call_from_thread(self._set_stage, 1, "manual")
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
app.call_from_thread(self._log, f"Quantize error: {exc}", "#f85149")
|
|
142
|
+
app.call_from_thread(self._set_stage, 1, "failed")
|
|
143
|
+
|
|
144
|
+
# ---- stage 3: deploy ----
|
|
145
|
+
app.call_from_thread(self._set_stage, 2, "running")
|
|
146
|
+
try:
|
|
147
|
+
from pathlib import Path
|
|
148
|
+
|
|
149
|
+
from zeroquantz.deploy import (
|
|
150
|
+
can_provision,
|
|
151
|
+
generate_assets,
|
|
152
|
+
get_target,
|
|
153
|
+
is_runtime_available,
|
|
154
|
+
launch,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
target = get_target(app.deploy_target)
|
|
158
|
+
deploy_model = quant_out or ctx.session.model_local_path or model_id
|
|
159
|
+
name = model_id.split("/")[-1] or "model"
|
|
160
|
+
out = Path.cwd() / f"zeroquantz-{name}-{fmt.id}" / "deploy"
|
|
161
|
+
bundle = generate_assets(deploy_model, fmt, target, out)
|
|
162
|
+
app.call_from_thread(self._log, f"Deploy assets → {bundle.directory}", "#3fb950")
|
|
163
|
+
app.call_from_thread(self._log, f" serve: {bundle.serve_command}", "#3fb950")
|
|
164
|
+
if bundle.endpoint:
|
|
165
|
+
app.call_from_thread(self._log, f" endpoint (once running): {bundle.endpoint}", "#8b949e")
|
|
166
|
+
if is_runtime_available(target):
|
|
167
|
+
app.call_from_thread(self._log, f"Launching {target.label} locally…", "#d29922")
|
|
168
|
+
try:
|
|
169
|
+
launch(target, bundle.serve_command, cuda_version=ctx.hardware.cuda_version)
|
|
170
|
+
app.call_from_thread(self._log, f"Started — {bundle.endpoint or 'see logs'}", "#3fb950")
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
app.call_from_thread(self._log, f"Launch skipped: {exc}", "#6e7681")
|
|
173
|
+
elif can_provision(target):
|
|
174
|
+
app.call_from_thread(
|
|
175
|
+
self._log,
|
|
176
|
+
f"{target.label} runs in its own isolated env (not built yet). Provision + serve:",
|
|
177
|
+
"#8b949e",
|
|
178
|
+
)
|
|
179
|
+
app.call_from_thread(self._log, f" zeroquantz serve {deploy_model} -r {target.name}", "#58a6ff")
|
|
180
|
+
app.call_from_thread(self._log, " (or copy the assets to a GPU server)", "#6e7681")
|
|
181
|
+
else:
|
|
182
|
+
app.call_from_thread(self._log, f"{target.label} not installed here — copy the assets to your GPU server and run.", "#8b949e")
|
|
183
|
+
app.call_from_thread(self._set_stage, 2, "done")
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
app.call_from_thread(self._log, f"Deploy failed: {exc}", "#f85149")
|
|
186
|
+
app.call_from_thread(self._set_stage, 2, "failed")
|
|
187
|
+
|
|
188
|
+
app.call_from_thread(self._log, "\nPlan executed. Esc to go back, h for home.", "bold #58a6ff")
|
|
189
|
+
|
|
190
|
+
def action_back(self) -> None:
|
|
191
|
+
self.app.pop_screen()
|
|
192
|
+
|
|
193
|
+
def action_home(self) -> None:
|
|
194
|
+
self.app.go_home()
|