1482 lines
67 KiB
Python
1482 lines
67 KiB
Python
# Copyright 2024 NVIDIA CORPORATION & AFFILIATES
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import datetime
|
|
import getpass
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import os.path as osp
|
|
import time
|
|
import types
|
|
import warnings
|
|
from copy import deepcopy
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pyrallis
|
|
import torch
|
|
import torch.nn as nn
|
|
import torch.nn.functional as F
|
|
from accelerate import Accelerator, InitProcessGroupKwargs, skip_first_batches
|
|
from accelerate.utils import DistributedType
|
|
from PIL import Image
|
|
from termcolor import colored
|
|
from tqdm import tqdm
|
|
|
|
warnings.filterwarnings("ignore") # ignore warning
|
|
os.environ["DISABLE_XFORMERS"] = "1"
|
|
|
|
|
|
from diffusion import SCMScheduler
|
|
from diffusion.data.builder import build_dataloader, build_dataset
|
|
from diffusion.data.wids import DistributedRangedSampler
|
|
from diffusion.model.builder import build_model, get_tokenizer_and_text_encoder, get_vae, vae_decode, vae_encode
|
|
from diffusion.model.model_growth_utils import ModelGrowthInitializer
|
|
from diffusion.model.nets.sana_ladd import DiscHeadModel, SanaMSCMDiscriminator
|
|
from diffusion.model.respace import compute_density_for_timestep_sampling
|
|
from diffusion.model.utils import get_weight_dtype
|
|
from diffusion.utils.checkpoint import load_checkpoint, save_checkpoint
|
|
from diffusion.utils.config import SanaConfig, model_init_config
|
|
from diffusion.utils.data_sampler import AspectRatioBatchSampler
|
|
from diffusion.utils.dist_utils import clip_grad_norm_, dist, flush, get_world_size
|
|
from diffusion.utils.logger import LogBuffer, get_root_logger
|
|
from diffusion.utils.lr_scheduler import build_lr_scheduler
|
|
from diffusion.utils.misc import DebugUnderflowOverflow, init_random_seed, set_random_seed
|
|
from diffusion.utils.optimizer import auto_scale_lr, build_optimizer
|
|
from tools.download import find_model
|
|
|
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
|
|
|
|
def set_fsdp_env():
|
|
os.environ["ACCELERATE_USE_FSDP"] = "true"
|
|
os.environ["FSDP_AUTO_WRAP_POLICY"] = "TRANSFORMER_BASED_WRAP"
|
|
os.environ["FSDP_BACKWARD_PREFETCH"] = "BACKWARD_PRE"
|
|
os.environ["FSDP_TRANSFORMER_CLS_TO_WRAP"] = "SanaBlock"
|
|
|
|
|
|
def ema_update(model_dest: nn.Module, model_src: nn.Module, rate):
|
|
param_dict_src = dict(model_src.named_parameters())
|
|
for p_name, p_dest in model_dest.named_parameters():
|
|
p_src = param_dict_src[p_name]
|
|
assert p_src is not p_dest
|
|
p_dest.data.mul_(rate).add_((1 - rate) * p_src.data)
|
|
|
|
|
|
@torch.inference_mode()
|
|
@torch.no_grad()
|
|
def log_validation(accelerator, config, model, logger, step, device, vae=None, init_noise=None, generator=None):
|
|
torch.cuda.empty_cache()
|
|
vis_sampler = config.scheduler.vis_sampler
|
|
model = accelerator.unwrap_model(model).eval()
|
|
hw = torch.tensor([[image_size, image_size]], dtype=torch.float, device=device).repeat(1, 1)
|
|
ar = torch.tensor([[1.0]], device=device).repeat(1, 1)
|
|
null_y = torch.load(null_embed_path, map_location="cpu")
|
|
null_y = null_y["uncond_prompt_embeds"].to(device)
|
|
sigma_data = config.scheduler.sigma_data
|
|
|
|
# Create sampling noise:
|
|
logger.info("Running validation... ")
|
|
image_logs = []
|
|
|
|
def run_sampling(init_z=None, label_suffix="", vae=None, sampler="dpm-solver"):
|
|
latent_outputs = []
|
|
current_image_logs = []
|
|
for prompt in validation_prompts:
|
|
latents = (
|
|
torch.randn(1, config.vae.vae_latent_dim, latent_size, latent_size, device=device)
|
|
if init_z is None
|
|
else init_z
|
|
) * sigma_data
|
|
embed = torch.load(
|
|
osp.join(config.train.valid_prompt_embed_root, f"{prompt[:50]}_{valid_prompt_embed_suffix}"),
|
|
map_location="cpu",
|
|
)
|
|
caption_embs, emb_masks = embed["caption_embeds"].to(device), embed["emb_mask"].to(device)
|
|
model_kwargs = dict(data_info={"img_hw": hw, "aspect_ratio": ar}, mask=emb_masks)
|
|
|
|
scheduler = SCMScheduler()
|
|
scheduler.set_timesteps(
|
|
num_inference_steps=2,
|
|
max_timesteps=1.57080,
|
|
intermediate_timesteps=1.0,
|
|
)
|
|
timesteps = scheduler.timesteps
|
|
|
|
model_kwargs["data_info"].update(
|
|
{"cfg_scale": torch.tensor([config.model.cfg_scale] * latents.shape[0]).to(device)}
|
|
)
|
|
|
|
# sCM MultiStep Sampling Loop:
|
|
for i, t in tqdm(list(enumerate(timesteps[:-1]))):
|
|
timestep = t.expand(latents.shape[0]).to(device)
|
|
|
|
# model prediction
|
|
model_pred = sigma_data * model(
|
|
latents / sigma_data,
|
|
timestep,
|
|
caption_embs,
|
|
**model_kwargs,
|
|
)
|
|
|
|
# compute the previous noisy sample x_t -> x_t-1
|
|
latents, denoised = scheduler.step(model_pred, i, t, latents, generator=generator, return_dict=False)
|
|
|
|
latent_outputs.append(denoised / sigma_data)
|
|
|
|
torch.cuda.empty_cache()
|
|
if vae is None:
|
|
vae = get_vae(config.vae.vae_type, config.vae.vae_pretrained, accelerator.device).to(vae_dtype)
|
|
for prompt, latent in zip(validation_prompts, latent_outputs):
|
|
latent = latent.to(vae_dtype)
|
|
samples = vae_decode(config.vae.vae_type, vae, latent)
|
|
samples = (
|
|
torch.clamp(127.5 * samples + 128.0, 0, 255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()[0]
|
|
)
|
|
image = Image.fromarray(samples)
|
|
current_image_logs.append({"validation_prompt": prompt + label_suffix, "images": [image]})
|
|
|
|
return current_image_logs
|
|
|
|
# First run with original noise
|
|
image_logs += run_sampling(init_z=None, label_suffix="", vae=vae, sampler=vis_sampler)
|
|
|
|
# Second run with init_noise if provided
|
|
if init_noise is not None:
|
|
init_noise = torch.clone(init_noise).to(device)
|
|
image_logs += run_sampling(init_z=init_noise, label_suffix=" w/ init noise", vae=vae, sampler=vis_sampler)
|
|
|
|
formatted_images = []
|
|
for log in image_logs:
|
|
images = log["images"]
|
|
validation_prompt = log["validation_prompt"]
|
|
for image in images:
|
|
formatted_images.append((validation_prompt, np.asarray(image)))
|
|
|
|
for tracker in accelerator.trackers:
|
|
if tracker.name == "tensorboard":
|
|
for validation_prompt, image in formatted_images:
|
|
tracker.writer.add_images(validation_prompt, image[None, ...], step, dataformats="NHWC")
|
|
elif tracker.name == "wandb":
|
|
import wandb
|
|
|
|
wandb_images = []
|
|
for validation_prompt, image in formatted_images:
|
|
wandb_images.append(wandb.Image(image, caption=validation_prompt, file_type="jpg"))
|
|
tracker.log({"validation": wandb_images})
|
|
else:
|
|
logger.warn(f"image logging not implemented for {tracker.name}")
|
|
|
|
def concatenate_images(image_caption, images_per_row=5, image_format="webp"):
|
|
import io
|
|
|
|
images = [log["images"][0] for log in image_caption]
|
|
if images[0].size[0] > 1024:
|
|
images = [image.resize((1024, 1024)) for image in images]
|
|
|
|
widths, heights = zip(*(img.size for img in images))
|
|
max_width = max(widths)
|
|
total_height = sum(heights[i : i + images_per_row][0] for i in range(0, len(images), images_per_row))
|
|
|
|
new_im = Image.new("RGB", (max_width * images_per_row, total_height))
|
|
|
|
y_offset = 0
|
|
for i in range(0, len(images), images_per_row):
|
|
row_images = images[i : i + images_per_row]
|
|
x_offset = 0
|
|
for img in row_images:
|
|
new_im.paste(img, (x_offset, y_offset))
|
|
x_offset += max_width
|
|
y_offset += heights[i]
|
|
webp_image_bytes = io.BytesIO()
|
|
new_im.save(webp_image_bytes, format=image_format)
|
|
webp_image_bytes.seek(0)
|
|
new_im = Image.open(webp_image_bytes)
|
|
|
|
return new_im
|
|
|
|
if config.train.local_save_vis:
|
|
file_format = "webp"
|
|
local_vis_save_path = osp.join(config.work_dir, "log_vis")
|
|
os.umask(0o000)
|
|
os.makedirs(local_vis_save_path, exist_ok=True)
|
|
concatenated_image = concatenate_images(image_logs, images_per_row=5, image_format=file_format)
|
|
save_path = (
|
|
osp.join(local_vis_save_path, f"vis_{step}.{file_format}")
|
|
if init_noise is None
|
|
else osp.join(local_vis_save_path, f"vis_{step}_w_init.{file_format}")
|
|
)
|
|
concatenated_image.save(save_path)
|
|
|
|
model.train()
|
|
del vae
|
|
flush()
|
|
return image_logs
|
|
|
|
|
|
def train(
|
|
config,
|
|
args,
|
|
accelerator,
|
|
model,
|
|
model_ema,
|
|
optimizer_G,
|
|
optimizer_D,
|
|
lr_scheduler,
|
|
train_dataloader,
|
|
logger,
|
|
pretrained_model,
|
|
disc,
|
|
):
|
|
if getattr(config.train, "debug_nan", False):
|
|
DebugUnderflowOverflow(model, max_frames_to_save=100)
|
|
logger.info("NaN debugger registered. Start to detect overflow during training.")
|
|
log_buffer = LogBuffer()
|
|
|
|
global_step = start_step + 1
|
|
skip_step = max(config.train.skip_step, global_step) % train_dataloader_len
|
|
skip_step = skip_step if skip_step < (train_dataloader_len - 20) else 0
|
|
loss_nan_timer = 0
|
|
|
|
# Cache Dataset for BatchSampler
|
|
if args.caching and config.model.multi_scale:
|
|
caching_start = time.time()
|
|
logger.info(
|
|
f"Start caching your dataset for batch_sampler at {cache_file}. \n"
|
|
f"This may take a lot of time...No training will launch"
|
|
)
|
|
train_dataloader.batch_sampler.sampler.set_start(max(train_dataloader.batch_sampler.exist_ids, 0))
|
|
for index, _ in enumerate(train_dataloader):
|
|
accelerator.wait_for_everyone()
|
|
if index % 2000 == 0:
|
|
logger.info(
|
|
f"rank: {rank}, Cached file len: {len(train_dataloader.batch_sampler.cached_idx)} / {len(train_dataloader)}"
|
|
)
|
|
print(
|
|
f"rank: {rank}, Cached file len: {len(train_dataloader.batch_sampler.cached_idx)} / {len(train_dataloader)}"
|
|
)
|
|
if (time.time() - caching_start) / 3600 > 3.7:
|
|
json.dump(train_dataloader.batch_sampler.cached_idx, open(cache_file, "w"), indent=4)
|
|
accelerator.wait_for_everyone()
|
|
break
|
|
if len(train_dataloader.batch_sampler.cached_idx) == len(train_dataloader) - 1000:
|
|
logger.info(
|
|
f"Saving rank: {rank}, Cached file len: {len(train_dataloader.batch_sampler.cached_idx)} / {len(train_dataloader)}"
|
|
)
|
|
json.dump(train_dataloader.batch_sampler.cached_idx, open(cache_file, "w"), indent=4)
|
|
continue
|
|
accelerator.wait_for_everyone()
|
|
print(f"Saving rank-{rank} Cached file len: {len(train_dataloader.batch_sampler.cached_idx)}")
|
|
json.dump(train_dataloader.batch_sampler.cached_idx, open(cache_file, "w"), indent=4)
|
|
return
|
|
|
|
phase = "G"
|
|
sigma_data = config.scheduler.sigma_data
|
|
uncond_y = pretrained_model.y_embedder.y_embedding.repeat(config.train.train_batch_size, 1, 1, 1)
|
|
# Now you train the model
|
|
g_step = 0
|
|
d_step = 0
|
|
|
|
for epoch in range(start_epoch + 1, config.train.num_epochs + 1):
|
|
time_start, last_tic = time.time(), time.time()
|
|
sampler = (
|
|
train_dataloader.batch_sampler.sampler
|
|
if (num_replicas > 1 or config.model.multi_scale)
|
|
else train_dataloader.sampler
|
|
)
|
|
sampler.set_epoch(epoch)
|
|
sampler.set_start(max((skip_step - 1) * config.train.train_batch_size, 0))
|
|
if skip_step > 1 and accelerator.is_main_process:
|
|
logger.info(f"Skipped Steps: {skip_step}")
|
|
skip_step = 1
|
|
data_time_start = time.time()
|
|
data_time_all = 0
|
|
lm_time_all = 0
|
|
vae_time_all = 0
|
|
model_time_all = 0
|
|
for step, batch in enumerate(train_dataloader):
|
|
# image, json_info, key = batch
|
|
data_time_all += time.time() - data_time_start
|
|
vae_time_start = time.time()
|
|
if load_vae_feat:
|
|
z = batch[0].to(accelerator.device)
|
|
else:
|
|
with torch.no_grad():
|
|
z = vae_encode(config.vae.vae_type, vae, batch[0], config.vae.sample_posterior, accelerator.device)
|
|
|
|
vae_time_all += time.time() - vae_time_start
|
|
|
|
clean_images = z * sigma_data
|
|
data_info = batch[3]
|
|
|
|
lm_time_start = time.time()
|
|
if load_text_feat:
|
|
y = batch[1] # bs, 1, N, C
|
|
y_mask = batch[2] # bs, 1, 1, N
|
|
else:
|
|
if "T5" in config.text_encoder.text_encoder_name:
|
|
with torch.no_grad():
|
|
txt_tokens = tokenizer(
|
|
batch[1], max_length=max_length, padding="max_length", truncation=True, return_tensors="pt"
|
|
).to(accelerator.device)
|
|
y = text_encoder(txt_tokens.input_ids, attention_mask=txt_tokens.attention_mask)[0][:, None]
|
|
y_mask = txt_tokens.attention_mask[:, None, None]
|
|
elif (
|
|
"gemma" in config.text_encoder.text_encoder_name or "Qwen" in config.text_encoder.text_encoder_name
|
|
):
|
|
with torch.no_grad():
|
|
if not config.text_encoder.chi_prompt:
|
|
max_length_all = config.text_encoder.model_max_length
|
|
prompt = batch[1]
|
|
else:
|
|
chi_prompt = "\n".join(config.text_encoder.chi_prompt)
|
|
prompt = [chi_prompt + i for i in batch[1]]
|
|
num_chi_prompt_tokens = len(tokenizer.encode(chi_prompt))
|
|
max_length_all = (
|
|
num_chi_prompt_tokens + config.text_encoder.model_max_length - 2
|
|
) # magic number 2: [bos], [_]
|
|
txt_tokens = tokenizer(
|
|
prompt,
|
|
padding="max_length",
|
|
max_length=max_length_all,
|
|
truncation=True,
|
|
return_tensors="pt",
|
|
).to(accelerator.device)
|
|
select_index = [0] + list(
|
|
range(-config.text_encoder.model_max_length + 1, 0)
|
|
) # first bos and end N-1
|
|
y = text_encoder(txt_tokens.input_ids, attention_mask=txt_tokens.attention_mask)[0][:, None][
|
|
:, :, select_index
|
|
]
|
|
y_mask = txt_tokens.attention_mask[:, None, None][:, :, :, select_index]
|
|
else:
|
|
print("error")
|
|
exit()
|
|
|
|
# Sample a random timestep for each image
|
|
bs = clean_images.shape[0]
|
|
|
|
def get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme,
|
|
logit_mean=config.scheduler.logit_mean,
|
|
logit_std=config.scheduler.logit_std,
|
|
):
|
|
if weighting_scheme == "logit_normal_trigflow":
|
|
u = compute_density_for_timestep_sampling(
|
|
weighting_scheme=weighting_scheme,
|
|
batch_size=bs,
|
|
logit_mean=logit_mean,
|
|
logit_std=logit_std,
|
|
mode_scale=None,
|
|
)
|
|
denoise_timesteps = None
|
|
elif weighting_scheme == "logit_normal_trigflow_ladd":
|
|
indices = torch.randint(0, len(config.scheduler.add_noise_timesteps), (bs,))
|
|
u = torch.tensor([config.scheduler.add_noise_timesteps[i] for i in indices])
|
|
if len(config.scheduler.add_noise_timesteps) == 1:
|
|
# zero-SNR
|
|
denoise_timesteps = torch.tensor([1.57080 for i in indices]).float().to(clean_images.device)
|
|
else:
|
|
denoise_timesteps = u.float().to(clean_images.device)
|
|
|
|
return u.float().to(clean_images.device), denoise_timesteps
|
|
|
|
timesteps, denoise_timesteps = get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme,
|
|
logit_mean=config.scheduler.logit_mean,
|
|
logit_std=config.scheduler.logit_std,
|
|
)
|
|
|
|
grad_norm = None
|
|
lm_time_all += time.time() - lm_time_start
|
|
model_time_start = time.time()
|
|
|
|
# get images and timesteps
|
|
x0 = clean_images
|
|
t = timesteps.view(-1, 1, 1, 1)
|
|
t_G = denoise_timesteps.view(-1, 1, 1, 1) if denoise_timesteps is not None else t
|
|
|
|
z = torch.randn_like(x0) * sigma_data
|
|
x_t = torch.cos(t) * x0 + torch.sin(t) * z
|
|
|
|
model_kwargs = dict(y=y, mask=y_mask, data_info=data_info)
|
|
|
|
if config.model.cfg_embed:
|
|
config.train.scm_cfg_scale = (
|
|
config.train.scm_cfg_scale
|
|
if isinstance(config.train.scm_cfg_scale, list)
|
|
else [config.train.scm_cfg_scale]
|
|
)
|
|
# sample cfg scales
|
|
scm_cfg_scale = torch.tensor(
|
|
np.random.choice(config.train.scm_cfg_scale, size=bs, replace=True),
|
|
device=x_t.device,
|
|
)
|
|
data_info["cfg_scale"] = scm_cfg_scale
|
|
|
|
def model_wrapper(scaled_x_t, t):
|
|
pred, logvar = accelerator.unwrap_model(model)(
|
|
scaled_x_t, t.flatten(), y=y, mask=y_mask, data_info=data_info, return_logvar=True, jvp=True
|
|
)
|
|
return pred, logvar
|
|
|
|
if g_step % config.train.gradient_accumulation_steps == 0:
|
|
optimizer_G.zero_grad()
|
|
|
|
if phase == "G":
|
|
disc.eval()
|
|
model.train()
|
|
|
|
if config.train.scm_loss:
|
|
with torch.no_grad():
|
|
if config.train.scm_cfg_scale[0] > 1 and config.model.cfg_embed:
|
|
cfg_x_t = torch.cat([x_t, x_t], dim=0)
|
|
cfg_t = torch.cat([t, t], dim=0)
|
|
cfg_y = torch.cat([uncond_y, y], dim=0)
|
|
cfg_y_mask = torch.cat([y_mask, y_mask], dim=0)
|
|
|
|
cfg_model_kwargs = dict(y=cfg_y, mask=cfg_y_mask)
|
|
|
|
cfg_pretrain_pred = pretrained_model(
|
|
cfg_x_t / sigma_data, cfg_t.flatten(), **cfg_model_kwargs
|
|
)
|
|
cfg_dxt_dt = sigma_data * cfg_pretrain_pred
|
|
|
|
dxt_dt_uncond, dxt_dt = cfg_dxt_dt.chunk(2)
|
|
|
|
scm_cfg_scale = scm_cfg_scale.view(-1, 1, 1, 1)
|
|
dxt_dt = dxt_dt_uncond + scm_cfg_scale * (dxt_dt - dxt_dt_uncond)
|
|
else:
|
|
pretrain_pred = pretrained_model(x_t / sigma_data, t.flatten(), **model_kwargs)
|
|
dxt_dt = sigma_data * pretrain_pred
|
|
|
|
v_x = torch.cos(t) * torch.sin(t) * dxt_dt / sigma_data
|
|
v_t = torch.cos(t) * torch.sin(t)
|
|
|
|
# Adapt from https://github.com/xandergos/sCM-mnist/blob/master/train_consistency.py
|
|
with torch.no_grad():
|
|
F_theta, F_theta_grad, logvar = torch.func.jvp(
|
|
model_wrapper, (x_t / sigma_data, t), (v_x, v_t), has_aux=True
|
|
)
|
|
|
|
F_theta, logvar = model(
|
|
x_t / sigma_data,
|
|
t.flatten(),
|
|
y=y,
|
|
mask=y_mask,
|
|
data_info=data_info,
|
|
return_logvar=True,
|
|
jvp=False,
|
|
)
|
|
|
|
logvar = logvar.view(-1, 1, 1, 1)
|
|
F_theta_grad = F_theta_grad.detach()
|
|
F_theta_minus = F_theta.detach()
|
|
|
|
# Warmup steps
|
|
r = min(1, global_step / config.train.tangent_warmup_steps)
|
|
|
|
# Calculate gradient g using JVP rearrangement
|
|
g = -torch.cos(t) * torch.cos(t) * (sigma_data * F_theta_minus - dxt_dt)
|
|
second_term = -r * (torch.cos(t) * torch.sin(t) * x_t + sigma_data * F_theta_grad)
|
|
g = g + second_term
|
|
|
|
# Tangent normalization
|
|
g_norm = torch.linalg.vector_norm(g, dim=(1, 2, 3), keepdim=True)
|
|
g = g / (g_norm + 0.1) # 0.1 is the constant c, can be modified but 0.1 was used in the paper
|
|
|
|
sigma = torch.tan(t) * sigma_data
|
|
weight = 1 / sigma
|
|
|
|
l2_loss = torch.square(F_theta - F_theta_minus - g)
|
|
|
|
# Calculate loss with normalization factor
|
|
loss = (weight / torch.exp(logvar)) * l2_loss + logvar
|
|
|
|
loss = loss.mean()
|
|
|
|
loss_no_logvar = weight * torch.square(F_theta - F_theta_minus - g)
|
|
loss_no_logvar = loss_no_logvar.mean()
|
|
loss_no_weight = l2_loss.mean()
|
|
g_norm = g_norm.mean()
|
|
|
|
else:
|
|
F_theta = model(
|
|
x_t / sigma_data,
|
|
t_G.flatten(),
|
|
y=y,
|
|
mask=y_mask,
|
|
data_info=data_info,
|
|
return_logvar=False,
|
|
jvp=False,
|
|
)
|
|
|
|
pred_x_0 = torch.cos(t_G) * x_t - torch.sin(t_G) * F_theta * sigma_data
|
|
|
|
if config.train.train_largest_timestep:
|
|
pred_x_0.detach()
|
|
timesteps, denoise_timesteps = get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme,
|
|
logit_mean=config.scheduler.logit_mean,
|
|
logit_std=config.scheduler.logit_std,
|
|
)
|
|
t_new = timesteps.view(-1, 1, 1, 1)
|
|
|
|
random_mask = torch.rand_like(t_new) < config.train.largest_timestep_prob
|
|
|
|
t_new = torch.where(random_mask, torch.full_like(t_new, config.train.largest_timestep), t_new)
|
|
z_new = torch.randn_like(x0) * sigma_data
|
|
x_t_new = torch.cos(t_new) * x0 + torch.sin(t_new) * z_new
|
|
|
|
F_theta = model(
|
|
x_t_new / sigma_data,
|
|
t_new.flatten(),
|
|
y=y,
|
|
mask=y_mask,
|
|
data_info=data_info,
|
|
return_logvar=False,
|
|
jvp=False,
|
|
)
|
|
|
|
pred_x_0 = torch.cos(t_new) * x_t_new - torch.sin(t_new) * F_theta * sigma_data
|
|
|
|
# Sample timesteps for discriminator
|
|
timesteps_D, _ = get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme_discriminator,
|
|
logit_mean=config.scheduler.logit_mean_discriminator,
|
|
logit_std=config.scheduler.logit_std_discriminator,
|
|
)
|
|
t_D = timesteps_D.view(-1, 1, 1, 1)
|
|
|
|
# Add noise to predicted x0
|
|
z_D = torch.randn_like(x0) * sigma_data
|
|
noised_predicted_x0 = torch.cos(t_D) * pred_x_0 + torch.sin(t_D) * z_D
|
|
|
|
# Calculate adversarial loss
|
|
pred_fake = disc(noised_predicted_x0 / sigma_data, t_D.flatten(), **model_kwargs)
|
|
if config.train.discriminator_loss == "cross entropy":
|
|
adv_loss = F.binary_cross_entropy_with_logits(pred_fake, torch.ones_like(pred_fake))
|
|
elif config.train.discriminator_loss == "hinge":
|
|
adv_loss = -torch.mean(pred_fake)
|
|
else:
|
|
raise ValueError(f"Invalid adversarial loss type: {config.train.discriminator_loss}")
|
|
|
|
# Total loss = sCM loss / reconstruct loss + LADD loss
|
|
if config.train.scm_loss:
|
|
total_loss = config.train.scm_lambda * loss + adv_loss * config.train.adv_lambda
|
|
elif config.train.reconstruct_loss:
|
|
total_loss = loss + adv_loss * config.train.adv_lambda
|
|
else:
|
|
total_loss = adv_loss
|
|
|
|
total_loss = total_loss / config.train.gradient_accumulation_steps
|
|
|
|
accelerator.backward(total_loss)
|
|
|
|
g_step += 1
|
|
|
|
if g_step % config.train.gradient_accumulation_steps == 0:
|
|
if accelerator.sync_gradients:
|
|
grad_norm = accelerator.clip_grad_norm_(model.parameters(), config.train.gradient_clip)
|
|
if torch.logical_or(grad_norm.isnan(), grad_norm.isinf()):
|
|
optimizer_G.zero_grad(set_to_none=True)
|
|
optimizer_D.zero_grad(set_to_none=True)
|
|
logger.warning("NaN or Inf detected in grad_norm, skipping iteration...")
|
|
continue
|
|
|
|
# switch phase to D
|
|
phase = "D"
|
|
|
|
optimizer_G.step()
|
|
lr_scheduler.step()
|
|
optimizer_G.zero_grad(set_to_none=True)
|
|
|
|
elif phase == "D":
|
|
if d_step % config.train.gradient_accumulation_steps == 0:
|
|
optimizer_D.zero_grad()
|
|
|
|
disc.train()
|
|
model.eval()
|
|
|
|
with torch.no_grad():
|
|
scm_cfg_scale = torch.tensor(
|
|
np.random.choice(config.train.scm_cfg_scale, size=bs, replace=True), device=x_t.device
|
|
)
|
|
data_info["cfg_scale"] = scm_cfg_scale
|
|
|
|
if config.train.train_largest_timestep:
|
|
random_mask = torch.rand_like(t_G) < config.train.largest_timestep_prob
|
|
t_G = torch.where(random_mask, torch.full_like(t_G, config.train.largest_timestep), t_G)
|
|
|
|
z_new = torch.randn_like(x0) * sigma_data
|
|
x_t = torch.cos(t_G) * x0 + torch.sin(t_G) * z_new
|
|
|
|
F_theta = model(
|
|
x_t / sigma_data,
|
|
t_G.flatten(),
|
|
y=y,
|
|
mask=y_mask,
|
|
data_info=data_info,
|
|
return_logvar=False,
|
|
)
|
|
pred_x_0 = torch.cos(t_G) * x_t - torch.sin(t_G) * F_theta * sigma_data
|
|
|
|
# Sample timesteps for fake and real samples
|
|
timestep_D_fake, _ = get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme_discriminator,
|
|
logit_mean=config.scheduler.logit_mean_discriminator,
|
|
logit_std=config.scheduler.logit_std_discriminator,
|
|
)
|
|
if config.train.diff_timesteps_D:
|
|
timesteps_D_real, _ = get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme_discriminator,
|
|
logit_mean=config.scheduler.logit_mean_discriminator,
|
|
logit_std=config.scheduler.logit_std_discriminator,
|
|
)
|
|
else:
|
|
timesteps_D_real = timestep_D_fake
|
|
|
|
t_D_fake = timestep_D_fake.view(-1, 1, 1, 1)
|
|
t_D_real = timesteps_D_real.view(-1, 1, 1, 1)
|
|
|
|
# Add noise to predicted x0 and real x0
|
|
z_D_fake = torch.randn_like(x0) * sigma_data
|
|
z_D_real = torch.randn_like(x0) * sigma_data
|
|
noised_predicted_x0 = torch.cos(t_D_fake) * pred_x_0 + torch.sin(t_D_fake) * z_D_fake
|
|
noised_latents = torch.cos(t_D_real) * x0 + torch.sin(t_D_real) * z_D_real
|
|
|
|
# Add misaligned pairs if enabled and batch size > 1
|
|
if config.train.misaligned_pairs_D and bs > 1:
|
|
# Create shifted pairs
|
|
shifted_x0 = torch.roll(x0, 1, 0)
|
|
timesteps_D_shifted, _ = get_timesteps(
|
|
weighting_scheme=config.scheduler.weighting_scheme_discriminator,
|
|
logit_mean=config.scheduler.logit_mean_discriminator,
|
|
logit_std=config.scheduler.logit_std_discriminator,
|
|
)
|
|
t_D_shifted = timesteps_D_shifted.view(-1, 1, 1, 1)
|
|
|
|
# Add noise to shifted pairs
|
|
z_D_shifted = torch.randn_like(shifted_x0) * sigma_data
|
|
noised_shifted_x0 = torch.cos(t_D_shifted) * shifted_x0 + torch.sin(t_D_shifted) * z_D_shifted
|
|
|
|
# Concatenate with original noised samples
|
|
noised_predicted_x0 = torch.cat([noised_predicted_x0, noised_shifted_x0], dim=0)
|
|
t_D_fake = torch.cat([t_D_fake, t_D_shifted], dim=0)
|
|
y = torch.cat([y, y], dim=0)
|
|
y_mask = torch.cat([y_mask, y_mask], dim=0)
|
|
fake_kwargs = {**model_kwargs, "y": y, "mask": y_mask}
|
|
else:
|
|
fake_kwargs = model_kwargs
|
|
|
|
# Calculate D loss
|
|
pred_fake = disc(noised_predicted_x0 / sigma_data, t_D_fake.flatten(), **fake_kwargs)
|
|
pred_true = disc(noised_latents / sigma_data, t_D_real.flatten(), **model_kwargs)
|
|
|
|
# cross entropy loss
|
|
if config.train.discriminator_loss == "cross entropy":
|
|
loss_gen = F.binary_cross_entropy_with_logits(pred_fake, torch.zeros_like(pred_fake))
|
|
loss_real = F.binary_cross_entropy_with_logits(pred_true, torch.ones_like(pred_true))
|
|
loss_D = loss_gen + loss_real
|
|
# hinge loss
|
|
elif config.train.discriminator_loss == "hinge":
|
|
loss_real = torch.mean(F.relu(1.0 - pred_true))
|
|
loss_gen = torch.mean(F.relu(1.0 + pred_fake))
|
|
loss_D = 0.5 * (loss_real + loss_gen)
|
|
else:
|
|
raise ValueError(f"Invalid discriminator loss type: {config.train.discriminator_loss}")
|
|
|
|
def calculate_gradient_penalty(discriminator):
|
|
from torch.utils.checkpoint import checkpoint
|
|
|
|
head_inputs = discriminator.get_head_inputs()
|
|
bs = head_inputs[0].shape[0]
|
|
|
|
grad_penalty = 0.0
|
|
|
|
for i, head_input in enumerate(head_inputs):
|
|
head_input = torch.autograd.Variable(head_input, requires_grad=True)
|
|
|
|
def forward_head(head_input):
|
|
return discriminator.heads[i](head_input, None)
|
|
|
|
discriminator_logits = checkpoint(forward_head, head_input, use_reentrant=False)
|
|
|
|
gradients = torch.autograd.grad(
|
|
outputs=discriminator_logits,
|
|
inputs=head_input,
|
|
grad_outputs=torch.ones(discriminator_logits.size()).to(head_input.device),
|
|
create_graph=True,
|
|
retain_graph=True,
|
|
)[0]
|
|
|
|
gradients = gradients.reshape(bs, -1)
|
|
grad_penalty += gradients.norm(2, dim=1) ** 2
|
|
|
|
grad_penalty = grad_penalty.mean() / len(head_inputs)
|
|
|
|
return grad_penalty
|
|
|
|
if config.train.r1_penalty:
|
|
r1_penalty = calculate_gradient_penalty(
|
|
accelerator.unwrap_model(disc),
|
|
)
|
|
loss_D = loss_D + config.train.r1_penalty_weight * r1_penalty
|
|
|
|
loss_D = loss_D / config.train.gradient_accumulation_steps
|
|
|
|
accelerator.backward(loss_D)
|
|
|
|
d_step += 1
|
|
|
|
if d_step % config.train.gradient_accumulation_steps == 0:
|
|
if accelerator.sync_gradients:
|
|
grad_norm = accelerator.clip_grad_norm_(disc.parameters(), config.train.gradient_clip)
|
|
if torch.logical_or(grad_norm.isnan(), grad_norm.isinf()):
|
|
optimizer_G.zero_grad(set_to_none=True)
|
|
optimizer_D.zero_grad(set_to_none=True)
|
|
logger.warning("NaN or Inf detected in grad_norm, skipping iteration...")
|
|
continue
|
|
|
|
# switch back to phase G and add global step by one.
|
|
phase = "G"
|
|
|
|
optimizer_D.step()
|
|
optimizer_D.zero_grad(set_to_none=True)
|
|
|
|
model_time_all += time.time() - model_time_start
|
|
|
|
# update log information
|
|
if (phase == "G" and g_step % config.train.gradient_accumulation_steps == 0) or (
|
|
phase == "D" and d_step % config.train.gradient_accumulation_steps == 0
|
|
):
|
|
lr = lr_scheduler.get_last_lr()[0]
|
|
logs = {}
|
|
if config.train.scm_loss:
|
|
logs.update({args.loss_report_name: accelerator.gather(loss).mean().item()})
|
|
logs.update({"loss_no_logvar": accelerator.gather(loss_no_logvar).mean().item()})
|
|
logs.update({"loss_no_weight": accelerator.gather(loss_no_weight).mean().item()})
|
|
logs.update({"g_norm": accelerator.gather(g_norm).mean().item()})
|
|
if phase == "D": # since we already change the phase to D, but the current step is still in G.
|
|
logs.update({"total_loss": accelerator.gather(total_loss).mean().item()})
|
|
logs.update({"adv_loss": accelerator.gather(adv_loss).mean().item()})
|
|
else:
|
|
logs.update(
|
|
{
|
|
"D_loss": accelerator.gather(loss_D).mean().item(),
|
|
"loss_gen": accelerator.gather(loss_gen).mean().item(),
|
|
"loss_real": accelerator.gather(loss_real).mean().item(),
|
|
}
|
|
)
|
|
if config.train.r1_penalty:
|
|
logs.update({"r1_penalty": accelerator.gather(r1_penalty).mean().item()})
|
|
if grad_norm is not None:
|
|
logs.update(grad_norm=accelerator.gather(grad_norm).mean().item())
|
|
log_buffer.update(logs)
|
|
if (step + 1) % config.train.log_interval == 0 or (step + 1) == 1:
|
|
accelerator.wait_for_everyone()
|
|
t = (time.time() - last_tic) / config.train.log_interval
|
|
t_d = data_time_all / config.train.log_interval
|
|
t_m = model_time_all / config.train.log_interval
|
|
t_lm = lm_time_all / config.train.log_interval
|
|
t_vae = vae_time_all / config.train.log_interval
|
|
avg_time = (time.time() - time_start) / (step + 1)
|
|
eta = str(datetime.timedelta(seconds=int(avg_time * (total_steps - global_step - 1))))
|
|
eta_epoch = str(
|
|
datetime.timedelta(
|
|
seconds=int(
|
|
avg_time
|
|
* (
|
|
train_dataloader_len
|
|
- sampler.step_start // config.train.train_batch_size
|
|
- step
|
|
- 1
|
|
)
|
|
)
|
|
)
|
|
)
|
|
log_buffer.average()
|
|
|
|
current_step = (
|
|
global_step - sampler.step_start // config.train.train_batch_size
|
|
) % train_dataloader_len
|
|
current_step = train_dataloader_len if current_step == 0 else current_step
|
|
info = (
|
|
f"Epoch: {epoch} | Global Step: {global_step} | Local Step: {current_step} // {train_dataloader_len}, "
|
|
f"total_eta: {eta}, epoch_eta:{eta_epoch}, time: all:{t:.3f}, model:{t_m:.3f}, data:{t_d:.3f}, "
|
|
f"lm:{t_lm:.3f}, vae:{t_vae:.3f}, lr:{lr:.3e}, Cap: {batch[5][0]}, "
|
|
)
|
|
info += (
|
|
f"s:({model.module.h}, {model.module.w}), "
|
|
if hasattr(model, "module")
|
|
else f"s:({model.h}, {model.w}), "
|
|
)
|
|
info += f"phase: {phase}, "
|
|
|
|
info += ", ".join([f"{k}:{v:.4f}" for k, v in log_buffer.output.items()])
|
|
last_tic = time.time()
|
|
log_buffer.clear()
|
|
data_time_all = 0
|
|
model_time_all = 0
|
|
lm_time_all = 0
|
|
vae_time_all = 0
|
|
if accelerator.is_main_process:
|
|
logger.info(info)
|
|
|
|
logs.update(lr=lr)
|
|
if accelerator.is_main_process:
|
|
accelerator.log(logs, step=global_step)
|
|
|
|
global_step += 1
|
|
if loss_nan_timer > 20:
|
|
raise ValueError("Loss is NaN too much times. Break here.")
|
|
if (
|
|
global_step % config.train.save_model_steps == 0
|
|
or (time.time() - training_start_time) / 3600 > config.train.early_stop_hours
|
|
):
|
|
if accelerator.is_main_process:
|
|
os.umask(0o000)
|
|
ckpt_saved_path = save_checkpoint(
|
|
osp.join(config.work_dir, "checkpoints"),
|
|
epoch=epoch,
|
|
step=global_step,
|
|
model=accelerator.unwrap_model(model),
|
|
optimizer=optimizer_G,
|
|
lr_scheduler=lr_scheduler,
|
|
generator=generator,
|
|
add_symlink=True,
|
|
)
|
|
|
|
save_checkpoint(
|
|
osp.join(config.work_dir, "checkpoints"),
|
|
epoch=epoch,
|
|
model=DiscHeadModel(accelerator.unwrap_model(disc)),
|
|
optimizer=optimizer_D,
|
|
step=global_step,
|
|
add_suffix=config.train.suffix_checkpoints,
|
|
)
|
|
if config.train.online_metric and global_step % config.train.eval_metric_step == 0 and step > 1:
|
|
online_metric_monitor_dir = osp.join(config.work_dir, config.train.online_metric_dir)
|
|
os.makedirs(online_metric_monitor_dir, exist_ok=True)
|
|
with open(f"{online_metric_monitor_dir}/{ckpt_saved_path.split('/')[-1]}.txt", "w") as f:
|
|
f.write(osp.join(config.work_dir, "config.py") + "\n")
|
|
f.write(ckpt_saved_path)
|
|
|
|
if (time.time() - training_start_time) / 3600 > config.train.early_stop_hours:
|
|
logger.info(f"Stopping training at epoch {epoch}, step {global_step} due to time limit.")
|
|
return
|
|
if config.train.visualize and (global_step % config.train.eval_sampling_steps == 0 or (step + 1) == 1):
|
|
if accelerator.is_main_process:
|
|
if validation_noise is not None:
|
|
log_validation(
|
|
accelerator=accelerator,
|
|
config=config,
|
|
model=model,
|
|
logger=logger,
|
|
step=global_step,
|
|
device=accelerator.device,
|
|
vae=vae,
|
|
init_noise=validation_noise,
|
|
generator=torch.Generator(device="cuda").manual_seed(0),
|
|
)
|
|
else:
|
|
log_validation(
|
|
accelerator=accelerator,
|
|
config=config,
|
|
model=model,
|
|
logger=logger,
|
|
step=global_step,
|
|
device=accelerator.device,
|
|
vae=vae,
|
|
)
|
|
|
|
# avoid dead-lock of multiscale data batch sampler
|
|
# for internal, refactor dataloader logic to remove the ad-hoc implementation
|
|
if (
|
|
config.model.multi_scale
|
|
and (train_dataloader_len - sampler.step_start // config.train.train_batch_size - step) < 30
|
|
):
|
|
# global_step = epoch * train_dataloader_len
|
|
global_step = (
|
|
(global_step + train_dataloader_len - 1) // train_dataloader_len
|
|
) * train_dataloader_len + 1
|
|
logger.info("Early stop current iteration")
|
|
skip_first_batches(train_dataloader, True)
|
|
break
|
|
|
|
data_time_start = time.time()
|
|
|
|
if epoch % config.train.save_model_epochs == 0 or epoch == config.train.num_epochs and not config.debug:
|
|
accelerator.wait_for_everyone()
|
|
if accelerator.is_main_process:
|
|
# os.umask(0o000)
|
|
ckpt_saved_path = save_checkpoint(
|
|
osp.join(config.work_dir, "checkpoints"),
|
|
epoch=epoch,
|
|
step=global_step,
|
|
model=accelerator.unwrap_model(model),
|
|
optimizer=optimizer_G,
|
|
lr_scheduler=lr_scheduler,
|
|
generator=generator,
|
|
add_symlink=True,
|
|
)
|
|
|
|
online_metric_monitor_dir = osp.join(config.work_dir, config.train.online_metric_dir)
|
|
os.makedirs(online_metric_monitor_dir, exist_ok=True)
|
|
with open(f"{online_metric_monitor_dir}/{ckpt_saved_path.split('/')[-1]}.txt", "w") as f:
|
|
f.write(osp.join(config.work_dir, "config.py") + "\n")
|
|
f.write(ckpt_saved_path)
|
|
|
|
save_checkpoint(
|
|
osp.join(config.work_dir, "checkpoints"),
|
|
epoch=epoch,
|
|
model=DiscHeadModel(accelerator.unwrap_model(disc)),
|
|
optimizer=optimizer_D,
|
|
step=global_step,
|
|
add_suffix=config.train.suffix_checkpoints,
|
|
)
|
|
|
|
|
|
@pyrallis.wrap()
|
|
def main(cfg: SanaConfig) -> None:
|
|
global train_dataloader_len, start_epoch, start_step, vae, generator, num_replicas, rank, training_start_time
|
|
global load_vae_feat, load_text_feat, validation_noise, text_encoder, tokenizer, model_weight_dtype
|
|
global max_length, validation_prompts, latent_size, valid_prompt_embed_suffix, null_embed_path
|
|
global image_size, cache_file, total_steps, vae_dtype
|
|
|
|
config = cfg
|
|
args = cfg
|
|
|
|
training_start_time = time.time()
|
|
load_from = True
|
|
if args.resume_from or config.model.resume_from:
|
|
load_from = False
|
|
config.model.resume_from = dict(
|
|
checkpoint=args.resume_from or config.model.resume_from,
|
|
load_ema=False,
|
|
resume_optimizer=True,
|
|
resume_lr_scheduler=config.train.resume_lr_scheduler,
|
|
)
|
|
|
|
if args.debug:
|
|
config.train.log_interval = 1
|
|
config.train.train_batch_size = min(64, config.train.train_batch_size)
|
|
args.report_to = "tensorboard"
|
|
|
|
os.umask(0o000)
|
|
os.makedirs(config.work_dir, exist_ok=True)
|
|
|
|
init_handler = InitProcessGroupKwargs()
|
|
init_handler.timeout = datetime.timedelta(seconds=5400) # change timeout to avoid a strange NCCL bug
|
|
# Initialize accelerator and tensorboard logging
|
|
if config.train.use_fsdp:
|
|
init_train = "FSDP"
|
|
from accelerate import FullyShardedDataParallelPlugin
|
|
from torch.distributed.fsdp.fully_sharded_data_parallel import FullStateDictConfig
|
|
|
|
set_fsdp_env()
|
|
fsdp_plugin = FullyShardedDataParallelPlugin(
|
|
state_dict_config=FullStateDictConfig(offload_to_cpu=False, rank0_only=False),
|
|
)
|
|
else:
|
|
init_train = "DDP"
|
|
fsdp_plugin = None
|
|
|
|
accelerator = Accelerator(
|
|
mixed_precision=config.model.mixed_precision,
|
|
gradient_accumulation_steps=config.train.gradient_accumulation_steps,
|
|
log_with=args.report_to,
|
|
project_dir=osp.join(config.work_dir, "logs"),
|
|
fsdp_plugin=fsdp_plugin,
|
|
kwargs_handlers=[init_handler],
|
|
)
|
|
|
|
log_name = "train_log.log"
|
|
logger = get_root_logger(osp.join(config.work_dir, log_name))
|
|
logger.info(accelerator.state)
|
|
|
|
config.train.seed = init_random_seed(getattr(config.train, "seed", None))
|
|
set_random_seed(config.train.seed + int(os.environ["LOCAL_RANK"]))
|
|
generator = torch.Generator(device="cpu").manual_seed(config.train.seed)
|
|
|
|
if accelerator.is_main_process:
|
|
pyrallis.dump(config, open(osp.join(config.work_dir, "config.yaml"), "w"), sort_keys=False, indent=4)
|
|
if args.report_to == "wandb":
|
|
import wandb
|
|
|
|
wandb.init(project=args.tracker_project_name, name=args.name, resume="allow", id=args.name)
|
|
|
|
logger.info(f"Config: \n{config}")
|
|
logger.info(f"World_size: {get_world_size()}, seed: {config.train.seed}")
|
|
logger.info(f"Initializing: {init_train} for training")
|
|
cluster = os.environ.get("CLUSTER", "cs")
|
|
if cluster == "cs":
|
|
config.train.early_stop_hours = 3.9
|
|
elif cluster == "nrt":
|
|
config.train.early_stop_hours = 1.9
|
|
image_size = config.model.image_size
|
|
latent_size = int(image_size) // config.vae.vae_downsample_rate
|
|
max_length = config.text_encoder.model_max_length
|
|
model_weight_dtype = get_weight_dtype(config.model.mixed_precision)
|
|
vae = None
|
|
vae_dtype = get_weight_dtype(config.vae.weight_dtype)
|
|
validation_noise = (
|
|
torch.randn(
|
|
1,
|
|
config.vae.vae_latent_dim,
|
|
latent_size,
|
|
latent_size,
|
|
device="cpu",
|
|
generator=torch.Generator(device="cpu").manual_seed(0),
|
|
)
|
|
if getattr(config.train, "deterministic_validation", False)
|
|
else None
|
|
)
|
|
if not config.data.load_vae_feat:
|
|
vae = get_vae(config.vae.vae_type, config.vae.vae_pretrained, accelerator.device).to(vae_dtype)
|
|
tokenizer = text_encoder = None
|
|
if not config.data.load_text_feat:
|
|
tokenizer, text_encoder = get_tokenizer_and_text_encoder(
|
|
name=config.text_encoder.text_encoder_name, device=accelerator.device
|
|
)
|
|
text_embed_dim = text_encoder.config.hidden_size
|
|
else:
|
|
text_embed_dim = config.text_encoder.caption_channels
|
|
config.text_encoder.caption_channels = text_embed_dim
|
|
|
|
logger.info(f"vae type: {config.vae.vae_type}, path: {config.vae.vae_pretrained}, weight_dtype: {vae_dtype}")
|
|
if config.text_encoder.chi_prompt:
|
|
chi_prompt = "\n".join(config.text_encoder.chi_prompt)
|
|
logger.info(f"Complex Human Instruct: {chi_prompt}")
|
|
|
|
os.makedirs(config.train.null_embed_root, exist_ok=True)
|
|
null_embed_path = osp.join(
|
|
config.train.null_embed_root,
|
|
f"null_embed_diffusers_{config.text_encoder.text_encoder_name}_{max_length}token_{text_embed_dim}.pth",
|
|
)
|
|
if config.train.visualize and len(config.train.validation_prompts):
|
|
# preparing embeddings for visualization. We put it here for saving GPU memory
|
|
valid_prompt_embed_suffix = f"{max_length}token_{config.text_encoder.text_encoder_name}_{text_embed_dim}.pth"
|
|
validation_prompts = config.train.validation_prompts
|
|
skip = True
|
|
if config.text_encoder.chi_prompt:
|
|
uuid_chi_prompt = hashlib.sha256(chi_prompt.encode()).hexdigest()
|
|
else:
|
|
uuid_chi_prompt = hashlib.sha256(b"").hexdigest()
|
|
config.train.valid_prompt_embed_root = osp.join(config.train.valid_prompt_embed_root, uuid_chi_prompt)
|
|
Path(config.train.valid_prompt_embed_root).mkdir(parents=True, exist_ok=True)
|
|
|
|
if config.text_encoder.chi_prompt:
|
|
# Save complex human instruct to a file
|
|
chi_prompt_file = osp.join(config.train.valid_prompt_embed_root, "chi_prompt.txt")
|
|
with open(chi_prompt_file, "w", encoding="utf-8") as f:
|
|
f.write(chi_prompt)
|
|
|
|
for prompt in validation_prompts:
|
|
prompt_embed_path = osp.join(
|
|
config.train.valid_prompt_embed_root, f"{prompt[:50]}_{valid_prompt_embed_suffix}"
|
|
)
|
|
if not (osp.exists(prompt_embed_path) and osp.exists(null_embed_path)):
|
|
skip = False
|
|
logger.info("Preparing Visualization prompt embeddings...")
|
|
break
|
|
if accelerator.is_main_process and not skip:
|
|
if config.data.load_text_feat and (tokenizer is None or text_encoder is None):
|
|
logger.info(f"Loading text encoder and tokenizer from {config.text_encoder.text_encoder_name} ...")
|
|
tokenizer, text_encoder = get_tokenizer_and_text_encoder(name=config.text_encoder.text_encoder_name)
|
|
|
|
for prompt in validation_prompts:
|
|
prompt_embed_path = osp.join(
|
|
config.train.valid_prompt_embed_root, f"{prompt[:50]}_{valid_prompt_embed_suffix}"
|
|
)
|
|
if "T5" in config.text_encoder.text_encoder_name:
|
|
txt_tokens = tokenizer(
|
|
prompt, max_length=max_length, padding="max_length", truncation=True, return_tensors="pt"
|
|
).to(accelerator.device)
|
|
caption_emb = text_encoder(txt_tokens.input_ids, attention_mask=txt_tokens.attention_mask)[0]
|
|
caption_emb_mask = txt_tokens.attention_mask
|
|
elif (
|
|
"gemma" in config.text_encoder.text_encoder_name or "Qwen" in config.text_encoder.text_encoder_name
|
|
):
|
|
if not config.text_encoder.chi_prompt:
|
|
max_length_all = config.text_encoder.model_max_length
|
|
else:
|
|
chi_prompt = "\n".join(config.text_encoder.chi_prompt)
|
|
prompt = chi_prompt + prompt
|
|
num_chi_prompt_tokens = len(tokenizer.encode(chi_prompt))
|
|
max_length_all = (
|
|
num_chi_prompt_tokens + config.text_encoder.model_max_length - 2
|
|
) # magic number 2: [bos], [_]
|
|
|
|
txt_tokens = tokenizer(
|
|
prompt,
|
|
max_length=max_length_all,
|
|
padding="max_length",
|
|
truncation=True,
|
|
return_tensors="pt",
|
|
).to(accelerator.device)
|
|
select_index = [0] + list(range(-config.text_encoder.model_max_length + 1, 0))
|
|
caption_emb = text_encoder(txt_tokens.input_ids, attention_mask=txt_tokens.attention_mask)[0][
|
|
:, select_index
|
|
]
|
|
caption_emb_mask = txt_tokens.attention_mask[:, select_index]
|
|
else:
|
|
raise ValueError(f"{config.text_encoder.text_encoder_name} is not supported!!")
|
|
|
|
torch.save({"caption_embeds": caption_emb, "emb_mask": caption_emb_mask}, prompt_embed_path)
|
|
|
|
null_tokens = tokenizer(
|
|
"", max_length=max_length, padding="max_length", truncation=True, return_tensors="pt"
|
|
).to(accelerator.device)
|
|
if "T5" in config.text_encoder.text_encoder_name:
|
|
null_token_emb = text_encoder(null_tokens.input_ids, attention_mask=null_tokens.attention_mask)[0]
|
|
elif "gemma" in config.text_encoder.text_encoder_name or "Qwen" in config.text_encoder.text_encoder_name:
|
|
null_token_emb = text_encoder(null_tokens.input_ids, attention_mask=null_tokens.attention_mask)[0]
|
|
else:
|
|
raise ValueError(f"{config.text_encoder.text_encoder_name} is not supported!!")
|
|
torch.save(
|
|
{"uncond_prompt_embeds": null_token_emb, "uncond_prompt_embeds_mask": null_tokens.attention_mask},
|
|
null_embed_path,
|
|
)
|
|
if config.data.load_text_feat:
|
|
del tokenizer
|
|
del text_encoder
|
|
del null_token_emb
|
|
del null_tokens
|
|
flush()
|
|
|
|
os.environ["AUTOCAST_LINEAR_ATTN"] = "true" if config.model.autocast_linear_attn else "false"
|
|
|
|
# 1. build scheduler
|
|
predict_info = ""
|
|
if config.scheduler.weighting_scheme in ["logit_normal", "mode", "logit_normal_trigflow"]:
|
|
predict_info += (
|
|
f"flow weighting: {config.scheduler.weighting_scheme}, "
|
|
f"logit-mean: {config.scheduler.logit_mean}, logit-std: {config.scheduler.logit_std}, "
|
|
f"logit-mean-discriminator: {config.scheduler.logit_mean_discriminator}, logit-std-discriminator: {config.scheduler.logit_std_discriminator}"
|
|
)
|
|
logger.info(predict_info)
|
|
|
|
# 2. build models
|
|
# student
|
|
model_kwargs = model_init_config(config, latent_size=latent_size)
|
|
model = build_model(
|
|
config.model.model,
|
|
config.train.grad_checkpointing,
|
|
getattr(config.model, "fp32_attention", False),
|
|
logvar=config.model.logvar,
|
|
cfg_embed=config.model.cfg_embed,
|
|
cfg_embed_scale=config.model.cfg_embed_scale,
|
|
lr_scale=config.train.lr_scale,
|
|
**model_kwargs,
|
|
).train()
|
|
|
|
# teacher
|
|
teacher_model_kwargs = model_init_config(config, latent_size=latent_size)
|
|
teacher_model_kwargs.update({"cross_attn_type": "flash"})
|
|
pretrained_model = build_model(
|
|
config.model.teacher if config.model.teacher else config.model.model,
|
|
config.train.grad_checkpointing,
|
|
use_fp32_attention=False,
|
|
**teacher_model_kwargs,
|
|
).eval()
|
|
|
|
# 3. build discriminator
|
|
disc = SanaMSCMDiscriminator(
|
|
pretrained_model,
|
|
is_multiscale=config.model.ladd_multi_scale,
|
|
head_block_ids=config.model.head_block_ids,
|
|
)
|
|
disc.train()
|
|
disc.model.requires_grad_(False)
|
|
|
|
if config.train.ema_update:
|
|
model_ema = deepcopy(model).eval()
|
|
else:
|
|
model_ema = None
|
|
|
|
logger.info(
|
|
colored(
|
|
f"{model.__class__.__name__}:{config.model.model}, "
|
|
f"Model Parameters: {sum(p.numel() for p in model.parameters()) / 1e6:.2f}M",
|
|
"green",
|
|
attrs=["bold"],
|
|
)
|
|
)
|
|
# 2-1. load model
|
|
if args.load_from is not None:
|
|
config.model.load_from = args.load_from
|
|
if config.model.load_from is not None and load_from:
|
|
# load student model
|
|
load_result = load_checkpoint(
|
|
config.model.load_from,
|
|
model,
|
|
model_ema=model_ema,
|
|
load_ema=config.model.resume_from.get("load_ema", False),
|
|
null_embed_path=null_embed_path,
|
|
)
|
|
_, missing, unexpected, _, _ = load_result
|
|
logger.warning(colored(f"Missing keys: {missing}", "red"))
|
|
logger.warning(colored(f"Unexpected keys: {unexpected}", "red"))
|
|
|
|
# 2-2. model growth
|
|
if config.model_growth is not None:
|
|
assert config.model.load_from is None
|
|
model_growth_initializer = ModelGrowthInitializer(model, config.model_growth)
|
|
model = model_growth_initializer.initialize(
|
|
strategy=config.model_growth.init_strategy, **config.model_growth.init_params
|
|
)
|
|
|
|
if config.train.ema_update:
|
|
ema_update(model_ema, model, 0.0)
|
|
# prepare for FSDP clip grad norm calculation
|
|
if accelerator.distributed_type == DistributedType.FSDP:
|
|
for m in accelerator._models:
|
|
m.clip_grad_norm_ = types.MethodType(clip_grad_norm_, m)
|
|
|
|
# 3. build dataloader
|
|
config.data.data_dir = config.data.data_dir if isinstance(config.data.data_dir, list) else [config.data.data_dir]
|
|
config.data.data_dir = [
|
|
data if data.startswith(("https://", "http://", "gs://", "/", "~")) else osp.abspath(osp.expanduser(data))
|
|
for data in config.data.data_dir
|
|
]
|
|
|
|
num_replicas = int(os.environ["WORLD_SIZE"])
|
|
rank = int(os.environ["RANK"])
|
|
if config.model.aspect_ratio_type is not None:
|
|
config.data.aspect_ratio_type = config.model.aspect_ratio_type
|
|
dataset = build_dataset(
|
|
asdict(config.data),
|
|
resolution=image_size,
|
|
real_prompt_ratio=config.train.real_prompt_ratio,
|
|
max_length=max_length,
|
|
config=config,
|
|
caption_proportion=config.data.caption_proportion,
|
|
sort_dataset=config.data.sort_dataset,
|
|
vae_downsample_rate=config.vae.vae_downsample_rate,
|
|
)
|
|
if config.model.multi_scale:
|
|
drop_last = True
|
|
uuid = hashlib.sha256("-".join(config.data.data_dir).encode()).hexdigest()[:8]
|
|
cache_dir = osp.expanduser(f"~/.cache/_wids_batchsampler_cache")
|
|
os.makedirs(cache_dir, exist_ok=True)
|
|
base_pattern = (
|
|
f"{cache_dir}/{getpass.getuser()}-{uuid}-sort_dataset{config.data.sort_dataset}"
|
|
f"-hq_only{config.data.hq_only}-valid_num{config.data.valid_num}"
|
|
f"-aspect_ratio{len(dataset.aspect_ratio)}-droplast{drop_last}"
|
|
f"dataset_len{len(dataset)}"
|
|
)
|
|
cache_file = f"{base_pattern}-num_replicas{num_replicas}-rank{rank}"
|
|
for i in config.data.data_dir:
|
|
cache_file += f"-{i}"
|
|
cache_file += ".json"
|
|
|
|
sampler = DistributedRangedSampler(dataset, num_replicas=num_replicas, rank=rank)
|
|
batch_sampler = AspectRatioBatchSampler(
|
|
sampler=sampler,
|
|
dataset=dataset,
|
|
batch_size=config.train.train_batch_size,
|
|
aspect_ratios=dataset.aspect_ratio,
|
|
drop_last=drop_last,
|
|
ratio_nums=dataset.ratio_nums,
|
|
config=config,
|
|
valid_num=config.data.valid_num,
|
|
hq_only=config.data.hq_only,
|
|
cache_file=cache_file,
|
|
caching=args.caching,
|
|
clipscore_filter_thres=args.data.del_img_clip_thr,
|
|
)
|
|
train_dataloader = build_dataloader(dataset, batch_sampler=batch_sampler, num_workers=config.train.num_workers)
|
|
train_dataloader_len = len(train_dataloader)
|
|
logger.info(f"rank-{rank} Cached file len: {len(train_dataloader.batch_sampler.cached_idx)}")
|
|
else:
|
|
sampler = DistributedRangedSampler(dataset, num_replicas=num_replicas, rank=rank)
|
|
train_dataloader = build_dataloader(
|
|
dataset,
|
|
num_workers=config.train.num_workers,
|
|
batch_size=config.train.train_batch_size,
|
|
shuffle=False,
|
|
sampler=sampler,
|
|
)
|
|
train_dataloader_len = len(train_dataloader)
|
|
load_vae_feat = getattr(train_dataloader.dataset, "load_vae_feat", False)
|
|
load_text_feat = getattr(train_dataloader.dataset, "load_text_feat", False)
|
|
|
|
# 4. build optimizer and lr scheduler
|
|
lr_scale_ratio = 1
|
|
if getattr(config.train, "auto_lr", None):
|
|
lr_scale_ratio = auto_scale_lr(
|
|
config.train.train_batch_size * get_world_size() * config.train.gradient_accumulation_steps,
|
|
config.train.optimizer,
|
|
**config.train.auto_lr,
|
|
)
|
|
optimizer_G = build_optimizer(model, config.train.optimizer)
|
|
# only build optimizer for discriminator's head
|
|
optimizer_D = build_optimizer(disc.heads, config.train.optimizer)
|
|
|
|
# print learning rates
|
|
if accelerator.is_main_process and config.train.show_gradient:
|
|
logger.info("Learning rates for different layers:")
|
|
logger.info("Generator learning rates:")
|
|
for group in optimizer_G.param_groups:
|
|
if "name" in group:
|
|
logger.info(f"Layer: {group['name']}, Learning rate: {group['lr']:.8f}")
|
|
else:
|
|
logger.info(f"Layer: unnamed, Learning rate: {group['lr']:.8f}")
|
|
|
|
logger.info("Discriminator learning rates:")
|
|
for group in optimizer_D.param_groups:
|
|
if "name" in group:
|
|
logger.info(f"Layer: {group['name']}, Learning rate: {group['lr']:.8f}")
|
|
else:
|
|
logger.info(f"Layer: unnamed, Learning rate: {group['lr']:.8f}")
|
|
|
|
lr_scheduler = build_lr_scheduler(config.train, optimizer_G, train_dataloader, lr_scale_ratio)
|
|
logger.warning(
|
|
f"{colored(f'Basic Setting: ', 'green', attrs=['bold'])}"
|
|
f"lr: {config.train.optimizer['lr']:.5f}, bs: {config.train.train_batch_size}, gc: {config.train.grad_checkpointing}, "
|
|
f"gc_accum_step: {config.train.gradient_accumulation_steps}, qk norm: {config.model.qk_norm}, "
|
|
f"fp32 attn: {config.model.fp32_attention}, attn type: {config.model.attn_type}, ffn type: {config.model.ffn_type}, "
|
|
f"text encoder: {config.text_encoder.text_encoder_name}, captions: {config.data.caption_proportion}, precision: {config.model.mixed_precision}"
|
|
)
|
|
|
|
timestamp = time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime())
|
|
|
|
if accelerator.is_main_process:
|
|
tracker_config = dict(vars(config))
|
|
try:
|
|
accelerator.init_trackers(args.tracker_project_name, tracker_config)
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize trackers: {e}")
|
|
accelerator.init_trackers(f"tb_{timestamp}")
|
|
|
|
start_epoch = 0
|
|
start_step = 0
|
|
total_steps = train_dataloader_len * config.train.num_epochs
|
|
complete_state_dict = {}
|
|
|
|
# Resume training
|
|
if config.model.resume_from is not None and config.model.resume_from["checkpoint"] is not None:
|
|
ckpt_path = osp.join(config.work_dir, "checkpoints")
|
|
check_flag = osp.exists(ckpt_path) and len(os.listdir(ckpt_path)) != 0
|
|
if config.model.resume_from["checkpoint"] == "latest":
|
|
if check_flag:
|
|
config.model.resume_from["resume_optimizer"] = True
|
|
config.model.resume_from["resume_lr_scheduler"] = True
|
|
checkpoints = os.listdir(ckpt_path)
|
|
if "latest.pth" in checkpoints and osp.exists(osp.join(ckpt_path, "latest.pth")):
|
|
config.model.resume_from["checkpoint"] = osp.realpath(osp.join(ckpt_path, "latest.pth"))
|
|
else:
|
|
# Filter out discriminator checkpoints (those with suffix_checkpoints suffix)
|
|
# The discriminator is loaded separately later
|
|
suffix = config.train.suffix_checkpoints
|
|
checkpoints = [
|
|
i
|
|
for i in checkpoints
|
|
if i.startswith("epoch_") and not (suffix and i.endswith(f"_{suffix}.pth"))
|
|
]
|
|
checkpoints = sorted(checkpoints, key=lambda x: int(x.replace(".pth", "").split("_")[3]))
|
|
config.model.resume_from["checkpoint"] = osp.join(ckpt_path, checkpoints[-1])
|
|
else:
|
|
config.model.resume_from["resume_optimizer"] = config.train.load_from_optimizer
|
|
config.model.resume_from["resume_lr_scheduler"] = config.train.load_from_lr_scheduler
|
|
config.model.resume_from["checkpoint"] = config.model.load_from
|
|
|
|
if config.model.resume_from["checkpoint"] is not None:
|
|
load_result = load_checkpoint(
|
|
**config.model.resume_from,
|
|
model=model,
|
|
model_ema=model_ema,
|
|
optimizer=optimizer_G,
|
|
lr_scheduler=lr_scheduler,
|
|
null_embed_path=null_embed_path,
|
|
)
|
|
_, missing, unexpected, _, _ = load_result
|
|
logger.warning(colored(f"Generator Missing keys: {missing}", "red"))
|
|
logger.warning(colored(f"Generator Unexpected keys: {unexpected}", "red"))
|
|
|
|
disc_ckpt_path = config.model.resume_from["checkpoint"].replace(
|
|
".pth", f"_{config.train.suffix_checkpoints}.pth"
|
|
)
|
|
if osp.exists(disc_ckpt_path):
|
|
checkpoint = find_model(disc_ckpt_path)
|
|
heads_state = checkpoint.get("state_dict", checkpoint)
|
|
|
|
heads_state = {k: v for k, v in heads_state.items() if not k.startswith("transformer.")}
|
|
complete_state_dict.update(heads_state)
|
|
|
|
if optimizer_D is not None and "optimizer" in checkpoint:
|
|
try:
|
|
optimizer_D.load_state_dict(checkpoint["optimizer"])
|
|
except Exception as e:
|
|
logger.warning(colored(f"Skipping discriminator optimizer resume: {e}", "red"))
|
|
|
|
path = osp.basename(config.model.resume_from["checkpoint"])
|
|
try:
|
|
start_epoch = int(path.replace(".pth", "").split("_")[1]) - 1
|
|
start_step = int(path.replace(".pth", "").split("_")[3])
|
|
except:
|
|
pass
|
|
|
|
if config.model.teacher_model is not None:
|
|
checkpoint = find_model(config.model.teacher_model)
|
|
backbone_state = checkpoint.get("state_dict", checkpoint)
|
|
|
|
has_transformer_prefix = any(k.startswith("transformer.") for k in backbone_state.keys())
|
|
if not has_transformer_prefix:
|
|
backbone_state = {f"transformer.{k}": v for k, v in backbone_state.items()}
|
|
|
|
complete_state_dict.update(backbone_state)
|
|
|
|
if complete_state_dict:
|
|
missing, unexpected = disc.load_state_dict(complete_state_dict, strict=False)
|
|
logger.warning(colored(f"Discriminator Missing keys: {missing}", "red"))
|
|
logger.warning(colored(f"Discriminator Unexpected keys: {unexpected}", "red"))
|
|
|
|
# resume randomise
|
|
set_random_seed((start_step + 1) // config.train.save_model_steps + int(os.environ["LOCAL_RANK"]))
|
|
logger.info(f'Set seed: {(start_step + 1) // config.train.save_model_steps + int(os.environ["LOCAL_RANK"])}')
|
|
|
|
# Prepare everything
|
|
# There is no specific order to remember, you just need to unpack the
|
|
# objects in the same order you gave them to the prepare method.
|
|
model, pretrained_model = accelerator.prepare(model, pretrained_model)
|
|
disc = accelerator.prepare(disc)
|
|
optimizer_G, optimizer_D, lr_scheduler = accelerator.prepare(optimizer_G, optimizer_D, lr_scheduler)
|
|
|
|
# Start Training
|
|
train(
|
|
config=config,
|
|
args=args,
|
|
accelerator=accelerator,
|
|
model=model,
|
|
model_ema=model_ema,
|
|
optimizer_G=optimizer_G,
|
|
optimizer_D=optimizer_D,
|
|
lr_scheduler=lr_scheduler,
|
|
train_dataloader=train_dataloader,
|
|
logger=logger,
|
|
pretrained_model=pretrained_model,
|
|
disc=disc,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|