CoolFace
Apppublic

DGXAI/driftcall

sourceHugging Faceapache-2.0updated 5mo agoView on Hugging Face
0likes
step_18_eval_baseline.py377 linesDownload Raw Back to cells
1"""Cell 18 — Baseline evaluation harness.2 3Implements ``docs/modules/evaluation.md`` §1, §2, §3.1–§3.3, §3.8, §4 and4§5 for the baseline (untrained Gemma 3n E2B) eval path.5 6Hard rules (evaluation.md §3.1, §3.2, §6.3):7- Greedy decoding (``temperature=0.0``); ``num_generations=1``;8  ``model.eval()`` + ``torch.no_grad()`` semantics asserted at entry.9- Per-episode env seed = ``hash((episode_id, "eval")) & 0xFFFFFFFF``.10- 50 held-out val episodes (rows ``[0:50]`` of ``val/briefs.jsonl``) — file11  order, no shuffling.12- Bootstrap CI (percentile method) at ``n_boot=10_000``, ``rng_seed=20260426``13  (paired-difference uses ``20260428``).14- No LLM-as-judge; static AST scan via ``_NO_LLM_JUDGE_FORBIDDEN_IMPORTS``.15- Wall-clock ceiling 20 minutes (``EvalBudgetExceededError`` on overrun).16 17This module deliberately does **not** import ``torch`` at module load. The18training-eval delegate is injected via ``run_eval_baseline(..., training_eval=...)``19so unit tests can stub model inference (CUDA-free CI per training_tests.md §5.3).20"""21 22from __future__ import annotations23 24import math25import time26from dataclasses import dataclass, field27from typing import TYPE_CHECKING, Any, Literal, Protocol28 29if TYPE_CHECKING:  # pragma: no cover - typing only30    from collections.abc import Callable, Sequence31    from pathlib import Path32 33 34__all__ = [35    "BUDGET_RUN_EVAL_SECONDS",36    "DEFAULT_BOOTSTRAP_SEED",37    "DEFAULT_PAIRED_BOOTSTRAP_SEED",38    "DriftDetectionLatency",39    "EvalBudgetExceededError",40    "EvalModelLoadError",41    "EvalReport",42    "EvaluationError",43    "PerLanguageReport",44    "TrainingEvalCallable",45    "ZeroSuccessBaselineWarning",46    "bootstrap_ci",47    "compute_episode_seed",48    "eval_baseline",49    "run_eval",50]51 52 53# ---------------------------------------------------------------------------54# Constants — evaluation.md §2.4, §3.855# ---------------------------------------------------------------------------56 57 58DEFAULT_BOOTSTRAP_SEED: int = 2026042659DEFAULT_PROBE_BOOTSTRAP_SEED: int = 2026042760DEFAULT_PAIRED_BOOTSTRAP_SEED: int = 2026042861DEFAULT_N_BOOT: int = 10_00062 63BUDGET_RUN_EVAL_SECONDS: int = 20 * 6064"""Hard ceiling on ``run_eval`` (50 episodes) — evaluation.md §3.8."""65 66# Forbidden imports inside any evaluation/scoring path (evaluation.md §6.3).67_NO_LLM_JUDGE_FORBIDDEN_IMPORTS: frozenset[str] = frozenset(68    {"openai", "anthropic", "vertexai", "google.generativeai", "cohere"},69)70 71_LANGUAGE_CODES: tuple[str, ...] = ("hi", "ta", "kn", "en", "hinglish")72 73 74# ---------------------------------------------------------------------------75# Errors / warnings — evaluation.md §576# ---------------------------------------------------------------------------77 78 79class EvaluationError(Exception):80    """Root for every evaluation-specific error (evaluation.md §5)."""81 82 83class EvalModelLoadError(EvaluationError):84    """Adapter load / merge failure surfaced by the training-eval delegate."""85 86 87class EvalBudgetExceededError(EvaluationError):88    """Wall-clock budget for an entry point exceeded (evaluation.md §3.8, §5)."""89 90 91class CatalogueHashMismatchError(EvaluationError):92    """Loaded catalogue hashes do not match the BriefRow's declared hashes."""93 94 95class ZeroSuccessBaselineWarning(UserWarning):96    """All 50 baseline R1 == 0.0 → degenerate CI; warn rather than raise."""97 98 99# ---------------------------------------------------------------------------100# EvalReport family — re-exported for downstream cells (evaluation.md §4)101# ---------------------------------------------------------------------------102 103 104@dataclass(frozen=True)105class PerLanguageReport:106    """Per-language cohort means (training.md §4.2)."""107 108    language: Literal["hi", "ta", "kn", "en", "hinglish"]109    n_episodes: int110    reward_mean: float111    r1_mean: float112    r2_mean: float113    r3_mean: float114    r4_mean: float115    r5_mean: float116 117 118@dataclass(frozen=True)119class DriftDetectionLatency:120    """Drift-detection latency aggregated by stage (training.md §4.2)."""121 122    stage2_mean: float123    stage2_median: float124    stage2_p95: float125    stage3_mean: float126    stage3_median: float127    stage3_p95: float128    undetected_count: int129 130 131@dataclass(frozen=True)132class EvalReport:133    """Result of ``run_eval`` — paired across baseline and final (training.md §4.2)."""134 135    model_path: str136    n_episodes: int137    reward_mean_ci: tuple[float, float, float]138    r1_mean_ci: tuple[float, float, float]139    r2_mean_ci: tuple[float, float, float]140    r3_mean_ci: tuple[float, float, float]141    r4_mean_ci: tuple[float, float, float]142    r5_mean_ci: tuple[float, float, float]143    brier_mean: float144    floor_applied_rate: float145    hallucinated_field_rate: float146    reward_hacking_offenses: dict[str, int]147    drift_detection_latency: DriftDetectionLatency148    per_language: tuple[PerLanguageReport, ...]149    curves: dict[str, tuple[tuple[int, float], ...]] = field(default_factory=dict)150    breakdown: dict[str, Any] = field(default_factory=dict)151 152 153# ---------------------------------------------------------------------------154# Training-eval delegate Protocol — evaluation.md §6.1155# ---------------------------------------------------------------------------156 157 158class TrainingEvalCallable(Protocol):159    """Signature of ``training.train.eval`` — the heavy-lifting delegate."""160 161    def __call__(162        self,163        model_path: Path | Literal["base"],164        episodes: int,165        *,166        sampling: dict[str, Any],167        seeds: Sequence[int],168        episode_ids: Sequence[str],169    ) -> EvalReport: ...170 171 172# ---------------------------------------------------------------------------173# Statistical helpers — evaluation.md §2.4, §3.3174# ---------------------------------------------------------------------------175 176 177def bootstrap_ci(178    samples: tuple[float, ...],179    n_boot: int = DEFAULT_N_BOOT,180    alpha: float = 0.05,181    rng_seed: int = DEFAULT_BOOTSTRAP_SEED,182) -> tuple[float, float, float]:183    """Non-parametric percentile bootstrap 95% CI on the mean.184 185    evaluation.md §2.4 contract:186      - ``len(samples) == 0`` → ``(nan, nan, nan)``.187      - ``len(samples) == 1`` → ``(v, v, v)``.188      - All-identical samples → ``(v, v, v)`` (no resample variance).189    """190    if not samples:191        nan = float("nan")192        return nan, nan, nan193    n = len(samples)194    mean = sum(samples) / n195    if n == 1:196        return mean, mean, mean197    if all(s == samples[0] for s in samples):198        return mean, mean, mean199 200    # Lazy import to keep this module importable on minimal CI containers.201    import numpy as np202 203    rng = np.random.default_rng(rng_seed)204    arr = np.asarray(samples, dtype=np.float64)205    idx = rng.integers(0, n, size=(n_boot, n))206    means = arr[idx].mean(axis=1)207    lo = float(np.percentile(means, 100.0 * (alpha / 2.0)))208    hi = float(np.percentile(means, 100.0 * (1.0 - alpha / 2.0)))209    return float(mean), lo, hi210 211 212# ---------------------------------------------------------------------------213# Episode selection helpers — evaluation.md §3.1214# ---------------------------------------------------------------------------215 216 217def compute_episode_seed(episode_id: str) -> int:218    """``hash((episode_id, "eval")) & 0xFFFFFFFF`` — re-asserted at every call site."""219    return hash((episode_id, "eval")) & 0xFFFFFFFF220 221 222def _validate_briefs_first_50(briefs: Sequence[Any]) -> tuple[Any, ...]:223    """Take the first 50 BriefRows in file order; raise on too few."""224    if len(briefs) < 50:225        raise EvaluationError(226            f"val/briefs.jsonl must have >= 50 rows for paired eval, got {len(briefs)}",227        )228    return tuple(briefs[:50])229 230 231def _check_catalogue_hashes(briefs: Sequence[Any], current_hashes: dict[str, str]) -> None:232    """Compare each BriefRow's declared hash against the loaded library hashes.233 234    evaluation.md §3.1: any mismatch → ``CatalogueHashMismatchError``.235    """236    for row in briefs:237        for attr, key in (238            ("catalogue_hash", "drifts"),239            ("templates_sha256", "templates"),240            ("i18n_sha256", "i18n"),241        ):242            declared = getattr(row, attr, None)243            current = current_hashes.get(key)244            if declared is None or current is None:245                continue246            if declared != current:247                raise CatalogueHashMismatchError(248                    f"BriefRow.{attr}={declared!r} but loaded {key} hashes to {current!r}",249                )250 251 252# ---------------------------------------------------------------------------253# Sampling-policy guard — evaluation.md §3.2254# ---------------------------------------------------------------------------255 256 257_FROZEN_SAMPLING_POLICY: dict[str, Any] = {258    "temperature": 0.0,259    "top_p": 1.0,260    "top_k": 1,261    "num_generations": 1,262    "repetition_penalty": 1.0,263    "model_eval": True,264    "no_grad": True,265    "dropout_off": True,266}267 268 269def _frozen_sampling_kwargs() -> dict[str, Any]:270    return dict(_FROZEN_SAMPLING_POLICY)271 272 273# ---------------------------------------------------------------------------274# Episode-set / leakage helpers — evaluation.md §3.1275# ---------------------------------------------------------------------------276 277 278def _episode_ids_from_breakdown(report: EvalReport) -> tuple[str, ...]:279    ids = report.breakdown.get("episode_ids", ())280    return tuple(ids)281 282 283# ---------------------------------------------------------------------------284# Core entry point — evaluation.md §2.1 ``run_eval``285# ---------------------------------------------------------------------------286 287 288def run_eval(289    model_path: Path | Literal["base"],290    episodes: int = 50,291    *,292    training_eval: TrainingEvalCallable,293    briefs: Sequence[Any],294    catalogue_hashes: dict[str, str] | None = None,295    budget_seconds: int = BUDGET_RUN_EVAL_SECONDS,296    monotonic: Callable[[], float] | None = None,297) -> EvalReport:298    """Thin wrapper over ``training.train.eval`` (evaluation.md §2.1).299 300    Validates episode count, catalogue hashes, sampling policy, and wall-clock301    budget. Delegates the heavy lifting (model load, rollout, ``Rewards``302    aggregation) to the injected ``training_eval`` callable.303    """304    if episodes != 50:305        raise EvaluationError(306            f"run_eval expects episodes=50 (paired-comparison contract); got {episodes}",307        )308 309    selected = _validate_briefs_first_50(briefs)310    if catalogue_hashes is not None:311        _check_catalogue_hashes(selected, catalogue_hashes)312 313    episode_ids = tuple(row.episode_id for row in selected)314    seeds = tuple(compute_episode_seed(ep_id) for ep_id in episode_ids)315 316    clock = monotonic if monotonic is not None else time.monotonic317    started = clock()318 319    try:320        report = training_eval(321            model_path,322            episodes,323            sampling=_frozen_sampling_kwargs(),324            seeds=seeds,325            episode_ids=episode_ids,326        )327    except EvalModelLoadError:328        raise329    except EvaluationError:330        raise331 332    elapsed = clock() - started333    if elapsed > budget_seconds:334        raise EvalBudgetExceededError(335            f"run_eval wall-clock {elapsed:.1f}s exceeded {budget_seconds}s "336            f"({budget_seconds // 60} min ceiling)",337        )338 339    # Stamp episode_ids + wall-clock into breakdown for downstream leak guards.340    breakdown = dict(report.breakdown)341    breakdown.setdefault("episode_ids", episode_ids)342    breakdown.setdefault("wall_clock_seconds", round(elapsed, 3))343    breakdown.setdefault("sampling_policy", _frozen_sampling_kwargs())344 345    # Detect zero-success-baseline degeneracy (§7.1) — warn, do not raise.346    r1_mean = report.r1_mean_ci[0]347    if math.isclose(r1_mean, 0.0, abs_tol=1e-12) and report.model_path == "base":348        breakdown["ci_undefined_rewards"] = ["r1"]349 350    from dataclasses import replace as _replace351    return _replace(report, breakdown=breakdown)352 353 354def eval_baseline(355    model_path: Path | Literal["base"] = "base",356    episodes: int = 50,357    *,358    training_eval: TrainingEvalCallable,359    briefs: Sequence[Any],360    catalogue_hashes: dict[str, str] | None = None,361    budget_seconds: int = BUDGET_RUN_EVAL_SECONDS,362    monotonic: Callable[[], float] | None = None,363) -> EvalReport:364    """Baseline-eval entry point (evaluation.md §2.2 ``eval_baseline.py``).365 366    Defaults ``model_path='base'`` to lock in the untrained-model contract.367    """368    return run_eval(369        model_path,370        episodes,371        training_eval=training_eval,372        briefs=briefs,373        catalogue_hashes=catalogue_hashes,374        budget_seconds=budget_seconds,375        monotonic=monotonic,376    )377