Skip to content

Commit ddffafe

Browse files
committed
Fix up a few issues with variable image/batch size scheduler
1 parent 8bc1023 commit ddffafe

6 files changed

Lines changed: 209 additions & 94 deletions

File tree

tests/test_naflex_dataset.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import warnings
2+
from types import SimpleNamespace
3+
14
import torch
25
from torch.utils.data import DataLoader, Dataset
36

47
from timm.data import NaFlexMapDatasetWrapper
58

69

710
class _TensorImageDataset(Dataset):
8-
911
def __init__(self, length=32):
1012
self.length = length
1113

@@ -68,19 +70,29 @@ def test_naflex_persistent_workers_read_shared_epoch():
6870
dataset.set_epoch(1)
6971
epoch_1_indices = _loader_indices(loader)
7072

71-
expected_epoch_0 = [
72-
index
73-
for _, _, indices in dataset._prepare_epoch_batches(0)
74-
for index in indices
75-
]
76-
expected_epoch_1 = [
77-
index
78-
for _, _, indices in dataset._prepare_epoch_batches(1)
79-
for index in indices
80-
]
73+
expected_epoch_0 = [index for _, _, indices in dataset._prepare_epoch_batches(0) for index in indices]
74+
expected_epoch_1 = [index for _, _, indices in dataset._prepare_epoch_batches(1) for index in indices]
8175
assert epoch_0_indices == expected_epoch_0
8276
assert epoch_1_indices == expected_epoch_1
8377
assert epoch_1_indices != epoch_0_indices
8478
finally:
8579
if loader._iterator is not None:
8680
loader._iterator._shutdown_workers()
81+
82+
83+
def test_naflex_epoch_prep_warnings_only_emitted_by_worker_zero(monkeypatch):
84+
dataset = _create_naflex_dataset()
85+
86+
def prepare(_epoch):
87+
warnings.warn('epoch prep mismatch')
88+
return []
89+
90+
dataset._prepare_epoch_batches = prepare
91+
92+
for worker_id, expected_warnings in ((0, 1), (1, 0)):
93+
worker_info = SimpleNamespace(id=worker_id, num_workers=2)
94+
monkeypatch.setattr(torch.utils.data, 'get_worker_info', lambda: worker_info)
95+
with warnings.catch_warnings(record=True) as caught:
96+
warnings.simplefilter('always')
97+
list(dataset)
98+
assert len(caught) == expected_warnings

tests/test_scheduled_sampler.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010

1111
class _ImageDataset(Dataset):
12-
1312
def __init__(self, length=64, image_size=48):
1413
self.length = length
1514
self.image_size = image_size
@@ -55,7 +54,6 @@ def test_scheduled_batch_sampler_stable_epoch_length_and_composition():
5554

5655
def test_sample_budget_schedule_is_created_once_and_cached():
5756
class TrackingScheduledBatchSampler(ScheduledBatchSampler):
58-
5957
def __init__(self, *args, **kwargs):
6058
self.schedule_create_count = 0
6159
super().__init__(*args, **kwargs)
@@ -75,6 +73,60 @@ def _create_sample_budget_schedule(self):
7573
assert batch_sampler.schedule_create_count == 1
7674

7775

