Files
2026-07-13 13:17:40 +08:00

187 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Dict, Iterator, Tuple
import logging
from abc import ABC, abstractmethod
import multiprocessing
import torch
from torch.utils.data import IterableDataset
import ray
import ray.train
from constants import DatasetKey
from config import BenchmarkConfig, TorchConfig
from dataloader_factory import BaseDataLoaderFactory
from logger_utils import ContextLoggerAdapter
logger = ContextLoggerAdapter(logging.getLogger(__name__))
class TorchDataLoaderFactory(BaseDataLoaderFactory, ABC):
"""Factory for creating PyTorch DataLoaders."""
@staticmethod
def worker_init_fn(worker_id: int):
"""Initialize each worker with proper CUDA settings and seed.
Args:
worker_id: The ID of the worker being initialized
"""
# Set worker-specific seed for reproducibility
worker_seed = torch.initial_seed() % 2**32
torch.manual_seed(worker_seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(worker_seed)
torch.cuda.manual_seed_all(worker_seed)
logger.info(f"Initialized worker {worker_id} with seed {worker_seed}")
def __init__(
self,
benchmark_config: BenchmarkConfig,
):
"""Initialize the factory.
Args:
benchmark_config: Configuration for the benchmark
"""
super().__init__(benchmark_config)
dataloader_config = self.get_dataloader_config()
assert isinstance(dataloader_config, TorchConfig), type(dataloader_config)
# Get worker configuration
num_gpus = torch.cuda.device_count() if torch.cuda.is_available() else 1
self.num_torch_workers = dataloader_config.num_torch_workers
self.num_ray_workers = benchmark_config.num_workers
# Log configuration without worker rank since context may not be initialized
logger.info(
f"Configuration: {self.num_ray_workers * self.num_torch_workers} total workers "
f"({self.num_ray_workers} Ray × {self.num_torch_workers} Torch) "
f"across {num_gpus} GPUs"
)
def _get_device(self) -> torch.device:
"""Get the device for the current worker using Ray Train's device management."""
try:
device = ray.train.torch.get_device()
except RuntimeError:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
worker_rank = ray.train.get_context().get_world_rank()
logger.info(f"Worker {worker_rank}: Using device: {device}")
return device
@abstractmethod
def create_batch_iterator(
self, dataloader: torch.utils.data.DataLoader, device: torch.device
) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
"""Create a safe iterator that handles device transfer and error handling.
Args:
dataloader: The PyTorch DataLoader to iterate over
device: The device to move tensors to
Returns:
An iterator that yields batches moved to the specified device
"""
pass
@abstractmethod
def get_iterable_datasets(self) -> Dict[str, IterableDataset]:
"""Get the train and validation datasets.
Returns:
A dictionary containing the train and validation datasets.
"""
pass
def _create_multiprocessing_context(self):
# Importing libs in torch dataloader worker subprocesses is very slow.
# Preload some modules to speed up subprocess forking.
ctx = multiprocessing.get_context("forkserver")
modules = ["torch", "torchvision", "pandas", "numpy", "boto3", "fsspec"]
ctx.set_forkserver_preload(modules)
return ctx
def _create_dataloader(self, dataset_key: DatasetKey, batch_size: int):
worker_rank = ray.train.get_context().get_world_rank()
dataloader_config = self.get_dataloader_config()
# Create dataset and dataloader
ds = self.get_iterable_datasets()[dataset_key]
device = self._get_device()
# Adjust worker settings for 0 workers case
num_workers = max(0, self.num_torch_workers)
persistent_workers = num_workers > 0
pin_memory = dataloader_config.torch_pin_memory
if dataloader_config.torch_prefetch_factor >= 0:
prefetch_factor = dataloader_config.torch_prefetch_factor
else:
prefetch_factor = None
timeout = (
dataloader_config.torch_dataloader_timeout_seconds if num_workers > 0 else 0
)
logger.info(
f"Worker {worker_rank}: Creating train DataLoader with "
f"num_workers={num_workers}, pin_memory={pin_memory}, "
f"persistent_workers={persistent_workers}, prefetch_factor={prefetch_factor}, "
f"timeout={timeout}, batch_size={batch_size}"
)
multiprocessing_args = {}
if num_workers > 0:
multiprocessing_args = dict(
multiprocessing_context=self._create_multiprocessing_context(),
worker_init_fn=self.worker_init_fn,
persistent_workers=persistent_workers,
)
dataloader = torch.utils.data.DataLoader(
dataset=ds,
batch_size=batch_size,
num_workers=num_workers,
pin_memory=pin_memory,
prefetch_factor=prefetch_factor,
timeout=timeout,
drop_last=False,
**multiprocessing_args,
)
# Add a DistributedSampler to the dataloader if possible (map-style datasets)
dataloader = ray.train.torch.prepare_data_loader(
dataloader, move_to_device=False
)
return self.create_batch_iterator(dataloader, device)
def get_train_dataloader(self) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
"""Create a DataLoader for training data.
Returns:
An iterator that yields (image, label) tensors for training
"""
worker_rank = ray.train.get_context().get_world_rank()
logger.info(f"Worker {worker_rank}: Creating train dataloader")
return self._create_dataloader(
DatasetKey.TRAIN, self.get_dataloader_config().train_batch_size
)
def get_val_dataloader(self) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
"""Create a DataLoader for validation data.
Returns:
An iterator that yields (image, label) tensors for validation
"""
worker_rank = ray.train.get_context().get_world_rank()
logger.info(f"Worker {worker_rank}: Creating validation dataloader")
return self._create_dataloader(
DatasetKey.VALID, self.get_dataloader_config().validation_batch_size
)