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,154 @@
1
+ """Deployment targets: the runtimes ZeroQuantz can generate serve/deploy assets for.
2
+
3
+ Each target maps a quantization format to the right serve command, the correct
4
+ ``--quantization`` flag, a container image, and (for OpenAI-compatible servers) a
5
+ client snippet. Templates are best-effort against current runtime CLIs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from zeroquantz.quantization.catalog import QuantFormat
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DeployTarget:
20
+ name: str # matches catalog runtime names
21
+ label: str
22
+ kind: str # "server" | "library" | "gguf"
23
+ default_port: int
24
+ openai_compatible: bool
25
+ container_image: str | None
26
+ install_hint: str
27
+ #: format.family -> serving --quantization flag (None = auto-detected / not needed)
28
+ quant_flags: dict[str, str | None] = field(default_factory=dict)
29
+ serve_template: Callable[[str, QuantFormat, int, str | None], str] | None = None
30
+ note: str = ""
31
+ # ---- isolated sub-environment (vLLM / SGLang / TensorRT-LLM) ----
32
+ #: When True, this runtime is provisioned in its own venv under
33
+ #: ~/.zeroquantz/envs/serve/<name>/ (its stack conflicts with the others).
34
+ isolated_env: bool = False
35
+ #: pip packages that install the runtime into that env.
36
+ pip: tuple[str, ...] = ()
37
+ #: extra package index (e.g. NVIDIA's PyPI for TensorRT-LLM).
38
+ extra_index_url: str | None = None
39
+ #: Python version for the venv.
40
+ env_python: str = "3.11"
41
+
42
+ def quant_flag(self, fmt: QuantFormat | None) -> str | None:
43
+ if fmt is not None and fmt.family in self.quant_flags:
44
+ return self.quant_flags[fmt.family]
45
+ return None
46
+
47
+ def serve_command(
48
+ self, model_path: str, fmt: QuantFormat | None = None, *, port: int | None = None
49
+ ) -> str:
50
+ port = port or self.default_port
51
+ flag = self.quant_flag(fmt)
52
+ return self.serve_template(model_path, fmt, port, flag) if self.serve_template else ""
53
+
54
+
55
+ # ---- serve-command templates ------------------------------------------------
56
+
57
+
58
+ def _vllm(path: str, fmt: QuantFormat, port: int, flag: str | None) -> str:
59
+ cmd = f"vllm serve {path}"
60
+ if flag:
61
+ cmd += f" --quantization {flag}"
62
+ return cmd + f" --max-model-len 4096 --port {port}"
63
+
64
+
65
+ def _sglang(path: str, fmt: QuantFormat, port: int, flag: str | None) -> str:
66
+ cmd = f"python -m sglang.launch_server --model-path {path}"
67
+ if flag:
68
+ cmd += f" --quantization {flag}"
69
+ return cmd + f" --host 0.0.0.0 --port {port}"
70
+
71
+
72
+ def _trtllm(path: str, fmt: QuantFormat, port: int, flag: str | None) -> str:
73
+ return f"trtllm-serve {path} --backend pytorch --host 0.0.0.0 --port {port}"
74
+
75
+
76
+ def _transformers(path: str, fmt: QuantFormat, port: int, flag: str | None) -> str:
77
+ return (
78
+ "python -c \"from transformers import AutoModelForCausalLM, AutoTokenizer; "
79
+ f"m=AutoModelForCausalLM.from_pretrained('{path}', device_map='auto'); "
80
+ f"t=AutoTokenizer.from_pretrained('{path}'); print('loaded', m.config.model_type)\""
81
+ )
82
+
83
+
84
+ def _llamacpp(path: str, fmt: QuantFormat, port: int, flag: str | None) -> str:
85
+ return f"llama-server -m {path} --host 0.0.0.0 --port {port} -c 4096 -ngl 999"
86
+
87
+
88
+ def _ollama(path: str, fmt: QuantFormat, port: int, flag: str | None) -> str:
89
+ return "ollama create zeroquantz-model -f Modelfile && ollama run zeroquantz-model"
90
+
91
+
92
+ # ---- the targets ------------------------------------------------------------
93
+
94
+ _VLLM_FLAGS = {
95
+ "awq": "awq_marlin", "gptq": "gptq_marlin", "autoround": "gptq_marlin",
96
+ "fp8": "fp8", "compressed_tensors": None, "torchao": "torchao",
97
+ "bitsandbytes": "bitsandbytes", "mxfp4": "mxfp4", "nvfp4": "modelopt_fp4",
98
+ "hqq": "hqq", "aqlm": "aqlm",
99
+ }
100
+ _SGLANG_FLAGS = {
101
+ "awq": "awq_marlin", "gptq": "gptq_marlin", "autoround": "gptq_marlin",
102
+ "fp8": "fp8", "compressed_tensors": "w8a8_int8", "nvfp4": "modelopt_fp4",
103
+ "mxfp4": "mxfp4",
104
+ }
105
+ _TRT_FLAGS = {"fp8": "fp8", "nvfp4": "nvfp4", "awq": "int4_awq", "gptq": "int4_gptq"}
106
+
107
+ _TARGETS: dict[str, DeployTarget] = {
108
+ "vllm": DeployTarget(
109
+ "vllm", "vLLM", "server", 8000, True, "vllm/vllm-openai:latest",
110
+ "pip install vllm (Linux + CUDA)", _VLLM_FLAGS, _vllm,
111
+ note="High-throughput OpenAI-compatible server.",
112
+ isolated_env=True, pip=("vllm",), env_python="3.12",
113
+ ),
114
+ "sglang": DeployTarget(
115
+ "sglang", "SGLang", "server", 30000, True, "lmsysorg/sglang:latest",
116
+ "pip install \"sglang[all]\" (Linux + CUDA)", _SGLANG_FLAGS, _sglang,
117
+ note="High-throughput server with RadixAttention.",
118
+ isolated_env=True, pip=("sglang[all]",), env_python="3.12",
119
+ ),
120
+ "tensorrt_llm": DeployTarget(
121
+ "tensorrt_llm", "TensorRT-LLM", "server", 8000, True,
122
+ "nvcr.io/nvidia/tritonserver:24.10-trtllm-python-py3",
123
+ "pip install tensorrt-llm (Linux; FP4 needs Blackwell)", _TRT_FLAGS, _trtllm,
124
+ note="NVIDIA's fastest inference; FP8/FP4 on recent GPUs.",
125
+ isolated_env=True, pip=("tensorrt-llm",),
126
+ extra_index_url="https://pypi.nvidia.com", env_python="3.12",
127
+ ),
128
+ "transformers": DeployTarget(
129
+ "transformers", "Transformers", "library", 0, False, "pytorch/pytorch:latest",
130
+ "pip install transformers accelerate", {}, _transformers,
131
+ note="Library load (not a server); good for scripts / TGI.",
132
+ ),
133
+ "llamacpp": DeployTarget(
134
+ "llamacpp", "llama.cpp", "gguf", 8080, True, "ghcr.io/ggml-org/llama.cpp:server",
135
+ "download a llama.cpp release (prebuilt binaries)", {}, _llamacpp,
136
+ note="Runs GGUF on CPU + many GPUs; OpenAI-compatible /v1.",
137
+ ),
138
+ "ollama": DeployTarget(
139
+ "ollama", "Ollama", "gguf", 11434, True, "ollama/ollama:latest",
140
+ "install Ollama from ollama.com", {}, _ollama,
141
+ note="Simplest local GGUF serving.",
142
+ ),
143
+ }
144
+
145
+
146
+ def get_target(name: str) -> DeployTarget | None:
147
+ return _TARGETS.get(name)
148
+
149
+
150
+ def deploy_targets_for(fmt: QuantFormat) -> list[DeployTarget]:
151
+ """Targets that can serve ``fmt`` (intersection of the format's runtimes and
152
+ the targets we can generate assets for)."""
153
+ targets = [_TARGETS[r] for r in fmt.runtimes if r in _TARGETS]
154
+ return targets or [_TARGETS["transformers"]]
@@ -0,0 +1,8 @@
1
+ """Export quantized models and generate ZeroQuantz reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.export.exporter import Exporter
6
+ from zeroquantz.export.report import QuantizationReport, build_report
7
+
8
+ __all__ = ["Exporter", "QuantizationReport", "build_report"]
@@ -0,0 +1,68 @@
1
+ """Export orchestration: write a quantized model + report, safely.
2
+
3
+ A partially-written export is marked with a ``.zeroquantz_incomplete`` sentinel
4
+ that is only removed once everything has been written, so an interrupted export
5
+ never masquerades as a finished model (spec §22).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from zeroquantz.core.exceptions import ZeroQuantzError
14
+ from zeroquantz.utils.logging import get_logger
15
+
16
+ if TYPE_CHECKING:
17
+ from zeroquantz.export.report import QuantizationReport
18
+ from zeroquantz.quantization.base import QuantizationBackend, QuantizationResult
19
+
20
+ log = get_logger(__name__)
21
+
22
+ _INCOMPLETE_MARKER = ".zeroquantz_incomplete"
23
+
24
+
25
+ class Exporter:
26
+ """Write models and reports to disk with completeness guarantees."""
27
+
28
+ @staticmethod
29
+ def write_report(report: QuantizationReport, output_dir: str | Path) -> tuple[Path, Path]:
30
+ """Write just the JSON + Markdown report (no model weights)."""
31
+ return report.write(output_dir)
32
+
33
+ @staticmethod
34
+ def export_model(
35
+ result: QuantizationResult,
36
+ output_dir: str | Path,
37
+ backend: QuantizationBackend,
38
+ *,
39
+ report: QuantizationReport | None = None,
40
+ ) -> Path:
41
+ """Write the quantized model (and optional report) to ``output_dir``."""
42
+ out = Path(output_dir)
43
+ out.mkdir(parents=True, exist_ok=True)
44
+ marker = out / _INCOMPLETE_MARKER
45
+ marker.write_text("export in progress\n", encoding="utf-8")
46
+
47
+ try:
48
+ backend.export(result, str(out))
49
+ if report is not None:
50
+ report.model_size_after_gb = (
51
+ result.quantized_size_gb or report.model_size_after_gb
52
+ )
53
+ report.write(out)
54
+ except ZeroQuantzError:
55
+ raise
56
+ except Exception as exc:
57
+ raise ZeroQuantzError(
58
+ f"Failed to export the quantized model to '{out}'.",
59
+ detail=str(exc),
60
+ suggestions=["ensure the target directory is writable"],
61
+ ) from exc
62
+ else:
63
+ marker.unlink(missing_ok=True)
64
+ return out
65
+
66
+ @staticmethod
67
+ def is_incomplete(directory: str | Path) -> bool:
68
+ return (Path(directory) / _INCOMPLETE_MARKER).exists()
@@ -0,0 +1,203 @@
1
+ """The ZeroQuantz report: ``zeroquantz_report.json`` (machine-readable) and
2
+ ``zeroquantz_report.md`` (human-readable, HF-card friendly)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from zeroquantz.benchmark.runner import BenchmarkResult
13
+ from zeroquantz.quantization.config import QuantizationConfig
14
+ from zeroquantz.utils import units
15
+ from zeroquantz.utils.env import capture_environment, utc_timestamp
16
+ from zeroquantz.verification.report import VerificationReport
17
+
18
+ if TYPE_CHECKING:
19
+ from zeroquantz.hardware.capabilities import HardwareProfile
20
+ from zeroquantz.models.metadata import ModelProfile
21
+ from zeroquantz.optimization.constraints import OptimizationGoal
22
+ from zeroquantz.optimization.planner import MixedPrecisionPlan
23
+
24
+
25
+ class QuantizationReport(BaseModel):
26
+ """Everything worth recording about a quantization run."""
27
+
28
+ base_model: str
29
+ architecture: str | None = None
30
+ model_type: str | None = None
31
+ parameter_count: int = 0
32
+ original_dtype: str | None = None
33
+
34
+ backend: str | None = None
35
+ method: str | None = None
36
+ quantization_config: dict = Field(default_factory=dict)
37
+ calibration: dict = Field(default_factory=dict)
38
+
39
+ model_size_before_gb: float | None = None
40
+ model_size_after_gb: float | None = None
41
+ peak_vram_gb: float | None = None
42
+
43
+ runtime_target: str | None = None
44
+ goal: dict = Field(default_factory=dict)
45
+ plan: dict | None = None
46
+
47
+ benchmark: BenchmarkResult | None = None
48
+ verification: VerificationReport | None = None
49
+
50
+ zeroquantz_version: str = ""
51
+ timestamp: str = ""
52
+ environment: dict = Field(default_factory=dict)
53
+
54
+ # ---- serialization ------------------------------------------------------
55
+
56
+ def to_json(self, *, indent: int = 2) -> str:
57
+ return json.dumps(self.model_dump(mode="json"), indent=indent)
58
+
59
+ def to_markdown(self) -> str:
60
+ lines: list[str] = []
61
+ a = lines.append
62
+ a(f"# ZeroQuantz Report — {self.base_model}")
63
+ a("")
64
+ a(f"*Generated {self.timestamp} by ZeroQuantz {self.zeroquantz_version}*")
65
+ a("")
66
+
67
+ a("## Model")
68
+ a("")
69
+ a("| Field | Value |")
70
+ a("| --- | --- |")
71
+ a(f"| Base model | `{self.base_model}` |")
72
+ if self.architecture:
73
+ a(f"| Architecture | {self.architecture} |")
74
+ if self.parameter_count:
75
+ a(f"| Parameters | {units.humanize_params(self.parameter_count)} |")
76
+ if self.original_dtype:
77
+ a(f"| Original dtype | {self.original_dtype.upper()} |")
78
+ a("")
79
+
80
+ a("## Quantization")
81
+ a("")
82
+ a("| Field | Value |")
83
+ a("| --- | --- |")
84
+ if self.backend:
85
+ a(f"| Backend | {self.backend} |")
86
+ if self.method:
87
+ a(f"| Method | {self.method} |")
88
+ if self.runtime_target:
89
+ a(f"| Runtime target | {self.runtime_target} |")
90
+ if self.model_size_before_gb is not None:
91
+ a(f"| Size before | {self.model_size_before_gb:.2f} GB |")
92
+ if self.model_size_after_gb is not None:
93
+ a(f"| Size after | {self.model_size_after_gb:.2f} GB |")
94
+ if self.peak_vram_gb is not None:
95
+ a(f"| Peak VRAM | {self.peak_vram_gb:.2f} GB |")
96
+ a("")
97
+ if self.quantization_config:
98
+ a("<details><summary>Full quantization config</summary>")
99
+ a("")
100
+ a("```json")
101
+ a(json.dumps(self.quantization_config, indent=2))
102
+ a("```")
103
+ a("</details>")
104
+ a("")
105
+
106
+ if self.plan:
107
+ a("## Mixed-precision plan")
108
+ a("")
109
+ a("| Modules | Precision |")
110
+ a("| --- | --- |")
111
+ for name, prec in self.plan.get("groups", []):
112
+ a(f"| {name} | {prec} |")
113
+ a("")
114
+
115
+ if self.benchmark is not None:
116
+ a("## Benchmark (measured)")
117
+ a("")
118
+ a("| Metric | Value |")
119
+ a("| --- | --- |")
120
+ for label, value in self.benchmark.summary_rows():
121
+ a(f"| {label} | {value} |")
122
+ a("")
123
+
124
+ if self.verification is not None:
125
+ a("## Verification")
126
+ a("")
127
+ a("| Metric | Value |")
128
+ a("| --- | --- |")
129
+ for label, value in self.verification.summary_rows():
130
+ a(f"| {label} | {value} |")
131
+ a("")
132
+
133
+ a("## Environment")
134
+ a("")
135
+ a("```json")
136
+ a(json.dumps(self.environment, indent=2))
137
+ a("```")
138
+ a("")
139
+ return "\n".join(lines)
140
+
141
+ def write(self, output_dir: str | Path) -> tuple[Path, Path]:
142
+ out = Path(output_dir)
143
+ out.mkdir(parents=True, exist_ok=True)
144
+ json_path = out / "zeroquantz_report.json"
145
+ md_path = out / "zeroquantz_report.md"
146
+ json_path.write_text(self.to_json(), encoding="utf-8")
147
+ md_path.write_text(self.to_markdown(), encoding="utf-8")
148
+ return json_path, md_path
149
+
150
+
151
+ def build_report(
152
+ model: ModelProfile,
153
+ *,
154
+ config: QuantizationConfig | None = None,
155
+ hardware: HardwareProfile | None = None,
156
+ goal: OptimizationGoal | None = None,
157
+ plan: MixedPrecisionPlan | None = None,
158
+ benchmark: BenchmarkResult | None = None,
159
+ verification: VerificationReport | None = None,
160
+ size_after_gb: float | None = None,
161
+ peak_vram_gb: float | None = None,
162
+ calibration: dict | None = None,
163
+ ) -> QuantizationReport:
164
+ """Assemble a :class:`QuantizationReport` from the pieces ZeroQuantz has."""
165
+ from zeroquantz import __version__
166
+
167
+ plan_dict = None
168
+ if plan is not None:
169
+ plan_dict = {
170
+ "base_precision": plan.base_precision,
171
+ "low_precision": plan.low_precision,
172
+ "high_precision": plan.high_precision,
173
+ "estimated_size_gb": plan.estimated_size_gb,
174
+ "estimated_vram_gb": plan.estimated_vram_gb,
175
+ "groups": plan.grouped_rows(),
176
+ }
177
+
178
+ return QuantizationReport(
179
+ base_model=model.model_id,
180
+ architecture=model.architecture,
181
+ model_type=model.model_type,
182
+ parameter_count=model.parameter_count,
183
+ original_dtype=model.dtype,
184
+ backend=config.backend if config else None,
185
+ method=config.method if config else None,
186
+ quantization_config=config.model_dump(mode="json") if config else {},
187
+ calibration=calibration or {},
188
+ model_size_before_gb=model.estimated_weight_size_gb,
189
+ model_size_after_gb=size_after_gb
190
+ if size_after_gb is not None
191
+ else (benchmark.model_size_gb if benchmark else None),
192
+ peak_vram_gb=peak_vram_gb
193
+ if peak_vram_gb is not None
194
+ else (benchmark.peak_vram_gb if benchmark else None),
195
+ runtime_target=goal.runtime if goal else None,
196
+ goal=goal.model_dump(mode="json") if goal else {},
197
+ plan=plan_dict,
198
+ benchmark=benchmark,
199
+ verification=verification,
200
+ zeroquantz_version=__version__,
201
+ timestamp=utc_timestamp(),
202
+ environment=capture_environment(hardware),
203
+ )
@@ -0,0 +1,15 @@
1
+ """Hardware detection and capability modelling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.hardware.capabilities import HardwareProfile, PrecisionStatus, derive_precision_support
6
+ from zeroquantz.hardware.detector import HardwareDetector
7
+ from zeroquantz.hardware.gpu import GpuInfo
8
+
9
+ __all__ = [
10
+ "GpuInfo",
11
+ "HardwareDetector",
12
+ "HardwareProfile",
13
+ "PrecisionStatus",
14
+ "derive_precision_support",
15
+ ]
@@ -0,0 +1,152 @@
1
+ """Hardware capability modelling: which numeric precisions a device supports.
2
+
3
+ This is the single home for "does this GPU support precision X" reasoning. Both
4
+ the ``/hardware`` view and the candidate generator consult it, so support rules
5
+ live here rather than being scattered across the codebase.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+ from zeroquantz.hardware.gpu import GpuInfo
15
+
16
+ # The set of precisions ZeroQuantz reasons about, in display order.
17
+ PRECISIONS: tuple[str, ...] = ("fp32", "fp16", "bf16", "int8", "int4", "fp8")
18
+
19
+
20
+ class PrecisionStatus(str, Enum):
21
+ """How well a precision is supported on a given device."""
22
+
23
+ SUPPORTED = "supported"
24
+ LIMITED = "limited"
25
+ UNSUPPORTED = "unsupported"
26
+
27
+ @property
28
+ def label(self) -> str:
29
+ return {"supported": "yes", "limited": "limited", "unsupported": "no"}[self.value]
30
+
31
+ def __bool__(self) -> bool: # truthy if usable at all
32
+ return self is not PrecisionStatus.UNSUPPORTED
33
+
34
+
35
+ def derive_precision_support(
36
+ compute_capability: tuple[int, int] | None,
37
+ *,
38
+ cuda_available: bool,
39
+ ) -> dict[str, PrecisionStatus]:
40
+ """Map compute capability -> per-precision :class:`PrecisionStatus`.
41
+
42
+ Rules (CUDA):
43
+ * fp32/fp16 — supported on every supported CUDA GPU.
44
+ * bf16 — native on Ampere+ (SM >= 8.0); limited (emulated) below.
45
+ * int8 — supported from Turing/late-Pascal (SM >= 6.1).
46
+ * int4 — supported from Turing (SM >= 7.5) via weight-only kernels.
47
+ * fp8 — full on Hopper+ (SM >= 9.0), limited on Ada (SM 8.9),
48
+ unsupported below.
49
+
50
+ Without CUDA everything but fp32 is unsupported (CPU int8 exists but is out of
51
+ scope for v0.1's CUDA-first target).
52
+ """
53
+ S, L, U = (
54
+ PrecisionStatus.SUPPORTED,
55
+ PrecisionStatus.LIMITED,
56
+ PrecisionStatus.UNSUPPORTED,
57
+ )
58
+
59
+ if not cuda_available or compute_capability is None:
60
+ return {"fp32": S, "fp16": L, "bf16": U, "int8": U, "int4": U, "fp8": U}
61
+
62
+ major, minor = compute_capability
63
+ cc = major * 10 + minor # e.g. (8, 9) -> 89
64
+
65
+ return {
66
+ "fp32": S,
67
+ "fp16": S if cc >= 60 else L,
68
+ "bf16": S if cc >= 80 else L,
69
+ "int8": S if cc >= 61 else L,
70
+ "int4": S if cc >= 75 else (L if cc >= 61 else U),
71
+ "fp8": S if cc >= 90 else (L if cc >= 89 else U),
72
+ }
73
+
74
+
75
+ class HardwareProfile(BaseModel):
76
+ """Detected hardware plus its precision capabilities.
77
+
78
+ The spec-shaped scalar fields (``gpu_name``, ``total_vram_gb``,
79
+ ``compute_capability``, ``cuda_version``, ``supports_*``) are exposed as
80
+ properties derived from the primary GPU, while ``gpus`` and
81
+ ``precision_support`` carry the full detail.
82
+ """
83
+
84
+ gpus: list[GpuInfo] = Field(default_factory=list)
85
+ cuda_available: bool = False
86
+ cuda_version: str | None = None
87
+ driver_version: str | None = None
88
+ torch_version: str | None = None
89
+ cpu_cores: int | None = None
90
+ system_ram_gb: float | None = None
91
+ precision_support: dict[str, PrecisionStatus] = Field(default_factory=dict)
92
+ notes: list[str] = Field(default_factory=list)
93
+
94
+ # ---- primary-GPU conveniences (match the spec's flat shape) -------------
95
+
96
+ @property
97
+ def primary_gpu(self) -> GpuInfo | None:
98
+ return self.gpus[0] if self.gpus else None
99
+
100
+ @property
101
+ def gpu_name(self) -> str:
102
+ gpu = self.primary_gpu
103
+ return gpu.name if gpu else "CPU (no CUDA device)"
104
+
105
+ @property
106
+ def gpu_count(self) -> int:
107
+ return len(self.gpus)
108
+
109
+ @property
110
+ def total_vram_gb(self) -> float:
111
+ return self.primary_gpu.total_vram_gb if self.primary_gpu else 0.0
112
+
113
+ @property
114
+ def available_vram_gb(self) -> float | None:
115
+ return self.primary_gpu.available_vram_gb if self.primary_gpu else None
116
+
117
+ @property
118
+ def compute_capability(self) -> tuple[int, int] | None:
119
+ return self.primary_gpu.compute_capability if self.primary_gpu else None
120
+
121
+ @property
122
+ def sm(self) -> str | None:
123
+ return self.primary_gpu.sm if self.primary_gpu else None
124
+
125
+ # ---- precision helpers --------------------------------------------------
126
+
127
+ def status(self, precision: str) -> PrecisionStatus:
128
+ return self.precision_support.get(precision, PrecisionStatus.UNSUPPORTED)
129
+
130
+ def supports(self, precision: str) -> bool:
131
+ """True if ``precision`` is usable at all (supported or limited)."""
132
+ return bool(self.status(precision))
133
+
134
+ @property
135
+ def supports_fp16(self) -> bool:
136
+ return self.supports("fp16")
137
+
138
+ @property
139
+ def supports_bf16(self) -> bool:
140
+ return self.supports("bf16")
141
+
142
+ @property
143
+ def supports_int8(self) -> bool:
144
+ return self.supports("int8")
145
+
146
+ @property
147
+ def supports_int4(self) -> bool:
148
+ return self.supports("int4")
149
+
150
+ @property
151
+ def supports_fp8(self) -> bool:
152
+ return self.supports("fp8")