iris-3b / iris3b /data /builder.py
lisperidlabs's picture
Iris-3B demo
2ede236 verified
Raw History Blame Contribute Delete
3.55 kB
"""Dataloader assembly producing the training batch contract."""
import random
import numpy as np
import torch
from torch.utils.data import DataLoader
from iris3b.config import DataConfig
from iris3b.data import datasets as _datasets # noqa: F401 - registers dataset types
from iris3b.data.samplers import RangedSampler, ShapeBatchSampler, key_cache_path
from iris3b.registry import DATASETS
def collate_batch(items: list[dict]) -> dict:
"""Stack per-sample dicts into {image, caption, index, img_hw, aspect_ratio}.
``index`` is the absolute dataset index. It is what makes the frozen
validation grid invariant to batching: (timestep, noise seed) key on the
sample, not on its position in the stream, so grouping by shape, changing
the batch size, or a future packed policy cannot silently re-label it.
"""
return {
"image": torch.stack([it["image"] for it in items]).float(),
"caption": [it["caption"] for it in items],
"index": torch.tensor([int(it["index"]) for it in items], dtype=torch.int64),
"img_hw": torch.stack([torch.as_tensor(it["img_hw"], dtype=torch.int64) for it in items]),
"aspect_ratio": torch.tensor([float(it["aspect_ratio"]) for it in items], dtype=torch.float32),
}
class _WorkerSeeder:
"""Deterministic per-worker seeding of the caption-sampling rngs."""
def __init__(self, base: int):
self.base = base
def __call__(self, worker_id: int) -> None:
s = (self.base + worker_id) % 2**32
random.seed(s)
np.random.seed(s)
torch.manual_seed(s)
def shape_policy_id(data_cfg: DataConfig, patch_size: int) -> str:
"""Identity of everything that changes a sample's shape key.
Part of the shape-plan cache path, so it must be derived in exactly one
place: a drifting id silently sends the pre-staged plan to a file no rank
ever reads.
"""
return (
f"{data_cfg.resolved_shape_policy()}:{data_cfg.image_size}:{data_cfg.aspect_ratio_bucket}"
f":{data_cfg.shape_align}:{data_cfg.shape_max_ratio}:{patch_size}"
)
def build_dataloader(
data_cfg: DataConfig,
batch_size: int,
rank: int,
world_size: int,
seed: int,
patch_size: int = 16,
) -> tuple[DataLoader, RangedSampler | ShapeBatchSampler]:
"""Build the per-rank loader; the returned sampler exposes set_epoch/set_start.
A uniform shape policy batches the rank's sequential chunk directly. Any
shape-varying policy routes it through shape-homogeneous batching
(drop_last), because the dense ``[B, C, H, W]`` collate below admits exactly
one shape per batch.
"""
dataset = DATASETS.build(data_cfg.type, data_cfg, patch_size)
dataset.seed = seed # keys positional caption sampling; trainer sets .epoch
sampler = RangedSampler(len(dataset), rank=rank, world_size=world_size)
common = {
"num_workers": data_cfg.num_workers,
"pin_memory": True,
"collate_fn": collate_batch,
"worker_init_fn": _WorkerSeeder(seed * 100_003 + rank * 1_009),
}
if dataset.policy.uniform:
return DataLoader(dataset, batch_size=batch_size, sampler=sampler, **common), sampler
policy_id = shape_policy_id(data_cfg, patch_size)
cache = key_cache_path(data_cfg.data_dirs, world_size, rank, policy_id) if data_cfg.data_dirs else None
batch_sampler = ShapeBatchSampler(sampler, dataset, batch_size, drop_last=True, cache_path=cache)
return DataLoader(dataset, batch_sampler=batch_sampler, **common), batch_sampler