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,202 @@
|
|
|
1
|
+
"""Shared sub-environment primitive: create and drive isolated venvs.
|
|
2
|
+
|
|
3
|
+
ZeroQuantz runs mutually-incompatible toolchains in dedicated virtual environments
|
|
4
|
+
so they never pollute your main Python — or each other. Two consumers share this:
|
|
5
|
+
|
|
6
|
+
* **Quantization** toolchains (AWQ, GPTQ, AutoRound, HQQ) — see
|
|
7
|
+
:mod:`zeroquantz.quantization.isolated`.
|
|
8
|
+
* **Serving** runtimes (vLLM, SGLang, TensorRT-LLM) — see
|
|
9
|
+
:mod:`zeroquantz.deploy.runtime_env`.
|
|
10
|
+
|
|
11
|
+
Each env lives under a two-level, namespaced layout::
|
|
12
|
+
|
|
13
|
+
~/.zeroquantz/envs/
|
|
14
|
+
quant/ awq/ gptq/ autoround/ hqq/
|
|
15
|
+
serve/ vllm/ sglang/ tensorrt_llm/
|
|
16
|
+
|
|
17
|
+
``uv venv`` is used when ``uv`` is on PATH (fast), otherwise ``python -m venv``.
|
|
18
|
+
Builds are cached behind a ``.zeroquantz_ready`` marker and reused. Nothing is ever
|
|
19
|
+
installed into your main environment.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
from zeroquantz.core.exceptions import ZeroQuantzError
|
|
34
|
+
from zeroquantz.utils.logging import get_logger
|
|
35
|
+
from zeroquantz.utils.paths import paths
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
|
|
40
|
+
log = get_logger(__name__)
|
|
41
|
+
|
|
42
|
+
# PyTorch CUDA wheel index by detected CUDA version (best-effort nearest match).
|
|
43
|
+
_TORCH_CUDA_INDEX = {
|
|
44
|
+
"12.8": "https://download.pytorch.org/whl/cu128",
|
|
45
|
+
"12.6": "https://download.pytorch.org/whl/cu126",
|
|
46
|
+
"12.4": "https://download.pytorch.org/whl/cu124",
|
|
47
|
+
"12.1": "https://download.pytorch.org/whl/cu121",
|
|
48
|
+
"11.8": "https://download.pytorch.org/whl/cu118",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def torch_index_for(cuda_version: str | None) -> str | None:
|
|
53
|
+
"""Best-effort PyTorch CUDA wheel index URL for a detected CUDA version."""
|
|
54
|
+
if not cuda_version:
|
|
55
|
+
return None
|
|
56
|
+
for prefix, url in _TORCH_CUDA_INDEX.items():
|
|
57
|
+
if cuda_version.startswith(prefix):
|
|
58
|
+
return url
|
|
59
|
+
return "https://download.pytorch.org/whl/cu124" # newest known
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EnvSpec:
|
|
64
|
+
"""A declarative description of one isolated environment."""
|
|
65
|
+
|
|
66
|
+
kind: str # "quant" | "serve"
|
|
67
|
+
name: str # family or runtime name (the leaf dir)
|
|
68
|
+
pip: tuple[str, ...] = () # packages to install (the toolchain/runtime)
|
|
69
|
+
torch_index: str | None = None # install torch first from this CUDA wheel index
|
|
70
|
+
extra_index_url: str | None = None # e.g. https://pypi.nvidia.com for TensorRT-LLM
|
|
71
|
+
python_version: str = "3.11" # used only by `uv venv --python`
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class EnvPlan:
|
|
76
|
+
"""A previewable plan for building an environment (no side effects)."""
|
|
77
|
+
|
|
78
|
+
spec: EnvSpec
|
|
79
|
+
env_dir: Path
|
|
80
|
+
python: Path
|
|
81
|
+
commands: list[str] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SubEnvManager:
|
|
85
|
+
"""Create and drive per-toolchain / per-runtime isolated environments."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, root: Path | None = None) -> None:
|
|
88
|
+
self.root = root or (paths().ensure().home / "envs")
|
|
89
|
+
|
|
90
|
+
# ---- path resolution ----------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def env_dir(self, spec: EnvSpec) -> Path:
|
|
93
|
+
return self.root / spec.kind / spec.name
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def venv_python(env_dir: Path) -> Path:
|
|
97
|
+
return env_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def bin_dir(env_dir: Path) -> Path:
|
|
101
|
+
"""The env's console-scripts directory (Scripts on Windows, bin elsewhere)."""
|
|
102
|
+
return env_dir / ("Scripts" if os.name == "nt" else "bin")
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def uv_available() -> bool:
|
|
106
|
+
return shutil.which("uv") is not None
|
|
107
|
+
|
|
108
|
+
def is_ready(self, spec: EnvSpec) -> bool:
|
|
109
|
+
env_dir = self.env_dir(spec)
|
|
110
|
+
return (env_dir / ".zeroquantz_ready").exists() and self.venv_python(env_dir).exists()
|
|
111
|
+
|
|
112
|
+
# ---- planning / building ------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def _pip_prefix(self, py: Path, use_uv: bool) -> list[str]:
|
|
115
|
+
if use_uv:
|
|
116
|
+
return [shutil.which("uv") or "uv", "pip", "install", "--python", str(py)]
|
|
117
|
+
return [str(py), "-m", "pip", "install", "--upgrade"]
|
|
118
|
+
|
|
119
|
+
def plan(self, spec: EnvSpec) -> EnvPlan:
|
|
120
|
+
"""The exact create + install commands, without running anything."""
|
|
121
|
+
env_dir = self.env_dir(spec)
|
|
122
|
+
py = self.venv_python(env_dir)
|
|
123
|
+
use_uv = self.uv_available()
|
|
124
|
+
creator = (
|
|
125
|
+
["uv", "venv", str(env_dir), "--python", spec.python_version, "--seed"]
|
|
126
|
+
if use_uv
|
|
127
|
+
else [sys.executable, "-m", "venv", str(env_dir)]
|
|
128
|
+
)
|
|
129
|
+
pip = self._pip_prefix(py, use_uv)
|
|
130
|
+
commands = [" ".join(creator)]
|
|
131
|
+
if spec.torch_index:
|
|
132
|
+
commands.append(" ".join([*pip, "torch", "--index-url", spec.torch_index]))
|
|
133
|
+
if spec.pip:
|
|
134
|
+
extra = ["--extra-index-url", spec.extra_index_url] if spec.extra_index_url else []
|
|
135
|
+
commands.append(" ".join([*pip, *spec.pip, *extra]))
|
|
136
|
+
return EnvPlan(spec=spec, env_dir=env_dir, python=py, commands=commands)
|
|
137
|
+
|
|
138
|
+
def ensure(
|
|
139
|
+
self, spec: EnvSpec, *, progress: Callable[[str, float], None] | None = None
|
|
140
|
+
) -> Path:
|
|
141
|
+
"""Build the env if it isn't ready; return the venv's Python interpreter."""
|
|
142
|
+
env_dir = self.env_dir(spec)
|
|
143
|
+
py = self.venv_python(env_dir)
|
|
144
|
+
if self.is_ready(spec):
|
|
145
|
+
return py
|
|
146
|
+
|
|
147
|
+
use_uv = self.uv_available()
|
|
148
|
+
env_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
if progress:
|
|
150
|
+
progress(f"creating venv for {spec.kind}/{spec.name}", 0.05)
|
|
151
|
+
if use_uv:
|
|
152
|
+
self.run([shutil.which("uv"), "venv", str(env_dir), "--python", spec.python_version, "--seed"])
|
|
153
|
+
else:
|
|
154
|
+
self.run([sys.executable, "-m", "venv", str(env_dir)])
|
|
155
|
+
if not py.exists():
|
|
156
|
+
raise ZeroQuantzError(f"venv creation failed for {spec.kind}/{spec.name} at {env_dir}.")
|
|
157
|
+
|
|
158
|
+
pip = self._pip_prefix(py, use_uv)
|
|
159
|
+
if spec.torch_index:
|
|
160
|
+
if progress:
|
|
161
|
+
progress("installing torch (CUDA)", 0.2)
|
|
162
|
+
self.run([*pip, "torch", "--index-url", spec.torch_index])
|
|
163
|
+
if spec.pip:
|
|
164
|
+
if progress:
|
|
165
|
+
progress(f"installing {spec.name} ({spec.kind})", 0.5)
|
|
166
|
+
extra = ["--extra-index-url", spec.extra_index_url] if spec.extra_index_url else []
|
|
167
|
+
self.run([*pip, *spec.pip, *extra])
|
|
168
|
+
|
|
169
|
+
(env_dir / ".zeroquantz_ready").write_text(
|
|
170
|
+
json.dumps(
|
|
171
|
+
{"pip": list(spec.pip), "torch_index": spec.torch_index,
|
|
172
|
+
"extra_index_url": spec.extra_index_url}
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return py
|
|
176
|
+
|
|
177
|
+
# ---- subprocess helper --------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def run(
|
|
180
|
+
self, cmd: list, *, progress: Callable[[str, float], None] | None = None
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Run a command in the child process, logging its output line by line."""
|
|
183
|
+
log.info("subenv: %s", " ".join(str(c) for c in cmd))
|
|
184
|
+
try:
|
|
185
|
+
proc = subprocess.Popen(
|
|
186
|
+
[str(c) for c in cmd],
|
|
187
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
188
|
+
text=True, encoding="utf-8", errors="replace",
|
|
189
|
+
)
|
|
190
|
+
except OSError as exc:
|
|
191
|
+
raise ZeroQuantzError(f"Failed to launch: {' '.join(map(str, cmd))}", detail=str(exc)) from exc
|
|
192
|
+
assert proc.stdout is not None
|
|
193
|
+
for line in proc.stdout:
|
|
194
|
+
line = line.rstrip()
|
|
195
|
+
if line:
|
|
196
|
+
log.debug("child: %s", line)
|
|
197
|
+
code = proc.wait()
|
|
198
|
+
if code != 0:
|
|
199
|
+
raise ZeroQuantzError(
|
|
200
|
+
f"Sub-env command failed (exit {code}): {' '.join(map(str, cmd[:3]))} …",
|
|
201
|
+
detail="See ~/.zeroquantz/logs for the child process output.",
|
|
202
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Deployment: turn a (model, quantization format, runtime) choice into runnable
|
|
2
|
+
serve commands, an OpenAI-compatible client, and container assets.
|
|
3
|
+
|
|
4
|
+
vLLM / SGLang / TensorRT-LLM are each provisioned and served in their own isolated
|
|
5
|
+
sub-environment (see :mod:`zeroquantz.deploy.runtime_env`)."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from zeroquantz.deploy.assets import DeployBundle, generate_assets
|
|
10
|
+
from zeroquantz.deploy.launcher import can_provision, is_runtime_available, launch
|
|
11
|
+
from zeroquantz.deploy.runtime_env import RuntimeEnvRunner, runtime_spec
|
|
12
|
+
from zeroquantz.deploy.targets import DeployTarget, deploy_targets_for, get_target
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DeployBundle",
|
|
16
|
+
"DeployTarget",
|
|
17
|
+
"RuntimeEnvRunner",
|
|
18
|
+
"can_provision",
|
|
19
|
+
"deploy_targets_for",
|
|
20
|
+
"generate_assets",
|
|
21
|
+
"get_target",
|
|
22
|
+
"is_runtime_available",
|
|
23
|
+
"launch",
|
|
24
|
+
"runtime_spec",
|
|
25
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Generate deployment assets for a (model, format, target): serve script,
|
|
2
|
+
OpenAI-compatible client, and container files (Dockerfile-free — uses the
|
|
3
|
+
runtime's official image via docker-compose / docker run)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from zeroquantz.deploy.targets import DeployTarget
|
|
14
|
+
from zeroquantz.quantization.catalog import QuantFormat
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DeployBundle:
|
|
19
|
+
directory: str
|
|
20
|
+
target: str
|
|
21
|
+
serve_command: str
|
|
22
|
+
endpoint: str | None
|
|
23
|
+
files: list[str] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_assets(
|
|
27
|
+
model_path: str,
|
|
28
|
+
fmt: QuantFormat,
|
|
29
|
+
target: DeployTarget,
|
|
30
|
+
out_dir: str | Path,
|
|
31
|
+
*,
|
|
32
|
+
port: int | None = None,
|
|
33
|
+
) -> DeployBundle:
|
|
34
|
+
port = port or target.default_port
|
|
35
|
+
out = Path(out_dir)
|
|
36
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
files: list[str] = []
|
|
38
|
+
|
|
39
|
+
def write(name: str, content: str, *, executable: bool = False) -> None:
|
|
40
|
+
path = out / name
|
|
41
|
+
path.write_text(content, encoding="utf-8")
|
|
42
|
+
files.append(str(path))
|
|
43
|
+
|
|
44
|
+
is_local = Path(model_path).exists()
|
|
45
|
+
local_serve = target.serve_command(model_path, fmt, port=port)
|
|
46
|
+
|
|
47
|
+
# 1) local serve script
|
|
48
|
+
write("serve.sh", f"#!/usr/bin/env bash\nset -e\n{local_serve}\n", executable=True)
|
|
49
|
+
|
|
50
|
+
endpoint = f"http://localhost:{port}/v1" if target.openai_compatible else None
|
|
51
|
+
|
|
52
|
+
# 2) container assets for server runtimes
|
|
53
|
+
if target.kind == "server" and target.container_image:
|
|
54
|
+
container_path = "/model" if is_local else model_path
|
|
55
|
+
container_serve = target.serve_command(container_path, fmt, port=port)
|
|
56
|
+
write("docker-compose.yml", _compose(target, model_path, container_serve, port, is_local))
|
|
57
|
+
write("run-docker.sh", _run_docker(target, model_path, container_serve, port, is_local),
|
|
58
|
+
executable=True)
|
|
59
|
+
|
|
60
|
+
# 3) gguf helpers
|
|
61
|
+
if target.name == "ollama":
|
|
62
|
+
write("Modelfile", f"FROM {Path(model_path).name if is_local else './model.gguf'}\n")
|
|
63
|
+
|
|
64
|
+
# 4) OpenAI-compatible client
|
|
65
|
+
if target.openai_compatible:
|
|
66
|
+
model_id = Path(model_path).name if is_local else model_path
|
|
67
|
+
write("client.py", _client_py(endpoint, model_id))
|
|
68
|
+
write("client.sh", _client_sh(endpoint, model_id), executable=True)
|
|
69
|
+
|
|
70
|
+
# 5) README
|
|
71
|
+
write("README.md", _readme(target, fmt, model_path, port, endpoint, is_local))
|
|
72
|
+
|
|
73
|
+
return DeployBundle(str(out), target.name, local_serve, endpoint, files)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _compose(target, model_path, container_serve, port, is_local): # noqa: ANN001
|
|
77
|
+
volumes = f' - "{Path(model_path).resolve()}:/model"\n' if is_local else ""
|
|
78
|
+
env = "" if is_local else " environment:\n - HF_TOKEN=${HF_TOKEN}\n"
|
|
79
|
+
ports = f' - "{port}:{port}"\n' if port else ""
|
|
80
|
+
return (
|
|
81
|
+
"services:\n"
|
|
82
|
+
f" {target.name}:\n"
|
|
83
|
+
f" image: {target.container_image}\n"
|
|
84
|
+
" ipc: host\n"
|
|
85
|
+
+ (f" ports:\n{ports}" if ports else "")
|
|
86
|
+
+ (f" volumes:\n{volumes}" if volumes else "")
|
|
87
|
+
+ env
|
|
88
|
+
+ f" command: {container_serve}\n"
|
|
89
|
+
" deploy:\n"
|
|
90
|
+
" resources:\n"
|
|
91
|
+
" reservations:\n"
|
|
92
|
+
" devices:\n"
|
|
93
|
+
" - driver: nvidia\n"
|
|
94
|
+
" count: all\n"
|
|
95
|
+
" capabilities: [gpu]\n"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_docker(target, model_path, container_serve, port, is_local): # noqa: ANN001
|
|
100
|
+
mount = f'-v "{Path(model_path).resolve()}:/model" ' if is_local else ""
|
|
101
|
+
env = "" if is_local else '-e HF_TOKEN="$HF_TOKEN" '
|
|
102
|
+
pub = f"-p {port}:{port} " if port else ""
|
|
103
|
+
return (
|
|
104
|
+
"#!/usr/bin/env bash\nset -e\n"
|
|
105
|
+
f"docker run --gpus all --ipc=host {pub}{mount}{env}{target.container_image} \\\n"
|
|
106
|
+
f" {container_serve}\n"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _client_py(endpoint: str | None, model_id: str) -> str:
|
|
111
|
+
return (
|
|
112
|
+
"from openai import OpenAI\n\n"
|
|
113
|
+
f'client = OpenAI(base_url="{endpoint}", api_key="EMPTY")\n'
|
|
114
|
+
"resp = client.chat.completions.create(\n"
|
|
115
|
+
f' model="{model_id}",\n'
|
|
116
|
+
' messages=[{"role": "user", "content": "Explain quantization in one sentence."}],\n'
|
|
117
|
+
")\nprint(resp.choices[0].message.content)\n"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _client_sh(endpoint: str | None, model_id: str) -> str:
|
|
122
|
+
body = json.dumps({"model": model_id, "messages": [{"role": "user", "content": "Hello!"}]})
|
|
123
|
+
return (
|
|
124
|
+
"#!/usr/bin/env bash\n"
|
|
125
|
+
f"curl {endpoint}/chat/completions \\\n"
|
|
126
|
+
' -H "Content-Type: application/json" \\\n'
|
|
127
|
+
f" -d '{body}'\n"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _readme(target, fmt, model_path, port, endpoint, is_local): # noqa: ANN001
|
|
132
|
+
lines = [
|
|
133
|
+
f"# Deploy {model_path} ({fmt.label}) on {target.label}",
|
|
134
|
+
"",
|
|
135
|
+
f"{target.note}",
|
|
136
|
+
"",
|
|
137
|
+
f"**Install:** {target.install_hint}",
|
|
138
|
+
"",
|
|
139
|
+
"## Run locally",
|
|
140
|
+
"```bash",
|
|
141
|
+
"bash serve.sh",
|
|
142
|
+
"```",
|
|
143
|
+
]
|
|
144
|
+
if target.kind == "server" and target.container_image:
|
|
145
|
+
lines += [
|
|
146
|
+
"",
|
|
147
|
+
"## Run with Docker",
|
|
148
|
+
"```bash",
|
|
149
|
+
"bash run-docker.sh # or: docker compose up",
|
|
150
|
+
"```",
|
|
151
|
+
"",
|
|
152
|
+
"> Note: official runtime images may define their own ENTRYPOINT; if so,",
|
|
153
|
+
"> pass only the arguments after the server executable.",
|
|
154
|
+
]
|
|
155
|
+
if endpoint:
|
|
156
|
+
lines += ["", f"## Query it (OpenAI-compatible, {endpoint})", "```bash", "python client.py # or: bash client.sh", "```"]
|
|
157
|
+
if not is_local:
|
|
158
|
+
lines += ["", f"> `{model_path}` is a Hub id — set `HF_TOKEN` for gated models; the server downloads it on start."]
|
|
159
|
+
if fmt.requires_precision == "fp8":
|
|
160
|
+
lines += ["", "> This format needs FP8/FP4-class hardware (Hopper/Blackwell) to run at full speed."]
|
|
161
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Local launch of a deployment target.
|
|
2
|
+
|
|
3
|
+
Two kinds of runtime:
|
|
4
|
+
|
|
5
|
+
* **Isolated** (vLLM, SGLang, TensorRT-LLM) — provisioned in a dedicated venv under
|
|
6
|
+
``~/.zeroquantz/envs/serve/<name>/`` and launched from it (that env's bin/Scripts
|
|
7
|
+
is prepended to PATH so ``vllm`` / ``python -m sglang…`` / ``trtllm-serve``
|
|
8
|
+
resolve to it). ``launch`` builds the env on first use.
|
|
9
|
+
* **Base/system** (Transformers, llama.cpp, Ollama) — launched directly, requiring
|
|
10
|
+
the tool to be importable or on PATH.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
|
|
24
|
+
from zeroquantz.deploy.targets import DeployTarget
|
|
25
|
+
|
|
26
|
+
# How to detect a *base/system* runtime locally: a CLI on PATH or an importable module.
|
|
27
|
+
_CLI = {"llamacpp": "llama-server", "ollama": "ollama"}
|
|
28
|
+
_MODULE = {"transformers": "transformers"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def can_provision(target: DeployTarget) -> bool:
|
|
32
|
+
"""True if ZeroQuantz can build an isolated env for this runtime on demand."""
|
|
33
|
+
return bool(getattr(target, "isolated_env", False) and target.pip)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_runtime_available(target: DeployTarget) -> bool:
|
|
37
|
+
"""True if this target can be launched *right now* (no build needed).
|
|
38
|
+
|
|
39
|
+
For isolated runtimes that means the venv is already built; for base/system
|
|
40
|
+
runtimes, that the CLI is on PATH or the module is importable.
|
|
41
|
+
"""
|
|
42
|
+
if can_provision(target):
|
|
43
|
+
from zeroquantz.deploy.runtime_env import RuntimeEnvRunner
|
|
44
|
+
|
|
45
|
+
return RuntimeEnvRunner().is_ready(target)
|
|
46
|
+
cli = _CLI.get(target.name)
|
|
47
|
+
if cli and shutil.which(cli):
|
|
48
|
+
return True
|
|
49
|
+
module = _MODULE.get(target.name)
|
|
50
|
+
return bool(module and importlib.util.find_spec(module) is not None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def launch(
|
|
54
|
+
target: DeployTarget,
|
|
55
|
+
serve_command: str,
|
|
56
|
+
*,
|
|
57
|
+
cuda_version: str | None = None,
|
|
58
|
+
progress: Callable[[str, float], None] | None = None,
|
|
59
|
+
):
|
|
60
|
+
"""Start the serve command as a background process. Returns a Popen.
|
|
61
|
+
|
|
62
|
+
For an isolated runtime this **builds the venv on first use** (a multi-GB
|
|
63
|
+
install) and runs the server from it. For base/system runtimes it requires the
|
|
64
|
+
runtime to be installed. Raises RuntimeError if it can't be launched.
|
|
65
|
+
"""
|
|
66
|
+
env = os.environ.copy()
|
|
67
|
+
|
|
68
|
+
if can_provision(target):
|
|
69
|
+
from zeroquantz.deploy.runtime_env import RuntimeEnvRunner
|
|
70
|
+
|
|
71
|
+
runner = RuntimeEnvRunner()
|
|
72
|
+
runner.ensure(target, progress=progress) # build the venv if needed
|
|
73
|
+
bin_dir = str(runner.bin_dir(target))
|
|
74
|
+
env["PATH"] = bin_dir + os.pathsep + env.get("PATH", "")
|
|
75
|
+
env["VIRTUAL_ENV"] = str(runner.env_dir(target))
|
|
76
|
+
return subprocess.Popen(serve_command, shell=True, env=env)
|
|
77
|
+
|
|
78
|
+
if not is_runtime_available(target):
|
|
79
|
+
raise RuntimeError(f"{target.label} is not installed locally.")
|
|
80
|
+
return subprocess.Popen(serve_command, shell=True, env=env)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Build and serve each inference runtime in its own isolated sub-environment.
|
|
2
|
+
|
|
3
|
+
vLLM, SGLang, and TensorRT-LLM pin mutually-incompatible torch/CUDA stacks, so
|
|
4
|
+
each is provisioned in a dedicated venv under ``~/.zeroquantz/envs/serve/<runtime>/``
|
|
5
|
+
(via the shared :class:`~zeroquantz.core.subenv.SubEnvManager`) and served from that
|
|
6
|
+
env's interpreter — never from, and never polluting, your main environment.
|
|
7
|
+
|
|
8
|
+
Runtimes that are plain system tools or run in the base env (llama.cpp, Ollama,
|
|
9
|
+
Transformers) are *not* isolated and are handled directly by the launcher.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from zeroquantz.core.subenv import EnvPlan, EnvSpec, SubEnvManager
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
from zeroquantz.deploy.targets import DeployTarget
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def runtime_spec(target: DeployTarget) -> EnvSpec:
|
|
26
|
+
"""The EnvSpec for a runtime's isolated serving env.
|
|
27
|
+
|
|
28
|
+
We do *not* pin a torch CUDA index here: vLLM/SGLang/TensorRT-LLM wheels pull a
|
|
29
|
+
matching torch themselves, and forcing one risks a conflict.
|
|
30
|
+
"""
|
|
31
|
+
return EnvSpec(
|
|
32
|
+
kind="serve",
|
|
33
|
+
name=target.name,
|
|
34
|
+
pip=tuple(target.pip),
|
|
35
|
+
torch_index=None,
|
|
36
|
+
extra_index_url=target.extra_index_url,
|
|
37
|
+
python_version=target.env_python,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RuntimeEnvRunner:
|
|
42
|
+
"""Provision and locate per-runtime isolated serving environments."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, root: Path | None = None) -> None:
|
|
45
|
+
self._envs = SubEnvManager(root=root)
|
|
46
|
+
|
|
47
|
+
def supports(self, target: DeployTarget) -> bool:
|
|
48
|
+
return bool(getattr(target, "isolated_env", False) and target.pip)
|
|
49
|
+
|
|
50
|
+
def is_ready(self, target: DeployTarget) -> bool:
|
|
51
|
+
return self.supports(target) and self._envs.is_ready(runtime_spec(target))
|
|
52
|
+
|
|
53
|
+
def env_dir(self, target: DeployTarget) -> Path:
|
|
54
|
+
return self._envs.env_dir(runtime_spec(target))
|
|
55
|
+
|
|
56
|
+
def bin_dir(self, target: DeployTarget) -> Path:
|
|
57
|
+
return SubEnvManager.bin_dir(self.env_dir(target))
|
|
58
|
+
|
|
59
|
+
def plan(self, target: DeployTarget) -> EnvPlan:
|
|
60
|
+
return self._envs.plan(runtime_spec(target))
|
|
61
|
+
|
|
62
|
+
def ensure(
|
|
63
|
+
self, target: DeployTarget, *, progress: Callable[[str, float], None] | None = None
|
|
64
|
+
) -> Path:
|
|
65
|
+
"""Build the runtime's venv if needed; return its Python interpreter."""
|
|
66
|
+
return self._envs.ensure(runtime_spec(target), progress=progress)
|