76+
def test_zero_weight_choices_are_excluded_from_progressive_random_mix_and_sample_budget():
77+
progressive_sampler = ScheduledBatchSampler(
78+
SequentialSampler(range(64)),
79+
batch_sizes=(16, 8, 4),
80+
choice_weights=(1, 0, 0),
81+
choice_schedule='progressive',
82+
schedule_epochs=3,
83+
schedule_random_mix=0.1,
84+
)
85+
86+
for epoch in range(3):
87+
assert progressive_sampler.choice_weights_for_epoch(epoch)[1:].tolist() == [0, 0]
88+
89+
snap_sampler = ScheduledBatchSampler(
90+
SequentialSampler(range(64)),
91+
batch_sizes=(16, 12, 8, 4),
92+
choice_weights=(1, 0, 0, 1),
93+
choice_schedule='progressive',
94+
schedule_epochs=4,
95+
schedule_spread=0,
96+
schedule_random_mix=0,
97+
)
98+
assert snap_sampler.choice_weights_for_epoch(1).tolist() == [1, 0, 0, 0]
99+
assert snap_sampler.choice_weights_for_epoch(2).tolist() == [0, 0, 0, 1]
100+
101+
sample_budget_sampler = ScheduledBatchSampler(
102+
SequentialSampler(range(12)),
103+
batch_sizes=(8, 4),
104+
choice_weights=(1, 0),
105+
)
106+
batches = list(sample_budget_sampler)
107+
assert _batch_signature(batches) == [(0, 8)]
108+
109+
110+
def test_progressive_schedule_rejects_when_no_full_batch_fits():
111+
with pytest.raises(ValueError, match='No full scheduled batch'):
112+
ScheduledBatchSampler(
113+
SequentialSampler(range(3)),
114+
batch_sizes=(8, 4),
115+
choice_schedule='progressive',
116+
schedule_epochs=3,
117+
)
118+
119+
120+
def test_sample_budget_schedule_rejects_when_no_full_batch_fits():
121+
with pytest.raises(ValueError, match='No full scheduled batch'):
122+
ScheduledBatchSampler(SequentialSampler(range(3)), batch_sizes=(8, 4))
123+
124+
125+
def test_scheduled_batch_sampler_rejects_empty_sampler():
126+
with pytest.raises(ValueError, match='non-empty sampler'):
127+
ScheduledBatchSampler(SequentialSampler(range(0)), batch_sizes=(8, 4))
128+
129+
78130
def test_scheduled_batch_sampler_distributed_shapes_match():
79131
dataset = list(range(96))
80132
rank_0_sampler = DistributedSampler(dataset, num_replicas=2, rank=0, shuffle=True, seed=11)
@@ -123,13 +175,13 @@ def test_progressive_schedule_moves_choices_and_preserves_constant_batch_budget(
123175
assert [index for batch in epoch_0 for index, _ in batch] == list(range(1000))
124176
assert [index for batch in epoch_4 for index, _ in batch] == list(range(1000))
125177
assert sum(choice for choice, _ in _batch_signature(epoch_0)) < sum(
126-
choice for choice, _ in _batch_signature(epoch_4)
178+
choice
179+
for choice, _ in _batch_signature(epoch_4)
127180
)
128181

129182

130183
def test_progressive_schedule_is_created_when_iteration_starts():
131184
class TrackingScheduledBatchSampler(ScheduledBatchSampler):
132-
133185
def __init__(self, *args, **kwargs):
134186
self.created_epochs = []
135187
super().__init__(*args, **kwargs)
@@ -168,19 +220,21 @@ def test_progressive_schedule_infers_policy_average_batch_budget_and_cycles_indi
168220
schedule_random_mix=0.1,
169221
)
170222
batch_sizes = torch.tensor(batch_sampler.batch_sizes, dtype=torch.float64)
171-
expected_average = torch.stack([
172-
torch.dot(batch_sampler.choice_weights_for_epoch(epoch), batch_sizes)
173-
for epoch in range(5)
174-
]).mean().item()
223+
expected_average = (
224+
torch
225+
.stack([torch.dot(batch_sampler.choice_weights_for_epoch(epoch), batch_sizes) for epoch in range(5)])
226+
.mean()
227+
.item()
228+
)
175229

176230
assert batch_sampler.average_batch_size == pytest.approx(expected_average)
177231
assert len(batch_sampler) == int(len(sampler) / expected_average)
178232

179233
epoch_0 = list(batch_sampler)
180234
epoch_0_indices = [index for batch in epoch_0 for index, _ in batch]
181235
assert len(epoch_0_indices) > len(sampler)
182-
assert epoch_0_indices[:len(sampler)] == list(range(len(sampler)))
183-
assert epoch_0_indices[len(sampler):] == list(range(len(epoch_0_indices) - len(sampler)))
236+
assert epoch_0_indices[: len(sampler)] == list(range(len(sampler)))
237+
assert epoch_0_indices[len(sampler) :] == list(range(len(epoch_0_indices) - len(sampler)))
184238

185239
batch_sampler.set_epoch(4)
186240
epoch_4 = list(batch_sampler)
@@ -190,10 +244,7 @@ def test_progressive_schedule_infers_policy_average_batch_budget_and_cycles_indi
190244

