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,383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared HuggingFace adapter mixin.
|
|
3
|
+
=================================
|
|
4
|
+
|
|
5
|
+
Provides reusable functionality for InvarLock's HuggingFace adapters:
|
|
6
|
+
- Device resolution helpers
|
|
7
|
+
- Snapshot/restore with device awareness
|
|
8
|
+
- Chunked snapshot helpers to reduce peak memory usage
|
|
9
|
+
- Lightweight config serialization
|
|
10
|
+
- Weight-tying detection plumbing
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import torch
|
|
23
|
+
|
|
24
|
+
from invarlock.security import is_secure_path
|
|
25
|
+
|
|
26
|
+
SCALAR_TYPES = (int, float, str, bool)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sanitize_param_name(name: str) -> str:
|
|
30
|
+
"""Return a filesystem-safe parameter name."""
|
|
31
|
+
return name.replace(".", "__").replace("/", "_")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_secure_dir(path: Path) -> None:
|
|
35
|
+
"""Ensure snapshot directory uses 0o700 permissions."""
|
|
36
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
os.chmod(path, 0o700)
|
|
38
|
+
if not is_secure_path(path):
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
f"Snapshot directory {path} must have permissions 0o700 for security."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_named_parameter(
|
|
45
|
+
module: torch.nn.Module, path: str
|
|
46
|
+
) -> torch.nn.Parameter | None:
|
|
47
|
+
"""Resolve a parameter by dotted path, returning None if missing."""
|
|
48
|
+
current: Any = module
|
|
49
|
+
parts = path.split(".")
|
|
50
|
+
for name in parts[:-1]:
|
|
51
|
+
current = getattr(current, name, None)
|
|
52
|
+
if current is None:
|
|
53
|
+
return None
|
|
54
|
+
leaf = getattr(current, parts[-1], None)
|
|
55
|
+
if isinstance(leaf, torch.nn.Parameter):
|
|
56
|
+
return leaf
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HFAdapterMixin:
|
|
61
|
+
"""Reusable utilities for HuggingFace-backed adapters."""
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# Device helpers
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
def _resolve_device(
|
|
67
|
+
self, device: str | torch.device | None = "auto"
|
|
68
|
+
) -> torch.device:
|
|
69
|
+
"""
|
|
70
|
+
Resolve a target torch.device for model placement.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
device: Requested device ("auto" selects CUDA→MPS→CPU).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
torch.device for placement.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if isinstance(device, torch.device):
|
|
80
|
+
return device
|
|
81
|
+
|
|
82
|
+
device_str = "auto" if device is None else str(device)
|
|
83
|
+
device_str = device_str.lower()
|
|
84
|
+
|
|
85
|
+
if device_str == "auto":
|
|
86
|
+
if torch.cuda.is_available():
|
|
87
|
+
return torch.device("cuda")
|
|
88
|
+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
89
|
+
return torch.device("mps")
|
|
90
|
+
return torch.device("cpu")
|
|
91
|
+
|
|
92
|
+
return torch.device(device_str)
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# HF save/export helpers
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
def save_pretrained(self, model: torch.nn.Module, path: str | Path) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Save a HuggingFace model in a HF-loadable directory.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
model: HF Transformers model implementing save_pretrained
|
|
103
|
+
path: Target directory path
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True on success, False otherwise
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
p = Path(path)
|
|
110
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
# Most HF models implement save_pretrained
|
|
112
|
+
save = getattr(model, "save_pretrained", None)
|
|
113
|
+
if callable(save):
|
|
114
|
+
save(str(p))
|
|
115
|
+
return True
|
|
116
|
+
except Exception:
|
|
117
|
+
return False
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Snapshot / restore
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
def snapshot(self, model: torch.nn.Module) -> bytes:
|
|
124
|
+
"""
|
|
125
|
+
Serialize model state with device awareness and weight-tying metadata.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
model: HuggingFace model instance.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Bytes payload produced by torch.save.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
state_dict: dict[str, Any] = {}
|
|
135
|
+
device_map: dict[str, str] = {}
|
|
136
|
+
|
|
137
|
+
for name, param in model.named_parameters():
|
|
138
|
+
state_key = f"params.{name}"
|
|
139
|
+
state_dict[state_key] = param.detach().cpu().clone()
|
|
140
|
+
device_map[state_key] = str(param.device)
|
|
141
|
+
|
|
142
|
+
for name, buffer in model.named_buffers():
|
|
143
|
+
state_key = f"buffers.{name}"
|
|
144
|
+
state_dict[state_key] = buffer.detach().cpu().clone()
|
|
145
|
+
device_map[state_key] = str(buffer.device)
|
|
146
|
+
|
|
147
|
+
if hasattr(model, "config"):
|
|
148
|
+
state_dict["config"] = self._serialize_config(model.config)
|
|
149
|
+
|
|
150
|
+
state_dict["device_map"] = device_map
|
|
151
|
+
state_dict["model_class"] = model.__class__.__name__
|
|
152
|
+
state_dict["weight_tying"] = self._extract_weight_tying_info(model)
|
|
153
|
+
|
|
154
|
+
buffer = io.BytesIO()
|
|
155
|
+
torch.save(state_dict, buffer)
|
|
156
|
+
return buffer.getvalue()
|
|
157
|
+
|
|
158
|
+
def restore(self, model: torch.nn.Module, blob: bytes) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Restore model state produced by `snapshot`.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
model: Model to restore in-place.
|
|
164
|
+
blob: Bytes payload from snapshot.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
buffer = io.BytesIO(blob)
|
|
168
|
+
state_dict = torch.load(buffer, map_location="cpu", weights_only=False)
|
|
169
|
+
|
|
170
|
+
device_map: dict[str, str] = state_dict.get("device_map", {})
|
|
171
|
+
|
|
172
|
+
for name, param in model.named_parameters():
|
|
173
|
+
state_key = f"params.{name}"
|
|
174
|
+
if state_key not in state_dict:
|
|
175
|
+
continue
|
|
176
|
+
target_device = torch.device(device_map.get(state_key, "cpu"))
|
|
177
|
+
with torch.no_grad():
|
|
178
|
+
param.copy_(state_dict[state_key].to(target_device))
|
|
179
|
+
|
|
180
|
+
for name, buffer_param in model.named_buffers():
|
|
181
|
+
state_key = f"buffers.{name}"
|
|
182
|
+
if state_key not in state_dict:
|
|
183
|
+
continue
|
|
184
|
+
target_device = torch.device(device_map.get(state_key, "cpu"))
|
|
185
|
+
buffer_param.copy_(state_dict[state_key].to(target_device))
|
|
186
|
+
|
|
187
|
+
original_tying = state_dict.get("weight_tying", {})
|
|
188
|
+
if isinstance(original_tying, dict) and original_tying:
|
|
189
|
+
current_tying = self._extract_weight_tying_info(model)
|
|
190
|
+
for tied_param, source_param in original_tying.items():
|
|
191
|
+
if current_tying.get(tied_param) != source_param:
|
|
192
|
+
self._restore_weight_tying(model, tied_param, source_param)
|
|
193
|
+
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
# Chunked snapshot helpers
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
def snapshot_chunked(
|
|
198
|
+
self, model: torch.nn.Module, *, prefix: str = "invarlock-snap-"
|
|
199
|
+
) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Create a chunked snapshot on disk to minimise in-memory footprint.
|
|
202
|
+
|
|
203
|
+
Each parameter and buffer is serialized individually so only a single
|
|
204
|
+
tensor resides in memory at a time. Metadata is recorded in manifest.json.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
snapshot_dir = Path(tempfile.mkdtemp(prefix=prefix))
|
|
208
|
+
_ensure_secure_dir(snapshot_dir)
|
|
209
|
+
|
|
210
|
+
manifest: dict[str, Any] = {
|
|
211
|
+
"model_class": model.__class__.__name__,
|
|
212
|
+
"config": self._serialize_config(model.config)
|
|
213
|
+
if hasattr(model, "config")
|
|
214
|
+
else {},
|
|
215
|
+
"params": {},
|
|
216
|
+
"buffers": {},
|
|
217
|
+
"device_map": {},
|
|
218
|
+
"weight_tying": self._extract_weight_tying_info(model),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for name, param in model.named_parameters():
|
|
222
|
+
filename = f"param__{_sanitize_param_name(name)}.pt"
|
|
223
|
+
file_path = snapshot_dir / filename
|
|
224
|
+
torch.save(param.detach().cpu(), file_path)
|
|
225
|
+
manifest["params"][name] = filename
|
|
226
|
+
manifest["device_map"][name] = str(param.device)
|
|
227
|
+
|
|
228
|
+
for name, buffer in model.named_buffers():
|
|
229
|
+
filename = f"buffer__{_sanitize_param_name(name)}.pt"
|
|
230
|
+
file_path = snapshot_dir / filename
|
|
231
|
+
torch.save(buffer.detach().cpu(), file_path)
|
|
232
|
+
manifest["buffers"][name] = filename
|
|
233
|
+
manifest["device_map"][f"buffer::{name}"] = str(buffer.device)
|
|
234
|
+
|
|
235
|
+
manifest_path = snapshot_dir / "manifest.json"
|
|
236
|
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
237
|
+
return str(snapshot_dir)
|
|
238
|
+
|
|
239
|
+
def restore_chunked(self, model: torch.nn.Module, snapshot_path: str) -> None:
|
|
240
|
+
"""
|
|
241
|
+
Restore a chunked snapshot produced by `snapshot_chunked`.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
model: Model to restore in-place.
|
|
245
|
+
snapshot_path: Directory path created by `snapshot_chunked`.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
snapshot_dir = Path(snapshot_path)
|
|
249
|
+
manifest_path = snapshot_dir / "manifest.json"
|
|
250
|
+
if not manifest_path.exists():
|
|
251
|
+
raise FileNotFoundError(f"Missing manifest for snapshot at {snapshot_path}")
|
|
252
|
+
|
|
253
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
254
|
+
param_map = dict(model.named_parameters())
|
|
255
|
+
buffer_map = dict(model.named_buffers())
|
|
256
|
+
|
|
257
|
+
device_map = manifest.get("device_map", {})
|
|
258
|
+
|
|
259
|
+
for name, filename in manifest.get("params", {}).items():
|
|
260
|
+
file_path = snapshot_dir / filename
|
|
261
|
+
if name not in param_map or not file_path.exists():
|
|
262
|
+
continue
|
|
263
|
+
target = param_map[name]
|
|
264
|
+
target_device = torch.device(device_map.get(name, str(target.device)))
|
|
265
|
+
tensor = torch.load(file_path, map_location="cpu")
|
|
266
|
+
with torch.no_grad():
|
|
267
|
+
target.copy_(tensor.to(target_device))
|
|
268
|
+
|
|
269
|
+
for name, filename in manifest.get("buffers", {}).items():
|
|
270
|
+
file_path = snapshot_dir / filename
|
|
271
|
+
if name not in buffer_map or not file_path.exists():
|
|
272
|
+
continue
|
|
273
|
+
target = buffer_map[name]
|
|
274
|
+
key = f"buffer::{name}"
|
|
275
|
+
target_device = torch.device(device_map.get(key, str(target.device)))
|
|
276
|
+
tensor = torch.load(file_path, map_location="cpu")
|
|
277
|
+
target.copy_(tensor.to(target_device))
|
|
278
|
+
|
|
279
|
+
original_tying = manifest.get("weight_tying", {})
|
|
280
|
+
if isinstance(original_tying, dict) and original_tying:
|
|
281
|
+
current_tying = self._extract_weight_tying_info(model)
|
|
282
|
+
for tied_param, source_param in original_tying.items():
|
|
283
|
+
if current_tying.get(tied_param) != source_param:
|
|
284
|
+
self._restore_weight_tying(model, tied_param, source_param)
|
|
285
|
+
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
# Weight-tying hooks (overridden by concrete adapters)
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
def _extract_weight_tying_info(self, model: torch.nn.Module) -> dict[str, str]:
|
|
290
|
+
"""Return mapping of tied parameter names to source parameter names."""
|
|
291
|
+
|
|
292
|
+
tying: dict[str, str] = {}
|
|
293
|
+
param_names = set(dict(model.named_parameters()).keys())
|
|
294
|
+
|
|
295
|
+
if "lm_head.weight" in param_names and "transformer.wte.weight" in param_names:
|
|
296
|
+
tying["lm_head.weight"] = "transformer.wte.weight"
|
|
297
|
+
|
|
298
|
+
decoder_name = "cls.predictions.decoder.weight"
|
|
299
|
+
if decoder_name in param_names:
|
|
300
|
+
for candidate in (
|
|
301
|
+
"bert.embeddings.word_embeddings.weight",
|
|
302
|
+
"embeddings.word_embeddings.weight",
|
|
303
|
+
):
|
|
304
|
+
if candidate in param_names:
|
|
305
|
+
tying[decoder_name] = candidate
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
return tying
|
|
309
|
+
|
|
310
|
+
def _restore_weight_tying(
|
|
311
|
+
self, model: torch.nn.Module, tied_param: str, source_param: str
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Restore a weight-tying relationship (no-op by default)."""
|
|
314
|
+
model_params = dict(model.named_parameters())
|
|
315
|
+
tied = model_params.get(tied_param)
|
|
316
|
+
source = model_params.get(source_param)
|
|
317
|
+
if tied is None or source is None:
|
|
318
|
+
return
|
|
319
|
+
with torch.no_grad():
|
|
320
|
+
tied.copy_(source)
|
|
321
|
+
|
|
322
|
+
def validate_weight_tying(self, model: torch.nn.Module) -> None:
|
|
323
|
+
"""Raise if a known weight-tying relationship has been broken."""
|
|
324
|
+
tying = self._extract_weight_tying_info(model)
|
|
325
|
+
if not tying:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
model_params = dict(model.named_parameters())
|
|
329
|
+
for tied_param, source_param in tying.items():
|
|
330
|
+
tied = model_params.get(tied_param)
|
|
331
|
+
source = model_params.get(source_param)
|
|
332
|
+
if tied is None:
|
|
333
|
+
tied = _resolve_named_parameter(model, tied_param)
|
|
334
|
+
if source is None:
|
|
335
|
+
source = _resolve_named_parameter(model, source_param)
|
|
336
|
+
if tied is None or source is None:
|
|
337
|
+
from invarlock.core.exceptions import AdapterError
|
|
338
|
+
|
|
339
|
+
raise AdapterError(
|
|
340
|
+
code="E202",
|
|
341
|
+
message="ADAPTER-STRUCTURE-INVALID: missing tied/source parameter",
|
|
342
|
+
details={
|
|
343
|
+
"tied_param": tied_param,
|
|
344
|
+
"source_param": source_param,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
if not torch.allclose(tied, source):
|
|
348
|
+
from invarlock.core.exceptions import AdapterError
|
|
349
|
+
|
|
350
|
+
raise AdapterError(
|
|
351
|
+
code="E202",
|
|
352
|
+
message="ADAPTER-STRUCTURE-INVALID: weight-tying invariant violated",
|
|
353
|
+
details={
|
|
354
|
+
"tied_param": tied_param,
|
|
355
|
+
"source_param": source_param,
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# ------------------------------------------------------------------
|
|
360
|
+
# Helper utilities
|
|
361
|
+
# ------------------------------------------------------------------
|
|
362
|
+
def _serialize_config(self, config: Any) -> dict[str, Any]:
|
|
363
|
+
"""Serialize HuggingFace config fields into simple Python types."""
|
|
364
|
+
|
|
365
|
+
result: dict[str, Any] = {}
|
|
366
|
+
for key in dir(config):
|
|
367
|
+
if key.startswith("_"):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
value = getattr(config, key)
|
|
372
|
+
except AttributeError:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
if callable(value):
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
if value is None or isinstance(value, SCALAR_TYPES):
|
|
379
|
+
result[key] = value
|
|
380
|
+
elif isinstance(value, list | dict):
|
|
381
|
+
result[key] = value
|
|
382
|
+
|
|
383
|
+
return result
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HuggingFace Optimum ONNX Runtime Adapter
|
|
3
|
+
=======================================
|
|
4
|
+
|
|
5
|
+
Minimal adapter to load CPU-friendly ONNX Runtime models via Optimum for
|
|
6
|
+
causal language modeling. This enables certification of pre-quantized int8
|
|
7
|
+
models published for ONNX/Optimum, when used with `edit: noop`.
|
|
8
|
+
|
|
9
|
+
Notes
|
|
10
|
+
- This adapter targets inference (perplexity/log-loss) and does not expose
|
|
11
|
+
parameter/module traversal; guards that require PyTorch module access may
|
|
12
|
+
be inapplicable for ONNX models.
|
|
13
|
+
- Optional dependency: `optimum[onnxruntime]` and `onnxruntime`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from invarlock.core.api import ModelAdapter
|
|
21
|
+
from invarlock.core.error_utils import wrap_errors
|
|
22
|
+
from invarlock.core.exceptions import DependencyError, ModelLoadError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HF_ORT_CausalLM_Adapter(ModelAdapter):
|
|
26
|
+
"""Optimum/ONNXRuntime causal LM adapter.
|
|
27
|
+
|
|
28
|
+
Provides a lightweight bridge that loads an ORTModelForCausalLM and
|
|
29
|
+
presents enough interface compatibility for evaluation paths that operate
|
|
30
|
+
on logits (perplexity). Snapshot/restore are not supported and will fall
|
|
31
|
+
back to reload in the CLI runner.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name = "hf_onnx"
|
|
35
|
+
|
|
36
|
+
# --- Loading ---------------------------------------------------------
|
|
37
|
+
def load_model(self, model_id: str, device: str = "cpu", **kwargs: Any): # type: ignore[override]
|
|
38
|
+
with wrap_errors(
|
|
39
|
+
DependencyError,
|
|
40
|
+
"E203",
|
|
41
|
+
"DEPENDENCY-MISSING: optimum/onnxruntime",
|
|
42
|
+
lambda e: {"dependency": "optimum[onnxruntime]"},
|
|
43
|
+
):
|
|
44
|
+
from optimum.onnxruntime import ORTModelForCausalLM # type: ignore
|
|
45
|
+
|
|
46
|
+
# Prefer CPUExecutionProvider by default; callers may override via kwargs
|
|
47
|
+
providers = kwargs.pop("providers", None)
|
|
48
|
+
if providers is None:
|
|
49
|
+
providers = ["CPUExecutionProvider"]
|
|
50
|
+
|
|
51
|
+
# Trust remote code where necessary; users can set to False via kwargs
|
|
52
|
+
trust_remote_code = kwargs.pop("trust_remote_code", True)
|
|
53
|
+
|
|
54
|
+
# Some repos use non-default file names; accept overrides but default to standard
|
|
55
|
+
file_name = kwargs.pop("file_name", None)
|
|
56
|
+
|
|
57
|
+
with wrap_errors(
|
|
58
|
+
ModelLoadError,
|
|
59
|
+
"E201",
|
|
60
|
+
"MODEL-LOAD-FAILED: optimum ORTModelForCausalLM",
|
|
61
|
+
lambda e: {"model_id": model_id},
|
|
62
|
+
):
|
|
63
|
+
model = ORTModelForCausalLM.from_pretrained(
|
|
64
|
+
model_id,
|
|
65
|
+
file_name=file_name,
|
|
66
|
+
provider="CPUExecutionProvider"
|
|
67
|
+
if providers == ["CPUExecutionProvider"]
|
|
68
|
+
else None,
|
|
69
|
+
providers=providers,
|
|
70
|
+
trust_remote_code=trust_remote_code,
|
|
71
|
+
**kwargs,
|
|
72
|
+
)
|
|
73
|
+
# ORT models manage device internally; return as-is
|
|
74
|
+
return model
|
|
75
|
+
|
|
76
|
+
# --- Capability probes ----------------------------------------------
|
|
77
|
+
def can_handle(self, model: Any) -> bool: # type: ignore[override]
|
|
78
|
+
cls_name = model.__class__.__name__
|
|
79
|
+
mod_name = getattr(model.__class__, "__module__", "")
|
|
80
|
+
if "optimum.onnxruntime" in mod_name and "ORTModel" in cls_name:
|
|
81
|
+
return True
|
|
82
|
+
# Heuristic: object exposes generate and is directly callable
|
|
83
|
+
return hasattr(model, "generate") and callable(model)
|
|
84
|
+
|
|
85
|
+
# --- Structure description (best-effort) -----------------------------
|
|
86
|
+
def describe(self, model: Any) -> dict[str, Any]: # type: ignore[override]
|
|
87
|
+
# Attempt to read from HF config when present
|
|
88
|
+
cfg = getattr(model, "config", None)
|
|
89
|
+
n_layer = int(
|
|
90
|
+
getattr(cfg, "n_layer", getattr(cfg, "num_hidden_layers", 0)) or 0
|
|
91
|
+
)
|
|
92
|
+
n_head = int(
|
|
93
|
+
getattr(cfg, "n_head", getattr(cfg, "num_attention_heads", 0)) or 0
|
|
94
|
+
)
|
|
95
|
+
heads = [n_head] * n_layer if n_layer and n_head else []
|
|
96
|
+
return {
|
|
97
|
+
"n_layer": n_layer,
|
|
98
|
+
"heads_per_layer": heads,
|
|
99
|
+
"mlp_dims": [],
|
|
100
|
+
"tying": {},
|
|
101
|
+
"backend": "onnxruntime",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# --- Snapshot/restore (unsupported; runner falls back to reload) -----
|
|
105
|
+
def snapshot(self, model: Any) -> bytes: # type: ignore[override]
|
|
106
|
+
raise NotImplementedError("snapshot not supported for ONNXRuntime models")
|
|
107
|
+
|
|
108
|
+
def restore(self, model: Any, blob: bytes) -> None: # type: ignore[override]
|
|
109
|
+
raise NotImplementedError("restore not supported for ONNXRuntime models")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = ["HF_ORT_CausalLM_Adapter"]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HuggingFace T5 Model Adapter
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
ModelAdapter implementation for HuggingFace T5 encoder-decoder models.
|
|
6
|
+
|
|
7
|
+
Loads AutoModelForSeq2SeqLM (e.g., t5-small/base/large) and exposes a minimal
|
|
8
|
+
describe() sufficient for guard policies and reporting.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn as nn
|
|
17
|
+
|
|
18
|
+
from invarlock.core.api import ModelAdapter
|
|
19
|
+
from invarlock.core.error_utils import wrap_errors
|
|
20
|
+
from invarlock.core.exceptions import AdapterError, DependencyError, ModelLoadError
|
|
21
|
+
|
|
22
|
+
from .hf_mixin import HFAdapterMixin
|
|
23
|
+
|
|
24
|
+
TensorType = torch.Tensor
|
|
25
|
+
ModuleType = nn.Module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HF_T5_Adapter(HFAdapterMixin, ModelAdapter):
|
|
29
|
+
"""HuggingFace T5 adapter using AutoModelForSeq2SeqLM."""
|
|
30
|
+
|
|
31
|
+
name = "hf_t5"
|
|
32
|
+
|
|
33
|
+
def load_model(self, model_id: str, device: str = "auto") -> ModuleType | Any: # type: ignore[override]
|
|
34
|
+
with wrap_errors(
|
|
35
|
+
DependencyError,
|
|
36
|
+
"E203",
|
|
37
|
+
"DEPENDENCY-MISSING: transformers",
|
|
38
|
+
lambda e: {"dependency": "transformers"},
|
|
39
|
+
):
|
|
40
|
+
from transformers import AutoModelForSeq2SeqLM # type: ignore
|
|
41
|
+
|
|
42
|
+
with wrap_errors(
|
|
43
|
+
ModelLoadError,
|
|
44
|
+
"E201",
|
|
45
|
+
"MODEL-LOAD-FAILED: transformers AutoModelForSeq2SeqLM",
|
|
46
|
+
lambda e: {"model_id": model_id},
|
|
47
|
+
):
|
|
48
|
+
model = AutoModelForSeq2SeqLM.from_pretrained(model_id)
|
|
49
|
+
return model.to(self._resolve_device(device))
|
|
50
|
+
|
|
51
|
+
def can_handle(self, model: ModuleType | Any) -> bool: # type: ignore[override]
|
|
52
|
+
cfg = getattr(model, "config", None)
|
|
53
|
+
if cfg is None:
|
|
54
|
+
return False
|
|
55
|
+
try:
|
|
56
|
+
mt = str(getattr(cfg, "model_type", "")).lower()
|
|
57
|
+
except Exception:
|
|
58
|
+
mt = ""
|
|
59
|
+
if mt == "t5":
|
|
60
|
+
return True
|
|
61
|
+
# Heuristic: encoder-decoder with lm_head and shared embedding
|
|
62
|
+
return bool(
|
|
63
|
+
getattr(cfg, "is_encoder_decoder", False)
|
|
64
|
+
and hasattr(model, "lm_head")
|
|
65
|
+
and hasattr(model, "shared")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def describe(self, model: ModuleType | Any) -> dict[str, Any]: # type: ignore[override]
|
|
69
|
+
cfg = getattr(model, "config", None)
|
|
70
|
+
if cfg is None:
|
|
71
|
+
raise AdapterError(
|
|
72
|
+
code="E202",
|
|
73
|
+
message="ADAPTER-STRUCTURE-INVALID: missing HuggingFace config on model",
|
|
74
|
+
details={"model_class": model.__class__.__name__},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Extract key dimensions with safe fallbacks
|
|
78
|
+
enc_layers = int(
|
|
79
|
+
getattr(cfg, "num_layers", getattr(cfg, "num_encoder_layers", 0)) or 0
|
|
80
|
+
)
|
|
81
|
+
dec_layers = int(getattr(cfg, "num_decoder_layers", enc_layers) or 0)
|
|
82
|
+
n_layer = int(enc_layers + dec_layers or max(enc_layers, dec_layers))
|
|
83
|
+
n_heads = int(
|
|
84
|
+
getattr(cfg, "num_heads", getattr(cfg, "num_attention_heads", 0)) or 0
|
|
85
|
+
)
|
|
86
|
+
d_model = int(getattr(cfg, "d_model", getattr(cfg, "hidden_size", 0)) or 0)
|
|
87
|
+
d_ff = int(getattr(cfg, "d_ff", (d_model * 4) if d_model else 0) or 0)
|
|
88
|
+
vocab_size = int(getattr(cfg, "vocab_size", 0) or 0)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
device = next(model.parameters()).device
|
|
92
|
+
except Exception:
|
|
93
|
+
device = torch.device("cpu")
|
|
94
|
+
|
|
95
|
+
heads_per_layer = [n_heads] * max(1, n_layer)
|
|
96
|
+
mlp_dims = [d_ff] * max(1, n_layer)
|
|
97
|
+
|
|
98
|
+
# Weight tying: T5 ties lm_head ↔ shared embedding
|
|
99
|
+
tying_map: dict[str, str] = {}
|
|
100
|
+
try:
|
|
101
|
+
if hasattr(model, "lm_head") and hasattr(model, "shared"):
|
|
102
|
+
if getattr(model.lm_head, "weight", None) is getattr(
|
|
103
|
+
model.shared, "weight", object()
|
|
104
|
+
):
|
|
105
|
+
tying_map["lm_head.weight"] = "shared.weight"
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
total_params = 0
|
|
110
|
+
try:
|
|
111
|
+
total_params = sum(p.numel() for p in model.parameters())
|
|
112
|
+
except Exception:
|
|
113
|
+
total_params = 0
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"n_layer": int(max(1, n_layer)),
|
|
117
|
+
"heads_per_layer": heads_per_layer,
|
|
118
|
+
"mlp_dims": mlp_dims,
|
|
119
|
+
"tying": tying_map,
|
|
120
|
+
"model_type": "t5",
|
|
121
|
+
"model_class": model.__class__.__name__,
|
|
122
|
+
"n_heads": n_heads,
|
|
123
|
+
"hidden_size": d_model,
|
|
124
|
+
"vocab_size": vocab_size,
|
|
125
|
+
"total_params": total_params,
|
|
126
|
+
"device": str(device),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# snapshot/restore provided by HFAdapterMixin
|
|
130
|
+
def snapshot(self, model: ModuleType) -> bytes: # type: ignore[override]
|
|
131
|
+
return super().snapshot(model)
|
|
132
|
+
|
|
133
|
+
def restore(self, model: ModuleType, blob: bytes) -> None: # type: ignore[override]
|
|
134
|
+
return super().restore(model, blob)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = ["HF_T5_Adapter"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561 type hints
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Assurance namespace (`invarlock.assurance`).
|
|
2
|
+
|
|
3
|
+
This namespace groups safety-certificate related surfaces. For now it forwards
|
|
4
|
+
to `invarlock.eval` and guard modules; future work may move implementations here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from invarlock.reporting.report_types import RunReport
|
|
12
|
+
|
|
13
|
+
try: # pragma: no cover - shim to reporting modules
|
|
14
|
+
from invarlock.reporting.certificate import (
|
|
15
|
+
CERTIFICATE_SCHEMA_VERSION,
|
|
16
|
+
make_certificate,
|
|
17
|
+
validate_certificate,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Prefer direct import from render for rendering APIs
|
|
21
|
+
from invarlock.reporting.render import render_certificate_markdown
|
|
22
|
+
except Exception: # pragma: no cover - provide soft stubs
|
|
23
|
+
CERTIFICATE_SCHEMA_VERSION = "v1"
|
|
24
|
+
|
|
25
|
+
def make_certificate(
|
|
26
|
+
report: RunReport,
|
|
27
|
+
baseline: RunReport | dict[str, Any],
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
raise ImportError("invarlock.reporting.certificate not available")
|
|
30
|
+
|
|
31
|
+
def render_certificate_markdown(certificate: dict[str, Any]) -> str:
|
|
32
|
+
raise ImportError("invarlock.reporting.certificate not available")
|
|
33
|
+
|
|
34
|
+
def validate_certificate(certificate: dict[str, Any]) -> bool:
|
|
35
|
+
raise ImportError("invarlock.reporting.certificate not available")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"CERTIFICATE_SCHEMA_VERSION",
|
|
40
|
+
"make_certificate",
|
|
41
|
+
"render_certificate_markdown",
|
|
42
|
+
"validate_certificate",
|
|
43
|
+
]
|