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,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")
|