invarlock 0.2.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 (132) hide show
  1. invarlock/__init__.py +33 -0
  2. invarlock/__main__.py +10 -0
  3. invarlock/_data/runtime/profiles/ci_cpu.yaml +15 -0
  4. invarlock/_data/runtime/profiles/release.yaml +23 -0
  5. invarlock/_data/runtime/tiers.yaml +76 -0
  6. invarlock/adapters/__init__.py +102 -0
  7. invarlock/adapters/_capabilities.py +45 -0
  8. invarlock/adapters/auto.py +99 -0
  9. invarlock/adapters/base.py +530 -0
  10. invarlock/adapters/base_types.py +85 -0
  11. invarlock/adapters/hf_bert.py +852 -0
  12. invarlock/adapters/hf_gpt2.py +403 -0
  13. invarlock/adapters/hf_llama.py +485 -0
  14. invarlock/adapters/hf_mixin.py +383 -0
  15. invarlock/adapters/hf_onnx.py +112 -0
  16. invarlock/adapters/hf_t5.py +137 -0
  17. invarlock/adapters/py.typed +1 -0
  18. invarlock/assurance/__init__.py +43 -0
  19. invarlock/cli/__init__.py +8 -0
  20. invarlock/cli/__main__.py +8 -0
  21. invarlock/cli/_evidence.py +25 -0
  22. invarlock/cli/_json.py +75 -0
  23. invarlock/cli/adapter_auto.py +162 -0
  24. invarlock/cli/app.py +287 -0
  25. invarlock/cli/commands/__init__.py +26 -0
  26. invarlock/cli/commands/certify.py +403 -0
  27. invarlock/cli/commands/doctor.py +1358 -0
  28. invarlock/cli/commands/explain_gates.py +151 -0
  29. invarlock/cli/commands/export_html.py +100 -0
  30. invarlock/cli/commands/plugins.py +1331 -0
  31. invarlock/cli/commands/report.py +354 -0
  32. invarlock/cli/commands/run.py +4146 -0
  33. invarlock/cli/commands/verify.py +1040 -0
  34. invarlock/cli/config.py +396 -0
  35. invarlock/cli/constants.py +68 -0
  36. invarlock/cli/device.py +92 -0
  37. invarlock/cli/doctor_helpers.py +74 -0
  38. invarlock/cli/errors.py +6 -0
  39. invarlock/cli/overhead_utils.py +60 -0
  40. invarlock/cli/provenance.py +66 -0
  41. invarlock/cli/utils.py +41 -0
  42. invarlock/config.py +56 -0
  43. invarlock/core/__init__.py +62 -0
  44. invarlock/core/abi.py +15 -0
  45. invarlock/core/api.py +274 -0
  46. invarlock/core/auto_tuning.py +317 -0
  47. invarlock/core/bootstrap.py +226 -0
  48. invarlock/core/checkpoint.py +221 -0
  49. invarlock/core/contracts.py +73 -0
  50. invarlock/core/error_utils.py +64 -0
  51. invarlock/core/events.py +298 -0
  52. invarlock/core/exceptions.py +95 -0
  53. invarlock/core/registry.py +481 -0
  54. invarlock/core/retry.py +146 -0
  55. invarlock/core/runner.py +2041 -0
  56. invarlock/core/types.py +154 -0
  57. invarlock/edits/__init__.py +12 -0
  58. invarlock/edits/_edit_utils.py +249 -0
  59. invarlock/edits/_external_utils.py +268 -0
  60. invarlock/edits/noop.py +47 -0
  61. invarlock/edits/py.typed +1 -0
  62. invarlock/edits/quant_rtn.py +801 -0
  63. invarlock/edits/registry.py +166 -0
  64. invarlock/eval/__init__.py +23 -0
  65. invarlock/eval/bench.py +1207 -0
  66. invarlock/eval/bootstrap.py +50 -0
  67. invarlock/eval/data.py +2052 -0
  68. invarlock/eval/metrics.py +2167 -0
  69. invarlock/eval/primary_metric.py +767 -0
  70. invarlock/eval/probes/__init__.py +24 -0
  71. invarlock/eval/probes/fft.py +139 -0
  72. invarlock/eval/probes/mi.py +213 -0
  73. invarlock/eval/probes/post_attention.py +323 -0
  74. invarlock/eval/providers/base.py +67 -0
  75. invarlock/eval/providers/seq2seq.py +111 -0
  76. invarlock/eval/providers/text_lm.py +113 -0
  77. invarlock/eval/providers/vision_text.py +93 -0
  78. invarlock/eval/py.typed +1 -0
  79. invarlock/guards/__init__.py +18 -0
  80. invarlock/guards/_contracts.py +9 -0
  81. invarlock/guards/invariants.py +640 -0
  82. invarlock/guards/policies.py +805 -0
  83. invarlock/guards/py.typed +1 -0
  84. invarlock/guards/rmt.py +2097 -0
  85. invarlock/guards/spectral.py +1419 -0
  86. invarlock/guards/tier_config.py +354 -0
  87. invarlock/guards/variance.py +3298 -0
  88. invarlock/guards_ref/__init__.py +15 -0
  89. invarlock/guards_ref/rmt_ref.py +40 -0
  90. invarlock/guards_ref/spectral_ref.py +135 -0
  91. invarlock/guards_ref/variance_ref.py +60 -0
  92. invarlock/model_profile.py +353 -0
  93. invarlock/model_utils.py +221 -0
  94. invarlock/observability/__init__.py +10 -0
  95. invarlock/observability/alerting.py +535 -0
  96. invarlock/observability/core.py +546 -0
  97. invarlock/observability/exporters.py +565 -0
  98. invarlock/observability/health.py +588 -0
  99. invarlock/observability/metrics.py +457 -0
  100. invarlock/observability/py.typed +1 -0
  101. invarlock/observability/utils.py +553 -0
  102. invarlock/plugins/__init__.py +12 -0
  103. invarlock/plugins/hello_guard.py +33 -0
  104. invarlock/plugins/hf_awq_adapter.py +82 -0
  105. invarlock/plugins/hf_bnb_adapter.py +79 -0
  106. invarlock/plugins/hf_gptq_adapter.py +78 -0
  107. invarlock/plugins/py.typed +1 -0
  108. invarlock/py.typed +1 -0
  109. invarlock/reporting/__init__.py +7 -0
  110. invarlock/reporting/certificate.py +3221 -0
  111. invarlock/reporting/certificate_schema.py +244 -0
  112. invarlock/reporting/dataset_hashing.py +215 -0
  113. invarlock/reporting/guards_analysis.py +948 -0
  114. invarlock/reporting/html.py +32 -0
  115. invarlock/reporting/normalizer.py +235 -0
  116. invarlock/reporting/policy_utils.py +517 -0
  117. invarlock/reporting/primary_metric_utils.py +265 -0
  118. invarlock/reporting/render.py +1442 -0
  119. invarlock/reporting/report.py +903 -0
  120. invarlock/reporting/report_types.py +278 -0
  121. invarlock/reporting/utils.py +175 -0
  122. invarlock/reporting/validate.py +631 -0
  123. invarlock/security.py +176 -0
  124. invarlock/sparsity_utils.py +323 -0
  125. invarlock/utils/__init__.py +150 -0
  126. invarlock/utils/digest.py +45 -0
  127. invarlock-0.2.0.dist-info/METADATA +586 -0
  128. invarlock-0.2.0.dist-info/RECORD +132 -0
  129. invarlock-0.2.0.dist-info/WHEEL +5 -0
  130. invarlock-0.2.0.dist-info/entry_points.txt +20 -0
  131. invarlock-0.2.0.dist-info/licenses/LICENSE +201 -0
  132. invarlock-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,457 @@
