Visitar URL original
fix(hle): compute cerr at attempt level for epochs > 1 by feiiiiii5 · Pull Request #2132 · UKGovernmentBEIS/inspect_evals · GitHub
Skip to content

fix(hle): compute cerr at attempt level for epochs > 1 - #2132

Merged
eleweek merged 11 commits into
UKGovernmentBEIS:mainfrom
feiiiiii5:fix/hle-attempt-level-cerr
Aug 19, 2026
Merged

eleweek merged 11 commits into
UKGovernmentBEIS:mainfrom
feiiiiii5:fix/hle-attempt-level-cerr

Conversation

@feiiiiii5

Copy link
Copy Markdown
Contributor

Description

At --epochs > 1, Inspect reduces per-epoch scores before metrics run. The built-in mean reducer therefore made cerr measure the calibration of epoch-averaged attempts: a confidently-wrong attempt plus an unconfidently-right attempt averaged into an apparently well-calibrated sample (verified scenario: 100 samples x 2 epochs, every attempt maximally miscalibrated → reduced cerr 0.0, attempt-level cerr 1.0). Unscored epochs also vanished from the reduction.

This PR pins an attempt-preserving reducer on the HLE task:

  • The reducer mean-reduces the headline {"score", "confidence"} exactly like the built-in mean (so accuracy/stderr semantics are unchanged) and stashes the per-epoch attempts in the reduced score's metadata (None for unscored epochs).
  • cerr expands stashed attempts and computes calibration at attempt level, falling back to the reduced value when no metadata is present (explicit --epochs-reducer overrides degrade gracefully).

The default single-epoch run is byte-identical; only --epochs > 1 results change.

Checklist

  • Does this change affect existing eval(s)? If yes:
    • Checked against the evaluation checklist — scoring-validity fix for multi-epoch runs; single-epoch (the official methodology) unchanged.
    • Have the affected task version(s) been incremented? Yes — HLE 3-B → 4-B (multi-epoch calibration results change).
    • Have the affected task changelog(s) been updated? Yes — HLE README ## Changelog entry [4-B] and changelog.d/20260812_000000_hle_attempt_preserving_reducer.md.
  • Is this change consequential to users? If yes:
    • Has uv run scriv create been run and the changelog fragment committed? Yes.
  • Does this change affect how future contributors write or submit evaluations? No.

Test

  • tests/hle/test_metrics.py: reducer headline/metadata parity with mean, unscored-epoch recording (None in the stash), attempt-level cerr (1.0 for the maximally-miscalibrated scenario vs 0.0 for the averaged headline), and the no-metadata fallback.
  • Full HLE suite: 18 passed (2 slow skipped); tools/check_changelog.py 122/122; ruff 0.15.11 + mypy clean.

AI Disclosure

AI-assisted implementation and regression tests; the semantic decision (attempt-level calibration with reduced-value fallback, stash-based unscored recording) and final diff were human-reviewed before submission.

Closes #2096

The built-in mean reducer averages per-epoch scores before metrics run,
so at --epochs > 1 cerr measured the calibration of epoch-averaged
attempts (a confidently-wrong attempt plus an unconfidently-right
attempt averaged into an apparently well-calibrated sample), and
unscored epochs vanished from the reduction.

Pin an attempt-preserving reducer on the HLE task: it mean-reduces the
headline {score, confidence} exactly like the built-in mean (accuracy
and stderr semantics unchanged) while stashing the per-epoch attempts
in the reduced score metadata (None for unscored epochs). cerr now
expands stashed attempts and computes calibration at attempt level,
falling back to the reduced value when no metadata is present.

Unit tests cover reducer headline/metadata, unscored-epoch recording,
attempt-level cerr (1.0 for maximally miscalibrated attempts vs 0.0 for
the averaged headline), and the no-metadata fallback. Bump HLE 3-B ->
4-B since epochs > 1 results change; the default single-epoch run is
unaffected.

Signed-off-by: feiiiiii5 <204683769+feiiiiii5@users.noreply.github.com>
Narrow the reduced Score value before indexing, call the metric through
MetricProtocol (cast from the Metric union), and point the cerr imports
in test_epoch_compatibility/test_hle at the new hle.metrics module.

Signed-off-by: feiiiiii5 <204683769+feiiiiii5@users.noreply.github.com>
@github-actions

This comment has been minimized.

