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.
- invarlock/__init__.py +33 -0
- invarlock/__main__.py +10 -0
- invarlock/_data/runtime/profiles/ci_cpu.yaml +15 -0
- invarlock/_data/runtime/profiles/release.yaml +23 -0
- invarlock/_data/runtime/tiers.yaml +76 -0
- invarlock/adapters/__init__.py +102 -0
- invarlock/adapters/_capabilities.py +45 -0
- invarlock/adapters/auto.py +99 -0
- invarlock/adapters/base.py +530 -0
- invarlock/adapters/base_types.py +85 -0
- invarlock/adapters/hf_bert.py +852 -0
- invarlock/adapters/hf_gpt2.py +403 -0
- invarlock/adapters/hf_llama.py +485 -0
- invarlock/adapters/hf_mixin.py +383 -0
- invarlock/adapters/hf_onnx.py +112 -0
- invarlock/adapters/hf_t5.py +137 -0
- invarlock/adapters/py.typed +1 -0
- invarlock/assurance/__init__.py +43 -0
- invarlock/cli/__init__.py +8 -0
- invarlock/cli/__main__.py +8 -0
- invarlock/cli/_evidence.py +25 -0
- invarlock/cli/_json.py +75 -0
- invarlock/cli/adapter_auto.py +162 -0
- invarlock/cli/app.py +287 -0
- invarlock/cli/commands/__init__.py +26 -0
- invarlock/cli/commands/certify.py +403 -0
- invarlock/cli/commands/doctor.py +1358 -0
- invarlock/cli/commands/explain_gates.py +151 -0
- invarlock/cli/commands/export_html.py +100 -0
- invarlock/cli/commands/plugins.py +1331 -0
- invarlock/cli/commands/report.py +354 -0
- invarlock/cli/commands/run.py +4146 -0
- invarlock/cli/commands/verify.py +1040 -0
- invarlock/cli/config.py +396 -0
- invarlock/cli/constants.py +68 -0
- invarlock/cli/device.py +92 -0
- invarlock/cli/doctor_helpers.py +74 -0
- invarlock/cli/errors.py +6 -0
- invarlock/cli/overhead_utils.py +60 -0
- invarlock/cli/provenance.py +66 -0
- invarlock/cli/utils.py +41 -0
- invarlock/config.py +56 -0
- invarlock/core/__init__.py +62 -0
- invarlock/core/abi.py +15 -0
- invarlock/core/api.py +274 -0
- invarlock/core/auto_tuning.py +317 -0
- invarlock/core/bootstrap.py +226 -0
- invarlock/core/checkpoint.py +221 -0
- invarlock/core/contracts.py +73 -0
- invarlock/core/error_utils.py +64 -0
- invarlock/core/events.py +298 -0
- invarlock/core/exceptions.py +95 -0
- invarlock/core/registry.py +481 -0
- invarlock/core/retry.py +146 -0
- invarlock/core/runner.py +2041 -0
- invarlock/core/types.py +154 -0
- invarlock/edits/__init__.py +12 -0
- invarlock/edits/_edit_utils.py +249 -0
- invarlock/edits/_external_utils.py +268 -0
- invarlock/edits/noop.py +47 -0
- invarlock/edits/py.typed +1 -0
- invarlock/edits/quant_rtn.py +801 -0
- invarlock/edits/registry.py +166 -0
- invarlock/eval/__init__.py +23 -0
- invarlock/eval/bench.py +1207 -0
- invarlock/eval/bootstrap.py +50 -0
- invarlock/eval/data.py +2052 -0
- invarlock/eval/metrics.py +2167 -0
- invarlock/eval/primary_metric.py +767 -0
- invarlock/eval/probes/__init__.py +24 -0
- invarlock/eval/probes/fft.py +139 -0
- invarlock/eval/probes/mi.py +213 -0
- invarlock/eval/probes/post_attention.py +323 -0
- invarlock/eval/providers/base.py +67 -0
- invarlock/eval/providers/seq2seq.py +111 -0
- invarlock/eval/providers/text_lm.py +113 -0
- invarlock/eval/providers/vision_text.py +93 -0
- invarlock/eval/py.typed +1 -0
- invarlock/guards/__init__.py +18 -0
- invarlock/guards/_contracts.py +9 -0
- invarlock/guards/invariants.py +640 -0
- invarlock/guards/policies.py +805 -0
- invarlock/guards/py.typed +1 -0
- invarlock/guards/rmt.py +2097 -0
- invarlock/guards/spectral.py +1419 -0
- invarlock/guards/tier_config.py +354 -0
- invarlock/guards/variance.py +3298 -0
- invarlock/guards_ref/__init__.py +15 -0
- invarlock/guards_ref/rmt_ref.py +40 -0
- invarlock/guards_ref/spectral_ref.py +135 -0
- invarlock/guards_ref/variance_ref.py +60 -0
- invarlock/model_profile.py +353 -0
- invarlock/model_utils.py +221 -0
- invarlock/observability/__init__.py +10 -0
- invarlock/observability/alerting.py +535 -0
- invarlock/observability/core.py +546 -0
- invarlock/observability/exporters.py +565 -0
- invarlock/observability/health.py +588 -0
- invarlock/observability/metrics.py +457 -0
- invarlock/observability/py.typed +1 -0
- invarlock/observability/utils.py +553 -0
- invarlock/plugins/__init__.py +12 -0
- invarlock/plugins/hello_guard.py +33 -0
- invarlock/plugins/hf_awq_adapter.py +82 -0
- invarlock/plugins/hf_bnb_adapter.py +79 -0
- invarlock/plugins/hf_gptq_adapter.py +78 -0
- invarlock/plugins/py.typed +1 -0
- invarlock/py.typed +1 -0
- invarlock/reporting/__init__.py +7 -0
- invarlock/reporting/certificate.py +3221 -0
- invarlock/reporting/certificate_schema.py +244 -0
- invarlock/reporting/dataset_hashing.py +215 -0
- invarlock/reporting/guards_analysis.py +948 -0
- invarlock/reporting/html.py +32 -0
- invarlock/reporting/normalizer.py +235 -0
- invarlock/reporting/policy_utils.py +517 -0
- invarlock/reporting/primary_metric_utils.py +265 -0
- invarlock/reporting/render.py +1442 -0
- invarlock/reporting/report.py +903 -0
- invarlock/reporting/report_types.py +278 -0
- invarlock/reporting/utils.py +175 -0
- invarlock/reporting/validate.py +631 -0
- invarlock/security.py +176 -0
- invarlock/sparsity_utils.py +323 -0
- invarlock/utils/__init__.py +150 -0
- invarlock/utils/digest.py +45 -0
- invarlock-0.2.0.dist-info/METADATA +586 -0
- invarlock-0.2.0.dist-info/RECORD +132 -0
- invarlock-0.2.0.dist-info/WHEEL +5 -0
- invarlock-0.2.0.dist-info/entry_points.txt +20 -0
- invarlock-0.2.0.dist-info/licenses/LICENSE +201 -0
- 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"]
|