sciev 0.2.1__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.
- sciev/__init__.py +6 -0
- sciev/calibration.py +644 -0
- sciev/dapt.py +187 -0
- sciev/data.py +422 -0
- sciev/decisions.py +258 -0
- sciev/eval.py +816 -0
- sciev/inference.py +473 -0
- sciev/metrics.py +268 -0
- sciev/model.py +455 -0
- sciev/protocol.py +287 -0
- sciev/readout.py +139 -0
- sciev/serve.py +228 -0
- sciev/train.py +479 -0
- sciev-0.2.1.dist-info/METADATA +196 -0
- sciev-0.2.1.dist-info/RECORD +18 -0
- sciev-0.2.1.dist-info/WHEEL +5 -0
- sciev-0.2.1.dist-info/licenses/LICENSE +186 -0
- sciev-0.2.1.dist-info/top_level.txt +1 -0
sciev/__init__.py
ADDED
sciev/calibration.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from numbers import Integral, Real
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
from . import decisions, metrics, protocol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["fit_calibration", "save_calibration", "load_calibration", "resolve_inference_settings"]
|
|
17
|
+
|
|
18
|
+
_INFERENCE_FIELDS = {"head_kind", "mode", "layers", "canonical_order", "seq_len",
|
|
19
|
+
"encoding", "strict_inputs", "decision_type"}
|
|
20
|
+
_KINDS = ("choice", "noul", "score")
|
|
21
|
+
_LIMITATIONS = [
|
|
22
|
+
"Calibration uses supplied labels, which may be weak or synthetic; their scientific validity is not verified.",
|
|
23
|
+
"Acceptance controls empirical dev risk only, not statistical deployment risk; freeze before independent test evaluation.",
|
|
24
|
+
"Split checks cover exact inputs and declared identifiers, not unreported document overlap or paraphrase leakage.",
|
|
25
|
+
"The checkpoint digest does not independently verify the supplied runtime model or full external backbone/revision/adapter provenance.",
|
|
26
|
+
"Scientific performance claims remain pending GPU reevaluation with independent data and frozen calibration.",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _object(value, name):
|
|
31
|
+
if not isinstance(value, Mapping):
|
|
32
|
+
raise ValueError(f"{name} must be an object")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _integer(value, name, minimum=0):
|
|
37
|
+
if isinstance(value, bool) or not isinstance(value, Integral) or value < minimum:
|
|
38
|
+
raise ValueError(f"{name} must be an integer >= {minimum}")
|
|
39
|
+
return int(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _real(value, name):
|
|
43
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
44
|
+
raise ValueError(f"{name} must be a finite real scalar")
|
|
45
|
+
try:
|
|
46
|
+
value = float(value)
|
|
47
|
+
except OverflowError as exc:
|
|
48
|
+
raise ValueError(f"{name} must be a finite real scalar") from exc
|
|
49
|
+
if not math.isfinite(value):
|
|
50
|
+
raise ValueError(f"{name} must be a finite real scalar")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _positive_temperature(value):
|
|
55
|
+
value = _real(value, "temperature")
|
|
56
|
+
if value <= 0:
|
|
57
|
+
raise ValueError("temperature must be positive")
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _text(value, name):
|
|
62
|
+
if not isinstance(value, str) or not value.strip():
|
|
63
|
+
raise ValueError(f"{name} must be a nonempty string")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _choice(value, name, choices):
|
|
68
|
+
if not isinstance(value, str) or value not in choices:
|
|
69
|
+
raise ValueError(f"{name} must be one of {choices}")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _boolean(value, name):
|
|
74
|
+
if not isinstance(value, bool):
|
|
75
|
+
raise ValueError(f"{name} must be a boolean")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _layers(value):
|
|
80
|
+
if not isinstance(value, (list, tuple)) or not value:
|
|
81
|
+
raise ValueError("layers must be a nonempty list or tuple of integer indices")
|
|
82
|
+
if any(isinstance(index, bool) or not isinstance(index, Integral) for index in value):
|
|
83
|
+
raise ValueError("layers must contain integer non-boolean indices")
|
|
84
|
+
return [int(index) for index in value]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _mode(value):
|
|
88
|
+
value = _text(value, "mode").removeprefix("r2_")
|
|
89
|
+
return _choice(value, "mode", ("marker", "spanpool"))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sources(checkpoint_data):
|
|
93
|
+
checkpoint_data = _object(checkpoint_data, "checkpoint")
|
|
94
|
+
inference = _object(checkpoint_data.get("inference", {}), "checkpoint inference")
|
|
95
|
+
meta = _object(checkpoint_data.get("meta", {}), "checkpoint meta")
|
|
96
|
+
return (inference, checkpoint_data, meta)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _stored(sources, *names):
|
|
100
|
+
for source in sources:
|
|
101
|
+
for name in names:
|
|
102
|
+
if source.get(name) is not None:
|
|
103
|
+
return source[name]
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _setting(name, supplied, stored, validate, default=None):
|
|
108
|
+
previous = validate(stored) if stored is not None else None
|
|
109
|
+
if supplied is None:
|
|
110
|
+
return previous if previous is not None else default
|
|
111
|
+
selected = validate(supplied)
|
|
112
|
+
if previous is not None and selected != previous:
|
|
113
|
+
raise ValueError(f"{name} conflicts with stored inference settings")
|
|
114
|
+
return selected
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def resolve_inference_settings(checkpoint_data, *, mode=None, layers=None, canonical=None,
|
|
118
|
+
strict=None, decision_type=None):
|
|
119
|
+
sources = _sources(checkpoint_data)
|
|
120
|
+
model_config = _object(checkpoint_data.get("model_config", {}), "checkpoint model_config")
|
|
121
|
+
training_data = checkpoint_data.get("training_data")
|
|
122
|
+
training_data = {} if training_data is None else _object(training_data, "checkpoint training_data")
|
|
123
|
+
head_kind = _stored(sources, "head_kind")
|
|
124
|
+
head_kind = "mlp" if head_kind is None else _choice(head_kind, "head_kind", ("mlp", "attnpool"))
|
|
125
|
+
count = _stored(sources, "n_layers")
|
|
126
|
+
count = None if count is None else _integer(count, "n_layers", 1)
|
|
127
|
+
stored_layers = _stored(sources, "layers", "layers_list", "r2_layers")
|
|
128
|
+
if isinstance(stored_layers, str):
|
|
129
|
+
stored_layers = _parse_layers(stored_layers)
|
|
130
|
+
stored_layers = None if stored_layers is None else _layers(stored_layers)
|
|
131
|
+
requested_layers = None if layers is None else _layers(layers)
|
|
132
|
+
if head_kind == "mlp":
|
|
133
|
+
effective_layers = [-1]
|
|
134
|
+
else:
|
|
135
|
+
if stored_layers is not None and requested_layers is not None and stored_layers != requested_layers:
|
|
136
|
+
raise ValueError("layers conflict with stored inference settings")
|
|
137
|
+
effective_layers = stored_layers if requested_layers is None else requested_layers
|
|
138
|
+
if effective_layers is None:
|
|
139
|
+
if count is not None and count > 1:
|
|
140
|
+
raise ValueError("legacy multi-layer attention requires explicit --r2-layers indices")
|
|
141
|
+
effective_layers = [-1]
|
|
142
|
+
if count is not None and len(effective_layers) != count:
|
|
143
|
+
raise ValueError("layers count does not match checkpoint n_layers")
|
|
144
|
+
resolved_mode = _setting("mode", mode, _stored(sources, "mode", "r2_mode"), _mode, "marker")
|
|
145
|
+
if head_kind == "attnpool" and resolved_mode != "spanpool":
|
|
146
|
+
raise ValueError("attnpool requires spanpool mode")
|
|
147
|
+
resolved_canonical = _setting(
|
|
148
|
+
"canonical_order", canonical, _stored(sources, "canonical_order", "canonical"),
|
|
149
|
+
lambda value: _boolean(value, "canonical_order"), False)
|
|
150
|
+
resolved_strict = _setting(
|
|
151
|
+
"strict_inputs", strict, _stored(sources, "strict_inputs", "strict"),
|
|
152
|
+
lambda value: _boolean(value, "strict_inputs"))
|
|
153
|
+
resolved_type = _setting(
|
|
154
|
+
"decision_type", decision_type, _stored(sources, "decision_type"),
|
|
155
|
+
lambda value: _choice(value, "decision_type", _KINDS))
|
|
156
|
+
encoding = _stored(sources, "encoding")
|
|
157
|
+
if encoding is None:
|
|
158
|
+
encoding = training_data.get("encoding")
|
|
159
|
+
if encoding is not None:
|
|
160
|
+
encoding = _text(encoding, "encoding")
|
|
161
|
+
seq_len = _stored(sources, "seq_len")
|
|
162
|
+
if seq_len is None:
|
|
163
|
+
seq_len = model_config.get("seq_len")
|
|
164
|
+
if seq_len is not None:
|
|
165
|
+
seq_len = _integer(seq_len, "seq_len", 1)
|
|
166
|
+
return {"head_kind": head_kind, "mode": resolved_mode, "layers": effective_layers,
|
|
167
|
+
"canonical_order": resolved_canonical, "seq_len": seq_len, "encoding": encoding,
|
|
168
|
+
"strict_inputs": resolved_strict, "decision_type": resolved_type}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _validated_rows(values, role):
|
|
172
|
+
if isinstance(values, (str, bytes, Mapping, set, frozenset)):
|
|
173
|
+
raise ValueError(f"{role} must be a nonempty ordered iterable of decision rows")
|
|
174
|
+
try:
|
|
175
|
+
values = list(values)
|
|
176
|
+
except TypeError as exc:
|
|
177
|
+
raise ValueError(f"{role} must be a nonempty ordered iterable of decision rows") from exc
|
|
178
|
+
if not values:
|
|
179
|
+
raise ValueError(f"{role} is empty; at least one decision is required")
|
|
180
|
+
return [decisions.validate_decision_row(row) for row in values]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _policy_parameters(error_budget, min_accepted):
|
|
184
|
+
error_budget = _real(error_budget, "error_budget")
|
|
185
|
+
if not 0 <= error_budget <= 1:
|
|
186
|
+
raise ValueError("error_budget must be in [0, 1]")
|
|
187
|
+
return error_budget, _integer(min_accepted, "min_accepted", 1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _check_fingerprint(expected, path):
|
|
191
|
+
actual = protocol.file_fingerprint(path)
|
|
192
|
+
if (actual["sha256"].lower() != expected["sha256"].lower()
|
|
193
|
+
or actual["bytes"] != expected["bytes"]):
|
|
194
|
+
raise ValueError("checkpoint fingerprint mismatch: checkpoint digest or bytes changed")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _match_contract(contract, inference, role):
|
|
198
|
+
if len(contract["kinds"]) > 1:
|
|
199
|
+
raise ValueError(f"{role} cannot mix decision types")
|
|
200
|
+
if contract["encoding"] != inference["encoding"]:
|
|
201
|
+
raise ValueError(f"{role} encoding conflicts with calibration inference protocol")
|
|
202
|
+
if contract["kinds"] and contract["kinds"] != [inference["decision_type"]]:
|
|
203
|
+
raise ValueError(f"{role} decision_type conflicts with calibration inference protocol")
|
|
204
|
+
if inference["decision_type"] == "noul" and contract["option_counts"] != [2]:
|
|
205
|
+
raise ValueError("noul requires exactly two options")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _prepare_calibration(checkpoint_path, temperature_rows, acceptance_rows, train_rows, *,
|
|
209
|
+
mode, layers, canonical, strict, decision_type, error_budget, min_accepted):
|
|
210
|
+
error_budget, min_accepted = _policy_parameters(error_budget, min_accepted)
|
|
211
|
+
named_rows = {"temperature_dev": _validated_rows(temperature_rows, "temperature_dev")}
|
|
212
|
+
if acceptance_rows is not None:
|
|
213
|
+
named_rows["acceptance_dev"] = _validated_rows(acceptance_rows, "acceptance_dev")
|
|
214
|
+
if train_rows is not None:
|
|
215
|
+
named_rows["train_reference"] = _validated_rows(train_rows, "train_reference")
|
|
216
|
+
checkpoint = protocol.file_fingerprint(checkpoint_path)
|
|
217
|
+
checkpoint_data = _object(torch.load(checkpoint_path, map_location="cpu", weights_only=True), "checkpoint")
|
|
218
|
+
_check_fingerprint(checkpoint, checkpoint_path)
|
|
219
|
+
inference = resolve_inference_settings(checkpoint_data, mode=mode, layers=layers,
|
|
220
|
+
canonical=canonical, strict=strict, decision_type=decision_type)
|
|
221
|
+
split_checks = protocol.assert_disjoint_splits(named_rows)
|
|
222
|
+
contracts = split_checks["splits"]
|
|
223
|
+
if any(len(contract["kinds"]) > 1 for contract in contracts.values()):
|
|
224
|
+
raise ValueError("calibration and reference datasets cannot mix decision types")
|
|
225
|
+
calibration_data = {name: contract for name, contract in contracts.items() if name != "train_reference"}
|
|
226
|
+
kinds = {kind for contract in calibration_data.values() for kind in contract["kinds"]}
|
|
227
|
+
if len(kinds) > 1:
|
|
228
|
+
raise ValueError("temperature_dev and acceptance_dev cannot mix decision types")
|
|
229
|
+
if inference["decision_type"] is None and kinds:
|
|
230
|
+
inference["decision_type"] = next(iter(kinds))
|
|
231
|
+
original_encoding = inference["encoding"]
|
|
232
|
+
inference["encoding"] = contracts["temperature_dev"]["encoding"]
|
|
233
|
+
if inference["strict_inputs"] is None:
|
|
234
|
+
inference["strict_inputs"] = inference["encoding"] == decisions.ENCODING_VERSION
|
|
235
|
+
for name, contract in calibration_data.items():
|
|
236
|
+
_match_contract(contract, inference, name)
|
|
237
|
+
combined = [row for name in calibration_data for row in named_rows[name]]
|
|
238
|
+
training_check = protocol.assert_checkpoint_disjoint(checkpoint_data, combined, role="calibration")
|
|
239
|
+
encoding_status = ("unverified" if original_encoding is None else
|
|
240
|
+
"matched" if original_encoding == inference["encoding"] else "changed")
|
|
241
|
+
sources = _sources(checkpoint_data)
|
|
242
|
+
provenance = {
|
|
243
|
+
"training_overlap_check": training_check,
|
|
244
|
+
"split_checks": split_checks,
|
|
245
|
+
"encoding_check": {"status": encoding_status, "checkpoint_encoding": original_encoding,
|
|
246
|
+
"calibration_encoding": inference["encoding"],
|
|
247
|
+
"limitation": "Legacy encoding changes or undeclared training encoding do not validate historical results."},
|
|
248
|
+
"inference_resolution": {
|
|
249
|
+
"undeclared_checkpoint_fields": sorted(field for field in _INFERENCE_FIELDS
|
|
250
|
+
if _stored(sources, field) is None),
|
|
251
|
+
"mlp_layers": "effective last hidden layer [-1]; legacy requested multi-layer indices never affected MLP",
|
|
252
|
+
"default_mode": "marker when neither caller nor checkpoint declares mode",
|
|
253
|
+
"default_canonical_order": "false when neither caller nor checkpoint declares canonical ordering",
|
|
254
|
+
"strict_default": "true for systemone-v2, false for legacy encodings when undeclared",
|
|
255
|
+
},
|
|
256
|
+
"model_provenance": {
|
|
257
|
+
"checkpoint_contains_backbone": isinstance(checkpoint_data.get("model"), Mapping),
|
|
258
|
+
"runtime_weights_verified": False,
|
|
259
|
+
"full_backbone_provenance": "unverified",
|
|
260
|
+
"saved_model_config_available": isinstance(checkpoint_data.get("model_config"), Mapping),
|
|
261
|
+
"saved_model_config_fields": sorted(key for key in checkpoint_data.get("model_config", {})
|
|
262
|
+
if isinstance(key, str)),
|
|
263
|
+
},
|
|
264
|
+
"label_quality": "unverified; supplied gold labels may be weak or synthetic",
|
|
265
|
+
}
|
|
266
|
+
for key in ("hf_backbone", "hf_revision", "revision", "lora_adapter"):
|
|
267
|
+
value = _stored(sources, key)
|
|
268
|
+
if value is not None:
|
|
269
|
+
provenance["model_provenance"][key] = _text(value, key)
|
|
270
|
+
if train_rows is not None:
|
|
271
|
+
provenance["train_reference_check"] = {
|
|
272
|
+
"status": "verified", "reason": "no_known_overlap_with_supplied_reference",
|
|
273
|
+
"scope": split_checks["scope"],
|
|
274
|
+
"limitation": "A supplied reference is not proof of the checkpoint's actual training identity.",
|
|
275
|
+
}
|
|
276
|
+
return {"checkpoint": checkpoint, "inference": inference, "named_rows": named_rows,
|
|
277
|
+
"calibration_data": calibration_data, "provenance": provenance,
|
|
278
|
+
"error_budget": error_budget, "min_accepted": min_accepted}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@contextmanager
|
|
282
|
+
def _evaluation_modes(*objects):
|
|
283
|
+
modules, adapters, roots, seen = [], [], [], set()
|
|
284
|
+
for obj in objects:
|
|
285
|
+
if isinstance(obj, torch.nn.Module):
|
|
286
|
+
roots.append(obj)
|
|
287
|
+
for module in obj.modules():
|
|
288
|
+
if id(module) not in seen:
|
|
289
|
+
seen.add(id(module))
|
|
290
|
+
modules.append((module, module.training))
|
|
291
|
+
elif (callable(getattr(obj, "eval", None)) and callable(getattr(obj, "train", None))
|
|
292
|
+
and isinstance(getattr(obj, "training", None), bool) and id(obj) not in seen):
|
|
293
|
+
seen.add(id(obj))
|
|
294
|
+
adapters.append((obj, obj.training))
|
|
295
|
+
roots.append(obj)
|
|
296
|
+
try:
|
|
297
|
+
for obj in roots:
|
|
298
|
+
obj.eval()
|
|
299
|
+
yield
|
|
300
|
+
finally:
|
|
301
|
+
for obj, previous in adapters:
|
|
302
|
+
obj.train(previous)
|
|
303
|
+
for module, previous in modules:
|
|
304
|
+
module.training = previous
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _fit_prepared(model, head, prepared, checkpoint_path, device):
|
|
308
|
+
from . import eval as rj_eval
|
|
309
|
+
from .model import AttnPoolHead, DecisionHead
|
|
310
|
+
|
|
311
|
+
_check_fingerprint(prepared["checkpoint"], checkpoint_path)
|
|
312
|
+
inference = dict(prepared["inference"])
|
|
313
|
+
seq_len = _integer(getattr(model, "seq_len", None), "model.seq_len", 1)
|
|
314
|
+
if inference["seq_len"] is not None and inference["seq_len"] != seq_len:
|
|
315
|
+
raise ValueError("actual model seq_len conflicts with stored inference settings")
|
|
316
|
+
inference["seq_len"] = seq_len
|
|
317
|
+
if isinstance(head, (AttnPoolHead, DecisionHead)):
|
|
318
|
+
actual_kind = "attnpool" if isinstance(head, AttnPoolHead) else "mlp"
|
|
319
|
+
if inference["head_kind"] != actual_kind:
|
|
320
|
+
raise ValueError("actual head_kind conflicts with checkpoint inference")
|
|
321
|
+
if actual_kind == "attnpool" and head.n_layers != len(inference["layers"]):
|
|
322
|
+
raise ValueError("actual attention head layer count conflicts with inference layers")
|
|
323
|
+
truncation = {}
|
|
324
|
+
for role in prepared["calibration_data"]:
|
|
325
|
+
counts = {"context_rows": 0, "option_rows": 0}
|
|
326
|
+
for row in prepared["named_rows"][role]:
|
|
327
|
+
layout = decisions.prepare_decision(
|
|
328
|
+
model, row["ctx"], row["opts"], mode=inference["mode"],
|
|
329
|
+
canonical=inference["canonical_order"], strict=inference["strict_inputs"])
|
|
330
|
+
counts["context_rows"] += int(layout.context_truncated > 0)
|
|
331
|
+
counts["option_rows"] += int(any(layout.option_truncated))
|
|
332
|
+
truncation[role] = counts
|
|
333
|
+
acceptance_policy = None
|
|
334
|
+
with _evaluation_modes(model, head), torch.no_grad():
|
|
335
|
+
temperature, _ = rj_eval.fit_r2_temperature_decisions(
|
|
336
|
+
model, head, prepared["named_rows"]["temperature_dev"], device,
|
|
337
|
+
mode=inference["mode"], layers=tuple(inference["layers"]),
|
|
338
|
+
canonical=inference["canonical_order"], strict=inference["strict_inputs"], return_logits=True)
|
|
339
|
+
temperature = _positive_temperature(temperature)
|
|
340
|
+
if "acceptance_dev" in prepared["named_rows"]:
|
|
341
|
+
confidences, corrects = [], []
|
|
342
|
+
for row in prepared["named_rows"]["acceptance_dev"]:
|
|
343
|
+
logits, layout = decisions.decision_logits(
|
|
344
|
+
model, head, row["ctx"], row["opts"], device, mode=inference["mode"],
|
|
345
|
+
layers=tuple(inference["layers"]), canonical=inference["canonical_order"],
|
|
346
|
+
strict=inference["strict_inputs"])
|
|
347
|
+
probabilities = decisions.decision_probabilities(logits, temperature)
|
|
348
|
+
prediction = decisions.decision_prediction(logits, layout)
|
|
349
|
+
confidences.append(max(probabilities))
|
|
350
|
+
corrects.append(int(prediction == row["gold"]))
|
|
351
|
+
acceptance_policy = metrics.fit_acceptance_policy(
|
|
352
|
+
confidences, corrects, error_budget=prepared["error_budget"], min_accepted=prepared["min_accepted"])
|
|
353
|
+
_check_fingerprint(prepared["checkpoint"], checkpoint_path)
|
|
354
|
+
provenance = dict(prepared["provenance"], truncation=truncation)
|
|
355
|
+
artifact = {"schema_version": 1, "kind": "r2_calibration", "checkpoint": prepared["checkpoint"],
|
|
356
|
+
"inference": inference, "temperature": temperature, "acceptance_policy": acceptance_policy,
|
|
357
|
+
"calibration_data": prepared["calibration_data"], "provenance": provenance,
|
|
358
|
+
"limitations": list(_LIMITATIONS)}
|
|
359
|
+
if any(sum(counts.values()) for counts in truncation.values()):
|
|
360
|
+
artifact["limitations"].append(
|
|
361
|
+
"Authorized truncation occurred; identity checks use declared original inputs, not all truncated-input collisions.")
|
|
362
|
+
_validate_artifact(artifact)
|
|
363
|
+
return artifact
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def fit_calibration(model, head, temperature_rows, acceptance_rows=None, *, checkpoint_path,
|
|
367
|
+
device="cpu", mode="spanpool", layers=(-1,), canonical=True, decision_type=None,
|
|
368
|
+
strict=None, train_rows=None, error_budget=0.05, min_accepted=20):
|
|
369
|
+
prepared = _prepare_calibration(
|
|
370
|
+
checkpoint_path, temperature_rows, acceptance_rows, train_rows, mode=mode, layers=layers,
|
|
371
|
+
canonical=canonical, strict=strict, decision_type=decision_type,
|
|
372
|
+
error_budget=error_budget, min_accepted=min_accepted)
|
|
373
|
+
return _fit_prepared(model, head, prepared, checkpoint_path, device)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _hashes(value, name):
|
|
377
|
+
if (not isinstance(value, list) or not value
|
|
378
|
+
or any(not isinstance(item, str) or re.fullmatch(r"[0-9a-fA-F]{64}", item) is None
|
|
379
|
+
for item in value)):
|
|
380
|
+
raise ValueError(f"{name} must be a nonempty list of SHA256 identity hashes")
|
|
381
|
+
normalized = {item.lower() for item in value}
|
|
382
|
+
if len(normalized) != len(value):
|
|
383
|
+
raise ValueError(f"{name} must not contain duplicate identity hashes")
|
|
384
|
+
return normalized
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _validate_contract(contract, name):
|
|
388
|
+
contract = _object(contract, name)
|
|
389
|
+
if _integer(contract.get("contract_version"), f"{name}.contract_version", 1) != 1:
|
|
390
|
+
raise ValueError(f"unsupported {name} contract_version")
|
|
391
|
+
count = _integer(contract.get("n_rows"), f"{name}.n_rows", 1)
|
|
392
|
+
_text(contract.get("encoding"), f"{name}.encoding")
|
|
393
|
+
kinds = contract.get("kinds")
|
|
394
|
+
if not isinstance(kinds, list) or len(kinds) > 1 or any(kind not in _KINDS for kind in kinds):
|
|
395
|
+
raise ValueError(f"{name} kinds must declare at most one decision type")
|
|
396
|
+
option_counts = contract.get("option_counts")
|
|
397
|
+
if not isinstance(option_counts, list) or not option_counts:
|
|
398
|
+
raise ValueError(f"{name} requires option_counts")
|
|
399
|
+
for value in option_counts:
|
|
400
|
+
_integer(value, f"{name}.option_counts", 2)
|
|
401
|
+
identities = _hashes(contract.get("identity_hashes"), f"{name}.identity_hashes")
|
|
402
|
+
for key in ("input_hashes", "effective_input_hashes"):
|
|
403
|
+
inputs = _hashes(contract.get(key), f"{name}.{key}")
|
|
404
|
+
if not inputs <= identities or len(inputs) > count:
|
|
405
|
+
raise ValueError(f"{name}.{key} is inconsistent with identity_hashes or n_rows")
|
|
406
|
+
return identities
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _validate_inference(inference):
|
|
410
|
+
inference = _object(inference, "inference")
|
|
411
|
+
if set(inference) != _INFERENCE_FIELDS:
|
|
412
|
+
raise ValueError("inference requires the complete supported protocol fields and no temperature")
|
|
413
|
+
kind = _choice(inference["head_kind"], "inference head_kind", ("mlp", "attnpool"))
|
|
414
|
+
mode = _choice(inference["mode"], "inference mode", ("marker", "spanpool"))
|
|
415
|
+
layers = _layers(inference["layers"])
|
|
416
|
+
if kind == "mlp" and layers != [-1]:
|
|
417
|
+
raise ValueError("inference MLP layers must be effective [-1]")
|
|
418
|
+
if kind == "attnpool" and mode != "spanpool":
|
|
419
|
+
raise ValueError("inference attnpool requires spanpool mode")
|
|
420
|
+
_boolean(inference["canonical_order"], "inference canonical_order")
|
|
421
|
+
_boolean(inference["strict_inputs"], "inference strict_inputs")
|
|
422
|
+
_integer(inference["seq_len"], "inference seq_len", 1)
|
|
423
|
+
_text(inference["encoding"], "inference encoding")
|
|
424
|
+
if inference["decision_type"] is not None:
|
|
425
|
+
_choice(inference["decision_type"], "inference decision_type", _KINDS)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _validate_policy_audit(policy, contract):
|
|
429
|
+
metrics.evaluate_acceptance_policy([], [], policy)
|
|
430
|
+
count = _integer(policy.get("dev_n"), "policy dev_n", 1)
|
|
431
|
+
accepted = _integer(policy.get("dev_accepted"), "policy dev_accepted")
|
|
432
|
+
errors = _integer(policy.get("dev_errors"), "policy dev_errors")
|
|
433
|
+
budget, minimum = _policy_parameters(policy.get("error_budget"), policy.get("min_accepted"))
|
|
434
|
+
if count != contract["n_rows"] or not errors <= accepted <= count:
|
|
435
|
+
raise ValueError("acceptance policy counts conflict with acceptance_dev")
|
|
436
|
+
expected_risk = errors / accepted if accepted else None
|
|
437
|
+
if policy.get("dev_coverage") != accepted / count or policy.get("dev_risk") != expected_risk:
|
|
438
|
+
raise ValueError("acceptance policy dev coverage/risk is inconsistent with its counts")
|
|
439
|
+
if (policy["threshold"] is None) != (accepted == 0):
|
|
440
|
+
raise ValueError("acceptance policy threshold conflicts with dev_accepted")
|
|
441
|
+
if accepted and (accepted < minimum or expected_risk > budget):
|
|
442
|
+
raise ValueError("acceptance policy violates its empirical dev support or error budget")
|
|
443
|
+
if policy.get("guarantee") is not False:
|
|
444
|
+
raise ValueError("acceptance policy cannot claim a deployment guarantee")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _validate_artifact(artifact):
|
|
448
|
+
artifact = _object(artifact, "calibration artifact")
|
|
449
|
+
required = {"schema_version", "kind", "checkpoint", "inference", "temperature", "acceptance_policy",
|
|
450
|
+
"calibration_data", "provenance", "limitations"}
|
|
451
|
+
if not required <= artifact.keys():
|
|
452
|
+
raise ValueError("calibration artifact is missing required schema fields")
|
|
453
|
+
if _integer(artifact["schema_version"], "schema_version", 1) != 1 or artifact["kind"] != "r2_calibration":
|
|
454
|
+
raise ValueError("unsupported calibration schema_version or kind")
|
|
455
|
+
_positive_temperature(artifact["temperature"])
|
|
456
|
+
_validate_inference(artifact["inference"])
|
|
457
|
+
fingerprint = _object(artifact["checkpoint"], "checkpoint fingerprint")
|
|
458
|
+
_text(fingerprint.get("path"), "checkpoint path")
|
|
459
|
+
_hashes([fingerprint.get("sha256")], "checkpoint sha256")
|
|
460
|
+
_integer(fingerprint.get("bytes"), "checkpoint bytes", 1)
|
|
461
|
+
contracts = _object(artifact["calibration_data"], "calibration_data")
|
|
462
|
+
if "temperature_dev" not in contracts or set(contracts) - {"temperature_dev", "acceptance_dev"}:
|
|
463
|
+
raise ValueError("calibration_data requires temperature_dev and optional acceptance_dev")
|
|
464
|
+
previous = set()
|
|
465
|
+
for role, contract in contracts.items():
|
|
466
|
+
identities = _validate_contract(contract, role)
|
|
467
|
+
_match_contract(contract, artifact["inference"], role)
|
|
468
|
+
if identities & previous:
|
|
469
|
+
raise ValueError("calibration dataset identity overlap between calibration roles")
|
|
470
|
+
previous.update(identities)
|
|
471
|
+
policy = artifact["acceptance_policy"]
|
|
472
|
+
if policy is None:
|
|
473
|
+
if "acceptance_dev" in contracts:
|
|
474
|
+
raise ValueError("acceptance_dev requires a fitted acceptance_policy, including accept-none policies")
|
|
475
|
+
else:
|
|
476
|
+
_object(policy, "acceptance_policy")
|
|
477
|
+
if "acceptance_dev" not in contracts:
|
|
478
|
+
raise ValueError("acceptance_policy requires acceptance_dev provenance")
|
|
479
|
+
_validate_policy_audit(policy, contracts["acceptance_dev"])
|
|
480
|
+
provenance = _object(artifact["provenance"], "provenance")
|
|
481
|
+
training = _object(provenance.get("training_overlap_check"), "training_overlap_check")
|
|
482
|
+
_choice(training.get("status"), "training_overlap_check status", ("verified", "unverified"))
|
|
483
|
+
_object(training.get("scope"), "training_overlap_check scope")
|
|
484
|
+
splits = _object(provenance.get("split_checks"), "split_checks")
|
|
485
|
+
if splits.get("status") != "verified":
|
|
486
|
+
raise ValueError("split_checks must record a scoped verified check")
|
|
487
|
+
_object(splits.get("scope"), "split_checks scope")
|
|
488
|
+
limitations = artifact["limitations"]
|
|
489
|
+
if not isinstance(limitations, list) or not limitations:
|
|
490
|
+
raise ValueError("calibration requires explicit limitations")
|
|
491
|
+
for limitation in limitations:
|
|
492
|
+
_text(limitation, "limitation")
|
|
493
|
+
try:
|
|
494
|
+
json.dumps(artifact, allow_nan=False)
|
|
495
|
+
except (TypeError, ValueError) as exc:
|
|
496
|
+
raise ValueError("calibration artifact must contain finite JSON-safe values") from exc
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def save_calibration(path, artifact):
|
|
500
|
+
_validate_artifact(artifact)
|
|
501
|
+
payload = json.dumps(artifact, ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False) + "\n"
|
|
502
|
+
with Path(path).open("x", encoding="utf-8") as stream:
|
|
503
|
+
stream.write(payload)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _invalid_json_constant(value):
|
|
507
|
+
raise ValueError(f"calibration JSON cannot contain nonfinite constant {value}")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def load_calibration(path, checkpoint_path, *, evaluation_rows=None, expected_inference=None):
|
|
511
|
+
with Path(path).open(encoding="utf-8") as stream:
|
|
512
|
+
artifact = json.load(stream, parse_constant=_invalid_json_constant)
|
|
513
|
+
_validate_artifact(artifact)
|
|
514
|
+
_check_fingerprint(artifact["checkpoint"], checkpoint_path)
|
|
515
|
+
inference = artifact["inference"]
|
|
516
|
+
if expected_inference is not None:
|
|
517
|
+
expected = dict(_object(expected_inference, "expected_inference"))
|
|
518
|
+
if set(expected) - _INFERENCE_FIELDS:
|
|
519
|
+
raise ValueError("expected_inference contains unsupported fields; temperature is fitted separately")
|
|
520
|
+
if "layers" in expected:
|
|
521
|
+
expected["layers"] = _layers(expected["layers"])
|
|
522
|
+
_validate_inference(dict(inference, **expected))
|
|
523
|
+
for key, value in expected.items():
|
|
524
|
+
if value != inference[key]:
|
|
525
|
+
raise ValueError(f"expected inference {key} conflicts with frozen calibration protocol")
|
|
526
|
+
if evaluation_rows is not None:
|
|
527
|
+
evaluation = protocol.dataset_contract(_validated_rows(evaluation_rows, "evaluation"))
|
|
528
|
+
_match_contract(evaluation, inference, "evaluation")
|
|
529
|
+
identities = set(evaluation["identity_hashes"])
|
|
530
|
+
for role, contract in artifact["calibration_data"].items():
|
|
531
|
+
if identities & {identity.lower() for identity in contract["identity_hashes"]}:
|
|
532
|
+
raise ValueError(f"evaluation/{role} identity overlap; calibration and test must be disjoint")
|
|
533
|
+
return artifact
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _parse_layers(value):
|
|
537
|
+
try:
|
|
538
|
+
parsed = [int(part.strip()) for part in value.split(",")]
|
|
539
|
+
except (AttributeError, ValueError) as exc:
|
|
540
|
+
raise ValueError("--r2-layers must be comma-separated integer indices") from exc
|
|
541
|
+
return _layers(parsed)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _output_preflight(path):
|
|
545
|
+
path = Path(path)
|
|
546
|
+
if path.exists() or path.is_symlink():
|
|
547
|
+
raise FileExistsError(f"refusing to overwrite calibration output: {path}")
|
|
548
|
+
if not path.parent.is_dir():
|
|
549
|
+
raise FileNotFoundError(f"calibration output parent must already exist: {path.parent}")
|
|
550
|
+
if not os.access(path.parent, os.W_OK):
|
|
551
|
+
raise PermissionError(f"calibration output parent is not writable: {path.parent}")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _native_config(path):
|
|
555
|
+
if path is None:
|
|
556
|
+
return None
|
|
557
|
+
import yaml
|
|
558
|
+
from .model import DEFAULT_CONFIG
|
|
559
|
+
|
|
560
|
+
document = _object(yaml.safe_load(Path(path).read_text(encoding="utf-8")), "native YAML config")
|
|
561
|
+
config = dict(_object(document.get("model"), "native YAML model config"))
|
|
562
|
+
if set(config) - DEFAULT_CONFIG.keys():
|
|
563
|
+
raise ValueError("native YAML model config contains unsupported architecture fields")
|
|
564
|
+
for key, value in config.items():
|
|
565
|
+
if isinstance(DEFAULT_CONFIG[key], bool):
|
|
566
|
+
_boolean(value, f"native model config {key}")
|
|
567
|
+
else:
|
|
568
|
+
_integer(value, f"native model config {key}", 1)
|
|
569
|
+
return config
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def main(argv=None):
|
|
573
|
+
parser = argparse.ArgumentParser(description="Fit checkpoint-bound R2 calibration on source-disjoint dev data.")
|
|
574
|
+
parser.add_argument("--ckpt", required=True)
|
|
575
|
+
parser.add_argument("--temperature-dev", required=True)
|
|
576
|
+
parser.add_argument("--acceptance-dev")
|
|
577
|
+
parser.add_argument("--train-reference")
|
|
578
|
+
parser.add_argument("--out", required=True)
|
|
579
|
+
parser.add_argument("--device", default="cpu")
|
|
580
|
+
parser.add_argument("--config")
|
|
581
|
+
parser.add_argument("--r2-mode", choices=("marker", "spanpool"))
|
|
582
|
+
parser.add_argument("--r2-layers", type=_parse_layers)
|
|
583
|
+
canonical = parser.add_mutually_exclusive_group()
|
|
584
|
+
canonical.add_argument("--canonical-order", dest="canonical", action="store_true")
|
|
585
|
+
canonical.add_argument("--no-canonical-order", dest="canonical", action="store_false")
|
|
586
|
+
strict = parser.add_mutually_exclusive_group()
|
|
587
|
+
strict.add_argument("--strict-inputs", dest="strict", action="store_true")
|
|
588
|
+
strict.add_argument("--allow-truncation", dest="strict", action="store_false")
|
|
589
|
+
parser.set_defaults(canonical=None, strict=None)
|
|
590
|
+
parser.add_argument("--decision-type", choices=_KINDS)
|
|
591
|
+
parser.add_argument("--error-budget", type=float, default=0.05)
|
|
592
|
+
parser.add_argument("--min-accepted", type=int, default=20)
|
|
593
|
+
args = parser.parse_args(argv)
|
|
594
|
+
_policy_parameters(args.error_budget, args.min_accepted)
|
|
595
|
+
_output_preflight(args.out)
|
|
596
|
+
torch.device(args.device)
|
|
597
|
+
config = _native_config(args.config)
|
|
598
|
+
from .data import load_decisions_ids
|
|
599
|
+
|
|
600
|
+
paths = {"temperature_dev": args.temperature_dev}
|
|
601
|
+
if args.acceptance_dev is not None:
|
|
602
|
+
paths["acceptance_dev"] = args.acceptance_dev
|
|
603
|
+
if args.train_reference is not None:
|
|
604
|
+
paths["train_reference"] = args.train_reference
|
|
605
|
+
dataset_files = {role: protocol.file_fingerprint(path) for role, path in paths.items()}
|
|
606
|
+
datasets = {role: load_decisions_ids(path) for role, path in paths.items()}
|
|
607
|
+
prepared = _prepare_calibration(
|
|
608
|
+
args.ckpt, datasets["temperature_dev"], datasets.get("acceptance_dev"), datasets.get("train_reference"),
|
|
609
|
+
mode=args.r2_mode, layers=args.r2_layers, canonical=args.canonical, strict=args.strict,
|
|
610
|
+
decision_type=args.decision_type, error_budget=args.error_budget, min_accepted=args.min_accepted)
|
|
611
|
+
for role, path in paths.items():
|
|
612
|
+
if protocol.file_fingerprint(path) != dataset_files[role]:
|
|
613
|
+
raise ValueError(f"{role} dataset changed during calibration preflight")
|
|
614
|
+
model_provenance = prepared["provenance"]["model_provenance"]
|
|
615
|
+
if config is not None and model_provenance.get("hf_backbone") is not None:
|
|
616
|
+
raise ValueError("--config is native YAML and cannot silently override an HF backbone")
|
|
617
|
+
declared_seq_len = prepared["inference"]["seq_len"]
|
|
618
|
+
if config and "seq_len" in config and declared_seq_len is not None and config["seq_len"] != declared_seq_len:
|
|
619
|
+
raise ValueError("native config seq_len conflicts with checkpoint inference")
|
|
620
|
+
from .model import DEFAULT_CONFIG, load_decision
|
|
621
|
+
|
|
622
|
+
configured_fields = set(model_provenance["saved_model_config_fields"]) | set(config or {})
|
|
623
|
+
potential_defaults = None if model_provenance.get("hf_backbone") is not None else {
|
|
624
|
+
key: DEFAULT_CONFIG[key] for key in sorted(DEFAULT_CONFIG) if key not in configured_fields}
|
|
625
|
+
prepared["provenance"]["dataset_files"] = dataset_files
|
|
626
|
+
prepared["provenance"]["model_loading"] = {
|
|
627
|
+
"loader": "load_decision", "config_file": protocol.file_fingerprint(args.config) if args.config else None,
|
|
628
|
+
"explicit_model_config": config,
|
|
629
|
+
"potential_native_defaults": potential_defaults,
|
|
630
|
+
"legacy_defaults_possible": None if potential_defaults is None else bool(potential_defaults),
|
|
631
|
+
"limitation": "Undeclared native architecture fields may use listed defaults or be inferred from weights; external HF defaults are unverified. seq_len is recorded from the actual model.",
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
model, head = load_decision(args.ckpt, config=config, device=args.device)
|
|
635
|
+
artifact = _fit_prepared(model, head, prepared, args.ckpt, args.device)
|
|
636
|
+
save_calibration(args.out, artifact)
|
|
637
|
+
print(json.dumps({"calibration": str(args.out), "temperature": artifact["temperature"],
|
|
638
|
+
"training_overlap_check": artifact["provenance"]["training_overlap_check"]["status"],
|
|
639
|
+
"limitations": artifact["limitations"]}, allow_nan=False))
|
|
640
|
+
return artifact
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
if __name__ == "__main__":
|
|
644
|
+
main()
|