chugr 0.2.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chugr/__init__.py +46 -0
- chugr/app/test.py +105 -0
- chugr/common/__init__.py +9 -0
- chugr/common/cli.py +234 -0
- chugr/common/collate.py +104 -0
- chugr/common/config.py +335 -0
- chugr/common/random.py +76 -0
- chugr/common/serialize.py +150 -0
- chugr/common/task_config.py +24 -0
- chugr/common/types.py +122 -0
- chugr/common/urls.py +88 -0
- chugr/doc/__init__.py +17 -0
- chugr/doc/constants.py +15 -0
- chugr/doc/doc_processor.py +248 -0
- chugr/doc/doc_read_processor.py +249 -0
- chugr/doc/doc_vqa_processor.py +113 -0
- chugr/doc/synth_text.py +212 -0
- chugr/hfds/__init__.py +2 -0
- chugr/hfds/collate.py +83 -0
- chugr/hfds/loader.py +385 -0
- chugr/hfds/wrappers.py +108 -0
- chugr/image/__init__.py +4 -0
- chugr/image/build_transforms_doc.py +427 -0
- chugr/image/build_transforms_image.py +203 -0
- chugr/image/transforms_alb.py +112 -0
- chugr/image/transforms_factory.py +58 -0
- chugr/image/transforms_torch.py +137 -0
- chugr/loader.py +249 -0
- chugr/naflex.py +357 -0
- chugr/task_pipeline/__init__.py +7 -0
- chugr/task_pipeline/pipeline_doc_read.py +66 -0
- chugr/task_pipeline/pipeline_doc_vqa.py +124 -0
- chugr/task_pipeline/pipeline_factory.py +32 -0
- chugr/task_pipeline/pipeline_gtparse.py +93 -0
- chugr/task_pipeline/pipeline_image_text.py +90 -0
- chugr/task_pipeline/pipeline_manual.py +27 -0
- chugr/text/__init__.py +1 -0
- chugr/text/tokenization.py +222 -0
- chugr/version.py +1 -0
- chugr/wds/__init__.py +7 -0
- chugr/wds/dataset_info.py +65 -0
- chugr/wds/decode.py +319 -0
- chugr/wds/filters.py +142 -0
- chugr/wds/helpers.py +68 -0
- chugr/wds/loader.py +187 -0
- chugr/wds/pipeline.py +182 -0
- chugr/wds/shardlists.py +134 -0
- chugr/wds/tariterators.py +70 -0
- chugr-0.2.0.dev0.dist-info/METADATA +436 -0
- chugr-0.2.0.dev0.dist-info/RECORD +53 -0
- chugr-0.2.0.dev0.dist-info/WHEEL +4 -0
- chugr-0.2.0.dev0.dist-info/entry_points.txt +4 -0
- chugr-0.2.0.dev0.dist-info/licenses/LICENSE +201 -0
chugr/wds/loader.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import math
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Callable, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
import torch.utils.data
|
|
7
|
+
import webdataset as wds
|
|
8
|
+
|
|
9
|
+
from chugr.common import DistributedCfg, LoaderBundle, SharedCount, ShardSpec, rank_seed, seed_worker
|
|
10
|
+
from .helpers import expand_urls
|
|
11
|
+
from .pipeline import build_data_pipeline
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_SAMPLE_SHUFFLE_SIZE = 2000
|
|
15
|
+
_SAMPLE_SHUFFLE_INITIAL = 500
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_loader_wds(
|
|
19
|
+
shards: Union[str, List[str], ShardSpec],
|
|
20
|
+
task_pipeline: Optional[List[Callable]],
|
|
21
|
+
num_samples: Optional[int] = None,
|
|
22
|
+
is_training: bool = False,
|
|
23
|
+
batch_size: Optional[int] = 1,
|
|
24
|
+
resampled: bool = False,
|
|
25
|
+
multi_interval: bool = True,
|
|
26
|
+
num_batches_round: str = 'ceil',
|
|
27
|
+
num_workers: int = 4,
|
|
28
|
+
persistent_workers: bool = True,
|
|
29
|
+
start_interval: int = 0,
|
|
30
|
+
seed: int = 0,
|
|
31
|
+
handler: Callable = wds.reraise_exception,
|
|
32
|
+
collate_fn: Optional[Callable] = None,
|
|
33
|
+
sample_shuffle_size: int = _SAMPLE_SHUFFLE_SIZE,
|
|
34
|
+
sample_shuffle_initial: int = _SAMPLE_SHUFFLE_INITIAL,
|
|
35
|
+
distributed: DistributedCfg = DistributedCfg(),
|
|
36
|
+
naflex_scheduler=None,
|
|
37
|
+
naflex_stage_factory: Optional[Callable] = None,
|
|
38
|
+
):
|
|
39
|
+
""" Create a webdataset loader
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
shards:
|
|
43
|
+
task_pipeline:
|
|
44
|
+
is_training:
|
|
45
|
+
resampled: If True, shards are resampled with replacement.
|
|
46
|
+
multi_interval: If True, run loader in multi-interval mode (multi-epoch), num_samples is interpreted
|
|
47
|
+
as num_samples per interval (epoch if # samples == dataset length). Dataset length is set to a
|
|
48
|
+
fixed value to approximate at-least once sample visiting per interval.
|
|
49
|
+
If False, num_samples is treated as total samples to visit over training without paying attention
|
|
50
|
+
to # samples in underlying dataset. Dataset length is not accessible.
|
|
51
|
+
start_interval:
|
|
52
|
+
seed:
|
|
53
|
+
num_workers:
|
|
54
|
+
persistent_workers:
|
|
55
|
+
batch_size:
|
|
56
|
+
num_batches_round:
|
|
57
|
+
collate_fn:
|
|
58
|
+
sample_shuffle_size:
|
|
59
|
+
sample_shuffle_initial:
|
|
60
|
+
distributed:
|
|
61
|
+
naflex_scheduler: NaFlexBatchScheduler for schedule driven (variable seq_len & batch size) batching
|
|
62
|
+
naflex_stage_factory: interval counter -> pipeline stages (bucketing, NaFlexBatcher) replacing the
|
|
63
|
+
task pipeline and fixed size batching
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
resampled = resampled and is_training
|
|
69
|
+
|
|
70
|
+
if not isinstance(shards, ShardSpec):
|
|
71
|
+
if isinstance(shards, str):
|
|
72
|
+
shards = expand_urls(shards)
|
|
73
|
+
shards = ShardSpec(
|
|
74
|
+
urls=shards,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
num_shards = len(shards.urls)
|
|
78
|
+
# eval, an explicit num_samples below the (known) dataset size caps the eval set, otherwise all samples are used
|
|
79
|
+
max_samples = None
|
|
80
|
+
if not is_training and num_samples and not (shards.sizes and num_samples >= sum(shards.sizes)):
|
|
81
|
+
max_samples = num_samples
|
|
82
|
+
if num_samples is None:
|
|
83
|
+
if shards.sizes:
|
|
84
|
+
num_samples = sum(shards.sizes)
|
|
85
|
+
if is_training and not num_samples:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
'The number of dataset samples must be specified for the training dataset '
|
|
88
|
+
'if no dataset length info is present.')
|
|
89
|
+
|
|
90
|
+
num_batches_per_worker = 0
|
|
91
|
+
num_workers_nonzero = max(num_workers, 1)
|
|
92
|
+
if is_training and naflex_scheduler is not None:
|
|
93
|
+
# schedule driven batching, each worker yields its share of the rank identical schedule per interval,
|
|
94
|
+
# the sample stream must not run dry -> resampled shards, or (w/o replacement) repeating shard lists
|
|
95
|
+
num_batches = naflex_scheduler.num_batches_for_workers(num_workers_nonzero)
|
|
96
|
+
num_samples = naflex_scheduler.num_samples_for_workers(num_workers_nonzero)
|
|
97
|
+
batch_size = 0
|
|
98
|
+
elif is_training:
|
|
99
|
+
assert batch_size >= 1, 'batching must be enabled for train, set batch_size>=1'
|
|
100
|
+
# We want to see the same # of batches on each member of the distributed group (GPU),
|
|
101
|
+
# this is enforced by making each worker produce the same # of batches regardless of the
|
|
102
|
+
# underlying iterator, so we estimate and make the iterator wrap around if end is hit.
|
|
103
|
+
# This will repeat some samples and may miss some sample per interval as shards may be
|
|
104
|
+
# uneven or allocated unevenly across all workers. There are ways improve on this naive
|
|
105
|
+
# approach, to get closer to the ideal of each sample in an interval (epoch) seen once,
|
|
106
|
+
# but difficult to achieve perfectly, and most improvements require full co-ordination across
|
|
107
|
+
# all workers via out-of-band IPC/RPC.
|
|
108
|
+
|
|
109
|
+
# roll over and repeat a few samples to get same number of full batches on each node
|
|
110
|
+
round_fn = math.floor if num_batches_round == 'floor' else math.ceil
|
|
111
|
+
global_batch_size = batch_size * distributed.world_size
|
|
112
|
+
num_batches = round_fn(num_samples / global_batch_size)
|
|
113
|
+
num_batches_per_worker = round_fn(num_batches / num_workers_nonzero) # per dataloader worker
|
|
114
|
+
num_batches = num_batches_per_worker * num_workers_nonzero
|
|
115
|
+
num_samples = num_batches * global_batch_size
|
|
116
|
+
else:
|
|
117
|
+
# Eval / inference will exhaust the iterator if the size is not specified.
|
|
118
|
+
# Samples (or shards, w/ enough of them and no sample cap) are split across ranks, the counts per rank
|
|
119
|
+
# differ, and ranks may see different numbers of (partial) batches, num_batches is an estimate. Eval
|
|
120
|
+
# loops must not run collectives per batch.
|
|
121
|
+
num_samples = num_samples or 0
|
|
122
|
+
if batch_size:
|
|
123
|
+
num_batches = math.ceil(math.ceil(num_samples / distributed.world_size) / batch_size)
|
|
124
|
+
else:
|
|
125
|
+
num_batches = 0
|
|
126
|
+
|
|
127
|
+
if is_training:
|
|
128
|
+
# create a shared epoch store to sync epoch to dataloader worker proc
|
|
129
|
+
shared_interval_count = SharedCount(count=start_interval)
|
|
130
|
+
if not resampled:
|
|
131
|
+
assert num_shards >= num_workers_nonzero * distributed.world_size, 'number of shards must be >= total workers'
|
|
132
|
+
else:
|
|
133
|
+
shared_interval_count = None
|
|
134
|
+
|
|
135
|
+
if is_training and naflex_scheduler is not None:
|
|
136
|
+
task_pipeline = naflex_stage_factory(shared_interval_count)
|
|
137
|
+
|
|
138
|
+
# DataLoader worker seeds (resampled shards, sample shuffle, worker RNGs) must differ across ranks, the shard
|
|
139
|
+
# list shuffle (w/o resampling) uses the shared `seed` so the node / worker split stays consistent
|
|
140
|
+
worker_seed = rank_seed(seed, distributed.global_rank)
|
|
141
|
+
datapipe = build_data_pipeline(
|
|
142
|
+
shards=shards,
|
|
143
|
+
task_pipeline=task_pipeline,
|
|
144
|
+
is_training=is_training,
|
|
145
|
+
batch_size=batch_size,
|
|
146
|
+
resampled=resampled,
|
|
147
|
+
multi_interval=multi_interval,
|
|
148
|
+
seed=seed,
|
|
149
|
+
shared_interval_count=shared_interval_count,
|
|
150
|
+
num_batches_per_worker=num_batches_per_worker,
|
|
151
|
+
sample_shuffle_initial=sample_shuffle_initial,
|
|
152
|
+
sample_shuffle_size=sample_shuffle_size,
|
|
153
|
+
handler=handler,
|
|
154
|
+
collate_fn=collate_fn,
|
|
155
|
+
max_samples=max_samples,
|
|
156
|
+
repeat_shards=is_training and naflex_scheduler is not None and not resampled,
|
|
157
|
+
worker_seed=worker_seed,
|
|
158
|
+
rank=distributed.global_rank,
|
|
159
|
+
world_size=distributed.world_size,
|
|
160
|
+
num_workers=num_workers,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
dl_generator = torch.Generator()
|
|
164
|
+
dl_generator.manual_seed(worker_seed)
|
|
165
|
+
|
|
166
|
+
dataloader = wds.WebLoader(
|
|
167
|
+
datapipe,
|
|
168
|
+
batch_size=None, # batching done in data-pipeline
|
|
169
|
+
shuffle=False, # shuffling done in data-pipeline
|
|
170
|
+
num_workers=num_workers,
|
|
171
|
+
persistent_workers=persistent_workers and num_workers > 0,
|
|
172
|
+
generator=dl_generator,
|
|
173
|
+
worker_init_fn=seed_worker,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return LoaderBundle(
|
|
177
|
+
loader=dataloader,
|
|
178
|
+
num_batches=num_batches,
|
|
179
|
+
num_samples=num_samples,
|
|
180
|
+
shared_interval=shared_interval_count,
|
|
181
|
+
mean_batch_size=(
|
|
182
|
+
naflex_scheduler.mean_batch_size_for_workers(num_workers_nonzero)
|
|
183
|
+
if is_training and naflex_scheduler is not None else None
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
chugr/wds/pipeline.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from itertools import islice
|
|
3
|
+
from typing import Callable, List, Optional, Sequence, Union
|
|
4
|
+
|
|
5
|
+
import torch.utils.data
|
|
6
|
+
import webdataset
|
|
7
|
+
import webdataset as wds
|
|
8
|
+
|
|
9
|
+
from chugr.common import ShardSpec, collate
|
|
10
|
+
from .filters import detshuffle_v2
|
|
11
|
+
from .shardlists import ResampledShardsV2, ShuffledShardList
|
|
12
|
+
from .tariterators import tarfile_to_samples_nothrow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def split_samples_by_node_and_worker(src, rank: int = 0, world_size: int = 1):
|
|
16
|
+
""" Split a sample stream that is identical on all ranks / workers, rank r keeps every world_size-th sample
|
|
17
|
+
(independent of the worker count), its workers split those. """
|
|
18
|
+
info = torch.utils.data.get_worker_info()
|
|
19
|
+
num_workers = info.num_workers if info is not None else 1
|
|
20
|
+
worker_id = info.id if info is not None else 0
|
|
21
|
+
yield from islice(islice(src, rank, None, world_size), worker_id, None, num_workers)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_data_pipeline(
|
|
25
|
+
shards: ShardSpec,
|
|
26
|
+
task_pipeline: List[Callable],
|
|
27
|
+
is_training: bool = False,
|
|
28
|
+
batch_size: int = 0,
|
|
29
|
+
resampled: bool = False,
|
|
30
|
+
multi_interval: bool = False,
|
|
31
|
+
seed: int = 0,
|
|
32
|
+
shared_interval_count: int = -1,
|
|
33
|
+
num_batches_per_worker: int = 0,
|
|
34
|
+
sample_shuffle_initial: int = 1,
|
|
35
|
+
sample_shuffle_size: int = 1,
|
|
36
|
+
collate_fn: Optional[Callable] = None,
|
|
37
|
+
batched_task_pipeline: Optional[List[Callable]] = None,
|
|
38
|
+
handler=wds.reraise_exception,
|
|
39
|
+
max_samples: Optional[int] = None,
|
|
40
|
+
repeat_shards: bool = False,
|
|
41
|
+
worker_seed: Optional[int] = None,
|
|
42
|
+
rank: int = 0,
|
|
43
|
+
world_size: int = 1,
|
|
44
|
+
num_workers: int = 0,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
task_pipeline: task-specific pipeline for processing samples
|
|
50
|
+
shards: ShardSpec w/ shard url list and optional sizes and weights
|
|
51
|
+
is_training: Train mode. Enables shuffling of samples and forces consistent batch #s across workers.
|
|
52
|
+
batch_size: Batch for sample collation. Collation / batching disabled if batch_size == 0.
|
|
53
|
+
resampled: Enable resampling with replacement of shards.
|
|
54
|
+
multi_interval:
|
|
55
|
+
seed:
|
|
56
|
+
shared_interval_count:
|
|
57
|
+
num_batches_per_worker:
|
|
58
|
+
sample_shuffle_initial:
|
|
59
|
+
sample_shuffle_size:
|
|
60
|
+
collate_fn:
|
|
61
|
+
batched_task_pipeline: An optional task-specific pipeline for processing batched samples
|
|
62
|
+
handler: Exception handler
|
|
63
|
+
max_samples: Eval, limit the eval set to the first `max_samples` samples of the shard list. Exact for any
|
|
64
|
+
number of ranks / workers, every worker reads those samples and keeps its share, so leave it unset
|
|
65
|
+
to evaluate all samples (split by shard where possible).
|
|
66
|
+
repeat_shards: Train w/o resampling, cycle through the shard list (reshuffled each pass)
|
|
67
|
+
worker_seed: Seed for per worker randomness (shard resampling, sample shuffle) when the seed can't be
|
|
68
|
+
recovered from DataLoader workers (num_workers=0), must differ across ranks. Defaults to `seed`.
|
|
69
|
+
rank: Rank of this process, shards (train, eval w/ enough shards) or samples (eval) are split across
|
|
70
|
+
ranks. Passed explicitly, not derived from RANK / WORLD_SIZE env vars or torch.distributed state
|
|
71
|
+
(neither is reliably visible in dataloader workers for all launchers / start methods).
|
|
72
|
+
world_size: Number of ranks
|
|
73
|
+
num_workers: Number of dataloader workers (eval split)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
worker_seed = seed if worker_seed is None else worker_seed
|
|
79
|
+
if not isinstance(task_pipeline, (list, tuple)):
|
|
80
|
+
task_pipeline = [task_pipeline]
|
|
81
|
+
assert len(task_pipeline)
|
|
82
|
+
|
|
83
|
+
if resampled:
|
|
84
|
+
datapipe = [ResampledShardsV2(
|
|
85
|
+
shards.urls,
|
|
86
|
+
weights=shards.weights,
|
|
87
|
+
deterministic=True,
|
|
88
|
+
interval=shared_interval_count,
|
|
89
|
+
seed=worker_seed,
|
|
90
|
+
)]
|
|
91
|
+
else:
|
|
92
|
+
assert shards.weights is None, \
|
|
93
|
+
"upsampling_factors is only supported when sampling with replacement (resampled=False)."
|
|
94
|
+
if is_training:
|
|
95
|
+
datapipe = [ShuffledShardList(
|
|
96
|
+
shards.urls,
|
|
97
|
+
seed=seed,
|
|
98
|
+
interval=shared_interval_count,
|
|
99
|
+
repeat=repeat_shards,
|
|
100
|
+
)]
|
|
101
|
+
else:
|
|
102
|
+
datapipe = [wds.SimpleShardList(
|
|
103
|
+
shards.urls,
|
|
104
|
+
)]
|
|
105
|
+
|
|
106
|
+
# at this point we have an iterator over all the shards
|
|
107
|
+
if is_training:
|
|
108
|
+
if not resampled:
|
|
109
|
+
if world_size > 1:
|
|
110
|
+
# as wds.split_by_node, w/ the explicit rank
|
|
111
|
+
datapipe.append(wds.slice(rank, None, world_size))
|
|
112
|
+
datapipe.append(wds.split_by_worker)
|
|
113
|
+
# at this point, we have an iterator over the shards assigned to each worker at each node
|
|
114
|
+
datapipe.extend([
|
|
115
|
+
tarfile_to_samples_nothrow(handler=handler),
|
|
116
|
+
detshuffle_v2(
|
|
117
|
+
bufsize=sample_shuffle_size,
|
|
118
|
+
initial=sample_shuffle_initial,
|
|
119
|
+
seed=worker_seed,
|
|
120
|
+
interval=shared_interval_count,
|
|
121
|
+
unique_worker=True,
|
|
122
|
+
)
|
|
123
|
+
# wds.shuffle(
|
|
124
|
+
# bufsize=sample_shuffle_size,
|
|
125
|
+
# initial=sample_shuffle_initial,
|
|
126
|
+
# ),
|
|
127
|
+
])
|
|
128
|
+
else:
|
|
129
|
+
if max_samples:
|
|
130
|
+
# capped eval set, the first max_samples samples of the shard list for any world size / worker count.
|
|
131
|
+
# Every worker reads (but doesn't decode / process) that head of the stream and keeps its share.
|
|
132
|
+
datapipe.extend([
|
|
133
|
+
wds.tarfile_to_samples(handler=handler),
|
|
134
|
+
wds.slice(max_samples),
|
|
135
|
+
partial(split_samples_by_node_and_worker, rank=rank, world_size=world_size),
|
|
136
|
+
])
|
|
137
|
+
elif len(shards.urls) >= world_size * max(num_workers, 1):
|
|
138
|
+
# enough shards for every worker of every rank, split shards so each is read once
|
|
139
|
+
if world_size > 1:
|
|
140
|
+
datapipe.append(wds.slice(rank, None, world_size))
|
|
141
|
+
datapipe.extend([
|
|
142
|
+
wds.split_by_worker,
|
|
143
|
+
wds.tarfile_to_samples(handler=handler),
|
|
144
|
+
])
|
|
145
|
+
else:
|
|
146
|
+
# few (or one) eval shards, split samples across ranks, every rank reads the samples of its workers'
|
|
147
|
+
# shards but only decodes / processes its share
|
|
148
|
+
datapipe.extend([
|
|
149
|
+
wds.split_by_worker,
|
|
150
|
+
wds.tarfile_to_samples(handler=handler),
|
|
151
|
+
])
|
|
152
|
+
if world_size > 1:
|
|
153
|
+
datapipe.append(wds.slice(rank, None, world_size))
|
|
154
|
+
|
|
155
|
+
# task specific decode and map pipline (per-sample)
|
|
156
|
+
datapipe.extend(task_pipeline)
|
|
157
|
+
|
|
158
|
+
# collation, tensor output disabled with batch_size == 0 or None
|
|
159
|
+
if batch_size:
|
|
160
|
+
# NOTE torch default_collate handles dicts, wds default_collate does not
|
|
161
|
+
collate_fn = collate_fn or collate
|
|
162
|
+
datapipe.extend([
|
|
163
|
+
wds.batched(
|
|
164
|
+
batch_size,
|
|
165
|
+
partial=not is_training,
|
|
166
|
+
collation_fn=collate_fn,
|
|
167
|
+
)
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
# task-specific batched pipeline (per-batch)
|
|
171
|
+
if batched_task_pipeline:
|
|
172
|
+
datapipe.extend(batched_task_pipeline)
|
|
173
|
+
|
|
174
|
+
datapipe = wds.DataPipeline(*datapipe)
|
|
175
|
+
|
|
176
|
+
if is_training and num_batches_per_worker > 0:
|
|
177
|
+
if multi_interval:
|
|
178
|
+
datapipe = datapipe.with_epoch(num_batches_per_worker) # each worker is iterating over this
|
|
179
|
+
else:
|
|
180
|
+
datapipe = datapipe.repeat(nbatches=num_batches_per_worker)
|
|
181
|
+
|
|
182
|
+
return datapipe
|
chugr/wds/shardlists.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from torch.utils.data import IterableDataset
|
|
5
|
+
|
|
6
|
+
from chugr.common import SharedCount, get_pytorch_worker_seed
|
|
7
|
+
from .helpers import expand_urls
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShuffledShardList(IterableDataset):
|
|
11
|
+
"""An iterable dataset yielding a list of urls that is deterministically shuffled based on epoch."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
urls,
|
|
16
|
+
seed=0,
|
|
17
|
+
interval=-1,
|
|
18
|
+
num_sub_intervals=None,
|
|
19
|
+
repeat: bool = False,
|
|
20
|
+
):
|
|
21
|
+
"""Iterate through the list of shards.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
repeat: cycle through the shards indefinitely (each pass reshuffled w/ a seed shared by all
|
|
25
|
+
ranks / workers so node / worker splitting stays consistent), for schedule driven loaders that
|
|
26
|
+
need an infinite sample stream w/o sampling shards with replacement.
|
|
27
|
+
"""
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.repeat = repeat
|
|
30
|
+
self.urls = expand_urls(urls)
|
|
31
|
+
assert len(self.urls) and isinstance(self.urls[0], str)
|
|
32
|
+
self.seed = seed
|
|
33
|
+
self.interval = interval
|
|
34
|
+
self.num_sub_intervals = num_sub_intervals # FIXME experimental feature
|
|
35
|
+
|
|
36
|
+
def __len__(self):
|
|
37
|
+
return len(self.urls)
|
|
38
|
+
|
|
39
|
+
def __iter__(self):
|
|
40
|
+
"""Return an iterator over the shards."""
|
|
41
|
+
urls = self.urls.copy()
|
|
42
|
+
|
|
43
|
+
# Set epoch
|
|
44
|
+
if isinstance(self.interval, SharedCount):
|
|
45
|
+
interval = self.interval.get_value()
|
|
46
|
+
else:
|
|
47
|
+
# NOTE: this is interval tracking is problematic in a multiprocess (dataloader workers or train)
|
|
48
|
+
# situation as different workers may wrap at different times (or not at all).
|
|
49
|
+
self.interval += 1
|
|
50
|
+
interval = self.interval
|
|
51
|
+
|
|
52
|
+
pass_idx = 0
|
|
53
|
+
while True:
|
|
54
|
+
pass_urls = list(urls)
|
|
55
|
+
if self.seed is not None:
|
|
56
|
+
# Shuffle with the same seed across all nodes/workers in each interval or super interval
|
|
57
|
+
if self.num_sub_intervals is None:
|
|
58
|
+
seed = self.seed + interval
|
|
59
|
+
else:
|
|
60
|
+
# Keep shuffling consistent across the super epochs
|
|
61
|
+
seed = self.seed + (interval // self.num_sub_intervals)
|
|
62
|
+
random.Random(seed * 1000003 + pass_idx if pass_idx else seed).shuffle(pass_urls)
|
|
63
|
+
|
|
64
|
+
# Restrict to shards in the sub epoch if needed
|
|
65
|
+
if self.num_sub_intervals is not None:
|
|
66
|
+
pass_urls = pass_urls[interval % self.num_sub_intervals::self.num_sub_intervals]
|
|
67
|
+
|
|
68
|
+
# Yield shards
|
|
69
|
+
for url in pass_urls:
|
|
70
|
+
yield dict(url=url)
|
|
71
|
+
|
|
72
|
+
if not self.repeat:
|
|
73
|
+
break
|
|
74
|
+
pass_idx += 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ResampledShardsV2(IterableDataset):
|
|
78
|
+
"""An iterable dataset yielding a list of urls."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
urls,
|
|
83
|
+
weights=None,
|
|
84
|
+
nshards=sys.maxsize,
|
|
85
|
+
worker_seed_fn=None,
|
|
86
|
+
deterministic=False,
|
|
87
|
+
interval=-1,
|
|
88
|
+
seed=None,
|
|
89
|
+
):
|
|
90
|
+
"""Sample shards from the shard list with replacement.
|
|
91
|
+
|
|
92
|
+
:param urls: a list of URLs as a Python list or brace notation string
|
|
93
|
+
"""
|
|
94
|
+
super().__init__()
|
|
95
|
+
if weights is not None:
|
|
96
|
+
self.urls, self.weights = expand_urls(urls, weights)
|
|
97
|
+
assert len(self.urls) == len(self.weights), \
|
|
98
|
+
f"Number of urls {len(self.urls)} and weights {len(self.weights)} should match."
|
|
99
|
+
else:
|
|
100
|
+
self.urls = expand_urls(urls)
|
|
101
|
+
self.weights = None
|
|
102
|
+
assert isinstance(self.urls[0], str)
|
|
103
|
+
self.nshards = nshards
|
|
104
|
+
self.rng = random.Random()
|
|
105
|
+
self.worker_seed_fn = worker_seed_fn
|
|
106
|
+
self.deterministic = deterministic
|
|
107
|
+
self.interval = interval
|
|
108
|
+
self.seed = seed # only used when seed cannot be recovered from DL workers
|
|
109
|
+
|
|
110
|
+
def __iter__(self):
|
|
111
|
+
"""Return an iterator over the shards."""
|
|
112
|
+
if isinstance(self.interval, SharedCount):
|
|
113
|
+
interval = self.interval.get_value()
|
|
114
|
+
else:
|
|
115
|
+
# NOTE: this is epoch tracking is problematic in a multiprocess (dataloader workers or train)
|
|
116
|
+
# situation as different workers may wrap at different times (or not at all).
|
|
117
|
+
self.interval += 1
|
|
118
|
+
interval = self.interval
|
|
119
|
+
|
|
120
|
+
if self.deterministic:
|
|
121
|
+
# reset seed w/ interval if deterministic
|
|
122
|
+
if self.worker_seed_fn is None:
|
|
123
|
+
# pytorch worker seed should be deterministic (per-worker)
|
|
124
|
+
# It is init by process seed, rank, & worker id
|
|
125
|
+
seed = get_pytorch_worker_seed(interval, initial_seed=self.seed)
|
|
126
|
+
else:
|
|
127
|
+
seed = self.worker_seed_fn() + interval
|
|
128
|
+
self.rng.seed(seed)
|
|
129
|
+
|
|
130
|
+
for _ in range(self.nshards):
|
|
131
|
+
if self.weights is None:
|
|
132
|
+
yield dict(url=self.rng.choice(self.urls))
|
|
133
|
+
else:
|
|
134
|
+
yield dict(url=self.rng.choices(self.urls, weights=self.weights, k=1)[0])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from webdataset.filters import pipelinefilter
|
|
4
|
+
from webdataset.tariterators import base_plus_ext, valid_sample, url_opener, tar_file_expander
|
|
5
|
+
|
|
6
|
+
from .helpers import log_and_continue
|
|
7
|
+
|
|
8
|
+
BASE_RE = re.compile(r"^((?:.*/|)[^.]+)[.]([^/]*)$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def base_plus_ext(path):
|
|
12
|
+
"""Split off all file extensions.
|
|
13
|
+
|
|
14
|
+
Returns base, allext.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
path: path with extensions
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
path with all extensions removed
|
|
21
|
+
"""
|
|
22
|
+
match = re.match(BASE_RE, path)
|
|
23
|
+
if not match:
|
|
24
|
+
return None, None
|
|
25
|
+
return match.group(1), match.group(2)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def group_by_keys_nothrow(
|
|
29
|
+
data,
|
|
30
|
+
keys=base_plus_ext,
|
|
31
|
+
lcase=True,
|
|
32
|
+
suffixes=None,
|
|
33
|
+
handler=None,
|
|
34
|
+
):
|
|
35
|
+
"""Return function over iterator that groups key, value pairs into samples.
|
|
36
|
+
|
|
37
|
+
:param keys: function that splits the key into key and extension (base_plus_ext)
|
|
38
|
+
:param lcase: convert suffixes to lower case (Default value = True)
|
|
39
|
+
"""
|
|
40
|
+
current_sample = None
|
|
41
|
+
for filesample in data:
|
|
42
|
+
assert isinstance(filesample, dict)
|
|
43
|
+
fname, value = filesample["fname"], filesample["data"]
|
|
44
|
+
prefix, suffix = keys(fname)
|
|
45
|
+
if prefix is None:
|
|
46
|
+
continue
|
|
47
|
+
if lcase:
|
|
48
|
+
suffix = suffix.lower()
|
|
49
|
+
# FIXME wds version throws if suffix in current_sample, but we have a potential for
|
|
50
|
+
# this happening in the current LAION400m dataset if a tar ends with same prefix as the next
|
|
51
|
+
# begins, rare, but can happen since prefix aren't unique across tar files in that dataset
|
|
52
|
+
if current_sample is None or prefix != current_sample["__key__"] or suffix in current_sample:
|
|
53
|
+
if valid_sample(current_sample):
|
|
54
|
+
yield current_sample
|
|
55
|
+
current_sample = dict(__key__=prefix, __url__=filesample["__url__"])
|
|
56
|
+
if suffixes is None or suffix in suffixes:
|
|
57
|
+
current_sample[suffix] = value
|
|
58
|
+
if valid_sample(current_sample):
|
|
59
|
+
yield current_sample
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tarfile_samples_nothrow(src, handler=log_and_continue):
|
|
63
|
+
# NOTE this is a re-impl of the wds impl with group_by_keys that doesn't throw
|
|
64
|
+
streams = url_opener(src, handler=handler)
|
|
65
|
+
files = tar_file_expander(streams, handler=handler)
|
|
66
|
+
samples = group_by_keys_nothrow(files, handler=handler)
|
|
67
|
+
return samples
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
tarfile_to_samples_nothrow = pipelinefilter(tarfile_samples_nothrow)
|