Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit 9f729d3

Browse files
committed
feat: Multiresolution dataloader support
Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
1 parent 9eaace1 commit 9f729d3

8 files changed

Lines changed: 2648 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .text_to_image_dataset import TextToImageDataset
16+
from .multi_tier_bucketing import MultiTierBucketCalculator
17+
from .dataloader import (
18+
SequentialBucketSampler,
19+
build_multiresolution_dataloader,
20+
collate_fn_production,
21+
)
22+
23+
__all__ = [
24+
"TextToImageDataset",
25+
"MultiTierBucketCalculator",
26+
"SequentialBucketSampler",
27+
"build_multiresolution_dataloader",
28+
"collate_fn_production",
29+
]
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import math
16+
from typing import Dict, List, Tuple, Optional, Iterator
17+
18+
import torch
19+
import torch.distributed as dist
20+
from torch.utils.data import Sampler, DataLoader
21+
import logging
22+
23+
from dfm.src.automodel.datasets.multiresolutionDataloader.text_to_image_dataset import TextToImageDataset
24+
25+
logger = logging.getLogger(__name__)
26+
27+
class SequentialBucketSampler(Sampler[List[int]]):
28+
"""
29+
Production-grade Sampler that:
30+
1. Supports Distributed Data Parallel (DDP) - splits data across GPUs
31+
2. Deterministic shuffling via torch.Generator (resumable training)
32+
3. Lazy batch generation (saves RAM compared to pre-computing all batches)
33+
4. Guarantees equal batch counts across all ranks (prevents DDP deadlocks)
34+
35+
- Processes all images in bucket A before moving to bucket B
36+
- Shuffles samples within each bucket (deterministically)
37+
- Drops incomplete batches at end of each bucket
38+
- Uses dynamic batch sizes based on resolution
39+
"""
40+
41+
def __init__(
42+
self,
43+
dataset: TextToImageDataset,
44+
base_batch_size: int = 32,
45+
base_resolution: Tuple[int, int] = (512, 512),
46+
drop_last: bool = True,
47+
shuffle_buckets: bool = True,
48+
shuffle_within_bucket: bool = True,
49+
dynamic_batch_size: bool = False,
50+
seed: int = 42,
51+
num_replicas: Optional[int] = None,
52+
rank: Optional[int] = None,
53+
):
54+
"""
55+
Args:
56+
dataset: TextToImageDataset
57+
base_batch_size: Batch size (fixed if dynamic_batch_size=False,
58+
or base for scaling if dynamic_batch_size=True)
59+
base_resolution: Reference resolution for batch size scaling
60+
(only used if dynamic_batch_size=True)
61+
drop_last: Drop incomplete batches
62+
shuffle_buckets: Shuffle bucket order
63+
shuffle_within_bucket: Shuffle samples within each bucket
64+
dynamic_batch_size: If True, scale batch size based on resolution.
65+
If False (default), use base_batch_size for all buckets.
66+
seed: Random seed for deterministic shuffling (resumable training)
67+
num_replicas: Number of distributed processes (auto-detected if None)
68+
rank: Rank of current process (auto-detected if None)
69+
"""
70+
self.dataset = dataset
71+
self.base_batch_size = base_batch_size
72+
self.base_resolution = base_resolution
73+
self.drop_last = drop_last
74+
self.shuffle_buckets = shuffle_buckets
75+
self.shuffle_within_bucket = shuffle_within_bucket
76+
self.dynamic_batch_size = dynamic_batch_size
77+
self.seed = seed
78+
self.epoch = 0
79+
80+
# Handle Distributed Training (DDP)
81+
if num_replicas is None:
82+
if dist.is_available() and dist.is_initialized():
83+
num_replicas = dist.get_world_size()
84+
else:
85+
num_replicas = 1
86+
if rank is None:
87+
if dist.is_available() and dist.is_initialized():
88+
rank = dist.get_rank()
89+
else:
90+
rank = 0
91+
92+
self.num_replicas = num_replicas
93+
self.rank = rank
94+
95+
self.bucket_keys = dataset.sorted_bucket_keys
96+
self.bucket_groups = dataset.bucket_groups
97+
self.calculator = dataset.calculator
98+
99+
# Pre-calculate total batches (same for all ranks)
100+
self._total_batches = self._calculate_total_batches()
101+
102+
logger.info(f"\nSequentialBucketSampler created:")
103+
logger.info(f" Total batches per rank: {self._total_batches}")
104+
logger.info(f" Dynamic batch size: {dynamic_batch_size}")
105+
logger.info(f" Base batch size: {base_batch_size}" +
106+
(f" @ {base_resolution}" if dynamic_batch_size else " (fixed)"))
107+
logger.info(f" DDP: rank {self.rank} of {self.num_replicas}")
108+
109+
def _get_batch_size(self, resolution: Tuple[int, int]) -> int:
110+
"""Get batch size for resolution (dynamic or fixed based on setting)."""
111+
if not self.dynamic_batch_size:
112+
return self.base_batch_size
113+
114+
return self.calculator.get_dynamic_batch_size(
115+
resolution,
116+
self.base_batch_size,
117+
self.base_resolution,
118+
)
119+
120+
def _calculate_total_batches(self) -> int:
121+
"""
122+
Calculate total batches ensuring ALL ranks get the same count.
123+
We pad each bucket to be divisible by (num_replicas * batch_size).
124+
"""
125+
count = 0
126+
for bucket_key in self.bucket_keys:
127+
total_indices = len(self.bucket_groups[bucket_key]['indices'])
128+
batch_size = self._get_batch_size(self.bucket_groups[bucket_key]['resolution'])
129+
130+
# Pad to make divisible by num_replicas first
131+
padded_total = math.ceil(total_indices / self.num_replicas) * self.num_replicas
132+
per_rank_indices = padded_total // self.num_replicas
133+
134+
if self.drop_last:
135+
count += per_rank_indices // batch_size
136+
else:
137+
count += (per_rank_indices + batch_size - 1) // batch_size
138+
139+
return count
140+
141+
def set_epoch(self, epoch: int):
142+
"""Crucial for reproducibility and different shuffles per epoch."""
143+
self.epoch = epoch
144+
145+
def __iter__(self) -> Iterator[List[int]]:
146+
# Deterministic generator - SAME seed across all ranks
147+
g = torch.Generator()
148+
g.manual_seed(self.seed + self.epoch)
149+
150+
# 1. Bucket Order Shuffling (deterministic, same across all ranks)
151+
current_bucket_keys = list(self.bucket_keys)
152+
if self.shuffle_buckets:
153+
perm = torch.randperm(len(current_bucket_keys), generator=g).tolist()
154+
current_bucket_keys = [current_bucket_keys[i] for i in perm]
155+
156+
# 2. Iterate Buckets
157+
for key in current_bucket_keys:
158+
bucket = self.bucket_groups[key]
159+
indices = bucket['indices'].copy()
160+
resolution = bucket['resolution']
161+
batch_size = self._get_batch_size(resolution)
162+
163+
# 3. Deterministic Shuffle within bucket (same across all ranks)
164+
if self.shuffle_within_bucket:
165+
rand_indices = torch.randperm(len(indices), generator=g).tolist()
166+
indices = [indices[i] for i in rand_indices]
167+
168+
# 4. Pad indices to ensure equal distribution across ranks
169+
total_size = math.ceil(len(indices) / self.num_replicas) * self.num_replicas
170+
padding_size = total_size - len(indices)
171+
if padding_size > 0:
172+
# Pad by repeating indices from the beginning
173+
indices = indices + indices[:padding_size]
174+
175+
# 5. DDP Splitting: Subsample indices for this rank
176+
indices = indices[self.rank :: self.num_replicas]
177+
178+
# 6. Yield Batches (Lazy Evaluation)
179+
for i in range(0, len(indices), batch_size):
180+
batch = indices[i : i + batch_size]
181+
182+
if self.drop_last and len(batch) < batch_size:
183+
continue
184+
185+
if not batch:
186+
continue
187+
188+
yield batch
189+
190+
def __len__(self) -> int:
191+
return self._total_batches
192+
193+
def get_batch_info(self, batch_idx: int) -> Dict:
194+
"""Get information about a specific batch.
195+
196+
Note: With lazy evaluation, we don't pre-compute batches,
197+
so this returns bucket-level info for the estimated batch.
198+
"""
199+
# Estimate which bucket this batch belongs to
200+
running_count = 0
201+
for bucket_key in self.bucket_keys:
202+
bucket = self.bucket_groups[bucket_key]
203+
total_indices = len(bucket['indices'])
204+
batch_size = self._get_batch_size(bucket['resolution'])
205+
206+
padded_total = math.ceil(total_indices / self.num_replicas) * self.num_replicas
207+
per_rank_indices = padded_total // self.num_replicas
208+
209+
if self.drop_last:
210+
num_batches = per_rank_indices // batch_size
211+
else:
212+
num_batches = (per_rank_indices + batch_size - 1) // batch_size
213+
214+
if batch_idx < running_count + num_batches:
215+
return {
216+
'bucket_key': bucket_key,
217+
'resolution': bucket['resolution'],
218+
'batch_size': batch_size,
219+
'aspect_name': bucket['aspect_name'],
220+
}
221+
running_count += num_batches
222+
223+
return {}
224+
225+
226+
def collate_fn_production(batch: List[Dict]) -> Dict:
227+
"""Production collate function with verification."""
228+
# Verify all samples have same resolution
229+
resolutions = [tuple(item["crop_resolution"].tolist()) for item in batch]
230+
assert len(set(resolutions)) == 1, f"Mixed resolutions in batch: {set(resolutions)}"
231+
232+
# Stack tensors
233+
latents = torch.stack([item["latent"] for item in batch])
234+
crop_resolutions = torch.stack([item["crop_resolution"] for item in batch])
235+
original_resolutions = torch.stack([item["original_resolution"] for item in batch])
236+
crop_offsets = torch.stack([item["crop_offset"] for item in batch])
237+
238+
# Collect metadata
239+
prompts = [item["prompt"] for item in batch]
240+
image_paths = [item["image_path"] for item in batch]
241+
bucket_ids = [item["bucket_id"] for item in batch]
242+
aspect_ratios = [item["aspect_ratio"] for item in batch]
243+
244+
output = {
245+
"latent": latents,
246+
"crop_resolution": crop_resolutions,
247+
"original_resolution": original_resolutions,
248+
"crop_offset": crop_offsets,
249+
"prompt": prompts,
250+
"image_path": image_paths,
251+
"bucket_id": bucket_ids,
252+
"aspect_ratio": aspect_ratios,
253+
}
254+
255+
# Handle text encodings
256+
if "clip_hidden" in batch[0]:
257+
output["clip_hidden"] = torch.stack([item["clip_hidden"] for item in batch])
258+
output["clip_pooled"] = torch.stack([item["clip_pooled"] for item in batch])
259+
output["t5_hidden"] = torch.stack([item["t5_hidden"] for item in batch])
260+
else:
261+
output["clip_tokens"] = torch.stack([item["clip_tokens"] for item in batch])
262+
output["t5_tokens"] = torch.stack([item["t5_tokens"] for item in batch])
263+
264+
return output
265+
266+
267+
def build_multiresolution_dataloader(
268+
*,
269+
dataset: TextToImageDataset,
270+
base_batch_size: int,
271+
dp_rank: int,
272+
dp_world_size: int,
273+
base_resolution: Tuple[int, int] = (512, 512),
274+
drop_last: bool = True,
275+
shuffle: bool = True,
276+
dynamic_batch_size: bool = False,
277+
num_workers: int = 4,
278+
pin_memory: bool = True,
279+
prefetch_factor: int = 2,
280+
) -> Tuple[DataLoader, SequentialBucketSampler]:
281+
"""
282+
Build production dataloader with sequential bucket iteration and distributed training support.
283+
284+
Args:
285+
dataset: TextToImageDataset instance
286+
base_batch_size: Batch size (fixed, or base for scaling if dynamic_batch_size=True)
287+
dp_rank: Rank of current process in data parallel group
288+
dp_world_size: Total number of processes in data parallel group
289+
base_resolution: Reference resolution (only used if dynamic_batch_size=True)
290+
drop_last: Drop incomplete batches
291+
shuffle: Shuffle bucket order and samples within buckets each epoch
292+
dynamic_batch_size: If True, scale batch size based on resolution.
293+
If False (default), use base_batch_size for all buckets.
294+
num_workers: Number of data loading workers
295+
pin_memory: Pin memory for faster GPU transfer
296+
prefetch_factor: How many batches to prefetch per worker
297+
298+
Returns:
299+
Tuple of (DataLoader, SequentialBucketSampler) for production training
300+
"""
301+
sampler = SequentialBucketSampler(
302+
dataset,
303+
base_batch_size=base_batch_size,
304+
base_resolution=base_resolution,
305+
drop_last=drop_last,
306+
shuffle_buckets=shuffle,
307+
shuffle_within_bucket=shuffle,
308+
dynamic_batch_size=dynamic_batch_size,
309+
num_replicas=dp_world_size,
310+
rank=dp_rank,
311+
)
312+
313+
dataloader = DataLoader(
314+
dataset,
315+
batch_sampler=sampler,
316+
collate_fn=collate_fn_production,
317+
num_workers=num_workers,
318+
pin_memory=pin_memory,
319+
prefetch_factor=prefetch_factor if num_workers > 0 else None,
320+
persistent_workers=num_workers > 0, # Keep workers alive between epochs
321+
)
322+
323+
return dataloader, sampler

0 commit comments

Comments
 (0)