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,553 @@
1
+ """
2
+ Monitoring utilities and helper functions.
3
+ """
4
+
5
+ import logging
6
+ import threading
7
+ import time
8
+ from collections.abc import Callable
9
+ from contextlib import contextmanager
10
+ from dataclasses import dataclass
11
+ from functools import wraps
12
+ from typing import Any
13
+
14
+ import psutil
15
+
16
+
17
+ @dataclass
18
+ class TimingContext:
19
+ """Context for timing operations."""
20
+
21
+ start_time: float
22
+ end_time: float | None = None
23
+ duration: float | None = None
24
+ operation: str = ""
25
+ metadata: dict[str, Any] | None = None
26
+
27
+ def __post_init__(self):
28
+ if self.metadata is None:
29
+ self.metadata = {}
30
+
31
+ def finish(self):
32
+ """Mark timing as finished."""
33
+ self.end_time = time.time()
34
+ self.duration = self.end_time - self.start_time
35
+ return self.duration
36
+
37
+
38
+ class Timer:
39
+ """Simple timer for measuring operation durations."""
40
+
41
+ def __init__(self, name: str = "", auto_log: bool = False):
42
+ self.name = name
43
+ self.auto_log = auto_log
44
+ self.logger = logging.getLogger(__name__)
45
+ self.start_time: float | None = None
46
+ self.end_time: float | None = None
47
+ self.duration: float | None = None
48
+
49
+ def start(self):
50
+ """Start the timer."""
51
+ self.start_time = time.time()
52
+ return self
53
+
54
+ def stop(self) -> float:
55
+ """Stop the timer and return duration."""
56
+ if self.start_time is None:
57
+ raise ValueError("Timer not started")
58
+
59
+ self.end_time = time.time()
60
+ self.duration = self.end_time - self.start_time
61
+
62
+ if self.auto_log:
63
+ operation = self.name or "operation"
64
+ self.logger.info(f"{operation} completed in {self.duration:.3f}s")
65
+
66
+ return self.duration
67
+
68
+ def __enter__(self):
69
+ return self.start()
70
+
71
+ def __exit__(self, exc_type, exc_val, exc_tb):
72
+ self.stop()
73
+
74
+
75
+ @contextmanager
76
+ def timed_operation(
77
+ operation_name: str,
78
+ metadata: dict[str, Any] | None = None,
79
+ callback: Callable[[TimingContext], None] | None = None,
80
+ ):
81
+ """Context manager for timing operations with callback support."""
82
+ context = TimingContext(
83
+ start_time=time.time(), operation=operation_name, metadata=metadata or {}
84
+ )
85
+
86
+ try:
87
+ yield context
88
+ finally:
89
+ context.finish()
90
+ if callback:
91
+ callback(context)
92
+
93
+
94
+ def timing_decorator(
95
+ operation_name: str | None = None,
96
+ auto_log: bool = True,
97
+ callback: Callable[[TimingContext], None] | None = None,
98
+ ):
99
+ """Decorator for timing function execution."""
100
+
101
+ def decorator(func):
102
+ @wraps(func)
103
+ def wrapper(*args, **kwargs):
104
+ name = operation_name or f"{func.__module__}.{func.__name__}"
105
+
106
+ def timing_callback(context):
107
+ if auto_log:
108
+ logger = logging.getLogger(__name__)
109
+ logger.info(f"{name} completed in {context.duration:.3f}s")
110
+ if callback:
111
+ callback(context)
112
+
113
+ with timed_operation(name, callback=timing_callback):
114
+ return func(*args, **kwargs)
115
+
116
+ return wrapper
117
+
118
+ return decorator
119
+
120
+
121
+ class RateLimiter:
122
+ """Simple rate limiter for monitoring operations."""
123
+
124
+ def __init__(self, max_calls: int, window_seconds: float):
125
+ self.max_calls = max_calls
126
+ self.window_seconds = window_seconds
127
+ self.calls: list[float] = []
128
+ self.lock = threading.Lock()
129
+
130
+ def is_allowed(self) -> bool:
131
+ """Check if operation is allowed within rate limit."""
132
+ now = time.time()
133
+
134
+ with self.lock:
135
+ # Remove old calls outside the window
136
+ self.calls = [
137
+ call_time
138
+ for call_time in self.calls
139
+ if now - call_time < self.window_seconds
140
+ ]
141
+
142
+ # Check if we're under the limit
143
+ if len(self.calls) < self.max_calls:
144
+ self.calls.append(now)
145
+ return True
146
+
147
+ return False
148
+
149
+ def get_stats(self) -> dict[str, Any]:
150
+ """Get rate limiter statistics."""
151
+ now = time.time()
152
+
153
+ with self.lock:
154
+ recent_calls = [
155
+ call_time
156
+ for call_time in self.calls
157
+ if now - call_time < self.window_seconds
158
+ ]
159
+
160
+ return {
161
+ "current_calls": len(recent_calls),
162
+ "max_calls": self.max_calls,
163
+ "window_seconds": self.window_seconds,
164
+ "utilization": len(recent_calls) / self.max_calls,
165
+ "next_available": min(recent_calls) + self.window_seconds
166
+ if recent_calls
167
+ else now,
168
+ }
169
+
170
+
171
+ class CircularBuffer:
172
+ """Circular buffer for storing recent metrics."""
173
+
174
+ def __init__(self, size: int):
175
+ self.size = size
176
+ self.buffer = [None] * size
177
+ self.head = 0
178
+ self.count = 0
179
+ self.lock = threading.Lock()
180
+
181
+ def append(self, item):
182
+ """Add item to buffer."""
183
+ with self.lock:
184
+ self.buffer[self.head] = item
185
+ self.head = (self.head + 1) % self.size
186
+ self.count = min(self.count + 1, self.size)
187
+
188
+ def get_all(self) -> list[Any]:
189
+ """Get all items in chronological order."""
190
+ with self.lock:
191
+ if self.count == 0:
192
+ return []
193
+
194
+ if self.count < self.size:
195
+ return [item for item in self.buffer[: self.count] if item is not None]
196
+ else:
197
+ return self.buffer[self.head :] + self.buffer[: self.head]
198
+
199
+ def get_recent(self, n: int) -> list[Any]:
200
+ """Get n most recent items."""
201
+ all_items = self.get_all()
202
+ return all_items[-n:] if n <= len(all_items) else all_items
203
+
204
+ def clear(self):
205
+ """Clear the buffer."""
206
+ with self.lock:
207
+ self.buffer = [None] * self.size
208
+ self.head = 0
209
+ self.count = 0
210
+
211
+ def __len__(self):
212
+ return self.count
213
+
214
+
215
+ class MovingAverage:
216
+ """Calculate moving average of values."""
217
+
218
+ def __init__(self, window_size: int):
219
+ self.window_size = window_size
220
+ self.values = CircularBuffer(window_size)
221
+ self.sum = 0.0
222
+ self.lock = threading.Lock()
223
+
224
+ def add(self, value: float):
225
+ """Add a value to the moving average."""
226
+ with self.lock:
227
+ old_values = self.values.get_all()
228
+
229
+ # If buffer is full, subtract the oldest value
230
+ if len(old_values) == self.window_size:
231
+ oldest = old_values[0] if old_values else 0
232
+ self.sum -= oldest
233
+
234
+ # Add new value
235
+ self.values.append(value)
236
+ self.sum += value
237
+
238
+ def get_average(self) -> float:
239
+ """Get current moving average."""
240
+ with self.lock:
241
+ count = len(self.values)
242
+ return self.sum / count if count > 0 else 0
243
+
244
+ def get_stats(self) -> dict[str, float]:
245
+ """Get statistics about the moving average."""
246
+ with self.lock:
247
+ values = self.values.get_all()
248
+ if not values:
249
+ return {"average": 0, "min": 0, "max": 0, "count": 0}
250
+
251
+ return {
252
+ "average": self.sum / len(values),
253
+ "min": min(values),
254
+ "max": max(values),
255
+ "count": len(values),
256
+ }
257
+
258
+
259
+ class PercentileCalculator:
260
+ """Calculate percentiles from a stream of values."""
261
+
262
+ def __init__(self, window_size: int = 1000):
263
+ self.values = CircularBuffer(window_size)
264
+
265
+ def add(self, value: float):
266
+ """Add a value."""
267
+ self.values.append(value)
268
+
269
+ def get_percentile(self, percentile: float) -> float:
270
+ """Get the specified percentile (0-100)."""
271
+ values = self.values.get_all()
272
+ if not values:
273
+ return 0
274
+
275
+ sorted_values = sorted(values)
276
+ index = int((percentile / 100) * (len(sorted_values) - 1))
277
+ return float(sorted_values[index])
278
+
279
+ def get_percentiles(self, percentiles: list[float]) -> dict[float, float]:
280
+ """Get multiple percentiles at once."""
281
+ values = self.values.get_all()
282
+ if not values:
283
+ return dict.fromkeys(percentiles, 0)
284
+
285
+ sorted_values = sorted(values)
286
+ result = {}
287
+
288
+ for percentile in percentiles:
289
+ index = int((percentile / 100) * (len(sorted_values) - 1))
290
+ result[percentile] = sorted_values[index]
291
+
292
+ return result
293
+
294
+
295
+ def get_system_info() -> dict[str, Any]:
296
+ """Get comprehensive system information."""
297
+ try:
298
+ cpu_count = psutil.cpu_count()
299
+ cpu_count_logical = psutil.cpu_count(logical=True)
300
+ memory = psutil.virtual_memory()
301
+ disk = psutil.disk_usage("/")
302
+
303
+ # Get GPU info if available
304
+ gpu_info = {}
305
+ try:
306
+ import torch
307
+
308
+ if torch.cuda.is_available():
309
+ gpu_info = {
310
+ "gpu_available": True,
311
+ "gpu_count": torch.cuda.device_count(),
312
+ "gpu_names": [
313
+ torch.cuda.get_device_name(i)
314
+ for i in range(torch.cuda.device_count())
315
+ ],
316
+ "cuda_version": torch.version.cuda,
317
+ }
318
+ else:
319
+ gpu_info = {"gpu_available": False}
320
+ except ImportError:
321
+ gpu_info = {"gpu_available": False, "torch_available": False}
322
+
323
+ return {
324
+ "cpu": {
325
+ "count_physical": cpu_count,
326
+ "count_logical": cpu_count_logical,
327
+ "frequency": psutil.cpu_freq()._asdict() if psutil.cpu_freq() else None,
328
+ },
329
+ "memory": {
330
+ "total": memory.total,
331
+ "available": memory.available,
332
+ "percent": memory.percent,
333
+ },
334
+ "disk": {
335
+ "total": disk.total,
336
+ "used": disk.used,
337
+ "free": disk.free,
338
+ "percent": (disk.used / disk.total) * 100,
339
+ },
340
+ "gpu": gpu_info,
341
+ "python_version": getattr(psutil, "sys", {}).get("version", "unknown"),
342
+ "platform": getattr(psutil, "os", {}).get("name", "unknown"),
343
+ }
344
+ except Exception as e:
345
+ logging.getLogger(__name__).error(f"Failed to get system info: {e}")
346
+ return {"error": str(e)}
347
+
348
+
349
+ def format_bytes(bytes_value: int | float) -> str:
350
+ """Format bytes into human readable string."""
351
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
352
+ if bytes_value < 1024.0:
353
+ return f"{bytes_value:.1f} {unit}"
354
+ bytes_value /= 1024.0
355
+ return f"{bytes_value:.1f} PB"
356
+
357
+
358
+ def format_duration(seconds: float) -> str:
359
+ """Format duration into human readable string."""
360
+ if seconds < 60:
361
+ return f"{seconds:.2f}s"
362
+ elif seconds < 3600:
363
+ minutes = seconds / 60
364
+ return f"{minutes:.1f}m"
365
+ elif seconds < 86400:
366
+ hours = seconds / 3600
367
+ return f"{hours:.1f}h"
368
+ else:
369
+ days = seconds / 86400
370
+ return f"{days:.1f}d"
371
+
372
+
373
+ def safe_divide(numerator: float, denominator: float, default: float = 0) -> float:
374
+ """Safely divide two numbers, returning default if denominator is zero."""
375
+ try:
376
+ return numerator / denominator if denominator != 0 else default
377
+ except (TypeError, ValueError):
378
+ return default
379
+
380
+
381
+ def clamp(value: float, min_val: float, max_val: float) -> float:
382
+ """Clamp value between min and max."""
383
+ return max(min_val, min(value, max_val))
384
+
385
+
386
+ def exponential_backoff(
387
+ attempt: int, base_delay: float = 1.0, max_delay: float = 60.0
388
+ ) -> float:
389
+ """Calculate exponential backoff delay."""
390
+ delay = base_delay * (2**attempt)
391
+ return float(min(delay, max_delay))
392
+
393
+
394
+ class DebounceTimer:
395
+ """Debounce timer for limiting rapid operations."""
396
+
397
+ def __init__(self, delay: float):
398
+ self.delay = delay
399
+ self.last_call = 0.0
400
+ self.timer: threading.Timer | None = None
401
+ self.lock = threading.Lock()
402
+
403
+ def call(self, func: Callable, *args, **kwargs):
404
+ """Call function with debouncing."""
405
+ with self.lock:
406
+ current_time = time.time()
407
+
408
+ # Cancel existing timer
409
+ if self.timer:
410
+ self.timer.cancel()
411
+
412
+ # Schedule new call
413
+ time_since_last = current_time - self.last_call
414
+ if time_since_last >= self.delay:
415
+ # Call immediately
416
+ self.last_call = current_time
417
+ func(*args, **kwargs)
418
+ else:
419
+ # Schedule for later
420
+ remaining_delay = self.delay - time_since_last
421
+ self.timer = threading.Timer(
422
+ remaining_delay, self._delayed_call, args=[func, args, kwargs]
423
+ )
424
+ self.timer.start()
425
+
426
+ def _delayed_call(self, func: Callable, args: tuple, kwargs: dict):
427
+ """Execute delayed call."""
428
+ with self.lock:
429
+ self.last_call = time.time()
430
+ self.timer = None # Clear the timer reference
431
+ func(*args, **kwargs)
432
+
433
+
434
+ class ThresholdMonitor:
435
+ """Monitor values against thresholds with hysteresis."""
436
+
437
+ def __init__(self, threshold: float, hysteresis: float = 0.1):
438
+ self.threshold = threshold
439
+ self.hysteresis = hysteresis
440
+ self.triggered = False
441
+ self.last_value: float | None = None
442
+ self.trigger_count = 0
443
+ self.last_trigger_time: float | None = None
444
+
445
+ def check(self, value: float) -> bool:
446
+ """Check value against threshold. Returns True if threshold is crossed."""
447
+ self.last_value = value
448
+ current_time = time.time()
449
+
450
+ if not self.triggered:
451
+ # Check for threshold breach
452
+ if value > self.threshold:
453
+ self.triggered = True
454
+ self.trigger_count += 1
455
+ self.last_trigger_time = float(current_time)
456
+ return True
457
+ else:
458
+ # Check for recovery (with hysteresis)
459
+ if value < (self.threshold - self.hysteresis):
460
+ self.triggered = False
461
+
462
+ return False
463
+
464
+ def get_stats(self) -> dict[str, Any]:
465
+ """Get threshold monitor statistics."""
466
+ return {
467
+ "threshold": self.threshold,
468
+ "hysteresis": self.hysteresis,
469
+ "triggered": self.triggered,
470
+ "last_value": self.last_value,
471
+ "trigger_count": self.trigger_count,
472
+ "last_trigger_time": self.last_trigger_time,
473
+ }
474
+
475
+
476
+ # Error handling utilities
477
+ class MonitoringError(Exception):
478
+ """Base exception for monitoring operations."""
479
+
480
+ pass
481
+
482
+
483
+ class MetricsCollectionError(MonitoringError):
484
+ """Error during metrics collection."""
485
+
486
+ pass
487
+
488
+
489
+ class ExportError(MonitoringError):
490
+ """Error during metrics export."""
491
+
492
+ pass
493
+
494
+
495
+ class HealthCheckError(MonitoringError):
496
+ """Error during health check."""
497
+
498
+ pass
499
+
500
+
501
+ def retry_with_backoff(
502
+ max_attempts: int = 3, base_delay: float = 1.0, exceptions: tuple = (Exception,)
503
+ ):
504
+ """Decorator for retrying operations with exponential backoff."""
505
+
506
+ def decorator(func):
507
+ @wraps(func)
508
+ def wrapper(*args, **kwargs):
509
+ last_exception = None
510
+
511
+ for attempt in range(max_attempts):
512
+ try:
513
+ return func(*args, **kwargs)
514
+ except exceptions as e:
515
+ last_exception = e
516
+ if attempt < max_attempts - 1:
517
+ delay = exponential_backoff(attempt, base_delay)
518
+ time.sleep(delay)
519
+ else:
520
+ break
521
+
522
+ if last_exception:
523
+ raise last_exception
524
+ raise RuntimeError("Failed after all retry attempts")
525
+
526
+ return wrapper
527
+
528
+ return decorator
529
+
530
+
531
+ def log_exceptions(
532
+ logger: logging.Logger | None = None,
533
+ level: int = logging.ERROR,
534
+ reraise: bool = True,
535
+ ):
536
+ """Decorator for logging exceptions."""
537
+ if logger is None:
538
+ logger = logging.getLogger(__name__)
539
+
540
+ def decorator(func):
541
+ @wraps(func)
542
+ def wrapper(*args, **kwargs):
543
+ try:
544
+ return func(*args, **kwargs)
545
+ except Exception as e:
546
+ logger.log(level, f"Exception in {func.__name__}: {e}", exc_info=True)
547
+ if reraise:
548
+ raise
549
+ return None
550
+
551
+ return wrapper
552
+
553
+ return decorator
@@ -0,0 +1,12 @@
1
+ """Plugin template namespace (`invarlock.plugins`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invarlock.core.abi import INVARLOCK_CORE_ABI as INVARLOCK_CORE_ABI
6
+
7
+ from .hello_guard import HelloGuard
8
+
9
+ __all__ = [
10
+ "HelloGuard",
11
+ "INVARLOCK_CORE_ABI",
12
+ ]
@@ -0,0 +1,33 @@
1
+ """Template guard plugin for entry point demonstrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from invarlock.core.api import Guard, ModelAdapter
8
+
9
+
10
+ class HelloGuard(Guard):
11
+ """Simple guard that checks a score in the validation context."""
12
+
13
+ name = "hello_guard"
14
+
15
+ def __init__(self, threshold: float = 1.0):
16
+ self.threshold = float(threshold)
17
+
18
+ def validate(
19
+ self,
20
+ model: Any,
21
+ adapter: ModelAdapter,
22
+ context: dict[str, Any],
23
+ ) -> dict[str, Any]:
24
+ score = float(context.get("hello_score", 0.0))
25
+ passed = score <= self.threshold
26
+ return {
27
+ "passed": passed,
28
+ "action": "warn" if passed else "abort",
29
+ "message": f"Hello guard score {score:.3f} (threshold {self.threshold:.3f})",
30
+ "metrics": {
31
+ "score": score,
32
+ },
33
+ }
@@ -0,0 +1,82 @@
1
+ """
2
+ HuggingFace AWQ Adapter (plugin)
3
+ ================================
4
+
5
+ Optional adapter for loading AWQ-quantized causal LMs from the Hub.
6
+ Requires the `autoawq` extra on supported platforms (typically Linux/CUDA).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from invarlock.adapters.hf_mixin import HFAdapterMixin
14
+ from invarlock.core.api import ModelAdapter
15
+ from invarlock.core.error_utils import wrap_errors
16
+ from invarlock.core.exceptions import DependencyError, ModelLoadError
17
+
18
+
19
+ class HF_AWQ_Adapter(HFAdapterMixin, ModelAdapter):
20
+ name = "hf_awq"
21
+
22
+ def load_model(self, model_id: str, device: str = "auto", **kwargs: Any):
23
+ # Try common import paths used by AWQ projects
24
+ AutoAWQForCausalLM = None
25
+ with wrap_errors(
26
+ DependencyError,
27
+ "E203",
28
+ "DEPENDENCY-MISSING: autoawq/awq",
29
+ lambda e: {"dependency": "autoawq/awq"},
30
+ ):
31
+ for mod_path, attr in (
32
+ ("autoawq", "AutoAWQForCausalLM"),
33
+ ("awq", "AutoAWQForCausalLM"),
34
+ ):
35
+ try: # pragma: no cover - exercised in integration
36
+ mod = __import__(mod_path, fromlist=[attr])
37
+ AutoAWQForCausalLM = getattr(mod, attr)
38
+ break
39
+ except Exception:
40
+ continue
41
+
42
+ if AutoAWQForCausalLM is None: # pragma: no cover
43
+ # wrap_errors above will have raised; this is a safety
44
+ raise DependencyError(
45
+ code="E203", message="DEPENDENCY-MISSING: autoawq/awq"
46
+ )
47
+
48
+ with wrap_errors(
49
+ ModelLoadError,
50
+ "E201",
51
+ "MODEL-LOAD-FAILED: awq",
52
+ lambda e: {"model_id": model_id},
53
+ ):
54
+ model = AutoAWQForCausalLM.from_quantized(
55
+ model_id,
56
+ trust_remote_code=True,
57
+ **{k: v for k, v in kwargs.items() if k != "device"},
58
+ )
59
+ return model.to(self._resolve_device(device))
60
+
61
+ def can_handle(self, model: Any) -> bool:
62
+ cfg = getattr(model, "config", None)
63
+ return hasattr(cfg, "n_layer") or hasattr(cfg, "num_hidden_layers")
64
+
65
+ def describe(self, model: Any) -> dict[str, Any]:
66
+ cfg = getattr(model, "config", None)
67
+ n_layer = int(
68
+ getattr(cfg, "n_layer", getattr(cfg, "num_hidden_layers", 0)) or 0
69
+ )
70
+ n_head = int(
71
+ getattr(cfg, "n_head", getattr(cfg, "num_attention_heads", 0)) or 0
72
+ )
73
+ heads = [n_head] * n_layer if n_layer and n_head else []
74
+ return {
75
+ "n_layer": n_layer,
76
+ "heads_per_layer": heads,
77
+ "mlp_dims": [],
78
+ "tying": {},
79
+ }
80
+
81
+
82
+ __all__ = ["HF_AWQ_Adapter"]