191245
def test_progressive_schedule_distributed_shapes_match_and_advance():
192246
dataset = list(range(96))
193-
samplers = [
194-
DistributedSampler(dataset, num_replicas=2, rank=rank, shuffle=True, seed=11)
195-
for rank in range(2)
196-
]
247+
samplers = [DistributedSampler(dataset, num_replicas=2, rank=rank, shuffle=True, seed=11) for rank in range(2)]
197248
batch_samplers = [
198249
ScheduledBatchSampler(
199250
sampler,
@@ -220,16 +271,12 @@ def test_progressive_schedule_distributed_shapes_match_and_advance():
220271
for rank_batches in (*epoch_0, *epoch_2):
221272
assert len(rank_batches) == 8
222273
for rank_epochs in (epoch_0, epoch_2):
223-
rank_indices = [
224-
{index for batch in rank_batches for index, _ in batch}
225-
for rank_batches in rank_epochs
226-
]
274+
rank_indices = [{index for batch in rank_batches for index, _ in batch} for rank_batches in rank_epochs]
227275
assert rank_indices[0].isdisjoint(rank_indices[1])
228276

229277

230278
def test_scheduled_transform_dataset_preserves_sample_fields():
231279
class ExtraFieldDataset(Dataset):
232-
233280
def __getitem__(self, index):
234281
return index, index + 1, f'sample-{index}'
235282

@@ -349,6 +396,30 @@ def test_create_loader_rejects_scheduled_resolutions_with_multi_epochs_loader():
349396
)
350397

351398

399+
@pytest.mark.parametrize(
400+
'scheduled_option',
401+
(
402+
{'batch_choice_seed': 7},
403+
{'batch_schedule_epochs': 3},
404+
{'batch_schedule_spread': 0.5},
405+
{'batch_schedule_random_mix': 0.2},
406+
),
407+
)
408+
def test_create_loader_rejects_scheduled_options_without_input_size_choices(scheduled_option):
409+
with pytest.raises(ValueError, match='input_size_choices'):
410+
create_loader(
411+
_ImageDataset(length=32),
412+
input_size=(3, 32, 32),
413+
batch_size=4,
414+
is_training=True,
415+
no_aug=True,
416+
use_prefetcher=False,
417+
num_workers=0,
418+
persistent_workers=False,
419+
**scheduled_option,
420+
)
421+
422+
352423
def test_create_loader_standard_batching_path_is_unchanged():
353424
dataset = _ImageDataset()
354425
loader = create_loader(

timm/data/loader.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ def create_loader(
302302
batch_choice_schedule: Choice schedule mode, either ``constant`` or ``progressive``.
303303
batch_schedule_epochs: Number of epochs over which a progressive schedule moves through its choices.
304304
batch_schedule_spread: Standard deviation of the progressive choice window, in choice-index units.
305-
batch_schedule_random_mix: Fraction of uniform random exploration in a progressive schedule.
305+
batch_schedule_random_mix: Fraction of uniform random exploration over nonzero-weight choices in a
306+
progressive schedule.
306307
num_batches: Fixed number of loader batches per epoch. Inferred from the schedule-average batch size
307308
for progressive schedules when not specified.
308309
@@ -387,7 +388,11 @@ def create_loader(
387388
if (
388389
batch_size_choices is not None
389390
or batch_choice_weights is not None
391+
or batch_choice_seed != 0
390392
or batch_choice_schedule != 'constant'
393+
or batch_schedule_epochs is not None
394+
or batch_schedule_spread != 0.65
395+
or batch_schedule_random_mix != 0.1
391396
or num_batches is not None
392397
):
393398
raise ValueError('input_size_choices must be specified when using scheduled batch options.')

timm/data/naflex_dataset.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,13 @@ def __iter__(self) -> Iterator[Tuple[Dict[str, torch.Tensor], torch.Tensor]]:
512512

513513
# Snapshot the shared epoch once, then build process-local derived state.
514514
epoch = self.shared_epoch.value
515-
batches_for_worker = self._prepare_epoch_batches(epoch)[worker_id::num_workers]
515+
if worker_id == 0:
516+
epoch_batches = self._prepare_epoch_batches(epoch)
517+
else:
518+
with warnings.catch_warnings():
519+
warnings.simplefilter('ignore')
520+
epoch_batches = self._prepare_epoch_batches(epoch)
521+
batches_for_worker = epoch_batches[worker_id::num_workers]
516522
for seq_len, patch_idx, indices in batches_for_worker:
517523
if not indices: # Skip if a batch ended up with no indices (shouldn't happen often)
518524
continue

0 commit comments

Comments
 (0)