One function

EpochShuffledBatchSampler in gepa-ai/gepa

The author described this change as Fix batch sampler so it updates when data-loader size increases and add tests to validate logic works. It counts as a record because the checks below fail on the code as it stood at 7604b32be and pass on 2f825d627, with nothing else changed between the two runs.

Fix saved2025-10-23
Sharing licenceMIT · LICENSE
Change size+20 3

What the code was meant to do, written into the code itself as a docstring

Mirrors the original batching logic: - Shuffle ids each epoch - Pad to minibatch size with least frequent ids - Deterministic via state.rng1

The change

1111 self.shuffled_ids: list[DataId] = []
1212 self.epoch = -1
1313 self.id_freqs = Counter()
14+ self.last_trainset_size = 0
1415 if rng is None:
1516 self.rng = random.Random(0)
1617 else:
1920 def _update_shuffled(self, loader: DataLoader[DataId, DataInst]):
2021 all_ids = list(loader.all_ids())
2122 trainset_size = len(loader)
23+ self.last_trainset_size = trainset_size
24+
25+ if trainset_size == 0:
26+ self.shuffled_ids = []
27+ self.id_freqs = Counter()
28+ return
29+
2230 self.shuffled_ids = list(all_ids)
2331 self.rng.shuffle(self.shuffled_ids)
24- for i in self.shuffled_ids:
25- self.id_freqs[i] += 1
32+ self.id_freqs = Counter(self.shuffled_ids)
2633
2734 mod = trainset_size % self.minibatch_size
2835 num_to_pad = (self.minibatch_size - mod) if mod != 0 else 0
3340 self.id_freqs[selected_id] += 1
3441
3542 def next_minibatch_ids(self, loader: DataLoader[DataId, DataInst], state: GEPAState) -> list[DataId]:
43+ trainset_size = len(loader)
44+ if trainset_size == 0:
45+ raise ValueError("Cannot sample a minibatch from an empty loader.")
46+
3647 base_idx = state.i * self.minibatch_size
3748 curr_epoch = 0 if self.epoch == -1 else base_idx // max(len(self.shuffled_ids), 1)
38- if curr_epoch > self.epoch:
49+
50+ needs_refresh = (
51+ not self.shuffled_ids
52+ or trainset_size != self.last_trainset_size
53+ or curr_epoch > self.epoch
54+ )
55+ if needs_refresh:
3956 self.epoch = curr_epoch
4057 self._update_shuffled(loader)
4158

The check that tells the two apart

failpass·tests/test_batch_sampler.py::test_epoch_sampler_errors_when_loader_empty
failpass·tests/test_batch_sampler.py::test_epoch_sampler_refreshes_when_loader_expands

Check file tests/test_batch_sampler.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.

Origin and history

The code before it7604b32bea8e96b9ad594f6056075b484b680dba
Broken version dated2025-10-23
Modulegepa.strategies.batch_sampler
Units changedEpochShuffledBatchSampler
Fingerprint447fa5bae7a5125a
Checked2026-08-18 by goldset/0.1

Every field above is generated by our program. None of it is written by hand.

Other bugs found in gepa-ai/gepa