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

925 lines
31 KiB
Python

import ray
from ray import train
from ray.train import DataConfig, ScalingConfig, RunConfig, Checkpoint
from ray.train.torch import TorchTrainer
from ray.data.datasource.partitioning import Partitioning
import tempfile
import itertools
import os
import time
from benchmark import Benchmark, BenchmarkMetric
import numpy as np
import torch
import torch.distributed as dist
import torchvision
from torchvision.models import resnet50
from torchvision.transforms.functional import pil_to_tensor
import torch.nn as nn
import torch.optim as optim
from PIL import Image
import streaming
from streaming import LocalDataset, StreamingDataset
from dataset_benchmark_util import (
get_prop_parquet_paths,
IMG_S3_ROOT,
get_mosaic_epoch_size,
IMAGENET_WNID_TO_ID,
)
# This benchmark does the following:
# 1) Read files (images or parquet) with ray.data
# 2) Apply preprocessing with map()
# 3) Train TorchTrainer on processed data with resnet50 model
# Metrics recorded to the output file are:
# - Runtime of benchmark (s)
# - Final epoch throughput (img/s)
# - Final epoch top-1 accuracy (%)
def parse_args():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--data-root", type=str, help="Root of data directory")
parser.add_argument(
"--file-type",
default="image",
type=str,
help="Input file type; choose from: ['image', 'parquet']",
)
parser.add_argument(
"--skip-train-model",
default=False,
action="store_true",
help="Whether to skip training a model (i.e. only consume data). "
"Set to True if file_type == 'parquet'.",
)
parser.add_argument(
"--skip-ray-trainer",
default=False,
action="store_true",
help=(
"Whether to skip using Ray Train TorchTrainer to consume the data, and "
"instead iterate over the dataset with the Ray Data "
"iter_torch_batches() method."
),
)
parser.add_argument(
"--repeat-ds",
default=1,
type=int,
help="Read the input dataset n times, used to increase the total data size.",
)
parser.add_argument(
"--target-worker-gb",
default=10,
type=int,
help=(
"Number of GB per worker for selecting a subset "
"from default dataset. -1 means the whole dataset"
),
)
parser.add_argument(
"--batch-size",
default=32,
type=int,
help=(
"Batch size to use. Set to -1 to use batch_size=None "
"(Ray Data will use the entire block as a batch)."
),
)
parser.add_argument(
"--prefetch-batches",
default=1,
type=int,
help="prefetch_batches for iter_torch_batches()",
)
parser.add_argument(
"--local-shuffle-buffer-size",
default=None,
type=int,
help="local_shuffle_buffer_size for iter_torch_batches()",
)
parser.add_argument(
"--target-max-block-size-mb",
default=None,
type=int,
help="DataContext.target_max_block_size in MB. Default is 128MB.",
)
parser.add_argument(
"--num-epochs",
# Use 5 epochs and report the avg per-epoch throughput
# (excluding first epoch in case there is warmup).
default=5,
type=int,
help="Number of epochs to run. The avg per-epoch throughput will be reported.",
)
parser.add_argument(
"--num-retries",
default=3,
type=int,
help="Number of retries for the Trainer before exiting the benchmark.",
)
parser.add_argument(
"--num-workers",
default=1,
type=int,
help="Number of workers.",
)
parser.add_argument(
"--use-gpu",
action="store_true",
default=False,
help="Whether to use GPU with TorchTrainer.",
)
parser.add_argument(
"--preserve-order",
action="store_true",
default=False,
help="Whether to configure Train with preserve_order flag.",
)
parser.add_argument(
"--use-torch",
action="store_true",
default=False,
help="Whether to use PyTorch DataLoader.",
)
parser.add_argument(
"--use-mosaic",
action="store_true",
default=False,
help="",
)
parser.add_argument(
"--use-synthetic-data",
action="store_true",
default=False,
help=(
"Whether to use synthetic Torch data (repeat a "
"randomly generated batch 1000 times)"
),
)
parser.add_argument(
"--torch-num-workers",
default=None,
type=int,
)
parser.add_argument(
"--split-input",
action="store_true",
default=False,
help="Whether to pre-split the input dataset instead of using streaming split.",
)
parser.add_argument(
"--cache-input-ds",
action="store_true",
default=False,
help="Whether to cache input dataset (before preprocessing).",
)
parser.add_argument(
"--cache-output-ds",
action="store_true",
default=False,
help="Whether to cache output dataset (after preprocessing).",
)
args = parser.parse_args()
ray.init(
runtime_env={
"working_dir": os.path.dirname(__file__),
}
)
if not (args.use_torch or args.use_mosaic or args.use_synthetic_data):
args.use_ray_data = True
else:
args.use_ray_data = False
if args.data_root is None and not args.use_mosaic:
# use default datasets if data root is not provided
if args.file_type == "image":
args.data_root = IMG_S3_ROOT
elif args.file_type == "parquet":
args.data_root = get_prop_parquet_paths(
num_workers=args.num_workers, target_worker_gb=args.target_worker_gb
)
else:
raise Exception(
f"Unknown file type {args.file_type}; "
"expected one of: ['image', 'parquet']"
)
if args.repeat_ds > 1:
args.data_root = [args.data_root] * args.repeat_ds
if args.file_type == "parquet" or args.use_torch or args.use_mosaic:
# Training model is only supported for images currently.
# Parquet files do not have labels.
args.skip_train_model = True
if args.batch_size == -1:
args.batch_size = None
return args
# Constants and utility methods for image-based benchmarks.
DEFAULT_IMAGE_SIZE = 224
def get_transform(to_torch_tensor):
# Note(swang): This is a different order from tf.data.
# torch: decode -> randCrop+resize -> randFlip
# tf.data: decode -> randCrop -> randFlip -> resize
transform = torchvision.transforms.Compose(
[
torchvision.transforms.RandomResizedCrop(
antialias=True,
size=DEFAULT_IMAGE_SIZE,
scale=(0.05, 1.0),
ratio=(0.75, 1.33),
),
torchvision.transforms.RandomHorizontalFlip(),
]
+ ([torchvision.transforms.ToTensor()] if to_torch_tensor else [])
+ [
torchvision.transforms.Normalize(
mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
)
]
)
return transform
# Capture `transform` in the map UDFs.
transform = get_transform(False)
def crop_and_flip_image(row):
# Make sure to use torch.tensor here to avoid a copy from numpy.
row["image"] = transform(
torch.tensor(np.transpose(row["image"], axes=(2, 0, 1))) / 255.0
)
return row
def center_crop_image(row):
# Used to generate the validation set. The main difference between
# `crop_and_flip_image` and this method is that the validation set
# should avoid random cropping from the full image, but instead
# should resize and take the center crop to generate more consistent
# outputs.
val_transform = torchvision.transforms.Compose(
[
torchvision.transforms.Resize(256),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.Normalize(
mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
),
]
)
# Make sure to use torch.tensor here to avoid a copy from numpy.
row["image"] = val_transform(
torch.tensor(
np.transpose(row["image"], axes=(2, 0, 1)),
)
/ 255.0
)
return row
def decode_image_crop_and_flip(row):
row["image"] = Image.frombytes("RGB", (row["height"], row["width"]), row["image"])
# Convert back np to avoid storing a np.object array.
return {"image": np.array(transform(pil_to_tensor(row["image"]) / 255.0))}
class MosaicDataset(LocalDataset):
def __init__(self, local, transforms):
super().__init__(local=local)
self.transforms = transforms
def __getitem__(self, idx):
obj = super().__getitem__(idx)
image = obj["image"]
label = obj["label"]
return self.transforms(image), label
class S3MosaicDataset(StreamingDataset):
def __init__(
self,
s3_bucket,
num_physical_nodes,
cache_dir,
transforms,
cache_limit=None,
epoch_size=None,
):
super().__init__(
remote=s3_bucket,
local=cache_dir,
cache_limit=cache_limit,
epoch_size=epoch_size,
# Set StreamingDataset to read sequentially.
shuffle=False,
num_canonical_nodes=num_physical_nodes,
)
self.transforms = transforms
def __getitem__(self, idx):
obj = super().__getitem__(idx)
image = obj["image"]
label = obj["label"]
return self.transforms(image), label
def get_mosaic_dataloader(
mosaic_data_root,
batch_size,
num_physical_nodes,
epoch_size=None,
num_workers=None,
cache_limit=None,
):
use_s3 = mosaic_data_root.startswith("s3://")
if not use_s3:
assert epoch_size is None, "epoch_size not supported for streaming.LocalDataset"
assert (
cache_limit is None
), "cache_limit not supported for streaming.LocalDataset"
if use_s3:
MOSAIC_CACHE = "/tmp/mosaic_cache"
try:
import shutil
shutil.rmtree(MOSAIC_CACHE)
except (OSError, FileNotFoundError):
pass
streaming.base.util.clean_stale_shared_memory()
print(f"Initializing mosaic StreamingDataset, cache_limit={cache_limit}")
mosaic_ds = S3MosaicDataset(
s3_bucket=mosaic_data_root,
num_physical_nodes=num_physical_nodes,
cache_dir=MOSAIC_CACHE,
cache_limit=cache_limit,
epoch_size=epoch_size,
transforms=get_transform(True),
)
else:
mosaic_ds = MosaicDataset(mosaic_data_root, transforms=get_transform(True))
if num_workers is None:
num_workers = os.cpu_count()
print(f"Initializing torch DataLoader with {num_workers} workers.")
mosaic_dl = torch.utils.data.DataLoader(
mosaic_ds,
batch_size=batch_size,
num_workers=num_workers,
drop_last=True,
)
return mosaic_dl
def _get_ray_data_batch_iterator(args, worker_rank):
if args.split_input:
it = train.get_dataset_shard(f"train_{worker_rank}")
else:
it = train.get_dataset_shard("train")
return it, it.iter_torch_batches(
batch_size=args.batch_size,
prefetch_batches=args.prefetch_batches,
local_shuffle_buffer_size=args.local_shuffle_buffer_size,
)
def _get_batch_num_rows(batch):
if not (args.use_torch or args.use_mosaic):
return batch["image"].size(dim=0)
return batch.size(dim=0)
def train_loop_per_worker():
worker_rank = train.get_context().get_world_rank()
device = train.torch.get_device()
world_size = ray.train.get_context().get_world_size()
local_world_size = ray.train.get_context().get_local_world_size()
torch_num_workers = args.torch_num_workers or os.cpu_count()
# Divide by the number of Train workers because each has its own dataloader.
torch_num_workers //= local_world_size
# Setup the model
raw_model = resnet50(weights=None)
model = train.torch.prepare_model(raw_model)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# Get the configured data loading solution.
batch_iter = None
if args.use_torch:
batch_iter = get_torch_data_loader(
worker_rank=worker_rank,
batch_size=args.batch_size,
num_workers=torch_num_workers,
transform=get_transform(False),
)
elif args.use_mosaic:
target_epoch_size = get_mosaic_epoch_size(
args.num_workers, target_worker_gb=args.target_worker_gb
)
print(
"Epoch size:",
target_epoch_size if target_epoch_size is not None else "all",
"images",
)
num_physical_nodes = world_size // local_world_size
batch_iter = get_mosaic_dataloader(
args.data_root,
batch_size=args.batch_size,
num_physical_nodes=num_physical_nodes,
epoch_size=target_epoch_size,
num_workers=torch_num_workers,
)
all_workers_time_list_across_epochs = []
validation_accuracy_per_epoch = []
# Validation loop with non-random cropped dataset
# is only supported for image dataset.
run_validation_set = (
args.use_ray_data and not args.skip_train_model and args.file_type == "image"
)
# Begin training over the configured number of epochs.
for epoch in range(args.num_epochs):
# Ray Data needs to call iter_torch_batches on each epoch.
if args.use_ray_data:
ds_shard, batch_iter = _get_ray_data_batch_iterator(args, worker_rank)
if run_validation_set:
val_ds = train.get_dataset_shard("val")
batch_iter_val = val_ds.iter_torch_batches(
batch_size=args.batch_size,
prefetch_batches=args.prefetch_batches,
local_shuffle_buffer_size=args.local_shuffle_buffer_size,
)
# For synthetic data, we need to create the iterator each epoch.
elif args.use_synthetic_data:
# Generate a random batch, and continuously yield the same batch 1000 times.
NUM_BATCHES_PER_EPOCH = 1000
sample_batch = {
"image": torch.rand(
(args.batch_size, 3, DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE),
device=device,
),
"label": torch.randint(
0,
NUM_BATCHES_PER_EPOCH,
(args.batch_size,),
device=device,
),
}
batch_iter = itertools.repeat(sample_batch, NUM_BATCHES_PER_EPOCH)
print(f"Epoch {epoch+1} of {args.num_epochs}")
num_rows = 0
start_t = time.time()
num_batches = 0.0
total_loss = 0.0
for batch_idx, batch in enumerate(batch_iter):
num_rows += _get_batch_num_rows(batch)
if not args.skip_train_model:
# get the inputs; data is a list of [inputs, labels]
inputs = torch.as_tensor(batch["image"], dtype=torch.float32).to(
device=device
)
labels = torch.as_tensor(batch["label"], dtype=torch.int64).to(
device=device
)
# forward + backward + optimize
outputs = model(inputs)
loss = criterion(outputs, labels)
# zero the parameter gradients
optimizer.zero_grad()
loss.backward()
optimizer.step()
num_batches += 1
total_loss += loss.item()
# print statistics
if batch_idx % 2000 == 1999: # print every 2000 mini-batches
print(
f"[{epoch + 1}, {batch_idx + 1:5d}]"
f"loss: {total_loss / 2000:.3f}"
)
end_t = time.time()
epoch_accuracy_val = None
if run_validation_set:
print(f"Starting validation set for epoch {epoch+1}")
num_correct_val = 0
num_rows_val = 0
with torch.no_grad():
for batch_idx, batch in enumerate(batch_iter_val):
inputs = torch.as_tensor(batch["image"], dtype=torch.float32).to(
device=device
)
labels = torch.as_tensor(batch["label"], dtype=torch.int64).to(
device=device
)
outputs = model(inputs)
loss = criterion(outputs, labels)
output_classes = outputs.argmax(dim=1)
num_rows_val += len(labels)
num_correct_val += (output_classes == labels).sum().item()
epoch_accuracy_val = num_correct_val / num_rows_val
validation_accuracy_per_epoch.append(epoch_accuracy_val)
with tempfile.TemporaryDirectory() as tmpdir:
torch.save(model.state_dict(), os.path.join(tmpdir, "model.pt"))
checkpoint = Checkpoint.from_directory(tmpdir)
train.report(
dict(
epoch_accuracy=epoch_accuracy_val,
loss_avg=(total_loss / num_batches) if num_batches > 0 else 0,
),
checkpoint=checkpoint,
)
# Workaround to report the epoch start/end time from each worker, so that we
# can aggregate them at the end when calculating throughput.
all_workers_time_list = [
torch.zeros((2), dtype=torch.double, device=device)
for _ in range(world_size)
]
curr_worker_time = torch.tensor(
[start_t, end_t], dtype=torch.double, device=device
)
dist.all_gather(all_workers_time_list, curr_worker_time)
all_workers_time_list_across_epochs.append(all_workers_time_list)
print(
f"Epoch {epoch+1} of {args.num_epochs}, "
f"tput: {num_rows / (end_t - start_t)}, "
f"run time: {end_t - start_t}, "
f"validation accuracy: "
f"{epoch_accuracy_val * 100 if epoch_accuracy_val else 0:.3f}%"
)
if args.use_ray_data:
print(f"iter stats: {ds_shard.stats()}")
if run_validation_set:
print(f"val iter stats: {val_ds.stats()}")
# Similar reporting for aggregating number of rows across workers
all_num_rows = [
torch.zeros((1), dtype=torch.int32, device=device) for _ in range(world_size)
]
curr_num_rows = torch.tensor([num_rows], dtype=torch.int32, device=device)
dist.all_gather(all_num_rows, curr_num_rows)
per_epoch_times = {
f"epoch_{i}_times": [
tensor.tolist() for tensor in all_workers_time_list_across_epochs[i]
]
for i in range(args.num_epochs)
}
final_train_report_metrics = {
**per_epoch_times,
"num_rows": [tensor.item() for tensor in all_num_rows],
}
if run_validation_set:
all_num_rows_val = [
torch.zeros((1), dtype=torch.int32, device=device)
for _ in range(world_size)
]
curr_num_rows_val = torch.tensor(
[num_rows_val], dtype=torch.int32, device=device
)
dist.all_gather(all_num_rows_val, curr_num_rows_val)
all_num_rows_correct_val = [
torch.zeros((1), dtype=torch.int32, device=device)
for _ in range(world_size)
]
curr_num_rows_correct = torch.tensor(
[num_correct_val], dtype=torch.int32, device=device
)
dist.all_gather(all_num_rows_correct_val, curr_num_rows_correct)
final_train_report_metrics.update(
{
"num_rows_val": [tensor.item() for tensor in all_num_rows_val],
"num_rows_correct_val": [
tensor.item() for tensor in all_num_rows_correct_val
],
# Report the validation accuracy of the final epoch
"epoch_accuracy": validation_accuracy_per_epoch[-1],
}
)
with tempfile.TemporaryDirectory() as tmpdir:
torch.save(model.state_dict(), os.path.join(tmpdir, "model.pt"))
checkpoint = Checkpoint.from_directory(tmpdir)
train.report(
final_train_report_metrics,
checkpoint=checkpoint,
)
# The input files URLs per training worker.
INPUT_FILES_PER_WORKER = []
def split_input_files_per_worker(args):
"""Set the input files per each training worker."""
global INPUT_FILES_PER_WORKER
import numpy as np
from torchdata.datapipes.iter import IterableWrapper
data_root_iter = args.data_root
if isinstance(data_root_iter, str):
data_root_iter = [data_root_iter]
file_url_dp = IterableWrapper(data_root_iter).list_files_by_s3()
all_files = list(file_url_dp)
INPUT_FILES_PER_WORKER = [
f.tolist() for f in np.array_split(all_files, args.num_workers)
]
def get_s3fs_with_boto_creds():
import boto3
from pyarrow import fs
credentials = boto3.Session().get_credentials()
s3fs = fs.S3FileSystem(
access_key=credentials.access_key,
secret_key=credentials.secret_key,
session_token=credentials.token,
region="us-west-2",
)
return s3fs
def get_torch_data_loader(worker_rank, batch_size, num_workers, transform=None):
"""Get PyTorch DataLoader for the specified training worker.
The input files are split across all workers, and this PyTorch DataLoader
would only read the portion of files for itself.
"""
import os
import numpy as np
from torchdata.datapipes.iter import IterableWrapper, S3FileLoader
# NOTE: these two variables need to be set to read from S3 successfully.
os.environ["S3_VERIFY_SSL"] = "0"
os.environ["AWS_REGION"] = "us-west-2"
def load_image(inputs):
import io
from PIL import Image
url, fd = inputs
data = fd.file_obj.read()
image = Image.open(io.BytesIO(data))
image = image.convert("RGB")
if transform is not None:
image = transform(
pil_to_tensor(image) / 255.0,
)
return image
class FileURLDataset:
"""The PyTorch Dataset to split input files URLs among workers."""
def __init__(self, file_urls):
self._file_urls = file_urls
def __iter__(self):
worker_info = torch.utils.data.get_worker_info()
assert worker_info is not None
torch_worker_id = worker_info.id
return iter(self._file_urls[torch_worker_id])
file_urls = INPUT_FILES_PER_WORKER[worker_rank]
file_urls = [f.tolist() for f in np.array_split(file_urls, num_workers)]
file_url_dp = IterableWrapper(FileURLDataset(file_urls))
file_dp = S3FileLoader(file_url_dp)
image_dp = file_dp.map(load_image)
# NOTE: the separate implementation for using fsspec.
# Comment out by default. Leave it here as reference.
#
# subdir_url_dp = IterableWrapper([root_dir]).list_files_by_fsspec()
# file_url_dp = subdir_url_dp.list_files_by_fsspec()
# file_dp = file_url_dp.open_files_by_fsspec(mode="rb")
# image_dp = file_dp.map(load_image)
data_loader = torch.utils.data.DataLoader(
image_dp,
batch_size=batch_size,
num_workers=num_workers,
)
return data_loader
def benchmark_code(
args,
):
ctx = ray.data.DataContext.get_current()
# This release test runs into ACCESS_DENIED errors fairly often.
# We add ACCESS_DENIED as a retryable exception type to avoid flakiness.
# See for more details: https://github.com/ray-project/ray/issues/47230
ctx.retried_io_errors.append("AWS Error ACCESS_DENIED")
if args.target_max_block_size_mb is not None:
ctx.target_max_block_size = args.target_max_block_size_mb * 1024 * 1024
cache_input_ds = args.cache_input_ds
cache_output_ds = args.cache_output_ds
assert (
sum([cache_output_ds, cache_input_ds]) <= 1
), "Can only test one caching variant at a time"
if args.use_torch or args.split_input:
split_input_files_per_worker(args)
ray_datasets_dict = {}
if not (args.use_mosaic or args.use_torch):
# Only create one dataset if `args.split_input` is True.
# Otherwise, create N datasets for N training workers,
# each dataset reads the corresponding portion of input data.
num_datasets = 1
if args.split_input:
num_datasets = args.num_workers
for i in range(num_datasets):
if args.split_input:
input_paths = INPUT_FILES_PER_WORKER[i]
ds_name = f"train_{i}"
else:
input_paths = args.data_root
ds_name = "train"
# 1) Read in data with read_images() / read_parquet()
if args.file_type == "image":
# Obtain WNID from filepath, then convert to numerical class ID
partitioning = Partitioning(
"dir",
field_names=["class"],
base_dir=args.data_root,
)
# Note: We explicitly define a filesystem using boto credentials
# due to `AWS Error ACCESS_DENIED` issues with pyarrow.fs.
# See for more details and potential downsides:
# https://github.com/ray-project/ray/issues/47230#issuecomment-2313645254 # noqa: E501
fs = get_s3fs_with_boto_creds()
ray_dataset = ray.data.read_images(
input_paths,
filesystem=fs,
mode="RGB",
shuffle="files",
partitioning=partitioning,
)
val_dataset = ray.data.Dataset.copy(ray_dataset)
# Full random shuffle results in OOM. Instead, use the
# `ds.iter_batches(local_shuffle_buffer_size=...)`
# parameter in the training loop.
# ray_dataset = ray_dataset.random_shuffle()
def wnid_to_index(row):
row["label"] = IMAGENET_WNID_TO_ID[row["class"]]
row.pop("class")
return row
ray_dataset = ray_dataset.map(wnid_to_index)
val_dataset = val_dataset.map(wnid_to_index)
elif args.file_type == "parquet":
ray_dataset = ray.data.read_parquet(
args.data_root,
)
else:
raise Exception(f"Unknown file type {args.file_type}")
if cache_input_ds:
ray_dataset = ray_dataset.materialize()
# 2) Preprocess data by applying transformation with map/map_batches()
if args.file_type == "image":
ray_dataset = ray_dataset.map(crop_and_flip_image)
val_dataset = val_dataset.map(center_crop_image)
ray_datasets_dict["val"] = val_dataset
elif args.file_type == "parquet":
ray_dataset = ray_dataset.map(decode_image_crop_and_flip)
if cache_output_ds:
ray_dataset = ray_dataset.materialize()
ray_datasets_dict[ds_name] = ray_dataset
# 3) Train TorchTrainer on processed data
options = DataConfig.default_ingest_options()
options.preserve_order = args.preserve_order
if args.skip_ray_trainer:
start_t = time.time()
num_rows = 0
for batch in ray_datasets_dict["train"].iter_torch_batches(
batch_size=args.batch_size,
prefetch_batches=args.prefetch_batches,
local_shuffle_buffer_size=args.local_shuffle_buffer_size,
):
num_rows += len(batch["label"])
end_t = time.time()
tput = num_rows / (end_t - start_t)
data_benchmark_metrics = {BenchmarkMetric.THROUGHPUT: tput}
return data_benchmark_metrics
torch_trainer = TorchTrainer(
train_loop_per_worker,
datasets=ray_datasets_dict,
scaling_config=ScalingConfig(
num_workers=args.num_workers,
use_gpu=args.use_gpu,
),
dataset_config=ray.train.DataConfig(
datasets_to_split=[] if args.split_input else "all",
execution_options=options,
),
run_config=RunConfig(
storage_path="/mnt/cluster_storage",
failure_config=train.FailureConfig(args.num_retries),
),
)
result = torch_trainer.fit()
data_benchmark_metrics = {}
# Report the average of per-epoch throughput, excluding the first epoch.
# Unless there is only one epoch, in which case we report the epoch
# throughput directly.
start_epoch_tput = 0 if args.num_epochs == 1 else 1
epoch_tputs = []
num_rows_per_epoch = sum(result.metrics["num_rows"])
for i in range(start_epoch_tput, args.num_epochs):
time_start_epoch_i, time_end_epoch_i = zip(*result.metrics[f"epoch_{i}_times"])
runtime_epoch_i = max(time_end_epoch_i) - min(time_start_epoch_i)
tput_epoch_i = num_rows_per_epoch / runtime_epoch_i
epoch_tputs.append(tput_epoch_i)
avg_per_epoch_tput = sum(epoch_tputs) / len(epoch_tputs)
print("Total num rows read per epoch:", num_rows_per_epoch, "images")
print("Averaged per-epoch throughput:", avg_per_epoch_tput, "img/s")
data_benchmark_metrics.update(
{
BenchmarkMetric.THROUGHPUT: avg_per_epoch_tput,
}
)
# Report the training accuracy of the final epoch.
if result.metrics.get("num_rows_val") is not None:
final_epoch_acc = sum(result.metrics["num_rows_correct_val"]) / sum(
result.metrics["num_rows_val"]
)
print(f"Final epoch accuracy: {final_epoch_acc * 100:.3f}%")
data_benchmark_metrics.update(
{
BenchmarkMetric.ACCURACY: final_epoch_acc,
}
)
return data_benchmark_metrics
if __name__ == "__main__":
args = parse_args()
data_type = "synthetic" if args.use_synthetic_data else args.file_type
benchmark_name = (
f"read_{data_type}_repeat{args.repeat_ds}_train_"
f"{args.num_workers}workers_{args.target_worker_gb}gb_per_worker"
)
if args.preserve_order:
benchmark_name = f"{benchmark_name}_preserve_order"
if not args.skip_train_model:
benchmark_name = f"{benchmark_name}_resnet50"
if args.cache_input_ds:
case_name = "cache-input"
elif args.cache_output_ds:
case_name = "cache-output"
else:
case_name = "cache-none"
benchmark = Benchmark()
benchmark.run_fn(case_name, benchmark_code, args=args)
benchmark.write_result()