chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
# Standard library imports
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Tuple, Iterator, Generator, Optional, Union
|
||||
|
||||
# Third-party imports
|
||||
import torch
|
||||
import torchvision
|
||||
import pyarrow
|
||||
import ray
|
||||
import ray.train
|
||||
from ray.data.collate_fn import ArrowBatchCollateFn, CollateFn
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from ray.data.dataset import TorchDeviceType
|
||||
|
||||
# Local imports
|
||||
from benchmark_factory import BenchmarkFactory
|
||||
from config import BenchmarkConfig, DataloaderType, ImageClassificationConfig
|
||||
from dataloader_factory import BaseDataLoaderFactory
|
||||
from torch_dataloader_factory import TorchDataLoaderFactory
|
||||
from ray_dataloader_factory import RayDataLoaderFactory
|
||||
from logger_utils import ContextLoggerAdapter
|
||||
|
||||
logger = ContextLoggerAdapter(logging.getLogger(__name__))
|
||||
|
||||
|
||||
def mock_dataloader(
|
||||
num_batches: int = 64, batch_size: int = 32
|
||||
) -> Generator[Tuple[torch.Tensor, torch.Tensor], None, None]:
|
||||
"""Generate mock image and label tensors for testing.
|
||||
|
||||
Args:
|
||||
num_batches: Number of batches to generate
|
||||
batch_size: Number of samples per batch
|
||||
|
||||
Yields:
|
||||
Tuple of (image_tensor, label_tensor) for each batch
|
||||
"""
|
||||
device = ray.train.torch.get_device()
|
||||
|
||||
images = torch.randn(batch_size, 3, 224, 224).to(device)
|
||||
labels = torch.randint(0, 1000, (batch_size,)).to(device)
|
||||
|
||||
for _ in range(num_batches):
|
||||
yield images, labels
|
||||
|
||||
|
||||
class ImageClassificationTorchDataLoaderFactory(TorchDataLoaderFactory):
|
||||
"""Factory for creating PyTorch DataLoaders for image classification tasks.
|
||||
|
||||
Features:
|
||||
- Distributed file reading with round-robin worker distribution
|
||||
- Device transfer and error handling for data batches
|
||||
- Configurable row limits per worker for controlled processing
|
||||
- Performance monitoring and logging
|
||||
"""
|
||||
|
||||
def __init__(self, benchmark_config: BenchmarkConfig):
|
||||
super().__init__(benchmark_config)
|
||||
|
||||
def _calculate_rows_per_worker(
|
||||
self, total_rows: int, num_workers: int
|
||||
) -> Optional[int]:
|
||||
"""Calculate rows per worker for balanced data distribution.
|
||||
|
||||
Args:
|
||||
total_rows: Total rows to process across all workers (-1 for unlimited)
|
||||
num_workers: Total workers (Ray workers × Torch workers)
|
||||
|
||||
Returns:
|
||||
Rows per worker or None if no limit. Each worker gets at least 1 row.
|
||||
"""
|
||||
if total_rows < 0:
|
||||
return None
|
||||
|
||||
if num_workers == 0:
|
||||
return total_rows
|
||||
|
||||
return max(1, total_rows // num_workers)
|
||||
|
||||
def _get_worker_row_limits(self) -> Tuple[Optional[int], Optional[int]]:
|
||||
"""Calculate row limits per worker for training and validation.
|
||||
|
||||
Returns:
|
||||
Tuple of (training_rows_per_worker, validation_rows_per_worker)
|
||||
"""
|
||||
dataloader_config = self.get_dataloader_config()
|
||||
num_workers = max(1, dataloader_config.num_torch_workers)
|
||||
total_workers = self.benchmark_config.num_workers * num_workers
|
||||
|
||||
limit_training_rows_per_worker = self._calculate_rows_per_worker(
|
||||
self.get_dataloader_config().limit_training_rows, total_workers
|
||||
)
|
||||
|
||||
limit_validation_rows_per_worker = self._calculate_rows_per_worker(
|
||||
self.get_dataloader_config().limit_validation_rows, total_workers
|
||||
)
|
||||
|
||||
return limit_training_rows_per_worker, limit_validation_rows_per_worker
|
||||
|
||||
def create_batch_iterator(
|
||||
self, dataloader: torch.utils.data.DataLoader, device: torch.device
|
||||
) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Create iterator with device transfer and error handling.
|
||||
|
||||
Args:
|
||||
dataloader: PyTorch DataLoader to iterate over
|
||||
device: Target device for tensor transfer
|
||||
|
||||
Returns:
|
||||
Iterator yielding (image_tensor, label_tensor) on target device
|
||||
"""
|
||||
worker_rank = ray.train.get_context().get_world_rank()
|
||||
logger.info(f"Worker {worker_rank}: Starting batch iteration")
|
||||
|
||||
try:
|
||||
last_batch_time = time.time()
|
||||
for batch_idx, batch in enumerate(dataloader):
|
||||
try:
|
||||
# Monitor batch processing delays
|
||||
current_time = time.time()
|
||||
time_since_last_batch = current_time - last_batch_time
|
||||
if time_since_last_batch > 10:
|
||||
logger.warning(
|
||||
f"Worker {worker_rank}: Long delay ({time_since_last_batch:.2f}s) "
|
||||
f"between batches {batch_idx-1} and {batch_idx}"
|
||||
)
|
||||
|
||||
# Process and transfer batch to device
|
||||
images, labels = batch
|
||||
logger.info(
|
||||
f"Worker {worker_rank}: Processing batch {batch_idx} (shape: {images.shape}, "
|
||||
f"time since last: {time_since_last_batch:.2f}s)"
|
||||
)
|
||||
|
||||
# Transfer tensors to target device
|
||||
transfer_start = time.time()
|
||||
dataloader_config = self.get_dataloader_config()
|
||||
images = images.to(
|
||||
device, non_blocking=dataloader_config.torch_non_blocking
|
||||
)
|
||||
labels = labels.to(
|
||||
device, non_blocking=dataloader_config.torch_non_blocking
|
||||
)
|
||||
transfer_time = time.time() - transfer_start
|
||||
|
||||
# Monitor device transfer performance
|
||||
if transfer_time > 5:
|
||||
logger.warning(
|
||||
f"Worker {worker_rank}: Slow device transfer ({transfer_time:.2f}s) "
|
||||
f"for batch {batch_idx}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Worker {worker_rank}: Completed device transfer for batch {batch_idx} in "
|
||||
f"{transfer_time:.2f}s"
|
||||
)
|
||||
|
||||
last_batch_time = time.time()
|
||||
yield images, labels
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Worker {worker_rank}: Error processing batch {batch_idx}: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Worker {worker_rank}: Error in batch iterator: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class CustomArrowCollateFn(ArrowBatchCollateFn):
|
||||
"""Custom collate function for converting Arrow batches to PyTorch tensors."""
|
||||
|
||||
_DEFAULT_NUM_WORKERS = 4
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dtypes: Optional[Union["torch.dtype", Dict[str, "torch.dtype"]]] = None,
|
||||
device: Optional["TorchDeviceType"] = None,
|
||||
pin_memory: bool = False,
|
||||
num_workers: int = _DEFAULT_NUM_WORKERS,
|
||||
):
|
||||
"""Initialize the collate function.
|
||||
|
||||
Args:
|
||||
dtypes: Optional torch dtype(s) for the tensors
|
||||
device: Optional device to place tensors on
|
||||
pin_memory: Whether to pin the memory of the created tensors
|
||||
num_workers: Number of worker threads for parallel tensor conversion
|
||||
Defaults to `_DEFAULT_NUM_WORKERS`.
|
||||
"""
|
||||
import torch
|
||||
|
||||
self.dtypes = dtypes
|
||||
if isinstance(device, (str, int)):
|
||||
self.device = torch.device(device)
|
||||
else:
|
||||
self.device = device
|
||||
self.pin_memory = pin_memory
|
||||
self.num_workers = num_workers
|
||||
self._threadpool: Optional[ThreadPoolExecutor] = None
|
||||
|
||||
def __del__(self):
|
||||
"""Clean up threadpool on destruction."""
|
||||
if getattr(self, "_threadpool", None):
|
||||
self._threadpool.shutdown(wait=False)
|
||||
|
||||
def __call__(self, batch: "pyarrow.Table") -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Convert an Arrow batch to PyTorch tensors.
|
||||
|
||||
Args:
|
||||
batch: PyArrow Table to convert
|
||||
|
||||
Returns:
|
||||
Tuple of (image_tensor, label_tensor)
|
||||
"""
|
||||
from ray.data.util.torch_utils import (
|
||||
arrow_batch_to_tensors,
|
||||
)
|
||||
|
||||
if self.num_workers > 0 and self._threadpool is None:
|
||||
self._threadpool = ThreadPoolExecutor(max_workers=self.num_workers)
|
||||
|
||||
# For GPU transfer, we can skip the combining chunked arrays. This is because
|
||||
# we can convert the chunked arrays to corresponding numpy format and then to
|
||||
# Tensors and transfer the corresponding list of Tensors to GPU directly.
|
||||
# However, for CPU transfer, we need to combine the chunked arrays first
|
||||
# before converting to numpy format and then to Tensors.
|
||||
combine_chunks = self.device is not None and self.device.type == "cpu"
|
||||
tensors = arrow_batch_to_tensors(
|
||||
batch,
|
||||
dtypes=self.dtypes,
|
||||
combine_chunks=combine_chunks,
|
||||
pin_memory=self.pin_memory,
|
||||
threadpool=self._threadpool,
|
||||
)
|
||||
return tensors["image"], tensors["label"]
|
||||
|
||||
|
||||
class ImageClassificationRayDataLoaderFactory(RayDataLoaderFactory):
|
||||
"""Factory for creating Ray DataLoader for image classification tasks."""
|
||||
|
||||
def __init__(self, benchmark_config: BenchmarkConfig):
|
||||
super().__init__(benchmark_config)
|
||||
|
||||
def _get_collate_fn(self) -> Optional[CollateFn]:
|
||||
return CustomArrowCollateFn(
|
||||
device=ray.train.torch.get_device(),
|
||||
pin_memory=self.get_dataloader_config().ray_data_pin_memory,
|
||||
)
|
||||
|
||||
|
||||
class ImageClassificationMockDataLoaderFactory(BaseDataLoaderFactory):
|
||||
"""Factory for creating mock dataloaders for testing.
|
||||
|
||||
Provides mock implementations of training and validation dataloaders
|
||||
that generate random image and label tensors.
|
||||
"""
|
||||
|
||||
def get_train_dataloader(
|
||||
self,
|
||||
) -> Generator[Tuple[torch.Tensor, torch.Tensor], None, None]:
|
||||
"""Get mock training dataloader.
|
||||
|
||||
Returns:
|
||||
Generator yielding (image_tensor, label_tensor) batches
|
||||
"""
|
||||
dataloader_config = self.get_dataloader_config()
|
||||
return mock_dataloader(
|
||||
num_batches=1024, batch_size=dataloader_config.train_batch_size
|
||||
)
|
||||
|
||||
def get_val_dataloader(
|
||||
self,
|
||||
) -> Generator[Tuple[torch.Tensor, torch.Tensor], None, None]:
|
||||
"""Get mock validation dataloader.
|
||||
|
||||
Returns:
|
||||
Generator yielding (image_tensor, label_tensor) batches
|
||||
"""
|
||||
dataloader_config = self.get_dataloader_config()
|
||||
return mock_dataloader(
|
||||
num_batches=512, batch_size=dataloader_config.validation_batch_size
|
||||
)
|
||||
|
||||
|
||||
def get_imagenet_data_dirs(task_config: ImageClassificationConfig) -> Dict[str, str]:
|
||||
"""Returns a dict with the root imagenet dataset directories for train/val/test,
|
||||
corresponding to the data format and local/s3 dataset location."""
|
||||
from image_classification.imagenet import IMAGENET_LOCALFS_SPLIT_DIRS
|
||||
from image_classification.jpeg.imagenet import (
|
||||
IMAGENET_JPEG_SPLIT_S3_DIRS,
|
||||
)
|
||||
from image_classification.parquet.imagenet import (
|
||||
IMAGENET_PARQUET_SPLIT_S3_DIRS,
|
||||
IMAGENET_PARQUET_SPLIT_1T_S3_DIRS,
|
||||
)
|
||||
from image_classification.s3_url.imagenet import (
|
||||
IMAGENET_S3_URL_SPLIT_DIRS,
|
||||
)
|
||||
|
||||
data_format = task_config.image_classification_data_format
|
||||
|
||||
if task_config.image_classification_local_dataset:
|
||||
return IMAGENET_LOCALFS_SPLIT_DIRS
|
||||
|
||||
if data_format == ImageClassificationConfig.ImageFormat.JPEG:
|
||||
return IMAGENET_JPEG_SPLIT_S3_DIRS
|
||||
elif data_format == ImageClassificationConfig.ImageFormat.PARQUET:
|
||||
if task_config.image_classification_use_1t_dataset:
|
||||
return IMAGENET_PARQUET_SPLIT_1T_S3_DIRS
|
||||
return IMAGENET_PARQUET_SPLIT_S3_DIRS
|
||||
elif data_format == ImageClassificationConfig.ImageFormat.S3_URL:
|
||||
return IMAGENET_S3_URL_SPLIT_DIRS
|
||||
else:
|
||||
raise ValueError(f"Unknown data format: {data_format}")
|
||||
|
||||
|
||||
class ImageClassificationFactory(BenchmarkFactory):
|
||||
def get_dataloader_factory(self) -> BaseDataLoaderFactory:
|
||||
dataloader_type = self.benchmark_config.dataloader_type
|
||||
task_config = self.benchmark_config.task_config
|
||||
assert isinstance(task_config, ImageClassificationConfig)
|
||||
|
||||
data_dirs = get_imagenet_data_dirs(task_config)
|
||||
|
||||
data_format = task_config.image_classification_data_format
|
||||
|
||||
if dataloader_type == DataloaderType.MOCK:
|
||||
return ImageClassificationMockDataLoaderFactory(self.benchmark_config)
|
||||
|
||||
elif dataloader_type == DataloaderType.RAY_DATA:
|
||||
if data_format == ImageClassificationConfig.ImageFormat.JPEG:
|
||||
from image_classification.jpeg.factory import (
|
||||
ImageClassificationJpegRayDataLoaderFactory,
|
||||
)
|
||||
|
||||
return ImageClassificationJpegRayDataLoaderFactory(
|
||||
self.benchmark_config, data_dirs
|
||||
)
|
||||
elif data_format == ImageClassificationConfig.ImageFormat.PARQUET:
|
||||
from image_classification.parquet.factory import (
|
||||
ImageClassificationParquetRayDataLoaderFactory,
|
||||
)
|
||||
|
||||
return ImageClassificationParquetRayDataLoaderFactory(
|
||||
self.benchmark_config, data_dirs
|
||||
)
|
||||
elif data_format == ImageClassificationConfig.ImageFormat.S3_URL:
|
||||
# NOTE: This format downloads images via ray data expressions,
|
||||
# which is less efficient than native Ray Data S3 reading (JPEG format or Parquet format).
|
||||
# Use this primarily for testing the S3 URL download pattern.
|
||||
from image_classification.s3_url.factory import (
|
||||
ImageClassificationS3UrlRayDataLoaderFactory,
|
||||
)
|
||||
|
||||
return ImageClassificationS3UrlRayDataLoaderFactory(
|
||||
self.benchmark_config, data_dirs
|
||||
)
|
||||
|
||||
elif dataloader_type == DataloaderType.TORCH:
|
||||
if data_format == ImageClassificationConfig.ImageFormat.JPEG:
|
||||
from image_classification.jpeg.factory import (
|
||||
ImageClassificationJpegTorchDataLoaderFactory,
|
||||
)
|
||||
|
||||
return ImageClassificationJpegTorchDataLoaderFactory(
|
||||
self.benchmark_config, data_dirs
|
||||
)
|
||||
elif data_format == ImageClassificationConfig.ImageFormat.PARQUET:
|
||||
from image_classification.parquet.factory import (
|
||||
ImageClassificationParquetTorchDataLoaderFactory,
|
||||
)
|
||||
|
||||
return ImageClassificationParquetTorchDataLoaderFactory(
|
||||
self.benchmark_config, data_dirs
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid dataloader configuration: {dataloader_type}\n"
|
||||
f"{task_config}\n{self.benchmark_config.dataloader_config}"
|
||||
)
|
||||
|
||||
def get_model(self) -> torch.nn.Module:
|
||||
return torchvision.models.resnet50(weights=None)
|
||||
|
||||
def get_loss_fn(self) -> torch.nn.Module:
|
||||
return torch.nn.CrossEntropyLoss()
|
||||
File diff suppressed because it is too large
Load Diff
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
DATA_DIR="/mnt/local_storage/imagenet"
|
||||
ZIP_NAME="imagenet-64k.zip"
|
||||
ZIP_URL="s3://anyscale-imagenet/ILSVRC/Data/CLS-LOC/$ZIP_NAME"
|
||||
ZIP_PATH="$DATA_DIR/$ZIP_NAME"
|
||||
TRAIN_DIR="$DATA_DIR/train"
|
||||
|
||||
if [ ! -d "$TRAIN_DIR" ]; then
|
||||
echo "Downloading and extracting ImageNet subset to $DATA_DIR..."
|
||||
mkdir -p "$DATA_DIR"
|
||||
pushd "$DATA_DIR" || exit
|
||||
|
||||
echo "Fetching $ZIP_URL..."
|
||||
aws s3 cp "$ZIP_URL" "$ZIP_PATH"
|
||||
|
||||
echo "Unzipping..."
|
||||
unzip -q "$ZIP_NAME"
|
||||
rm "$ZIP_NAME"
|
||||
|
||||
popd || exit
|
||||
else
|
||||
echo "Dataset already exists at $TRAIN_DIR. Skipping download and unzip."
|
||||
fi
|
||||
|
||||
echo "Duplicating images in-place..."
|
||||
|
||||
python3 <<EOF
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tqdm import tqdm
|
||||
|
||||
dataset_dir = Path("$TRAIN_DIR")
|
||||
|
||||
for class_dir in tqdm(sorted(dataset_dir.iterdir()), desc="Processing classes"):
|
||||
if not class_dir.is_dir():
|
||||
continue
|
||||
|
||||
# Skip if already duplicated
|
||||
if any(class_dir.glob("*_copy1.JPEG")):
|
||||
print(f"Skipping {class_dir.name} (already duplicated)")
|
||||
continue
|
||||
|
||||
for img_path in class_dir.glob("*.JPEG"):
|
||||
for i in range(1, 8):
|
||||
copy_name = img_path.stem + f"_copy{i}" + img_path.suffix
|
||||
copy_path = class_dir / copy_name
|
||||
shutil.copy2(img_path, copy_path)
|
||||
EOF
|
||||
|
||||
echo "Image duplication complete."
|
||||
@@ -0,0 +1,214 @@
|
||||
# Standard library imports
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
# Third-party imports
|
||||
import torchvision
|
||||
from torch.utils.data import IterableDataset
|
||||
import pyarrow.fs
|
||||
|
||||
# Ray imports
|
||||
import ray.train
|
||||
from ray.data.datasource.partitioning import Partitioning
|
||||
|
||||
# Local imports
|
||||
from constants import DatasetKey
|
||||
from config import BenchmarkConfig
|
||||
from image_classification.factory import (
|
||||
ImageClassificationRayDataLoaderFactory,
|
||||
ImageClassificationTorchDataLoaderFactory,
|
||||
)
|
||||
from image_classification.imagenet import get_transform
|
||||
from s3_reader import AWS_REGION
|
||||
from .imagenet import get_preprocess_map_fn
|
||||
from .jpeg_iterable_dataset import S3JpegImageIterableDataset
|
||||
from s3_jpeg_reader import S3JpegReader
|
||||
from logger_utils import ContextLoggerAdapter
|
||||
|
||||
logger = ContextLoggerAdapter(logging.getLogger(__name__))
|
||||
|
||||
|
||||
class ImageClassificationJpegRayDataLoaderFactory(
|
||||
ImageClassificationRayDataLoaderFactory
|
||||
):
|
||||
"""Factory for creating Ray DataLoader for JPEG image classification.
|
||||
|
||||
Extends ImageClassificationRayDataLoaderFactory to provide:
|
||||
1. S3 filesystem configuration with boto credentials
|
||||
2. Ray dataset creation with partitioning by class
|
||||
3. Resource allocation for concurrent validation
|
||||
4. Image preprocessing with optional random transforms
|
||||
"""
|
||||
|
||||
def __init__(self, benchmark_config: BenchmarkConfig, dataset_dirs: Dict[str, str]):
|
||||
super().__init__(benchmark_config)
|
||||
self._dataset_dirs = dataset_dirs
|
||||
|
||||
def get_s3fs_with_boto_creds(
|
||||
self, connection_timeout: int = 60, request_timeout: int = 60
|
||||
) -> pyarrow.fs.S3FileSystem:
|
||||
"""Create S3 filesystem with boto credentials.
|
||||
|
||||
Args:
|
||||
connection_timeout: Timeout for establishing connection in seconds
|
||||
request_timeout: Timeout for requests in seconds
|
||||
|
||||
Returns:
|
||||
Configured S3FileSystem instance with boto credentials
|
||||
"""
|
||||
import boto3
|
||||
|
||||
credentials = boto3.Session().get_credentials()
|
||||
|
||||
s3fs = pyarrow.fs.S3FileSystem(
|
||||
access_key=credentials.access_key,
|
||||
secret_key=credentials.secret_key,
|
||||
session_token=credentials.token,
|
||||
region=AWS_REGION,
|
||||
connect_timeout=connection_timeout,
|
||||
request_timeout=request_timeout,
|
||||
)
|
||||
return s3fs
|
||||
|
||||
def get_ray_datasets(self) -> Dict[str, ray.data.Dataset]:
|
||||
"""Get Ray datasets for training and validation.
|
||||
|
||||
Creates training and validation datasets with:
|
||||
1. Partitioning by class for efficient data loading
|
||||
2. Image preprocessing with optional random transforms
|
||||
3. Resource allocation for concurrent validation
|
||||
4. Row limits based on benchmark configuration
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- "train": Training dataset with random transforms
|
||||
- "val": Validation dataset without transforms
|
||||
"""
|
||||
train_dir = self._dataset_dirs[DatasetKey.TRAIN]
|
||||
# TODO: The validation dataset directory is not partitioned by class.
|
||||
val_dir = train_dir
|
||||
|
||||
filesystem = (
|
||||
self.get_s3fs_with_boto_creds() if train_dir.startswith("s3://") else None
|
||||
)
|
||||
|
||||
# Create training dataset with class-based partitioning
|
||||
train_partitioning = Partitioning(
|
||||
"dir", base_dir=train_dir, field_names=["class"]
|
||||
)
|
||||
train_ds = ray.data.read_images(
|
||||
train_dir,
|
||||
mode="RGB",
|
||||
include_paths=False,
|
||||
partitioning=train_partitioning,
|
||||
filesystem=filesystem,
|
||||
).map(get_preprocess_map_fn(random_transforms=True))
|
||||
|
||||
if self.get_dataloader_config().limit_training_rows > 0:
|
||||
train_ds = train_ds.limit(self.get_dataloader_config().limit_training_rows)
|
||||
|
||||
# Create validation dataset with same partitioning
|
||||
val_partitioning = Partitioning("dir", base_dir=val_dir, field_names=["class"])
|
||||
val_ds = ray.data.read_images(
|
||||
val_dir,
|
||||
mode="RGB",
|
||||
include_paths=False,
|
||||
partitioning=val_partitioning,
|
||||
filesystem=filesystem,
|
||||
).map(get_preprocess_map_fn(random_transforms=False))
|
||||
|
||||
if self.get_dataloader_config().limit_validation_rows > 0:
|
||||
val_ds = val_ds.limit(self.get_dataloader_config().limit_validation_rows)
|
||||
|
||||
return {
|
||||
DatasetKey.TRAIN: train_ds,
|
||||
DatasetKey.VALID: val_ds,
|
||||
}
|
||||
|
||||
|
||||
class ImageClassificationJpegTorchDataLoaderFactory(
|
||||
ImageClassificationTorchDataLoaderFactory, S3JpegReader
|
||||
):
|
||||
"""Factory for creating PyTorch DataLoaders for JPEG image classification.
|
||||
|
||||
Features:
|
||||
- S3-based JPEG file reading with round-robin worker distribution
|
||||
- Device transfer and error handling for data batches
|
||||
- Row limits per worker for controlled processing
|
||||
- Dataset caching for efficiency
|
||||
"""
|
||||
|
||||
def __init__(self, benchmark_config: BenchmarkConfig, data_dirs: Dict[str, str]):
|
||||
super().__init__(benchmark_config)
|
||||
S3JpegReader.__init__(self) # Initialize S3JpegReader to set up _s3_client
|
||||
self._data_dirs = data_dirs
|
||||
self._cached_datasets = None
|
||||
|
||||
def get_iterable_datasets(self) -> Dict[str, IterableDataset]:
|
||||
"""Get train and validation datasets with worker-specific configurations.
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- "train": Training dataset with random transforms
|
||||
- "val": Validation dataset without transforms
|
||||
"""
|
||||
if self._cached_datasets is not None:
|
||||
return self._cached_datasets
|
||||
|
||||
if self._data_dirs[DatasetKey.TRAIN].startswith("s3://"):
|
||||
return self._get_iterable_datasets_s3()
|
||||
else:
|
||||
return self._get_iterable_datasets_local()
|
||||
|
||||
def _get_iterable_datasets_local(self) -> Dict[str, IterableDataset]:
|
||||
"""Get train and validation datasets from local filesystem."""
|
||||
train_dir = self._data_dirs[DatasetKey.TRAIN]
|
||||
val_dir = self._data_dirs[DatasetKey.VALID]
|
||||
|
||||
train_dataset = torchvision.datasets.ImageFolder(
|
||||
root=train_dir,
|
||||
transform=get_transform(to_torch_tensor=True, random_transforms=True),
|
||||
)
|
||||
|
||||
val_dataset = torchvision.datasets.ImageFolder(
|
||||
root=val_dir,
|
||||
transform=get_transform(to_torch_tensor=True, random_transforms=False),
|
||||
)
|
||||
|
||||
return {
|
||||
DatasetKey.TRAIN: train_dataset,
|
||||
DatasetKey.VALID: val_dataset,
|
||||
}
|
||||
|
||||
def _get_iterable_datasets_s3(self) -> Dict[str, IterableDataset]:
|
||||
"""Get train and validation datasets from S3."""
|
||||
|
||||
train_dir = self._data_dirs[DatasetKey.TRAIN]
|
||||
|
||||
# Get row limits for workers and total processing
|
||||
(
|
||||
limit_training_rows_per_worker,
|
||||
limit_validation_rows_per_worker,
|
||||
) = self._get_worker_row_limits()
|
||||
|
||||
# Get file URLs for training and validation
|
||||
train_file_urls = val_file_urls = self._get_file_urls(train_dir)
|
||||
train_ds = S3JpegImageIterableDataset(
|
||||
file_urls=train_file_urls,
|
||||
random_transforms=True,
|
||||
limit_rows_per_worker=limit_training_rows_per_worker,
|
||||
)
|
||||
|
||||
# TODO: IMAGENET_JPEG_SPLIT_S3_DIRS["val"] does not have the label
|
||||
# partitioning like "train" does. So we use "train" for validation.
|
||||
val_ds = S3JpegImageIterableDataset(
|
||||
file_urls=val_file_urls,
|
||||
random_transforms=False,
|
||||
limit_rows_per_worker=limit_validation_rows_per_worker,
|
||||
)
|
||||
|
||||
self._cached_datasets = {
|
||||
DatasetKey.TRAIN: train_ds,
|
||||
DatasetKey.VALID: val_ds,
|
||||
}
|
||||
return self._cached_datasets
|
||||
@@ -0,0 +1,57 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from typing import Dict, Union, Callable
|
||||
from PIL import Image
|
||||
|
||||
from constants import DatasetKey
|
||||
from image_classification.imagenet import (
|
||||
get_transform,
|
||||
IMAGENET_WNID_TO_ID,
|
||||
)
|
||||
|
||||
|
||||
IMAGENET_JPEG_SPLIT_S3_ROOT = "s3://anyscale-imagenet/ILSVRC/Data/CLS-LOC"
|
||||
IMAGENET_JPEG_SPLIT_S3_DIRS = {
|
||||
DatasetKey.TRAIN: f"{IMAGENET_JPEG_SPLIT_S3_ROOT}/train",
|
||||
DatasetKey.VALID: f"{IMAGENET_JPEG_SPLIT_S3_ROOT}/val",
|
||||
DatasetKey.TEST: f"{IMAGENET_JPEG_SPLIT_S3_ROOT}/test",
|
||||
}
|
||||
|
||||
|
||||
def get_preprocess_map_fn(
|
||||
random_transforms: bool = True,
|
||||
) -> Callable[[Dict[str, Union[np.ndarray, str]]], Dict[str, torch.Tensor]]:
|
||||
"""Get a map function that transforms a row to the format expected by the training loop.
|
||||
|
||||
Args:
|
||||
random_transforms: Whether to use random transforms for training
|
||||
|
||||
Returns:
|
||||
A function that takes a row dict with:
|
||||
- "image": numpy array in HWC format
|
||||
- "class": WNID string
|
||||
|
||||
The output is a dict with "image" and "label" keys.
|
||||
"""
|
||||
crop_resize_transform = get_transform(
|
||||
to_torch_tensor=True, random_transforms=random_transforms
|
||||
)
|
||||
|
||||
def map_fn(row: Dict[str, Union[np.ndarray, str]]) -> Dict[str, torch.Tensor]:
|
||||
"""Process a single row into the expected format.
|
||||
|
||||
Args:
|
||||
row: Dict containing "image" and "class" keys
|
||||
|
||||
Returns:
|
||||
Dict with "image" and "label" keys
|
||||
"""
|
||||
# Convert NumPy HWC image to PIL
|
||||
image_pil = Image.fromarray(row["image"])
|
||||
# Apply transform (includes ToTensor + Normalize)
|
||||
image = crop_resize_transform(image_pil)
|
||||
# Convert label
|
||||
label = IMAGENET_WNID_TO_ID[row["class"]]
|
||||
return {"image": image, "label": label}
|
||||
|
||||
return map_fn
|
||||
@@ -0,0 +1,266 @@
|
||||
# Standard library imports
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from typing import Iterator, List, Optional, Tuple, Callable
|
||||
|
||||
# Third-party imports
|
||||
import numpy as np
|
||||
from PIL import Image as PILImage
|
||||
import torch
|
||||
from torch.utils.data import IterableDataset
|
||||
|
||||
# Ray imports
|
||||
import ray
|
||||
import ray.train
|
||||
|
||||
# Local imports
|
||||
from s3_jpeg_reader import S3JpegReader
|
||||
from .imagenet import get_preprocess_map_fn
|
||||
from logger_utils import ContextLoggerAdapter
|
||||
|
||||
logger = ContextLoggerAdapter(logging.getLogger(__name__))
|
||||
|
||||
|
||||
class S3JpegImageIterableDataset(S3JpegReader, IterableDataset):
|
||||
"""An iterable dataset that loads images from S3-stored JPEG files.
|
||||
|
||||
Features:
|
||||
- Direct image fetching from S3
|
||||
- Optional random transforms for training
|
||||
- Row limits per worker for controlled processing
|
||||
- Progress logging and performance metrics
|
||||
"""
|
||||
|
||||
# Constants
|
||||
LOG_FREQUENCY = 1000 # Log progress every 1000 rows
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_urls: List[str],
|
||||
random_transforms: bool = True,
|
||||
limit_rows_per_worker: Optional[int] = None,
|
||||
):
|
||||
"""Initialize the dataset.
|
||||
|
||||
Args:
|
||||
file_urls: List of S3 URLs to load
|
||||
random_transforms: Whether to use random transforms for training
|
||||
limit_rows_per_worker: Maximum number of rows to process per worker
|
||||
"""
|
||||
super().__init__()
|
||||
self.file_urls = file_urls
|
||||
self.limit_rows_per_worker = limit_rows_per_worker
|
||||
self.random_transforms = random_transforms
|
||||
|
||||
worker_rank = ray.train.get_context().get_world_rank()
|
||||
logger.info(
|
||||
f"Worker {worker_rank}: Initialized with {len(file_urls)} files"
|
||||
f"{f' (limit: {limit_rows_per_worker} rows)' if limit_rows_per_worker else ''}"
|
||||
)
|
||||
|
||||
def _get_worker_info(self) -> Tuple[int, int]:
|
||||
"""Get current worker information.
|
||||
|
||||
Returns:
|
||||
Tuple of (worker_id, num_workers)
|
||||
"""
|
||||
worker_info = torch.utils.data.get_worker_info()
|
||||
worker_id = worker_info.id if worker_info else 0
|
||||
num_workers = worker_info.num_workers if worker_info else 1
|
||||
return worker_id, num_workers
|
||||
|
||||
def _has_reached_row_limit(self, rows_processed: int) -> bool:
|
||||
"""Check if we've reached the row limit per worker.
|
||||
|
||||
Args:
|
||||
rows_processed: Number of rows processed so far
|
||||
|
||||
Returns:
|
||||
True if we've reached the limit, False otherwise
|
||||
"""
|
||||
return (
|
||||
self.limit_rows_per_worker is not None
|
||||
and rows_processed >= self.limit_rows_per_worker
|
||||
)
|
||||
|
||||
def _log_progress(
|
||||
self, worker_id: int, rows_processed: int, last_log_time: float
|
||||
) -> float:
|
||||
"""Log processing progress and return updated last_log_time.
|
||||
|
||||
Args:
|
||||
worker_id: ID of the current worker
|
||||
rows_processed: Number of rows processed so far
|
||||
last_log_time: Time of last progress log
|
||||
|
||||
Returns:
|
||||
Updated last_log_time
|
||||
"""
|
||||
if rows_processed % self.LOG_FREQUENCY == 0:
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - last_log_time
|
||||
rows_per_second = (
|
||||
self.LOG_FREQUENCY / elapsed_time if elapsed_time > 0 else 0
|
||||
)
|
||||
logger.info(
|
||||
f"Worker {worker_id}: Processed {rows_processed} rows "
|
||||
f"({rows_per_second:.2f} rows/sec)"
|
||||
)
|
||||
return current_time
|
||||
return last_log_time
|
||||
|
||||
def _fetch_image(self, file_url: str) -> Tuple[str, Optional[PILImage.Image]]:
|
||||
"""Fetch a single image from S3.
|
||||
|
||||
Args:
|
||||
file_url: S3 URL to fetch (e.g., "s3://bucket/path/to/image.jpg")
|
||||
|
||||
Returns:
|
||||
Tuple of (file_url, PIL Image) where PIL Image is None if fetch failed
|
||||
"""
|
||||
worker_id, _ = self._get_worker_info()
|
||||
try:
|
||||
bucket = file_url.replace("s3://", "").split("/")[0]
|
||||
key = "/".join(file_url.replace("s3://", "").split("/")[1:])
|
||||
|
||||
response = self.s3_client.get_object(Bucket=bucket, Key=key)
|
||||
image_data = response["Body"].read()
|
||||
image = PILImage.open(io.BytesIO(image_data))
|
||||
return file_url, image
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Worker {worker_id}: Error fetching image from {file_url}: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
return file_url, None
|
||||
|
||||
def _process_image(
|
||||
self,
|
||||
image: PILImage.Image,
|
||||
file_url: str,
|
||||
preprocess_fn: Callable,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Process a single image and convert to tensors.
|
||||
|
||||
Args:
|
||||
image: PIL Image to process
|
||||
file_url: URL of the image file
|
||||
preprocess_fn: Preprocessing function to apply
|
||||
|
||||
Returns:
|
||||
Tuple of (image_tensor, label_tensor)
|
||||
"""
|
||||
try:
|
||||
# Convert to RGB and numpy array
|
||||
if image.mode != "RGB":
|
||||
image = image.convert("RGB")
|
||||
image_array = np.array(image, dtype=np.uint8)
|
||||
|
||||
# Ensure HWC format (Height x Width x Channels)
|
||||
if len(image_array.shape) == 2: # Grayscale
|
||||
image_array = np.stack([image_array] * 3, axis=-1)
|
||||
elif len(image_array.shape) == 3 and image_array.shape[0] == 3: # CHW
|
||||
image_array = np.transpose(image_array, (1, 2, 0))
|
||||
|
||||
wnid = file_url.split("/")[-2] # Extract WNID from path
|
||||
|
||||
processed = preprocess_fn({"image": image_array, "class": wnid})
|
||||
|
||||
image = torch.as_tensor(processed["image"], dtype=torch.float32)
|
||||
label = torch.as_tensor(processed["label"], dtype=torch.int64)
|
||||
|
||||
return image, label
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing {file_url}: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
def _process_files(
|
||||
self, files_to_read: List[str], preprocess_fn: Callable, worker_id: int
|
||||
) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Process multiple files and yield processed rows.
|
||||
|
||||
Args:
|
||||
files_to_read: List of file URLs to process
|
||||
preprocess_fn: Preprocessing function to apply
|
||||
worker_id: ID of the current worker
|
||||
|
||||
Yields:
|
||||
Tuple of (image_tensor, label_tensor)
|
||||
"""
|
||||
rows_processed = 0
|
||||
last_log_time = time.time()
|
||||
total_start_time = time.time()
|
||||
|
||||
for file_url in files_to_read:
|
||||
if self._has_reached_row_limit(rows_processed):
|
||||
logger.info(f"Worker {worker_id}: Reached row limit: {rows_processed}")
|
||||
break
|
||||
|
||||
file_url, image = self._fetch_image(file_url)
|
||||
if image is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
image, label = self._process_image(
|
||||
image,
|
||||
file_url,
|
||||
preprocess_fn,
|
||||
)
|
||||
rows_processed += 1
|
||||
last_log_time = self._log_progress(
|
||||
worker_id, rows_processed, last_log_time
|
||||
)
|
||||
yield image, label
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Log final statistics
|
||||
total_time = time.time() - total_start_time
|
||||
logger.info(
|
||||
f"Worker {worker_id}: Finished: {rows_processed} rows in {total_time:.2f}s "
|
||||
f"({rows_processed/total_time:.2f} rows/sec)"
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Iterate through the dataset and yield (image, label) tensors.
|
||||
|
||||
Yields:
|
||||
Tuple[torch.Tensor, torch.Tensor]: (image, label) tensors
|
||||
|
||||
Raises:
|
||||
Exception: If there's a fatal error during iteration
|
||||
"""
|
||||
try:
|
||||
# Get worker info for file distribution
|
||||
worker_id, num_workers = self._get_worker_info()
|
||||
logger.info(f"Worker {worker_id}/{num_workers}: Starting")
|
||||
|
||||
# Distribute files among workers
|
||||
files_to_read = (
|
||||
self.file_urls
|
||||
if num_workers == 1
|
||||
else self.file_urls[worker_id::num_workers]
|
||||
)
|
||||
|
||||
logger.info(f"Worker {worker_id}: Processing {len(files_to_read)} files")
|
||||
|
||||
# Initialize preprocessing function
|
||||
preprocess_fn = get_preprocess_map_fn(
|
||||
random_transforms=self.random_transforms
|
||||
)
|
||||
|
||||
# Process files and yield results
|
||||
yield from self._process_files(files_to_read, preprocess_fn, worker_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Worker {worker_id}: Fatal error: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
@@ -0,0 +1,139 @@
|
||||
# Standard library imports
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
# Third-party imports
|
||||
from torch.utils.data import IterableDataset
|
||||
import ray
|
||||
import ray.data
|
||||
import ray.train
|
||||
|
||||
# Local imports
|
||||
from constants import DatasetKey
|
||||
from config import BenchmarkConfig
|
||||
from image_classification.factory import (
|
||||
ImageClassificationRayDataLoaderFactory,
|
||||
ImageClassificationTorchDataLoaderFactory,
|
||||
)
|
||||
from .imagenet import get_preprocess_map_fn
|
||||
from .parquet_iterable_dataset import S3ParquetImageIterableDataset
|
||||
from s3_parquet_reader import S3ParquetReader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageClassificationParquetRayDataLoaderFactory(
|
||||
ImageClassificationRayDataLoaderFactory
|
||||
):
|
||||
"""Factory for creating Ray DataLoader for Parquet image classification.
|
||||
|
||||
Features:
|
||||
- Parquet file reading with column selection
|
||||
- Image decoding and preprocessing
|
||||
- Resource allocation for concurrent validation
|
||||
- Row limits based on benchmark configuration
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, benchmark_config: BenchmarkConfig, data_dirs: Dict[str, str]
|
||||
) -> None:
|
||||
super().__init__(benchmark_config)
|
||||
self._data_dirs = data_dirs
|
||||
|
||||
def get_ray_datasets(self) -> Dict[str, ray.data.Dataset]:
|
||||
"""Get Ray datasets for training and validation.
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- "train": Training dataset with random transforms
|
||||
- "val": Validation dataset without transforms
|
||||
"""
|
||||
# Create training dataset with image decoding and transforms
|
||||
train_ds = ray.data.read_parquet(
|
||||
self._data_dirs[DatasetKey.TRAIN],
|
||||
columns=["image", "label"],
|
||||
).map(get_preprocess_map_fn(decode_image=True, random_transforms=True))
|
||||
|
||||
if self.get_dataloader_config().limit_training_rows > 0:
|
||||
train_ds = train_ds.limit(self.get_dataloader_config().limit_training_rows)
|
||||
|
||||
# Create validation dataset without random transforms
|
||||
val_ds = ray.data.read_parquet(
|
||||
self._data_dirs[DatasetKey.TRAIN],
|
||||
columns=["image", "label"],
|
||||
).map(get_preprocess_map_fn(decode_image=True, random_transforms=False))
|
||||
|
||||
if self.get_dataloader_config().limit_validation_rows > 0:
|
||||
val_ds = val_ds.limit(self.get_dataloader_config().limit_validation_rows)
|
||||
|
||||
return {
|
||||
DatasetKey.TRAIN: train_ds,
|
||||
DatasetKey.VALID: val_ds,
|
||||
}
|
||||
|
||||
|
||||
class ImageClassificationParquetTorchDataLoaderFactory(
|
||||
ImageClassificationTorchDataLoaderFactory, S3ParquetReader
|
||||
):
|
||||
"""Factory for creating PyTorch DataLoaders for Parquet image classification.
|
||||
|
||||
Features:
|
||||
- Parquet file reading with row count-based distribution
|
||||
- Worker-based file distribution for balanced workloads
|
||||
- Row limits per worker for controlled processing
|
||||
- Dataset instance caching for efficiency
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, benchmark_config: BenchmarkConfig, data_dirs: Dict[str, str]
|
||||
) -> None:
|
||||
"""Initialize factory with benchmark configuration.
|
||||
|
||||
Args:
|
||||
benchmark_config: Configuration for benchmark parameters
|
||||
"""
|
||||
super().__init__(benchmark_config)
|
||||
S3ParquetReader.__init__(
|
||||
self
|
||||
) # Initialize S3ParquetReader to set up _s3_client
|
||||
self.train_url = data_dirs[DatasetKey.TRAIN]
|
||||
self._cached_datasets: Optional[Dict[str, IterableDataset]] = None
|
||||
|
||||
def get_iterable_datasets(self) -> Dict[str, IterableDataset]:
|
||||
"""Get train and validation datasets with worker-specific configurations.
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- "train": Training dataset with random transforms
|
||||
- "val": Validation dataset without transforms
|
||||
"""
|
||||
if self._cached_datasets is not None:
|
||||
return self._cached_datasets
|
||||
|
||||
# Get row limits for workers and total processing
|
||||
(
|
||||
limit_training_rows_per_worker,
|
||||
limit_validation_rows_per_worker,
|
||||
) = self._get_worker_row_limits()
|
||||
|
||||
# Create training dataset
|
||||
train_file_urls = self._get_file_urls(self.train_url)
|
||||
train_ds = S3ParquetImageIterableDataset(
|
||||
file_urls=train_file_urls,
|
||||
random_transforms=True,
|
||||
limit_rows_per_worker=limit_training_rows_per_worker,
|
||||
)
|
||||
|
||||
# Create validation dataset
|
||||
val_file_urls = train_file_urls
|
||||
val_ds = S3ParquetImageIterableDataset(
|
||||
file_urls=val_file_urls,
|
||||
random_transforms=False,
|
||||
limit_rows_per_worker=limit_validation_rows_per_worker,
|
||||
)
|
||||
|
||||
self._cached_datasets = {
|
||||
DatasetKey.TRAIN: train_ds,
|
||||
DatasetKey.VALID: val_ds,
|
||||
}
|
||||
return self._cached_datasets
|
||||
@@ -0,0 +1,69 @@
|
||||
import io
|
||||
import numpy as np
|
||||
from typing import Dict, Union, Callable
|
||||
from PIL import Image
|
||||
from torchvision.transforms.functional import pil_to_tensor
|
||||
|
||||
from constants import DatasetKey
|
||||
from image_classification.imagenet import (
|
||||
get_transform,
|
||||
IMAGENET_WNID_TO_ID,
|
||||
)
|
||||
|
||||
IMAGENET_PARQUET_SPLIT_S3_ROOT = (
|
||||
"s3://ray-benchmark-data-internal-us-west-2/imagenet/parquet_split"
|
||||
)
|
||||
IMAGENET_PARQUET_SPLIT_S3_DIRS = {
|
||||
DatasetKey.TRAIN: f"{IMAGENET_PARQUET_SPLIT_S3_ROOT}/train",
|
||||
DatasetKey.VALID: f"{IMAGENET_PARQUET_SPLIT_S3_ROOT}/val",
|
||||
DatasetKey.TEST: f"{IMAGENET_PARQUET_SPLIT_S3_ROOT}/test",
|
||||
}
|
||||
|
||||
# Much larger parquet dataset used to sustain Ray Data backpressure in the
|
||||
# slow-consumer ingest benchmarks.
|
||||
IMAGENET_PARQUET_SPLIT_1T_S3_ROOT = (
|
||||
"s3://ray-benchmark-data-internal-us-west-2/imagenet/parquet_split_1t"
|
||||
)
|
||||
IMAGENET_PARQUET_SPLIT_1T_S3_DIRS = {
|
||||
DatasetKey.TRAIN: f"{IMAGENET_PARQUET_SPLIT_1T_S3_ROOT}/train",
|
||||
DatasetKey.VALID: f"{IMAGENET_PARQUET_SPLIT_1T_S3_ROOT}/val",
|
||||
DatasetKey.TEST: f"{IMAGENET_PARQUET_SPLIT_1T_S3_ROOT}/test",
|
||||
}
|
||||
|
||||
|
||||
def get_preprocess_map_fn(
|
||||
decode_image: bool = True, random_transforms: bool = True
|
||||
) -> Callable[[Dict[str, Union[bytes, str]]], Dict[str, Union[np.ndarray, int]]]:
|
||||
"""Get a map function that transforms a row of the dataset to the format
|
||||
expected by the training loop.
|
||||
|
||||
Args:
|
||||
decode_image: Whether to decode the image bytes into a tensor
|
||||
random_transforms: Whether to use random transforms for training
|
||||
|
||||
Returns:
|
||||
A function that takes a row dict and returns a processed dict.
|
||||
Input row dict should have:
|
||||
- "image": bytes or tensor in CHW format
|
||||
- "label": WNID string
|
||||
|
||||
Output dict has:
|
||||
- "image": np.array of the transformed, normalized image
|
||||
- "label": An integer index of the WNID
|
||||
"""
|
||||
crop_resize_transform = get_transform(
|
||||
to_torch_tensor=False, random_transforms=random_transforms
|
||||
)
|
||||
|
||||
def map_fn(row: Dict[str, Union[bytes, str]]) -> Dict[str, Union[np.ndarray, int]]:
|
||||
assert "image" in row and "label" in row, row.keys()
|
||||
|
||||
if decode_image:
|
||||
row["image"] = pil_to_tensor(Image.open(io.BytesIO(row["image"]))) / 255.0
|
||||
|
||||
row["image"] = np.array(crop_resize_transform(row["image"]))
|
||||
row["label"] = IMAGENET_WNID_TO_ID[row["label"]]
|
||||
|
||||
return {"image": row["image"], "label": row["label"]}
|
||||
|
||||
return map_fn
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
# Standard library imports
|
||||
from typing import List, Tuple, Optional, Iterator, Callable
|
||||
import logging
|
||||
import io
|
||||
import time
|
||||
|
||||
# Third-party imports
|
||||
import pandas as pd
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
from torch.utils.data import IterableDataset
|
||||
|
||||
# Ray imports
|
||||
import ray
|
||||
import ray.train
|
||||
|
||||
# Local imports
|
||||
from s3_parquet_reader import S3ParquetReader
|
||||
from .imagenet import get_preprocess_map_fn
|
||||
from logger_utils import ContextLoggerAdapter
|
||||
|
||||
logger = ContextLoggerAdapter(logging.getLogger(__name__))
|
||||
|
||||
|
||||
# TODO Look into https://github.com/webdataset/webdataset for more canonical way to do data
|
||||
# distribution between Ray Train and Torch Dataloader workers.
|
||||
|
||||
|
||||
class S3ParquetImageIterableDataset(S3ParquetReader, IterableDataset):
|
||||
"""An iterable dataset that loads images from S3-stored Parquet files.
|
||||
|
||||
This dataset:
|
||||
1. Reads Parquet files from S3 one row group at a time
|
||||
2. Processes images with optional random transforms
|
||||
3. Yields (image, label) tensors
|
||||
4. Supports row limits per worker for controlled data processing
|
||||
"""
|
||||
|
||||
LOG_FREQUENCY = 1000 # Log progress every 1000 rows
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_urls: List[str],
|
||||
random_transforms: bool = True,
|
||||
limit_rows_per_worker: Optional[int] = None,
|
||||
):
|
||||
"""Initialize the dataset.
|
||||
|
||||
Args:
|
||||
file_urls: List of S3 URLs to load
|
||||
random_transforms: Whether to use random transforms for training
|
||||
limit_rows_per_worker: Maximum number of rows to process per worker (None for all rows)
|
||||
"""
|
||||
super().__init__()
|
||||
self.file_urls = file_urls
|
||||
self.limit_rows_per_worker = limit_rows_per_worker
|
||||
self.random_transforms = random_transforms
|
||||
|
||||
worker_rank = ray.train.get_context().get_world_rank()
|
||||
logger.info(
|
||||
f"Worker {worker_rank}: Initialized with {len(file_urls)} files"
|
||||
f"{f' (limit: {limit_rows_per_worker} rows)' if limit_rows_per_worker else ''}"
|
||||
)
|
||||
|
||||
def _get_worker_info(self) -> Tuple[int, int]:
|
||||
"""Get current worker information.
|
||||
|
||||
Returns:
|
||||
Tuple of (worker_id, num_workers)
|
||||
"""
|
||||
worker_info = torch.utils.data.get_worker_info()
|
||||
worker_id = worker_info.id if worker_info else 0
|
||||
num_workers = worker_info.num_workers if worker_info else 1
|
||||
return worker_id, num_workers
|
||||
|
||||
def _has_reached_row_limit(self, rows_processed: int) -> bool:
|
||||
"""Check if we've reached the row limit per worker.
|
||||
|
||||
Args:
|
||||
rows_processed: Number of rows processed so far
|
||||
|
||||
Returns:
|
||||
True if we've reached the limit, False otherwise
|
||||
"""
|
||||
return (
|
||||
self.limit_rows_per_worker is not None
|
||||
and rows_processed >= self.limit_rows_per_worker
|
||||
)
|
||||
|
||||
def _log_progress(
|
||||
self, worker_id: int, rows_processed: int, last_log_time: float
|
||||
) -> float:
|
||||
"""Log processing progress and return updated last_log_time.
|
||||
|
||||
Args:
|
||||
worker_id: ID of the current worker
|
||||
rows_processed: Number of rows processed so far
|
||||
last_log_time: Time of last progress log
|
||||
|
||||
Returns:
|
||||
Updated last_log_time
|
||||
"""
|
||||
if rows_processed % self.LOG_FREQUENCY == 0:
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - last_log_time
|
||||
rows_per_second = (
|
||||
self.LOG_FREQUENCY / elapsed_time if elapsed_time > 0 else 0
|
||||
)
|
||||
logger.info(
|
||||
f"Worker {worker_id}: Processed {rows_processed} rows "
|
||||
f"({rows_per_second:.2f} rows/sec)"
|
||||
)
|
||||
return current_time
|
||||
return last_log_time
|
||||
|
||||
def _read_parquet_file(self, file_url: str) -> Iterator[pd.DataFrame]:
|
||||
"""Read a Parquet file from S3 one row group at a time.
|
||||
|
||||
This method:
|
||||
1. Fetches the Parquet file from S3
|
||||
2. Reads it row group by row group
|
||||
3. Converts each row group to a pandas DataFrame
|
||||
|
||||
Args:
|
||||
file_url: S3 URL of the Parquet file
|
||||
|
||||
Yields:
|
||||
DataFrame containing one row group at a time
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error reading the file
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
worker_id, _ = self._get_worker_info()
|
||||
logger.info(f"Worker {worker_id}: Reading Parquet file: {file_url}")
|
||||
|
||||
# Get parquet file metadata
|
||||
bucket, key = self._parse_s3_url(file_url)
|
||||
response = self.s3_client.get_object(Bucket=bucket, Key=key)
|
||||
parquet_file = pq.ParquetFile(io.BytesIO(response["Body"].read()))
|
||||
num_row_groups = parquet_file.num_row_groups
|
||||
|
||||
logger.info(
|
||||
f"Worker {worker_id}: Found {num_row_groups} row groups in {file_url}"
|
||||
)
|
||||
|
||||
for row_group in range(num_row_groups):
|
||||
# Read row group and convert to pandas
|
||||
table = parquet_file.read_row_group(row_group)
|
||||
df = table.to_pandas()
|
||||
yield df
|
||||
|
||||
total_time = time.time() - start_time
|
||||
logger.info(
|
||||
f"Worker {worker_id}: Completed reading {file_url} in {total_time:.2f}s"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
worker_id, _ = self._get_worker_info()
|
||||
logger.error(
|
||||
f"Worker {worker_id}: Error reading file {file_url}: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
def _process_file(
|
||||
self,
|
||||
file_url: str,
|
||||
preprocess_fn: Callable,
|
||||
) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Process a single file and yield processed rows.
|
||||
|
||||
Args:
|
||||
file_url: URL of the file to process
|
||||
preprocess_fn: Preprocessing function to apply
|
||||
|
||||
Yields:
|
||||
Tuple of (image_tensor, label_tensor)
|
||||
"""
|
||||
for df in self._read_parquet_file(file_url):
|
||||
for _, row in df.iterrows():
|
||||
try:
|
||||
# Process row and convert to tensors
|
||||
processed = preprocess_fn(row)
|
||||
image = torch.as_tensor(processed["image"], dtype=torch.float32)
|
||||
label = torch.as_tensor(processed["label"], dtype=torch.int64)
|
||||
yield image, label
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _process_files(
|
||||
self, files_to_read: List[str], preprocess_fn: Callable, worker_id: int
|
||||
) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Process multiple files and yield processed rows.
|
||||
|
||||
Args:
|
||||
files_to_read: List of file URLs to process
|
||||
preprocess_fn: Preprocessing function to apply
|
||||
worker_id: ID of the current worker
|
||||
|
||||
Yields:
|
||||
Tuple of (image_tensor, label_tensor)
|
||||
"""
|
||||
rows_processed = 0
|
||||
last_log_time = time.time()
|
||||
total_start_time = time.time()
|
||||
|
||||
for file_url in files_to_read:
|
||||
if self._has_reached_row_limit(rows_processed):
|
||||
logger.info(f"Worker {worker_id}: Reached row limit: {rows_processed}")
|
||||
break
|
||||
|
||||
for image, label in self._process_file(file_url, preprocess_fn):
|
||||
if self._has_reached_row_limit(rows_processed):
|
||||
break
|
||||
|
||||
rows_processed += 1
|
||||
last_log_time = self._log_progress(
|
||||
worker_id, rows_processed, last_log_time
|
||||
)
|
||||
yield image, label
|
||||
|
||||
# Log final statistics
|
||||
total_time = time.time() - total_start_time
|
||||
logger.info(
|
||||
f"Worker {worker_id}: Finished: {rows_processed} rows in {total_time:.2f}s "
|
||||
f"({rows_processed/total_time:.2f} rows/sec)"
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Main iteration method that processes files and yields (image, label) tensors.
|
||||
|
||||
This method:
|
||||
1. Distributes files among workers
|
||||
2. Processes rows with image transforms
|
||||
3. Converts to tensors
|
||||
4. Respects row limits per worker
|
||||
|
||||
Yields:
|
||||
Tuple of (image_tensor, label_tensor)
|
||||
|
||||
Raises:
|
||||
Exception: If there's a fatal error during processing
|
||||
"""
|
||||
try:
|
||||
# Get worker info for file distribution
|
||||
worker_id, num_workers = self._get_worker_info()
|
||||
logger.info(f"Worker {worker_id}/{num_workers}: Starting")
|
||||
|
||||
# Initialize preprocessing function
|
||||
preprocess_fn = get_preprocess_map_fn(
|
||||
decode_image=True, random_transforms=self.random_transforms
|
||||
)
|
||||
|
||||
# Distribute files among workers
|
||||
files_to_read = (
|
||||
self.file_urls
|
||||
if num_workers == 1
|
||||
else self.file_urls[worker_id::num_workers]
|
||||
)
|
||||
|
||||
logger.info(f"Worker {worker_id}: Processing {len(files_to_read)} files")
|
||||
|
||||
# Process files and yield results
|
||||
yield from self._process_files(files_to_read, preprocess_fn, worker_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Worker {worker_id}: Fatal error: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
@@ -0,0 +1,77 @@
|
||||
# Standard library imports
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
# Third-party imports
|
||||
import ray.data
|
||||
|
||||
# Local imports
|
||||
from constants import DatasetKey
|
||||
from config import BenchmarkConfig
|
||||
from image_classification.factory import ImageClassificationRayDataLoaderFactory
|
||||
from .imagenet import (
|
||||
create_s3_url_dataset,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageClassificationS3UrlRayDataLoaderFactory(
|
||||
ImageClassificationRayDataLoaderFactory
|
||||
):
|
||||
"""Factory for creating Ray DataLoader that downloads images from S3 URLs.
|
||||
|
||||
This factory:
|
||||
1. Lists JPEG files from S3 using boto3
|
||||
2. Creates a Ray dataset from the file records
|
||||
3. Uses map_batches to download and process images from S3
|
||||
|
||||
This approach separates file listing from image downloading, which can be
|
||||
more efficient for certain workloads as it allows parallel downloads during
|
||||
the map_batches execution on CPU workers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, benchmark_config: BenchmarkConfig, data_dirs: Dict[str, str]
|
||||
) -> None:
|
||||
super().__init__(benchmark_config)
|
||||
self._data_dirs = data_dirs
|
||||
|
||||
def get_ray_datasets(self) -> Dict[str, ray.data.Dataset]:
|
||||
"""Get Ray datasets for training and validation.
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- "train": Training dataset with random transforms
|
||||
- "val": Validation dataset without transforms
|
||||
"""
|
||||
dataloader_config = self.get_dataloader_config()
|
||||
|
||||
# Create training dataset
|
||||
train_limit = (
|
||||
dataloader_config.limit_training_rows
|
||||
if dataloader_config.limit_training_rows > 0
|
||||
else None
|
||||
)
|
||||
train_ds = create_s3_url_dataset(
|
||||
data_dir=self._data_dirs[DatasetKey.TRAIN],
|
||||
random_transforms=True,
|
||||
limit_rows=train_limit,
|
||||
)
|
||||
|
||||
# Create validation dataset
|
||||
val_limit = (
|
||||
dataloader_config.limit_validation_rows
|
||||
if dataloader_config.limit_validation_rows > 0
|
||||
else None
|
||||
)
|
||||
val_ds = create_s3_url_dataset(
|
||||
data_dir=self._data_dirs[DatasetKey.TRAIN],
|
||||
random_transforms=False,
|
||||
limit_rows=val_limit,
|
||||
)
|
||||
|
||||
return {
|
||||
DatasetKey.TRAIN: train_ds,
|
||||
DatasetKey.VALID: val_ds,
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
"""ImageNet dataset loading via S3 URL download with Ray Data expressions.
|
||||
|
||||
This module provides dataset loading that:
|
||||
1. Lists JPEG files from S3 using boto3 (parallelized via Ray tasks)
|
||||
2. Creates a Ray dataset from the file records
|
||||
3. Uses Ray Data expressions (alpha) to download image bytes efficiently
|
||||
4. Uses map_batches to decode and process images
|
||||
|
||||
This approach leverages Ray Data's expressions API for optimized parallel I/O,
|
||||
separating the download step from image processing for better throughput.
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import boto3
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from torchvision.transforms.functional import pil_to_tensor
|
||||
|
||||
import ray.data
|
||||
from ray.data.expressions import download
|
||||
|
||||
from constants import DatasetKey
|
||||
from image_classification.imagenet import (
|
||||
get_transform,
|
||||
IMAGENET_WNID_TO_ID,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# S3 configuration for ImageNet JPEG data
|
||||
AWS_REGION = "us-west-2"
|
||||
S3_ROOT = "s3://anyscale-imagenet/ILSVRC/Data/CLS-LOC"
|
||||
IMAGENET_S3_URL_SPLIT_DIRS = {
|
||||
DatasetKey.TRAIN: f"{S3_ROOT}/train",
|
||||
DatasetKey.VALID: f"{S3_ROOT}/val",
|
||||
DatasetKey.TEST: f"{S3_ROOT}/test",
|
||||
}
|
||||
|
||||
|
||||
def _get_class_labels(bucket: str, prefix: str) -> List[str]:
|
||||
"""Get all class label directories from S3.
|
||||
|
||||
Args:
|
||||
bucket: S3 bucket name
|
||||
prefix: S3 prefix path
|
||||
|
||||
Returns:
|
||||
List of class label directory names
|
||||
"""
|
||||
from typing import Set
|
||||
|
||||
# Ensure prefix ends with /
|
||||
if prefix and not prefix.endswith("/"):
|
||||
prefix += "/"
|
||||
|
||||
# List directories using delimiter
|
||||
s3_client = boto3.client("s3", region_name=AWS_REGION)
|
||||
paginator = s3_client.get_paginator("list_objects_v2")
|
||||
|
||||
# Use delimiter to get "directory" level
|
||||
labels: Set[str] = set()
|
||||
for page in paginator.paginate(Bucket=bucket, Prefix=prefix, Delimiter="/"):
|
||||
# CommonPrefixes contains the "directories"
|
||||
for common_prefix in page.get("CommonPrefixes", []):
|
||||
prefix_path = common_prefix["Prefix"]
|
||||
# Extract the directory name
|
||||
label = prefix_path.rstrip("/").split("/")[-1]
|
||||
labels.add(label)
|
||||
|
||||
return sorted(labels)
|
||||
|
||||
|
||||
@ray.remote
|
||||
def _list_files_for_label(
|
||||
bucket: str, prefix: str, label: str
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Ray task to list all image files for a specific label.
|
||||
|
||||
Args:
|
||||
bucket: S3 bucket name
|
||||
prefix: S3 prefix (parent directory)
|
||||
label: Class label (subdirectory name)
|
||||
|
||||
Returns:
|
||||
List of tuples with (file_path, class_name)
|
||||
"""
|
||||
s3_client = boto3.client("s3", region_name=AWS_REGION)
|
||||
paginator = s3_client.get_paginator("list_objects_v2")
|
||||
|
||||
# Construct the full prefix for this label
|
||||
label_prefix = f"{prefix}/{label}/" if prefix else f"{label}/"
|
||||
|
||||
file_records = []
|
||||
for page in paginator.paginate(Bucket=bucket, Prefix=label_prefix):
|
||||
for obj in page.get("Contents", []):
|
||||
key = obj["Key"]
|
||||
if key.lower().endswith((".jpg", ".jpeg")):
|
||||
file_path = f"s3://{bucket}/{key}"
|
||||
file_records.append((file_path, label))
|
||||
|
||||
return file_records
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _list_s3_image_files_cached(data_dir: str) -> Tuple[Tuple[str, str], ...]:
|
||||
"""Cached implementation of S3 file listing using Ray tasks for parallelism.
|
||||
|
||||
Returns a tuple of tuples for hashability (required by lru_cache).
|
||||
"""
|
||||
logger.info(f"Listing JPEG files from {data_dir}...")
|
||||
|
||||
# Parse S3 URL: s3://bucket/prefix
|
||||
s3_path = data_dir
|
||||
if s3_path.startswith("s3://"):
|
||||
s3_path = s3_path[5:]
|
||||
parts = s3_path.split("/", 1)
|
||||
bucket = parts[0]
|
||||
prefix = parts[1].rstrip("/") if len(parts) > 1 else ""
|
||||
|
||||
# Get all class labels
|
||||
labels = _get_class_labels(bucket, prefix)
|
||||
logger.info(
|
||||
f"Found {len(labels)} class labels, launching Ray tasks for parallel listing..."
|
||||
)
|
||||
|
||||
# Launch Ray tasks for each label
|
||||
futures = [_list_files_for_label.remote(bucket, prefix, label) for label in labels]
|
||||
|
||||
# Wait for all tasks to complete and aggregate results
|
||||
results = ray.get(futures)
|
||||
|
||||
# Flatten the list of lists
|
||||
file_records = []
|
||||
for records in results:
|
||||
file_records.extend(records)
|
||||
|
||||
logger.info(f"Listed and cached {len(file_records)} JPEG files")
|
||||
return tuple(file_records)
|
||||
|
||||
|
||||
def list_s3_image_files(data_dir: str) -> List[Dict[str, str]]:
|
||||
"""List JPEG files from S3 with class labels extracted from path.
|
||||
|
||||
Results are cached to avoid repeated S3 listings.
|
||||
|
||||
Args:
|
||||
data_dir: S3 path to list files from (e.g., "s3://bucket/prefix")
|
||||
|
||||
Returns:
|
||||
List of dicts with "path" (S3 URL) and "class" (WNID) keys
|
||||
"""
|
||||
cached_records = _list_s3_image_files_cached(data_dir)
|
||||
return [{"path": path, "class": cls} for path, cls in cached_records]
|
||||
|
||||
|
||||
def get_process_batch_fn(
|
||||
random_transforms: bool = True,
|
||||
label_to_id_map: Optional[Dict[str, int]] = None,
|
||||
) -> Callable[[Dict[str, np.ndarray]], Dict[str, np.ndarray]]:
|
||||
"""Get a map_batches function that processes pre-downloaded image bytes.
|
||||
|
||||
This function expects image bytes to already be downloaded (via Ray Data
|
||||
expressions) and handles decoding and transformations.
|
||||
|
||||
Args:
|
||||
random_transforms: Whether to use random transforms for training
|
||||
label_to_id_map: Mapping from WNID strings to integer IDs
|
||||
|
||||
Returns:
|
||||
A function suitable for use with dataset.map_batches()
|
||||
"""
|
||||
if label_to_id_map is None:
|
||||
label_to_id_map = IMAGENET_WNID_TO_ID
|
||||
|
||||
transform = get_transform(
|
||||
to_torch_tensor=False, random_transforms=random_transforms
|
||||
)
|
||||
|
||||
def process_batch(
|
||||
batch: Dict[str, np.ndarray],
|
||||
) -> Dict[str, np.ndarray]:
|
||||
"""Process pre-downloaded image bytes.
|
||||
|
||||
Args:
|
||||
batch: Dict with "bytes" (image data) and "class" arrays
|
||||
|
||||
Returns:
|
||||
Dict with "image" (numpy array) and "label" (int) arrays
|
||||
"""
|
||||
processed_images = []
|
||||
labels = []
|
||||
|
||||
image_bytes_list = list(batch["bytes"])
|
||||
classes = list(batch["class"])
|
||||
|
||||
for data, wnid in zip(image_bytes_list, classes):
|
||||
# Decode and transform image
|
||||
image_pil = Image.open(io.BytesIO(data)).convert("RGB")
|
||||
image_tensor = pil_to_tensor(image_pil) / 255.0
|
||||
processed_image = np.array(transform(image_tensor))
|
||||
processed_images.append(processed_image)
|
||||
|
||||
# Convert label
|
||||
labels.append(label_to_id_map[wnid])
|
||||
|
||||
return {
|
||||
"image": np.stack(processed_images),
|
||||
"label": np.array(labels),
|
||||
}
|
||||
|
||||
return process_batch
|
||||
|
||||
|
||||
def create_s3_url_dataset(
|
||||
data_dir: str,
|
||||
random_transforms: bool = True,
|
||||
limit_rows: Optional[int] = None,
|
||||
) -> ray.data.Dataset:
|
||||
"""Create a Ray dataset that downloads images from S3 URLs.
|
||||
|
||||
Uses Ray Data expressions (alpha) for efficient parallel downloads,
|
||||
then map_batches for image decoding and transformations.
|
||||
|
||||
Args:
|
||||
data_dir: S3 path to the image directory
|
||||
random_transforms: Whether to use random transforms
|
||||
limit_rows: Optional row limit
|
||||
|
||||
Returns:
|
||||
Ray dataset with "image" and "label" columns
|
||||
"""
|
||||
file_records = list_s3_image_files(data_dir)
|
||||
|
||||
ds = ray.data.from_items(file_records)
|
||||
|
||||
if limit_rows is not None and limit_rows > 0:
|
||||
ds = ds.limit(limit_rows)
|
||||
|
||||
# Download image bytes using Ray Data expressions (alpha)
|
||||
# This enables optimized parallel I/O managed by Ray Data
|
||||
ds = ds.with_column("bytes", download("path"))
|
||||
|
||||
# Process downloaded bytes (decode and transform)
|
||||
process_fn = get_process_batch_fn(random_transforms=random_transforms)
|
||||
ds = ds.map_batches(process_fn)
|
||||
|
||||
return ds
|
||||
Reference in New Issue
Block a user