1
+ """
2
+ Metrics collection and registry system.
3
+ """
4
+
5
+ import threading
6
+ import time
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+
13
+ class MetricType(Enum):
14
+ """Types of metrics supported."""
15
+
16
+ COUNTER = "counter"
17
+ GAUGE = "gauge"
18
+ HISTOGRAM = "histogram"
19
+ TIMER = "timer"
20
+
21
+
22
+ @dataclass
23
+ class MetricValue:
24
+ """Container for metric values with metadata."""
25
+
26
+ value: int | float
27
+ labels: dict[str, str]
28
+ timestamp: float
29
+
30
+ def __post_init__(self):
31
+ if not hasattr(self, "timestamp") or self.timestamp is None:
32
+ self.timestamp = time.time()
33
+
34
+
35
+ class Counter:
36
+ """Counter metric - monotonically increasing value."""
37
+
38
+ def __init__(self, name: str, description: str = ""):
39
+ self.name = name
40
+ self.description = description
41
+ self._values: dict[str, float] = defaultdict(float)
42
+ # Use RLock to allow nested acquisitions in helper methods
43
+ self._lock = threading.RLock()
44
+
45
+ def inc(self, amount: float = 1.0, labels: dict[str, str] | None = None):
46
+ """Increment counter by amount."""
47
+ labels = labels or {}
48
+ label_key = self._labels_to_key(labels)
49
+
50
+ with self._lock:
51
+ self._values[label_key] += amount
52
+
53
+ def get(self, labels: dict[str, str] | None = None) -> float:
54
+ """Get current counter value."""
55
+ labels = labels or {}
56
+ label_key = self._labels_to_key(labels)
57
+
58
+ with self._lock:
59
+ return float(self._values[label_key])
60
+
61
+ def get_all(self) -> list[MetricValue]:
62
+ """Get all counter values with labels."""
63
+ with self._lock:
64
+ return [
65
+ MetricValue(
66
+ value=value, labels=self._key_to_labels(key), timestamp=time.time()
67
+ )
68
+ for key, value in self._values.items()
69
+ ]
70
+
71
+ def reset(self, labels: dict[str, str] | None = None):
72
+ """Reset counter to zero."""
73
+ labels = labels or {}
74
+ label_key = self._labels_to_key(labels)
75
+
76
+ with self._lock:
77
+ self._values[label_key] = 0.0
78
+
79
+ @staticmethod
80
+ def _labels_to_key(labels: dict[str, str]) -> str:
81
+ """Convert labels dict to string key."""
82
+ return "|".join(f"{k}={v}" for k, v in sorted(labels.items()))
83
+
84
+ @staticmethod
85
+ def _key_to_labels(key: str) -> dict[str, str]:
86
+ """Convert string key back to labels dict."""
87
+ if not key:
88
+ return {}
89
+
90
+ labels = {}
91
+ for pair in key.split("|"):
92
+ if "=" in pair:
93
+ k, v = pair.split("=", 1)
94
+ labels[k] = v
95
+ return labels
96
+
97
+
98
+ class Gauge:
99
+ """Gauge metric - value that can increase or decrease."""
100
+
101
+ def __init__(self, name: str, description: str = ""):
102
+ self.name = name
103
+ self.description = description
104
+ self._values: dict[str, float] = defaultdict(float)
105
+ # Use RLock to allow nested acquisitions in helper methods
106
+ self._lock = threading.RLock()
107
+
108
+ def set(self, value: float, labels: dict[str, str] | None = None):
109
+ """Set gauge to specific value."""
110
+ labels = labels or {}
111
+ label_key = Counter._labels_to_key(labels)
112
+
113
+ with self._lock:
114
+ self._values[label_key] = value
115
+
116
+ def inc(self, amount: float = 1.0, labels: dict[str, str] | None = None):
117
+ """Increment gauge by amount."""
118
+ labels = labels or {}
119
+ label_key = Counter._labels_to_key(labels)
120
+
121
+ with self._lock:
122
+ self._values[label_key] += amount
123
+
124
+ def dec(self, amount: float = 1.0, labels: dict[str, str] | None = None):
125
+ """Decrement gauge by amount."""
126
+ self.inc(-amount, labels)
127
+
128
+ def get(self, labels: dict[str, str] | None = None) -> float:
129
+ """Get current gauge value."""
130
+ labels = labels or {}
131
+ label_key = Counter._labels_to_key(labels)
132
+
133
+ with self._lock:
134
+ return float(self._values[label_key])
135
+
136
+ def get_all(self) -> list[MetricValue]:
137
+ """Get all gauge values with labels."""
138
+ with self._lock:
139
+ return [
140
+ MetricValue(
141
+ value=value,
142
+ labels=Counter._key_to_labels(key),
143
+ timestamp=time.time(),
144
+ )
145
+ for key, value in self._values.items()
146
+ ]
147
+
148
+
149
+ class Histogram:
150
+ """Histogram metric - distribution of values."""
151
+
152
+ def __init__(
153
+ self, name: str, description: str = "", buckets: list[float] | None = None
154
+ ):
155
+ self.name = name
156
+ self.description = description
157
+ self.buckets = buckets or [
158
+ 0.005,
159
+ 0.01,
160
+ 0.025,
161
+ 0.05,
162
+ 0.1,
163
+ 0.25,
164
+ 0.5,
165
+ 1.0,
166
+ 2.5,
167
+ 5.0,
168
+ 10.0,
169
+ ]
170
+
171
+ # Store observations for each label set
172
+ self._observations: dict[str, list[float]] = defaultdict(list)
173
+ self._bucket_counts: dict[str, dict[float, int]] = defaultdict(
174
+ lambda: defaultdict(int)
175
+ )
176
+ self._sum: dict[str, float] = defaultdict(float)
177
+ self._count: dict[str, int] = defaultdict(int)
178
+ # Use RLock to allow nested acquisitions when get_stats calls get_percentile
179
+ self._lock = threading.RLock()
180
+
181
+ def observe(self, value: float, labels: dict[str, str] | None = None):
182
+ """Observe a value."""
183
+ labels = labels or {}
184
+ label_key = Counter._labels_to_key(labels)
185
+
186
+ with self._lock:
187
+ # Store observation
188
+ self._observations[label_key].append(value)
189
+
190
+ # Keep only recent observations (last 10000)
191
+ if len(self._observations[label_key]) > 10000:
192
+ self._observations[label_key] = self._observations[label_key][-10000:]
193
+
194
+ # Update bucket counts
195
+ for bucket in self.buckets:
196
+ if value <= bucket:
197
+ self._bucket_counts[label_key][bucket] += 1
198
+
199
+ # Update sum and count
200
+ self._sum[label_key] += value
201
+ self._count[label_key] += 1
202
+
203
+ def get_percentile(
204
+ self, percentile: float, labels: dict[str, str] | None = None
205
+ ) -> float:
206
+ """Get percentile value."""
207
+ labels = labels or {}
208
+ label_key = Counter._labels_to_key(labels)
209
+
210
+ with self._lock:
211
+ observations = self._observations[label_key]
212
+ if not observations:
213
+ return 0.0
214
+
215
+ sorted_obs = sorted(observations)
216
+ index = int(len(sorted_obs) * percentile / 100)
217
+ return float(sorted_obs[min(index, len(sorted_obs) - 1)])
218
+
219
+ def get_stats(self, labels: dict[str, str] | None = None) -> dict[str, float]:
220
+ """Get histogram statistics."""
221
+ labels = labels or {}
222
+ label_key = Counter._labels_to_key(labels)
223
+
224
+ with self._lock:
225
+ observations = self._observations[label_key]
226
+ if not observations:
227
+ return {}
228
+
229
+ count = self._count[label_key]
230
+ total = self._sum[label_key]
231
+
232
+ return {
233
+ "count": count,
234
+ "sum": total,
235
+ "mean": total / count if count > 0 else 0,
236
+ "min": min(observations),
237
+ "max": max(observations),
238
+ "p50": self.get_percentile(50, labels),
239
+ "p90": self.get_percentile(90, labels),
240
+ "p95": self.get_percentile(95, labels),
241
+ "p99": self.get_percentile(99, labels),
242
+ }
243
+
244
+ def get_buckets(self, labels: dict[str, str] | None = None) -> dict[float, int]:
245
+ """Get bucket counts."""
246
+ labels = labels or {}
247
+ label_key = Counter._labels_to_key(labels)
248
+
249
+ with self._lock:
250
+ return dict(self._bucket_counts[label_key])
251
+
252
+
253
+ class Timer:
254
+ """Timer metric - specialized histogram for timing operations."""
255
+
256
+ def __init__(self, name: str, description: str = ""):
257
+ self.name = name
258
+ self.histogram = Histogram(name, description)
259
+
260
+ def time(self, labels: dict[str, str] | None = None):
261
+ """Context manager for timing operations."""
262
+ return TimerContext(self, labels)
263
+
264
+ def record(self, duration: float, labels: dict[str, str] | None = None):
265
+ """Record a duration."""
266
+ self.histogram.observe(duration, labels)
267
+
268
+ def get_stats(self, labels: dict[str, str] | None = None) -> dict[str, float]:
269
+ """Get timing statistics."""
270
+ return self.histogram.get_stats(labels)
271
+
272
+
273
+ class TimerContext:
274
+ """Context manager for timing operations."""
275
+
276
+ def __init__(self, timer: Timer, labels: dict[str, str] | None = None):
277
+ self.timer = timer
278
+ self.labels = labels
279
+ self.start_time: float | None = None
280
+
281
+ def __enter__(self):
282
+ self.start_time = time.time()
283
+ return self
284
+
285
+ def __exit__(self, exc_type, exc_val, exc_tb):
286
+ if self.start_time is not None:
287
+ duration = time.time() - self.start_time
288
+ self.timer.record(duration, self.labels)
289
+
290
+
291
+ class MetricsRegistry:
292
+ """Central registry for all metrics."""
293
+
294
+ def __init__(self):
295
+ self._metrics: dict[str, Counter | Gauge | Histogram | Timer] = {}
296
+ # Use RLock as registry may invoke metric methods that also lock internally
297
+ self._lock = threading.RLock()
298
+
299
+ def register_counter(self, name: str, description: str = "") -> Counter:
300
+ """Register a counter metric."""
301
+ with self._lock:
302
+ if name in self._metrics:
303
+ metric = self._metrics[name]
304
+ if not isinstance(metric, Counter):
305
+ raise ValueError(f"Metric {name} already exists as different type")
306
+ return metric
307
+
308
+ counter = Counter(name, description)
309
+ self._metrics[name] = counter
310
+ return counter
311
+
312
+ def register_gauge(self, name: str, description: str = "") -> Gauge:
313
+ """Register a gauge metric."""
314
+ with self._lock:
315
+ if name in self._metrics:
316
+ metric = self._metrics[name]
317
+ if not isinstance(metric, Gauge):
318
+ raise ValueError(f"Metric {name} already exists as different type")
319
+ return metric
320
+
321
+ gauge = Gauge(name, description)
322
+ self._metrics[name] = gauge
323
+ return gauge
324
+
325
+ def register_histogram(
326
+ self, name: str, description: str = "", buckets: list[float] | None = None
327
+ ) -> Histogram:
328
+ """Register a histogram metric."""
329
+ with self._lock:
330
+ if name in self._metrics:
331
+ metric = self._metrics[name]
332
+ if not isinstance(metric, Histogram):
333
+ raise ValueError(f"Metric {name} already exists as different type")
334
+ return metric
335
+
336
+ histogram = Histogram(name, description, buckets)
337
+ self._metrics[name] = histogram
338
+ return histogram
339
+
340
+ def register_timer(self, name: str, description: str = "") -> Timer:
341
+ """Register a timer metric."""
342
+ with self._lock:
343
+ if name in self._metrics:
344
+ metric = self._metrics[name]
345
+ if not isinstance(metric, Timer):
346
+ raise ValueError(f"Metric {name} already exists as different type")
347
+ return metric
348
+
349
+ timer = Timer(name, description)
350
+ self._metrics[name] = timer
351
+ return timer
352
+
353
+ def get_counter(self, name: str) -> Counter:
354
+ """Get or create counter metric."""
355
+ if name not in self._metrics:
356
+ return self.register_counter(name)
357
+
358
+ metric = self._metrics[name]
359
+ if not isinstance(metric, Counter):
360
+ raise ValueError(f"Metric {name} is not a counter")
361
+ return metric
362
+
363
+ def get_gauge(self, name: str) -> Gauge:
364
+ """Get or create gauge metric."""
365
+ if name not in self._metrics:
366
+ return self.register_gauge(name)
367
+
368
+ metric = self._metrics[name]
369
+ if not isinstance(metric, Gauge):
370
+ raise ValueError(f"Metric {name} is not a gauge")
371
+ return metric
372
+
373
+ def get_histogram(self, name: str) -> Histogram:
374
+ """Get or create histogram metric."""
375
+ if name not in self._metrics:
376
+ return self.register_histogram(name)
377
+
378
+ metric = self._metrics[name]
379
+ if not isinstance(metric, Histogram):
380
+ raise ValueError(f"Metric {name} is not a histogram")
381
+ return metric
382
+
383
+ def get_timer(self, name: str) -> Timer:
384
+ """Get or create timer metric."""
385
+ if name not in self._metrics:
386
+ return self.register_timer(name)
387
+
388
+ metric = self._metrics[name]
389
+ if not isinstance(metric, Timer):
390
+ raise ValueError(f"Metric {name} is not a timer")
391
+ return metric
392
+
393
+ def get_all_metrics(self) -> dict[str, Any]:
394
+ """Get all metrics data."""
395
+ with self._lock:
396
+ result: dict[str, Any] = {}
397
+
398
+ for name, metric in self._metrics.items():
399
+ if isinstance(metric, Counter | Gauge):
400
+ result[name] = {
401
+ "type": type(metric).__name__.lower(),
402
+ "description": metric.description,
403
+ "values": [value.__dict__ for value in metric.get_all()],
404
+ }
405
+ elif isinstance(metric, Histogram):
406
+ result[name] = {
407
+ "type": "histogram",
408
+ "description": metric.description,
409
+ "buckets": metric.buckets,
410
+ "stats": metric.get_stats(),
411
+ }
412
+ elif isinstance(metric, Timer):
413
+ result[name] = {
414
+ "type": "timer",
415
+ "description": metric.histogram.description,
416
+ "stats": metric.get_stats(),
417
+ }
418
+
419
+ return result
420
+
421
+ def clear_all(self):
422
+ """Clear all metrics."""
423
+ with self._lock:
424
+ self._metrics.clear()
425
+
426
+ def remove_metric(self, name: str):
427
+ """Remove a specific metric."""
428
+ with self._lock:
429
+ self._metrics.pop(name, None)
430
+
431
+ def list_metrics(self) -> list[str]:
432
+ """List all registered metric names."""
433
+ with self._lock:
434
+ return list(self._metrics.keys())
435
+
436
+
437
+ # Utility functions for common metric patterns
438
+ def create_operation_metrics(
439
+ registry: MetricsRegistry, operation: str
440
+ ) -> dict[str, Any]:
441
+ """Create standard metrics for an operation."""
442
+ return {
443
+ "counter": registry.register_counter(f"invarlock.{operation}.total"),
444
+ "timer": registry.register_timer(f"invarlock.{operation}.duration"),
445
+ "errors": registry.register_counter(f"invarlock.{operation}.errors"),
446
+ "success_rate": registry.register_gauge(f"invarlock.{operation}.success_rate"),
447
+ }
448
+
449
+
450
+ def create_resource_metrics(registry: MetricsRegistry) -> dict[str, Any]:
451
+ """Create standard resource monitoring metrics."""
452
+ return {
453
+ "cpu_usage": registry.register_gauge("invarlock.resource.cpu_percent"),
454
+ "memory_usage": registry.register_gauge("invarlock.resource.memory_percent"),
455
+ "gpu_memory": registry.register_gauge("invarlock.resource.gpu_memory_percent"),
456
+ "disk_usage": registry.register_gauge("invarlock.resource.disk_percent"),
457
+ }
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561 type hints