test(metrics): pin the MMLU/GSM8K extractor invariants exhaustively (closes #32) - #37
Open
dchaudhari7177 wants to merge 2 commits into
Open
test(metrics): pin the MMLU/GSM8K extractor invariants exhaustively (closes #32)#37dchaudhari7177 wants to merge 2 commits into
dchaudhari7177 wants to merge 2 commits into
Conversation
extract_mmlu_answer and extract_gsm8k_answer carry format-tolerance and precedence logic that had example-based coverage only. This adds ten invariant tests covering the whole documented input space. No new dependency. The issue offered hypothesis or hand-rolled parametric cases; I went with the latter for two reasons: - The MMLU space is small enough to enumerate *exhaustively* - 10 letters x 6 wrappings x 10 choice-counts - which is strictly stronger than sampling it. - Adding hypothesis to the dev group meant relocking, and `uv lock` on this tree regenerates uv.lock into a 2,893-line diff that pulls torch, CUDA and datasets into the lockfile. Not worth it for a test-only change. Where a value is genuinely unbounded (a GSM8K number) a seeded Random supplies boundary values plus a spread, so failures stay reproducible. Two behaviours worth flagging, both pinned as-is rather than changed: - The standalone-letter pattern is case-sensitive, so a bare lowercase "a" extracts nothing, while the explicit "answer: a" form does (it is IGNORECASE). The docstring lists "lowercase" among the tolerated formats without that distinction. - num_choices=0 clamps to 1 rather than rejecting, so it returns 0 for "A". The range invariant is therefore asserted over num_choices >= 1. Verified the tests are load-bearing by mutation rather than by passing alone: making the first standalone letter win, dropping the num_choices filter, and removing the #### marker preference each fail the corresponding invariant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) A card stated effect and cliff numbers but not the conditions they were measured under. Provenance now records n_seeds, model_id, repeng_version, hardware and wall_clock_s, rendered at the foot of both the markdown and HTML cards and exposed through --json. Design notes: - Used a frozen dataclass rather than the pydantic model the issue suggested. Core declares `dependencies = []` ("core stays light") and report.py models every other type as a frozen dataclass, so pydantic here would be the first core dependency. Easy to switch if you would rather have it. - n_seeds is the MINIMUM across dose and layer points, not the mean - the weakest point bounds the claim. - provenance_from() always recomputes n_seeds from the parsed curves and only passes the caller's other fields through, so the seed count printed on an artifact cannot disagree with the CSVs it came from. A test asserts a caller-supplied n_seeds=999 is overridden. - Missing fields render as "unknown" rather than failing, and an UNKNOWN seed count is deliberately not a contract violation - only a known count below MIN_SEEDS warns. - build_report() keeps its signature and return type; the new `provenance` argument is optional, so existing callers are unaffected. --model was documented as "recorded in output" but was never actually read. It now feeds model_id as the fallback when no vector is passed; a vector's own metadata takes precedence over it. Also adds --hardware and --wall-clock-s for the GPU sweep to supply, and a stderr warning when a card is built below the seed contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #32.
Ten invariant tests over
extract_mmlu_answer/extract_gsm8k_answer, covering the documented input space rather than sampled points. Test-only; no production code touched.Why not hypothesis
The issue offered either hypothesis or hand-rolled parametric cases. I went with parametric, for two reasons:
hypothesisto the dev group requires relocking, anduv lockon this tree regeneratesuv.lockinto a 2,893-line diff that pullstorch,cuda-*anddatasetsinto the lockfile (the committed lock is currently minimal). That is a large, risky change to carry on a test-only PR — happy to add it in a separate PR if you'd rather have it.Where a value is genuinely unbounded (a GSM8K number), a seeded
random.Random(20260812)supplies boundary values (0, 1, 999, 1000, 1_000_000) plus a spread, so runs are deterministic and failures reproducible — meeting the "no random seed drift" criterion.Invariants
MMLU — every wrapping of a valid letter agrees; a letter beyond
num_choicesis never returned; the result is alwaysNoneor a valid index; the last standalone letter wins; an explicitanswer is/:beats a standalone letter even when the standalone one appears later (precedence is by kind, not position).GSM8K —
#### nalways beats a trailing number;$, thousands commas and a trailing period normalise alike (checked both inline and behind the marker); the last number wins absent a marker; digit-free text isNone; the result is alwaysNoneor afloat()-parseable canonical string.The last MMLU and GSM8K invariants matter because
score_mmluindexes gold answers with the result andscore_gsm8kcallsfloat()on it — a bad value would surface as a silent mis-score or a crash in the scorer, not here.Verified by mutation, not by passing
Tests that only ever pass prove little, so I confirmed each is load-bearing:
test_mmlu_last_standalone_letter_winsfailsnum_choicesvalidity filtertest_mmlu_result_is_always_none_or_a_valid_indexfails####marker preferencetest_gsm8k_marker_always_beats_a_trailing_numberfailsTwo behaviours worth your call
Both are pinned as they are, not changed — flagging rather than deciding:
"a"extracts nothing, while"answer: a"does (that pattern isIGNORECASE). The docstring lists "lowercase" among tolerated formats without drawing this distinction.num_choices=0clamps to1rather than rejecting, soextract_mmlu_answer("A", 0)returns0. The range invariant is asserted overnum_choices >= 1accordingly.Local results
mypy srcreports one error on this machine —numpy/__init__.pyi:737: Type statement is only supported in Python 3.12 and greater— which reproduces identically on a pristine tree (git stash), so it is a local venv/numpy-stub artefact rather than anything from this change.mypy srcdoes not covertests/in any case.🤖 Generated with Claude Code