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/naflex.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
""" NaFlex (variable resolution patch sequence) batching for document pipelines.
|
|
2
|
+
|
|
3
|
+
Adapted from the open_clip NaFlex streaming data pipeline:
|
|
4
|
+
|
|
5
|
+
* NaFlexBatchScheduler: a deterministic (seq_len, batch_size) schedule, identical across ranks (seeded), sampled
|
|
6
|
+
per batch from a set of patch sequence length buckets, w/ batch size from a token budget (patches + an optional
|
|
7
|
+
per-row text token cost). Shuffled per interval, padded and stride-split across dataloader workers.
|
|
8
|
+
* NaFlexBatcher: webdataset stage that pulls samples, runs the task processor for the batch's seq_len bucket
|
|
9
|
+
(decode / render + image transform + text processing), skips & replaces samples that fail to process so every
|
|
10
|
+
batch is full (equal step shapes across ranks), and collates patches padded to the bucket seq_len.
|
|
11
|
+
* NaFlexMapDataset: the same schedule driven batching over a map-style dataset (e.g. non-streaming HF datasets),
|
|
12
|
+
adapted from the open_clip NaFlexMapDatasetWrapper, w/ rank sharded shuffled indices in place of a sample stream.
|
|
13
|
+
* LengthBucketer: reorders samples so similar (text) lengths batch together, reduces decoder padding.
|
|
14
|
+
|
|
15
|
+
NOTE schedule driven batching of a stream needs an effectively infinite sample stream so every rank / worker can
|
|
16
|
+
always fill its schedule, the wds loader uses resampled shards or (w/o replacement) a repeating, per-pass reshuffled
|
|
17
|
+
shard list.
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import math
|
|
22
|
+
import os
|
|
23
|
+
import random
|
|
24
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
from chugr.common import collate, collate_naflex, shuffled_interval_indices
|
|
29
|
+
|
|
30
|
+
_logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_MAX_CONSECUTIVE_FAILURES = int(os.environ.get('CHUGR_MAX_CONSECUTIVE_FAILURES', '1000'))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def calculate_batch_size(tokens_per_batch: int, row_tokens: int, divisor: int = 1, max_size: Optional[int] = None):
|
|
36
|
+
batch_size = tokens_per_batch // max(row_tokens, 1)
|
|
37
|
+
if divisor > 1:
|
|
38
|
+
batch_size = (batch_size // divisor) * divisor
|
|
39
|
+
batch_size = max(batch_size, 1)
|
|
40
|
+
if max_size is not None:
|
|
41
|
+
batch_size = min(batch_size, max_size)
|
|
42
|
+
return batch_size
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def collate_naflex_batch(samples: List[Dict[str, Any]], image_key: str, seq_len: int) -> Dict[str, Any]:
|
|
46
|
+
""" Collate processed samples, NaFlex image patches padded to the batch's bucket `seq_len`. """
|
|
47
|
+
images = [s.pop(image_key) for s in samples]
|
|
48
|
+
out = collate(samples)
|
|
49
|
+
out[image_key] = collate_naflex(images, pad_to=seq_len)
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _interval_value(interval) -> int:
|
|
54
|
+
if hasattr(interval, 'get_value'):
|
|
55
|
+
return int(interval.get_value())
|
|
56
|
+
return int(interval or 0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NaFlexBatchScheduler:
|
|
60
|
+
""" Deterministic, rank identical NaFlex (seq_len, batch_size) schedule for one interval (epoch). """
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
seq_lens: Sequence[int],
|
|
65
|
+
max_tokens_per_batch: int,
|
|
66
|
+
num_samples: Optional[int] = None,
|
|
67
|
+
num_tokens: Optional[int] = None,
|
|
68
|
+
seq_len_probs: Optional[Sequence[float]] = None,
|
|
69
|
+
per_row_text_tokens: int = 0,
|
|
70
|
+
batch_divisor: int = 8,
|
|
71
|
+
seed: int = 42,
|
|
72
|
+
shuffle: bool = True,
|
|
73
|
+
world_size: int = 1,
|
|
74
|
+
):
|
|
75
|
+
if (num_samples is None) == (num_tokens is None):
|
|
76
|
+
raise ValueError('Specify exactly one of `num_samples` or `num_tokens` for NaFlex batching.')
|
|
77
|
+
if seq_len_probs is not None:
|
|
78
|
+
if len(seq_len_probs) != len(seq_lens):
|
|
79
|
+
raise ValueError('`seq_len_probs` must match `seq_lens` length.')
|
|
80
|
+
pairs = sorted({int(s): float(p) for s, p in zip(seq_lens, seq_len_probs)}.items())
|
|
81
|
+
self.seq_lens = [s for s, _ in pairs]
|
|
82
|
+
total = sum(p for _, p in pairs)
|
|
83
|
+
assert total > 0
|
|
84
|
+
self.seq_len_probs = [p / total for _, p in pairs]
|
|
85
|
+
else:
|
|
86
|
+
self.seq_lens = sorted(set(int(s) for s in seq_lens))
|
|
87
|
+
self.seq_len_probs = None
|
|
88
|
+
assert self.seq_lens and all(s > 0 for s in self.seq_lens)
|
|
89
|
+
self.max_tokens_per_batch = int(max_tokens_per_batch)
|
|
90
|
+
self.per_row_text_tokens = int(per_row_text_tokens or 0)
|
|
91
|
+
self.batch_divisor = int(batch_divisor)
|
|
92
|
+
self.seed = int(seed)
|
|
93
|
+
self.shuffle = shuffle
|
|
94
|
+
self.world_size = max(1, int(world_size))
|
|
95
|
+
|
|
96
|
+
generator = torch.Generator()
|
|
97
|
+
generator.manual_seed(self.seed)
|
|
98
|
+
schedule = []
|
|
99
|
+
if num_samples is not None:
|
|
100
|
+
remaining = math.ceil(int(num_samples) / self.world_size)
|
|
101
|
+
while remaining > 0:
|
|
102
|
+
seq_len = self._next_seq_len(generator)
|
|
103
|
+
bs = calculate_batch_size(
|
|
104
|
+
self.max_tokens_per_batch, seq_len + self.per_row_text_tokens, self.batch_divisor, remaining)
|
|
105
|
+
schedule.append((seq_len, bs))
|
|
106
|
+
remaining -= bs
|
|
107
|
+
else:
|
|
108
|
+
remaining = math.ceil(int(num_tokens) / self.world_size)
|
|
109
|
+
while remaining > 0:
|
|
110
|
+
seq_len = self._next_seq_len(generator)
|
|
111
|
+
bs = calculate_batch_size(
|
|
112
|
+
min(self.max_tokens_per_batch, remaining), seq_len + self.per_row_text_tokens, self.batch_divisor)
|
|
113
|
+
schedule.append((seq_len, bs))
|
|
114
|
+
remaining -= bs * seq_len
|
|
115
|
+
self._schedule = schedule
|
|
116
|
+
|
|
117
|
+
def _next_seq_len(self, generator: torch.Generator) -> int:
|
|
118
|
+
if self.seq_len_probs is None:
|
|
119
|
+
idx = int(torch.randint(0, len(self.seq_lens), (1,), generator=generator).item())
|
|
120
|
+
else:
|
|
121
|
+
idx = int(torch.multinomial(torch.tensor(self.seq_len_probs), 1, generator=generator).item())
|
|
122
|
+
return self.seq_lens[idx]
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def pad_schedule_for_workers(schedule: List[Tuple[int, int]], num_workers: int) -> List[Tuple[int, int]]:
|
|
126
|
+
if num_workers <= 1 or not schedule:
|
|
127
|
+
return schedule
|
|
128
|
+
target = math.ceil(len(schedule) / num_workers) * num_workers
|
|
129
|
+
pad = target - len(schedule)
|
|
130
|
+
if pad > 0:
|
|
131
|
+
schedule = schedule + (schedule * math.ceil(pad / len(schedule)))[:pad]
|
|
132
|
+
return schedule
|
|
133
|
+
|
|
134
|
+
def interval_schedule(self, interval: int, num_workers: int = 1) -> List[Tuple[int, int]]:
|
|
135
|
+
schedule = self.pad_schedule_for_workers(list(self._schedule), max(1, num_workers))
|
|
136
|
+
if self.shuffle:
|
|
137
|
+
random.Random(self.seed + interval).shuffle(schedule)
|
|
138
|
+
return schedule
|
|
139
|
+
|
|
140
|
+
def worker_schedule(self, interval: int) -> List[Tuple[int, int]]:
|
|
141
|
+
info = torch.utils.data.get_worker_info()
|
|
142
|
+
num_workers = info.num_workers if info is not None else 1
|
|
143
|
+
worker_id = info.id if info is not None else 0
|
|
144
|
+
return self.interval_schedule(interval, num_workers)[worker_id::num_workers]
|
|
145
|
+
|
|
146
|
+
def num_batches_for_workers(self, num_workers: int) -> int:
|
|
147
|
+
""" Batches per rank per interval. """
|
|
148
|
+
return len(self.pad_schedule_for_workers(list(self._schedule), max(1, num_workers)))
|
|
149
|
+
|
|
150
|
+
def num_samples_for_workers(self, num_workers: int) -> int:
|
|
151
|
+
""" Samples per interval (all ranks). """
|
|
152
|
+
sched = self.pad_schedule_for_workers(list(self._schedule), max(1, num_workers))
|
|
153
|
+
return sum(bs for _, bs in sched) * self.world_size
|
|
154
|
+
|
|
155
|
+
def mean_batch_size_for_workers(self, num_workers: int) -> float:
|
|
156
|
+
sched = self.pad_schedule_for_workers(list(self._schedule), max(1, num_workers))
|
|
157
|
+
return sum(bs for _, bs in sched) / len(sched)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class NaFlexBatcher:
|
|
161
|
+
""" webdataset stage, schedule driven NaFlex batching w/ per seq_len bucket task processors. """
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
scheduler: NaFlexBatchScheduler,
|
|
166
|
+
processors: Dict[int, Callable[[Dict[str, Any]], Any]],
|
|
167
|
+
image_key: str = 'image_input',
|
|
168
|
+
interval=0,
|
|
169
|
+
handler: Optional[Callable[[Exception], bool]] = None,
|
|
170
|
+
drop_last: bool = True,
|
|
171
|
+
):
|
|
172
|
+
missing = set(scheduler.seq_lens) - set(processors.keys())
|
|
173
|
+
assert not missing, f'Missing processors for seq_lens {missing}'
|
|
174
|
+
self.scheduler = scheduler
|
|
175
|
+
self.processors = processors
|
|
176
|
+
self.image_key = image_key
|
|
177
|
+
self.interval = interval
|
|
178
|
+
self.handler = handler
|
|
179
|
+
self.drop_last = drop_last
|
|
180
|
+
|
|
181
|
+
def _collate(self, samples: List[Dict[str, Any]], seq_len: int) -> Dict[str, Any]:
|
|
182
|
+
return collate_naflex_batch(samples, self.image_key, seq_len)
|
|
183
|
+
|
|
184
|
+
def __call__(self, src: Iterable[Dict[str, Any]]):
|
|
185
|
+
interval = _interval_value(self.interval)
|
|
186
|
+
samples_iter = iter(src)
|
|
187
|
+
for seq_len, batch_size in self.scheduler.worker_schedule(interval):
|
|
188
|
+
processor = self.processors[seq_len]
|
|
189
|
+
batch = []
|
|
190
|
+
failures = 0
|
|
191
|
+
while len(batch) < batch_size:
|
|
192
|
+
try:
|
|
193
|
+
sample = next(samples_iter)
|
|
194
|
+
except StopIteration:
|
|
195
|
+
break
|
|
196
|
+
try:
|
|
197
|
+
processed = processor(sample)
|
|
198
|
+
except Exception as ex:
|
|
199
|
+
# skip & replace, a falsy handler return (e.g. reraise) stops
|
|
200
|
+
if self.handler is not None and not self.handler(ex):
|
|
201
|
+
raise
|
|
202
|
+
failures += 1
|
|
203
|
+
if failures > _MAX_CONSECUTIVE_FAILURES:
|
|
204
|
+
raise
|
|
205
|
+
continue
|
|
206
|
+
failures = 0
|
|
207
|
+
batch.append(processed)
|
|
208
|
+
if len(batch) == batch_size or (batch and not self.drop_last):
|
|
209
|
+
yield self._collate(batch, seq_len)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class NaFlexMapDataset(torch.utils.data.IterableDataset):
|
|
213
|
+
""" Schedule driven NaFlex batching over a map-style dataset (e.g. a non-streaming HF dataset), yields batches.
|
|
214
|
+
|
|
215
|
+
Adapted from the open_clip NaFlexMapDatasetWrapper. Every rank / worker derives the same shuffled index list
|
|
216
|
+
and (seq_len, batch_size) schedule for the interval, a rank takes indices [rank::world_size], a worker every
|
|
217
|
+
num_workers-th batch of the schedule. Use w/ a DataLoader w/ batch_size=None.
|
|
218
|
+
|
|
219
|
+
Differences from the open_clip wrapper:
|
|
220
|
+
* Indices continue through the dataset permutation across intervals (see `shuffled_interval_indices`), so
|
|
221
|
+
intervals shorter than the dataset visit every sample before repeating (vs the head of a new permutation).
|
|
222
|
+
* Samples that fail to load / process are replaced w/ a random other sample (subject to `handler`, as
|
|
223
|
+
NaFlexBatcher), so batches are always full and step shapes equal across ranks.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
dataset,
|
|
229
|
+
scheduler: NaFlexBatchScheduler,
|
|
230
|
+
processors: Dict[int, Callable[[Dict[str, Any]], Any]],
|
|
231
|
+
image_key: str = 'image_input',
|
|
232
|
+
interval=0,
|
|
233
|
+
rank: int = 0,
|
|
234
|
+
world_size: int = 1,
|
|
235
|
+
seed: int = 42,
|
|
236
|
+
handler: Optional[Callable[[Exception], bool]] = None,
|
|
237
|
+
):
|
|
238
|
+
if not hasattr(dataset, '__len__') or not hasattr(dataset, '__getitem__'):
|
|
239
|
+
raise TypeError('NaFlex map batching requires a map-style dataset.')
|
|
240
|
+
missing = set(scheduler.seq_lens) - set(processors.keys())
|
|
241
|
+
assert not missing, f'Missing processors for seq_lens {missing}'
|
|
242
|
+
assert 0 <= rank < world_size
|
|
243
|
+
assert world_size == scheduler.world_size, 'scheduler world_size must match (per rank schedule)'
|
|
244
|
+
self.dataset = dataset
|
|
245
|
+
self.scheduler = scheduler
|
|
246
|
+
self.processors = processors
|
|
247
|
+
self.image_key = image_key
|
|
248
|
+
self.interval = interval
|
|
249
|
+
self.rank = int(rank)
|
|
250
|
+
self.world_size = int(world_size)
|
|
251
|
+
self.seed = int(seed)
|
|
252
|
+
self.handler = handler
|
|
253
|
+
|
|
254
|
+
def __iter__(self):
|
|
255
|
+
interval = _interval_value(self.interval)
|
|
256
|
+
info = torch.utils.data.get_worker_info()
|
|
257
|
+
num_workers = info.num_workers if info is not None else 1
|
|
258
|
+
worker_id = info.id if info is not None else 0
|
|
259
|
+
dataset_len = len(self.dataset)
|
|
260
|
+
|
|
261
|
+
# same (padded, shuffled) schedule and index list on every rank / worker
|
|
262
|
+
schedule = self.scheduler.interval_schedule(interval, num_workers)
|
|
263
|
+
samples_per_rank = sum(batch_size for _, batch_size in schedule)
|
|
264
|
+
indices = shuffled_interval_indices(
|
|
265
|
+
dataset_len, samples_per_rank * self.world_size, interval, self.seed)[self.rank::self.world_size]
|
|
266
|
+
# replacement indices for failed samples, distinct per interval / rank / worker
|
|
267
|
+
rng = random.Random(self.seed + 7919 * interval + 104729 * self.rank + 1299709 * worker_id)
|
|
268
|
+
|
|
269
|
+
offset = 0
|
|
270
|
+
for batch_idx, (seq_len, batch_size) in enumerate(schedule):
|
|
271
|
+
batch_indices = indices[offset:offset + batch_size]
|
|
272
|
+
offset += batch_size
|
|
273
|
+
if batch_idx % num_workers != worker_id:
|
|
274
|
+
continue
|
|
275
|
+
processor = self.processors[seq_len]
|
|
276
|
+
batch = []
|
|
277
|
+
failures = 0
|
|
278
|
+
pos = 0
|
|
279
|
+
while len(batch) < batch_size:
|
|
280
|
+
if pos < len(batch_indices):
|
|
281
|
+
idx = batch_indices[pos]
|
|
282
|
+
pos += 1
|
|
283
|
+
else:
|
|
284
|
+
idx = rng.randrange(dataset_len)
|
|
285
|
+
try:
|
|
286
|
+
processed = processor(self.dataset[idx])
|
|
287
|
+
except Exception as ex:
|
|
288
|
+
# skip & replace, a falsy handler return (e.g. reraise) stops
|
|
289
|
+
if self.handler is not None and not self.handler(ex):
|
|
290
|
+
raise
|
|
291
|
+
failures += 1
|
|
292
|
+
if failures > _MAX_CONSECUTIVE_FAILURES:
|
|
293
|
+
raise
|
|
294
|
+
continue
|
|
295
|
+
failures = 0
|
|
296
|
+
batch.append(processed)
|
|
297
|
+
yield collate_naflex_batch(batch, self.image_key, seq_len)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def annotation_text_length(sample: Dict[str, Any], keys: Sequence[str] = ('json',)) -> int:
|
|
301
|
+
""" Approximate text length (characters) of a raw document sample's annotation (for length bucketing). """
|
|
302
|
+
for k in keys:
|
|
303
|
+
if k not in sample:
|
|
304
|
+
continue
|
|
305
|
+
anno = sample[k]
|
|
306
|
+
if isinstance(anno, bytes):
|
|
307
|
+
anno = json.loads(anno)
|
|
308
|
+
sample[k] = anno # keep decoded
|
|
309
|
+
pages = anno.get('pages', anno) if isinstance(anno, dict) else anno
|
|
310
|
+
total = 0
|
|
311
|
+
try:
|
|
312
|
+
for page in pages:
|
|
313
|
+
page = page.get('lines', page)
|
|
314
|
+
total += sum(len(t) for t in page.get('text', []))
|
|
315
|
+
except (AttributeError, TypeError):
|
|
316
|
+
return 0
|
|
317
|
+
n = max(len(pages), 1)
|
|
318
|
+
return total // n # mean per page (pages are sampled)
|
|
319
|
+
return 0
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class LengthBucketer:
|
|
323
|
+
""" webdataset stage, reorder samples in pools sorted by length (chunks shuffled), reorder only. """
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
length_fn: Callable[[Dict[str, Any]], int] = annotation_text_length,
|
|
328
|
+
pool: int = 1024,
|
|
329
|
+
chunk: int = 64,
|
|
330
|
+
seed: int = 42,
|
|
331
|
+
interval=0,
|
|
332
|
+
):
|
|
333
|
+
self.length_fn = length_fn
|
|
334
|
+
self.pool = max(1, int(pool))
|
|
335
|
+
self.chunk = max(1, int(chunk))
|
|
336
|
+
self.seed = int(seed)
|
|
337
|
+
self.interval = interval
|
|
338
|
+
|
|
339
|
+
def _flush(self, buffer, rng):
|
|
340
|
+
buffer.sort(key=self.length_fn)
|
|
341
|
+
chunks = [buffer[i:i + self.chunk] for i in range(0, len(buffer), self.chunk)]
|
|
342
|
+
rng.shuffle(chunks)
|
|
343
|
+
for c in chunks:
|
|
344
|
+
yield from c
|
|
345
|
+
|
|
346
|
+
def __call__(self, src):
|
|
347
|
+
info = torch.utils.data.get_worker_info()
|
|
348
|
+
worker_id = info.id if info is not None else 0
|
|
349
|
+
rng = random.Random(self.seed + _interval_value(self.interval) * 131 + worker_id)
|
|
350
|
+
buffer = []
|
|
351
|
+
for sample in src:
|
|
352
|
+
buffer.append(sample)
|
|
353
|
+
if len(buffer) >= self.pool:
|
|
354
|
+
yield from self._flush(buffer, rng)
|
|
355
|
+
buffer = []
|
|
356
|
+
if buffer:
|
|
357
|
+
yield from self._flush(buffer, rng)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .pipeline_doc_read import build_task_pipeline_doc_read, DataTaskDocReadCfg
|
|
2
|
+
from .pipeline_doc_vqa import build_task_pipeline_doc_vqa, DataTaskDocVqaCfg
|
|
3
|
+
from .pipeline_gtparse import build_task_pipeline_gtparse
|
|
4
|
+
from .pipeline_image_text import build_task_pipeline_image_text, DataTaskImageTextCfg
|
|
5
|
+
from .pipeline_manual import build_task_pipeline_manual, DataTaskManualCfg
|
|
6
|
+
|
|
7
|
+
from .pipeline_factory import create_task_pipeline, create_task_processor
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
import webdataset as wds
|
|
5
|
+
|
|
6
|
+
from chugr.common import DataTaskCfg, FeatureInfo, ImageFeatureInfo
|
|
7
|
+
from chugr.doc import DocReadProcessor, DEFAULT_DOC_FEAT
|
|
8
|
+
from chugr.doc.synth_text import SynthTextCfg
|
|
9
|
+
from chugr.wds import get_error_handler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DataTaskDocReadCfg(DataTaskCfg):
|
|
14
|
+
image_input_feat: ImageFeatureInfo = DEFAULT_DOC_FEAT
|
|
15
|
+
text_input_feat: FeatureInfo = FeatureInfo('text_input', input_key='pages')
|
|
16
|
+
text_target_feat: Optional[FeatureInfo] = FeatureInfo('text_target', input_key=None)
|
|
17
|
+
page_sampling: str = 'random'
|
|
18
|
+
render_dpi: int = 150
|
|
19
|
+
synth_prob: float = 0. # probability of a synthetic text rendering (of the sample's text) replacing the page
|
|
20
|
+
synth_cfg: Optional[SynthTextCfg] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_processor_doc_read(cfg: DataTaskDocReadCfg, image_process_fn: Optional[Callable] = None):
|
|
24
|
+
return DocReadProcessor(
|
|
25
|
+
image_process_fn=image_process_fn if image_process_fn is not None else cfg.image_process_fn,
|
|
26
|
+
text_process_fn=cfg.text_process_fn,
|
|
27
|
+
image_input_feat=cfg.image_input_feat,
|
|
28
|
+
text_input_feat=cfg.text_input_feat,
|
|
29
|
+
text_target_feat=cfg.text_target_feat,
|
|
30
|
+
page_sampling=cfg.page_sampling,
|
|
31
|
+
render_dpi=cfg.render_dpi,
|
|
32
|
+
flatten_json=cfg.flatten_json,
|
|
33
|
+
synth_prob=cfg.synth_prob,
|
|
34
|
+
synth_cfg=cfg.synth_cfg,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_task_pipeline_doc_read(
|
|
39
|
+
cfg: DataTaskDocReadCfg,
|
|
40
|
+
):
|
|
41
|
+
handler = get_error_handler(cfg.error_handler)
|
|
42
|
+
pipe = []
|
|
43
|
+
|
|
44
|
+
# document decoding & pre-processing done together, there is coupling in random page
|
|
45
|
+
# selection and in the future, masking of image and/or text
|
|
46
|
+
pipe += [wds.map(create_processor_doc_read(cfg), handler=handler)]
|
|
47
|
+
|
|
48
|
+
if cfg.output_tuple:
|
|
49
|
+
# NOTE in this mode we lose '_parse' key and would need to derive from target
|
|
50
|
+
# Unless we add support for parse as the last tuple element?
|
|
51
|
+
if cfg.text_target_feat is not None:
|
|
52
|
+
pipe += [
|
|
53
|
+
wds.to_tuple(
|
|
54
|
+
cfg.image_input_feat.output_name,
|
|
55
|
+
cfg.text_input_feat.output_name,
|
|
56
|
+
cfg.text_target_feat.output_name,
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
else:
|
|
60
|
+
pipe += [
|
|
61
|
+
wds.to_tuple(
|
|
62
|
+
cfg.image_input_feat.output_name,
|
|
63
|
+
cfg.text_input_feat.output_name,
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
return pipe
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Callable, Dict, Optional, Union
|
|
4
|
+
|
|
5
|
+
import webdataset as wds
|
|
6
|
+
|
|
7
|
+
from chugr.common import DataTaskCfg, FeatureInfo, ImageFeatureInfo
|
|
8
|
+
from chugr.wds import get_error_handler
|
|
9
|
+
from chugr.doc import (
|
|
10
|
+
DocVqaProcessor,
|
|
11
|
+
DEFAULT_DOC_KEY,
|
|
12
|
+
DEFAULT_QUESTION_KEY,
|
|
13
|
+
DEFAULT_QUESTION_ID_KEY,
|
|
14
|
+
DEFAULT_ANSWER_KEY,
|
|
15
|
+
DEFAULT_DOC_KEY_TUPLE,
|
|
16
|
+
DEFAULT_QUESTION_KEY_TUPLE,
|
|
17
|
+
DEFAULT_ANSWER_KEY_TUPLE,
|
|
18
|
+
DEFAULT_DOC_FEAT,
|
|
19
|
+
DEFAULT_QUESTION_FEAT,
|
|
20
|
+
DEFAULT_QUESTION_ID_FEAT,
|
|
21
|
+
DEFAULT_ANSWER_FEAT
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def filter_missing(
|
|
26
|
+
sample,
|
|
27
|
+
image_key=DEFAULT_DOC_KEY_TUPLE,
|
|
28
|
+
question_key=DEFAULT_QUESTION_KEY_TUPLE,
|
|
29
|
+
answer_key=DEFAULT_ANSWER_KEY_TUPLE,
|
|
30
|
+
):
|
|
31
|
+
has_question = any(k in sample for k in question_key)
|
|
32
|
+
has_answer = any(k in sample for k in answer_key)
|
|
33
|
+
has_image = any(k in sample for k in image_key)
|
|
34
|
+
return has_question and has_answer and has_image
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Currently assuming this schema as default, one set of question/answers per image, images possibly duplicated
|
|
38
|
+
# sample = {
|
|
39
|
+
# 'png': bytes,
|
|
40
|
+
# 'question_id': 33,
|
|
41
|
+
# 'doc_id': 55, # optional
|
|
42
|
+
# 'question': 'what is a trumpet?',
|
|
43
|
+
# 'answers': ['an instrument', 'a brass instrument']
|
|
44
|
+
# }
|
|
45
|
+
#
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class DataTaskDocVqaCfg(DataTaskCfg):
|
|
50
|
+
"""
|
|
51
|
+
Attributes:
|
|
52
|
+
answer_feat:
|
|
53
|
+
question_feat:
|
|
54
|
+
question_id_feat:
|
|
55
|
+
image_input_feat:
|
|
56
|
+
text_input_feat:
|
|
57
|
+
text_target_feat:
|
|
58
|
+
question_prefix:
|
|
59
|
+
question_suffix:
|
|
60
|
+
answer_prefix:
|
|
61
|
+
answer_suffix:
|
|
62
|
+
render_dpi:
|
|
63
|
+
"""
|
|
64
|
+
answer_feat: FeatureInfo = DEFAULT_ANSWER_FEAT
|
|
65
|
+
question_feat: FeatureInfo = DEFAULT_QUESTION_FEAT
|
|
66
|
+
question_id_feat: FeatureInfo = DEFAULT_QUESTION_ID_FEAT
|
|
67
|
+
image_input_feat: ImageFeatureInfo = DEFAULT_DOC_FEAT
|
|
68
|
+
text_input_feat: FeatureInfo = FeatureInfo('text_input', input_key=None)
|
|
69
|
+
text_target_feat: Optional[FeatureInfo] = FeatureInfo('text_target', input_key=None)
|
|
70
|
+
question_prefix: Optional[str] = '<s_question>'
|
|
71
|
+
question_suffix: Optional[str] = '</s_question>'
|
|
72
|
+
answer_prefix: Optional[str] = '<s_answer>'
|
|
73
|
+
answer_suffix: Optional[str] = '</s_answer>'
|
|
74
|
+
# FIXME prompt templates instead of prefix+suffix above?
|
|
75
|
+
render_dpi: int = 144
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_processor_doc_vqa(cfg: DataTaskDocVqaCfg, image_process_fn: Optional[Callable] = None):
|
|
79
|
+
return DocVqaProcessor(
|
|
80
|
+
image_process_fn=image_process_fn if image_process_fn is not None else cfg.image_process_fn,
|
|
81
|
+
text_process_fn=cfg.text_process_fn,
|
|
82
|
+
image_input_feat=cfg.image_input_feat,
|
|
83
|
+
text_input_feat=cfg.text_input_feat,
|
|
84
|
+
text_target_feat=cfg.text_target_feat,
|
|
85
|
+
question_feat=cfg.question_feat,
|
|
86
|
+
answer_feat=cfg.answer_feat,
|
|
87
|
+
question_id_feat=cfg.question_id_feat,
|
|
88
|
+
render_dpi=cfg.render_dpi,
|
|
89
|
+
question_prefix=cfg.question_prefix,
|
|
90
|
+
question_suffix=cfg.question_suffix,
|
|
91
|
+
answer_prefix=cfg.answer_prefix,
|
|
92
|
+
answer_suffix=cfg.answer_suffix,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_task_pipeline_doc_vqa(
|
|
97
|
+
cfg: DataTaskDocVqaCfg,
|
|
98
|
+
):
|
|
99
|
+
# document decoding & pre-processing done together, there is coupling in random page
|
|
100
|
+
# selection and in the future, masking of image and/or text
|
|
101
|
+
handler = get_error_handler(cfg.error_handler)
|
|
102
|
+
|
|
103
|
+
pipe = [wds.map(create_processor_doc_vqa(cfg), handler=handler)]
|
|
104
|
+
|
|
105
|
+
if cfg.output_tuple:
|
|
106
|
+
# NOTE in this mode we lose '_parse' key and would need to derive from target
|
|
107
|
+
# Unless we add support for parse as the last tuple element?
|
|
108
|
+
if cfg.text_target_feat is not None:
|
|
109
|
+
pipe += [
|
|
110
|
+
wds.to_tuple(
|
|
111
|
+
cfg.image_input_feat.output_name,
|
|
112
|
+
cfg.text_input_feat.output_name,
|
|
113
|
+
cfg.text_target_feat.output_name,
|
|
114
|
+
)
|
|
115
|
+
]
|
|
116
|
+
else:
|
|
117
|
+
pipe += [
|
|
118
|
+
wds.to_tuple(
|
|
119
|
+
cfg.image_input_feat.output_name,
|
|
120
|
+
cfg.text_input_feat.output_name,
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
return pipe
|
|
124
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from chugr.common import DataTaskCfg
|
|
2
|
+
|
|
3
|
+
from .pipeline_doc_read import build_task_pipeline_doc_read, create_processor_doc_read, DataTaskDocReadCfg
|
|
4
|
+
from .pipeline_doc_vqa import build_task_pipeline_doc_vqa, create_processor_doc_vqa, DataTaskDocVqaCfg
|
|
5
|
+
from .pipeline_image_text import build_task_pipeline_image_text, DataTaskImageTextCfg
|
|
6
|
+
# from .donut_gtparse_pipe import
|
|
7
|
+
from .pipeline_manual import build_task_pipeline_manual, DataTaskManualCfg
|
|
8
|
+
|
|
9
|
+
_cfg_to_create = {
|
|
10
|
+
DataTaskDocReadCfg: build_task_pipeline_doc_read,
|
|
11
|
+
DataTaskDocVqaCfg: build_task_pipeline_doc_vqa,
|
|
12
|
+
DataTaskImageTextCfg: build_task_pipeline_image_text,
|
|
13
|
+
DataTaskManualCfg: build_task_pipeline_manual,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_task_pipeline(cfg: DataTaskCfg):
|
|
18
|
+
create_fn = _cfg_to_create[type(cfg)]
|
|
19
|
+
return create_fn(cfg)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_cfg_to_processor = {
|
|
23
|
+
DataTaskDocReadCfg: create_processor_doc_read,
|
|
24
|
+
DataTaskDocVqaCfg: create_processor_doc_vqa,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_task_processor(cfg: DataTaskCfg, image_process_fn=None):
|
|
29
|
+
""" Per-sample processor (decode + image / text processing) for tasks that support NaFlex bucketed batching. """
|
|
30
|
+
if type(cfg) not in _cfg_to_processor:
|
|
31
|
+
raise ValueError(f'NaFlex batching is not supported for task cfg {type(cfg).__name__}.')
|
|
32
|
+
return _cfg_to_processor[type(cfg)](cfg, image_process_fn=image_process_fn)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Callable, Optional, Union
|
|
4
|
+
|
|
5
|
+
import webdataset as wds
|
|
6
|
+
|
|
7
|
+
from chugr.common import DataTaskCfg, ImageFeatureInfo, FeatureInfo
|
|
8
|
+
from chugr.wds import get_error_handler, create_image_decoder
|
|
9
|
+
|
|
10
|
+
_DEFAULT_IMG_KEY = "jpg;png;jpeg;webp;tif"
|
|
11
|
+
_DEFAULT_TXT_KEY = "ground_truth"
|
|
12
|
+
_DEFAULT_IMG_KEY_TUPLE = tuple(_DEFAULT_IMG_KEY.split(';'))
|
|
13
|
+
_DEFAULT_TXT_KEY_TUPLE = tuple(_DEFAULT_TXT_KEY.split(';'))
|
|
14
|
+
|
|
15
|
+
_DEFAULT_IMAGE_FEAT = ImageFeatureInfo('image_input', input_key=_DEFAULT_IMG_KEY, image_mode='L')
|
|
16
|
+
_DEFAULT_TXT_FEAT = FeatureInfo('ground_truth', input_key=_DEFAULT_TXT_KEY)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DataTaskImageTextCfg(DataTaskCfg):
|
|
21
|
+
image_input_feat: ImageFeatureInfo = _DEFAULT_IMAGE_FEAT
|
|
22
|
+
text_input_feat: FeatureInfo = _DEFAULT_TXT_FEAT
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def filter_no_caption_or_no_image(
|
|
26
|
+
sample,
|
|
27
|
+
image_key=_DEFAULT_IMG_KEY_TUPLE,
|
|
28
|
+
text_key=_DEFAULT_TXT_KEY_TUPLE
|
|
29
|
+
):
|
|
30
|
+
has_caption = any(k in sample for k in text_key)
|
|
31
|
+
has_image = any(k in sample for k in image_key)
|
|
32
|
+
return has_caption and has_image
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_task_pipeline_gtparse(
|
|
36
|
+
cfg: DataTaskImageTextCfg,
|
|
37
|
+
):
|
|
38
|
+
""" Create pipeline for dual image & text input pipelines.
|
|
39
|
+
FIXME add support for caption target for caption tasks or separate pipe?
|
|
40
|
+
"""
|
|
41
|
+
handler = get_error_handler(cfg.error_handler)
|
|
42
|
+
pipe = []
|
|
43
|
+
|
|
44
|
+
if cfg.filter_valid:
|
|
45
|
+
filter_fn = partial(
|
|
46
|
+
filter_no_caption_or_no_image,
|
|
47
|
+
image_key=tuple(cfg.image_input_feat.input_key.split(';')),
|
|
48
|
+
text_key=tuple(cfg.text_input_feat.input_key.split(';')),
|
|
49
|
+
)
|
|
50
|
+
pipe += [
|
|
51
|
+
wds.select(filter_fn)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
if cfg.decode_and_process_fn:
|
|
55
|
+
pipe += [
|
|
56
|
+
wds.map(cfg.decode_and_process_fn)
|
|
57
|
+
]
|
|
58
|
+
else:
|
|
59
|
+
decode_fn = create_image_decoder(
|
|
60
|
+
cfg.decode_fn,
|
|
61
|
+
image_mode=cfg.image_input_feat.image_mode,
|
|
62
|
+
handler=handler,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
rename_dict = {
|
|
66
|
+
cfg.image_input_feat.output_name: cfg.text_input_feat.input_key,
|
|
67
|
+
cfg.text_input_feat.output_name: cfg.text_input_feat.input_key,
|
|
68
|
+
}
|
|
69
|
+
pipe += [
|
|
70
|
+
decode_fn,
|
|
71
|
+
wds.rename(**rename_dict),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
map_dict = {}
|
|
75
|
+
if cfg.image_process_fn is not None:
|
|
76
|
+
map_dict[cfg.image_input_feat.output_name] = cfg.image_process_fn
|
|
77
|
+
if cfg.text_process_fn is not None:
|
|
78
|
+
map_dict[cfg.text_input_feat.output_name] = cfg.text_process_fn
|
|
79
|
+
|
|
80
|
+
if map_dict:
|
|
81
|
+
pipe += [
|
|
82
|
+
wds.map_dict(**map_dict, handler=handler)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if cfg.output_tuple:
|
|
86
|
+
pipe += [
|
|
87
|
+
wds.to_tuple(
|
|
88
|
+
cfg.image_input_feat.output_name,
|
|
89
|
+
cfg.text_input_feat.output_name,
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
return pipe
|