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