Single-owner dataset statistics: pure stats functions, unified transform init, on-disk stats cache#786
Open
stefaanhessmann wants to merge 15 commits into
Open
Single-owner dataset statistics: pure stats functions, unified transform init, on-disk stats cache#786stefaanhessmann wants to merge 15 commits into
stefaanhessmann wants to merge 15 commits into
Conversation
Turn the out-of-tree synthetic-dataset script used to verify review finding S1 (wrong offset statistics for late-initialized model postprocessors) into proper tests: RemoveOffsets and a late-initialized AddOffsets postprocessor must hold the identical, independently recomputed raw training mean and estimated atomrefs, and the data pipeline must actually remove that mean. Guard rail for the upcoming statistics refactor. Co-Authored-By: Claude Fable 5 <[email protected]>
calculate_stats and estimate_atomrefs accept an optional indices parameter. When given, they internally build an index-restricted raw view of the passed dataset (no transforms, no split label) and iterate over that — 'statistics are computed on raw data' becomes part of the functions' contract instead of a caller convention. Existing callers are untouched. The shared synthetic-dataset fixture moves to tests/data/conftest.py. Co-Authored-By: Claude Fable 5 <[email protected]>
…indices The provider now calls the stats functions with explicit indices; the raw-view guarantee lives in those functions' contract, so the shallow-copy/strip-transforms trick is deleted. The datamodule passes the base dataset and the train index list it already has at setup time. Public get_stats/get_atomrefs are unchanged. Co-Authored-By: Claude Fable 5 <[email protected]>
get_atomrefs(property, is_extensive, estimate=True) on both the provider and the datamodule: with estimate=False, only dataset-provided atomrefs are returned, and a clear error is raised if they are absent. Prepares replacing the side-channel atomrefs= argument of transform initialization. Co-Authored-By: Claude Fable 5 <[email protected]>
…ument Transforms' initialize() now takes one duck-typed stats source (anything with get_stats/get_atomrefs — the provider or the datamodule). The strict no-estimation atomref case goes through the stats source's estimate=False mode, so the side-channel atomrefs= argument and the dataset's atomref read in initialize_transforms are gone. The datamodule() hooks are untouched in this commit. Co-Authored-By: Claude Fable 5 <[email protected]>
AtomisticModel.initialize_transforms now calls the unified initialize(stats) with the datamodule, which satisfies the stats-source protocol and shares one cached provider with the data-pipeline transforms. The per-transform datamodule() hooks and the base-class hook are removed (hard removal: this release already breaks the datamodule constructor API, so downstream transforms migrate once). The ported end-to-end tests (late postprocessor initialization) pin the model path to identical raw-training-data offsets. Co-Authored-By: Claude Fable 5 <[email protected]>
The datamodule computes a short deterministic fingerprint of the train index list (sorted, plus dataset length) whenever it creates or loads a split. No consumer yet; it will key the persisted statistics. Co-Authored-By: Claude Fable 5 <[email protected]>
The provider takes an optional stats file plus the train-partition fingerprint. On an in-memory miss it first reads the file; entries are keyed by the query triple and only valid if the stored fingerprint matches. Computed entries are written back in a read-modify-write under the same inter-process lock as split creation, so concurrent DDP ranks compute each entry at most once. The datamodule derives the default stats path from split_file (<split>_stats.npz); stats_file=None disables persistence entirely. Co-Authored-By: Claude Fable 5 <[email protected]>
Same stats file, same fingerprint rule, keyed per property and extensivity flag. Dataset-provided atomrefs are never persisted — they already live in the database metadata. Co-Authored-By: Claude Fable 5 <[email protected]>
Add estimate_atomrefs and StatsAtomrefProvider to the data API docs, describe the datamodule's statistics ownership and on-disk persistence in the user guide overview, and sweep the remaining docstrings that referenced the removed datamodule() hook path. Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Hold the inter-process lock across the whole miss-compute-write cycle, so concurrent DDP ranks compute each entry at most once instead of merely serializing their writes. The lock file name becomes a shared constant (SPLITTING_LOCK) so the datamodule's split creation and the provider's stats persistence cannot drift apart. Co-Authored-By: Claude Fable 5 <[email protected]>
A cache file left corrupt by a crash mid-write must never break setup(): load failures fall back to recompute-and-overwrite. Co-Authored-By: Claude Fable 5 <[email protected]>
np.savez appends ".npz" to filenames missing it, while the read path checked the verbatim path — a user-supplied stats_file without the extension was written but never read back, silently recomputing statistics on every run. Normalize the path once in the provider. Co-Authored-By: Claude Fable 5 <[email protected]>
The plan's testing decision reads "regenerated split -> fingerprint mismatch -> recompute"; cover it literally at the datamodule level: delete split.npz, set up a fresh datamodule with a different partition, and assert the stored statistics are recomputed rather than served. Co-Authored-By: Claude Fable 5 <[email protected]>
Collaborator
Author
Review summary (post-implementation audit)Audited the branch against Verified:
Found & fixed (pushed to this branch):
Remaining review notes are style-level judgement calls (mirror-image Verdict: ready to undraft. 🤖 Generated with Claude Code |
stefaanhessmann
marked this pull request as ready for review
July 22, 2026 15:53
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.
Follow-up to the dataset refactor (#781), executing the statistics refactor plan (REVIEW_FIXES.md items S1/D3). Statistics become a pure function of (base dataset, train indices) with a single owner and one query interface, and are persisted next to the split file.
What changed
calculate_stats/estimate_atomrefstake an optionalindicesparameter and build their own raw (transform-free, split-free) view internally. "Statistics are computed on raw data" is now the functions' contract, enforced in one place; the shallow-copy trick in the provider is deleted.initialize(stats)taking any object withget_stats/get_atomrefs(the provider or the datamodule). TheTransform.datamodule()hook is hard-deleted;AtomisticModel.initialize_transformspasses the datamodule as the stats source. The strict no-estimation atomref case is an explicitestimate=Falsemode of the atomref query, replacing the side-channelatomrefs=argument.<split>_stats.npz, keyed by a fingerprint of the train partition, read-modify-written while holding the same inter-process lock as split creation (held across miss→compute→write, so concurrent DDP ranks compute each entry at most once). On by default;stats_file=Noneopts out. Dataset-provided atomrefs are never persisted. Unreadable cache files fall back to recompute-and-overwrite.The cornerstone test (commit 1) ports the review's end-to-end S1 check into the suite:
RemoveOffsetsand a late-initializedAddOffsetspostprocessor must hold the identical, independently recomputed raw training mean. It passes on the base branch and on every commit of this PR.Commits
Ten planned commits (tests-first, each leaving the suite green — verified by running the full suite at every commit), plus black formatting, two hardening fixes from a pre-PR review pass (lock held across compute; corrupt-cache tolerance), and two commits from a post-implementation audit: a stats-file extension fix (a user-supplied
stats_filewithout.npzwas written bynp.savezunder the appended extension but read back at the verbatim path, so the cache silently never hit) and a literal regenerated-split invalidation test.Known limitations (deliberate)
split.npz. Delete the stats file or setstats_file=Nonein that case.calculate_stats, which reduces each property to scalar mean/std.Test plan
pytest tests/: 86 passed, 3 skipped (base branch: 60 passed, 3 skipped); suite verified green at each individual commit.black --check .clean with the pinned black==24.4.2..npzstats-file round-trip.🤖 Generated with Claude Code