Files
2026-07-13 13:24:13 +08:00

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)