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,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
+ ]
@@ -0,0 +1,8 @@
1
+ """CLI namespace wrapper for unified import path (`invarlock.cli`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Re-export common entry points for convenience
6
+ from .app import app
7
+
8
+ __all__ = ["app"]