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,459 @@
1
+ """Non-interactive Typer subcommands.
2
+
3
+ Each command builds an ephemeral (scratch) context, runs the same intents the
4
+ interactive surfaces use, and renders with the shared Rich renderers — so
5
+ ``zeroquantz recommend`` prints exactly what ``/recommend`` shows in the TUI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from zeroquantz.agent.dispatcher import CommandResult, Dispatcher
16
+ from zeroquantz.agent.intents import Intent, IntentKind
17
+ from zeroquantz.core.exceptions import ZeroQuantzError
18
+
19
+ # AppContext (hardware detection + registries), render_result, and Objective are
20
+ # imported lazily inside the functions that use them. Building the Typer app and
21
+ # registering commands must stay cheap so `zeroquantz --help`/`--version` and shell
22
+ # completion don't drag in the detection + optimization stack.
23
+ if TYPE_CHECKING:
24
+ from zeroquantz.core.context import AppContext
25
+
26
+ console = Console()
27
+
28
+
29
+ def _context(gpu: str | None = None) -> AppContext:
30
+ from zeroquantz.core.context import AppContext
31
+
32
+ ctx = AppContext.create() # scratch session, never persisted
33
+ if gpu and gpu.lower() != "auto":
34
+ from zeroquantz.hardware.detector import HardwareDetector
35
+
36
+ ctx.hardware = HardwareDetector.simulate(gpu)
37
+ ctx.session.hardware = ctx.hardware
38
+ return ctx
39
+
40
+
41
+ def _cli_progress(stage: str, fraction: float) -> None:
42
+ console.print(f"[grey50] {stage} … {fraction * 100:.0f}%[/grey50]")
43
+
44
+
45
+ def _emit(result: CommandResult) -> None:
46
+ from zeroquantz.render import render_result
47
+
48
+ console.print(render_result(result))
49
+ if not result.success:
50
+ raise typer.Exit(code=1)
51
+
52
+
53
+ def _apply_goal(ctx: AppContext, **updates: object) -> None:
54
+ from zeroquantz.optimization.constraints import Objective
55
+
56
+ clean = {k: v for k, v in updates.items() if v is not None}
57
+ if "objective" in clean and clean["objective"] is not None:
58
+ clean["objective"] = Objective(str(clean["objective"]))
59
+ if clean:
60
+ ctx.session.goal = ctx.session.goal.with_updates(**clean)
61
+
62
+
63
+ def register(app: typer.Typer) -> None:
64
+ disp = Dispatcher()
65
+
66
+ @app.command()
67
+ def hardware() -> None:
68
+ """Show the detected GPU, VRAM, and precision support."""
69
+ _emit(disp.dispatch(Intent(IntentKind.HARDWARE), _context()))
70
+
71
+ @app.command()
72
+ def inspect(model: str = typer.Argument(..., help="Hugging Face id or local path.")) -> None:
73
+ """Inspect a model's architecture and size (metadata only)."""
74
+ ctx = _context()
75
+ _emit(disp.dispatch(Intent(IntentKind.LOAD_MODEL, {"model_id": model}), ctx))
76
+
77
+ @app.command()
78
+ def recommend(
79
+ model: str = typer.Argument(..., help="Hugging Face id or local path."),
80
+ max_vram: float | None = typer.Option(None, "--max-vram", help="VRAM budget in GB."),
81
+ max_size: float | None = typer.Option(None, "--max-size", help="Model size budget in GB."),
82
+ runtime: str | None = typer.Option(None, "--runtime", help="Target runtime (vllm, transformers)."),
83
+ objective: str = typer.Option("balanced", "--objective", help="quality | speed | memory | balanced."),
84
+ context: int = typer.Option(4096, "--context", help="Context length (tokens) for the fit check."),
85
+ gpu: str | None = typer.Option(None, "--gpu", help="Plan for a named GPU (e.g. 'RTX 4090') or 'auto'."),
86
+ show_all: bool = typer.Option(False, "--all", help="Show every catalogued format, not just the top 15."),
87
+ ) -> None:
88
+ """Rank quantization strategies for a model under your constraints."""
89
+ ctx = _context(gpu)
90
+ res = disp.dispatch(Intent(IntentKind.LOAD_MODEL, {"model_id": model}), ctx)
91
+ if not res.success:
92
+ _emit(res)
93
+ _apply_goal(
94
+ ctx, max_vram_gb=max_vram, max_model_size_gb=max_size,
95
+ runtime=runtime, objective=objective, context_length=context,
96
+ )
97
+ result = disp.dispatch(Intent(IntentKind.RECOMMEND), ctx)
98
+ if result.success and not show_all and isinstance(result.payload, dict):
99
+ result.payload["ranked"] = result.payload["ranked"][:15]
100
+ _emit(result)
101
+
102
+ @app.command()
103
+ def plan(
104
+ model: str = typer.Argument(...),
105
+ max_vram: float | None = typer.Option(None, "--max-vram", help="VRAM budget in GB."),
106
+ gpu: str | None = typer.Option(None, "--gpu", help="Plan for a named GPU or 'auto'."),
107
+ ) -> None:
108
+ """Build a mixed-precision plan for a memory budget."""
109
+ ctx = _context(gpu)
110
+ res = disp.dispatch(Intent(IntentKind.LOAD_MODEL, {"model_id": model}), ctx)
111
+ if not res.success:
112
+ _emit(res)
113
+ _apply_goal(ctx, max_vram_gb=max_vram)
114
+ _emit(disp.dispatch(Intent(IntentKind.PLAN), ctx))
115
+
116
+ @app.command()
117
+ def profile(
118
+ model: str = typer.Argument(...),
119
+ measured: bool = typer.Option(False, "--measured", help="Run a measured pass (needs torch + GPU)."),
120
+ ) -> None:
121
+ """Estimate per-layer sensitivity (heuristic, or --measured)."""
122
+ ctx = _context()
123
+ res = disp.dispatch(Intent(IntentKind.LOAD_MODEL, {"model_id": model}), ctx)
124
+ if not res.success:
125
+ _emit(res)
126
+ if measured:
127
+ from zeroquantz.profiling.sensitivity import SensitivityProfiler
128
+
129
+ try:
130
+ sp = SensitivityProfiler.profile(model, progress=_cli_progress)
131
+ _emit(CommandResult("profile", "Measured layer sensitivity.", sp))
132
+ except ZeroQuantzError as exc:
133
+ _emit(CommandResult("error", exc.format(), success=False))
134
+ else:
135
+ _emit(disp.dispatch(Intent(IntentKind.PROFILE), ctx))
136
+
137
+ @app.command()
138
+ def quantize(
139
+ model: str = typer.Argument(...),
140
+ backend: str | None = typer.Option(None, "--backend", help="Backend name (bitsandbytes, torchao)."),
141
+ method: str | None = typer.Option(None, "--method", help="Method (nf4, int8_bnb, int8_torchao, int4_torchao)."),
142
+ output: str | None = typer.Option(None, "--output", "-o", help="Directory to write the quantized model."),
143
+ ) -> None:
144
+ """Quantize a model (requires a backend + CUDA GPU)."""
145
+ ctx = _context()
146
+ res = disp.dispatch(Intent(IntentKind.LOAD_MODEL, {"model_id": model}), ctx)
147
+ if not res.success:
148
+ _emit(res)
149
+ result = disp.dispatch(Intent(IntentKind.QUANTIZE, {"method": method}), ctx, _cli_progress)
150
+ from zeroquantz.render import render_result
151
+
152
+ console.print(render_result(result))
153
+ if result.success and output:
154
+ _emit(disp.dispatch(Intent(IntentKind.EXPORT, {"path": output}), ctx))
155
+ elif not result.success:
156
+ raise typer.Exit(code=1)
157
+
158
+ @app.command()
159
+ def benchmark(
160
+ model: str = typer.Argument(..., help="Model id or a quantized directory."),
161
+ prompts: str | None = typer.Option(None, "--prompts", help="JSONL/TXT file of prompts."),
162
+ runs: int = typer.Option(5, "--runs"),
163
+ warmup: int = typer.Option(2, "--warmup"),
164
+ quality: bool = typer.Option(False, "--quality", help="Also measure perplexity."),
165
+ ) -> None:
166
+ """Benchmark VRAM, latency, and throughput (requires torch + GPU)."""
167
+ from zeroquantz.benchmark.runner import BenchmarkRunner
168
+
169
+ ctx = _context()
170
+ prompt_list = _load_prompts(prompts)
171
+ try:
172
+ result = BenchmarkRunner.run(
173
+ model, prompts=prompt_list, runs=runs, warmup=warmup,
174
+ measure_quality=quality, hardware=ctx.hardware, progress=_cli_progress,
175
+ )
176
+ _emit(CommandResult("benchmark", f"Benchmarked {model}.", result))
177
+ except ZeroQuantzError as exc:
178
+ _emit(CommandResult("error", exc.format(), success=False))
179
+
180
+ @app.command()
181
+ def verify(
182
+ base: str = typer.Argument(..., help="Baseline (full-precision) model."),
183
+ quantized: str = typer.Argument(..., help="Quantized model directory."),
184
+ prompts: str | None = typer.Option(None, "--prompts", help="JSONL/TXT file of prompts."),
185
+ ) -> None:
186
+ """Compare a quantized model against its baseline (requires torch + GPU)."""
187
+ from zeroquantz.verification.report import Verifier
188
+
189
+ ctx = _context()
190
+ try:
191
+ report = Verifier.verify(
192
+ base, quantized, prompts=_load_prompts(prompts),
193
+ hardware=ctx.hardware, progress=_cli_progress,
194
+ )
195
+ _emit(CommandResult("verification", f"Verification: {report.status}", report))
196
+ except ZeroQuantzError as exc:
197
+ _emit(CommandResult("error", exc.format(), success=False))
198
+
199
+ @app.command()
200
+ def backends() -> None:
201
+ """List quantization backends and whether they are installed."""
202
+ from rich.table import Table
203
+
204
+ ctx = _context()
205
+ table = Table(title="Quantization backends")
206
+ table.add_column("Backend", style="cyan")
207
+ table.add_column("Available")
208
+ table.add_column("Methods", style="grey62")
209
+ for b in ctx.backends.all_backends():
210
+ ok = "[green]yes[/green]" if b.is_available() else "[grey62]no[/grey62]"
211
+ table.add_row(b.name, ok, ", ".join(b.methods))
212
+ console.print(table)
213
+
214
+ @app.command()
215
+ def convert(
216
+ model: str = typer.Argument(..., help="Local model directory (HF safetensors + config.json)."),
217
+ output: str = typer.Option(..., "--output", "-o", help="Output .gguf path."),
218
+ quant: str = typer.Option("q4_0", "--quant", help="q4_0 | q8_0 | f16 | f32."),
219
+ ) -> None:
220
+ """Convert an HF model dir to GGUF via the native candle prototype (fast, CPU).
221
+
222
+ Uses the ``zeroquantz-convert`` Rust binary when built (Cargo.toml + src/ at
223
+ the repo root); otherwise prints the build steps and the equivalent
224
+ llama.cpp commands.
225
+ """
226
+ import subprocess
227
+ from pathlib import Path
228
+
229
+ binary = _find_convert_binary()
230
+ if binary is None:
231
+ _print_convert_unavailable(model, output, quant)
232
+ raise typer.Exit(code=1)
233
+ if not Path(model).expanduser().is_dir():
234
+ console.print(
235
+ f"[yellow]'{model}' is not a local directory.[/yellow] Download the model "
236
+ "first (the TUI download step, or `huggingface-cli download`), then pass "
237
+ "its snapshot folder."
238
+ )
239
+ raise typer.Exit(code=1)
240
+ cmd = [str(binary), "--model", str(Path(model).expanduser()), "--out", output, "--quant", quant]
241
+ console.print(f"[grey50]$ {' '.join(cmd)}[/grey50]")
242
+ raise typer.Exit(code=subprocess.run(cmd).returncode)
243
+
244
+ @app.command()
245
+ def serve(
246
+ model: str = typer.Argument(..., help="Local model dir or HF id to serve."),
247
+ runtime: str = typer.Option("vllm", "--runtime", "-r", help="vllm | sglang | tensorrt_llm."),
248
+ format_id: str | None = typer.Option(None, "--format", help="Catalog format id (adds the right --quantization flag)."),
249
+ port: int | None = typer.Option(None, "--port"),
250
+ plan: bool = typer.Option(False, "--plan", help="Show the isolated-env build + serve commands without running."),
251
+ ) -> None:
252
+ """Serve a model on vLLM/SGLang/TensorRT-LLM — each in its own isolated env.
253
+
254
+ The runtime is provisioned in a dedicated venv under
255
+ ``~/.zeroquantz/envs/serve/<runtime>/`` and served from it. Use ``--plan`` to
256
+ preview the exact build + serve commands without installing anything.
257
+ """
258
+ from zeroquantz.deploy import can_provision, get_target, launch
259
+ from zeroquantz.deploy.runtime_env import RuntimeEnvRunner
260
+ from zeroquantz.quantization import catalog
261
+
262
+ target = get_target(runtime)
263
+ if target is None or not can_provision(target):
264
+ console.print(
265
+ f"[yellow]'{runtime}' is not an isolated serving runtime.[/yellow] "
266
+ "Choose one of: vllm, sglang, tensorrt_llm. For llama.cpp / Ollama / "
267
+ "Transformers, use the generated deploy assets instead."
268
+ )
269
+ raise typer.Exit(code=1)
270
+ fmt = catalog.get_format(format_id) if format_id else None
271
+ if format_id and fmt is None:
272
+ console.print(f"[yellow]Unknown format '{format_id}'.[/yellow] Try `zeroquantz recommend {model}`.")
273
+ raise typer.Exit(code=1)
274
+
275
+ runner = RuntimeEnvRunner()
276
+ env_plan = runner.plan(target)
277
+ serve_cmd = target.serve_command(model, fmt, port=port)
278
+ endpoint = f"http://localhost:{port or target.default_port}/v1" if target.openai_compatible else None
279
+
280
+ if plan:
281
+ console.print(f"[bold cyan]{target.label}[/bold cyan] isolated env → [grey50]{env_plan.env_dir}[/grey50]")
282
+ for c in env_plan.commands:
283
+ console.print(f" [cyan]{c}[/cyan]")
284
+ console.print(f" [cyan]{serve_cmd}[/cyan] [grey50](run inside that env)[/grey50]")
285
+ state = "[green]built[/green]" if runner.is_ready(target) else "[grey62]not built yet[/grey62]"
286
+ console.print(f"status: {state} · endpoint (once running): {endpoint or 'n/a'}")
287
+ return
288
+
289
+ cuda = None
290
+ try:
291
+ from zeroquantz.hardware.detector import HardwareDetector
292
+
293
+ cuda = HardwareDetector.detect().cuda_version
294
+ except Exception:
295
+ pass
296
+ console.print(
297
+ f"[grey50]Provisioning {target.label} in {env_plan.env_dir} "
298
+ "(first run installs the runtime; this can be several GB)…[/grey50]"
299
+ )
300
+ try:
301
+ proc = launch(target, serve_cmd, cuda_version=cuda, progress=_cli_progress)
302
+ except Exception as exc:
303
+ console.print(f"[red]Could not start {target.label}:[/red] {exc}")
304
+ raise typer.Exit(code=1) from exc
305
+ console.print(f"[green]Started {target.label}[/green] — {serve_cmd}")
306
+ if endpoint:
307
+ console.print(f"endpoint: {endpoint}")
308
+ console.print("[grey50](serving in the foreground; Ctrl+C to stop)[/grey50]")
309
+ raise typer.Exit(code=proc.wait())
310
+
311
+ @app.command("gguf-quantize")
312
+ def gguf_quantize(
313
+ gguf_in: str = typer.Argument(..., metavar="INPUT.gguf", help="Input f16/f32 GGUF to quantize."),
314
+ output: str = typer.Option(..., "--output", "-o", help="Output quantized .gguf path."),
315
+ type_: str = typer.Option("Q4_K_M", "--type", "-t", help="Q4_K_M | Q5_K_M | Q6_K | Q8_0 | Q4_0 …"),
316
+ verify: bool = typer.Option(True, "--verify/--no-verify", help="Load-test the output after quantizing."),
317
+ ) -> None:
318
+ """Quantize a GGUF via llama-cpp-python (pip-only; llama.cpp-loadable output).
319
+
320
+ Runs in the isolated ``tools/gguf`` env, built on first use from prebuilt
321
+ wheels — no compiler. ``convert`` makes an f16 GGUF; this quantizes it.
322
+ """
323
+ from pathlib import Path
324
+
325
+ from zeroquantz.quantization.gguf_pipeline import GgufPipeline
326
+
327
+ if not Path(gguf_in).expanduser().is_file():
328
+ console.print(
329
+ f"[yellow]'{gguf_in}' not found.[/yellow] Produce an f16 GGUF first "
330
+ "(e.g. `zeroquantz build <model> --keep-f16`)."
331
+ )
332
+ raise typer.Exit(code=1)
333
+ pipe = GgufPipeline()
334
+ try:
335
+ out = pipe.quantize(str(Path(gguf_in).expanduser()), output, type_, progress=_cli_progress)
336
+ if verify:
337
+ pipe.verify(str(out), progress=_cli_progress)
338
+ except ZeroQuantzError as exc:
339
+ console.print(f"[red]Quantize failed:[/red] {exc.format()}")
340
+ raise typer.Exit(code=1) from exc
341
+ console.print(f"[green]✓ quantized[/green] {out}")
342
+
343
+ @app.command()
344
+ def build(
345
+ model: str = typer.Argument(..., help="HF id or local model directory."),
346
+ quant: str = typer.Option("Q4_K_M", "--quant", "-q", help="GGML type: Q4_K_M | Q5_K_M | Q6_K | Q8_0 …"),
347
+ output: str | None = typer.Option(None, "--out", "-o", help="Output dir (default ./zeroquantz-<name>-<quant>)."),
348
+ serve_with: str | None = typer.Option(None, "--serve", help="After building, serve with: llamacpp | ollama."),
349
+ keep_f16: bool = typer.Option(False, "--keep-f16", help="Keep the intermediate f16 GGUF."),
350
+ plan: bool = typer.Option(False, "--plan", help="Preview the whole pipeline without running."),
351
+ ) -> None:
352
+ """Production GGUF pipeline: download → convert → quantize → verify → (serve).
353
+
354
+ Pip-only, no compiler: llama.cpp's convert_hf_to_gguf.py (all architectures +
355
+ tokenizers) for convert, and llama-cpp-python for quantize + a load-test
356
+ verify, so the output is a proven llama.cpp/Ollama-loadable GGUF. Each stage
357
+ hard-fails; `--plan` previews it. Runs in the isolated tools/gguf env.
358
+ """
359
+ from pathlib import Path
360
+
361
+ from zeroquantz.quantization.gguf_pipeline import GgufPipeline
362
+
363
+ is_local = Path(model).expanduser().is_dir()
364
+ name = (Path(model).expanduser().resolve().name if is_local
365
+ else model.rstrip("/").split("/")[-1]) or "model"
366
+ out_dir = Path(output).expanduser() if output else Path.cwd() / f"zeroquantz-{name}-{quant}"
367
+ pipe = GgufPipeline()
368
+
369
+ if plan:
370
+ model_disp = str(Path(model).expanduser()) if is_local else f"<downloaded {model}>"
371
+ console.print(f"[bold cyan]GGUF pipeline[/bold cyan] {model} → {out_dir} ({quant})")
372
+ console.print(" [grey50]0 download[/grey50] " + ("local dir (skip)" if is_local else f"download {model}"))
373
+ for i, st in enumerate(pipe.plan(model_disp, quant, out_dir), start=1):
374
+ state = "[green]ready[/green]" if st.ready else "[grey62]needs setup[/grey62]"
375
+ console.print(f" [grey50]{i} {st.name}[/grey50] {state}")
376
+ console.print(f" [cyan]{' '.join(st.command)}[/cyan]")
377
+ if not st.ready and st.hint:
378
+ console.print(f" [grey62]↳ {st.hint}[/grey62]")
379
+ if serve_with:
380
+ console.print(f" [grey50]4 serve[/grey50] via {serve_with} (llama-server / ollama)")
381
+ return
382
+
383
+ if is_local:
384
+ model_dir = Path(model).expanduser()
385
+ else:
386
+ from zeroquantz.models.downloader import download_model, plan_download
387
+
388
+ console.print(f"[grey50]downloading {model} …[/grey50]")
389
+ files, _total = plan_download(model)
390
+ model_dir = Path(download_model(model, files))
391
+
392
+ def prog(stage: str, frac: float) -> None:
393
+ console.print(f"[grey50] {stage} … {frac * 100:.0f}%[/grey50]")
394
+
395
+ try:
396
+ arts = pipe.run(str(model_dir), quant, out_dir, progress=prog, keep_f16=keep_f16)
397
+ except ZeroQuantzError as exc:
398
+ console.print(f"[red]Pipeline failed:[/red] {exc.format()}")
399
+ raise typer.Exit(code=1) from exc
400
+ console.print(f"[green]✓ built[/green] {arts.quantized_gguf}")
401
+
402
+ try:
403
+ from zeroquantz.core.artifacts import default_quantized_registry
404
+
405
+ default_quantized_registry().add(
406
+ str(arts.quantized_gguf), base_model=model,
407
+ format_id=f"gguf_{quant.lower()}", method=quant,
408
+ )
409
+ except Exception:
410
+ pass
411
+
412
+ if serve_with == "llamacpp":
413
+ import subprocess
414
+
415
+ port = 8080
416
+ cmd = pipe.serve_command(str(arts.quantized_gguf), port)
417
+ console.print(f"[grey50]serve:[/grey50] [cyan]{' '.join(cmd)}[/cyan]")
418
+ console.print(
419
+ f"endpoint: http://localhost:{port}/v1 "
420
+ "[grey50](llama-cpp-python server; Ctrl+C to stop)[/grey50]"
421
+ )
422
+ raise typer.Exit(code=subprocess.run(cmd).returncode)
423
+ if serve_with:
424
+ from zeroquantz.deploy import get_target, is_runtime_available, launch
425
+
426
+ target = get_target(serve_with)
427
+ if target is None:
428
+ console.print(f"[yellow]Unknown serve runtime '{serve_with}'.[/yellow] Try: llamacpp | ollama.")
429
+ return
430
+ serve_cmd = target.serve_command(str(arts.quantized_gguf))
431
+ console.print(f"[grey50]serve:[/grey50] [cyan]{serve_cmd}[/cyan]")
432
+ if is_runtime_available(target):
433
+ launch(target, serve_cmd)
434
+ console.print(f"[green]started {target.label}[/green]")
435
+ else:
436
+ console.print(f"[grey62]{target.label} not installed here — run the command above.[/grey62]")
437
+
438
+
439
+ def _find_convert_binary():
440
+ """Locate the native Rust converter (on PATH or ./target/release)."""
441
+ from zeroquantz.quantization.native import find_native_binary
442
+
443
+ return find_native_binary("zeroquantz-convert")
444
+
445
+
446
+ def _print_convert_unavailable(model: str, output: str, quant: str) -> None:
447
+ console.print("[yellow]Native converter not built.[/yellow] Build it (needs a Rust toolchain):")
448
+ console.print(" [cyan]cargo build --release[/cyan] (from the repo root)")
449
+ console.print("Then re-run `zeroquantz convert`. Meanwhile, the llama.cpp path:")
450
+ console.print(f" [cyan]python convert_hf_to_gguf.py {model} --outfile model.f16.gguf[/cyan]")
451
+ console.print(f" [cyan]llama-quantize model.f16.gguf {output} {quant.upper()}[/cyan]")
452
+
453
+
454
+ def _load_prompts(path: str | None) -> list[str] | None:
455
+ if not path:
456
+ return None
457
+ from zeroquantz.profiling.calibration import CalibrationDataset
458
+
459
+ return CalibrationDataset.from_file(path).prompts
@@ -0,0 +1,56 @@
1
+ """A plain-text REPL used when the Textual TUI can't run (no TTY, ``--no-tui``).
2
+
3
+ Same parser, same dispatcher, same renderers as the TUI — just a readline loop
4
+ instead of a full-screen app.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich.console import Console
10
+ from rich.text import Text
11
+
12
+ from zeroquantz.agent.dispatcher import Dispatcher
13
+ from zeroquantz.agent.intents import Intent, IntentKind
14
+ from zeroquantz.agent.parser import IntentParser
15
+ from zeroquantz.core.context import AppContext
16
+ from zeroquantz.render import render_result
17
+ from zeroquantz.tui.banner import banner_text
18
+
19
+
20
+ def run_repl(*, session_name: str | None = None, continue_last: bool = False) -> None:
21
+ console = Console()
22
+ ctx = AppContext.create(session_name=session_name, continue_last=continue_last)
23
+ dispatcher = Dispatcher()
24
+
25
+ console.print(Text(banner_text(console.width - 4), style="bold #c9d1d9"))
26
+ console.print("Interactive Model Optimization Environment", style="grey62")
27
+ hw = ctx.hardware
28
+ if hw.cuda_available:
29
+ console.print(
30
+ f"[green]●[/green] {hw.gpu_name} [grey62]VRAM {hw.total_vram_gb:g} GB · "
31
+ f"CUDA {hw.cuda_version or 'detected'}[/grey62]"
32
+ )
33
+ else:
34
+ console.print("[yellow]⚠ No CUDA GPU detected — planning-only mode[/yellow]")
35
+ console.print("Type [cyan]/help[/cyan] for commands, [cyan]/exit[/cyan] to quit.\n")
36
+
37
+ def progress(stage: str, fraction: float) -> None:
38
+ console.print(f"[grey50] {stage} … {fraction * 100:.0f}%[/grey50]")
39
+
40
+ while True:
41
+ try:
42
+ text = console.input("[bold green]›[/bold green] ").strip()
43
+ except (EOFError, KeyboardInterrupt):
44
+ console.print()
45
+ break
46
+ if not text:
47
+ continue
48
+ intent = IntentParser.parse(text)
49
+ if intent.kind is IntentKind.EXIT:
50
+ break
51
+ result = dispatcher.dispatch(intent, ctx, progress)
52
+ console.print(render_result(result))
53
+ if result.should_exit:
54
+ break
55
+
56
+ console.print("Goodbye.", style="grey62")
@@ -0,0 +1,7 @@
1
+ """Core session/context/event plumbing shared by every ZeroQuantz surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.core import exceptions
6
+
7
+ __all__ = ["exceptions"]