- Rename the reducer's value_to_float parameter to to_float so it no
  longer shadows the module-level import (matches cerr's pattern).
- Add test_cerr_skips_none_attempts: with 100 samples each carrying one
  scored attempt and one None attempt, cerr reflects only the scored
  attempts (1.0), pinning the unscored-accounting behavior.

Signed-off-by: feiiiiii5 <204683769+feiiiiii5@users.noreply.github.com>
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Thanks — both points addressed in 17c26508:

  1. attempt_preserving_mean's parameter renamed to to_float (no longer shadows the module-level value_to_float import, matching cerr's pattern).
  2. Added test_cerr_skips_none_attempts: 100 samples each with one scored attempt and one None attempt → cerr reflects only the scored attempts (1.0), pinning the unscored-accounting behavior.

HLE suite 19 passed, ruff/mypy clean.

Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com>
@github-actions

This comment has been minimized.

Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com>
@github-actions

This comment has been minimized.

eleweek and others added 4 commits August 19, 2026 11:54
Replaces the reduce.__name__ assignment (and its type: ignore) with the
registry decorator. Beyond removing the workaround, registration is what
makes the reducer survive eval-retry: inspect reconstructs a retried run's
Epochs from the logged reducer name via a registry lookup, and the logged
name for an unregistered closure cannot resolve -- so retrying an
interrupted epochs>1 run would fail or silently drop attempt preservation.
Verified round-trip: the reducer logs as "inspect_evals/attempt_preserving_mean"
and create_reducers() resolves that name back to the reducer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reducer reimplemented dict-mean and NaN-skipping internally and returned
a bare Score, losing answer/explanation and clobbering base metadata.
Delegating reduction to mean_score() inherits inspect's own conventions:
the unanimity carry for answer/explanation, metadata pass-through (attempts
are now merged into it rather than replacing it), per-key NaN skipping, and
the root-NaN sentinel for all-unscored samples -- which the hand-rolled
version returned as a {"score": nan, "confidence": nan} dict that slips past
the metrics filter. Attempt recording is unchanged, so cerr's input is
identical; the reducer body is now just the stash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fix as wired never ran: Task's deprecated epochs_reducer argument is
converted to the epochs parameter only `if isinstance(epochs, int)`
(task.py), and hle() passes no epochs count -- so the reducer was silently
dropped, the default mean reduced (producing an identical headline, which
hid the failure), no attempts were stashed, and cerr fell back to the
epoch-averaged pairs it was meant to replace. Only the deprecation warning
hinted at it.

Pinning epochs=Epochs(1, attempt_preserving_mean()) takes the non-deprecated
path; a bare eval-level epochs count still overrides the count while keeping
this reducer.

The new end-to-end test runs the real hle task at epochs=2 with a mocked
dataset, solver and grader role, and asserts via a spy on the pair
extraction that cerr consumed two attempt pairs rather than one reduced
pair -- this is the test that caught the wiring failure, since headline
values and even the cerr number are identical on both paths for small runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eleweek and others added 2 commits August 19, 2026 14:11
Left behind when the e2e assertions moved from reduction metadata (which
serializes without it) to the pair-extraction spy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eleweek
eleweek merged commit d49e7da into UKGovernmentBEIS:main Aug 19, 2026
26 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review

Summary

Reviewed the HLE attempt-level cerr change (metrics.py, judge.py, hle.py, tests, README/changelog, version bump). The core mechanism is sound and the version bump, package changelog fragment, and README changelog entry are all present. Four issues below, the first of which is a claim in the changelog that the code does not support.

Issues Found

Scoring and Metrics — documented behaviour not implemented

Issue: Both the package changelog and the reducer docstring claim this PR fixes unscored accounting, but nothing consumes the None entries, and the behaviour for unscored epochs is byte-for-byte identical to main.

Trace a sample with epochs [scored(CORRECT, 90), unscored]:

  • Before: mean skips the root-NaN epoch → reduced {"score": 1.0, "confidence": 90} → cerr sees one pair (1.0, 0.9).
  • After: attempts = [{"score": 1.0, "confidence": 90.0}, None] → the isinstance(attempt, dict) guard in _score_confidence_pairs skips the None → cerr sees one pair (1.0, 0.9).

So changelog.d/20260812_000000_hle_attempt_preserving_reducer.md:3 ("...fixing distorted calibration and unscored accounting") and metrics.py:125-126 ("unscored epochs are recorded as None so downstream metrics can count them") both describe capability that does not exist. test_cerr_skips_none_attempts pins the opposite — that they are not counted. The None stash is currently write-only machinery.

Location: changelog.d/20260812_000000_hle_attempt_preserving_reducer.md:3, src/inspect_evals/hle/metrics.py:125-126

