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