1162 lines
49 KiB
Python
1162 lines
49 KiB
Python
"""Distillation training for TAE (Tiny AutoEncoder) against the full Wan VAE.
|
|
|
|
The TAE (https://github.com/madebyollin/taehv, ported as AutoencoderTinyWan in
|
|
videox_fun/models/wan_tae.py) shares the exact latent space of the full-size
|
|
Wan VAE, so training is a plain reconstruction distillation:
|
|
|
|
x (video, [-1, 1])
|
|
-> teacher (full VAE, frozen) : z_full = teacher.encode(x).mode()
|
|
-> TAE encoder : z_tae = tae.encode(x).mode()
|
|
-> TAE decoder : x_hat = tae.decode(z_tae).sample
|
|
loss = pixel L1(x_hat, x) + latent_weight * MSE(z_tae, z_full)
|
|
|
|
The latent MSE anchors the TAE latents to the native VAE latent space, which
|
|
is what makes TAE latents interchangeable with the diffusion model latents.
|
|
|
|
The teacher VAE family is selected by --config_path:
|
|
config/wan2.1/wan_civitai.yaml -> AutoencoderKLWan (16ch, taew2_1)
|
|
config/wan2.2/wan_civitai_5b.yaml -> AutoencoderKLWan3_8 (48ch, taew2_2)
|
|
(any wan2.2 *_2.2vae.yaml also works, they share the same vae_kwargs)
|
|
"""
|
|
#!/usr/bin/env python
|
|
# coding=utf-8
|
|
# Copyright 2025 The VideoX-Fun Team. All rights reserved.
|
|
#
|
|
# 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
|
|
|
|
import argparse
|
|
import gc
|
|
import inspect
|
|
import logging
|
|
import math
|
|
import os
|
|
import pickle
|
|
import random
|
|
import shutil
|
|
import sys
|
|
|
|
import accelerate
|
|
import diffusers
|
|
import numpy as np
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import transformers
|
|
from accelerate import Accelerator
|
|
from accelerate.logging import get_logger
|
|
from accelerate.state import AcceleratorState
|
|
from accelerate.utils import ProjectConfiguration, set_seed
|
|
from diffusers.optimization import get_scheduler
|
|
from diffusers.utils import check_min_version, deprecate
|
|
from diffusers.utils.torch_utils import is_compiled_module
|
|
from einops import rearrange
|
|
from omegaconf import OmegaConf
|
|
from packaging import version
|
|
from PIL import Image
|
|
from torch.utils.data import RandomSampler
|
|
from torch.utils.tensorboard import SummaryWriter
|
|
from torchvision import transforms
|
|
from transformers.utils import ContextManagers
|
|
|
|
import datasets
|
|
|
|
current_file_path = os.path.abspath(__file__)
|
|
project_roots = [os.path.dirname(current_file_path), os.path.dirname(os.path.dirname(current_file_path)), os.path.dirname(os.path.dirname(os.path.dirname(current_file_path)))]
|
|
for project_root in project_roots:
|
|
sys.path.insert(0, project_root) if project_root not in sys.path else None
|
|
|
|
from videox_fun.data import (ASPECT_RATIO_512, ASPECT_RATIO_RANDOM_CROP_512,
|
|
ASPECT_RATIO_RANDOM_CROP_PROB,
|
|
AspectRatioBatchImageVideoSampler,
|
|
ImageVideoDataset, ImageVideoSampler,
|
|
RandomSampler, VideoDataset,
|
|
get_closest_ratio)
|
|
from videox_fun.models import (AutoencoderKLWan, AutoencoderKLWan3_8,
|
|
AutoencoderTinyWan)
|
|
from videox_fun.utils.fsdp_ema import FSDPEMA
|
|
from videox_fun.utils.tqdm_bar import PauseAwareTqdm
|
|
from videox_fun.utils.utils import save_videos_grid
|
|
|
|
# Will error if the minimal version of diffusers is not installed.
|
|
check_min_version("0.18.0.dev0")
|
|
|
|
logger = get_logger(__name__, log_level="INFO")
|
|
|
|
|
|
def filter_kwargs(cls, kwargs):
|
|
"""Filter kwargs to only include parameters accepted by the class __init__."""
|
|
sig = inspect.signature(cls.__init__)
|
|
valid_keys = set(sig.parameters.keys()) - {"self"}
|
|
return {k: v for k, v in kwargs.items() if k in valid_keys}
|
|
|
|
|
|
def linear_decay(initial_value, final_value, total_steps, current_step):
|
|
if current_step >= total_steps:
|
|
return final_value
|
|
current_step = max(0, current_step)
|
|
step_size = (final_value - initial_value) / total_steps
|
|
current_value = initial_value + step_size * current_step
|
|
return current_value
|
|
|
|
|
|
def log_validation(vae, tae, args, accelerator, weight_dtype, global_step):
|
|
"""Validation: encode video -> decode with TAE / full VAE, save comparison videos."""
|
|
try:
|
|
with torch.no_grad(), torch.cuda.amp.autocast(dtype=weight_dtype):
|
|
logger.info("Running validation...")
|
|
|
|
if args.validation_paths is None or len(args.validation_paths) == 0:
|
|
logger.info("No validation_paths provided, skipping validation.")
|
|
return
|
|
|
|
from decord import VideoReader
|
|
|
|
unwrapped_tae = accelerator.unwrap_model(tae)
|
|
|
|
for i, video_path in enumerate(args.validation_paths):
|
|
if not os.path.exists(video_path):
|
|
logger.warning(f"Validation video not found: {video_path}")
|
|
continue
|
|
|
|
# Load video frames
|
|
vr = VideoReader(video_path)
|
|
num_frames = min(len(vr), args.video_sample_n_frames)
|
|
# Align to temporal compression ratio (4k+1)
|
|
temporal_ratio = vae.config.temporal_compression_ratio
|
|
num_frames = (num_frames - 1) // temporal_ratio * temporal_ratio + 1
|
|
if num_frames <= 0:
|
|
num_frames = 1
|
|
|
|
indices = list(range(num_frames))
|
|
frames = vr.get_batch(indices).asnumpy() # [F, H, W, C]
|
|
|
|
# Preprocess to tensor [1, C, F, H, W] in [-1, 1]
|
|
pixel_values = torch.from_numpy(frames).permute(0, 3, 1, 2).float() / 255.0
|
|
pixel_values = pixel_values * 2.0 - 1.0
|
|
|
|
# Resize to target size aligned to the spatial compression ratio
|
|
spatial_ratio = int(vae.config.spatial_compression_ratio)
|
|
h_target = int(args.video_sample_size / spatial_ratio) * spatial_ratio
|
|
w_target = h_target # square for simplicity in validation
|
|
pixel_values = F.interpolate(
|
|
pixel_values, size=(h_target, w_target), mode='bilinear', align_corners=False
|
|
)
|
|
pixel_values = pixel_values.unsqueeze(0).permute(0, 2, 1, 3, 4) # [1, C, F, H, W]
|
|
pixel_values = pixel_values.to(device=accelerator.device, dtype=weight_dtype)
|
|
|
|
# TAE reconstruction
|
|
z_tae = unwrapped_tae.encode(pixel_values).latent_dist.mode()
|
|
decoded_tae = unwrapped_tae.decode(z_tae).sample
|
|
decoded_tae = decoded_tae[:, :, :pixel_values.shape[2]]
|
|
|
|
# Full VAE reconstruction for comparison
|
|
z_full = vae.encode(pixel_values).latent_dist.mode()
|
|
decoded_full = vae.decode(z_full, return_dict=False)[0]
|
|
|
|
# Save videos
|
|
os.makedirs(os.path.join(args.output_dir, "sample"), exist_ok=True)
|
|
save_videos_grid(
|
|
pixel_values,
|
|
os.path.join(args.output_dir, f"sample/step{global_step}_val{i}_input.mp4"),
|
|
rescale=True, fps=24
|
|
)
|
|
save_videos_grid(
|
|
decoded_tae,
|
|
os.path.join(args.output_dir, f"sample/step{global_step}_val{i}_taehv.mp4"),
|
|
rescale=True, fps=24
|
|
)
|
|
save_videos_grid(
|
|
decoded_full,
|
|
os.path.join(args.output_dir, f"sample/step{global_step}_val{i}_fullvae.mp4"),
|
|
rescale=True, fps=24
|
|
)
|
|
logger.info(f"Saved validation video {i} at step {global_step}")
|
|
|
|
except Exception as e:
|
|
gc.collect()
|
|
torch.cuda.empty_cache()
|
|
print(f"Eval error on rank {accelerator.process_index} with info {e}")
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Training script for Wan TAE (Tiny AutoEncoder) distillation.")
|
|
parser.add_argument(
|
|
"--config_path",
|
|
type=str,
|
|
default="config/wan2.1/wan_civitai.yaml",
|
|
help="Path to the model config yaml file. Its vae_kwargs selects the teacher "
|
|
"full VAE family (AutoencoderKLWan -> taew2_1, AutoencoderKLWan3_8 -> taew2_2).",
|
|
)
|
|
parser.add_argument(
|
|
"--pretrained_model_name_or_path",
|
|
type=str,
|
|
default=None,
|
|
required=True,
|
|
help="Path to pretrained model directory (contains the full VAE weights).",
|
|
)
|
|
parser.add_argument(
|
|
"--revision",
|
|
type=str,
|
|
default=None,
|
|
required=False,
|
|
help="Revision of pretrained model identifier from huggingface.co/models.",
|
|
)
|
|
parser.add_argument(
|
|
"--variant",
|
|
type=str,
|
|
default=None,
|
|
help="Variant of the model files of the pretrained model identifier.",
|
|
)
|
|
parser.add_argument(
|
|
"--train_data_dir",
|
|
type=str,
|
|
default=None,
|
|
help=("A folder containing the training data."),
|
|
)
|
|
parser.add_argument(
|
|
"--train_data_meta",
|
|
type=str,
|
|
default=None,
|
|
help=("A json/csv containing the training data meta."),
|
|
)
|
|
parser.add_argument(
|
|
"--max_train_samples",
|
|
type=int,
|
|
default=None,
|
|
help=(
|
|
"For debugging purposes or quicker training, truncate the number of training examples to this "
|
|
"value if set."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--validation_paths",
|
|
type=str,
|
|
default=None,
|
|
nargs="+",
|
|
help=("Video paths for validation (encode -> decode with TAE and full VAE)."),
|
|
)
|
|
parser.add_argument(
|
|
"--output_dir",
|
|
type=str,
|
|
default="output_dir_taehv",
|
|
help="The output directory where the model predictions and checkpoints will be written.",
|
|
)
|
|
parser.add_argument(
|
|
"--cache_dir",
|
|
type=str,
|
|
default=None,
|
|
help="The directory where the downloaded models and datasets will be stored.",
|
|
)
|
|
parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.")
|
|
parser.add_argument(
|
|
"--train_batch_size", type=int, default=1, help="Batch size (per device) for the training dataloader."
|
|
)
|
|
parser.add_argument(
|
|
"--vae_mini_batch", type=int, default=1, help="Mini batch size for VAE encoding."
|
|
)
|
|
parser.add_argument("--num_train_epochs", type=int, default=100)
|
|
parser.add_argument(
|
|
"--max_train_steps",
|
|
type=int,
|
|
default=None,
|
|
help="Total number of training steps to perform. If provided, overrides num_train_epochs.",
|
|
)
|
|
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(
|
|
"--gradient_checkpointing",
|
|
action="store_true",
|
|
help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.",
|
|
)
|
|
parser.add_argument(
|
|
"--learning_rate",
|
|
type=float,
|
|
default=1e-4,
|
|
help="Initial learning rate (after the potential warmup period) to use.",
|
|
)
|
|
parser.add_argument(
|
|
"--scale_lr",
|
|
action="store_true",
|
|
default=False,
|
|
help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.",
|
|
)
|
|
parser.add_argument(
|
|
"--lr_scheduler",
|
|
type=str,
|
|
default="constant_with_warmup",
|
|
help=(
|
|
'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",'
|
|
' "constant", "constant_with_warmup"]'
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--lr_warmup_steps", type=int, default=100, help="Number of steps for the warmup in the lr scheduler."
|
|
)
|
|
parser.add_argument(
|
|
"--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes."
|
|
)
|
|
parser.add_argument(
|
|
"--use_came", action="store_true", help="Whether or not to use CAME optimizer."
|
|
)
|
|
parser.add_argument(
|
|
"--allow_tf32",
|
|
action="store_true",
|
|
help=(
|
|
"Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training."
|
|
),
|
|
)
|
|
parser.add_argument("--use_ema", action="store_true", help="Whether to use EMA model.")
|
|
parser.add_argument(
|
|
"--non_ema_revision",
|
|
type=str,
|
|
default=None,
|
|
required=False,
|
|
help="Revision of pretrained non-ema model identifier.",
|
|
)
|
|
parser.add_argument(
|
|
"--dataloader_num_workers",
|
|
type=int,
|
|
default=0,
|
|
help="Number of subprocesses to use for data loading.",
|
|
)
|
|
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.999, help="The beta2 parameter for the Adam optimizer.")
|
|
parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.")
|
|
parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer.")
|
|
parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.")
|
|
parser.add_argument(
|
|
"--logging_dir",
|
|
type=str,
|
|
default="logs",
|
|
help="TensorBoard log directory.",
|
|
)
|
|
parser.add_argument(
|
|
"--report_model_info", action="store_true", help="Whether or not to report more info about model."
|
|
)
|
|
parser.add_argument(
|
|
"--mixed_precision",
|
|
type=str,
|
|
default=None,
|
|
choices=["no", "fp16", "bf16"],
|
|
help="Whether to use mixed precision. Choose between fp16 and bf16.",
|
|
)
|
|
parser.add_argument(
|
|
"--report_to",
|
|
type=str,
|
|
default="tensorboard",
|
|
help='The integration to report the results and logs to.',
|
|
)
|
|
parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank")
|
|
parser.add_argument(
|
|
"--checkpointing_steps",
|
|
type=int,
|
|
default=500,
|
|
help="Save a checkpoint of the training state every X updates.",
|
|
)
|
|
parser.add_argument(
|
|
"--checkpoints_total_limit",
|
|
type=int,
|
|
default=None,
|
|
help=("Max number of checkpoints to store."),
|
|
)
|
|
parser.add_argument(
|
|
"--resume_from_checkpoint",
|
|
type=str,
|
|
default=None,
|
|
help="Whether training should be resumed from a previous checkpoint.",
|
|
)
|
|
parser.add_argument(
|
|
"--validation_epochs",
|
|
type=int,
|
|
default=5,
|
|
help="Run validation every X epochs.",
|
|
)
|
|
parser.add_argument(
|
|
"--validation_steps",
|
|
type=int,
|
|
default=2000,
|
|
help="Run validation every X steps.",
|
|
)
|
|
parser.add_argument(
|
|
"--tracker_project_name",
|
|
type=str,
|
|
default="taehv-train",
|
|
help="The `project_name` argument passed to Accelerator.init_trackers.",
|
|
)
|
|
parser.add_argument(
|
|
"--enable_bucket", action="store_true", help="Whether enable bucket sample in datasets."
|
|
)
|
|
parser.add_argument(
|
|
"--random_ratio_crop", action="store_true", help="Whether enable random ratio crop sample in datasets."
|
|
)
|
|
parser.add_argument(
|
|
"--random_hw_adapt", action="store_true", help="Whether enable random adapt height and width in datasets."
|
|
)
|
|
parser.add_argument(
|
|
"--video_sample_size",
|
|
type=int,
|
|
default=512,
|
|
help="Training video sample size.",
|
|
)
|
|
parser.add_argument(
|
|
"--video_sample_stride",
|
|
type=int,
|
|
default=1,
|
|
help="Sample stride of the video.",
|
|
)
|
|
parser.add_argument(
|
|
"--video_sample_n_frames",
|
|
type=int,
|
|
default=33,
|
|
help="Num frame of video. Must be 4k+1 to match the causal temporal compression.",
|
|
)
|
|
parser.add_argument(
|
|
"--video_repeat",
|
|
type=int,
|
|
default=0,
|
|
help="Num of repeat video.",
|
|
)
|
|
parser.add_argument(
|
|
"--tae_path",
|
|
type=str,
|
|
default=None,
|
|
help="Optional TAE weights to warm-start from (taew2_x.pth/.safetensors, a directory, "
|
|
"or a previous checkpoint-xxx/taehv dir). If not given, train from scratch.",
|
|
)
|
|
parser.add_argument(
|
|
"--tae_arch_variant",
|
|
type=str,
|
|
default=None,
|
|
choices=[None, "super"],
|
|
help="TAE decoder architecture variant when training from scratch. "
|
|
"None (base) or 'super' (higher-quality, ~2x decoder params).",
|
|
)
|
|
parser.add_argument(
|
|
"--vae_path",
|
|
type=str,
|
|
default=None,
|
|
help=("If you want to load the weight from other full vaes, input its path."),
|
|
)
|
|
parser.add_argument(
|
|
"--freeze_tae_encoder", action="store_true",
|
|
help="Train the TAE decoder only (turns the training into a decoder-only distillation).",
|
|
)
|
|
parser.add_argument(
|
|
"--use_taehv_sequential", action="store_true",
|
|
help="Run the TAE in sequential (O(1)-memory) mode instead of parallel.",
|
|
)
|
|
parser.add_argument(
|
|
"--latent_loss_weight",
|
|
type=float,
|
|
default=1.0,
|
|
help="Weight of the latent-space MSE loss (TAE latent vs. full VAE latent) "
|
|
"relative to the pixel L1 loss.",
|
|
)
|
|
parser.add_argument(
|
|
'--trainable_modules',
|
|
nargs='+',
|
|
default=["."],
|
|
help='Enter a list of trainable modules',
|
|
)
|
|
parser.add_argument(
|
|
'--trainable_modules_low_learning_rate',
|
|
nargs='+',
|
|
default=[],
|
|
help='Enter a list of trainable modules with lower learning rate',
|
|
)
|
|
parser.add_argument(
|
|
"--use_deepspeed", action="store_true", help="Whether or not to use deepspeed."
|
|
)
|
|
parser.add_argument(
|
|
"--use_fsdp", action="store_true", help="Whether or not to use fsdp."
|
|
)
|
|
parser.add_argument(
|
|
"--low_vram", action="store_true", help="Whether enable low_vram mode."
|
|
)
|
|
parser.add_argument(
|
|
"--abnormal_norm_clip_start",
|
|
type=int,
|
|
default=1000,
|
|
help='When do we start doing additional processing on abnormal gradients.',
|
|
)
|
|
parser.add_argument(
|
|
"--initial_grad_norm_ratio",
|
|
type=int,
|
|
default=5,
|
|
help='The initial gradient is relative to the multiple of the max_grad_norm.',
|
|
)
|
|
parser.add_argument(
|
|
"--multi_stream", action="store_true", help="Whether to use cuda multi-stream."
|
|
)
|
|
parser.add_argument(
|
|
"--save_state", action="store_true", help="Whether to save accelerator state."
|
|
)
|
|
|
|
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.non_ema_revision is None:
|
|
args.non_ema_revision = args.revision
|
|
|
|
# Both the full VAE and the TAE are causal 4x temporal compressors, so the
|
|
# clip length must be 4k+1 for the decode output to match the input.
|
|
assert (args.video_sample_n_frames - 1) % 4 == 0, \
|
|
f"video_sample_n_frames must be 4k+1, got {args.video_sample_n_frames}"
|
|
|
|
return args
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
if args.non_ema_revision is not None:
|
|
deprecate(
|
|
"non_ema_revision!=None", "0.15.0",
|
|
message="Downloading 'non_ema' weights from revision branches of the Hub is deprecated.",
|
|
)
|
|
logging_dir = os.path.join(args.output_dir, args.logging_dir)
|
|
|
|
accelerator_project_config = ProjectConfiguration(project_dir=args.output_dir, logging_dir=logging_dir)
|
|
accelerator = Accelerator(
|
|
gradient_accumulation_steps=args.gradient_accumulation_steps,
|
|
mixed_precision=args.mixed_precision,
|
|
log_with=args.report_to,
|
|
project_config=accelerator_project_config,
|
|
)
|
|
|
|
deepspeed_plugin = accelerator.state.deepspeed_plugin if hasattr(accelerator.state, "deepspeed_plugin") else None
|
|
fsdp_plugin = accelerator.state.fsdp_plugin if hasattr(accelerator.state, "fsdp_plugin") else None
|
|
if deepspeed_plugin is not None:
|
|
zero_stage = int(deepspeed_plugin.zero_stage)
|
|
fsdp_stage = 0
|
|
args.use_deepspeed = True
|
|
if zero_stage == 3:
|
|
args.save_state = True
|
|
elif fsdp_plugin is not None:
|
|
from torch.distributed.fsdp import ShardingStrategy
|
|
zero_stage = 0
|
|
if fsdp_plugin.sharding_strategy in (ShardingStrategy.FULL_SHARD, None):
|
|
fsdp_stage = 3
|
|
elif fsdp_plugin.sharding_strategy is ShardingStrategy.SHARD_GRAD_OP:
|
|
fsdp_stage = 2
|
|
else:
|
|
fsdp_stage = 0
|
|
args.use_fsdp = True
|
|
if fsdp_stage == 3:
|
|
args.save_state = True
|
|
else:
|
|
zero_stage = 0
|
|
fsdp_stage = 0
|
|
|
|
if accelerator.is_main_process:
|
|
writer = SummaryWriter(log_dir=logging_dir)
|
|
|
|
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()
|
|
transformers.utils.logging.set_verbosity_warning()
|
|
diffusers.utils.logging.set_verbosity_info()
|
|
else:
|
|
datasets.utils.logging.set_verbosity_error()
|
|
transformers.utils.logging.set_verbosity_error()
|
|
diffusers.utils.logging.set_verbosity_error()
|
|
|
|
if args.seed is not None:
|
|
set_seed(args.seed)
|
|
rng = np.random.default_rng(np.random.PCG64(args.seed + accelerator.process_index))
|
|
torch_rng = torch.Generator(accelerator.device).manual_seed(args.seed + accelerator.process_index)
|
|
else:
|
|
rng = None
|
|
torch_rng = None
|
|
index_rng = np.random.default_rng(np.random.PCG64(43))
|
|
print(f"Init rng with seed {args.seed + accelerator.process_index if args.seed else 'None'}. Process_index is {accelerator.process_index}")
|
|
|
|
if accelerator.is_main_process:
|
|
if args.output_dir is not None:
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
|
|
# For mixed precision training
|
|
weight_dtype = torch.float32
|
|
if accelerator.mixed_precision == "fp16":
|
|
weight_dtype = torch.float16
|
|
args.mixed_precision = accelerator.mixed_precision
|
|
elif accelerator.mixed_precision == "bf16":
|
|
weight_dtype = torch.bfloat16
|
|
args.mixed_precision = accelerator.mixed_precision
|
|
|
|
# ==================== Load Config ====================
|
|
config = OmegaConf.load(args.config_path)
|
|
|
|
# ==================== Load Models ====================
|
|
# Teacher: full VAE (frozen), family selected by the config yaml
|
|
Chosen_AutoencoderKL = {
|
|
"AutoencoderKLWan": AutoencoderKLWan,
|
|
"AutoencoderKLWan3_8": AutoencoderKLWan3_8,
|
|
}[config['vae_kwargs'].get('vae_type', 'AutoencoderKLWan')]
|
|
|
|
vae = Chosen_AutoencoderKL.from_pretrained(
|
|
os.path.join(args.pretrained_model_name_or_path, config['vae_kwargs'].get('vae_subpath', 'vae')),
|
|
additional_kwargs=OmegaConf.to_container(config['vae_kwargs']),
|
|
)
|
|
vae.eval()
|
|
vae.requires_grad_(False)
|
|
|
|
if args.vae_path is not None:
|
|
print(f"Loading full VAE from checkpoint: {args.vae_path}")
|
|
if args.vae_path.endswith("safetensors"):
|
|
from safetensors.torch import load_file
|
|
state_dict = load_file(args.vae_path)
|
|
else:
|
|
state_dict = torch.load(args.vae_path, map_location="cpu")
|
|
state_dict = state_dict["state_dict"] if "state_dict" in state_dict else state_dict
|
|
m, u = vae.load_state_dict(state_dict, strict=False)
|
|
print(f"VAE missing keys: {len(m)}, unexpected keys: {len(u)}")
|
|
|
|
# Student: TAE (trainable), latent space follows the teacher
|
|
latent_channels = int(vae.config.latent_channels)
|
|
tae_patch_size = 2 if latent_channels == 48 else 1
|
|
if args.tae_path is not None:
|
|
print(f"Warm-starting TAE from checkpoint: {args.tae_path}")
|
|
tae_load_kwargs = {"latent_channels": latent_channels, "patch_size": tae_patch_size}
|
|
if args.tae_arch_variant is not None:
|
|
tae_load_kwargs["arch_variant"] = args.tae_arch_variant
|
|
tae = AutoencoderTinyWan.from_pretrained(
|
|
args.tae_path,
|
|
additional_kwargs=tae_load_kwargs,
|
|
)
|
|
else:
|
|
print(f"Training TAE from scratch (latent_channels={latent_channels}, "
|
|
f"patch_size={tae_patch_size}, arch_variant={args.tae_arch_variant})")
|
|
tae = AutoencoderTinyWan(
|
|
latent_channels=latent_channels,
|
|
patch_size=tae_patch_size,
|
|
arch_variant=args.tae_arch_variant,
|
|
)
|
|
tae.parallel = not args.use_taehv_sequential
|
|
# The TAE is tiny (~20MB); keep it fully in weight_dtype so forward works
|
|
# regardless of whether autocast is active.
|
|
tae.to(weight_dtype)
|
|
|
|
# Set trainable parameters
|
|
tae.requires_grad_(False)
|
|
tae.train()
|
|
if accelerator.is_main_process:
|
|
accelerator.print(f"Trainable modules '{args.trainable_modules}'.")
|
|
for name, param in tae.named_parameters():
|
|
if args.freeze_tae_encoder and "model.encoder" in name:
|
|
continue
|
|
for trainable_module_name in args.trainable_modules + args.trainable_modules_low_learning_rate:
|
|
if trainable_module_name in name:
|
|
param.requires_grad = True
|
|
break
|
|
|
|
# Print trainable params info
|
|
trainable_params = list(filter(lambda p: p.requires_grad, tae.parameters()))
|
|
total_params = sum(p.numel() for p in tae.parameters())
|
|
trainable_count = sum(p.numel() for p in trainable_params)
|
|
if accelerator.is_main_process:
|
|
print(f"Total params: {total_params / 1e6:.2f}M, Trainable params: {trainable_count / 1e6:.2f}M")
|
|
|
|
# EMA
|
|
if args.use_ema:
|
|
from diffusers.training_utils import EMAModel
|
|
if zero_stage == 3:
|
|
raise NotImplementedError("DeepSpeed Zero-3 does not support EMA.")
|
|
ema_module = AutoencoderTinyWan(**dict(tae.config))
|
|
if args.use_fsdp:
|
|
# The EMA copy gets the same FSDP wrap as the live model so that
|
|
# every local shard of the copy pairs 1:1 with the live shard.
|
|
ema_tae = FSDPEMA(ema_module, source=tae, accelerator=accelerator, fsdp_plugin=fsdp_plugin)
|
|
else:
|
|
ema_module = ema_module.to(weight_dtype)
|
|
if args.tae_path is not None:
|
|
ema_module.load_state_dict(tae.state_dict())
|
|
ema_tae = EMAModel(ema_module.parameters(), model_cls=AutoencoderTinyWan, model_config=ema_module.config)
|
|
|
|
# ==================== Save/Load Hooks ====================
|
|
if version.parse(accelerate.__version__) >= version.parse("0.16.0"):
|
|
if fsdp_stage != 0 or zero_stage == 3:
|
|
def save_model_hook(models, weights, output_dir):
|
|
accelerate_state_dict = accelerator.get_state_dict(models[-1], unwrap=True)
|
|
if accelerator.is_main_process:
|
|
from safetensors.torch import save_file
|
|
safetensor_save_path = os.path.join(output_dir, "diffusion_pytorch_model.safetensors")
|
|
accelerate_state_dict = {k: v.to(dtype=weight_dtype) for k, v in accelerate_state_dict.items()}
|
|
save_file(accelerate_state_dict, safetensor_save_path, metadata={"format": "pt"})
|
|
with open(os.path.join(output_dir, "sampler_pos_start.pkl"), 'wb') as file:
|
|
pickle.dump([batch_sampler.sampler._pos_start, first_epoch], file)
|
|
if args.use_ema:
|
|
# Every rank joins the FULL_STATE_DICT all-gather inside.
|
|
ema_tae.save_pretrained(os.path.join(output_dir, "taehv_ema"))
|
|
|
|
def load_model_hook(models, input_dir):
|
|
if args.use_ema:
|
|
ema_tae.load_pretrained(os.path.join(input_dir, "taehv_ema"))
|
|
pkl_path = os.path.join(input_dir, "sampler_pos_start.pkl")
|
|
if os.path.exists(pkl_path):
|
|
with open(pkl_path, 'rb') as file:
|
|
loaded_number, _ = pickle.load(file)
|
|
batch_sampler.sampler._pos_start = max(loaded_number - args.dataloader_num_workers * accelerator.num_processes * 2, 0)
|
|
print(f"Load pkl from {pkl_path}. Get loaded_number = {loaded_number}.")
|
|
else:
|
|
def save_model_hook(models, weights, output_dir):
|
|
if accelerator.is_main_process:
|
|
if args.use_ema:
|
|
ema_tae.save_pretrained(os.path.join(output_dir, "taehv_ema"))
|
|
models[0].save_pretrained(os.path.join(output_dir, "taehv"))
|
|
if not args.use_deepspeed:
|
|
weights.pop()
|
|
with open(os.path.join(output_dir, "sampler_pos_start.pkl"), 'wb') as file:
|
|
pickle.dump([batch_sampler.sampler._pos_start, first_epoch], file)
|
|
|
|
def load_model_hook(models, input_dir):
|
|
if args.use_ema:
|
|
ema_path = os.path.join(input_dir, "taehv_ema")
|
|
if os.path.exists(ema_path):
|
|
load_model = AutoencoderTinyWan.from_pretrained(input_dir, subfolder="taehv_ema")
|
|
load_ema = EMAModel(load_model.parameters(), model_cls=AutoencoderTinyWan, model_config=load_model.config)
|
|
ema_tae.load_state_dict(load_ema.state_dict())
|
|
ema_tae.to(accelerator.device)
|
|
del load_model, load_ema
|
|
|
|
for i in range(len(models)):
|
|
model = models.pop()
|
|
load_model = AutoencoderTinyWan.from_pretrained(input_dir, subfolder="taehv")
|
|
model.register_to_config(**load_model.config)
|
|
model.load_state_dict(load_model.state_dict())
|
|
del load_model
|
|
|
|
pkl_path = os.path.join(input_dir, "sampler_pos_start.pkl")
|
|
if os.path.exists(pkl_path):
|
|
with open(pkl_path, 'rb') as file:
|
|
loaded_number, _ = pickle.load(file)
|
|
batch_sampler.sampler._pos_start = max(loaded_number - args.dataloader_num_workers * accelerator.num_processes * 2, 0)
|
|
print(f"Load pkl from {pkl_path}. Get loaded_number = {loaded_number}.")
|
|
|
|
accelerator.register_save_state_pre_hook(save_model_hook)
|
|
accelerator.register_load_state_pre_hook(load_model_hook)
|
|
|
|
if args.gradient_checkpointing:
|
|
tae.enable_gradient_checkpointing()
|
|
|
|
if args.allow_tf32:
|
|
torch.backends.cuda.matmul.allow_tf32 = True
|
|
|
|
if args.scale_lr:
|
|
args.learning_rate = (
|
|
args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes
|
|
)
|
|
|
|
# ==================== Optimizer ====================
|
|
if args.use_8bit_adam:
|
|
import bitsandbytes as bnb
|
|
optimizer_cls = bnb.optim.AdamW8bit
|
|
elif args.use_came:
|
|
from came_pytorch import CAME
|
|
optimizer_cls = CAME
|
|
else:
|
|
optimizer_cls = torch.optim.AdamW
|
|
|
|
trainable_params_optim = [
|
|
{'params': [], 'lr': args.learning_rate},
|
|
{'params': [], 'lr': args.learning_rate / 2},
|
|
]
|
|
in_already = []
|
|
for name, param in tae.named_parameters():
|
|
if not param.requires_grad:
|
|
continue
|
|
high_lr_flag = False
|
|
if name in in_already:
|
|
continue
|
|
for trainable_module_name in args.trainable_modules:
|
|
if trainable_module_name in name:
|
|
in_already.append(name)
|
|
high_lr_flag = True
|
|
trainable_params_optim[0]['params'].append(param)
|
|
if accelerator.is_main_process:
|
|
print(f"Set {name} to lr: {args.learning_rate}")
|
|
break
|
|
if high_lr_flag:
|
|
continue
|
|
for trainable_module_name in args.trainable_modules_low_learning_rate:
|
|
if trainable_module_name in name:
|
|
in_already.append(name)
|
|
trainable_params_optim[1]['params'].append(param)
|
|
if accelerator.is_main_process:
|
|
print(f"Set {name} to lr: {args.learning_rate / 2}")
|
|
break
|
|
|
|
if args.use_came:
|
|
optimizer = optimizer_cls(
|
|
trainable_params_optim, lr=args.learning_rate,
|
|
betas=(0.9, 0.999, 0.9999), eps=(1e-30, 1e-16)
|
|
)
|
|
else:
|
|
optimizer = optimizer_cls(
|
|
trainable_params_optim, lr=args.learning_rate,
|
|
betas=(args.adam_beta1, args.adam_beta2),
|
|
weight_decay=args.adam_weight_decay, eps=args.adam_epsilon,
|
|
)
|
|
|
|
# ==================== Dataset ====================
|
|
sample_n_frames_bucket_interval = vae.config.temporal_compression_ratio
|
|
|
|
train_dataset = VideoDataset(
|
|
args.train_data_meta, args.train_data_dir,
|
|
sample_size=args.video_sample_size, sample_stride=args.video_sample_stride,
|
|
sample_n_frames=args.video_sample_n_frames,
|
|
enable_bucket=args.enable_bucket, enable_inpaint=False,
|
|
)
|
|
|
|
def worker_init_fn(_seed):
|
|
_seed = _seed * 256
|
|
def _worker_init_fn(worker_id):
|
|
np.random.seed(_seed + worker_id)
|
|
random.seed(_seed + worker_id)
|
|
return _worker_init_fn
|
|
|
|
if args.enable_bucket:
|
|
aspect_ratio_sample_size = {key: [x / 512 * args.video_sample_size for x in ASPECT_RATIO_512[key]] for key in ASPECT_RATIO_512.keys()}
|
|
batch_sampler_generator = torch.Generator().manual_seed(args.seed)
|
|
batch_sampler = AspectRatioBatchImageVideoSampler(
|
|
sampler=RandomSampler(train_dataset, generator=batch_sampler_generator),
|
|
dataset=train_dataset.dataset,
|
|
batch_size=args.train_batch_size, train_folder=args.train_data_dir, drop_last=True,
|
|
aspect_ratios=aspect_ratio_sample_size,
|
|
)
|
|
|
|
def collate_fn(examples):
|
|
new_examples = {}
|
|
new_examples["pixel_values"] = []
|
|
|
|
pixel_value = examples[0]["pixel_values"]
|
|
f, h, w, c = np.shape(pixel_value)
|
|
|
|
closest_size, closest_ratio = get_closest_ratio(h, w, ratios=aspect_ratio_sample_size)
|
|
spatial_ratio = int(vae.config.spatial_compression_ratio)
|
|
closest_size = [int(x / spatial_ratio) * spatial_ratio for x in closest_size]
|
|
|
|
min_example_length = min([example["pixel_values"].shape[0] for example in examples])
|
|
batch_video_length = int(min(args.video_sample_n_frames + sample_n_frames_bucket_interval, min_example_length))
|
|
batch_video_length = (batch_video_length - 1) // sample_n_frames_bucket_interval * sample_n_frames_bucket_interval + 1
|
|
if batch_video_length <= 0:
|
|
batch_video_length = 1
|
|
|
|
for example in examples:
|
|
pixel_values = torch.from_numpy(example["pixel_values"]).permute(0, 3, 1, 2).contiguous()
|
|
pixel_values = pixel_values / 255.0
|
|
|
|
if closest_size[0] / h > closest_size[1] / w:
|
|
resize_size = closest_size[0], int(w * closest_size[0] / h)
|
|
else:
|
|
resize_size = int(h * closest_size[1] / w), closest_size[1]
|
|
|
|
transform = transforms.Compose([
|
|
transforms.Resize(resize_size, interpolation=transforms.InterpolationMode.BILINEAR),
|
|
transforms.CenterCrop(closest_size),
|
|
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True),
|
|
])
|
|
pixel_values = transform(pixel_values)[:batch_video_length]
|
|
new_examples["pixel_values"].append(pixel_values)
|
|
|
|
new_examples["pixel_values"] = torch.stack(new_examples["pixel_values"])
|
|
return new_examples
|
|
|
|
train_dataloader = torch.utils.data.DataLoader(
|
|
train_dataset,
|
|
batch_sampler=batch_sampler,
|
|
collate_fn=collate_fn,
|
|
persistent_workers=True if args.dataloader_num_workers != 0 else False,
|
|
num_workers=args.dataloader_num_workers,
|
|
worker_init_fn=worker_init_fn(args.seed + accelerator.process_index)
|
|
)
|
|
else:
|
|
batch_sampler_generator = torch.Generator().manual_seed(args.seed)
|
|
batch_sampler = ImageVideoSampler(RandomSampler(train_dataset, generator=batch_sampler_generator), train_dataset, args.train_batch_size)
|
|
train_dataloader = torch.utils.data.DataLoader(
|
|
train_dataset,
|
|
batch_sampler=batch_sampler,
|
|
persistent_workers=True if args.dataloader_num_workers != 0 else False,
|
|
num_workers=args.dataloader_num_workers,
|
|
worker_init_fn=worker_init_fn(args.seed + accelerator.process_index)
|
|
)
|
|
|
|
# ==================== LR Scheduler ====================
|
|
overrode_max_train_steps = False
|
|
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
|
|
if args.max_train_steps is None:
|
|
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
|
|
overrode_max_train_steps = True
|
|
|
|
lr_scheduler = get_scheduler(
|
|
args.lr_scheduler, optimizer=optimizer,
|
|
num_warmup_steps=args.lr_warmup_steps * accelerator.num_processes,
|
|
num_training_steps=args.max_train_steps * accelerator.num_processes,
|
|
)
|
|
|
|
# ==================== Prepare with Accelerator ====================
|
|
tae, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
|
|
tae, optimizer, train_dataloader, lr_scheduler
|
|
)
|
|
|
|
if args.use_ema:
|
|
ema_tae.to(accelerator.device)
|
|
|
|
vae.to(accelerator.device if not args.low_vram else "cpu", dtype=weight_dtype)
|
|
vae.requires_grad_(False)
|
|
|
|
# Recalculate
|
|
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
|
|
if overrode_max_train_steps:
|
|
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
|
|
args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
|
|
|
|
if accelerator.is_main_process:
|
|
tracker_config = dict(vars(args))
|
|
keys_to_pop = [k for k, v in tracker_config.items() if isinstance(v, list)]
|
|
for k in keys_to_pop:
|
|
tracker_config.pop(k)
|
|
accelerator.init_trackers(args.tracker_project_name, tracker_config)
|
|
|
|
def unwrap_model(model):
|
|
model = accelerator.unwrap_model(model)
|
|
model = model._orig_mod if is_compiled_module(model) else model
|
|
return model
|
|
|
|
# ==================== Training ====================
|
|
total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps
|
|
|
|
logger.info("***** Running TAE distillation training *****")
|
|
logger.info(f" Num examples = {len(train_dataset)}")
|
|
logger.info(f" Num Epochs = {args.num_train_epochs}")
|
|
logger.info(f" Instantaneous batch size per device = {args.train_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 = {args.max_train_steps}")
|
|
global_step = 0
|
|
first_epoch = 0
|
|
|
|
# Resume from checkpoint
|
|
if args.resume_from_checkpoint:
|
|
if args.resume_from_checkpoint != "latest":
|
|
path = os.path.basename(args.resume_from_checkpoint)
|
|
else:
|
|
dirs = os.listdir(args.output_dir)
|
|
dirs = [d for d in dirs if d.startswith("checkpoint")]
|
|
dirs = sorted(dirs, key=lambda x: int(x.split("-")[1]))
|
|
path = dirs[-1] if len(dirs) > 0 else None
|
|
|
|
if path is None:
|
|
accelerator.print(f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run.")
|
|
args.resume_from_checkpoint = None
|
|
initial_global_step = 0
|
|
else:
|
|
global_step = int(path.split("-")[1])
|
|
initial_global_step = global_step
|
|
pkl_path = os.path.join(os.path.join(args.output_dir, path), "sampler_pos_start.pkl")
|
|
if os.path.exists(pkl_path):
|
|
with open(pkl_path, 'rb') as file:
|
|
_, first_epoch = pickle.load(file)
|
|
else:
|
|
first_epoch = global_step // num_update_steps_per_epoch
|
|
accelerator.print(f"Resuming from checkpoint {path}")
|
|
accelerator.load_state(os.path.join(args.output_dir, path))
|
|
else:
|
|
initial_global_step = 0
|
|
|
|
progress_bar = PauseAwareTqdm(
|
|
range(0, args.max_train_steps), initial=initial_global_step, desc="Steps",
|
|
disable=not accelerator.is_local_main_process,
|
|
)
|
|
|
|
for epoch in range(first_epoch, args.num_train_epochs):
|
|
train_loss = 0.0
|
|
batch_sampler.sampler.generator = torch.Generator().manual_seed(args.seed + epoch)
|
|
for step, batch in enumerate(train_dataloader):
|
|
with accelerator.accumulate(tae):
|
|
pixel_values = batch["pixel_values"].to(weight_dtype) # [B, F, C, H, W]
|
|
pixel_values = rearrange(pixel_values, "b f c h w -> b c f h w")
|
|
bsz = pixel_values.shape[0]
|
|
|
|
if args.low_vram:
|
|
torch.cuda.empty_cache()
|
|
vae.to(accelerator.device)
|
|
|
|
# 1. Teacher target latent (frozen, no grad), encoded in mini batches
|
|
with torch.no_grad():
|
|
bs = args.vae_mini_batch
|
|
gt_latents_list = []
|
|
for i in range(0, bsz, bs):
|
|
pv_bs = pixel_values[i:i + bs]
|
|
encoded = vae.encode(pv_bs).latent_dist.mode()
|
|
gt_latents_list.append(encoded)
|
|
gt_latents = torch.cat(gt_latents_list, dim=0)
|
|
|
|
if args.low_vram:
|
|
vae.to('cpu')
|
|
torch.cuda.empty_cache()
|
|
|
|
# 2. Student forward: TAE encode -> decode
|
|
# Use the unwrapped model since encode/decode are not exposed by DDP.
|
|
z_tae = unwrap_model(tae).encode(pixel_values).latent_dist.mode()
|
|
decoded = unwrap_model(tae).decode(z_tae).sample
|
|
# Trim to the input temporal span in case the TAE padded extra frames.
|
|
decoded = decoded[:, :, :pixel_values.shape[2]]
|
|
|
|
# 3. Compute loss
|
|
pixel_loss = F.l1_loss(decoded.float(), pixel_values.float())
|
|
latent_loss = F.mse_loss(z_tae.float(), gt_latents.float())
|
|
loss = pixel_loss + args.latent_loss_weight * latent_loss
|
|
|
|
# Gather losses for logging
|
|
avg_loss = accelerator.gather(loss.repeat(args.train_batch_size)).mean()
|
|
train_loss += avg_loss.item() / args.gradient_accumulation_steps
|
|
|
|
# Backpropagate
|
|
accelerator.backward(loss)
|
|
if accelerator.sync_gradients:
|
|
if not args.use_deepspeed and not args.use_fsdp:
|
|
trainable_params_grads = [p.grad for p in trainable_params if p.grad is not None]
|
|
if trainable_params_grads:
|
|
trainable_params_total_norm = torch.norm(
|
|
torch.stack([torch.norm(g.detach(), 2) for g in trainable_params_grads]), 2
|
|
)
|
|
max_grad_norm = linear_decay(
|
|
args.max_grad_norm * args.initial_grad_norm_ratio,
|
|
args.max_grad_norm, args.abnormal_norm_clip_start, global_step
|
|
)
|
|
if trainable_params_total_norm / max_grad_norm > 5 and global_step > args.abnormal_norm_clip_start:
|
|
actual_max_grad_norm = max_grad_norm / min((trainable_params_total_norm / max_grad_norm), 10)
|
|
else:
|
|
actual_max_grad_norm = max_grad_norm
|
|
else:
|
|
actual_max_grad_norm = args.max_grad_norm
|
|
else:
|
|
actual_max_grad_norm = args.max_grad_norm
|
|
|
|
accelerator.clip_grad_norm_(trainable_params, actual_max_grad_norm)
|
|
optimizer.step()
|
|
lr_scheduler.step()
|
|
optimizer.zero_grad()
|
|
|
|
# Post-step actions
|
|
if accelerator.sync_gradients:
|
|
if args.use_ema:
|
|
ema_tae.step(tae.parameters())
|
|
progress_bar.update(1)
|
|
global_step += 1
|
|
accelerator.log({
|
|
"train_loss": train_loss,
|
|
"pixel_loss": pixel_loss.detach().item(),
|
|
"latent_loss": latent_loss.detach().item(),
|
|
}, step=global_step)
|
|
train_loss = 0.0
|
|
|
|
# Checkpointing
|
|
if global_step % args.checkpointing_steps == 0:
|
|
if args.use_deepspeed or args.use_fsdp or accelerator.is_main_process:
|
|
if args.checkpoints_total_limit is not None:
|
|
checkpoints = os.listdir(args.output_dir)
|
|
checkpoints = [d for d in checkpoints if d.startswith("checkpoint")]
|
|
checkpoints = sorted(checkpoints, key=lambda x: int(x.split("-")[1]))
|
|
if len(checkpoints) >= args.checkpoints_total_limit:
|
|
num_to_remove = len(checkpoints) - args.checkpoints_total_limit + 1
|
|
removing_checkpoints = checkpoints[0:num_to_remove]
|
|
logger.info(f"Removing {len(removing_checkpoints)} checkpoints")
|
|
for removing_checkpoint in removing_checkpoints:
|
|
shutil.rmtree(os.path.join(args.output_dir, removing_checkpoint))
|
|
|
|
gc.collect()
|
|
torch.cuda.empty_cache()
|
|
save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
|
|
# Keep the checkpoint out of the progress bar rate: a minute-long save would
|
|
# otherwise land in the next step's interval and be shown as a slow step. The
|
|
# save also stages the whole state in host RAM (safetensors materializes every
|
|
# tensor as bytes) and leaves the freed blocks in the allocator caches, so the
|
|
# cache flushes run inside the same window.
|
|
with progress_bar.paused():
|
|
accelerator.save_state(save_path)
|
|
gc.collect()
|
|
torch.cuda.empty_cache()
|
|
logger.info(f"Saved state to {save_path}")
|
|
|
|
# Validation
|
|
if args.validation_paths is not None and global_step % args.validation_steps == 0:
|
|
with progress_bar.paused():
|
|
if args.use_ema:
|
|
ema_tae.store(tae.parameters())
|
|
ema_tae.copy_to(tae.parameters())
|
|
log_validation(vae, tae, args, accelerator, weight_dtype, global_step)
|
|
if args.use_ema:
|
|
ema_tae.restore(tae.parameters())
|
|
|
|
logs = {
|
|
"step_loss": loss.detach().item(),
|
|
"pixel": pixel_loss.detach().item(),
|
|
"latent": latent_loss.detach().item(),
|
|
"lr": lr_scheduler.get_last_lr()[0],
|
|
}
|
|
progress_bar.set_postfix(**logs)
|
|
|
|
if global_step >= args.max_train_steps:
|
|
break
|
|
|
|
# Epoch-level validation
|
|
if args.validation_paths is not None and epoch % args.validation_epochs == 0:
|
|
with progress_bar.paused():
|
|
if args.use_ema:
|
|
ema_tae.store(tae.parameters())
|
|
ema_tae.copy_to(tae.parameters())
|
|
log_validation(vae, tae, args, accelerator, weight_dtype, global_step)
|
|
if args.use_ema:
|
|
ema_tae.restore(tae.parameters())
|
|
|
|
# Close the bar before the end-of-run checkpoint: tqdm keeps redrawing a live bar whenever
|
|
# something else writes to the console. PauseAwareTqdm.close() rebases the closing line onto
|
|
# the smoothed rate, so the worker warm-up and the first dataloader fetch do not dilute it.
|
|
progress_bar.close()
|
|
|
|
# Final save
|
|
accelerator.wait_for_everyone()
|
|
if args.use_ema and args.use_fsdp:
|
|
# Under FSDP every rank must write its own shards, and the shards only
|
|
# exist while the model is still wrapped, so this runs before the
|
|
# `unwrap_model` below.
|
|
ema_tae.copy_to(tae.parameters())
|
|
if accelerator.is_main_process:
|
|
tae_unwrapped = unwrap_model(tae)
|
|
if args.use_ema and not args.use_fsdp:
|
|
ema_tae.copy_to(tae_unwrapped.parameters())
|
|
|
|
if args.use_deepspeed or args.use_fsdp or accelerator.is_main_process:
|
|
gc.collect()
|
|
torch.cuda.empty_cache()
|
|
save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
|
|
accelerator.save_state(save_path)
|
|
logger.info(f"Saved state to {save_path}")
|
|
|
|
accelerator.end_training()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|