mirror of
https://github.com/hanzoai/gym-v1.git
synced 2026-10-30 18:35:21 +00:00
394 lines
14 KiB
Python
394 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Image classification training using EXO Gym distributed training framework.
|
|
Supports ConvNeXt V2 Nano on ImageNet-100 with various distributed strategies.
|
|
|
|
Usage:
|
|
python example/image_train.py --strategy diloco --num_nodes 4
|
|
python example/image_train.py --strategy sparta --num_nodes 8 --p_sparta 0.01
|
|
"""
|
|
|
|
import argparse
|
|
import torch
|
|
import torch.nn as nn
|
|
import timm
|
|
import random
|
|
from functools import partial
|
|
from torchvision import transforms
|
|
from torch.utils.data import DataLoader, Subset
|
|
from datasets import load_dataset
|
|
|
|
from exogym.trainer import Trainer
|
|
from exogym.strategy.optim import OptimSpec
|
|
|
|
|
|
# Adapter class must be top-level to be picklable
|
|
class HFDataset(torch.utils.data.Dataset):
|
|
"""Wrap a Hugging Face split to behave like a torchvision dataset."""
|
|
|
|
def __init__(self, hf_ds, tf):
|
|
self.ds = hf_ds
|
|
self.tf = tf
|
|
|
|
def __len__(self):
|
|
return len(self.ds)
|
|
|
|
def __getitem__(self, idx):
|
|
ex = self.ds[idx]
|
|
img = ex["image"].convert("RGB")
|
|
return self.tf(img), ex["label"]
|
|
|
|
|
|
def image_dataset_factory(subset: float, seed: int, batch_size: int,
|
|
rank: int, num_nodes: int, train_dataset: bool) -> torch.utils.data.Dataset:
|
|
"""Dataset factory function for ImageNet-100 dataset."""
|
|
|
|
if train_dataset:
|
|
tf = transforms.Compose([
|
|
transforms.RandomResizedCrop(224),
|
|
transforms.RandomHorizontalFlip(),
|
|
transforms.ToTensor(),
|
|
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
|
])
|
|
ds = load_dataset("clane9/imagenet-100", split="train")
|
|
else:
|
|
tf = transforms.Compose([
|
|
transforms.Resize(256),
|
|
transforms.CenterCrop(224),
|
|
transforms.ToTensor(),
|
|
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
|
])
|
|
ds = load_dataset("clane9/imagenet-100", split="validation")
|
|
|
|
dataset = HFDataset(ds, tf)
|
|
|
|
# Apply subset for training data
|
|
if train_dataset and subset < 1.0:
|
|
random.seed(seed)
|
|
indices = list(range(len(dataset)))
|
|
random.shuffle(indices)
|
|
dataset = Subset(dataset, indices[:int(len(indices) * subset)])
|
|
|
|
# For distributed training, split dataset across nodes
|
|
if train_dataset and num_nodes > 1:
|
|
total_samples = len(dataset)
|
|
samples_per_node = total_samples // num_nodes
|
|
start_idx = rank * samples_per_node
|
|
end_idx = start_idx + samples_per_node if rank < num_nodes - 1 else total_samples
|
|
|
|
if hasattr(dataset, 'indices'): # Subset case
|
|
node_indices = dataset.indices[start_idx:end_idx]
|
|
dataset = Subset(dataset.dataset, node_indices)
|
|
else: # Full dataset case
|
|
indices = list(range(start_idx, end_idx))
|
|
dataset = Subset(dataset, indices)
|
|
|
|
return dataset
|
|
|
|
|
|
def gen_run_name(args, strategy):
|
|
"""Generate wandb name based on strategy and arguments."""
|
|
base_name = f"bs{args.batch_size}_lr{args.lr:.0e}_subset{args.subset}"
|
|
|
|
if strategy == "ddp":
|
|
return f"image_ddp_{base_name}_n{args.num_nodes}"
|
|
elif strategy == "fedavg":
|
|
return f"image_fedavg_{base_name}_H{args.H}_n{args.num_nodes}"
|
|
elif strategy == "sparta":
|
|
return f"image_sparta_p{args.p_sparta}_n{args.num_nodes}_lr{args.lr:.0e}"
|
|
elif strategy == "diloco":
|
|
return f"image_diloco_{base_name}_outer{args.outer_lr:.0e}_H{args.H}"
|
|
elif strategy == "demo":
|
|
return f"image_demo_{base_name}_topk{args.compression_topk}_decay{args.compression_decay}"
|
|
elif strategy == "diloco_sparta":
|
|
return f"image_diloco_sparta_{base_name}_outer{args.outer_lr:.0e}_H{args.H}_p{args.p_sparta}"
|
|
elif strategy == "dgc":
|
|
return f"image_dgc_{base_name}_sparsity{args.dgc_sparsity}_warmup{args.dgc_warmup_steps}"
|
|
else:
|
|
return f"image_{base_name}"
|
|
|
|
|
|
def arg_parse():
|
|
"""Create parser with all arguments for all strategies."""
|
|
parser = argparse.ArgumentParser(conflict_handler="resolve")
|
|
|
|
# Dataset arguments
|
|
parser.add_argument("--subset", type=float, default=1.0, help="Train subset fraction (0 < s ≤ 1)")
|
|
parser.add_argument("--seed", type=int, default=0)
|
|
|
|
# Training arguments
|
|
parser.add_argument("--num_nodes", type=int, default=4)
|
|
parser.add_argument("--device", type=str, default="")
|
|
parser.add_argument("--epochs", type=int, default=1)
|
|
parser.add_argument("--model_name", type=str, default="convnextv2_tiny",
|
|
help="timm model name")
|
|
|
|
# Optimization arguments
|
|
parser.add_argument("--batch_size", type=int, default=64)
|
|
parser.add_argument("--minibatch_size", type=int, default=64)
|
|
parser.add_argument("--lr", type=float, default=1e-3)
|
|
parser.add_argument("--weight_decay", type=float, default=0.01)
|
|
parser.add_argument("--max_norm", type=float, default=1.0)
|
|
parser.add_argument("--warmup_steps", type=int, default=5000)
|
|
parser.add_argument("--max_steps", type=int, default=30000)
|
|
parser.add_argument("--cosine_anneal", action="store_true", default=True)
|
|
|
|
# Logging and reproducibility
|
|
parser.add_argument("--wandb_project", type=str, default='image_exogym')
|
|
parser.add_argument("--run_name", type=str, default=None)
|
|
parser.add_argument("--val_size", type=int, default=512)
|
|
parser.add_argument("--val_interval", type=int, default=200)
|
|
parser.add_argument("--correlation_interval", type=int, default=None)
|
|
parser.add_argument("--checkpoint_interval", type=int, default=None)
|
|
parser.add_argument("--save_dir", type=str, default="./checkpoints")
|
|
|
|
# Strategy selection
|
|
parser.add_argument(
|
|
"--strategy",
|
|
type=str,
|
|
default="ddp",
|
|
choices=["base", "ddp", "fedavg", "sparta", "diloco", "demo", "diloco_sparta", "dgc"],
|
|
help="Training strategy to use",
|
|
)
|
|
|
|
# FedAvg-specific arguments
|
|
parser.add_argument("--H", type=int, default=100, help="FedAvg communication interval")
|
|
parser.add_argument("--island_size", type=int, default=None, help="FedAvg island size")
|
|
|
|
# SPARTA-specific arguments
|
|
parser.add_argument("--p_sparta", type=float, default=0.005, help="SPARTA sparsity parameter")
|
|
parser.add_argument("--async_sparta_delay", type=int, default=0, help="SPARTA async delay")
|
|
parser.add_argument("--sparta_interval", type=int, default=1, help="SPARTA communication interval")
|
|
|
|
# DiLoCo-specific arguments
|
|
parser.add_argument("--outer_lr", type=float, default=0.7, help="DiLoCo outer learning rate")
|
|
parser.add_argument("--nesterov", type=bool, default=True, help="DiLoCo Nesterov momentum")
|
|
parser.add_argument("--outer_momentum", type=float, default=0.9, help="DiLoCo outer momentum")
|
|
|
|
# DeMo-specific arguments
|
|
parser.add_argument("--compression_decay", type=float, default=0.999,
|
|
help="DeMo gradient error feedback decay")
|
|
parser.add_argument("--compression_topk", type=int, default=32, help="DeMo top-k compression")
|
|
parser.add_argument("--compression_chunk", type=int, default=64, help="DeMo DCT chunk size")
|
|
|
|
# DGC-specific arguments
|
|
parser.add_argument("--dgc_sparsity", type=float, default=0.001,
|
|
help="DGC target sparsity (0.001 = 0.1%)")
|
|
parser.add_argument("--dgc_warmup_steps", type=int, default=500,
|
|
help="DGC warmup steps before sparsification")
|
|
parser.add_argument("--dgc_clip_threshold", type=float, default=1.0,
|
|
help="DGC gradient clipping threshold")
|
|
parser.add_argument("--dgc_momentum", type=float, default=0.9, help="DGC momentum factor")
|
|
|
|
return parser
|
|
|
|
|
|
def create_strategy(args):
|
|
"""Create strategy based on args.strategy selection."""
|
|
|
|
# Common lr scheduler config
|
|
lr_scheduler_kwargs = {
|
|
"warmup_steps": args.warmup_steps,
|
|
"cosine_anneal": args.cosine_anneal,
|
|
}
|
|
|
|
if args.strategy == "ddp" or args.strategy == "base" or args.strategy == "":
|
|
from exogym.strategy.strategy import SimpleReduceStrategy
|
|
|
|
optim = OptimSpec(torch.optim.AdamW, lr=args.lr, weight_decay=args.weight_decay)
|
|
return SimpleReduceStrategy(
|
|
optim_spec=optim,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
)
|
|
|
|
elif args.strategy == "fedavg":
|
|
from exogym.strategy.federated_averaging import FedAvgStrategy
|
|
|
|
if args.island_size is None:
|
|
args.island_size = args.num_nodes
|
|
optim = OptimSpec(torch.optim.AdamW, lr=args.lr, weight_decay=args.weight_decay)
|
|
return FedAvgStrategy(
|
|
inner_optim_spec=optim,
|
|
H=args.H,
|
|
island_size=args.island_size,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
)
|
|
|
|
elif args.strategy == "sparta":
|
|
from exogym.strategy.sparta import SPARTAStrategy
|
|
|
|
optim = OptimSpec(torch.optim.AdamW, lr=args.lr, weight_decay=args.weight_decay)
|
|
return SPARTAStrategy(
|
|
optim_spec=optim,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
p_sparta=args.p_sparta,
|
|
async_sparta_delay=args.async_sparta_delay,
|
|
)
|
|
|
|
elif args.strategy == "diloco":
|
|
from exogym.strategy.diloco import DiLoCoStrategy
|
|
|
|
inner_optim = OptimSpec(torch.optim.AdamW, lr=args.lr, weight_decay=args.weight_decay)
|
|
outer_optim = OptimSpec(
|
|
torch.optim.SGD,
|
|
lr=args.outer_lr,
|
|
nesterov=args.nesterov,
|
|
momentum=args.outer_momentum,
|
|
)
|
|
return DiLoCoStrategy(
|
|
optim_spec=inner_optim,
|
|
outer_optim_spec=outer_optim,
|
|
H=args.H,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
)
|
|
|
|
elif args.strategy == "demo":
|
|
from exogym.strategy.demo import DeMoStrategy
|
|
|
|
optim = OptimSpec(
|
|
torch.optim.AdamW,
|
|
lr=args.lr,
|
|
weight_decay=args.weight_decay,
|
|
)
|
|
return DeMoStrategy(
|
|
optim_spec=optim,
|
|
compression_decay=args.compression_decay,
|
|
compression_topk=args.compression_topk,
|
|
compression_chunk=args.compression_chunk,
|
|
weight_decay=args.weight_decay,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
)
|
|
|
|
elif args.strategy == "diloco_sparta":
|
|
from exogym.strategy.sparta_diloco import SPARTADiLoCoStrategy
|
|
|
|
inner_optim = OptimSpec(torch.optim.AdamW, lr=args.lr, weight_decay=args.weight_decay)
|
|
outer_optim = OptimSpec(
|
|
torch.optim.SGD,
|
|
lr=args.outer_lr,
|
|
nesterov=args.nesterov,
|
|
momentum=args.outer_momentum,
|
|
)
|
|
return SPARTADiLoCoStrategy(
|
|
inner_optim_spec=inner_optim,
|
|
outer_optim_spec=outer_optim,
|
|
H=args.H,
|
|
p_sparta=args.p_sparta,
|
|
sparta_interval=args.sparta_interval,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
)
|
|
|
|
elif args.strategy == "dgc":
|
|
from exogym.strategy.dgc import DGCStrategy
|
|
|
|
# DGC works best with SGD + momentum
|
|
optim = OptimSpec(
|
|
torch.optim.SGD,
|
|
lr=args.lr,
|
|
momentum=args.dgc_momentum,
|
|
)
|
|
return DGCStrategy(
|
|
optim_spec=optim,
|
|
target_sparsity=args.dgc_sparsity,
|
|
warmup_steps=args.dgc_warmup_steps,
|
|
clip_threshold=args.dgc_clip_threshold,
|
|
momentum=args.dgc_momentum,
|
|
lr_scheduler="lambda_cosine",
|
|
lr_scheduler_kwargs=lr_scheduler_kwargs,
|
|
max_norm=args.max_norm,
|
|
)
|
|
|
|
else:
|
|
raise ValueError(f"Unknown strategy: {args.strategy}")
|
|
|
|
|
|
class ImageClassificationModel(nn.Module):
|
|
"""Wrapper around timm model to provide loss computation."""
|
|
|
|
def __init__(self, model_name: str, num_classes: int):
|
|
super().__init__()
|
|
self.model = timm.create_model(model_name, pretrained=False, num_classes=num_classes)
|
|
self.loss_fn = nn.CrossEntropyLoss()
|
|
|
|
def forward(self, batch):
|
|
"""Forward pass that returns loss for training."""
|
|
x, y = batch
|
|
logits = self.model(x)
|
|
return self.loss_fn(logits, y)
|
|
|
|
|
|
def main():
|
|
parser = arg_parse()
|
|
args = parser.parse_args()
|
|
|
|
# Set seeds
|
|
random.seed(args.seed)
|
|
torch.manual_seed(args.seed)
|
|
|
|
# Get number of classes from dataset
|
|
ds_train = load_dataset("clane9/imagenet-100", split="train")
|
|
num_classes = ds_train.features["label"].num_classes
|
|
|
|
# Create dataset factory
|
|
dataset_factory = partial(
|
|
image_dataset_factory,
|
|
args.subset,
|
|
args.seed,
|
|
args.batch_size
|
|
)
|
|
|
|
# Create model
|
|
model = ImageClassificationModel(args.model_name, num_classes)
|
|
|
|
# Create trainer
|
|
trainer = Trainer(
|
|
model,
|
|
dataset_factory, # train_dataset
|
|
dataset_factory, # val_dataset
|
|
)
|
|
|
|
# Create strategy based on selection
|
|
strategy = create_strategy(args)
|
|
|
|
# Define dataloader kwargs for better GPU utilization
|
|
dataloader_kwargs = {
|
|
"num_workers": 8,
|
|
"pin_memory": True,
|
|
"persistent_workers": True,
|
|
"prefetch_factor": 4,
|
|
}
|
|
|
|
# Train
|
|
trainer.fit(
|
|
num_epochs=args.epochs,
|
|
max_steps=args.max_steps,
|
|
strategy=strategy,
|
|
num_nodes=args.num_nodes,
|
|
device=args.device,
|
|
batch_size=args.batch_size,
|
|
minibatch_size=args.minibatch_size or args.batch_size,
|
|
shuffle=True,
|
|
val_size=args.val_size,
|
|
val_interval=args.val_interval,
|
|
correlation_interval=args.correlation_interval,
|
|
save_dir=args.save_dir,
|
|
wandb_project=args.wandb_project,
|
|
run_name=args.run_name or gen_run_name(args, args.strategy),
|
|
dataloader_kwargs=dataloader_kwargs,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |