-
Notifications
You must be signed in to change notification settings - Fork 6
chore: sampler serialization tests #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
d2405fd
b39bb6c
64846b2
4d8b3b6
d569be8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,18 @@ | |
| from __future__ import annotations | ||
|
|
||
| import math | ||
| import pickle | ||
| import sys | ||
| from functools import partial | ||
| from typing import TYPE_CHECKING | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| import pytest | ||
|
|
||
| from annbatch.abc import Sampler | ||
| from annbatch.samplers import DistributedSampler, RandomSampler, SequentialSampler | ||
| from annbatch.samplers import ClassSampler, DistributedSampler, RandomSampler, SequentialSampler | ||
| from annbatch.samplers._utils import WorkerInfo | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -879,3 +881,75 @@ def test_wraps_sequential_sampler(self, make_distributed_sampler: Callable[..., | |
| for j in range(i + 1, world_size): | ||
| assert set(all_indices[i]).isdisjoint(set(all_indices[j])) | ||
| assert set().union(*all_indices) == set(range(n_obs)) | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Serialization / reproducibility (all implemented samplers) | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| def _make_sampler(kind: str, seed: int, n_obs: int) -> Sampler: | ||
| """Build the sampler identified by ``kind``, seeded with ``seed``, sized for ``n_obs`` obs.""" | ||
| match kind: | ||
| case "random": | ||
| return RandomSampler(chunk_size=10, preload_nchunks=2, batch_size=5, rng=np.random.default_rng(seed)) | ||
| case "sequential": | ||
| return SequentialSampler(chunk_size=10, preload_nchunks=2, batch_size=5) | ||
| case "class": | ||
| # n_obs obs, 5 classes, each a contiguous run of n_obs // 5 (>= chunk_size) | ||
| classes = pd.Categorical(np.repeat(np.arange(5), n_obs // 5)) | ||
| return ClassSampler( | ||
| chunk_size=10, | ||
| preload_nchunks=2, | ||
| batch_size=5, | ||
| classes=classes, | ||
| num_samples=50, | ||
| rng=np.random.default_rng(seed), | ||
| ) | ||
| case "distributed": | ||
| inner = RandomSampler(chunk_size=10, preload_nchunks=2, batch_size=5, rng=np.random.default_rng(seed)) | ||
| # dist_info is only called at construction (rank 0 of 1) and is not stored, | ||
| # so the resulting sampler stays picklable. | ||
| return DistributedSampler(inner, dist_info=lambda: (0, 1)) | ||
| case _: | ||
| raise ValueError(f"unknown sampler kind {kind!r}") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("kind", ["random", "sequential", "class", "distributed"]) | ||
| def test_sampler_is_serializable_and_reproducible(kind: str): | ||
| """Every sampler pickles and its random *state* (not just the seed) round-trips.""" | ||
| n_obs = 100 | ||
|
|
||
| def advance_pickle_indices(seed: int) -> list[int]: | ||
| sampler = _make_sampler(kind, seed, n_obs) | ||
| collect_indices(sampler, n_obs) # advance the rng one pass | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to advance the rng first here and below?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because this also tests randomness. For example maybe we override the rng to always to np.default(0). The tests would still pass otherwise. I mean we already test randomness somewhere else but I wrote it in the very small chance that the seed I set and whatever seed could be set in the case of such a faulty override would be the same
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't follow - if you provide a seed explicitly, why does it matter how many times you advance state before/after making a copy?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah you are right sorry. |
||
| restored = pickle.loads(pickle.dumps(sampler)) | ||
| indices, _, _ = collect_indices(restored, n_obs) | ||
| return indices | ||
|
|
||
| sampler = _make_sampler(kind, seed=0, n_obs=n_obs) | ||
| fresh_indices, _, _ = collect_indices(_make_sampler(kind, seed=0, n_obs=n_obs), n_obs) | ||
|
|
||
| # advance the rng by one full pass, then snapshot via pickle | ||
| collect_indices(sampler, n_obs) | ||
| restored = pickle.loads(pickle.dumps(sampler)) | ||
| assert isinstance(restored, type(sampler)) | ||
|
|
||
| restored_indices, _, _ = collect_indices(restored, n_obs) | ||
| original_indices, _, _ = collect_indices(sampler, n_obs) | ||
| assert restored_indices, "sampler produced no indices" | ||
|
|
||
| assert restored_indices == original_indices | ||
|
|
||
| if restored.shuffle: | ||
| # tests for the controlled stochastic behavior | ||
| # without these tests, passing this test becomes | ||
| # trivial if there is a seed collapse etc. | ||
|
|
||
| # check if the state is not reset: an advanced-then-saved | ||
| # stochastic sampler must not match a fresh one | ||
| assert restored_indices != fresh_indices | ||
| # check if saved state tracks the seed: identical for a | ||
| # same-seed run, different otherwise | ||
| assert restored_indices == advance_pickle_indices(seed=0) | ||
| assert restored_indices != advance_pickle_indices(seed=1) | ||
Uh oh!
There was an error while loading. Please reload this page.