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,249 @@
1
+ """Inspect a model's metadata *without downloading its weights*.
2
+
3
+ Strategy, in order of preference:
4
+
5
+ 1. **safetensors headers** — exact parameter counts and dtypes, read from the
6
+ file headers only (a few KB over HTTP range requests on the Hub, or a local
7
+ header parse). This is the ideal path and never downloads multi-GB weights.
8
+ 2. **config.json** — architecture, layer/hidden dimensions, and (as a fallback)
9
+ an *estimated* parameter count derived from those dimensions.
10
+
11
+ The two never masquerade as each other: :attr:`ModelProfile.param_count_source`
12
+ records whether the count is exact or estimated.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import struct
19
+ from math import prod
20
+ from pathlib import Path
21
+
22
+ from zeroquantz.core.exceptions import DependencyError, ModelInspectionError
23
+ from zeroquantz.models import architecture as arch
24
+ from zeroquantz.models.metadata import ModelProfile, ParamCountSource
25
+ from zeroquantz.utils.logging import get_logger
26
+
27
+ log = get_logger(__name__)
28
+
29
+
30
+ class ModelInspector:
31
+ """Produce a :class:`ModelProfile` from a Hub id or a local directory."""
32
+
33
+ @staticmethod
34
+ def inspect(
35
+ model_id: str,
36
+ *,
37
+ revision: str | None = None,
38
+ token: str | None = None,
39
+ ) -> ModelProfile:
40
+ path = Path(model_id)
41
+ if path.exists() and path.is_dir():
42
+ return ModelInspector._inspect_local(path)
43
+ return ModelInspector._inspect_hub(model_id, revision=revision, token=token)
44
+
45
+ # ---- construction from an already-parsed config (pure, offline) ---------
46
+
47
+ @staticmethod
48
+ def from_config(
49
+ config: dict,
50
+ *,
51
+ model_id: str,
52
+ source: str = "hub",
53
+ exact_param_count: int | None = None,
54
+ dtype: str | None = None,
55
+ ) -> ModelProfile:
56
+ """Build a profile from a raw config dict. Used by the Hub/local paths and
57
+ directly by tests (no network, no weights)."""
58
+ dims = arch.extract_dimensions(config)
59
+
60
+ if exact_param_count is not None:
61
+ count = exact_param_count
62
+ source_kind = ParamCountSource.SAFETENSORS
63
+ else:
64
+ estimated = arch.estimate_parameter_count(config)
65
+ if estimated is None:
66
+ raise ModelInspectionError(
67
+ f"Could not determine a parameter count for '{model_id}'.",
68
+ detail=(
69
+ "No safetensors headers were available and the config did not "
70
+ "contain enough dimensions (layers, hidden size, heads, vocab) "
71
+ "to estimate one."
72
+ ),
73
+ )
74
+ count = estimated
75
+ source_kind = ParamCountSource.ESTIMATED
76
+
77
+ resolved_dtype = dtype or arch.normalize_st_dtype(
78
+ config.get("torch_dtype", "float16")
79
+ )
80
+
81
+ return ModelProfile(
82
+ model_id=model_id,
83
+ architecture=dims["architecture"],
84
+ model_type=dims["model_type"],
85
+ parameter_count=count,
86
+ param_count_source=source_kind,
87
+ dtype=resolved_dtype,
88
+ num_layers=dims["num_layers"],
89
+ hidden_size=dims["hidden_size"],
90
+ intermediate_size=dims["intermediate_size"],
91
+ num_attention_heads=dims["num_attention_heads"],
92
+ num_key_value_heads=dims["num_key_value_heads"],
93
+ head_dim=dims["head_dim"],
94
+ vocab_size=dims["vocab_size"],
95
+ max_position_embeddings=dims["max_position_embeddings"],
96
+ tie_word_embeddings=dims["tie_word_embeddings"],
97
+ source=source,
98
+ config=config,
99
+ )
100
+
101
+ # ---- Hub path -----------------------------------------------------------
102
+
103
+ @staticmethod
104
+ def _inspect_hub(
105
+ model_id: str, *, revision: str | None, token: str | None
106
+ ) -> ModelProfile:
107
+ try:
108
+ from huggingface_hub import hf_hub_download
109
+ from huggingface_hub.utils import (
110
+ EntryNotFoundError,
111
+ GatedRepoError,
112
+ RepositoryNotFoundError,
113
+ )
114
+ except ImportError as exc: # pragma: no cover - hf_hub is a base dep
115
+ raise DependencyError.for_extra(
116
+ "huggingface_hub", "torch", purpose="inspect models from the Hub"
117
+ ) from exc
118
+
119
+ # 1) config.json — cheap and gives us architecture + dimensions.
120
+ config: dict = {}
121
+ try:
122
+ config_path = hf_hub_download(
123
+ model_id, "config.json", revision=revision, token=token
124
+ )
125
+ config = json.loads(Path(config_path).read_text(encoding="utf-8"))
126
+ except GatedRepoError as exc:
127
+ raise ModelInspectionError(
128
+ f"'{model_id}' is a gated repository.",
129
+ detail="You must accept its license on the Hub and provide a token.",
130
+ suggestions=[
131
+ "huggingface-cli login",
132
+ f"open https://huggingface.co/{model_id} and accept the terms",
133
+ ],
134
+ ) from exc
135
+ except RepositoryNotFoundError as exc:
136
+ raise ModelInspectionError(
137
+ f"No model repository found for '{model_id}'.",
138
+ detail="Check the spelling, or whether the repo is private.",
139
+ suggestions=["zeroquantz inspect Qwen/Qwen3-8B"],
140
+ ) from exc
141
+ except EntryNotFoundError:
142
+ log.debug("no config.json for %s; relying on safetensors alone", model_id)
143
+ except Exception as exc:
144
+ raise ModelInspectionError(
145
+ f"Could not fetch metadata for '{model_id}'.",
146
+ detail=str(exc),
147
+ suggestions=["check your network connection", "verify the model id"],
148
+ ) from exc
149
+
150
+ # 2) safetensors headers — exact params + dtype, no weight download.
151
+ exact_count, dtype = ModelInspector._hub_safetensors_stats(
152
+ model_id, revision=revision, token=token
153
+ )
154
+
155
+ if not config and exact_count is None:
156
+ raise ModelInspectionError(
157
+ f"'{model_id}' exposes neither a config.json nor safetensors headers.",
158
+ detail="ZeroQuantz cannot inspect it without one of those.",
159
+ )
160
+
161
+ return ModelInspector.from_config(
162
+ config,
163
+ model_id=model_id,
164
+ source="hub",
165
+ exact_param_count=exact_count,
166
+ dtype=dtype,
167
+ )
168
+
169
+ @staticmethod
170
+ def _hub_safetensors_stats(
171
+ model_id: str, *, revision: str | None, token: str | None
172
+ ) -> tuple[int | None, str | None]:
173
+ try:
174
+ from huggingface_hub import get_safetensors_metadata
175
+ except ImportError:
176
+ return None, None
177
+ try:
178
+ meta = get_safetensors_metadata(model_id, revision=revision, token=token)
179
+ except Exception as exc:
180
+ log.debug("safetensors metadata unavailable for %s: %s", model_id, exc)
181
+ return None, None
182
+
183
+ by_dtype = getattr(meta, "parameter_count", None)
184
+ if by_dtype:
185
+ total = int(sum(by_dtype.values()))
186
+ dtype = arch.dominant_weight_dtype(by_dtype)
187
+ return total, dtype
188
+ return None, None
189
+
190
+ # ---- local path ---------------------------------------------------------
191
+
192
+ @staticmethod
193
+ def _inspect_local(directory: Path) -> ModelProfile:
194
+ config_file = directory / "config.json"
195
+ config: dict = {}
196
+ if config_file.exists():
197
+ config = json.loads(config_file.read_text(encoding="utf-8"))
198
+
199
+ exact_count, dtype = ModelInspector._local_safetensors_stats(directory)
200
+
201
+ if not config and exact_count is None:
202
+ raise ModelInspectionError(
203
+ f"'{directory}' contains neither config.json nor *.safetensors.",
204
+ suggestions=["point ZeroQuantz at a model directory or a Hub id"],
205
+ )
206
+
207
+ model_id = config.get("_name_or_path") or directory.name
208
+ return ModelInspector.from_config(
209
+ config,
210
+ model_id=str(model_id),
211
+ source="local",
212
+ exact_param_count=exact_count,
213
+ dtype=dtype,
214
+ )
215
+
216
+ @staticmethod
217
+ def _local_safetensors_stats(directory: Path) -> tuple[int | None, str | None]:
218
+ files = sorted(directory.glob("*.safetensors"))
219
+ if not files:
220
+ return None, None
221
+ counts_by_dtype: dict[str, int] = {}
222
+ for file in files:
223
+ for name, info in _read_safetensors_header(file).items():
224
+ if name == "__metadata__":
225
+ continue
226
+ shape = info.get("shape", [])
227
+ dtype = info.get("dtype", "F16")
228
+ counts_by_dtype[dtype] = counts_by_dtype.get(dtype, 0) + int(prod(shape))
229
+ if not counts_by_dtype:
230
+ return None, None
231
+ total = int(sum(counts_by_dtype.values()))
232
+ return total, arch.dominant_weight_dtype(counts_by_dtype)
233
+
234
+
235
+ def _read_safetensors_header(path: Path) -> dict:
236
+ """Parse only the JSON header of a .safetensors file (no tensor data).
237
+
238
+ Layout: ``<u64 header_len><header_len bytes of JSON>``.
239
+ """
240
+ with path.open("rb") as fh:
241
+ raw_len = fh.read(8)
242
+ if len(raw_len) < 8:
243
+ return {}
244
+ (header_len,) = struct.unpack("<Q", raw_len)
245
+ header_bytes = fh.read(header_len)
246
+ try:
247
+ return json.loads(header_bytes.decode("utf-8"))
248
+ except (UnicodeDecodeError, json.JSONDecodeError):
249
+ return {}
@@ -0,0 +1,108 @@
1
+ """The :class:`ModelProfile` domain model — everything ZeroQuantz knows about a
2
+ model *without* having loaded its weights."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import Enum
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from zeroquantz.utils import units
11
+
12
+
13
+ class ParamCountSource(str, Enum):
14
+ """How ``parameter_count`` was determined — never conflate the two."""
15
+
16
+ SAFETENSORS = "safetensors" # exact, summed from tensor headers
17
+ ESTIMATED = "estimated" # derived from config dimensions
18
+
19
+
20
+ class ModelProfile(BaseModel):
21
+ """Static, weight-free description of a causal language model.
22
+
23
+ ``estimated_weight_size_gb`` is always an estimate of the *original* footprint
24
+ at ``dtype``. ``parameter_count`` may be exact (from safetensors headers) or
25
+ estimated (from config) — ``param_count_source`` says which.
26
+ """
27
+
28
+ model_id: str
29
+ architecture: str | None = None
30
+ model_type: str | None = None
31
+ parameter_count: int = Field(ge=0)
32
+ param_count_source: ParamCountSource = ParamCountSource.ESTIMATED
33
+ dtype: str = "float16"
34
+
35
+ num_layers: int | None = None
36
+ hidden_size: int | None = None
37
+ intermediate_size: int | None = None
38
+ num_attention_heads: int | None = None
39
+ num_key_value_heads: int | None = None
40
+ head_dim: int | None = None
41
+ vocab_size: int | None = None
42
+ max_position_embeddings: int | None = None
43
+ tie_word_embeddings: bool = True
44
+
45
+ source: str = "hub" # "hub" or "local"
46
+ config: dict = Field(default_factory=dict, repr=False)
47
+
48
+ # ---- derived sizes ------------------------------------------------------
49
+
50
+ @property
51
+ def estimated_weight_size_gb(self) -> float:
52
+ """Footprint of all parameters at the model's native ``dtype`` (GiB)."""
53
+ return round(units.param_gb(self.parameter_count, self.dtype), 2)
54
+
55
+ def size_gb_at(self, precision: str) -> float:
56
+ """Footprint if *every* parameter were stored at ``precision`` (GiB)."""
57
+ return round(units.param_gb(self.parameter_count, precision), 2)
58
+
59
+ @property
60
+ def embedding_params(self) -> int:
61
+ """Input embedding parameter count (``vocab * hidden``), 0 if unknown."""
62
+ if self.vocab_size and self.hidden_size:
63
+ return self.vocab_size * self.hidden_size
64
+ return 0
65
+
66
+ @property
67
+ def lm_head_params(self) -> int:
68
+ """Output projection params — 0 when weights are tied to the embedding."""
69
+ if self.tie_word_embeddings:
70
+ return 0
71
+ return self.embedding_params
72
+
73
+ @property
74
+ def non_quantizable_params(self) -> int:
75
+ """Params that quantization backends keep at base precision by default.
76
+
77
+ Embeddings and (when untied) the LM head. Norm/bias params are negligible
78
+ and folded into the quantizable remainder.
79
+ """
80
+ return self.embedding_params + self.lm_head_params
81
+
82
+ @property
83
+ def quantizable_params(self) -> int:
84
+ """Linear-layer params that a weight-only backend will actually quantize."""
85
+ return max(self.parameter_count - self.non_quantizable_params, 0)
86
+
87
+ @property
88
+ def is_estimated(self) -> bool:
89
+ return self.param_count_source is ParamCountSource.ESTIMATED
90
+
91
+ def summary_rows(self) -> list[tuple[str, str]]:
92
+ """(label, value) pairs for tabular display."""
93
+ rows = [
94
+ ("Architecture", self.architecture or "unknown"),
95
+ ("Parameters", units.humanize_params(self.parameter_count)),
96
+ ("Original dtype", self.dtype.upper()),
97
+ ("Estimated weight size", f"{self.estimated_weight_size_gb:.1f} GB"),
98
+ ]
99
+ if self.num_layers is not None:
100
+ rows.append(("Layers", str(self.num_layers)))
101
+ if self.hidden_size is not None:
102
+ rows.append(("Hidden size", str(self.hidden_size)))
103
+ if self.num_attention_heads is not None:
104
+ heads = str(self.num_attention_heads)
105
+ if self.num_key_value_heads and self.num_key_value_heads != self.num_attention_heads:
106
+ heads += f" (KV: {self.num_key_value_heads})"
107
+ rows.append(("Attention heads", heads))
108
+ return rows
@@ -0,0 +1,71 @@
1
+ """Live Hugging Face Hub model search (metadata only).
2
+
3
+ Used by the model-selection screen to offer a popularity-ranked list of matching
4
+ text-generation models as the user types. Network-dependent and imported lazily;
5
+ callers handle the offline case.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from zeroquantz.utils import units
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ModelHit:
17
+ """One search result from the Hub."""
18
+
19
+ id: str
20
+ downloads: int | None = None
21
+ likes: int | None = None
22
+ pipeline_tag: str | None = None
23
+
24
+ def meta_line(self) -> str:
25
+ bits: list[str] = []
26
+ if self.downloads is not None:
27
+ bits.append(f"↓ {units.humanize_params(self.downloads)}")
28
+ if self.likes is not None:
29
+ bits.append(f"♥ {self.likes}")
30
+ if self.pipeline_tag:
31
+ bits.append(self.pipeline_tag)
32
+ return " ".join(bits)
33
+
34
+
35
+ def search_models(query: str, *, limit: int = 20, token: str | None = None) -> list[ModelHit]:
36
+ """Return Hub models matching ``query``, most-downloaded first.
37
+
38
+ Raises on network/Hub errors so the caller can distinguish "offline" from
39
+ "no matches".
40
+ """
41
+ from huggingface_hub import HfApi
42
+
43
+ api = HfApi(token=token)
44
+ # list_models()'s accepted kwargs vary across huggingface_hub versions; try the
45
+ # richest signature first and fall back until one is accepted.
46
+ attempts = (
47
+ {"search": query, "sort": "downloads", "direction": -1, "limit": limit, "pipeline_tag": "text-generation"},
48
+ {"search": query, "sort": "downloads", "limit": limit, "pipeline_tag": "text-generation"},
49
+ {"search": query, "sort": "downloads", "limit": limit, "filter": "text-generation"},
50
+ {"search": query, "sort": "downloads", "limit": limit},
51
+ {"search": query, "limit": limit},
52
+ )
53
+ last_error: Exception | None = None
54
+ for kwargs in attempts:
55
+ try:
56
+ infos = list(api.list_models(**kwargs))
57
+ except TypeError as exc: # unsupported kwarg on this version
58
+ last_error = exc
59
+ continue
60
+ return [
61
+ ModelHit(
62
+ id=info.id,
63
+ downloads=getattr(info, "downloads", None),
64
+ likes=getattr(info, "likes", None),
65
+ pipeline_tag=getattr(info, "pipeline_tag", None),
66
+ )
67
+ for info in infos
68
+ ]
69
+ if last_error is not None:
70
+ raise last_error
71
+ return []
@@ -0,0 +1,22 @@
1
+ """Optimization goals, candidate generation, ranking, and planning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.optimization.candidate import CandidateGenerator, QuantizationCandidate
6
+ from zeroquantz.optimization.constraints import Objective, OptimizationGoal
7
+ from zeroquantz.optimization.pareto import dominates, pareto_frontier
8
+ from zeroquantz.optimization.planner import MixedPrecisionPlan, MixedPrecisionPlanner
9
+ from zeroquantz.optimization.recommender import Recommendation, Recommender
10
+
11
+ __all__ = [
12
+ "CandidateGenerator",
13
+ "MixedPrecisionPlan",
14
+ "MixedPrecisionPlanner",
15
+ "Objective",
16
+ "OptimizationGoal",
17
+ "QuantizationCandidate",
18
+ "Recommendation",
19
+ "Recommender",
20
+ "dominates",
21
+ "pareto_frontier",
22
+ ]