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.
Files changed (53) hide show
  1. chugr/__init__.py +46 -0
  2. chugr/app/test.py +105 -0
  3. chugr/common/__init__.py +9 -0
  4. chugr/common/cli.py +234 -0
  5. chugr/common/collate.py +104 -0
  6. chugr/common/config.py +335 -0
  7. chugr/common/random.py +76 -0
  8. chugr/common/serialize.py +150 -0
  9. chugr/common/task_config.py +24 -0
  10. chugr/common/types.py +122 -0
  11. chugr/common/urls.py +88 -0
  12. chugr/doc/__init__.py +17 -0
  13. chugr/doc/constants.py +15 -0
  14. chugr/doc/doc_processor.py +248 -0
  15. chugr/doc/doc_read_processor.py +249 -0
  16. chugr/doc/doc_vqa_processor.py +113 -0
  17. chugr/doc/synth_text.py +212 -0
  18. chugr/hfds/__init__.py +2 -0
  19. chugr/hfds/collate.py +83 -0
  20. chugr/hfds/loader.py +385 -0
  21. chugr/hfds/wrappers.py +108 -0
  22. chugr/image/__init__.py +4 -0
  23. chugr/image/build_transforms_doc.py +427 -0
  24. chugr/image/build_transforms_image.py +203 -0
  25. chugr/image/transforms_alb.py +112 -0
  26. chugr/image/transforms_factory.py +58 -0
  27. chugr/image/transforms_torch.py +137 -0
  28. chugr/loader.py +249 -0
  29. chugr/naflex.py +357 -0
  30. chugr/task_pipeline/__init__.py +7 -0
  31. chugr/task_pipeline/pipeline_doc_read.py +66 -0
  32. chugr/task_pipeline/pipeline_doc_vqa.py +124 -0
  33. chugr/task_pipeline/pipeline_factory.py +32 -0
  34. chugr/task_pipeline/pipeline_gtparse.py +93 -0
  35. chugr/task_pipeline/pipeline_image_text.py +90 -0
  36. chugr/task_pipeline/pipeline_manual.py +27 -0
  37. chugr/text/__init__.py +1 -0
  38. chugr/text/tokenization.py +222 -0
  39. chugr/version.py +1 -0
  40. chugr/wds/__init__.py +7 -0
  41. chugr/wds/dataset_info.py +65 -0
  42. chugr/wds/decode.py +319 -0
  43. chugr/wds/filters.py +142 -0
  44. chugr/wds/helpers.py +68 -0
  45. chugr/wds/loader.py +187 -0
  46. chugr/wds/pipeline.py +182 -0
  47. chugr/wds/shardlists.py +134 -0
  48. chugr/wds/tariterators.py +70 -0
  49. chugr-0.2.0.dev0.dist-info/METADATA +436 -0
  50. chugr-0.2.0.dev0.dist-info/RECORD +53 -0
  51. chugr-0.2.0.dev0.dist-info/WHEEL +4 -0
  52. chugr-0.2.0.dev0.dist-info/entry_points.txt +4 -0
  53. 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
@@ -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)