sora 370953729b Fix perf stats (wrong import path), per-repeat checkpoints, README
- perf_stats aggregator lives in eval/, not model/: the import failed
  silently and EVERY perf column was empty (not just ttft). Now warns
  on stderr instead of swallowing.
- repeats > 1 get their own checkpoint key (:rep2, :rep3, ...): repeat 2
  previously restored repeat 1's predictions and finished instantly with
  identical scores. rep1 keeps the legacy key (existing checkpoints still
  resume).
- repeats summary: report the MEAN score and aggregate time/tokens over
  ALL runs (was: last run only).
- README: six-benchmark command as the primary example.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-11 13:38:04 +00:00

156 lines
5.5 KiB
Python

"""Generation-parameter profiles: named, per-bench gen_kwargs presets.
Problem being solved: ``DatasetSpec.gen_config`` is baked into the dataset
plugin at authoring time (the Qwen3 era defaults), but different models /
protocols need different parameters (dp4 wants t0/32768 everywhere, Qwen3
wanted mixed values). Without this layer every runner script re-declares
its own ``GEN = {...}`` dict and hand-merges overrides -- we did that for
days across seven stage scripts.
Resolution order (later wins):
1. DatasetSpec.gen_config (plugin's built-in default)
2. profile['default'] (protocol-wide baseline)
3. profile['<bench>'] (per-bench override)
4. explicit run_eval(gen_kwargs=...) (one-off)
Usage:
# register
@register_gen_profile('dp4-nothink')
def dp4():
return {'default': {'temperature': 0.0, 'max_tokens': 32768},
'simple_qa': {'max_tokens': 1024}}
# consume
run_eval(ds, spec, gen_profile='dp4-nothink')
evalharness eval run hle --model ... --profile dp4-nothink
"""
from pathlib import Path
from typing import Any, Callable, Dict, Optional
PROFILES: Dict[str, Callable[[], Dict[str, Dict[str, Any]]]] = {}
def register_gen_profile(name: str):
def decorator(fn):
PROFILES[name] = fn
return fn
return decorator
def get_profile(name: str) -> Optional[Dict[str, Dict[str, Any]]]:
"""Resolve a profile by name.
Lookup order:
1. @register_gen_profile registry (code-defined, built-ins live here)
2. YAML file, selected by (in order):
a. $EVALHARNESS_GEN_PROFILES env var (explicit path)
b. ./gen_profiles.yaml (next to the invocation / repo root)
c. ~/.config/evalharness/gen_profiles.yaml
A YAML file may define MANY profiles; the file's profiles are also merged
into list_profiles() so CLI completion/Errors can see them.
"""
fn = PROFILES.get(name)
if fn is not None:
return fn()
loaded = _load_yaml_profiles()
if name in loaded:
return loaded[name]
return None
_YAML_CACHE: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None
def _candidate_yaml_paths():
import os
paths = []
env = os.environ.get('EVALHARNESS_GEN_PROFILES')
if env:
paths.append(Path(env))
paths.append(Path('gen_profiles.yaml'))
paths.append(Path(__file__).parent.parent / 'config' / 'gen_profiles.yaml')
paths.append(Path.home() / '.config' / 'evalharness' / 'gen_profiles.yaml')
return paths
def _load_yaml_profiles() -> Dict[str, Dict[str, Dict[str, Any]]]:
"""Read every profile from the first YAML that exists; empty if none."""
global _YAML_CACHE
if _YAML_CACHE is not None:
return _YAML_CACHE
try:
import yaml
except ImportError:
_YAML_CACHE = {}
return _YAML_CACHE
for path in _candidate_yaml_paths():
try:
if path and path.exists():
data = yaml.safe_load(path.read_text(encoding='utf-8')) or {}
# accept both flat (file IS one profile: has 'default')
# and namespaced (top-level keys are profile names)
if 'default' in data or 'bench' in {k.split('.')[0] for k in data
if isinstance(data.get(k), dict)}:
data = {'default': data} if 'default' in data else data
_YAML_CACHE = {k: v for k, v in data.items() if isinstance(v, dict)}
return _YAML_CACHE
except Exception:
continue
_YAML_CACHE = {}
return _YAML_CACHE
def list_profiles():
return sorted(set(PROFILES) | set(_load_yaml_profiles()))
def merge_gen_kwargs(bench: str, spec, gen_kwargs: Optional[Dict[str, Any]],
profile_name: str = '') -> Dict[str, Any]:
"""Layered merge for one bench (later layers win)."""
out: Dict[str, Any] = {}
out.update(getattr(spec, 'gen_config', None) or {})
if profile_name:
prof = get_profile(profile_name)
if prof is None:
raise KeyError(f'unknown gen profile {profile_name!r}; '
f'available: {", ".join(list_profiles())}')
out.update(prof.get('default') or {})
out.update(prof.get(bench) or {})
out.update(gen_kwargs or {})
return out
# ------------------------------ built-ins ------------------------------
@register_gen_profile('dp4-nothink')
def _dp4_nothink():
"""DeepSeek-V4-Flash nothinking protocol (es DP4-flash-int8-nothinking):
t0 / 32768 / top_p 1.0 everywhere; judged benches can be trimmed."""
return {'default': {'temperature': 0.0, 'max_tokens': 32768, 'top_p': 1.0}}
@register_gen_profile('qwen3-es-parity')
def _qwen3_parity():
"""Qwen3-8B evalscope-parity protocol (the values used for the 28-bench
alignment): CoT benches get 32k room, short-answer benches stay small."""
return {
'default': {'temperature': 0.0, 'max_tokens': 32768},
'simple_qa': {'max_tokens': 1024},
'hle': {'max_tokens': 8192},
'gpqa_diamond': {'temperature': 1.0, 'max_tokens': 8192},
'aime24': {'temperature': 1.0},
'aime25': {'temperature': 1.0},
'aime26': {'temperature': 1.0},
'hmmt26': {'temperature': 1.0},
'imo_answerbench': {'temperature': 1.0},
}
@register_gen_profile('t1-short')
def _t1_short():
"""temp=1 sampling for small repeated benches (variance measurement)."""
return {'default': {'temperature': 1.0, 'max_tokens': 32768, 'top_p': 1.0}}