364 lines
19 KiB
Python
364 lines
19 KiB
Python
import argparse
|
|
import functools
|
|
import logging
|
|
import math
|
|
import os
|
|
from datetime import timedelta
|
|
|
|
import datasets
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import torch.distributed as dist
|
|
from accelerate import Accelerator, InitProcessGroupKwargs
|
|
from accelerate.logging import get_logger
|
|
from accelerate.utils import ProjectConfiguration, set_seed
|
|
|
|
from datasets import load_dataset
|
|
from torchvision import transforms
|
|
from torchvision.datasets import ImageFolder
|
|
|
|
import diffusers
|
|
from diffusers.training_utils import compute_snr
|
|
from diffusers.optimization import get_scheduler
|
|
|
|
from models import All_models, DiT, Transformer, EMAModel
|
|
from timm.models import create_model
|
|
from utils import center_crop_arr, safe_blob_write, load_vae
|
|
from schedule.ddpm import DDPMScheduler
|
|
|
|
import wandb
|
|
|
|
logger = get_logger(__name__, log_level="INFO")
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Simple example of a training script.")
|
|
|
|
# 基本参数
|
|
parser.add_argument("--seed", type=int, default=0, help="A seed to use for the random number generator. Can be negative to not set a seed.")
|
|
parser.add_argument("--output_dir", type=str, default="results", help="The output directory where the model predictions and checkpoints will be written.")
|
|
parser.add_argument("--cache_dir", type=str, default="/mnt/msranlp/yutao/cache", help="The directory where the downloaded models and datasets will be stored.")
|
|
parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank")
|
|
|
|
# 数据集参数
|
|
parser.add_argument("--dataset_name", type=str, default=None, help="The name of the Dataset (from the HuggingFace hub) to train on.")
|
|
parser.add_argument("--dataset_config_name", type=str, default=None, help="The config of the Dataset, leave as None if there's only one config.")
|
|
parser.add_argument("--train_data_dir", type=str, default="/tmp/ILSVRC/Data/CLS-LOC/train", help="A folder containing the training data.")
|
|
|
|
# 模型参数
|
|
parser.add_argument("--model", type=str, default="Transformer-L", help="The config of the UNet model to train.")
|
|
parser.add_argument("--vae", type=str, default=None, help="Path to pre-trained VAE model.")
|
|
parser.add_argument("--image_size", type=int, default=256, help="The image_size for input images.")
|
|
parser.add_argument("--num_classes", type=int, default=1000, help="Number of classes for the model.")
|
|
parser.add_argument("--dropout", type=float, default=0.0, help="Dropout probability.")
|
|
|
|
# 训练参数
|
|
parser.add_argument("--batch_size", type=int, default=32, help="Batch size (per device) for the training dataloader.")
|
|
parser.add_argument("--num_epochs", type=int, default=100, help="Number of epochs to train for.")
|
|
parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Number of updates steps to accumulate before performing a backward/update pass.")
|
|
parser.add_argument("--dataloader_num_workers", type=int, default=2, help="The number of subprocesses to use for data loading.")
|
|
|
|
# 优化器参数
|
|
parser.add_argument("--learning_rate", type=float, default=1e-4, help="Initial learning rate (after the potential warmup period) to use.")
|
|
parser.add_argument("--lr_scheduler", type=str, default="cosine", help="The scheduler type to use.")
|
|
parser.add_argument("--lr_warmup_steps", type=int, default=100, help="Number of steps for the warmup in the lr scheduler.")
|
|
parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.")
|
|
parser.add_argument("--adam_beta2", type=float, default=0.98, help="The beta2 parameter for the Adam optimizer.")
|
|
parser.add_argument("--adam_weight_decay", type=float, default=0.01, help="Weight decay magnitude for the Adam optimizer.")
|
|
parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer.")
|
|
|
|
# EMA参数
|
|
parser.add_argument("--use_ema", action="store_true", help="Whether to use Exponential Moving Average for the final model weights.")
|
|
parser.add_argument("--ema_inv_gamma", type=float, default=1.0, help="The inverse gamma value for the EMA decay.")
|
|
parser.add_argument("--ema_power", type=float, default=3 / 4, help="The power value for the EMA decay.")
|
|
parser.add_argument("--ema_max_decay", type=float, default=0.9999, help="The maximum decay magnitude for EMA.")
|
|
|
|
# 日志参数
|
|
parser.add_argument("--logger", type=str, default=None, help="The logger type to use.")
|
|
parser.add_argument("--logging_dir", type=str, default="logs", help="The directory to store logs.")
|
|
parser.add_argument("--wandb_project", type=str, default=None, help="The wandb project name.")
|
|
parser.add_argument("--wandb_entity", type=str, default=None, help="The wandb entity (username or team).")
|
|
parser.add_argument("--log_every", type=int, default=100, help="Log every X steps.")
|
|
|
|
# 分布式训练参数
|
|
parser.add_argument("--mixed_precision", type=str, default="no", choices=["no", "fp16", "bf16"], help="Whether to use mixed precision.")
|
|
|
|
# DDPM参数
|
|
parser.add_argument("--prediction_type", type=str, default="epsilon", help="Whether the model should predict the 'epsilon'/noise error or directly the reconstructed image 'x0'.")
|
|
parser.add_argument("--ddpm_num_steps", type=int, default=1000, help="The number of steps to use for DDPM.")
|
|
parser.add_argument("--ddpm_num_inference_steps", type=int, default=1000, help="The number of inference steps to use for DDPM.")
|
|
parser.add_argument("--ddpm_beta_schedule", type=str, default="cosine", help="The beta schedule to use for DDPM.")
|
|
parser.add_argument("--ddpm_batch_mul", type=int, default=4, help="The batch multiplier to use for DDPM.")
|
|
parser.add_argument("--checkpointing_steps", type=int, default=5000, help="Save a checkpoint of the training state every X updates.")
|
|
parser.add_argument("--checkpoint", type=str, default=None, help="Resume training from a previous checkpoint.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
env_local_rank = int(os.environ.get("LOCAL_RANK", -1))
|
|
if env_local_rank != -1 and env_local_rank != args.local_rank:
|
|
args.local_rank = env_local_rank
|
|
if args.dataset_name is None and args.train_data_dir is None:
|
|
raise ValueError("You must specify either a dataset name from the hub or a train data directory.")
|
|
|
|
return args
|
|
|
|
|
|
def main(args):
|
|
set_seed(args.seed)
|
|
logging_dir = os.path.join(args.output_dir, args.logging_dir)
|
|
|
|
vae, input_size, latent_size, flatten_input = load_vae(args.vae, args.image_size)
|
|
|
|
model = All_models[args.model](
|
|
input_size=input_size,
|
|
in_channels=latent_size,
|
|
num_classes=args.num_classes,
|
|
flatten_input=flatten_input,
|
|
drop=args.dropout,
|
|
)
|
|
if args.mixed_precision == "bf16":
|
|
dtype = torch.bfloat16
|
|
elif args.mixed_precision == "fp16":
|
|
dtype = torch.float16
|
|
else:
|
|
dtype = torch.float32
|
|
|
|
# Create EMA for the model.
|
|
if args.use_ema:
|
|
ema_model = EMAModel(
|
|
model.parameters(),
|
|
decay=args.ema_max_decay,
|
|
min_decay=args.ema_max_decay,
|
|
use_ema_warmup=True,
|
|
inv_gamma=args.ema_inv_gamma,
|
|
power=args.ema_power,
|
|
)
|
|
# Initialize the scheduler
|
|
noise_scheduler = DDPMScheduler(num_train_timesteps=args.ddpm_num_steps, beta_schedule=args.ddpm_beta_schedule, prediction_type=args.prediction_type)
|
|
|
|
# Initialize the optimizer
|
|
optimizer = torch.optim.AdamW(
|
|
model.parameters(),
|
|
lr=args.learning_rate,
|
|
betas=(args.adam_beta1, args.adam_beta2),
|
|
weight_decay=args.adam_weight_decay,
|
|
eps=args.adam_epsilon,
|
|
)
|
|
|
|
# Initialize the accelerator
|
|
accelerator_project_config = ProjectConfiguration(project_dir=args.output_dir, logging_dir=logging_dir)
|
|
kwargs = InitProcessGroupKwargs(timeout=timedelta(seconds=7200)) # a big number for high image_size or big dataset
|
|
accelerator = Accelerator(
|
|
gradient_accumulation_steps=args.gradient_accumulation_steps,
|
|
mixed_precision=args.mixed_precision,
|
|
log_with=args.logger,
|
|
project_config=accelerator_project_config,
|
|
kwargs_handlers=[kwargs],
|
|
)
|
|
# Make one log on every process with the configuration for debugging.
|
|
logging.basicConfig(
|
|
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
|
datefmt="%m/%d/%Y %H:%M:%S",
|
|
level=logging.INFO,
|
|
)
|
|
logger.info(accelerator.state, main_process_only=False)
|
|
if accelerator.is_local_main_process:
|
|
datasets.utils.logging.set_verbosity_warning()
|
|
diffusers.utils.logging.set_verbosity_info()
|
|
else:
|
|
datasets.utils.logging.set_verbosity_error()
|
|
diffusers.utils.logging.set_verbosity_error()
|
|
|
|
logger.info(args)
|
|
if accelerator.is_main_process:
|
|
if args.output_dir is not None:
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
if args.wandb_project is not None:
|
|
wandb.init(project=args.wandb_project, entity=args.wandb_entity, config=args)
|
|
|
|
logger.info(model)
|
|
logger.info(f"Model Parameters: {sum(p.numel() for p in model.parameters()):,}")
|
|
|
|
# In distributed training, the load_dataset function guarantees that only one local process can concurrently
|
|
# download the dataset.
|
|
augmentations = transforms.Compose([
|
|
transforms.Lambda(lambda pil_image: center_crop_arr(pil_image, args.image_size)),
|
|
transforms.RandomHorizontalFlip(),
|
|
transforms.ToTensor(),
|
|
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True)
|
|
])
|
|
if args.dataset_name is not None:
|
|
dataset = load_dataset(
|
|
args.dataset_name,
|
|
args.dataset_config_name,
|
|
cache_dir=args.cache_dir,
|
|
split="train",
|
|
)
|
|
def transform_images(examples):
|
|
images = [augmentations(image.convert("RGB")) for image in examples["image"]]
|
|
return {"input": images}
|
|
dataset.set_transform(transform_images)
|
|
else:
|
|
dataset = ImageFolder(args.train_data_dir, transform=augmentations)
|
|
|
|
train_dataloader = torch.utils.data.DataLoader(
|
|
dataset, batch_size=args.batch_size, shuffle=True, num_workers=args.dataloader_num_workers
|
|
)
|
|
# Initialize the learning rate scheduler
|
|
lr_scheduler = get_scheduler(
|
|
args.lr_scheduler,
|
|
optimizer=optimizer,
|
|
num_warmup_steps=args.lr_warmup_steps * accelerator.num_processes,
|
|
num_training_steps=(len(train_dataloader) * args.num_epochs // args.gradient_accumulation_steps),
|
|
)
|
|
# Prepare everything with our `accelerator`.
|
|
model, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
|
|
model, optimizer, train_dataloader, lr_scheduler
|
|
)
|
|
# vae = accelerator.prepare_model(vae, evaluation_mode=True, device_placement=True)
|
|
vae.to(accelerator.device)
|
|
vae.eval()
|
|
if args.use_ema:
|
|
ema_model.to(accelerator.device)
|
|
|
|
# We need to initialize the trackers we use, and also store our configuration.
|
|
# The trackers initializes automatically on the main process.
|
|
if accelerator.is_main_process:
|
|
run = os.path.split(__file__)[-1].split(".")[0]
|
|
accelerator.init_trackers(run)
|
|
|
|
total_batch_size = args.batch_size * accelerator.num_processes * args.gradient_accumulation_steps
|
|
max_train_steps = len(train_dataloader) * args.num_epochs // args.gradient_accumulation_steps
|
|
|
|
logger.info("***** Running training *****")
|
|
logger.info(f" Num examples = {len(dataset)}")
|
|
logger.info(f" Num Epochs = {args.num_epochs}")
|
|
logger.info(f" Instantaneous batch size per device = {args.batch_size}")
|
|
logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}")
|
|
logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}")
|
|
logger.info(f" Total optimization steps = {max_train_steps}")
|
|
|
|
global_step = 0
|
|
running_loss = 0
|
|
first_epoch = 0
|
|
scaling_factor = None
|
|
bias_factor = None
|
|
# Potentially load in the weights and states from a previous save
|
|
checkpoint_path = args.checkpoint
|
|
if checkpoint_path is None and os.path.exists(os.path.join(args.output_dir, "latest")):
|
|
with open(os.path.join(args.output_dir, "latest"), "r") as f:
|
|
checkpoint_path = f.read().strip()
|
|
|
|
if checkpoint_path is not None:
|
|
accelerator.print(f"Resuming from checkpoint {checkpoint_path}")
|
|
accelerator.load_state(checkpoint_path)
|
|
other_state = torch.load(os.path.join(checkpoint_path, "other_state.pth"), map_location="cpu")
|
|
global_step = other_state["steps"]
|
|
scaling_factor = other_state["scaling_factor"]
|
|
bias_factor = other_state["bias_factor"]
|
|
if args.use_ema:
|
|
ema_model.load_state_dict(other_state["ema"])
|
|
logger.info("EMA model loaded successfully")
|
|
first_epoch = global_step * args.gradient_accumulation_steps // len(train_dataloader)
|
|
resume_step = global_step * args.gradient_accumulation_steps % len(train_dataloader)
|
|
|
|
# Train!
|
|
# snr = compute_snr(noise_scheduler, torch.arange(args.ddpm_num_steps, device=accelerator.device))
|
|
# sample_weight = (
|
|
# torch.stack([snr, 5 * torch.ones(args.ddpm_num_steps, device=accelerator.device)], dim=1).min(dim=1)[0] / snr
|
|
# )
|
|
sample_weight = torch.ones(args.ddpm_num_steps, device=accelerator.device)
|
|
for epoch in range(first_epoch, args.num_epochs):
|
|
model.train()
|
|
for step, (clean_images, label) in enumerate(train_dataloader):
|
|
# Skip steps until we reach the resumed step
|
|
if args.checkpoint and epoch == first_epoch:
|
|
if step < resume_step:
|
|
continue
|
|
|
|
with torch.no_grad():
|
|
vae_latent = vae.encode(clean_images)
|
|
clean_images = vae_latent.sample()
|
|
mode_images = vae_latent.mode()
|
|
if scaling_factor is None:
|
|
scaling_factor = 1. / clean_images.flatten().std()
|
|
bias_factor = -clean_images.flatten().mean()
|
|
dist.all_reduce(scaling_factor, op=dist.ReduceOp.SUM)
|
|
dist.all_reduce(bias_factor, op=dist.ReduceOp.SUM)
|
|
scaling_factor = scaling_factor.item() / dist.get_world_size()
|
|
bias_factor = bias_factor.item() / dist.get_world_size()
|
|
logger.info(f"Scaling factor: {scaling_factor}, Bias factor: {bias_factor}")
|
|
clean_images = (clean_images + bias_factor) * scaling_factor
|
|
mode_images = (mode_images + bias_factor) * scaling_factor
|
|
|
|
with accelerator.accumulate(model):
|
|
bsz, latent_size, h, w = clean_images.shape
|
|
if isinstance(model.module, Transformer):
|
|
noise = torch.randn((bsz * args.ddpm_batch_mul * h * w, latent_size), device=clean_images.device, dtype=clean_images.dtype)
|
|
timesteps = torch.multinomial(sample_weight, bsz * args.ddpm_batch_mul * h * w, replacement=True)
|
|
clean_images_repeated = clean_images.repeat_interleave(args.ddpm_batch_mul, dim=0).permute(0, 2, 3, 1).reshape(-1, clean_images.shape[1])
|
|
noisy_images = noise_scheduler.add_noise(clean_images_repeated, noise, timesteps)
|
|
velocity = noise_scheduler.get_velocity(clean_images_repeated, noise, timesteps)
|
|
noisy_images, noise, velocity = [x.reshape(bsz * args.ddpm_batch_mul, h, w, latent_size).permute(0, 3, 1, 2) for x in [noisy_images, noise, velocity]]
|
|
timesteps = timesteps.reshape(bsz * args.ddpm_batch_mul, h * w)
|
|
model_output = model(noisy_images.to(dtype), timesteps.to(dtype), x_start=clean_images.to(dtype), y=label, batch_mul=args.ddpm_batch_mul)
|
|
elif isinstance(model.module, DiT):
|
|
noise = torch.randn_like(clean_images)
|
|
timesteps = torch.multinomial(sample_weight, bsz, replacement=True)
|
|
noisy_images = noise_scheduler.add_noise(clean_images, noise, timesteps)
|
|
velocity = noise_scheduler.get_velocity(clean_images, noise, timesteps)
|
|
model_output = model(noisy_images.to(dtype), timesteps.to(dtype), y=label)
|
|
else:
|
|
raise NotImplementedError()
|
|
if args.prediction_type == "epsilon":
|
|
loss = F.mse_loss(model_output.float(), noise.float())
|
|
elif args.prediction_type == "v_prediction":
|
|
loss = F.mse_loss(model_output.float(), velocity.float())
|
|
else:
|
|
raise NotImplementedError()
|
|
accelerator.backward(loss)
|
|
if accelerator.sync_gradients:
|
|
gnorm = accelerator.clip_grad_norm_(model.parameters(), 1.0)
|
|
optimizer.step()
|
|
lr_scheduler.step()
|
|
optimizer.zero_grad()
|
|
|
|
running_loss += loss.item()
|
|
if accelerator.sync_gradients:
|
|
global_step += 1
|
|
if args.use_ema:
|
|
ema_model.step(model.parameters())
|
|
if global_step % args.log_every == 0:
|
|
avg_loss = running_loss / args.log_every / args.gradient_accumulation_steps
|
|
running_loss = 0
|
|
logs = {"loss": avg_loss, "lr": lr_scheduler.get_last_lr()[0], "step": global_step, "gnorm": gnorm.item(), "batch size": total_batch_size, "epoch": epoch}
|
|
if args.use_ema:
|
|
logs["ema_decay"] = ema_model.cur_decay_value
|
|
logger.info(logs)
|
|
accelerator.log(logs, step=global_step)
|
|
if accelerator.is_main_process and args.wandb_project is not None:
|
|
wandb.log(logs, step=global_step)
|
|
|
|
if global_step % args.checkpointing_steps == 0:
|
|
def save_checkpoint(path):
|
|
accelerator.save_state(path)
|
|
if accelerator.is_main_process:
|
|
other_state = {
|
|
"scaling_factor": scaling_factor,
|
|
"bias_factor": bias_factor,
|
|
"steps": global_step,
|
|
"ema": ema_model.state_dict() if args.use_ema else None,
|
|
}
|
|
torch.save(other_state, os.path.join(path, "other_state.pth"))
|
|
save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
|
|
save_checkpoint(os.path.join(save_path))
|
|
if accelerator.is_main_process:
|
|
safe_blob_write(os.path.join(args.output_dir, "latest"), save_path)
|
|
logger.info(f"Saved state to {save_path}")
|
|
|
|
accelerator.end_training()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = parse_args()
|
|
main(args) |