evalmetry 1.0.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.
evalmetry/models.py ADDED
@@ -0,0 +1,209 @@
1
+ """Custom HF models, provenance and optional input formatting.
2
+
3
+ Factories receive a JSON object and return ModelBundle. They own model loading,
4
+ placement and modifications; the evaluator never reloads their checkpoint.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ import hashlib
10
+ import importlib
11
+ import inspect
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any, Callable
15
+
16
+
17
+ def import_factory(path: str) -> Callable:
18
+ """Resolve an explicit package.module:function and report invalid entrypoints."""
19
+ module, separator, name = path.partition(":")
20
+ if not separator or not module or not name:
21
+ raise ValueError("model factory must be package.module:function")
22
+ value = getattr(importlib.import_module(module), name)
23
+ if not callable(value):
24
+ raise TypeError(f"{path} is not callable")
25
+ return value
26
+
27
+
28
+ def fingerprint_files(paths) -> dict[str, str]:
29
+ """Hash files or directory contents, streaming checkpoints without loading them.
30
+
31
+ This reads every byte, including large checkpoints. Run it once in a factory;
32
+ do not call it for each forward. Missing inputs are errors.
33
+ """
34
+ result = {}
35
+ for item in paths:
36
+ path = Path(item).resolve()
37
+ if not path.exists():
38
+ raise ValueError(f"provenance file does not exist: {path}")
39
+ files = sorted(p for p in path.rglob("*") if p.is_file()) if path.is_dir() else [path]
40
+ for file in files:
41
+ digest = hashlib.sha256()
42
+ with file.open("rb") as handle:
43
+ for block in iter(lambda: handle.read(1024 * 1024), b""):
44
+ digest.update(block)
45
+ result[str(file)] = digest.hexdigest()
46
+ return result
47
+
48
+
49
+ @dataclass
50
+ class ModelBundle:
51
+ """One already-loaded text causal LM with HF forward/generate interfaces.
52
+
53
+ model and tokenizer are passed by identity to HFLM. The caller owns device,
54
+ dtype and deterministic construction. Evaluation may call model.eval().
55
+ version identifies all implementation dependencies not in source_files.
56
+ checkpoint_id must identify immutable weights (Hub commit or file hashes),
57
+ not a mutable branch/path. Missing version/checkpoint disables resume.
58
+ adapter_factory(model) may return ModelAdapter or ModulePaths, and is only
59
+ called when internal signals are requested.
60
+ prompt_transform(text) changes context only, never answer continuations.
61
+ It must be deterministic and stateless. Samples retain original task prompts;
62
+ evaluation applies this function before tokenization and stores its result
63
+ alongside the original context; replay uses the saved result.
64
+ capabilities lists signals whose contracts the custom implementation retains.
65
+
66
+ Example: ModelBundle(model, tokenizer, "my-mlp", version="v1",
67
+ checkpoint_id=fingerprint_files([checkpoint_dir]))
68
+ """
69
+ model: Any
70
+ tokenizer: Any
71
+ model_id: str
72
+ version: str = ""
73
+ checkpoint_id: Any = None
74
+ adapter_factory: Callable | None = None
75
+ prompt_transform: Callable[[str], str] | None = None
76
+ source_files: list[str] = field(default_factory=list)
77
+ capabilities: tuple[str, ...] = ("logit_lens", "similarity", "hidden", "attention", "hooks")
78
+
79
+
80
+ def load_bundle(config):
81
+ """Validate a factory/object and capture the reproducible construction contract."""
82
+ if config.model_bundle is not None and config.model_factory:
83
+ raise ValueError("pass model_bundle or model_factory, not both")
84
+ if not isinstance(config.model_config, dict):
85
+ raise TypeError("model_config must be a JSON object")
86
+ json.dumps(config.model_config, allow_nan=False)
87
+ factory = import_factory(config.model_factory) if config.model_factory else None
88
+ # JSON round-trip isolates nested factory mutations from the saved settings.
89
+ bundle = factory(json.loads(json.dumps(config.model_config))) if factory else config.model_bundle
90
+ if not isinstance(bundle, ModelBundle):
91
+ raise TypeError("model factory must return ModelBundle")
92
+ if not bundle.model_id or bundle.model is None or bundle.tokenizer is None:
93
+ raise ValueError("ModelBundle requires model, tokenizer and model_id")
94
+ sources = list(bundle.source_files)
95
+ source_available = True
96
+ for function in (factory, bundle.adapter_factory, bundle.prompt_transform):
97
+ if function is None:
98
+ continue
99
+ if not callable(function):
100
+ raise TypeError("adapter_factory and prompt_transform must be callable")
101
+ try:
102
+ source = inspect.getsourcefile(function)
103
+ except TypeError:
104
+ source = None
105
+ if source and Path(source).is_file():
106
+ sources.append(source)
107
+ # Include helper modules in the entrypoint's local package, not just
108
+ # the factory function. External dependencies remain version's contract.
109
+ module = importlib.import_module(function.__module__.split('.')[0])
110
+ for root in getattr(module, "__path__", []):
111
+ sources.extend(str(p) for p in Path(root).rglob("*.py"))
112
+ else:
113
+ source_available = False
114
+ tokenizer = bundle.tokenizer
115
+ tok_backend = getattr(tokenizer, "backend_tokenizer", None)
116
+ slow_files = {}
117
+ if tok_backend is None:
118
+ # Vocabulary alone misses SentencePiece scores and tokenizer rules.
119
+ # Persist the actual tokenizer assets briefly, then hash their contents.
120
+ from tempfile import TemporaryDirectory
121
+ with TemporaryDirectory(prefix="eval-model-tokenizer-") as directory:
122
+ tokenizer.save_pretrained(directory)
123
+ slow_files = {str(Path(path).relative_to(directory)): digest
124
+ for path, digest in fingerprint_files([directory]).items()}
125
+ from importlib.metadata import version
126
+ provenance = {
127
+ "packages": {name: version(name) for name in ("torch", "transformers", "lm-eval")},
128
+ "backend_options": config.model_kwargs(),
129
+ "adapter": config.adapter,
130
+ "dtypes": sorted({str(p.dtype) for p in bundle.model.parameters()}),
131
+ "factory": config.model_factory, "config": config.model_config,
132
+ "model_id": bundle.model_id, "version": bundle.version,
133
+ "checkpoint_id": bundle.checkpoint_id,
134
+ "sources": fingerprint_files(sources),
135
+ "capabilities": sorted(bundle.capabilities),
136
+ "model_config": bundle.model.config.to_dict(),
137
+ "generation_config": bundle.model.generation_config.to_dict(),
138
+ "tokenizer": {"vocab_sha256": hashlib.sha256(json.dumps(tokenizer.get_vocab(), sort_keys=True).encode()).hexdigest(),
139
+ "special_tokens": tokenizer.special_tokens_map,
140
+ "model_max_length": tokenizer.model_max_length,
141
+ "init_sha256": hashlib.sha256(json.dumps(tokenizer.init_kwargs, sort_keys=True, default=str).encode()).hexdigest(),
142
+ "slow_files": slow_files,
143
+ "add_bos_token": getattr(tokenizer, "add_bos_token", None),
144
+ "add_eos_token": getattr(tokenizer, "add_eos_token", None),
145
+ "clean_up_tokenization_spaces": getattr(tokenizer, "clean_up_tokenization_spaces", None),
146
+ "chat_template": getattr(tokenizer, "chat_template", None),
147
+ "padding_side": tokenizer.padding_side,
148
+ "truncation_side": tokenizer.truncation_side,
149
+ "backend_sha256": hashlib.sha256(tok_backend.to_str().encode()).hexdigest() if tok_backend else None},
150
+ "resume_safe": bool(factory and bundle.version and bundle.checkpoint_id and source_available),
151
+ }
152
+ # Normalize tuples etc. before comparing a new object with a JSON manifest.
153
+ config.model_provenance = json.loads(json.dumps(provenance, allow_nan=False))
154
+ config.resolved_bundle = bundle
155
+ return bundle
156
+
157
+
158
+ def resolve_model_adapter(config, lm, *, collection=False):
159
+ """Resolve only requested capabilities; scoring alone needs no adapter."""
160
+ from .adapters import ModelAdapter, ModulePaths, resolve_adapter
161
+ requested = set(() if collection else config.signals)
162
+ if config.save_hidden:
163
+ requested.add("hidden")
164
+ if config.save_attention:
165
+ requested.add("attention")
166
+ if config.resolved_hooks:
167
+ requested.add("hooks")
168
+ if not requested:
169
+ return None
170
+ bundle = config.resolved_bundle
171
+ if bundle:
172
+ missing = requested - set(bundle.capabilities)
173
+ if missing:
174
+ raise ValueError(f"custom model does not support requested signals: {sorted(missing)}")
175
+ if bundle and bundle.adapter_factory:
176
+ if config.adapter:
177
+ raise ValueError("--adapter conflicts with ModelBundle.adapter_factory")
178
+ adapter = bundle.adapter_factory(lm.model)
179
+ if isinstance(adapter, ModulePaths):
180
+ adapter = resolve_adapter(lm.model, paths=adapter, require_decode="logit_lens" in requested)
181
+ if not isinstance(adapter, ModelAdapter):
182
+ raise TypeError("adapter_factory must return ModelAdapter or ModulePaths")
183
+ else:
184
+ adapter = resolve_adapter(lm.model, config.adapter, require_decode="logit_lens" in requested)
185
+ if "attention" in requested and (not any(adapter.attn_modules) or not any(adapter.v_projs)):
186
+ raise ValueError("attention collection requires attention and value projection modules")
187
+ return adapter
188
+
189
+
190
+ def save_effective_prompts(samples_by_task, lm):
191
+ """Save the formatted contexts so replay and report use the scored protocol.
192
+
193
+ The formatter contract is deterministic/stateless. Keep originals for audit,
194
+ and mark effective contexts so replay never applies the formatter twice.
195
+ """
196
+ if getattr(lm, "prompt_transform", None) is None:
197
+ return
198
+ import copy
199
+ from lm_eval.utils import hash_string
200
+ for samples in samples_by_task.values():
201
+ for sample in samples:
202
+ arguments = sample.get("arguments", [])
203
+ sample["original_arguments"] = copy.deepcopy(arguments)
204
+ sample["original_prompt_hash"] = sample.get("prompt_hash")
205
+ prepared = [[lm.prepare_context(args[0]), *args[1:]] for args in arguments]
206
+ sample["arguments"] = prepared
207
+ sample["model_prompt_prepared"] = True
208
+ if prepared:
209
+ sample["prompt_hash"] = hash_string(prepared[0][0])