weightlab 0.1.0a1__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 (57) hide show
  1. weightlab/__init__.py +23 -0
  2. weightlab/analysis/__init__.py +0 -0
  3. weightlab/analysis/accumulate.py +311 -0
  4. weightlab/analysis/change_report.py +669 -0
  5. weightlab/analysis/compat.py +146 -0
  6. weightlab/analysis/diff.py +93 -0
  7. weightlab/analysis/lineage.py +245 -0
  8. weightlab/analysis/lora.py +483 -0
  9. weightlab/analysis/pyramid.py +779 -0
  10. weightlab/analysis/report_io.py +374 -0
  11. weightlab/analysis/roi.py +128 -0
  12. weightlab/analysis/roles.py +75 -0
  13. weightlab/analysis/sampling.py +221 -0
  14. weightlab/analysis/tile.py +516 -0
  15. weightlab/analysis/tile_codec.py +311 -0
  16. weightlab/api/__init__.py +1 -0
  17. weightlab/api/app.py +466 -0
  18. weightlab/api/catalog.py +327 -0
  19. weightlab/api/config.py +80 -0
  20. weightlab/api/errors.py +89 -0
  21. weightlab/api/relations.py +321 -0
  22. weightlab/api/roi.py +221 -0
  23. weightlab/api/schemas.py +295 -0
  24. weightlab/api/security.py +203 -0
  25. weightlab/api/server.py +46 -0
  26. weightlab/api/tiles.py +509 -0
  27. weightlab/cache.py +509 -0
  28. weightlab/cli.py +224 -0
  29. weightlab/core/__init__.py +0 -0
  30. weightlab/core/errors.py +40 -0
  31. weightlab/core/limits.py +29 -0
  32. weightlab/core/memory.py +121 -0
  33. weightlab/db/__init__.py +44 -0
  34. weightlab/db/migrations/0001_initial.sql +121 -0
  35. weightlab/db/migrations/0002_model_files_ctime.sql +11 -0
  36. weightlab/db/migrations/__init__.py +1 -0
  37. weightlab/db/schema.py +292 -0
  38. weightlab/db/store.py +418 -0
  39. weightlab/formats/__init__.py +0 -0
  40. weightlab/formats/base.py +83 -0
  41. weightlab/formats/checkpoint.py +497 -0
  42. weightlab/formats/dtypes.py +88 -0
  43. weightlab/formats/fileio.py +168 -0
  44. weightlab/formats/safetensors_reader.py +120 -0
  45. weightlab/formats/selection.py +140 -0
  46. weightlab/formats/shards.py +78 -0
  47. weightlab/jobs.py +386 -0
  48. weightlab/schemas/change_report.schema.json +101 -0
  49. weightlab/ui/__init__.py +72 -0
  50. weightlab/ui/static/assets/index-DQKuRsGl.js +9 -0
  51. weightlab/ui/static/assets/index-hkYppvgX.css +1 -0
  52. weightlab/ui/static/favicon.svg +1 -0
  53. weightlab/ui/static/index.html +17 -0
  54. weightlab-0.1.0a1.dist-info/METADATA +268 -0
  55. weightlab-0.1.0a1.dist-info/RECORD +57 -0
  56. weightlab-0.1.0a1.dist-info/WHEEL +4 -0
  57. weightlab-0.1.0a1.dist-info/entry_points.txt +3 -0
