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,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()
@@ -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()