Recommendation: Pick one: either drop the "unscored accounting" claim from the changelog and reword the docstring to say the None placeholders preserve positional alignment (and are skipped by cerr), or actually surface an unscored-attempt count as a metric. Note the calibration fix alone already justifies the 4-B bump, so no version consequence either way.

Scoring and Metrics — cerr reports perfect calibration on no data

Issue: metrics.py:77-78 returns 0.0 when pairs is empty. 0.0 is the "perfectly calibrated" end of the scale, so an eval where every sample was unscored (all grader calls failed) now reports cerr 0.000 — indistinguishable from a flawless run. On main this case raised ZeroDivisionError, which was worse as a crash but at least not silently wrong. This path is also untested.

Location: src/inspect_evals/hle/metrics.py:77-78

Recommendation: Return float("nan") for the no-data case (metrics render NaN as absent rather than as a value), and add a unit test covering an all-None-attempts sample list.

Code Quality — unreachable branch

Issue: if not bins: return 0.0 at metrics.py:90-91 cannot fire. Past the if not pairs guard, len(pairs) >= 1, so BIN_SIZE = min(100, len(pairs)) >= 1 and len(confidence) // BIN_SIZE >= 1 for every input (when len(pairs) < 100, BIN_SIZE == len(pairs) and the floor division is exactly 1). Dead code implying a reachable state that isn't.

Location: src/inspect_evals/hle/metrics.py:90-91

Recommendation: Remove it.

Documentation, Environments, and Tooling — README Scoring section not updated

Issue: README.md:18-20 still says only "Accuracy and calibration error are reported over the dataset." Two user-visible facts introduced here are documented nowhere a user will look: (1) at --epochs > 1, cerr is now computed over n_samples × n_epochs attempts while accuracy/stderr remain over reduced samples, so the two headline metrics no longer share a denominator; (2) passing --epochs-reducer replaces the task-pinned reducer, at which point cerr silently reverts to the epoch-averaged semantics this PR exists to fix. The metric cannot detect that override (it has no visibility into the epoch count), so documentation is the only mitigation available.

Location: src/inspect_evals/hle/README.md:18-20

Recommendation: Add a short paragraph to the Scoring section covering both points. The changelog entry and PR body are not a substitute — a user reading the README to interpret their cerr number will not find either.

Notes

  • test_attempt_level_cerr_end_to_end: the cerr == 0.4 assertion at tests/hle/test_epoch_compatibility.py:160 does not discriminate the attempt path from the fallback — the reduced value {"score": 0.5, "confidence": 90} yields |0.9 - 0.5| = 0.4 too. The docstring acknowledges this and compensates with the _score_confidence_pairs spy, so coverage is real, but it makes the test dependent on a private function name. A fixture where attempt-level and reduced cerr differ numerically (as test_cerr_uses_attempt_level_pairs does at unit level) would let the e2e test assert on the metric alone.
  • test_hle_epoch_compatibility's docstring ("HLE stores confidence in Score.value... which is properly reduced across epochs") now describes the fallback path rather than how the real HLE task behaves, since that test builds its own Task without the pinned reducer. Worth a one-line note in the docstring so a future reader doesn't take it as the reference behaviour.
  • Delegating to mean_score rather than reimplementing the mean is the right call — it keeps NaN handling and the answer/explanation carry rules aligned with inspect, and the new dict is built fresh so scores[0].metadata is not mutated.
  • CI: Mypy (3.11/3.12/3.13), README check, and Render docs were still pending at review time. Worth confirming mypy is clean — cerr_value is annotated float but accumulates np.float64 from np.square(...).

Reviewer Feedback Status

Feedback Status
Parameter name value_to_float shadowed the module-level import (raised in prior automated review) Addressed in 17c26508 — renamed to to_float; confirmed at metrics.py:115
Missing test for cerr with None attempts (raised in prior automated review) Addressed in 17c26508 — test_cerr_skips_none_attempts present at tests/hle/test_metrics.py:534. See the first issue above: the test pins skip-behaviour, which contradicts what the changelog and docstring claim
@eleweek review, 2026-08-19 Approved, no body — no outstanding requests

No human reviewer feedback is unaddressed.

This is an automatic review performed by Claude Code. Any issues raised here should be fixed or justified, but a human review is still required in order for the PR to be merged.


Maintainers: comment /claude <instruction> on this PR and Claude will push a fix. To batch multiple changes, submit a review with body /claude and inline comments — Claude will address them all in one run. Single inline comments starting with /claude also work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HLE: cerr and unscored accounting are distorted at epochs > 1

4 participants