auditkit 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.
- auditkit/README.md +99 -0
- auditkit/__init__.py +177 -0
- auditkit/__main__.py +3 -0
- auditkit/_bootstrap.py +77 -0
- auditkit/_identity_guard.py +99 -0
- auditkit/adapter.py +264 -0
- auditkit/annotator.py +339 -0
- auditkit/api.py +502 -0
- auditkit/assets/auditkit_logo.png +0 -0
- auditkit/cache.py +47 -0
- auditkit/cli.py +417 -0
- auditkit/comparison.py +563 -0
- auditkit/diff.py +265 -0
- auditkit/errors.py +54 -0
- auditkit/evaluator.py +20 -0
- auditkit/experiment.py +145 -0
- auditkit/hf_publish.py +262 -0
- auditkit/lmeval_engine.py +550 -0
- auditkit/loaders.py +121 -0
- auditkit/logs.py +18 -0
- auditkit/metric.py +199 -0
- auditkit/metrics/README.md +15 -0
- auditkit/metrics/__init__.py +0 -0
- auditkit/metrics/code.py +222 -0
- auditkit/metrics/embedding.py +131 -0
- auditkit/metrics/encoder_judge.py +423 -0
- auditkit/metrics/generation.py +331 -0
- auditkit/metrics/guard.py +412 -0
- auditkit/metrics/hallucination.py +45 -0
- auditkit/metrics/judge.py +547 -0
- auditkit/metrics/pairwise.py +153 -0
- auditkit/metrics/perf.py +53 -0
- auditkit/metrics/rag.py +149 -0
- auditkit/metrics/security.py +64 -0
- auditkit/metrics/toxicity.py +238 -0
- auditkit/model/README.md +16 -0
- auditkit/model/__init__.py +485 -0
- auditkit/model/anthropic.py +94 -0
- auditkit/model/api_gen.py +133 -0
- auditkit/model/groq_gen.py +121 -0
- auditkit/model/hf_gen.py +385 -0
- auditkit/model/lexsi.py +155 -0
- auditkit/model/litellm_gen.py +65 -0
- auditkit/model/openai.py +90 -0
- auditkit/model/openrouter_gen.py +152 -0
- auditkit/model/vllm_gen.py +316 -0
- auditkit/model_compare.py +655 -0
- auditkit/redteam/README.md +9 -0
- auditkit/redteam/__init__.py +26 -0
- auditkit/redteam/detector.py +37 -0
- auditkit/redteam/detectors/README.md +5 -0
- auditkit/redteam/detectors/builtin.py +126 -0
- auditkit/redteam/probe.py +39 -0
- auditkit/redteam/probes/README.md +5 -0
- auditkit/redteam/probes/builtin.py +85 -0
- auditkit/redteam/runner.py +206 -0
- auditkit/registry.py +65 -0
- auditkit/report.py +278 -0
- auditkit/report_format.py +52 -0
- auditkit/router.py +54 -0
- auditkit/runner.py +575 -0
- auditkit/runspec.py +159 -0
- auditkit/sample.py +40 -0
- auditkit/scenario.py +88 -0
- auditkit/scenarios/README.md +10 -0
- auditkit/scenarios/__init__.py +4 -0
- auditkit/scenarios/arc.py +33 -0
- auditkit/scenarios/gsm8k.py +32 -0
- auditkit/scenarios/hellaswag.py +33 -0
- auditkit/scenarios/humaneval.py +32 -0
- auditkit/scenarios/mmlu.py +34 -0
- auditkit/scenarios/truthfulqa.py +33 -0
- auditkit/score.py +165 -0
- auditkit/scorers.py +117 -0
- auditkit/scoring.py +79 -0
- auditkit/types.py +69 -0
- auditkit-1.0.0.dist-info/METADATA +396 -0
- auditkit-1.0.0.dist-info/RECORD +81 -0
- auditkit-1.0.0.dist-info/WHEEL +4 -0
- auditkit-1.0.0.dist-info/entry_points.txt +2 -0
- auditkit-1.0.0.dist-info/licenses/LICENSE.md +92 -0
auditkit/api.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""The public evaluation API: :func:`evaluate`.
|
|
2
|
+
|
|
3
|
+
This is what users import::
|
|
4
|
+
|
|
5
|
+
import auditkit as ak
|
|
6
|
+
r = ak.evaluate([ak.Sample(input="hi", target="HI")], model=lambda prompts: ["HI" for _ in prompts])
|
|
7
|
+
r.headline # {"exact_match": 1.0}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Callable, Iterable, Optional, Sequence, Union
|
|
14
|
+
|
|
15
|
+
from .adapter import Adapter, GenerationAdapter
|
|
16
|
+
from .annotator import Annotator
|
|
17
|
+
from .experiment import Experiment, ExperimentDB
|
|
18
|
+
from .loaders import load_csv # noqa: F401 — re-exported via __init__
|
|
19
|
+
from .metric import ExactMatch, Metric, QuasiExactMatch
|
|
20
|
+
from .model import (
|
|
21
|
+
Model,
|
|
22
|
+
AutoModel,
|
|
23
|
+
)
|
|
24
|
+
from .report import RunResult
|
|
25
|
+
from .runner import Runner
|
|
26
|
+
from .runspec import RunConfig, RunSpec
|
|
27
|
+
from .sample import Sample
|
|
28
|
+
from .scenario import CallableScenario, ListScenario, Scenario
|
|
29
|
+
from .scorers import FunctionScorer, ScorerMetric, ScorerType, scorer # noqa: F401
|
|
30
|
+
from .errors import AuditKitError, RegistryError
|
|
31
|
+
from .registry import ADAPTERS, ANNOTATORS, METRICS, SCENARIOS
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("auditkit")
|
|
34
|
+
|
|
35
|
+
#: Known metric names resolvable from a string.
|
|
36
|
+
_BUILTIN_METRICS: dict[str, type[Metric]] = {
|
|
37
|
+
"exact_match": ExactMatch,
|
|
38
|
+
"quasi_exact_match": QuasiExactMatch,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _to_scenario(
|
|
43
|
+
dataset: Union[Scenario, Sequence[Sample], Callable[[], Iterable[Sample]], str],
|
|
44
|
+
) -> Scenario:
|
|
45
|
+
"""Coerce any supported dataset form to a :class:`Scenario`."""
|
|
46
|
+
if isinstance(dataset, Scenario):
|
|
47
|
+
return dataset
|
|
48
|
+
if isinstance(dataset, str):
|
|
49
|
+
try:
|
|
50
|
+
return SCENARIOS.get(dataset)()
|
|
51
|
+
except RegistryError:
|
|
52
|
+
available = ", ".join(SCENARIOS.names())
|
|
53
|
+
raise AuditKitError(f"Unknown dataset '{dataset}'. Available: [{available}]")
|
|
54
|
+
if callable(dataset):
|
|
55
|
+
return CallableScenario(dataset)
|
|
56
|
+
# treat it as a list/iterable of Samples
|
|
57
|
+
return ListScenario(list(dataset))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _to_metrics(
|
|
61
|
+
scorers: Any,
|
|
62
|
+
samples: Sequence[Sample],
|
|
63
|
+
) -> list[Metric]:
|
|
64
|
+
"""Coerce scorers/user metrics to a list of internal :class:`Metric`.
|
|
65
|
+
|
|
66
|
+
The resolution rules (from HANDOFF §Cycle-8 *step 4*):
|
|
67
|
+
|
|
68
|
+
- ``None``: auto-select. If any sample is golden (has a ``target``),
|
|
69
|
+
return ``[ExactMatch()]``; otherwise return ``[]``.
|
|
70
|
+
- ``str``: look up in the built-in map (``"exact_match"``,
|
|
71
|
+
``"quasi_exact_match"``), falling back to the :data:`METRICS` registry
|
|
72
|
+
(populated by every metric class in ``metrics/*.py``) for any other
|
|
73
|
+
registered name (e.g. ``"acc_norm"``, ``"bleu"``, ``"lexical_groundedness"``).
|
|
74
|
+
- :class:`Metric`: returned as-is.
|
|
75
|
+
- :class:`ScorerType` (``FunctionScorer`` / callable): wrap in
|
|
76
|
+
:class:`ScorerMetric`.
|
|
77
|
+
- ``list``: recurse on each element.
|
|
78
|
+
"""
|
|
79
|
+
if scorers is None:
|
|
80
|
+
if any(s.is_golden for s in samples):
|
|
81
|
+
return [ExactMatch()]
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
if isinstance(scorers, str):
|
|
85
|
+
cls = _BUILTIN_METRICS.get(scorers)
|
|
86
|
+
if cls is None:
|
|
87
|
+
try:
|
|
88
|
+
cls = METRICS.get(scorers)
|
|
89
|
+
except RegistryError:
|
|
90
|
+
cls = None
|
|
91
|
+
if cls is None:
|
|
92
|
+
known = sorted(set(_BUILTIN_METRICS) | set(METRICS.names()))
|
|
93
|
+
raise ValueError(f"unknown metric {scorers!r}; known: {known}")
|
|
94
|
+
return [cls()]
|
|
95
|
+
|
|
96
|
+
if isinstance(scorers, Metric):
|
|
97
|
+
return [scorers]
|
|
98
|
+
|
|
99
|
+
if isinstance(scorers, FunctionScorer) or callable(scorers):
|
|
100
|
+
# Wrap a bare callable as a FunctionScorer if needed, then as Metric
|
|
101
|
+
if not isinstance(scorers, FunctionScorer):
|
|
102
|
+
scorers = FunctionScorer(scorers)
|
|
103
|
+
return [ScorerMetric(scorers)]
|
|
104
|
+
|
|
105
|
+
if isinstance(scorers, list):
|
|
106
|
+
result: list[Metric] = []
|
|
107
|
+
for item in scorers:
|
|
108
|
+
result.extend(_to_metrics(item, samples))
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
raise TypeError(f"cannot interpret scorers={scorers!r}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _to_adapter(adapter: Union[Adapter, str, None], samples: Any = None) -> Adapter:
|
|
115
|
+
"""Coerce ``None``/``"auto"``/a name string/an :class:`Adapter` to an :class:`Adapter`.
|
|
116
|
+
|
|
117
|
+
``"auto"`` routes on the dataset's shape (``choices`` -> MCQ,
|
|
118
|
+
``retrieval_context`` -> RAG, else generation); it needs ``samples``.
|
|
119
|
+
"""
|
|
120
|
+
if adapter is None:
|
|
121
|
+
return GenerationAdapter()
|
|
122
|
+
if adapter == "auto":
|
|
123
|
+
from .router import route_adapter
|
|
124
|
+
return route_adapter(samples if samples is not None else [])
|
|
125
|
+
if isinstance(adapter, str):
|
|
126
|
+
try:
|
|
127
|
+
return ADAPTERS.get(adapter)()
|
|
128
|
+
except RegistryError:
|
|
129
|
+
raise AuditKitError(f"Unknown adapter {adapter!r}. Available: {ADAPTERS.names()}")
|
|
130
|
+
return adapter
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _to_annotators(annotators: Any) -> list[Annotator]:
|
|
134
|
+
"""Coerce ``None``/an :class:`Annotator`/a name string/a list of any of
|
|
135
|
+
those to ``list[Annotator]``."""
|
|
136
|
+
if annotators is None:
|
|
137
|
+
return []
|
|
138
|
+
if isinstance(annotators, Annotator):
|
|
139
|
+
return [annotators]
|
|
140
|
+
if isinstance(annotators, str):
|
|
141
|
+
try:
|
|
142
|
+
return [ANNOTATORS.get(annotators)()]
|
|
143
|
+
except RegistryError:
|
|
144
|
+
raise AuditKitError(f"Unknown annotator {annotators!r}. Available: {ANNOTATORS.names()}")
|
|
145
|
+
if isinstance(annotators, list):
|
|
146
|
+
result: list[Annotator] = []
|
|
147
|
+
for item in annotators:
|
|
148
|
+
result.extend(_to_annotators(item))
|
|
149
|
+
return result
|
|
150
|
+
raise TypeError(f"cannot interpret annotators={annotators!r}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _record_experiment(
|
|
154
|
+
result: RunResult, experiment_name: str | None, tags: list[str] | None
|
|
155
|
+
) -> RunResult:
|
|
156
|
+
"""Stamp experiment/tags on a result and persist it if named."""
|
|
157
|
+
result.experiment_name = experiment_name
|
|
158
|
+
result.tags = tags or []
|
|
159
|
+
if experiment_name:
|
|
160
|
+
exp = Experiment(name=experiment_name)
|
|
161
|
+
exp.add(result)
|
|
162
|
+
ExperimentDB().save(exp)
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def evaluate(
|
|
167
|
+
dataset: Union[Scenario, Sequence[Sample], Callable[[], Iterable[Sample]], str],
|
|
168
|
+
model: Union[Model, Callable[[list[str]], list[str]], str],
|
|
169
|
+
scorers: Any = None,
|
|
170
|
+
*,
|
|
171
|
+
engine: str = "native",
|
|
172
|
+
adapter: Union[Adapter, str, None] = None,
|
|
173
|
+
annotators: Any = None,
|
|
174
|
+
extract_with: str | None = None,
|
|
175
|
+
config: Optional[RunConfig] = None,
|
|
176
|
+
verbose: bool = False,
|
|
177
|
+
experiment_name: str | None = None,
|
|
178
|
+
tags: list[str] | None = None,
|
|
179
|
+
**opts: Any,
|
|
180
|
+
) -> RunResult:
|
|
181
|
+
"""Evaluate a model on a dataset and return a :class:`RunResult`.
|
|
182
|
+
|
|
183
|
+
The single front door for both engines. ``engine="native"`` (default) runs
|
|
184
|
+
the owned spine (Adapter → Model → Metric); ``engine="lmeval"`` runs the
|
|
185
|
+
lm-evaluation-harness with its full task machinery (see :func:`run_lmeval`),
|
|
186
|
+
treating ``dataset`` as the task name(s).
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
dataset
|
|
191
|
+
A :class:`Scenario`, a list of :class:`Sample`, a callable that yields
|
|
192
|
+
samples, or a scenario name (native). With ``engine="lmeval"``, an
|
|
193
|
+
lm-eval task name, comma-separated string, or list of names.
|
|
194
|
+
model
|
|
195
|
+
A :class:`Model` instance, a ``list[str] -> list[str]`` callable, or a
|
|
196
|
+
string model spec (e.g. ``"hf:gpt2"``, ``"groq:llama-3.3-70b-versatile"``,
|
|
197
|
+
``"precomputed"`` for samples with ``actual_output`` already set). With
|
|
198
|
+
``engine="lmeval"`` a string spec is required (``"hf:gpt2"``, …).
|
|
199
|
+
scorers
|
|
200
|
+
``None`` (auto-select ``ExactMatch`` if golden samples exist),
|
|
201
|
+
a metric name string, a :class:`Metric`, a :class:`FunctionScorer`,
|
|
202
|
+
a ``(sample, output) -> float|Score`` callable, or a list of any of
|
|
203
|
+
the above. Ignored by ``engine="lmeval"`` (lm-eval owns its scoring).
|
|
204
|
+
adapter
|
|
205
|
+
An :class:`Adapter` instance, a registered adapter name string (e.g.
|
|
206
|
+
``"mcq"``, ``"chat"``, ``"rag"`` — see ``ADAPTERS.names()``), or
|
|
207
|
+
``None``. Defaults to :class:`GenerationAdapter`.
|
|
208
|
+
annotators
|
|
209
|
+
An :class:`Annotator` instance, a registered name string (e.g.
|
|
210
|
+
``"regex"`` for :class:`RegexAnnotator`), a list of either, or
|
|
211
|
+
``None``. Annotator output lands in each :class:`Score`/metric's
|
|
212
|
+
``context`` under the annotator's name; pass ``extract_with=`` to
|
|
213
|
+
actually score against one annotator's extracted value.
|
|
214
|
+
extract_with
|
|
215
|
+
Names an annotator (by its ``.name``) whose ``context["extracted"]``
|
|
216
|
+
value should be scored instead of the raw model output — e.g. a
|
|
217
|
+
``RegexAnnotator(r"FINAL ANSWER:\\s*(\\d+)", group=1)`` pulling
|
|
218
|
+
``"42"`` out of a longer chain-of-thought reply. ``None`` (default):
|
|
219
|
+
score the raw output, unchanged from before this existed.
|
|
220
|
+
config
|
|
221
|
+
A :class:`RunConfig` with evaluation knobs. Defaults to ``RunConfig()``.
|
|
222
|
+
**opts
|
|
223
|
+
Extra keyword args passed to the model constructor (native) or to
|
|
224
|
+
lm-eval's ``model_args`` (``engine="lmeval"``: ``base_url``, ``dtype``, …).
|
|
225
|
+
"""
|
|
226
|
+
if engine == "lmeval":
|
|
227
|
+
from .lmeval_engine import run_benchmark
|
|
228
|
+
|
|
229
|
+
result = run_benchmark(
|
|
230
|
+
dataset, model, config=config, run_name=experiment_name or "", **opts
|
|
231
|
+
)
|
|
232
|
+
return _record_experiment(result, experiment_name, tags)
|
|
233
|
+
if engine != "native":
|
|
234
|
+
raise ValueError(f"unknown engine {engine!r}; use 'native' or 'lmeval'")
|
|
235
|
+
|
|
236
|
+
scenario = _to_scenario(dataset)
|
|
237
|
+
resolved_model = AutoModel.resolve(model, **opts)
|
|
238
|
+
samples = list(scenario.samples())
|
|
239
|
+
metrics = _to_metrics(scorers, samples)
|
|
240
|
+
cfg = config or RunConfig()
|
|
241
|
+
|
|
242
|
+
spec = RunSpec(
|
|
243
|
+
scenario=scenario,
|
|
244
|
+
model=resolved_model,
|
|
245
|
+
adapter=_to_adapter(adapter, samples),
|
|
246
|
+
metrics=metrics,
|
|
247
|
+
annotators=_to_annotators(annotators),
|
|
248
|
+
extracted_by=extract_with,
|
|
249
|
+
config=cfg,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
result = Runner().run(spec, verbose=verbose)
|
|
253
|
+
return _record_experiment(result, experiment_name, tags)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def run_lmeval(
|
|
257
|
+
tasks: Union[str, list[str]],
|
|
258
|
+
model: str,
|
|
259
|
+
*,
|
|
260
|
+
num_fewshot: int | None = None,
|
|
261
|
+
limit: int | None = None,
|
|
262
|
+
config: Optional[RunConfig] = None,
|
|
263
|
+
experiment_name: str | None = None,
|
|
264
|
+
tags: list[str] | None = None,
|
|
265
|
+
**opts: Any,
|
|
266
|
+
) -> RunResult:
|
|
267
|
+
"""Run academic benchmarks via lm-evaluation-harness (its full task machinery).
|
|
268
|
+
|
|
269
|
+
The dedicated front door for the lm-eval engine -- unlike :func:`evaluate`
|
|
270
|
+
(which defaults to the native spine and only reaches lm-eval via
|
|
271
|
+
``engine="lmeval"``), this function is *always* lm-eval, unconditionally;
|
|
272
|
+
there is no native/lmeval switch here. lm-eval owns the task, prompt
|
|
273
|
+
template, filters, and scoring; this returns the same uniform
|
|
274
|
+
:class:`RunResult` (headline + per-sample answer browser) so
|
|
275
|
+
:func:`compare`, diffs, and experiment tracking work identically.
|
|
276
|
+
|
|
277
|
+
Requires ``pip install auditkit[lmeval]``. For gated models (Llama,
|
|
278
|
+
Gemma, ...) pass ``hf_token=`` (or ``token=``); it is forwarded to lm-eval's
|
|
279
|
+
``model_args`` and mirrored into ``HF_TOKEN`` for gated dataset access.
|
|
280
|
+
|
|
281
|
+
Model-backend options (``base_url``, ``dtype``, ``device``, ``hf_token``, …)
|
|
282
|
+
and lm-eval run knobs all pass through. Common run knobs: ``apply_chat_template``
|
|
283
|
+
(set ``True`` for instruct/chat/fine-tuned models), ``gen_kwargs`` (generation
|
|
284
|
+
params for generative tasks), ``system_instruction``, ``fewshot_as_multiturn``.
|
|
285
|
+
Anything else ``simple_evaluate`` accepts goes via ``lmeval_kwargs={...}``.
|
|
286
|
+
|
|
287
|
+
Examples
|
|
288
|
+
--------
|
|
289
|
+
::
|
|
290
|
+
|
|
291
|
+
ak.run_lmeval(["arc_challenge", "gsm8k"], model="hf:gpt2", num_fewshot=5)
|
|
292
|
+
ak.run_lmeval("mmlu", model="vllm:meta-llama/Llama-3.2-1B", limit=100)
|
|
293
|
+
ak.run_lmeval("gsm8k", model="api:my-model", base_url="https://…/v1/completions")
|
|
294
|
+
ak.run_lmeval("mmlu", model="hf:meta-llama/Llama-3.2-1B", hf_token="hf_…") # gated
|
|
295
|
+
ak.run_lmeval("ifeval", model="hf:my-finetune", apply_chat_template=True) # instruct
|
|
296
|
+
ak.run_lmeval("gsm8k", model="hf:gpt2", gen_kwargs="temperature=0,max_gen_toks=256")
|
|
297
|
+
"""
|
|
298
|
+
from .lmeval_engine import run_benchmark
|
|
299
|
+
|
|
300
|
+
cfg = config
|
|
301
|
+
if cfg is None:
|
|
302
|
+
cfg = RunConfig(num_fewshot=num_fewshot, limit=limit)
|
|
303
|
+
result = run_benchmark(
|
|
304
|
+
tasks, model, config=cfg, run_name=experiment_name or "", **opts
|
|
305
|
+
)
|
|
306
|
+
return _record_experiment(result, experiment_name, tags)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def generate(
|
|
310
|
+
dataset: Union[Scenario, Sequence[Sample], Callable[[], Iterable[Sample]], str],
|
|
311
|
+
model: Union[Model, Callable[[list[str]], list[str]], str],
|
|
312
|
+
*,
|
|
313
|
+
adapter: Union[Adapter, str, None] = None,
|
|
314
|
+
config: Optional[RunConfig] = None,
|
|
315
|
+
verbose: bool = False,
|
|
316
|
+
**opts: Any,
|
|
317
|
+
) -> list[Sample]:
|
|
318
|
+
"""Generate answers for a dataset and return samples with ``actual_output`` set.
|
|
319
|
+
|
|
320
|
+
Stage 1 of the generate→score flow. Runs ``model`` over the dataset's inputs
|
|
321
|
+
and returns copies of the samples with their answers filled in. Score them
|
|
322
|
+
afterwards — repeatedly, with different scorers, without re-generating::
|
|
323
|
+
|
|
324
|
+
answers = ak.generate(data, model="hf:my-pruned-model")
|
|
325
|
+
ak.evaluate(answers, model="precomputed", scorers=[judge])
|
|
326
|
+
ak.evaluate(answers, model="precomputed", scorers=[ak.Factuality(judge_model="openai:gpt-4o-mini")])
|
|
327
|
+
"""
|
|
328
|
+
from dataclasses import replace
|
|
329
|
+
|
|
330
|
+
scenario = _to_scenario(dataset)
|
|
331
|
+
resolved_model = AutoModel.resolve(model, **opts)
|
|
332
|
+
samples = list(scenario.samples())
|
|
333
|
+
spec = RunSpec(
|
|
334
|
+
scenario=ListScenario(samples),
|
|
335
|
+
model=resolved_model,
|
|
336
|
+
adapter=_to_adapter(adapter, samples),
|
|
337
|
+
metrics=[],
|
|
338
|
+
config=config or RunConfig(),
|
|
339
|
+
)
|
|
340
|
+
result = Runner().run(spec, verbose=verbose)
|
|
341
|
+
return [replace(s, actual_output=p.raw_output)
|
|
342
|
+
for s, p in zip(samples, result.predictions)]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def evaluate_many(
|
|
346
|
+
datasets: Union[dict[str, Any], Sequence[Any]],
|
|
347
|
+
model: Union[Model, Callable[[list[str]], list[str]], str],
|
|
348
|
+
*,
|
|
349
|
+
scorers: Any = None,
|
|
350
|
+
adapter: Union[Adapter, str, None] = None,
|
|
351
|
+
engine: str = "native",
|
|
352
|
+
config: Optional[RunConfig] = None,
|
|
353
|
+
verbose: bool = False,
|
|
354
|
+
experiment_name: str | None = None,
|
|
355
|
+
tags: list[str] | None = None,
|
|
356
|
+
on_error: str = "raise",
|
|
357
|
+
**opts: Any,
|
|
358
|
+
) -> dict[str, RunResult]:
|
|
359
|
+
"""Evaluate one model across **several datasets** in a single call.
|
|
360
|
+
|
|
361
|
+
:func:`evaluate` takes exactly one dataset, one adapter, and one scorer set —
|
|
362
|
+
because a run is a single reproducible unit (one ``RunSpec``, one fingerprint).
|
|
363
|
+
Different benchmarks legitimately need different scorers (SORRY-Bench rewards
|
|
364
|
+
refusal; OR-Bench penalises *over*-refusal; GSM8K wants ``exact_match``) and
|
|
365
|
+
sometimes a different adapter, and each deserves its **own** headline number
|
|
366
|
+
and its own cache entry. So this runs each dataset as its own ``evaluate()``
|
|
367
|
+
call, **sequentially** (not concurrently), and returns one
|
|
368
|
+
:class:`RunResult` per dataset — never a blended mean.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
datasets
|
|
373
|
+
Either a ``dict[name -> spec]`` (recommended — you name each benchmark),
|
|
374
|
+
or a ``list[spec]`` (names are derived: a dataset string, else the
|
|
375
|
+
scenario's ``.name``, else ``"dataset_{i}"``). Each ``spec`` is either:
|
|
376
|
+
|
|
377
|
+
* a bare dataset — anything :func:`evaluate` accepts as ``dataset``
|
|
378
|
+
(a ``list[Sample]``, a :class:`Scenario`, a callable, or a scenario
|
|
379
|
+
name) — scored with the shared ``scorers``/``adapter`` below; or
|
|
380
|
+
* a **tuple** ``(dataset,)``, ``(dataset, scorers)``, or
|
|
381
|
+
``(dataset, scorers, adapter)`` to override the scorers/adapter for
|
|
382
|
+
just that benchmark. (A tuple is unambiguous here — no dataset form is
|
|
383
|
+
itself a tuple.)
|
|
384
|
+
model
|
|
385
|
+
Resolved **once** up front (native engine) and reused for every dataset,
|
|
386
|
+
so a local ``hf:`` checkpoint is loaded a single time rather than
|
|
387
|
+
re-loaded per benchmark. ``**opts`` are the model constructor kwargs.
|
|
388
|
+
scorers, adapter
|
|
389
|
+
Shared defaults used for any dataset whose ``spec`` doesn't override them.
|
|
390
|
+
on_error
|
|
391
|
+
``"raise"`` (default) — the first failing dataset aborts the batch, same
|
|
392
|
+
as calling :func:`evaluate` directly. ``"skip"`` — log the failure and
|
|
393
|
+
continue, omitting that dataset from the returned dict (so one bad
|
|
394
|
+
dataset load doesn't lose the benchmarks that did run).
|
|
395
|
+
experiment_name
|
|
396
|
+
When set, each dataset's run is persisted under ``f"{experiment_name}:{name}"``
|
|
397
|
+
so the per-benchmark runs stay individually addressable in the ExperimentDB.
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
dict[str, RunResult]
|
|
402
|
+
One entry per dataset, keyed by name, in input order.
|
|
403
|
+
|
|
404
|
+
Examples
|
|
405
|
+
--------
|
|
406
|
+
Heterogeneous safety benches, each with its own judge, one model, one call::
|
|
407
|
+
|
|
408
|
+
results = ak.evaluate_many(
|
|
409
|
+
{
|
|
410
|
+
"sorry_bench": (sorry_samples, [refusal_judge]), # refusal = good
|
|
411
|
+
"or_bench": (or_samples, [over_refusal_judge]), # over-refusal = bad
|
|
412
|
+
"gsm8k_local": (gsm_samples, ["exact_match"]),
|
|
413
|
+
},
|
|
414
|
+
model="groq:llama-3.3-70b-versatile",
|
|
415
|
+
)
|
|
416
|
+
for name, r in results.items():
|
|
417
|
+
print(name, r.headline)
|
|
418
|
+
|
|
419
|
+
A list of same-shaped datasets sharing one scorer::
|
|
420
|
+
|
|
421
|
+
results = ak.evaluate_many([ds_a, ds_b, ds_c], model="hf:gpt2",
|
|
422
|
+
scorers=["exact_match"], device="cpu")
|
|
423
|
+
"""
|
|
424
|
+
if on_error not in ("raise", "skip"):
|
|
425
|
+
raise ValueError(f"on_error must be 'raise' or 'skip', got {on_error!r}")
|
|
426
|
+
|
|
427
|
+
# Resolve the model ONCE (native) so a heavy local checkpoint isn't reloaded
|
|
428
|
+
# per dataset. lm-eval builds its own backend from a string spec, so leave
|
|
429
|
+
# the spec (and opts) untouched on that path.
|
|
430
|
+
if engine == "native":
|
|
431
|
+
model = AutoModel.resolve(model, **opts)
|
|
432
|
+
opts = {}
|
|
433
|
+
|
|
434
|
+
items = datasets.items() if isinstance(datasets, dict) else enumerate(datasets)
|
|
435
|
+
results: dict[str, RunResult] = {}
|
|
436
|
+
for key, spec in items:
|
|
437
|
+
if isinstance(spec, tuple):
|
|
438
|
+
ds = spec[0]
|
|
439
|
+
ds_scorers = spec[1] if len(spec) > 1 else scorers
|
|
440
|
+
ds_adapter = spec[2] if len(spec) > 2 else adapter
|
|
441
|
+
else:
|
|
442
|
+
ds, ds_scorers, ds_adapter = spec, scorers, adapter
|
|
443
|
+
|
|
444
|
+
if isinstance(key, str):
|
|
445
|
+
name = key
|
|
446
|
+
elif isinstance(ds, str):
|
|
447
|
+
name = ds
|
|
448
|
+
else:
|
|
449
|
+
name = getattr(ds, "name", None) or f"dataset_{key}"
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
results[name] = evaluate(
|
|
453
|
+
ds, model=model, scorers=ds_scorers, adapter=ds_adapter,
|
|
454
|
+
engine=engine, config=config, verbose=verbose,
|
|
455
|
+
experiment_name=f"{experiment_name}:{name}" if experiment_name else None,
|
|
456
|
+
tags=tags, **opts,
|
|
457
|
+
)
|
|
458
|
+
except Exception as e: # noqa: BLE001 — isolation is opt-in via on_error
|
|
459
|
+
if on_error == "raise":
|
|
460
|
+
raise
|
|
461
|
+
logger.error("evaluate_many: dataset %r failed, skipping: %s", name, e)
|
|
462
|
+
return results
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def compare(baseline_or_results, candidate=None, *, metric: str | None = None, **kw):
|
|
466
|
+
"""Two shapes:
|
|
467
|
+
|
|
468
|
+
- ``compare(baseline_run, candidate_run)`` — two :class:`RunResult`s → a
|
|
469
|
+
:class:`~auditkit.comparison.RunComparison` (per-task deltas, pass/warn/fail,
|
|
470
|
+
newly-wrong browser, retention, significance). This is the base-vs-pruned view.
|
|
471
|
+
- ``compare([run1, run2, ...], metric=...)`` — a list of runs → a leaderboard
|
|
472
|
+
(one dict per run, sorted by ``metric`` or the first headline metric).
|
|
473
|
+
|
|
474
|
+
``kw`` (``pass_threshold``/``warn_threshold``) is forwarded to ``RunComparison``.
|
|
475
|
+
"""
|
|
476
|
+
if candidate is not None:
|
|
477
|
+
from .comparison import RunComparison
|
|
478
|
+
return RunComparison(baseline_or_results, candidate, **kw)
|
|
479
|
+
|
|
480
|
+
results = baseline_or_results
|
|
481
|
+
if not results:
|
|
482
|
+
return []
|
|
483
|
+
first = metric or next(iter(results[0].headline.keys()), None)
|
|
484
|
+
rows = []
|
|
485
|
+
for r in results:
|
|
486
|
+
row = {"run_id": r.run_id, "fingerprint": r.fingerprint}
|
|
487
|
+
row.update(r.headline)
|
|
488
|
+
rows.append(row)
|
|
489
|
+
if first:
|
|
490
|
+
rows.sort(key=lambda x: x.get(first, 0), reverse=True)
|
|
491
|
+
return rows
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
__all__ = [
|
|
495
|
+
"run_lmeval",
|
|
496
|
+
"compare",
|
|
497
|
+
"evaluate",
|
|
498
|
+
"evaluate_many",
|
|
499
|
+
"generate",
|
|
500
|
+
"load_csv",
|
|
501
|
+
"scorer",
|
|
502
|
+
]
|
|
Binary file
|
auditkit/cache.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Simple disk cache for evaluation results keyed by fingerprint."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .report import RunResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DiskCache:
|
|
12
|
+
"""Persist RunResult objects keyed by run fingerprint.
|
|
13
|
+
|
|
14
|
+
Defaults to ``~/.cache/auditkit/runs/``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, cache_dir: str | None = None) -> None:
|
|
18
|
+
self._dir = Path(cache_dir or os.path.join(
|
|
19
|
+
os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")),
|
|
20
|
+
"auditkit", "runs",
|
|
21
|
+
))
|
|
22
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def _path(self, fingerprint: str) -> Path:
|
|
25
|
+
return self._dir / f"{fingerprint}.json"
|
|
26
|
+
|
|
27
|
+
def get(self, fingerprint: str) -> RunResult | None:
|
|
28
|
+
path = self._path(fingerprint)
|
|
29
|
+
if path.exists():
|
|
30
|
+
with open(path, "r") as fh:
|
|
31
|
+
return RunResult.from_dict(json.load(fh))
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def set(self, fingerprint: str, result: RunResult) -> None:
|
|
35
|
+
path = self._path(fingerprint)
|
|
36
|
+
tmp = path.with_suffix(".tmp")
|
|
37
|
+
with open(tmp, "w") as fh:
|
|
38
|
+
json.dump(result.to_dict(), fh, indent=2, default=str)
|
|
39
|
+
tmp.rename(path)
|
|
40
|
+
|
|
41
|
+
def clear(self) -> None:
|
|
42
|
+
for p in self._dir.iterdir():
|
|
43
|
+
if p.suffix == ".json":
|
|
44
|
+
p.unlink()
|
|
45
|
+
|
|
46
|
+
def __contains__(self, fingerprint: str) -> bool:
|
|
47
|
+
return self._path(fingerprint).exists()
|