jev-compatible-server 0.1.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.
@@ -0,0 +1,384 @@
1
+ """Generic final-hidden-state probe readout for Transformers checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Any
8
+
9
+ from .encoder_decoder import (
10
+ MarginTask,
11
+ _mapping,
12
+ _template,
13
+ aggregate_margin_answers,
14
+ compile_margin_tasks,
15
+ decision_metadata,
16
+ render_content,
17
+ )
18
+ from .protocol import DecisionRequest, DecisionResponse, NoulAnswer, Usage
19
+ from .runtime import DecisionRuntime, RuntimeErrorBase
20
+
21
+
22
+ def _state_path(state: Any, path: str) -> Any:
23
+ value = state
24
+ for part in path.split("."):
25
+ if not isinstance(value, Mapping) or part not in value:
26
+ raise RuntimeErrorBase(f"decision input state path is missing: {path}")
27
+ value = value[part]
28
+ return value
29
+
30
+
31
+ def render_probe_input(request: DecisionRequest, metadata: Mapping[str, Any]) -> str:
32
+ """Render a shared-state probe input entirely from declarative metadata."""
33
+
34
+ input_config = _mapping(metadata.get("input"), "decision.input")
35
+ template = _template(input_config.get("template"), "decision.input.template")
36
+ fields = _mapping(input_config.get("fields", {}), "decision.input.fields")
37
+ values: dict[str, str] = {"state": render_content(request.state)}
38
+ for name, path in fields.items():
39
+ if not isinstance(name, str) or not isinstance(path, str):
40
+ raise RuntimeErrorBase("decision.input.fields must map strings to state paths")
41
+ values[name] = render_content(_state_path(request.state, path))
42
+ try:
43
+ return template.format_map(values)
44
+ except KeyError as exc:
45
+ raise RuntimeErrorBase(
46
+ f"decision.input.template references an undeclared field: {exc.args[0]}"
47
+ ) from exc
48
+
49
+
50
+ def render_probe_task(task: MarginTask, metadata: Mapping[str, Any]) -> str:
51
+ """Render one candidate as an external answer for a verifier probe."""
52
+
53
+ input_config = _mapping(metadata.get("input"), "decision.input")
54
+ template = _template(input_config.get("template"), "decision.input.template")
55
+ try:
56
+ return template.format_map(
57
+ {
58
+ "query": task.query,
59
+ "instructions": task.instruction,
60
+ "candidate": task.document,
61
+ }
62
+ )
63
+ except KeyError as exc:
64
+ raise RuntimeErrorBase(
65
+ f"decision.input.template references an undeclared candidate field: "
66
+ f"{exc.args[0]}"
67
+ ) from exc
68
+
69
+
70
+ class HiddenStateProbeBackend(DecisionRuntime):
71
+ """Apply a configured linear or RBF probe to one hidden state per request."""
72
+
73
+ def __init__(
74
+ self,
75
+ model_id: str,
76
+ *,
77
+ config: dict[str, Any] | None = None,
78
+ device: str = "auto",
79
+ ) -> None:
80
+ try:
81
+ import numpy as np
82
+ import torch
83
+ from huggingface_hub import hf_hub_download
84
+ from transformers import AutoModel, AutoTokenizer
85
+ except ImportError as exc: # pragma: no cover - optional dependency
86
+ raise RuntimeErrorBase(
87
+ "HiddenStateProbeBackend requires the 'transformers' optional dependency"
88
+ ) from exc
89
+
90
+ self.model_name = str((config or {}).get("model", model_id))
91
+ self.config = config or {}
92
+ self.metadata = decision_metadata(self.config)
93
+ if self.metadata.get("readout") != "hidden_state_probe":
94
+ raise RuntimeErrorBase(
95
+ "HiddenStateProbeBackend requires decision.readout=hidden_state_probe"
96
+ )
97
+ self._np = np
98
+ self._torch = torch
99
+
100
+ loader = _mapping(self.metadata.get("loader", {}), "decision.loader")
101
+ revision = loader.get("revision")
102
+ if revision is not None and not isinstance(revision, str):
103
+ raise RuntimeErrorBase("decision.loader.revision must be a string")
104
+ trust_remote_code = loader.get("trust_remote_code", False)
105
+ if not isinstance(trust_remote_code, bool):
106
+ raise RuntimeErrorBase("decision.loader.trust_remote_code must be boolean")
107
+ tokenizer_id = loader.get("tokenizer", model_id)
108
+ if not isinstance(tokenizer_id, str):
109
+ raise RuntimeErrorBase("decision.loader.tokenizer must be a string")
110
+ common_kwargs: dict[str, Any] = {"trust_remote_code": trust_remote_code}
111
+ if revision is not None:
112
+ common_kwargs["revision"] = revision
113
+ self._tokenizer = AutoTokenizer.from_pretrained(tokenizer_id, **common_kwargs)
114
+ if self._tokenizer.pad_token_id is None:
115
+ if self._tokenizer.eos_token_id is None:
116
+ raise RuntimeErrorBase("tokenizer must define a pad token or an EOS token")
117
+ self._tokenizer.pad_token = self._tokenizer.eos_token
118
+ self._tokenizer.padding_side = "right"
119
+
120
+ requested_device = self.metadata.get("device", device)
121
+ target = (
122
+ "cuda"
123
+ if requested_device == "auto" and torch.cuda.is_available()
124
+ else "cpu"
125
+ if requested_device == "auto"
126
+ else requested_device
127
+ )
128
+ if not isinstance(target, str):
129
+ raise RuntimeErrorBase("decision.device must be a string")
130
+ if target.startswith("cuda") and not torch.cuda.is_available():
131
+ raise RuntimeErrorBase("CUDA was requested, but no CUDA device is available")
132
+ dtype = self._resolve_dtype(self.metadata.get("dtype"), target)
133
+ model_kwargs = dict(common_kwargs)
134
+ model_kwargs["dtype"] = dtype
135
+ model_kwargs["low_cpu_mem_usage"] = True
136
+ attention = loader.get("attn_implementation", "sdpa")
137
+ if attention is not None:
138
+ if not isinstance(attention, str):
139
+ raise RuntimeErrorBase(
140
+ "decision.loader.attn_implementation must be a string or null"
141
+ )
142
+ model_kwargs["attn_implementation"] = attention
143
+ device_map = loader.get("device_map")
144
+ if device_map is not None:
145
+ if not isinstance(device_map, str):
146
+ raise RuntimeErrorBase("decision.loader.device_map must be a string")
147
+ model_kwargs["device_map"] = device_map
148
+ self._model = AutoModel.from_pretrained(model_id, **model_kwargs)
149
+ if device_map is None:
150
+ self._model.to(target)
151
+ self._model.eval()
152
+
153
+ input_config = _mapping(self.metadata.get("input"), "decision.input")
154
+ self._input_mode = input_config.get("mode", "shared_state")
155
+ if self._input_mode not in {"shared_state", "candidate_tasks"}:
156
+ raise RuntimeErrorBase(
157
+ "decision.input.mode must be shared_state or candidate_tasks"
158
+ )
159
+ max_length = input_config.get("max_length", 2048)
160
+ if not isinstance(max_length, int) or isinstance(max_length, bool) or max_length <= 0:
161
+ raise RuntimeErrorBase("decision.input.max_length must be a positive integer")
162
+ self._max_length = max_length
163
+ batch_size = self.metadata.get("batch_size", 8)
164
+ if not isinstance(batch_size, int) or isinstance(batch_size, bool) or batch_size <= 0:
165
+ raise RuntimeErrorBase("decision.batch_size must be a positive integer")
166
+ self._batch_size = batch_size
167
+ hidden = _mapping(self.metadata.get("hidden", {}), "decision.hidden")
168
+ if hidden.get("token", "last_non_pad") != "last_non_pad":
169
+ raise RuntimeErrorBase(
170
+ "hidden_state_probe currently supports only hidden.token=last_non_pad"
171
+ )
172
+
173
+ probe = _mapping(self.metadata.get("probe"), "decision.probe")
174
+ self._probe_kind = probe.get("kind")
175
+ if self._probe_kind not in {"linear", "rbf"}:
176
+ raise RuntimeErrorBase("decision.probe.kind must be linear or rbf")
177
+ probe_file = probe.get("file")
178
+ if not isinstance(probe_file, str):
179
+ raise RuntimeErrorBase("decision.probe.file must be a string")
180
+ probe_repo = probe.get("repo", model_id)
181
+ if probe_repo == "auto":
182
+ probe_repo = model_id
183
+ if not isinstance(probe_repo, str):
184
+ raise RuntimeErrorBase("decision.probe.repo must be a string")
185
+ probe_revision = probe.get("revision", revision)
186
+ if probe_revision is not None and not isinstance(probe_revision, str):
187
+ raise RuntimeErrorBase("decision.probe.revision must be a string")
188
+ local_probe = hf_hub_download(
189
+ repo_id=probe_repo,
190
+ filename=probe_file,
191
+ revision=probe_revision,
192
+ )
193
+ payload = np.load(local_probe, allow_pickle=False)
194
+ required = (
195
+ ("w", "mu", "sd")
196
+ if self._probe_kind == "linear"
197
+ else ("anchors", "alpha", "mu", "sd", "med", "gamma")
198
+ )
199
+ missing = [name for name in required if name not in payload]
200
+ if missing:
201
+ raise RuntimeErrorBase(
202
+ f"decision probe is missing arrays: {', '.join(missing)}"
203
+ )
204
+ self._probe = {
205
+ name: torch.from_numpy(
206
+ np.asarray(payload[name], dtype=np.float32).copy()
207
+ )
208
+ for name in required
209
+ }
210
+ if bool((self._probe["sd"] == 0).any()):
211
+ raise RuntimeErrorBase("decision probe sd contains zero")
212
+ self._calibration: dict[str, float] | None
213
+ self._output = _mapping(self.metadata.get("output", {}), "decision.output")
214
+ transform = self._output.get("transform")
215
+ if self._input_mode == "shared_state" and transform not in {
216
+ "sigmoid",
217
+ "probe_calibrated_sigmoid",
218
+ }:
219
+ raise RuntimeErrorBase(
220
+ "shared_state probes require decision.output.transform="
221
+ "sigmoid or probe_calibrated_sigmoid"
222
+ )
223
+ if transform == "probe_calibrated_sigmoid":
224
+ calibration = ("s_mean", "s_std", "cal_A", "cal_B")
225
+ absent = [name for name in calibration if name not in payload]
226
+ if absent:
227
+ raise RuntimeErrorBase(
228
+ f"decision probe is missing calibration arrays: {', '.join(absent)}"
229
+ )
230
+ self._calibration = {
231
+ name: float(payload[name]) for name in calibration
232
+ }
233
+ if self._calibration["s_std"] == 0:
234
+ raise RuntimeErrorBase("decision probe calibration s_std is zero")
235
+ else:
236
+ self._calibration = None
237
+
238
+ def _resolve_dtype(self, value: Any, device: str) -> Any:
239
+ torch = self._torch
240
+ if value is None or value == "auto":
241
+ return torch.bfloat16 if device.startswith("cuda") else torch.float32
242
+ if value in {"bfloat16", "bf16", "torch.bfloat16"}:
243
+ return torch.bfloat16
244
+ if value in {"float16", "fp16", "torch.float16"}:
245
+ return torch.float16
246
+ if value in {"float32", "fp32", "torch.float32"}:
247
+ return torch.float32
248
+ raise RuntimeErrorBase(f"unsupported decision dtype: {value!r}")
249
+
250
+ def _input_device(self) -> Any:
251
+ embeddings = self._model.get_input_embeddings()
252
+ if embeddings is not None and hasattr(embeddings, "weight"):
253
+ return embeddings.weight.device
254
+ return next(self._model.parameters()).device
255
+
256
+ def _raw_scores(self, hidden: Any) -> Any:
257
+ torch = self._torch
258
+ probe = {name: value.to(hidden.device) for name, value in self._probe.items()}
259
+ standardized = (hidden.float() - probe["mu"]) / probe["sd"]
260
+ if self._probe_kind == "linear":
261
+ scores = standardized @ probe["w"]
262
+ else:
263
+ anchors = probe["anchors"]
264
+ squared = (
265
+ (anchors * anchors).sum(dim=1).unsqueeze(0)
266
+ + (standardized * standardized).sum(dim=1).unsqueeze(1)
267
+ - 2.0 * (standardized @ anchors.T)
268
+ )
269
+ denominator = probe["med"] * probe["gamma"]
270
+ if not bool(torch.isfinite(denominator)) or float(denominator) <= 0:
271
+ raise RuntimeErrorBase("decision RBF probe has an invalid denominator")
272
+ kernel = torch.exp(-torch.clamp(squared, min=0.0) / denominator)
273
+ scores = kernel @ probe["alpha"]
274
+ if not bool(torch.isfinite(scores).all()):
275
+ raise RuntimeErrorBase("decision probe produced non-finite scores")
276
+ return scores
277
+
278
+ def _probabilities(self, scores: Any) -> Any:
279
+ torch = self._torch
280
+ if self._calibration is not None:
281
+ values = (
282
+ self._calibration["cal_A"]
283
+ * (scores - self._calibration["s_mean"])
284
+ / self._calibration["s_std"]
285
+ + self._calibration["cal_B"]
286
+ )
287
+ else:
288
+ scale = self._output.get("scale", 1.0)
289
+ bias = self._output.get("bias", 0.0)
290
+ if not isinstance(scale, int | float) or not isinstance(bias, int | float):
291
+ raise RuntimeErrorBase("decision.output scale and bias must be numeric")
292
+ if not math.isfinite(float(scale)) or not math.isfinite(float(bias)):
293
+ raise RuntimeErrorBase("decision.output scale and bias must be finite")
294
+ values = scores * float(scale) + float(bias)
295
+ return torch.sigmoid(values)
296
+
297
+ def _score_texts(self, texts: Sequence[str]) -> tuple[list[float], list[int]]:
298
+ scores: list[float] = []
299
+ input_tokens: list[int] = []
300
+ torch = self._torch
301
+ device = self._input_device()
302
+ for start in range(0, len(texts), self._batch_size):
303
+ encoded = self._tokenizer(
304
+ list(texts[start : start + self._batch_size]),
305
+ return_tensors="pt",
306
+ padding=True,
307
+ truncation=True,
308
+ max_length=self._max_length,
309
+ )
310
+ encoded = {name: value.to(device) for name, value in encoded.items()}
311
+ with torch.inference_mode():
312
+ output = self._model(**encoded)
313
+ last_hidden = getattr(output, "last_hidden_state", None)
314
+ if last_hidden is None:
315
+ raise RuntimeErrorBase("model output has no last_hidden_state")
316
+ mask = encoded.get("attention_mask")
317
+ if mask is None:
318
+ raise RuntimeErrorBase("tokenizer output has no attention_mask")
319
+ positions = mask.sum(dim=1) - 1
320
+ rows = torch.arange(last_hidden.shape[0], device=last_hidden.device)
321
+ selected = last_hidden[rows, positions.to(last_hidden.device)]
322
+ scores.extend(
323
+ float(value)
324
+ for value in self._raw_scores(selected).cpu().tolist()
325
+ )
326
+ input_tokens.extend(int(value) for value in mask.sum(dim=1).cpu().tolist())
327
+ return scores, input_tokens
328
+
329
+ def _decide_candidate_tasks(
330
+ self, requests: Sequence[DecisionRequest]
331
+ ) -> list[DecisionResponse]:
332
+ compiled = [compile_margin_tasks(request, self.metadata) for request in requests]
333
+ tasks = [task for request_tasks in compiled for task in request_tasks]
334
+ scores, input_tokens = self._score_texts(
335
+ [render_probe_task(task, self.metadata) for task in tasks]
336
+ )
337
+ responses: list[DecisionResponse] = []
338
+ offset = 0
339
+ for request, request_tasks in zip(requests, compiled, strict=True):
340
+ end = offset + len(request_tasks)
341
+ responses.append(
342
+ DecisionResponse(
343
+ model=self.model_name,
344
+ answers=aggregate_margin_answers(
345
+ request,
346
+ request_tasks,
347
+ scores[offset:end],
348
+ self.metadata,
349
+ ),
350
+ usage=Usage(input_tokens=sum(input_tokens[offset:end])),
351
+ )
352
+ )
353
+ offset = end
354
+ return responses
355
+
356
+ def decide_batch(
357
+ self, requests: Sequence[DecisionRequest]
358
+ ) -> list[DecisionResponse]:
359
+ if self._input_mode == "candidate_tasks":
360
+ return self._decide_candidate_tasks(requests)
361
+ texts = [render_probe_input(request, self.metadata) for request in requests]
362
+ scores, input_tokens = self._score_texts(texts)
363
+ probabilities = [
364
+ float(value)
365
+ for value in self._probabilities(
366
+ self._torch.tensor(scores, dtype=self._torch.float32)
367
+ ).tolist()
368
+ ]
369
+
370
+ responses: list[DecisionResponse] = []
371
+ for request, probability, tokens in zip(
372
+ requests, probabilities, input_tokens, strict=True
373
+ ):
374
+ responses.append(
375
+ DecisionResponse(
376
+ model=self.model_name,
377
+ answers={
378
+ name: NoulAnswer(type="noul", noul=probability)
379
+ for name in request.questions
380
+ },
381
+ usage=Usage(input_tokens=tokens),
382
+ )
383
+ )
384
+ return responses
@@ -0,0 +1,135 @@
1
+ """Adapter for the published convaiinnovations/laya checkpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ from .protocol import (
11
+ ChoiceAnswer,
12
+ DecisionRequest,
13
+ DecisionResponse,
14
+ NoulAnswer,
15
+ ScoreAnswer,
16
+ Usage,
17
+ )
18
+ from .runtime import DecisionRuntime, RuntimeErrorBase
19
+
20
+
21
+ class LayaBackend(DecisionRuntime):
22
+ """Run Laya's checkpoint-native RLAgent API behind the JEV wire contract."""
23
+
24
+ def __init__(self, model_id: str, *, config: dict[str, Any] | None = None) -> None:
25
+ try:
26
+ from huggingface_hub import snapshot_download
27
+ except ImportError as exc: # pragma: no cover - optional dependency
28
+ raise RuntimeErrorBase("LayaBackend requires huggingface_hub") from exc
29
+
30
+ self.config = config or {}
31
+ self.model_name = str(self.config.get("model", model_id))
32
+ revision = self.config.get("revision")
33
+ download_kwargs: dict[str, Any] = {}
34
+ if isinstance(revision, str):
35
+ download_kwargs["revision"] = revision
36
+ model_dir = Path(snapshot_download(model_id, **download_kwargs))
37
+ # The published checkpoint ships its inference modules beside the weights.
38
+ source_dir = str(model_dir)
39
+ if source_dir not in sys.path:
40
+ sys.path.insert(0, source_dir)
41
+ try:
42
+ from rl_agent_api import RLAgent # type: ignore[import-not-found]
43
+ except ImportError as exc: # pragma: no cover - model-owned optional code
44
+ raise RuntimeErrorBase(
45
+ "Laya checkpoint does not contain rl_agent_api.py"
46
+ ) from exc
47
+ try:
48
+ self._agent = RLAgent(str(model_dir))
49
+ except Exception as exc: # pragma: no cover - model-owned loading errors
50
+ raise RuntimeErrorBase(f"failed to load Laya checkpoint {model_id!r}: {exc}") from exc
51
+
52
+ @staticmethod
53
+ def _question_payload(question: Any) -> dict[str, Any]:
54
+ if hasattr(question, "model_dump"):
55
+ payload = question.model_dump(mode="json")
56
+ if isinstance(payload, dict) and all(
57
+ isinstance(key, str) for key in payload
58
+ ):
59
+ return cast(dict[str, Any], payload)
60
+ raise RuntimeErrorBase("Laya requires pydantic decision questions")
61
+
62
+ @staticmethod
63
+ def _answer(value: Any) -> Any:
64
+ if not isinstance(value, dict) or not isinstance(value.get("type"), str):
65
+ raise RuntimeErrorBase("Laya returned an invalid answer")
66
+ kind = value["type"]
67
+ if kind == "choice":
68
+ probabilities = value.get("probabilities")
69
+ choice = value.get("choice")
70
+ confidence = value.get("confidence")
71
+ if (
72
+ not isinstance(probabilities, dict)
73
+ or not isinstance(choice, str)
74
+ or not isinstance(confidence, (int, float))
75
+ or isinstance(confidence, bool)
76
+ ):
77
+ raise RuntimeErrorBase("Laya returned an invalid choice answer")
78
+ return ChoiceAnswer(
79
+ type="choice",
80
+ choice=choice,
81
+ probabilities={str(key): float(prob) for key, prob in probabilities.items()},
82
+ confidence=float(confidence),
83
+ )
84
+ if kind == "score":
85
+ probabilities = value.get("probabilities")
86
+ legend = value.get("legend")
87
+ if not isinstance(probabilities, dict):
88
+ raise RuntimeErrorBase("Laya returned an invalid score answer")
89
+ ordered_legend: list[Any] | None = None
90
+ if isinstance(legend, dict):
91
+ ordered_legend = [legend[key] for key in sorted(legend, key=lambda item: int(item))]
92
+ elif isinstance(legend, list):
93
+ ordered_legend = legend
94
+ return ScoreAnswer(
95
+ type="score",
96
+ score=float(value["score"]),
97
+ probabilities={str(key): float(prob) for key, prob in probabilities.items()},
98
+ confidence=float(value["confidence"]),
99
+ legend=ordered_legend,
100
+ )
101
+ if kind == "noul":
102
+ return NoulAnswer(type="noul", noul=float(value["noul"]))
103
+ raise RuntimeErrorBase(f"Laya returned unsupported answer type: {kind!r}")
104
+
105
+ def decide_batch(self, requests: Sequence[DecisionRequest]) -> list[DecisionResponse]:
106
+ responses: list[DecisionResponse] = []
107
+ for request in requests:
108
+ questions = {
109
+ name: self._question_payload(question)
110
+ for name, question in request.questions.items()
111
+ }
112
+ try:
113
+ result = self._agent.system_one(request.state, questions)
114
+ except Exception as exc: # pragma: no cover - model-owned inference errors
115
+ raise RuntimeErrorBase(f"Laya inference failed: {exc}") from exc
116
+ if not isinstance(result, dict) or not isinstance(result.get("answers"), dict):
117
+ raise RuntimeErrorBase("Laya returned no answers")
118
+ answers = {
119
+ name: self._answer(result["answers"][name])
120
+ for name in questions
121
+ if name in result["answers"]
122
+ }
123
+ if len(answers) != len(questions):
124
+ raise RuntimeErrorBase("Laya omitted one or more requested answers")
125
+ usage_value = result.get("usage")
126
+ usage = Usage(
127
+ input_tokens=int(usage_value.get("input_tokens", 0))
128
+ if isinstance(usage_value, dict)
129
+ else 0,
130
+ output_tokens=int(usage_value.get("output_tokens", 0))
131
+ if isinstance(usage_value, dict)
132
+ else 0,
133
+ )
134
+ responses.append(DecisionResponse(model=self.model_name, answers=answers, usage=usage))
135
+ return responses