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/__init__.py +27 -0
- evalmetry/adapters.py +576 -0
- evalmetry/backend.py +942 -0
- evalmetry/benchmarks.py +261 -0
- evalmetry/debug.py +1421 -0
- evalmetry/hooks.py +782 -0
- evalmetry/judges.py +273 -0
- evalmetry/main.py +1504 -0
- evalmetry/models.py +209 -0
- evalmetry/module_stats.py +1084 -0
- evalmetry/recorder.py +556 -0
- evalmetry/reducers.py +578 -0
- evalmetry/report.py +2193 -0
- evalmetry/storage.py +1546 -0
- evalmetry-1.0.0.dist-info/METADATA +94 -0
- evalmetry-1.0.0.dist-info/RECORD +20 -0
- evalmetry-1.0.0.dist-info/WHEEL +5 -0
- evalmetry-1.0.0.dist-info/entry_points.txt +2 -0
- evalmetry-1.0.0.dist-info/licenses/LICENSE +21 -0
- evalmetry-1.0.0.dist-info/top_level.txt +1 -0
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])
|