feat(platinum): add PlatinumBench dataset + 5 math 0-shot generation tasks - #65
Merged
Conversation
18 tasks
Collaborator
Author
|
Cross-reference: #70 fixes the fleet-wide Suggested order: #70 first, then this one. This PR is already compliant either way — it is the only module in the repo that was — so nothing here needs to change. Merging the guard first just means this lands in a tree where the rule is enforced rather than one where it is a comment. |
Integrate MadryLab's PlatinumBench math subsets: one dataset serving all 14 non-vqa HF configs, and a task subpackage whose five leaves (gsm8k, svamp, multiarith, singleop, singleq) share one base. Prompts, extraction and scoring all follow upstream rather than sieval's native math path: the prompts are carried in the data (platinum_prompt / platinum_prompt_no_cot), and upstream's parse + float comparison are vendored byte-faithfully into sieval/community/platinum_bench.py under its CC-BY-4.0 terms. Rows whose cleaning_status is "rejected" are dropped, which is the point of the benchmark: 953 of 1042 rows kept. All five tasks ship status="experimental" — not yet validated against upstream's published per-dataset error counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaying upstream's own published inferences (madrylab/platinum-bench-paper-cache) through this pipeline reproduces all 120 math error counts of the paper's Table 3 -- 24 models x 5 subsets, exact, every cell. A fresh run could not have shown this: upstream samples at temperature 0.5 with no seed, and the metric is a 0-19 error count out of 100-274 rows, so binomial noise alone is ~2 errors wide. The replay is deterministic and costs nothing, and every one of the 968 prompts per model hits a cache entry, which is what proves our prompt is byte-identical to upstream's. Closing the last fidelity gap needed a third prompt variant. `platinum_prompt_no_cot` carries a dangling "Then, provide" -- a leftover conjunction from the CoT wording -- and upstream rewrites it to "Provide" for its o1 snapshots only, not for o1-mini / o3-mini. Both branches are now reachable as `prompt_variant="no_cot"` / `"no_cot_o1"`, which takes the port from 21 to 24 of 24 published rows. The paper's counts belong to madrylab/platinum-bench-paper-version (968 math rows kept). The shipped pin stays on the current madrylab/platinum-bench, matching upstream's own default, which rejects 15 more rows (953) and has no published table; the delta is recorded in the reference notes so nobody reads Table 3 as an exact target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t trap The Table-3 replay stubs the model layer, so it proves scoring fidelity and nothing about the wire. Ran all five subsets end-to-end instead -- 953 questions, Qwen3-32B over an OpenAI-protocol endpoint, temperature=0.5, one sample. Zero pipeline failures. 6 errors: singleq 0/100, multiarith 1/170, singleop 1/150, gsm8k 2/268, svamp 2/265. Five are ordinary wrong answers with a clean "Answer:" line; the records confirm max_tokens=6000 and temperature=0.5 reach `request_params`, so the task's budget is not merely set on the object. The sixth error is worth documenting rather than dismissing. Upstream sized max_tokens=6000 for non-thinking models; Qwen3-32B spent the whole budget inside the reasoning channel on one singleop question -- finish_reason "length", 10k characters of "Wait, no, 4 + 7 is 11?" -- and returned an empty answer, which scores as an error indistinguishable from a wrong one unless the reader knows to look. anomalies.json does separate them: that row is the only anomaly across all 953, flagged truncated_output + extraction_failure. Both the docstring and the reference notes now say to raise the budget on a thinking model and to read `errors` alongside anomalies.json, with tests pinning the wording so it cannot quietly drop out. No behaviour change -- notes only, plus the regenerated meta/index.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The replay that earned `status="stable"` stubs the model layer, and the live
run that exercised it used Qwen3-32B — a model upstream never published a row
for, so it could confirm the transport but not the number. This closes that
gap on a model upstream did publish.
Llama-3.3-70B-Instruct, all 5 math subsets over the paper version's 968
questions at temperature 0.5, one sample, served bf16 — the released precision.
That last part is not incidental: aggregators route across providers that
quantize differently, so the run pins its provider with fallbacks disabled and
the pin lands in the persisted `request_params`, making the silicon that
answered auditable after the fact.
0/0/0/8/7 = 15 errors live
0/0/0/7/7 = 14 errors Table 3
Four of five cells exact, +1 pooled against a ~3.7 Poisson sigma, 98.45% vs
98.55% accuracy, 0 pipeline failures, full 968-row coverage. All 15 misses are
genuine arithmetic errors: every one finished on a stop token with a parseable
integer, so none is a truncation or parse artifact — which is the distinction
that matters, since a scoring bug would surface exactly there.
Exact equality is not reachable: upstream samples at temperature 0.5 with no
seed and the metric is a raw 0–19 error count, so the replay is what pins
scoring fidelity and this run is what pins the live path.
`meta/index.json` is regenerated because the notes text is embedded in it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…wn provider The first live alignment picked a bf16 endpoint on the reasoning that released precision is the right standard. Upstream's model factory shows that is not what they did: both open-weight rows in Table 3 were served through DeepInfra. Two more 968-row runs on that provider, and the notes now report all three. Qwen2.5-72B-Instruct DeepInfra 0/0/0/4/4 = 8 vs Table 3 0/0/0/4/7 = 11 Llama-3.3-70B-Instruct DeepInfra 1/0/0/6/9 = 16 vs Table 3 0/0/0/7/7 = 14 Llama-3.3-70B-Instruct Crusoe 0/0/0/8/7 = 15 vs Table 3 0/0/0/7/7 = 14 All three land within 0.7 sigma_D, 0 pipeline failures, every miss a stop-token finish with a parseable integer. Keeping the bf16 run alongside the fp8 one brackets the serving-precision effect rather than hiding it. Two corrections to how the earlier number was stated: - The yardstick was the single-count Poisson sigma (~3.7). The statistic is a difference of two independent unseeded draws, so it is sigma_D = sqrt(paper + live) ~ 5.5. Understating the spread overstates the agreement. - "Four of five cells exact" oversold it: three of the five cells publish 0 errors, so agreement there is close to automatic. Only svamp and gsm8k carry signal, and the notes now say so. Counts alone cannot separate sampling noise from a scoring difference, so the notes add the discriminator: 9 of 15, 9 of 16 and 7 of 8 live misses are misses in upstream's own completions, with the remainder moving in both directions. Same scorer on upstream's text reproduces Table 3 exactly (the 120/120 replay); same scorer on fresh text differs only where the model sampled differently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`infer()` passed `max_tokens=6000` (upstream's engine default) as a
call-time kwarg. `agenerate` merges `{**self._kwargs, **kwargs}`, so that
did not supply a default — it overrode the caller. A user configuring
`max_tokens` in `models:` / `infer_args` got 6000 on the wire regardless,
silently, which is exactly the knob this benchmark most needs turned: the
score is budget-sensitive, and the docstring told readers to raise the
budget for a thinking model without saying that the obvious place to raise
it does nothing.
Drop the task-side budget and the `max_tokens` ctor arg entirely, and
document 6000 as an infer prerequisite instead (the `imo_answer_bench`
pattern). The task now injects no decoding params at all, matching
`gsm8k_0shot_gen` / `human_eval_0shot_base_gen` / `livecodebench_*`; the
tasks that do pass `max_tokens` are the ones where it is task-coupled
(`*_clp` scoring one next token, `ruler` reading a per-sample budget).
The test stub is why this was invisible. `CapturingChatModel` overrode
`_agenerate_impl`, which sits *below* the merge, so `last_kwargs` recorded
what the task passed rather than what the model would send — making every
"injects no decoding params" assertion vacuous. It now records the merged
request and takes model-side kwargs, and a new test pins that a configured
budget survives. Both new `infer` tests fail against the previous code.
Also: resolve `get_parse_fn("math")` once at import instead of rebuilding
the strategy table per sample; note that `PLATINUM_BENCH_REVISION` is not
forwarded to `load_dataset` (arc's wording); note that the dataset's
`categories`/`tags` cover only the subsets that ship a task, since one
sample TypedDict is the FK for all 14 configs.
Verified: replaying upstream's paper cache through the changed pipeline
reproduces the same Table 3 cells as before (30 spot-checked cells across
all three prompt variants, 968 cache hits per model, 0 fails) — the budget
never reached the stub, so scoring fidelity is untouched. 3010 unit + 64
integration/acceptance pass; ruff, ty, stubs and full preflight clean;
`meta/index.json` regenerated for the notes edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ethan-scitix
force-pushed
the
feat/platinum-bench-math
branch
from
August 6, 2026 09:37
0027ad9 to
ddb0837
Compare
The comment on `setup()`'s mis-wiring guard said the Dataset class serves all 15 configs. It serves 14: `vqa` is the 15th and `__init__` rejects it, since at the pinned revision it spells the strategy column `platinum_parsing_stratagy` and its prompts reference images stored outside the dataset repo. `sieval/datasets/platinum_bench.py` already says 14, so the two comments disagreed. Comment-only, and the string is not part of `PLATINUM_REFERENCE_NOTES`, so `sieval/meta/index.json` is unaffected (`check_meta_index_sync` passes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PlatinumBenchDataset` required a `subset=` keyword argument — the only custom
`__init__` among 37 dataset modules, and the only required dataset constructor
argument in the repo. It existed because a HuggingFace config is a load-time
choice that no existing row transform could express.
The cost was a redundant knob. Each leaf already pins its subset
(`subset: ClassVar[str] = "gsm8k"`), so `args.subset` was a second copy of a
fact the class fixes, and `setup()` spent a runtime check confirming the two
copies agreed. Mis-wiring was guarded, not unrepresentable.
So give `Dataset` the transform that was missing, and let the caller narrow:
datasets:
platinum_gsm8k:
class: PlatinumBenchDataset
operations:
- filter: {by: subset, value: gsm8k}
`Dataset.filter(by, value, *, split="test") -> Self` joins
repeat/slice/shuffle/stratified_sample and follows their conventions: a bare
verb, `by=` spelled as in `stratified_sample`, an immutable clone via
`_clone_with_new_dict`, and `self` returned for an absent or empty split. It
raises on an unknown column and — deliberately, unlike the size transforms — on
zero matches, because an empty split is a misspelled value far more often than
an intent, and would otherwise surface as a run that silently scores nothing.
`value` takes a scalar or a list, so two subsets need no sibling method. A
string stays atomic rather than becoming a set of characters.
Filtering in the outer layer is what keeps this simple: `Task.dataset` is
read-only, so a task cannot adopt a clone, and narrowing outside means no
setter and no task-side mutation. (`mmmlu_kshot_clp` mutates
`dataset_dict["test"]` in place for want of exactly this; migrating it needs a
`fraction=` budget on `stratified_sample` and is left alone here.)
`load()` now merges the 14 non-vqa configs, which measurably share one feature
signature across the six platinum columns and so concatenate without a cast.
Each config is loaded, cleaned and stamped in full isolation and only then
concatenated, so narrowing back to one subset is structurally — not just
empirically — the rows a single-config load produced, in order. `vqa` stays out:
it misspells the column as `platinum_parsing_stratagy` and fails
`select_columns` outright. `PLATINUM_SUBSETS` is a frozenset, so the merge sorts
it; without that the row order would follow the string hash seed and sample ids
would shift between processes.
`setup()` now checks the wired split carries only this task's subset via
`unique("subset")`. A missing split, another Dataset class and an empty split
all collapse to `[]`, so one comparison covers every way the wiring can be
wrong, and the message names the `filter` operation that fixes it. A stale
`args.subset` gets the migration rather than `load_dataset`'s opaque
"unexpected keyword argument", the same courtesy `session.py` extends to the
`select` -> `slice` rename.
The reproduction is unchanged, and that was verified rather than assumed:
* Row lists are byte-identical — content and order — between the old
per-config path and merge-then-filter, on the pinned revision (953 math rows:
268/265/170/150/100) and on the paper version (968).
* Re-running the paper-cache replay through this code reproduces **all 120 math
cells of Table 3, 24/24 models exact**.
* Shipped counts re-measured against the pinned snapshot: 2725 kept of 3062
across the 14 configs. The docstring said 2752 — the paper version's number —
and is corrected.
* Wiring smoke through the real session layer: the filter yields 268 rows and
setup passes; omitting it yields 2725 and setup rejects; the wrong subset
yields 265 and setup rejects.
`filter` is registered in both operation whitelists — the dispatch in
`session.py` and `_VALID_OPERATIONS` in `validation.py`. Only the first would
have let `--dry-run` reject a config the run executes fine. Its `value` is
required by presence, not truthiness, so `value: 0` and `value: false` stay
usable.
No CHANGELOG entry: the dataset and all five tasks are new in this same
unmerged PR, so no shipped config breaks.
ruff, ty, 3067 unit tests, 64 integration/acceptance, full preflight and both
sync scripts all clean; `meta/index.json` regenerated for the description edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
16 tasks
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.
Type
Summary
madrylab/platinum-bench) — benchmarks whose labels were re-verified so that a remaining failure is a real model failure, not label noise. Scope is the 5 math subsets (gsm8k,svamp,multiarith,singleop,singleq); the other 9 configs load but ship no task.PlatinumBenchDatasetmerges all 14 non-vqa configs into onetestsplit and stampssubseton every row; the caller narrows to one subset with afilteroperation (see Selecting a subset).sieval/tasks/platinum_bench/holds 5 leaves that are 2-line subclasses of one_base.py— same layout asarc/(feat(core): persist task identity in run meta.json #57). ThedatasetFK resolves off the base's sample generic through the MRO, so the leaves declare no generic of their own.cleaning_status == "rejected"marks a question the authors found unanswerable/mislabeled. 953 of 1042 rows kept (gsm8k 268/300, svamp 265/300, multiarith 170/174, singleop 150/159, singleq 100/109).platinum_prompt/platinum_prompt_no_cot); upstream'sget_parse_fn+check_prediction(exact float equality onplatinum_target[0]) are vendored byte-faithfully intosieval/community/platinum_bench.py.status="stable"on all five, earned by an exact reproduction: replaying upstream's own published inferences (madrylab/platinum-bench-paper-cache) through this pipeline reproduces all 120 math error counts of the paper's Table 3 — 24 models × 5 subsets, every cell. Independently, three live runs on two published checkpoints (Qwen2.5-72B-InstructandLlama-3.3-70B-Instruct, 968 rows each, on upstream's own serving provider) land within 0.7 σ_D of their Table 3 rows — 8 vs 11, 16 vs 14, 15 vs 14 — confirming the numbers survive real sampling over the wire. Tables below.prompt_variantnow takescot/no_cot/no_cot_o1. Upstream'splatinum_prompt_no_cotcolumn carries a dangling"Then, provide"(a leftover conjunction from the CoT wording) and upstream rewrites it to"Provide"for its o1 snapshots only — not for o1-mini / o3-mini, which ran against the unedited string. Both branches are needed, and having them takes the port from 21 to 24 of 24 published rows.examples/YAML, no changes to any existing task. One additivecore/change:Dataset.filter, plus its registration as a YAML operation (see below). Theget_task_class()subpackage-lookup fix these tasks need already landed in feat(core): persist task identity in run meta.json #57.Selecting a subset
A HuggingFace config is a load-time choice, so the loader reads all 14 non-vqa configs and the caller narrows:
This replaced an earlier required
subset=constructor argument — see Review Fixes for why and what was re-verified. Omit the operation, or name the wrong subset, andsetup()fails before any tokens are spent.Related Issues
Refs #57 (the
arc/subpackage layout and theget_task_class()subpackage lookup this builds on).Test Plan
Automated
ruff check+ruff format --check: all checks passed, 386 files formatted)ty check: all checks passed).mypy --strictreports 29 diagnostics of 4 classes (no-untyped-defon stage methods,no-untyped-callinto the vendor,union-attron upstream's unguardedre.search(...).group()); the existinggsm8kdataset+task pair reports 37 of the same classes, and[tool.mypy]excludessieval/community— so this is baseline, not new.Dataset.filter, plus the operation's two CLI whitelists)scripts/check_preflight.py): all PASS, includingcheck_meta_index_sync,check_tasks(45 tasks),check_datasets(70 exports, all sources pinned),check_imports,check_task_shot_knobsNew tests deliberately pin the failure modes that would otherwise be silent:
math_datasetslist — a misspelled subset silently degrades scoring from float equality to string membership, and"42" in ["42"]still passes, so nothing else would notice.vars()is exactly{subset, tags, model_type, n_shot}(the last three seeded by@sieval_task), so a leaf can never grow behaviour that drifts from its four siblings.kept,total,total - kept) so a stale number cannot agree with itself.prediction=Noneand the absentpredictionkey are covered separately, becauseobj_to_dictdropsNonekeys on the disk round-trip.AttributeErroron digit-free output, and the deadprediction != 'Parsing error'guard that letsfloat('parsing error')raiseValueError.Manual
sieval dataset download platinum_benchsucceeds (17 files at the pinned revision).platinum_parsing_strategy == "math"; surviving statuses are onlyconsensus/verified/revised; the selected columns are exactly the 6 shared ones plus the stampedsubset; and all 953 gold answers round-trip through upstream'sparse+check_prediction(953/953), so the vendored extractor and the pinned data agree.sieval task show platinum_gsm8k_0shot_genresolves by name (the subpackage-hosted lookup path) and renders the full reference-impl notes.Qwen/Qwen3-32Bover an OpenAI-protocol endpoint,temperature=0.5, one sample, driven throughsieval eval+ YAML. 0 pipeline failures, 6 errors: singleq 0/100, multiarith 1/170, singleop 1/150, gsm8k 2/268, svamp 2/265. Persistedrequest_paramsconfirmmax_tokens=6000andtemperature=0.5actually reach the wire. (That run predates the review fix below, which moved the budget out of the task; the request it sent is byte-identical either way, since the value is the same 6000 — only its source changed, frominfer()to the model config.) Five of the six errors are ordinary wrong answers with a cleanAnswer:line.max_tokens=6000for non-thinking models. Qwen3-32B spent the entire budget inside the reasoning channel on one singleop question (finish_reason="length", 10k characters of"Wait, no, 4 + 7 is 11?") and returned an empty answer — which scores as an error indistinguishable from a wrong one unless you know to look.anomalies.jsondoes distinguish it: that row is the only anomaly across all 953, flaggedtruncated_output+extraction_failure. The docstring and reference notes carry this as an infer prerequisite: setmax_tokens=6000to reproduce, raise it on a thinking model, and readerrorsalongsideanomalies.json. Tests pin the wording.Qwen2.5-72B-Instructandmeta-llama/Llama-3.3-70B-Instruct— byte-identical to the paper cache's model keys — all 5 subsets over the paper version's 968 questions,temperature=0.5, one sample. Upstream's model factory routes both of its open-weight rows through DeepInfra, so the runs pin that provider with fallbacks disabled and the pin lands in the persistedrequest_params; a third run on a bf16 endpoint brackets the serving-precision effect. 0 pipeline failures, full 968-row coverage, all three within 0.7 σ_D of their published rows, and the live misses overlap upstream's own while moving in both directions — numbers in Live alignment below.temperature=0.5with no seed. The two checks answer different questions on purpose — the replay pins scoring fidelity against upstream's own inferences, a live run pins that the transport and stage plumbing work on real completions.Score Comparison
Three checks, because none alone is sufficient: a replay of upstream's published inferences pins scoring fidelity against Table 3; live runs on two published models pin that the same numbers survive real sampling over the wire; and a live run on a thinking model exercises a decoding regime upstream never covered. All three are described below.
Why replay and not a fresh run
Upstream decodes at
temperature=0.5, one sample, no seed — so a fresh run against any single model is unreproducible by construction, and the metric makes that fatal rather than merely annoying. Upstream reports raw error counts, 0–19 out of 100–274 rows; for GSM8K (n=274, p≈0.011) the binomial σ is ≈1.7 errors, so "our count is within ±2 of theirs" would be unfalsifiable noise dressed up as agreement.Upstream also published the actual completions behind the paper's numbers (
madrylab/platinum-bench-paper-cache, 15 pickles). Replaying those through this pipeline is deterministic, costs nothing, and is a strictly stronger check than any live run:(prompt, temperature, 0, model)and raises on a miss. All 968 prompts × 25 cached models hit — which is what proves our prompt string is byte-identical to upstream's, not merely equivalent.preprocess/infer/postprocess/feedback/report, and the on-disk record round-tripTaskRunnerdrives.fails == 0on every run.madrylab/platinum-bench-paper-version(the revision the paper's numbers are computed on), which keeps 968 math rows.Table 3, math block — errors per subset (lower is better)
# Platinum Questions: SingleOp 150 · SingleEq 100 · MultiArith 171 · SVAMP 273 · GSM8K 274.no_cot_o1@ 1no_cot_o1@ 1no_cotno_cot_o1@ 1no_cot@ 1no_cotno_cot@ 1Every cell above is both ours and the paper's —
rows compared: 24, exact: 24, mismatched: 0; cells compared: 120, exact: 120. Blankvariant= the defaultcot@ 0.5. The cache holds a 25th model (gemini-2.0-flash-thinking-01-21) that Table 3 does not print; it replays cleanly too but has nothing to be compared against.Live alignment on published models
The replay stubs the model layer, so it cannot catch a defect that only appears with real sampling over the wire. Two of the checkpoints Table 3 scores are still served, so both were run live on the paper version's 968 rows at
temperature=0.5, one sample.Which provider is not a detail. An aggregator routes across providers that quantize differently, and quantization changes completions, so every run pins its provider with
allow_fallbacks: false— a wrong-silicon serve fails instead of silently routing, and the pin lands in the persistedrequest_paramswhere it stays auditable. The first run picked a bf16 endpoint on the reasoning that released precision is the right standard. Upstream's own model factory says otherwise: both open-weight rows in Table 3 were served through DeepInfra. So both models were run there, and the bf16 run is kept alongside rather than discarded — together they bracket the serving-precision effect instead of hiding it.The yardstick is σ_D = √(paper + live) ≈ 4.4–5.5, not √paper. The paper's own count is a single unseeded draw at temperature 0.5, so the comparison is a difference of two noisy draws and its spread is the sum of both variances. This also settles whether more sampling would help: it would not. Repeats shrink only our side of the variance, and the published number's own σ ≈ 3.7 is an irreducible floor — 5 repeats would move σ_D from 5.5 only to ≈ 4.2. Buying a different published model adds information; re-drawing the same one does not.
Three of the five cells publish 0 errors (singleop, singleq, multiarith — 421 rows at a near-zero true error rate), so agreeing there is close to automatic and should not be counted as evidence. Only svamp and gsm8k carry signal.
Counts alone cannot tell sampling noise from a scoring difference — both look like "a different number". The identity of the missed questions can. Upstream's cache is keyed by prompt, so its own completion for every live row is directly retrievable:
Misses overlap heavily and move in both directions in every run. That is the signature of resampling the same model; a scorer that had drifted looser or stricter would push one-sidedly. The strongest form of the argument: the same scorer applied to upstream's own completions reproduces Table 3 exactly (the 120/120 replay), and applied to fresh completions from the same model, provider and decoding it differs — so the delta lives entirely in the sampled text, not in the scoring. All 968 live prompts were verified byte-identical to the cache's keys.
Plumbing is clean in all three runs: 0 pipeline failures, full 968-row coverage, and
max_tokens=6000/temperature=0.5/ the provider pin on every persisted record. Every miss finished on a stop token with a parseable integer prediction, so none is a truncation or a parse artifact — precisely where a defect in the vendored extractor or themax_tokensbudget would surface. The one non-saturated surprise, asingleopmiss in the fp8 Llama run where Table 3 has 0, is a genuine arithmetic slip (4754vs4764), not a pipeline failure.Scored on the shipped 953-row pin from the same records, no extra inference: Qwen 0/0/0/4/4 = 8, Llama@DeepInfra 1/0/0/5/9 = 15, Llama@Crusoe 0/0/0/7/6 = 13.
Exact equality is not achievable here and is not the bar: upstream samples at temperature 0.5 with no seed. The replay is what pins scoring fidelity; these runs pin the live path.
The one caveat, and why the pin does not change
The counts above belong to
platinum-bench-paper-version(968 math rows). The shipped pin stays on the currentmadrylab/platinum-bench, matching upstream's own default, which rejects 15 more math rows — 953 total (multiarith 171→170, svamp 273→265, gsm8k 274→268). Raw row counts are identical in both repos, so the delta is purely additionalrejectedlabels from later cleaning. Upstream publishes no table for the current revision, so its scores will land near Table 3 but not on it. That is recorded inReferenceImpl.notes(with the reproduction recipe) and pinned by a test, so nobody reads Table 3 as an exact target for the default pin.Review Fixes
Round 1 — the token budget
Landed in
refactor(platinum): hand the token budget back to the model layer. The reproduction evidence above is unchanged — re-running the paper-cache replay against the modified code returns the same Table 3 cells (30 spot-checked cells across all three prompt variants, 968 cache hits per model, 0 fails), because the budget never reached the replay's stub model in the first place.max_tokensis no longer forwarded by the task.infer()used to passmax_tokens=6000as a call-time kwarg.ageneratemerges{**self._kwargs, **kwargs}, so that did not supply a default — it overrode the caller: amax_tokensset inmodels:/infer_argswas silently discarded and 6000 went on the wire regardless. That is the one knob this benchmark most needs a caller to turn, since the score is budget-sensitive, and the docstring was telling readers to raise the budget for a thinking model without saying that the obvious place to raise it does nothing.The
max_tokenstask arg and theUPSTREAM_MAX_TOKENSconstant are gone; 6000 is documented as an infer prerequisite instead (theimo_answer_benchpattern). The task now injects no decoding params, matchinggsm8k_0shot_gen/human_eval_0shot_base_gen/livecodebench_*. The tasks that legitimately do passmax_tokensare the ones where it is task-coupled —*_clpscoring a single next token,rulerreading a per-sample budget from the row.max_tokens: 6000on the model; omit it and you get the backend default, which can truncate CoT and under-score.The test stub is why this was invisible.
CapturingChatModeloverrode_agenerate_impl, which sits below the kwargs merge, solast_kwargsrecorded what the task passed rather than what the model would send — making every "the task injects no decoding params" assertion vacuous. It now records the merged request and accepts model-side kwargs, and a new test pins that a configured budget survives. Bothinfertests fail against the previous code (verified by reverting).Also in that commit:
get_parse_fn("math")is resolved once at import instead of rebuilding the strategy table per sample.PLATINUM_BENCH_REVISIONis not forwarded toload_dataset— the pin is honoured bysieval dataset downloadstaging, so passing the bare hub id gets whatever is current (same wording as_arc.py).categories/tagscover only the subsets that currently ship a task; one sampleTypedDictis the FK for all 14 configs, so a futuredrop/squadleaf must widen them or render as ElementaryMath.Re-verified after the change:
ruff+ruff format,ty, package stubs, 3010 unit and 64 integration/acceptance tests, and full preflight all clean;sieval/meta/index.jsonregenerated for the notes edit.Round 2 — the subset is selected, not constructed
Landed in
refactor(platinum): select the subset with a Dataset.filter operation(plus a one-line comment fix). The reproduction is unchanged, and that was verified rather than argued — see the re-verification list below.PlatinumBenchDatasetno longer takes asubset=argument. It was the only custom__init__among the repo's 37 dataset modules and the only required dataset constructor argument anywhere. The cost was a redundant knob: each leaf already pinssubset: ClassVar[str] = "gsm8k", soargs.subsetwas a second copy of a fact the class fixes, andsetup()spent a runtime check confirming the two copies agreed. Mis-wiring was guarded, not unrepresentable.Dataset.filter(by, value, *, split="test") -> Selfis the transform that was missing. It joinsrepeat/slice/shuffle/stratified_sampleand follows their conventions: a bare verb,by=spelled as instratified_sample, an immutable clone via_clone_with_new_dict,selfreturned for an absent or empty split. It raises on an unknown column and — deliberately, unlike the size transforms — on zero matches, because an empty split is a misspelled value far more often than an intent and would otherwise surface as a run that silently scores nothing.valuetakes a scalar or a list; a string stays atomic rather than becoming a set of characters.It does not subsume
stratified_sample:filterdrops non-matching groups,stratified_samplenever drops a group. Opposite purposes, no overlap.Filtering in the outer layer is what keeps this simple.
Task.datasetis read-only, so a task cannot adopt a clone; narrowing before the dataset reaches the task means noTask.datasetsetter and no task-side mutation.load()merges the 14 non-vqa configs. They measurably share one HuggingFace feature signature across the six platinum columns, so they concatenate without a cast. Each config is loaded, cleaned and stamped in full isolation and only then concatenated, which makes "narrow back to one subset == the rows a single-config load produced, in order" structural rather than merely observed.vqastays out: it misspells the column asplatinum_parsing_stratagyand failsselect_columnsoutright.PLATINUM_SUBSETSis afrozenset, so the merge sorts it — without that the row order would follow the string hash seed and sample ids would shift between processes.setup()now checks the wired split viaunique("subset"). A missing split, a differentDatasetclass and an empty split all collapse to[], so one comparison covers every way the wiring can be wrong, and the message names thefilteroperation that fixes it. A staleargs.subsetgets a migration error rather thanload_dataset's opaque "unexpected keyword argument" — the same courtesysession.pyextends to theselect→slicerename.filteris registered in both operation whitelists: the dispatch insession.pyand_VALID_OPERATIONSinsieval/cli/validation.py. Only the first would have let--dry-runreject a config the run executes fine. Itsvalueis required by presence, not truthiness, sovalue: 0andvalue: falsestay usable.A corrected number. The loader docstring claimed 2752 merged rows; that was the paper version's total. Re-measured against the pinned snapshot it is 2725 kept of 3062 across the 14 configs — later cleaning rejects 27 more non-math rows. The five math subsets (953) were always right. Caught by re-verifying against the real pinned data rather than the local paper-version copy.
Re-verified after the change:
platinum_parsing_strategy == "math", norejectedsurvivorsfilter→ 268 rows,setup()passes; operation omitted → 2725 rows,setup()rejects; wrong subset → 265 rows,setup()rejectsruff/ty/ full preflight / both sync scriptsNo CHANGELOG entry: the dataset and all five tasks are new in this same unmerged PR, so no shipped config breaks.
Follow-up, not this PR:
mmmlu_kshot_clpmutatesdataset_dict["test"]in place (:466,:550) for want of exactly this transform. Migrating it needs afraction=budget onstratified_sampleand a decision about task-state-dependent narrowing, so it ships separately.Checklist
Required (all PRs)
type(scope): description)AI-Generated Code - <model> (<provider>)in module docstringcore/— the onecore/change (Dataset.filter) adds no imports at all, only a method beside the existing transformsIf: New or Modified Benchmark
sieval dataset download platinum_benchsucceeds)__init__.py(lazy AST scan picks up the subpackage;.pyistubs regenerated,sieval/meta/index.jsonsynced)If: community/ Changes
sieval/community/platinum_bench.pyis a byte-faithful copy ofget_parse_fn+check_predictionfromsrc/utils.py@8fd2f82; its docstring names the two behaviours callers must absorb rather than fixing them in place. Two deliberate deviations, both recorded in each task'sReferenceImpl.notes(sosieval task showsurfaces them):prompt_variant(task arg, defaultcot) replaces upstream's hardcoded reasoning-model name list. All three of upstream's prompt strings are reachable explicitly —cot,no_cot, andno_cot_o1(upstream's o1-only"Then, provide"→"Provide"edit) — so a name list that ages badly is traded for an argument the caller sets. The reference notes say which value reproduces which published row.prediction=None/correct=Falseinstead of upstream's swallowedAttributeError.cc-by-sa-4.0(that's whatDatasetMeta.licenserecords); the two differ.If: New Dependency
Not applicable — no new dependencies.
🤖 Generated with Claude Code