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/hfds/loader.py ADDED
@@ -0,0 +1,385 @@
1
+ import math
2
+ import warnings
3
+ from copy import deepcopy
4
+ from typing import Callable, List, Optional
5
+
6
+ import torch
7
+ from torch.utils.data import DataLoader, Sampler
8
+
9
+ from chugr.common import (
10
+ LoaderBundle, DistributedCfg, SharedCount, rank_seed, seed_worker, shuffled_interval_indices)
11
+
12
+ from .collate import HfCollate
13
+ from .wrappers import SafeDataset, WrappedIterableDataset
14
+
15
+ _SAMPLE_SHUFFLE_SIZE = 2000
16
+
17
+
18
+ class IntervalSampler(Sampler):
19
+ """ Shuffled, rank sharded sampler w/ an optional per-interval sample limit (for intervals < 1 epoch).
20
+
21
+ Each interval draws a fresh permutation (seed + interval), samples are consumed sequentially across
22
+ intervals so that short intervals still cycle through the full dataset.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ dataset,
28
+ num_samples: Optional[int] = None,
29
+ rank: int = 0,
30
+ world_size: int = 1,
31
+ seed: int = 0,
32
+ interval: int = 0,
33
+ ):
34
+ self.dataset_len = len(dataset)
35
+ self.world_size = world_size
36
+ self.rank = rank
37
+ self.seed = seed
38
+ self.interval = interval
39
+ total = min(num_samples, self.dataset_len) if num_samples else self.dataset_len
40
+ self.num_samples_per_rank = total // world_size
41
+
42
+ def set_epoch(self, interval: int):
43
+ self.interval = interval
44
+
45
+ def __len__(self):
46
+ return self.num_samples_per_rank
47
+
48
+ def __iter__(self):
49
+ per_interval = self.num_samples_per_rank * self.world_size
50
+ indices = shuffled_interval_indices(self.dataset_len, per_interval, self.interval, self.seed)
51
+ return iter(indices[self.rank::self.world_size])
52
+
53
+
54
+ class EvalSampler(Sampler):
55
+ """ Rank strided eval sampler, every sample exactly once across ranks w/o padding.
56
+
57
+ Unlike DistributedSampler (drop_last=False), samples are not repeated to even out the per-rank counts (ranks
58
+ differ by at most one sample). Repeats would be counted twice in the gathered metrics and depend on the world
59
+ size. Eval has no per batch collectives, so uneven per-rank batch counts are fine.
60
+ """
61
+
62
+ def __init__(self, dataset, rank: int = 0, world_size: int = 1):
63
+ self.indices = range(rank, len(dataset), world_size)
64
+
65
+ def __len__(self):
66
+ return len(self.indices)
67
+
68
+ def __iter__(self):
69
+ return iter(self.indices)
70
+
71
+
72
+ def _disable_decode(ds):
73
+ import datasets
74
+
75
+ to_set = []
76
+ for k, v in ds.features.items():
77
+ if isinstance(v, datasets.Image):
78
+ d = deepcopy(v)
79
+ d.decode = False
80
+ to_set.append((k, d))
81
+ elif isinstance(v, datasets.Audio):
82
+ d = deepcopy(v)
83
+ d.decode = False
84
+ to_set.append((k, d))
85
+ for k, d in to_set:
86
+ ds = ds.cast_column(k, d)
87
+ return ds
88
+
89
+
90
+ def create_loader_hf(
91
+ source: str,
92
+ split: str,
93
+ task_pipeline: Optional[List[Callable]] = None,
94
+ data_dir: Optional[str] = None,
95
+ num_samples: Optional[int] = None,
96
+ streaming: bool = False,
97
+ is_training: bool = False,
98
+ batch_size: Optional[int] = 1,
99
+ resampled: bool = False,
100
+ multi_interval: bool = True,
101
+ num_batches_round: str = 'ceil',
102
+ num_workers: int = 4,
103
+ persistent_workers: bool = True,
104
+ start_interval: int = 0,
105
+ seed: int = 0,
106
+ collate_fn: Optional[Callable] = None,
107
+ sample_shuffle_size: int = _SAMPLE_SHUFFLE_SIZE,
108
+ distributed: DistributedCfg = DistributedCfg(),
109
+ disable_decode: bool = False,
110
+ naflex_factory: Optional[Callable] = None,
111
+ handler: Optional[Callable] = None,
112
+ ):
113
+ """
114
+
115
+ Args:
116
+ source:
117
+ split:
118
+ task_pipeline:
119
+ data_dir:
120
+ num_samples:
121
+ streaming:
122
+ is_training:
123
+ batch_size:
124
+ resampled:
125
+ multi_interval:
126
+ num_batches_round:
127
+ num_workers:
128
+ persistent_workers:
129
+ start_interval:
130
+ seed:
131
+ collate_fn:
132
+ sample_shuffle_size:
133
+ distributed:
134
+ disable_decode:
135
+ naflex_factory: num_samples (per interval) -> (NaFlexBatchScheduler, per seq_len processors, image key),
136
+ train w/ schedule driven (variable seq_len & batch size) NaFlex batching, replaces the task pipeline
137
+ and fixed size batching
138
+ handler: Exception handler for NaFlex batching (failed samples are replaced)
139
+
140
+ Returns:
141
+
142
+ """
143
+ from datasets import VerificationMode, load_dataset
144
+ batched = batch_size is not None and batch_size >= 0
145
+ naflex = is_training and naflex_factory is not None
146
+
147
+ if naflex:
148
+ pass # the NaFlex dataset wrapper processes and collates
149
+ elif collate_fn is not None:
150
+ assert task_pipeline is None, 'task_pipeline should not be set if custom collation function is used.'
151
+ elif batched or task_pipeline is not None:
152
+ # collation fn applies task pipeline
153
+ assert task_pipeline is not None, 'task_pipeline is needed'
154
+ collate_fn = HfCollate(
155
+ task_pipeline,
156
+ apply_collate=batched,
157
+ )
158
+
159
+ if streaming:
160
+ from datasets.distributed import split_dataset_by_node
161
+
162
+ dataset = load_dataset(
163
+ source,
164
+ data_dir=data_dir,
165
+ streaming=True,
166
+ )
167
+
168
+ if split not in dataset:
169
+ assert False, f'Split {split} not in dataset ({dataset.keys()})'
170
+ dataset = dataset[split]
171
+ if disable_decode:
172
+ dataset = _disable_decode(dataset)
173
+
174
+ if not is_training and num_samples:
175
+ # eval, cap the stream before the rank split, the count is exact and the eval set doesn't change w/ world
176
+ # size. NOTE w/ dataloader workers, HF splits the cap across the workers' shards, so which samples
177
+ # depends on num_workers (the first num_samples of the stream w/o workers)
178
+ dataset = dataset.take(num_samples)
179
+
180
+ # FIXME num_samples calc, get a reliable estimate from dataset in streaming mode
181
+ if num_samples is None:
182
+ info = dataset.info
183
+ if info.splits is not None and split in info.splits:
184
+ num_samples = info.splits[split].num_examples
185
+
186
+ if is_training and multi_interval:
187
+ assert num_samples, (
188
+ "num_samples must be available in dataset metadata or manually provided for multi-interval training")
189
+
190
+ if is_training:
191
+ dataset = dataset.shuffle(seed, buffer_size=sample_shuffle_size)
192
+
193
+ # FIXME split_dataset_by_node has some concerns as currently implemented
194
+ dataset = split_dataset_by_node(dataset, distributed.global_rank, distributed.world_size)
195
+ interval_count = SharedCount(start_interval)
196
+
197
+ if is_training:
198
+ # HF assigns the data shards (files) of this rank to the dataloader workers, a worker w/o a shard
199
+ # yields nothing, which a fixed number of batches per worker can't tolerate
200
+ num_shards = getattr(dataset, 'num_shards', None) or getattr(dataset, 'n_shards', None)
201
+ if num_shards and num_workers > num_shards:
202
+ warnings.warn(
203
+ f'Reducing num_workers from {num_workers} to {num_shards}, the number of data shards '
204
+ f'of the streaming dataset (per rank).')
205
+ num_workers = num_shards
206
+ num_workers_nonzero = max(num_workers, 1)
207
+ # worker seeds (augmentation) differ across ranks, the dataset shuffle uses the shared seed
208
+ dl_generator = torch.Generator()
209
+ dl_generator.manual_seed(rank_seed(seed, distributed.global_rank))
210
+ loader_kwargs = dict(
211
+ sampler=None,
212
+ shuffle=False,
213
+ num_workers=num_workers,
214
+ persistent_workers=persistent_workers and num_workers > 0,
215
+ generator=dl_generator,
216
+ worker_init_fn=seed_worker,
217
+ )
218
+
219
+ mean_batch_size = None
220
+ if naflex:
221
+ # schedule driven batching, every worker yields its share of the rank identical schedule from its
222
+ # (repeating) share of the stream
223
+ from chugr.naflex import NaFlexBatcher
224
+
225
+ assert num_samples, 'NaFlex batching requires num_samples (per interval).'
226
+ scheduler, processors, image_key = naflex_factory(num_samples)
227
+ batcher = NaFlexBatcher(
228
+ scheduler, processors, image_key=image_key, interval=interval_count, handler=handler)
229
+ dataset = WrappedIterableDataset(dataset, interval_count=interval_count, repeat=True, stages=[batcher])
230
+ base_loader = DataLoader(dataset=dataset, batch_size=None, **loader_kwargs)
231
+ num_batches = scheduler.num_batches_for_workers(num_workers_nonzero)
232
+ num_samples = scheduler.num_samples_for_workers(num_workers_nonzero)
233
+ mean_batch_size = scheduler.mean_batch_size_for_workers(num_workers_nonzero)
234
+ else:
235
+ num_samples_per_worker = None
236
+ num_batches = 0
237
+ if is_training and batched and batch_size and num_samples:
238
+ # the same number of full batches on every rank, enforced by a fixed number of samples per
239
+ # worker w/ the stream wrapping around (as the wds loader, see the notes there)
240
+ round_fn = math.floor if num_batches_round == 'floor' else math.ceil
241
+ global_batch_size = batch_size * distributed.world_size
242
+ num_batches = round_fn(num_samples / global_batch_size)
243
+ num_batches_per_worker = max(round_fn(num_batches / num_workers_nonzero), 1)
244
+ num_batches = num_batches_per_worker * num_workers_nonzero
245
+ num_samples = num_batches * global_batch_size
246
+ num_samples_per_worker = num_batches_per_worker * batch_size
247
+ elif num_samples:
248
+ # eval (or unbatched), an estimate, split_dataset_by_node gives each rank ~num_samples / world_size
249
+ num_batches = math.ceil(num_samples / distributed.world_size / (batch_size or 1))
250
+ dataset = WrappedIterableDataset(
251
+ dataset,
252
+ interval_count=interval_count,
253
+ num_samples=num_samples_per_worker,
254
+ repeat=num_samples_per_worker is not None,
255
+ )
256
+ # HF datasets treats batch_size differently than torch defaults, in torch batch_size = None
257
+ # disables batching, in HF it returns the full dataset. We restore torch behaviour here.
258
+ base_loader = DataLoader(
259
+ dataset=dataset,
260
+ collate_fn=collate_fn,
261
+ drop_last=batched and is_training,
262
+ batch_size=batch_size,
263
+ **loader_kwargs,
264
+ )
265
+
266
+ loader = LoaderBundle(
267
+ loader=base_loader,
268
+ num_batches=num_batches,
269
+ num_samples=num_samples,
270
+ shared_interval=interval_count,
271
+ mean_batch_size=mean_batch_size,
272
+ )
273
+
274
+ else:
275
+ # split may include slicing, e.g. 'train[:1000]' or 'validation[:10%]'
276
+ dataset = load_dataset(
277
+ source,
278
+ data_dir=data_dir,
279
+ split=split,
280
+ verification_mode=VerificationMode.BASIC_CHECKS,
281
+ )
282
+ if not is_training and num_samples and num_samples < len(dataset):
283
+ dataset = dataset.select(range(num_samples))
284
+ if disable_decode:
285
+ dataset = _disable_decode(dataset)
286
+ if naflex:
287
+ return _create_loader_hf_naflex(
288
+ dataset,
289
+ naflex_factory=naflex_factory,
290
+ num_samples=num_samples,
291
+ num_workers=num_workers,
292
+ persistent_workers=persistent_workers,
293
+ start_interval=start_interval,
294
+ seed=seed,
295
+ distributed=distributed,
296
+ handler=handler,
297
+ )
298
+
299
+ dataset = SafeDataset(dataset)
300
+
301
+ if is_training:
302
+ # shuffled (per interval), sharded by rank, optionally limited to num_samples per interval
303
+ sampler = IntervalSampler(
304
+ dataset,
305
+ num_samples=num_samples,
306
+ rank=distributed.global_rank,
307
+ world_size=distributed.world_size,
308
+ seed=seed,
309
+ interval=start_interval,
310
+ )
311
+ elif distributed.world_size > 1:
312
+ sampler = EvalSampler(dataset, rank=distributed.global_rank, world_size=distributed.world_size)
313
+ else:
314
+ sampler = None
315
+
316
+ base_loader = DataLoader(
317
+ dataset=dataset,
318
+ collate_fn=collate_fn,
319
+ sampler=sampler,
320
+ drop_last=is_training,
321
+ batch_size=batch_size,
322
+ num_workers=num_workers,
323
+ persistent_workers=persistent_workers and num_workers > 0,
324
+ )
325
+
326
+ loader = LoaderBundle(
327
+ loader=base_loader,
328
+ num_batches=len(base_loader),
329
+ num_samples=len(sampler) * distributed.world_size if is_training else len(dataset),
330
+ sampler=sampler,
331
+ )
332
+
333
+ return loader
334
+
335
+
336
+ def _create_loader_hf_naflex(
337
+ dataset,
338
+ naflex_factory: Callable,
339
+ num_samples: Optional[int],
340
+ num_workers: int,
341
+ persistent_workers: bool,
342
+ start_interval: int,
343
+ seed: int,
344
+ distributed: DistributedCfg,
345
+ handler: Optional[Callable],
346
+ ):
347
+ """ Train loader w/ schedule driven NaFlex batching over a map-style dataset. """
348
+ from chugr.naflex import NaFlexMapDataset
349
+
350
+ # samples per interval (all ranks), the schedule rounds up to full batches per rank / worker
351
+ num_samples = min(num_samples, len(dataset)) if num_samples else len(dataset)
352
+ scheduler, processors, image_key = naflex_factory(num_samples)
353
+ interval_count = SharedCount(start_interval)
354
+ # NOTE not wrapped in SafeDataset, the NaFlex wrapper replaces samples that fail to load or process
355
+ naflex_dataset = NaFlexMapDataset(
356
+ dataset,
357
+ scheduler=scheduler,
358
+ processors=processors,
359
+ image_key=image_key,
360
+ interval=interval_count,
361
+ rank=distributed.global_rank,
362
+ world_size=distributed.world_size,
363
+ seed=seed,
364
+ handler=handler,
365
+ )
366
+ # worker seeds (augmentation, replacement samples) differ across ranks, indices / schedule use the shared seed
367
+ dl_generator = torch.Generator()
368
+ dl_generator.manual_seed(rank_seed(seed, distributed.global_rank))
369
+ base_loader = DataLoader(
370
+ dataset=naflex_dataset,
371
+ batch_size=None, # batching done by the dataset wrapper
372
+ shuffle=False,
373
+ num_workers=num_workers,
374
+ persistent_workers=persistent_workers and num_workers > 0,
375
+ generator=dl_generator,
376
+ worker_init_fn=seed_worker,
377
+ )
378
+ num_workers_nonzero = max(num_workers, 1)
379
+ return LoaderBundle(
380
+ loader=base_loader,
381
+ num_batches=scheduler.num_batches_for_workers(num_workers_nonzero),
382
+ num_samples=scheduler.num_samples_for_workers(num_workers_nonzero),
383
+ shared_interval=interval_count,
384
+ mean_batch_size=scheduler.mean_batch_size_for_workers(num_workers_nonzero),
385
+ )
chugr/hfds/wrappers.py ADDED
@@ -0,0 +1,108 @@
1
+ import logging
2
+ from itertools import islice
3
+ from typing import Callable, List, Optional
4
+
5
+ from torch.utils.data import Dataset, IterableDataset
6
+
7
+ from chugr.common import SharedCount
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+ class SafeDataset(Dataset):
12
+ """
13
+ This is a Dataset wrapped by a try/except in the __getitem__ in case
14
+ the hfds datasets used have errors/corrupt data. A sample that fails to load is replaced by
15
+ the next loadable one (wrapping around at the end of the dataset), w/ a warning.
16
+ """
17
+
18
+ def __init__(self, original_dataset, max_retry=10):
19
+ self.ds = original_dataset
20
+ self.max_retry = max_retry
21
+
22
+ def __len__(self):
23
+ return len(self.ds)
24
+
25
+ def __getitem__(self, idx):
26
+ err = None
27
+ num = len(self.ds)
28
+ for try_idx in range(min(self.max_retry, num)):
29
+ try_at = (idx + try_idx) % num # wrap, idx + try_idx can run past the end
30
+ try:
31
+ item = self.ds[try_at]
32
+ except Exception as e:
33
+ err = e
34
+ continue
35
+ if try_idx:
36
+ _logger.warning(f'Sample {idx} failed to load ({err!r}), replaced by sample {try_at}.')
37
+ return item
38
+ raise err if err is not None else IndexError(idx)
39
+
40
+
41
+
42
+ class WrappedIterableDataset(IterableDataset):
43
+ """ Wrap a streaming (iterable) HF dataset for interval based training loops.
44
+
45
+ * The interval count (shared w/ dataloader workers) sets the dataset epoch (shard / buffer shuffle seed), set
46
+ from the main process only (via `set_interval_count`). The HF epoch is a shared memory tensor updated w/ a
47
+ non-atomic in-place add, workers setting it concurrently race (an overshot / differing epoch per worker breaks
48
+ the worker shard partition, shards are skipped or read twice).
49
+ * `num_samples` w/ `repeat=True` makes every dataloader worker yield exactly `num_samples` samples per
50
+ interval, wrapping around its share of the stream if it runs out (a few samples repeat), so every rank sees
51
+ the same number of batches (as the wds loader).
52
+ * `stages` are applied to the sample stream (generator fn taking an iterable, e.g. NaFlexBatcher which then
53
+ yields batches following its schedule).
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ original_dataset,
59
+ interval_count=None,
60
+ max_retry=10,
61
+ num_samples: Optional[int] = None,
62
+ repeat: bool = False,
63
+ stages: Optional[List[Callable]] = None,
64
+ ):
65
+ self.ds = original_dataset
66
+ self.max_retry = max_retry
67
+ self.interval_count = interval_count
68
+ self.ds.set_epoch(self._get_interval_count())
69
+ self.num_samples = num_samples
70
+ self.repeat = repeat
71
+ self.stages = list(stages or [])
72
+
73
+ def _get_interval_count(self):
74
+ if isinstance(self.interval_count, SharedCount):
75
+ return self.interval_count.get_value()
76
+ return self.interval_count or 0
77
+
78
+ def set_interval_count(self, interval_count):
79
+ """ Main process only, workers (persistent or not) see the shared count and HF epoch. """
80
+ if isinstance(self.interval_count, SharedCount):
81
+ self.interval_count.set_value(interval_count)
82
+ else:
83
+ self.interval_count = interval_count
84
+ self.ds.set_epoch(interval_count)
85
+
86
+ def _samples(self):
87
+ # NOTE no set_epoch here, see the class docstring
88
+ if not self.repeat:
89
+ yield from self.ds
90
+ return
91
+ while True:
92
+ # NOTE wraps w/ the same epoch (shuffle seed), a per rank / worker reshuffle could break the node split
93
+ count = 0
94
+ for sample in self.ds:
95
+ count += 1
96
+ yield sample
97
+ if not count:
98
+ raise RuntimeError(
99
+ 'No samples for this dataloader worker, a repeating streaming dataset needs at least one data '
100
+ 'shard (file) per worker of every rank.')
101
+
102
+ def __iter__(self):
103
+ src = self._samples()
104
+ if self.num_samples:
105
+ src = islice(src, self.num_samples)
106
+ for stage in self.stages:
107
+ src = stage(src)
108
+ yield from src
@@ -0,0 +1,4 @@
1
+ from .transforms_factory import build_image_transforms, create_image_preprocessor
2
+ from .build_transforms_doc import build_transforms_doc_better, build_transforms_doc_basic, build_transforms_doc_nougat, \
3
+ build_transforms_doc_naflex, ResizeToSeqLen
4
+ from .build_transforms_image import build_transforms_image_timm, build_transforms_image_basic