weightlab/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """WeightLab: local-first, CPU-only LLM checkpoint explorer.
2
+
3
+ Supports strict SafeTensors reads, exact ROI values, local heatmaps, and
4
+ checkpoint/LoRA change reports. The graphical comparison UI is still planned.
5
+ """
6
+
7
+ from weightlab.core.errors import ErrorCode, WeightLabError
8
+ from weightlab.formats.base import Consistency, TensorMeta
9
+ from weightlab.formats.checkpoint import Checkpoint, open_checkpoint
10
+
11
+ __version__ = "0.1.0a1"
12
+ PARSER_VERSION = 1
13
+
14
+ __all__ = [
15
+ "PARSER_VERSION",
16
+ "Checkpoint",
17
+ "Consistency",
18
+ "ErrorCode",
19
+ "TensorMeta",
20
+ "WeightLabError",
21
+ "__version__",
22
+ "open_checkpoint",
23
+ ]
File without changes
@@ -0,0 +1,311 @@
1
+ """Overflow-safe streaming accumulators for change metrics (doc 05 sections 3-4).
2
+
3
+ Sums of squares and dot products are kept as `mantissa * 2**exponent` with power-of-two chunk
4
+ scaling (LAPACK dnrm2 idea, but exact scaling), so a finite input never overflows an L2 norm or a
5
+ cosine; only a final value outside float64 range is reported as overflowed (None).
6
+ Every element is visited once; results are float64-rounded (`numeric_precision = rounded`).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from dataclasses import dataclass, field
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+
17
+ F64 = npt.NDArray[np.float64]
18
+
19
+
20
+ @dataclass
21
+ class ExpSum:
22
+ """Non-negative-or-signed running sum stored as m * 2**e with 0.5 <= |m| < 1 (or m == 0)."""
23
+
24
+ m: float = 0.0
25
+ e: int = 0
26
+
27
+ def add(self, mantissa: float, exponent: int) -> None:
28
+ if mantissa == 0.0:
29
+ return
30
+ if self.m == 0.0:
31
+ self.m, self.e = mantissa, exponent
32
+ elif exponent > self.e:
33
+ self.m = math.ldexp(self.m, self.e - exponent) + mantissa
34
+ self.e = exponent
35
+ else:
36
+ self.m += math.ldexp(mantissa, exponent - self.e)
37
+ fm, fe = math.frexp(self.m)
38
+ self.m, self.e = fm, self.e + fe
39
+
40
+ def merge(self, other: ExpSum) -> None:
41
+ self.add(other.m, other.e)
42
+
43
+ def add_squares(self, x: F64) -> None:
44
+ if x.size == 0:
45
+ return
46
+ peak = float(np.max(np.abs(x)))
47
+ if peak == 0.0:
48
+ return
49
+ k = math.frexp(peak)[1]
50
+ y = np.ldexp(x, -k)
51
+ self.add(float(np.dot(y, y)), 2 * k)
52
+
53
+ def add_products(self, a: F64, b: F64) -> None:
54
+ if a.size == 0:
55
+ return
56
+ pa, pb = float(np.max(np.abs(a))), float(np.max(np.abs(b)))
57
+ if pa == 0.0 or pb == 0.0:
58
+ return
59
+ ka, kb = math.frexp(pa)[1], math.frexp(pb)[1]
60
+ self.add(float(np.dot(np.ldexp(a, -ka), np.ldexp(b, -kb))), ka + kb)
61
+
62
+ def value(self) -> float | None:
63
+ return _ldexp(self.m, self.e)
64
+
65
+ def sqrt(self) -> float | None:
66
+ """sqrt of a non-negative sum, overflow-safe."""
67
+ if self.m == 0.0:
68
+ return 0.0
69
+ m, e = (self.m, self.e) if self.e % 2 == 0 else (self.m * 2.0, self.e - 1)
70
+ return _ldexp(math.sqrt(m), e // 2)
71
+
72
+
73
+ def _scaled(x: F64, peak: float) -> tuple[F64, int] | None:
74
+ """x * 2**-k with 2**(k-1) <= peak < 2**k (exact power-of-two scaling), or None if all zero."""
75
+ if peak == 0.0 or x.size == 0:
76
+ return None
77
+ k = math.frexp(peak)[1]
78
+ return np.ldexp(x, -k), k
79
+
80
+
81
+ def _ldexp(m: float, e: int) -> float | None:
82
+ try:
83
+ v = math.ldexp(m, e)
84
+ except OverflowError:
85
+ return None
86
+ return v if math.isfinite(v) else None
87
+
88
+
89
+ def sqrt_ratio(num: ExpSum, den: ExpSum) -> float | None:
90
+ """sqrt(num / den) without forming either value; None when den is zero or the result overflows."""
91
+ if den.m == 0.0:
92
+ return None
93
+ if num.m == 0.0:
94
+ return 0.0
95
+ m, e = num.m / den.m, num.e - den.e
96
+ if e % 2:
97
+ m, e = m * 2.0, e - 1
98
+ return _ldexp(math.sqrt(m), e // 2)
99
+
100
+
101
+ def cosine(dot: ExpSum, ssq_a: ExpSum, ssq_b: ExpSum) -> float | None:
102
+ """dot / (||a|| ||b||); None for a zero norm."""
103
+ if ssq_a.m == 0.0 or ssq_b.m == 0.0:
104
+ return None
105
+ m = dot.m / math.sqrt(ssq_a.m * ssq_b.m)
106
+ e2 = 2 * dot.e - ssq_a.e - ssq_b.e # exponent of the result, doubled
107
+ if e2 % 2:
108
+ m, e2 = m * math.sqrt(2.0), e2 - 1
109
+ v = _ldexp(m, e2 // 2)
110
+ return None if v is None else max(-1.0, min(1.0, v))
111
+
112
+
113
+ @dataclass
114
+ class PairStats:
115
+ """Paired statistics of A (reference) and B, with delta = B - A in float64.
116
+
117
+ Pairs where either input is non-finite are excluded from every finite-pair metric and counted
118
+ in `invalid_pairs`. A finite pair whose delta overflows float64 is counted in `delta_overflow`
119
+ and makes every delta metric `overflowed` (withheld), never a silent Inf.
120
+ """
121
+
122
+ elements: int = 0
123
+ finite_pairs: int = 0
124
+ invalid_pairs: int = 0
125
+ nan_a: int = 0
126
+ pos_inf_a: int = 0
127
+ neg_inf_a: int = 0
128
+ nan_b: int = 0
129
+ pos_inf_b: int = 0
130
+ neg_inf_b: int = 0
131
+ delta_overflow: int = 0
132
+ b_overflow: int = 0 # computed B (LoRA W + s B A) not representable although its inputs are finite
133
+ changed: int = 0
134
+ max_abs: float = 0.0
135
+ ssq_delta: ExpSum = field(default_factory=ExpSum)
136
+ ssq_a: ExpSum = field(default_factory=ExpSum)
137
+ ssq_b: ExpSum = field(default_factory=ExpSum)
138
+ dot_ab: ExpSum = field(default_factory=ExpSum)
139
+
140
+ def _count_nonfinite(self, arr: F64, finite: npt.NDArray[np.bool_], prefix: str) -> None:
141
+ bad = arr[~finite]
142
+ nan = int(np.isnan(bad).sum())
143
+ pos = int((bad > 0).sum())
144
+ setattr(self, f"nan_{prefix}", getattr(self, f"nan_{prefix}") + nan)
145
+ setattr(self, f"pos_inf_{prefix}", getattr(self, f"pos_inf_{prefix}") + pos)
146
+ setattr(self, f"neg_inf_{prefix}", getattr(self, f"neg_inf_{prefix}") + bad.size - nan - pos)
147
+
148
+ def update(self, a: F64, b: F64, delta: F64 | None = None) -> None:
149
+ """Feed one aligned chunk. `delta`, when given (LoRA), is the exact-as-computed B - A.
150
+
151
+ Fast path: an all-finite chunk (the usual case) skips every mask; each input is scaled by
152
+ a power of two once and reused for its sum of squares and for the dot product.
153
+ """
154
+ n = int(a.size)
155
+ self.elements += n
156
+ fa, fb = np.isfinite(a), np.isfinite(b)
157
+ all_a, all_b = bool(fa.all()), bool(fb.all())
158
+ if not all_a:
159
+ self._count_nonfinite(a, fa, "a")
160
+ if not all_b:
161
+ self._count_nonfinite(b, fb, "b")
162
+ ok: npt.NDArray[np.bool_] | None = None
163
+ if not (all_a and all_b):
164
+ ok = fa & fb
165
+ if delta is not None:
166
+ fd = np.isfinite(delta)
167
+ if not fd.all():
168
+ ok = fd if ok is None else ok & fd
169
+ if ok is not None:
170
+ a, b = a[ok], b[ok]
171
+ if delta is not None:
172
+ delta = delta[ok]
173
+ n_ok = int(a.size)
174
+ self.finite_pairs += n_ok
175
+ self.invalid_pairs += n - n_ok
176
+ if n_ok == 0:
177
+ return
178
+ peak_a, peak_b = float(np.max(np.abs(a))), float(np.max(np.abs(b)))
179
+ if delta is None:
180
+ if max(peak_a, peak_b) >= 2.0**1022: # only then can B - A overflow float64
181
+ with np.errstate(over="ignore", invalid="ignore"):
182
+ d = b - a
183
+ over = ~np.isfinite(d)
184
+ n_over = int(over.sum())
185
+ if n_over:
186
+ self.delta_overflow += n_over
187
+ d = d[~over]
188
+ else:
189
+ d = b - a
190
+ else:
191
+ d = delta
192
+ self.changed += int(np.count_nonzero(d))
193
+ peak_d = float(np.max(np.abs(d))) if d.size else 0.0
194
+ self.max_abs = max(self.max_abs, peak_d)
195
+ ya = _scaled(a, peak_a)
196
+ yb = _scaled(b, peak_b)
197
+ if ya is not None:
198
+ self.ssq_a.add(float(np.dot(ya[0], ya[0])), 2 * ya[1])
199
+ if yb is not None:
200
+ self.ssq_b.add(float(np.dot(yb[0], yb[0])), 2 * yb[1])
201
+ if ya is not None and yb is not None:
202
+ self.dot_ab.add(float(np.dot(ya[0], yb[0])), ya[1] + yb[1])
203
+ yd = _scaled(d, peak_d)
204
+ if yd is not None:
205
+ self.ssq_delta.add(float(np.dot(yd[0], yd[0])), 2 * yd[1])
206
+
207
+ def add_overflow(self, a: F64, n_delta: int, n_b: int) -> None:
208
+ """Record finite reference values `a` at positions whose computed delta (`n_delta` of them)
209
+ and/or computed B (`n_b`) left float64 range. They are not source NaN/Inf: they count as
210
+ finite pairs, still contribute to ||A|| (which stays exact), and withhold every metric
211
+ that needs the unrepresentable values (delta metrics; norm_b and cosine when B overflowed)."""
212
+ self.elements += int(a.size)
213
+ self.finite_pairs += int(a.size)
214
+ self.delta_overflow += n_delta
215
+ self.b_overflow += n_b
216
+ self.ssq_a.add_squares(a)
217
+
218
+ # -- derived metrics (None = not defined or withheld) ---------------------------------
219
+ @property
220
+ def delta_withheld(self) -> bool:
221
+ return self.delta_overflow > 0 or self.b_overflow > 0
222
+
223
+ @property
224
+ def b_withheld(self) -> bool:
225
+ return self.b_overflow > 0
226
+
227
+ def delta_l2(self) -> float | None:
228
+ return None if self.delta_withheld else self.ssq_delta.sqrt()
229
+
230
+ def delta_rms(self) -> float | None:
231
+ if self.delta_withheld or self.finite_pairs == 0:
232
+ return None
233
+ return sqrt_ratio(self.ssq_delta, ExpSum(*math.frexp(float(self.finite_pairs))))
234
+
235
+ def relative_l2(self, eps_norm: float) -> float | None:
236
+ if self.delta_withheld or self.finite_pairs == 0:
237
+ return None
238
+ return relative_l2(self.ssq_delta, self.ssq_a, eps_norm)
239
+
240
+ def near_zero_reference(self, eps_norm: float) -> bool:
241
+ return near_zero(self.ssq_a, eps_norm)
242
+
243
+ def cosine(self) -> float | None:
244
+ return None if self.b_withheld else cosine(self.dot_ab, self.ssq_a, self.ssq_b)
245
+
246
+ def max_abs_delta(self) -> float | None:
247
+ if self.delta_withheld or self.finite_pairs == 0:
248
+ return None
249
+ return self.max_abs
250
+
251
+
252
+ def _ratio_or_none(num: float | None, den: float) -> float | None:
253
+ if num is None:
254
+ return None
255
+ v = num / den
256
+ return v if math.isfinite(v) else None
257
+
258
+
259
+ def near_zero(ssq_ref: ExpSum, eps_norm: float) -> bool:
260
+ norm = ssq_ref.sqrt()
261
+ return norm is not None and norm < eps_norm
262
+
263
+
264
+ def relative_l2(ssq_delta: ExpSum, ssq_ref: ExpSum, eps_norm: float) -> float | None:
265
+ """||delta|| / max(||ref||, eps_norm): the one denominator policy for tensors, modules and
266
+ rollups. A zero or near-zero reference divides by eps_norm (never null for that reason)."""
267
+ if near_zero(ssq_ref, eps_norm):
268
+ return _ratio_or_none(ssq_delta.sqrt(), eps_norm)
269
+ return sqrt_ratio(ssq_delta, ssq_ref)
270
+
271
+
272
+ @dataclass
273
+ class Rollup:
274
+ """Aggregate of several tensors' paired stats (layer, role or whole model)."""
275
+
276
+ tensors: int = 0
277
+ elements: int = 0
278
+ finite_pairs: int = 0
279
+ invalid_pairs: int = 0
280
+ withheld: int = 0 # member tensors whose delta overflowed
281
+ max_abs: float = 0.0
282
+ ssq_delta: ExpSum = field(default_factory=ExpSum)
283
+ ssq_a: ExpSum = field(default_factory=ExpSum)
284
+
285
+ def add(self, s: PairStats, ssq_delta: ExpSum | None = None) -> None:
286
+ self.tensors += 1
287
+ self.elements += s.elements
288
+ self.finite_pairs += s.finite_pairs
289
+ self.invalid_pairs += s.invalid_pairs
290
+ if s.delta_withheld:
291
+ self.withheld += 1
292
+ else:
293
+ self.ssq_delta.merge(ssq_delta if ssq_delta is not None else s.ssq_delta)
294
+ self.max_abs = max(self.max_abs, s.max_abs)
295
+ self.ssq_a.merge(s.ssq_a)
296
+
297
+ def delta_l2(self) -> float | None:
298
+ return None if self.withheld else self.ssq_delta.sqrt()
299
+
300
+ def delta_rms(self) -> float | None:
301
+ if self.withheld or self.finite_pairs == 0:
302
+ return None
303
+ return sqrt_ratio(self.ssq_delta, ExpSum(*math.frexp(float(self.finite_pairs))))
304
+
305
+ def relative_l2(self, eps_norm: float) -> float | None:
306
+ if self.withheld or self.finite_pairs == 0:
307
+ return None
308
+ return relative_l2(self.ssq_delta, self.ssq_a, eps_norm)
309
+
310
+ def near_zero_reference(self, eps_norm: float) -> bool:
311
+ return near_zero(self.ssq_a, eps_norm)