Files
aigc-apps-VideoX-Fun/scripts/wan2.2_fun/train_reward_lora.py
T

2438 lines
120 KiB
Python

"""Modified from VideoX-Fun/scripts/wan2.2_fun/train_lora.py
"""
#!/usr/bin/env python
# coding=utf-8
# Copyright 2024 The HuggingFace Inc. 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 json
import logging
import math
import os
import pickle
import random
import shutil
import sys
from contextlib import contextmanager
from typing import List, Optional, Union
import accelerate
import diffusers
import numpy as np
import torch
import torch.nn.functional as F
import torch.utils.checkpoint
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 import FlowMatchEulerDiscreteScheduler
from diffusers.optimization import get_scheduler
from diffusers.utils import check_min_version, deprecate, is_wandb_available
from diffusers.utils.torch_utils import is_compiled_module
from einops import rearrange
from omegaconf import OmegaConf
from packaging import version
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from tqdm.auto import tqdm
from transformers import AutoTokenizer
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 torch.utils.data import BatchSampler
import videox_fun.reward.reward_fn as reward_fn
from videox_fun.data import (ASPECT_RATIO_512, ASPECT_RATIO_RANDOM_CROP_512,
ASPECT_RATIO_RANDOM_CROP_PROB,
AspectRatioBatchImageVideoSampler,
ImageVideoDataset, ImageVideoSampler,
RandomSampler, TextDataset, get_closest_ratio,
get_random_mask)
from videox_fun.models import (AutoencoderKLWan, AutoencoderKLWan3_8,
AutoencoderKLWan3_8_1Steps,
AutoencoderKLWan_1Steps,
Wan2_2Transformer3DModel, WanT5EncoderModel)
from videox_fun.pipeline import Wan2_2I2VPipeline, Wan2_2Pipeline
from videox_fun.utils.lora_utils import create_network, merge_lora
from videox_fun.utils.tqdm_bar import PauseAwareTqdm
from videox_fun.utils.utils import get_image_to_video_latent, save_videos_grid
if is_wandb_available():
import wandb
def filter_kwargs(cls, kwargs):
import inspect
sig = inspect.signature(cls.__init__)
valid_params = set(sig.parameters.keys()) - {'self', 'cls'}
filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_params}
return filtered_kwargs
def resize_mask(mask, latent, process_first_frame_only=True):
latent_size = latent.size()
batch_size, channels, num_frames, height, width = mask.shape
if process_first_frame_only:
target_size = list(latent_size[2:])
target_size[0] = 1
first_frame_resized = F.interpolate(
mask[:, :, 0:1, :, :],
size=target_size,
mode='trilinear',
align_corners=False
)
target_size = list(latent_size[2:])
target_size[0] = target_size[0] - 1
if target_size[0] != 0:
remaining_frames_resized = F.interpolate(
mask[:, :, 1:, :, :],
size=target_size,
mode='trilinear',
align_corners=False
)
resized_mask = torch.cat([first_frame_resized, remaining_frames_resized], dim=2)
else:
resized_mask = first_frame_resized
else:
target_size = list(latent_size[2:])
resized_mask = F.interpolate(
mask,
size=target_size,
mode='trilinear',
align_corners=False
)
return resized_mask
# Will error if the minimal version of diffusers is not installed. Remove at your own risks.
check_min_version("0.18.0.dev0")
logger = get_logger(__name__, log_level="INFO")
def _get_t5_prompt_embeds(
tokenizer,
text_encoder,
prompt: Union[str, List[str]] = None,
num_videos_per_prompt: int = 1,
max_sequence_length: int = 512,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
):
prompt = [prompt] if isinstance(prompt, str) else prompt
batch_size = len(prompt)
text_inputs = tokenizer(
prompt,
padding="max_length",
max_length=max_sequence_length,
truncation=True,
add_special_tokens=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids
prompt_attention_mask = text_inputs.attention_mask
untruncated_ids = tokenizer(prompt, padding="longest", return_tensors="pt").input_ids
if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):
removed_text = tokenizer.batch_decode(untruncated_ids[:, max_sequence_length - 1 : -1])
logger.warning(
"The following part of your input was truncated because `max_sequence_length` is set to "
f" {max_sequence_length} tokens: {removed_text}"
)
seq_lens = prompt_attention_mask.gt(0).sum(dim=1).long()
prompt_embeds = text_encoder(text_input_ids.to(device), attention_mask=prompt_attention_mask.to(device))[0]
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
# duplicate text embeddings for each generation per prompt, using mps friendly method
_, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1)
prompt_embeds = prompt_embeds.view(batch_size * num_videos_per_prompt, seq_len, -1)
return [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
def encode_prompt(
tokenizer,
text_encoder,
prompt: Union[str, List[str]],
negative_prompt: Optional[Union[str, List[str]]] = None,
do_classifier_free_guidance: bool = True,
num_videos_per_prompt: int = 1,
prompt_embeds: Optional[torch.Tensor] = None,
negative_prompt_embeds: Optional[torch.Tensor] = None,
max_sequence_length: int = 512,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
):
r"""
Encodes the prompt into text encoder hidden states.
Args:
prompt (`str` or `List[str]`, *optional*):
prompt to be encoded
negative_prompt (`str` or `List[str]`, *optional*):
The prompt or prompts not to guide the image generation. If not defined, one has to pass
`negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
less than `1`).
do_classifier_free_guidance (`bool`, *optional*, defaults to `True`):
Whether to use classifier free guidance or not.
num_videos_per_prompt (`int`, *optional*, defaults to 1):
Number of videos that should be generated per prompt. torch device to place the resulting embeddings on
prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
provided, text embeddings will be generated from `prompt` input argument.
negative_prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
argument.
device: (`torch.device`, *optional*):
torch device
dtype: (`torch.dtype`, *optional*):
torch dtype
"""
# device = device or self._execution_device
prompt = [prompt] if isinstance(prompt, str) else prompt
if prompt is not None:
batch_size = len(prompt)
else:
batch_size = prompt_embeds.shape[0]
if prompt_embeds is None:
prompt_embeds = _get_t5_prompt_embeds(
tokenizer,
text_encoder,
prompt=prompt,
num_videos_per_prompt=num_videos_per_prompt,
max_sequence_length=max_sequence_length,
device=device,
dtype=dtype,
)
if do_classifier_free_guidance and negative_prompt_embeds is None:
negative_prompt = negative_prompt or ""
negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
if prompt is not None and type(prompt) is not type(negative_prompt):
raise TypeError(
f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
f" {type(prompt)}."
)
elif batch_size != len(negative_prompt):
raise ValueError(
f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
" the batch size of `prompt`."
)
negative_prompt_embeds = _get_t5_prompt_embeds(
tokenizer,
text_encoder,
prompt=negative_prompt,
num_videos_per_prompt=num_videos_per_prompt,
max_sequence_length=max_sequence_length,
device=device,
dtype=dtype,
)
return prompt_embeds, negative_prompt_embeds
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs
def prepare_extra_step_kwargs(scheduler, generator, eta):
# prepare extra kwargs for the scheduler step, since not all schedulers have the same signature
# eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers.
# eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502
# and should be between [0, 1]
import inspect
accepts_eta = "eta" in set(inspect.signature(scheduler.step).parameters.keys())
extra_step_kwargs = {}
if accepts_eta:
extra_step_kwargs["eta"] = eta
# check if the scheduler accepts generator
accepts_generator = "generator" in set(inspect.signature(scheduler.step).parameters.keys())
if accepts_generator:
extra_step_kwargs["generator"] = generator
return extra_step_kwargs
def log_validation(vae, text_encoder, tokenizer, transformer3d, network, config, args, accelerator, weight_dtype, global_step, other_transformer3d=None):
try:
logger.info("Running validation... ")
scheduler = FlowMatchEulerDiscreteScheduler(
**filter_kwargs(FlowMatchEulerDiscreteScheduler, OmegaConf.to_container(config['scheduler_kwargs']))
)
# Build dual-transformer setup based on boundary_type (aligned with train_lora.py).
# The non-trainable expert is ALREADY resident on GPU during training (FSDP-sharded and
# used by the denoise rollout), so reuse it via `other_transformer3d` instead of reloading
# an unsharded full copy from disk -- the latter would spike VRAM by a whole ~14B expert on top of the resident reward models.
if args.boundary_type == "full":
transformer3d_1 = accelerator.unwrap_model(transformer3d) if type(transformer3d).__name__ == 'DistributedDataParallel' else transformer3d
transformer3d_2 = None
else:
if other_transformer3d is None:
raise ValueError("boundary_type != 'full' requires passing the resident other expert via `other_transformer3d`.")
if args.boundary_type == "low":
# transformer3d is the trained low-noise expert; other_transformer3d is the resident high-noise one.
transformer3d_1 = accelerator.unwrap_model(transformer3d) if type(transformer3d).__name__ == 'DistributedDataParallel' else transformer3d
transformer3d_2 = other_transformer3d
else:
# transformer3d is the trained high-noise expert; other_transformer3d is the resident low-noise one.
transformer3d_1 = other_transformer3d
transformer3d_2 = accelerator.unwrap_model(transformer3d) if type(transformer3d).__name__ == 'DistributedDataParallel' else transformer3d
if args.train_mode != "normal":
pipeline = Wan2_2I2VPipeline(
vae=accelerator.unwrap_model(vae).to(weight_dtype),
text_encoder=accelerator.unwrap_model(text_encoder),
tokenizer=tokenizer,
transformer=transformer3d_1,
transformer_2=transformer3d_2,
scheduler=scheduler,
)
else:
pipeline = Wan2_2Pipeline(
vae=accelerator.unwrap_model(vae).to(weight_dtype),
text_encoder=accelerator.unwrap_model(text_encoder),
tokenizer=tokenizer,
transformer=transformer3d_1,
transformer_2=transformer3d_2,
scheduler=scheduler,
)
pipeline = pipeline.to(accelerator.device)
if args.seed is None:
generator = None
else:
rank_seed = args.seed + accelerator.process_index
generator = torch.Generator(device=accelerator.device).manual_seed(rank_seed)
logger.info(f"Rank {accelerator.process_index} using seed: {rank_seed}")
for i in range(len(args.validation_prompts)):
with torch.no_grad():
if args.train_mode != "normal":
with torch.autocast("cuda", dtype=weight_dtype):
video_length = int((args.video_sample_n_frames - 1) // vae.config.temporal_compression_ratio * vae.config.temporal_compression_ratio) + 1 if args.video_sample_n_frames != 1 else 1
input_video, input_video_mask, _ = get_image_to_video_latent(None, None, video_length=video_length, sample_size=[args.video_sample_size, args.video_sample_size])
sample = pipeline(
args.validation_prompts[i],
num_frames = video_length,
negative_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
height = args.video_sample_size,
width = args.video_sample_size,
generator = generator,
video = input_video,
mask_video = input_video_mask,
num_inference_steps = args.num_inference_steps,
guidance_scale = args.guidance_scale,
boundary = config['transformer_additional_kwargs'].get('boundary', 0.900)
).videos
os.makedirs(os.path.join(args.output_dir, "sample"), exist_ok=True)
save_videos_grid(sample, os.path.join(args.output_dir, f"sample/sample-{global_step}-rank{accelerator.process_index}-image-{i}.mp4"))
else:
with torch.autocast("cuda", dtype=weight_dtype):
sample = pipeline(
args.validation_prompts[i],
num_frames = args.video_sample_n_frames,
negative_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
height = args.video_sample_size,
width = args.video_sample_size,
generator = generator,
num_inference_steps = args.num_inference_steps,
guidance_scale = args.guidance_scale,
boundary = config['transformer_additional_kwargs'].get('boundary', 0.900)
).videos
os.makedirs(os.path.join(args.output_dir, "sample"), exist_ok=True)
save_videos_grid(sample, os.path.join(args.output_dir, f"sample/sample-{global_step}-rank{accelerator.process_index}-image-{i}.mp4"))
del pipeline
# NOTE: transformer3d_1/transformer3d_2 may reference the *shared* training experts
# (via other_transformer3d), so they must NOT be deleted here.
gc.collect()
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
vae.to(accelerator.device if not args.low_vram else "cpu", dtype=weight_dtype)
transformer3d.to(accelerator.device, dtype=weight_dtype)
except Exception as e:
gc.collect()
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
print(f"Eval error with info {e}")
return None
def parse_args():
parser = argparse.ArgumentParser(description="Simple example of a training script.")
parser.add_argument(
"--pretrained_model_name_or_path",
type=str,
default=None,
required=True,
help="Path to pretrained model or model identifier from huggingface.co/models.",
)
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 from huggingface.co/models, 'e.g.' fp16",
)
parser.add_argument(
"--validation_prompts",
type=str,
default=None,
nargs="+",
help=("A set of prompts evaluated every `--validation_epochs` and logged to `--report_to`."),
)
parser.add_argument(
"--output_dir",
type=str,
default="sd-model-finetuned",
help="The output directory where the model predictions and checkpoints will be written.",
)
parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.")
parser.add_argument(
"--use_came",
action="store_true",
help="whether to use came",
)
parser.add_argument(
"--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader."
)
parser.add_argument(
"--vae_mini_batch", type=int, default=32, help="mini batch size for vae."
)
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 (for DiT) to save memory at the expense of slower backward pass.",
)
parser.add_argument(
"--vae_gradient_checkpointing",
action="store_true",
help="Whether or not to use gradient checkpointing (for VAE) to save memory at the expense of slower backward pass.",
)
parser.add_argument(
"--use_1steps_vae",
action="store_true",
help="Use the single-pass (no feat_cache) variant of the configured Wan VAE (AutoencoderKLWan -> AutoencoderKLWan_1Steps, AutoencoderKLWan3_8 -> AutoencoderKLWan3_8_1Steps) instead of the chunked causal VAE. Numerically equivalent, but decodes the whole latent in one forward so gradient checkpointing is applied per decoder block cleanly and is not limited to the first 3 latent frames.",
)
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",
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=500, 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(
"--allow_tf32",
action="store_true",
help=(
"Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see"
" https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices"
),
)
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. Must be a branch, tag or git identifier of the local or"
" remote repository specified with --pretrained_model_name_or_path."
),
)
parser.add_argument(
"--dataloader_num_workers",
type=int,
default=0,
help=(
"Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process."
),
)
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("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.")
parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.")
parser.add_argument(
"--hub_model_id",
type=str,
default=None,
help="The name of the repository to keep in sync with the local `output_dir`.",
)
parser.add_argument(
"--logging_dir",
type=str,
default="logs",
help=(
"[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to"
" *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***."
),
)
parser.add_argument(
"--mixed_precision",
type=str,
default=None,
choices=["no", "fp16", "bf16"],
help=(
"Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >="
" 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the"
" flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config."
),
)
parser.add_argument(
"--report_to",
type=str,
default="tensorboard",
help=(
'The integration to report the results and logs to. Supported platforms are `"tensorboard"`'
' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.'
),
)
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. These checkpoints are only suitable for resuming"
" training using `--resume_from_checkpoint`."
),
)
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. Use a path saved by"
' `--checkpointing_steps`, or `"latest"` to automatically select the last available 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="text2image-fine-tune",
help=(
"The `project_name` argument passed to Accelerator.init_trackers for"
" more information see https://huggingface.co/docs/accelerate/v0.17.0/en/package_reference/accelerator#accelerate.Accelerator"
),
)
parser.add_argument(
"--rank",
type=int,
default=128,
help=("The dimension of the LoRA update matrices."),
)
parser.add_argument(
"--network_alpha",
type=int,
default=64,
help=("The dimension of the LoRA update matrices."),
)
parser.add_argument(
"--train_text_encoder",
action="store_true",
help="Whether to train the text encoder. If set, the text encoder should be float32 precision.",
)
parser.add_argument(
"--config_path",
type=str,
default=None,
help=(
"The config of the model in training."
),
)
parser.add_argument(
"--transformer_path",
type=str,
default=None,
help=("If you want to load the weight from other transformers, input its path."),
)
parser.add_argument(
"--vae_path",
type=str,
default=None,
help=("If you want to load the weight from other vaes, input its path."),
)
parser.add_argument("--save_state", action="store_true", help="Whether or not to save state.")
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(
"--boundary_type",
type=str,
default="low",
help=(
'The training boundary type for dual-Transformer architecture. '
'Support `"low"` (train low-noise model), `"high"` (train high-noise model), '
'and `"full"` (single model training like 5B).'
),
)
parser.add_argument(
"--lora_skip_name",
type=str,
default=None,
help=("The module is not trained in loras. "),
)
parser.add_argument(
"--target_name",
type=str,
default=None,
help=("The module is trained in loras. "),
)
parser.add_argument(
"--train_mode",
type=str,
default="inpaint",
help=(
'The training mode. Support `"normal"` (T2V), '
'`"inpaint"` (I2V), and `"ti2v"` (Text-Image-to-Video, 5B only).'
),
)
parser.add_argument(
"--force_t2v",
action="store_true",
help=(
'When `train_mode` is `"inpaint"` (I2V) or `"ti2v"`, train as pure T2V while '
'keeping the model architecture: I2V/inpaint receives an all-zero `y` condition, '
'and TI2V skips mask blending / per-token timesteps entirely. Only prompts are '
'loaded (TextDataset), mirroring train_grpo_lora.py.'
),
)
parser.add_argument(
"--enable_text_encoder_in_dataloader", action="store_true", help="Whether or not to use text encoder in dataloader."
)
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_frame_crop", action="store_true", help="Whether enable random frame 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(
"--training_with_video_token_length", action="store_true", help="The training stage of the model in training.",
)
parser.add_argument(
"--token_sample_size",
type=int,
default=512,
help="Sample size of the token.",
)
parser.add_argument(
"--fix_sample_size",
nargs=2, type=int, default=None,
help="Fix Sample size [height, width] when using bucket and collate_fn."
)
parser.add_argument(
'--tokenizer_max_length',
type=int,
default=512,
help='Max length of tokenizer'
)
parser.add_argument(
"--train_data_meta",
type=str,
default=None,
required=True,
help="The path to the training data annotation file (.json).",
)
parser.add_argument(
"--train_data_dir",
type=str,
default=None,
help="Root directory for resolving relative file paths in the training data.",
)
parser.add_argument(
"--video_sample_size",
type=int,
default=512,
help="Sample size (height and width) of the video for training.",
)
parser.add_argument(
"--image_sample_size",
type=int,
default=512,
help="Sample size of the image.",
)
parser.add_argument(
"--video_sample_stride",
type=int,
default=4,
help="Sample stride of the video.",
)
parser.add_argument(
"--video_sample_n_frames",
type=int,
default=17,
help="Number of frames to sample from the video.",
)
parser.add_argument(
"--video_repeat",
type=int,
default=0,
help="Number of times to repeat video entries in the dataset.",
)
parser.add_argument(
'--eta',
type=float,
default=0.0,
help='eta parameter for the DDIM sampler. this controls the amount of noise injected into the sampling process, '
'with 0.0 being fully deterministic and 1.0 being equivalent to the DDPM sampler.'
)
parser.add_argument(
"--guidance_scale",
type=float,
default=6.0,
help="The classifier-free diffusion guidance."
)
parser.add_argument(
"--num_inference_steps",
type=int,
default=50,
help="The number of denoising steps in training and validation."
)
parser.add_argument(
"--num_decoded_latents",
type=int,
default=3,
help="The number of latents to be decoded."
)
parser.add_argument(
"--num_sampled_frames",
type=int,
default=None,
help="The number of sampled frames for the reward function."
)
parser.add_argument(
"--num_reward_frames",
type=int,
default=None,
help="Randomly select this many decoded frames for reward evaluation. None = use all."
)
parser.add_argument(
"--reward_fn",
type=str,
default="HPSReward",
help='The reward function. Comma-separated names (e.g. "HPSv3Reward,VideoAlignReward") enable multi-reward training; the rewards\' differentiable losses are combined by a weighted sum.'
)
parser.add_argument(
"--reward_fn_kwargs",
type=str,
default=None,
help='The keyword arguments of the reward function. Flat JSON dict for a single reward; nested JSON dict keyed by reward name for multiple rewards, e.g. \'{"HPSv3Reward": {"max_reward": 10}, "VideoAlignReward": {}}\'. '
)
parser.add_argument(
"--multi_reward_weights",
type=str,
default=None,
help='JSON dict of per-reward loss weights for multi-reward, e.g. \'{"HPSv3Reward": 0.5, "VideoAlignReward": 0.5}\'. Normalized to sum to 1; if omitted with multiple rewards, equal weights are used. Ignored for a single reward.'
)
parser.add_argument(
"--first_frame_mse_weight",
type=float,
default=0.0,
help="Weight for first-frame latent MSE loss in inpaint mode. "
"Anchors the denoised first frame to the condition image. 0 = disabled.",
)
parser.add_argument(
"--backprop",
action="store_true",
default=False,
help="Whether to use the reward backprop training mode.",
)
parser.add_argument(
"--backprop_step_list",
nargs="+",
type=int,
default=None,
help="The preset step list for reward backprop. If provided, overrides `backprop_strategy`."
)
parser.add_argument(
"--backprop_strategy",
choices=["last", "tail", "uniform", "random"],
default="last",
help="The strategy for reward backprop."
)
parser.add_argument(
"--stop_latent_model_input_gradient",
action="store_true",
default=False,
help="Whether to stop the gradient of the latents during reward backprop.",
)
parser.add_argument(
"--backprop_random_start_step",
type=int,
default=0,
help="The random start step for reward backprop. Only used when `backprop_strategy` is random."
)
parser.add_argument(
"--backprop_random_end_step",
type=int,
default=50,
help="The random end step for reward backprop. Only used when `backprop_strategy` is random."
)
parser.add_argument(
"--backprop_num_steps",
type=int,
default=5,
help="The number of steps for backprop. Only used when `backprop_strategy` is tail/uniform/random."
)
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
# default to using the same revision for the non-ema model if not specified
if args.non_ema_revision is None:
args.non_ema_revision = args.revision
return args
def main():
args = parse_args()
if args.report_to == "wandb" and args.hub_token is not None:
raise ValueError(
"You cannot use both --report_to=wandb and --hub_token due to a security risk of exposing your token."
" Please use `huggingface-cli login` to authenticate with the Hub."
)
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. Please make sure to"
" use `--variant=non_ema` instead."
),
)
logging_dir = os.path.join(args.output_dir, args.logging_dir)
config = OmegaConf.load(args.config_path)
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
print(f"Using DeepSpeed Zero stage: {zero_stage}")
args.use_deepspeed = True
if zero_stage == 3:
print(f"Auto set save_state to True because 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 is ShardingStrategy.FULL_SHARD:
fsdp_stage = 3
elif fsdp_plugin.sharding_strategy is None: # The fsdp_plugin.sharding_strategy is None in FSDP 2.
fsdp_stage = 3
elif fsdp_plugin.sharding_strategy is ShardingStrategy.SHARD_GRAD_OP:
fsdp_stage = 2
else:
fsdp_stage = 0
print(f"Using FSDP stage: {fsdp_stage}")
args.use_fsdp = True
if fsdp_stage == 3:
print(f"Auto set save_state to True because fsdp_stage == 3")
args.save_state = True
else:
zero_stage = 0
fsdp_stage = 0
print("DeepSpeed is not enabled.")
if accelerator.is_main_process:
writer = SummaryWriter(log_dir=logging_dir)
# Make one log on every process with the configuration for debugging.
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%m/%d/%Y %H:%M:%S",
level=logging.INFO,
)
logger.info(accelerator.state, main_process_only=False)
if accelerator.is_local_main_process:
datasets.utils.logging.set_verbosity_warning()
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()
# Sanity check for backpropagation
if args.backprop:
if args.backprop_step_list is not None:
logger.warning(
f"The backprop_strategy {args.backprop_strategy} will be ignored "
f"when using backprop_step_list {args.backprop_step_list}."
)
assert any(step <= args.num_inference_steps - 1 for step in args.backprop_step_list)
else:
if args.backprop_strategy in set(["tail", "uniform", "random"]):
assert args.backprop_num_steps <= args.num_inference_steps - 1
if args.backprop_strategy == "random":
assert args.backprop_random_start_step <= args.backprop_random_end_step
assert args.backprop_random_end_step <= args.num_inference_steps - 1
# If passed along, set the training seed now.
if args.seed is not None:
set_seed(args.seed, device_specific=True)
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
# Handle the repository creation
if accelerator.is_main_process:
if args.output_dir is not None:
os.makedirs(args.output_dir, exist_ok=True)
# For mixed precision training we cast all non-trainable weigths (vae, non-lora text_encoder and non-lora transformer3d) to half-precision
# as these weights are only used for inference, keeping weights in full precision is not required.
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 scheduler, tokenizer and models.
# TODO: support other noise schedulers (FlowUniPCMultistepScheduler and FlowDPMSolverMultistepScheduler)
noise_scheduler = FlowMatchEulerDiscreteScheduler(
**filter_kwargs(FlowMatchEulerDiscreteScheduler, OmegaConf.to_container(config['scheduler_kwargs']))
)
# Get Tokenizer
tokenizer = AutoTokenizer.from_pretrained(
os.path.join(args.pretrained_model_name_or_path, config['text_encoder_kwargs'].get('tokenizer_subpath', 'tokenizer')),
)
def deepspeed_zero_init_disabled_context_manager():
"""
returns either a context list that includes one that will disable zero.Init or an empty context list
"""
deepspeed_plugin = AcceleratorState().deepspeed_plugin if accelerate.state.is_initialized() else None
if deepspeed_plugin is None:
return []
return [deepspeed_plugin.zero3_init_context_manager(enable=False)]
# Currently Accelerate doesn't know how to handle multiple models under Deepspeed ZeRO stage 3.
# For this to work properly all models must be run through `accelerate.prepare`. But accelerate
# will try to assign the same optimizer with the same weights to all models during
# `deepspeed.initialize`, which of course doesn't work.
#
# For now the following workaround will partially support Deepspeed ZeRO-3, by excluding the 2
# frozen models from being partitioned during `zero.Init` which gets called during
# `from_pretrained` So CLIPTextModel and AutoencoderKL will not enjoy the parameter sharding
# across multiple gpus and only UNet2DConditionModel will get ZeRO sharded.
with ContextManagers(deepspeed_zero_init_disabled_context_manager()):
# Get Text encoder
text_encoder = WanT5EncoderModel.from_pretrained(
os.path.join(args.pretrained_model_name_or_path, config['text_encoder_kwargs'].get('text_encoder_subpath', 'text_encoder')),
additional_kwargs=OmegaConf.to_container(config['text_encoder_kwargs']),
low_cpu_mem_usage=True,
torch_dtype=weight_dtype,
)
text_encoder = text_encoder.eval()
# Get Vae
Chosen_AutoencoderKL = {
"AutoencoderKLWan": AutoencoderKLWan,
"AutoencoderKLWan3_8": AutoencoderKLWan3_8
}[config['vae_kwargs'].get('vae_type', 'AutoencoderKLWan')]
if args.use_1steps_vae:
# Opt-in single-pass VAE: same weights / interface, whole-latent decode
# with per-block gradient checkpointing (see --use_1steps_vae help).
Chosen_AutoencoderKL = {
AutoencoderKLWan: AutoencoderKLWan_1Steps,
AutoencoderKLWan3_8: AutoencoderKLWan3_8_1Steps,
}[Chosen_AutoencoderKL]
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()
# Some reward models comes from `transformers`. We also need `unset_hf_deepspeed_config` to prevent the
# model from being partitioned during `zero.Init`.
from transformers.integrations.deepspeed import \
unset_hf_deepspeed_config
unset_hf_deepspeed_config()
# loss function(s): a single reward, or multiple comma-separated rewards combined by a
# weighted sum of their differentiable losses. Multi-reward keeps every reward model on GPU
# at once (the whole graph is backpropagated together), so it costs extra VRAM; enabling
# --gradient_checkpointing mitigates this per reward model.
reward_fn_names = [name.strip() for name in args.reward_fn.split(',') if name.strip()]
is_multi_reward = len(reward_fn_names) > 1
reward_fn_kwargs_all = {}
if args.reward_fn_kwargs is not None:
parsed_kwargs = json.loads(args.reward_fn_kwargs)
if isinstance(parsed_kwargs, dict) and (is_multi_reward or any(n in parsed_kwargs for n in reward_fn_names)):
reward_fn_kwargs_all = parsed_kwargs
else:
reward_fn_kwargs_all = {reward_fn_names[0]: parsed_kwargs}
multi_reward_weights = {}
if args.multi_reward_weights is not None:
multi_reward_weights = json.loads(args.multi_reward_weights)
if is_multi_reward:
if not multi_reward_weights:
multi_reward_weights = {n: 1.0 / len(reward_fn_names) for n in reward_fn_names}
else:
total = sum(multi_reward_weights.values())
multi_reward_weights = {n: multi_reward_weights.get(n, 0.0) / total for n in reward_fn_names}
# The main process builds each reward first so HF weight download happens once.
if accelerator.is_main_process:
for _name in reward_fn_names:
getattr(reward_fn, _name)(device="cpu", dtype=weight_dtype, **reward_fn_kwargs_all.get(_name, {}))
accelerator.wait_for_everyone()
loss_fns = {}
for _name in reward_fn_names:
loss_fns[_name] = getattr(reward_fn, _name)(
device=accelerator.device, dtype=weight_dtype, **reward_fn_kwargs_all.get(_name, {})
)
# Enable gradient checkpointing for the reward model to save VRAM during differentiable training.
if args.gradient_checkpointing and hasattr(loss_fns[_name], 'inferencer'):
if hasattr(loss_fns[_name].inferencer.model, 'gradient_checkpointing_enable'):
loss_fns[_name].inferencer.model.gradient_checkpointing_enable()
logger.info(f"Enabled gradient checkpointing for reward model {_name}.")
if is_multi_reward:
logger.info(f"Multi-reward enabled: {reward_fn_names} with weights {multi_reward_weights}")
else:
logger.info(f"Single reward function: {reward_fn_names[0]}")
# Get Transformer
# To unify the LoRA training for the 5/14B (including the high and low noise model),
# the script contains numerous transformer3d variables.
# `transformer3d` refers to the (sub)model currently being trained.
# `low_transformer3d` is an instance of the 14B low noise model.
# `high_transformer3d` is an instance of the 14B high noise model.
# `local_transformer` refers to the (sub)model actually used during the denoising process.
if args.boundary_type == "low" or args.boundary_type == "full":
sub_path = config['transformer_additional_kwargs'].get('transformer_low_noise_model_subpath', 'transformer')
else:
sub_path = config['transformer_additional_kwargs'].get('transformer_high_noise_model_subpath', 'transformer')
if args.boundary_type != "full":
# 14B dual-expert: BOTH experts must always be loaded from their own subpath,
# independent of which one is trainable. `sub_path` above selects the *trainable*
# expert, so it must NOT be reused for `low_transformer3d`: when boundary_type=="high"
# sub_path points at the high-noise model, and loading the low-noise expert from it
# makes the denoising loop run the WRONG expert on the t<boundary refinement steps
# (line ~2119), producing noisy / oversaturated samples even at step 0.
low_sub_path = config['transformer_additional_kwargs'].get('transformer_low_noise_model_subpath', 'transformer')
high_sub_path = config['transformer_additional_kwargs'].get('transformer_high_noise_model_subpath', 'transformer')
low_transformer3d = Wan2_2Transformer3DModel.from_pretrained(
os.path.join(args.pretrained_model_name_or_path, low_sub_path),
transformer_additional_kwargs=OmegaConf.to_container(config['transformer_additional_kwargs']),
low_cpu_mem_usage=True
).to(weight_dtype)
high_transformer3d = Wan2_2Transformer3DModel.from_pretrained(
os.path.join(args.pretrained_model_name_or_path, high_sub_path),
transformer_additional_kwargs=OmegaConf.to_container(config['transformer_additional_kwargs']),
low_cpu_mem_usage=True
).to(weight_dtype)
else:
# 5B
transformer3d = Wan2_2Transformer3DModel.from_pretrained(
os.path.join(args.pretrained_model_name_or_path, sub_path),
transformer_additional_kwargs=OmegaConf.to_container(config['transformer_additional_kwargs']),
low_cpu_mem_usage=True
).to(weight_dtype)
# Freeze vae and text_encoder and set transformer3d to trainable
vae.requires_grad_(False)
text_encoder.requires_grad_(False)
if args.boundary_type != "full":
low_transformer3d.requires_grad_(False)
high_transformer3d.requires_grad_(False)
if args.boundary_type == "low":
transformer3d = low_transformer3d
else:
transformer3d = high_transformer3d
else:
transformer3d.requires_grad_(False)
# Lora will work with this...
network = create_network(
1.0,
args.rank,
args.network_alpha,
text_encoder,
transformer3d,
neuron_dropout=None,
target_name=args.target_name,
skip_name=args.lora_skip_name,
)
network = network.to(weight_dtype)
network.apply_to(text_encoder, transformer3d, args.train_text_encoder and not args.training_with_video_token_length, True)
if args.transformer_path is not None:
print(f"From checkpoint: {args.transformer_path}")
if args.transformer_path.endswith("safetensors"):
from safetensors.torch import load_file, safe_open
state_dict = load_file(args.transformer_path)
else:
state_dict = torch.load(args.transformer_path, map_location="cpu")
state_dict = state_dict["state_dict"] if "state_dict" in state_dict else state_dict
m, u = transformer3d.load_state_dict(state_dict, strict=False)
print(f"missing keys: {len(m)}, unexpected keys: {len(u)}")
assert len(u) == 0
if args.vae_path is not None:
print(f"From checkpoint: {args.vae_path}")
if args.vae_path.endswith("safetensors"):
from safetensors.torch import load_file, safe_open
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"missing keys: {len(m)}, unexpected keys: {len(u)}")
assert len(u) == 0
# `accelerate` 0.16.0 will have better support for customized saving
if version.parse(accelerate.__version__) >= version.parse("0.16.0"):
# create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format
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, f"lora_diffusion_pytorch_model.safetensors")
network_state_dict = {}
for key in accelerate_state_dict:
if "network" in key:
network_state_dict[key.replace("network.", "")] = accelerate_state_dict[key].to(weight_dtype)
save_file(network_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)
def load_model_hook(models, input_dir):
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:
# create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format
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, f"lora_diffusion_pytorch_model.safetensors")
network_state_dict = {}
for key in accelerate_state_dict:
if "network" in key:
network_state_dict[key.replace("network.", "")] = accelerate_state_dict[key].to(weight_dtype)
save_file(network_state_dict, safetensor_save_path, metadata={"format": "pt"})
if not args.use_deepspeed:
for _ in range(len(weights)):
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):
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:
transformer3d.enable_gradient_checkpointing()
# When training the high noise reward lora, the low noise transformer is still in the computation graph.
if args.boundary_type == "high":
low_transformer3d.enable_gradient_checkpointing()
if args.vae_gradient_checkpointing:
# Chunked causal VAE decodes autoregressively via feat_cache, so checkpointing
# is only safe while the cache stays unused (num_decoded_latents <= 3); beyond
# that the per-frame cache threading fights with activation recomputation.
# The single-pass VAE (--use_1steps_vae) has no cache and checkpoints per
# decoder block, so it is exempt from this limit.
if not args.use_1steps_vae and args.num_decoded_latents > 3:
raise ValueError(
"vae_gradient_checkpointing with the chunked causal VAE only supports "
"num_decoded_latents <= 3 (feat_cache kicks in beyond that). Pass "
"--use_1steps_vae to lift this limit."
)
vae.enable_gradient_checkpointing()
# Enable TF32 for faster training on Ampere GPUs,
# cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices
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
)
# Initialize the optimizer
if args.use_8bit_adam:
try:
import bitsandbytes as bnb
except ImportError:
raise ImportError(
"Please install bitsandbytes to use 8-bit Adam. You can do so by running `pip install bitsandbytes`"
)
optimizer_cls = bnb.optim.AdamW8bit
elif args.use_came:
try:
from came_pytorch import CAME
except Exception:
raise ImportError(
"Please install came_pytorch to use CAME. You can do so by running `pip install came_pytorch`"
)
optimizer_cls = CAME
else:
optimizer_cls = torch.optim.AdamW
logging.info("Add network parameters")
trainable_params = list(filter(lambda p: p.requires_grad, network.parameters()))
trainable_params_optim = network.prepare_optimizer_params(args.learning_rate / 2, args.learning_rate, args.learning_rate)
if args.use_came:
optimizer = optimizer_cls(
trainable_params_optim,
lr=args.learning_rate,
# weight_decay=args.adam_weight_decay,
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,
)
# Get the training dataset
sample_n_frames_bucket_interval = vae.config.temporal_compression_ratio
spatial_compression_ratio = vae.config.spatial_compression_ratio
if args.fix_sample_size is not None and args.enable_bucket:
args.video_sample_size = max(max(args.fix_sample_size), args.video_sample_size)
args.image_sample_size = max(max(args.fix_sample_size), args.image_sample_size)
args.training_with_video_token_length = False
args.random_hw_adapt = False
# For pure T2V ("normal") reward backprop, only prompts are needed: rollouts are
# generated from pure noise (torch.randn latent, shape derived from args) and scored
# by the reward model, so no real media is loaded. This mirrors train_grpo_lora.py's
# text_only branch. I2V/inpaint (train_mode != "normal") still uses ImageVideoDataset
# for the conditioning frames/masks, UNLESS --force_t2v is set: then we keep the I2V/
# ti2v model architecture but train as pure T2V (all-zero condition), so only prompts
# are needed and the dataset degenerates to TextDataset as well.
# NOTE: in text-only mode --train_data_meta must be a JSON array of {"text": ...}.
text_only = (args.train_mode == "normal") or args.force_t2v
if text_only:
train_dataset = TextDataset(
args.train_data_meta,
text_drop_ratio=0.0,
)
else:
train_dataset = ImageVideoDataset(
args.train_data_meta, args.train_data_dir,
video_sample_size=args.video_sample_size, video_sample_stride=args.video_sample_stride, video_sample_n_frames=args.video_sample_n_frames,
video_repeat=args.video_repeat,
image_sample_size=args.image_sample_size,
enable_bucket=args.enable_bucket, enable_inpaint=True if args.train_mode != "normal" else False,
)
def worker_init_fn(_seed):
_seed = _seed * 256
def _worker_init_fn(worker_id):
print(f"worker_init_fn with {_seed + worker_id}")
np.random.seed(_seed + worker_id)
random.seed(_seed + worker_id)
return _worker_init_fn
if text_only:
# Prompt-only pipeline (pure T2V): the batch carries just text. The latent shape
# is derived from args inside the training loop and prompts are re-encoded there
# via encode_prompt(train_prompt), so no bucket / aspect-ratio logic is needed.
def collate_fn(examples):
new_examples = {}
new_examples["text"] = [example["text"] for example in examples]
return new_examples
batch_sampler_generator = torch.Generator().manual_seed(args.seed)
batch_sampler = BatchSampler(
RandomSampler(train_dataset, generator=batch_sampler_generator),
batch_size=args.train_batch_size, drop_last=True,
)
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)
)
elif 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):
def get_length_to_frame_num(token_length):
if args.image_sample_size > args.video_sample_size:
sample_sizes = list(range(args.video_sample_size, args.image_sample_size + 1, 128))
if sample_sizes[-1] != args.image_sample_size:
sample_sizes.append(args.image_sample_size)
else:
sample_sizes = [args.image_sample_size]
length_to_frame_num = {
sample_size: min(token_length / sample_size / sample_size, args.video_sample_n_frames) // sample_n_frames_bucket_interval * sample_n_frames_bucket_interval + 1 for sample_size in sample_sizes
}
return length_to_frame_num
def get_random_downsample_ratio(sample_size, image_ratio=[],
all_choices=False, rng=None):
def _create_special_list(length):
if length == 1:
return [1.0]
if length >= 2:
first_element = 0.90
remaining_sum = 1.0 - first_element
other_elements_value = remaining_sum / (length - 1)
special_list = [first_element] + [other_elements_value] * (length - 1)
return special_list
if sample_size >= 1536:
number_list = [1, 1.25, 1.5, 2, 2.5, 3] + image_ratio
elif sample_size >= 1024:
number_list = [1, 1.25, 1.5, 2] + image_ratio
elif sample_size >= 768:
number_list = [1, 1.25, 1.5] + image_ratio
elif sample_size >= 512:
number_list = [1] + image_ratio
else:
number_list = [1]
if all_choices:
return number_list
number_list_prob = np.array(_create_special_list(len(number_list)))
if rng is None:
return np.random.choice(number_list, p = number_list_prob)
else:
return rng.choice(number_list, p = number_list_prob)
# Get token length
target_token_length = args.video_sample_n_frames * args.token_sample_size * args.token_sample_size
length_to_frame_num = get_length_to_frame_num(target_token_length)
# Create new output
new_examples = {}
new_examples["target_token_length"] = target_token_length
new_examples["pixel_values"] = []
new_examples["text"] = []
# Used in Inpaint mode
if args.train_mode != "normal":
new_examples["mask_pixel_values"] = []
new_examples["mask"] = []
new_examples["clip_pixel_values"] = []
# Get downsample ratio in image and videos
pixel_value = examples[0]["pixel_values"]
data_type = examples[0]["data_type"]
f, h, w, c = np.shape(pixel_value)
if data_type == 'image':
random_downsample_ratio = 1 if not args.random_hw_adapt else get_random_downsample_ratio(args.image_sample_size, image_ratio=[args.image_sample_size / args.video_sample_size])
aspect_ratio_sample_size = {key : [x / 512 * args.image_sample_size / random_downsample_ratio for x in ASPECT_RATIO_512[key]] for key in ASPECT_RATIO_512.keys()}
aspect_ratio_random_crop_sample_size = {key : [x / 512 * args.image_sample_size / random_downsample_ratio for x in ASPECT_RATIO_RANDOM_CROP_512[key]] for key in ASPECT_RATIO_RANDOM_CROP_512.keys()}
batch_video_length = args.video_sample_n_frames + sample_n_frames_bucket_interval
else:
if args.random_hw_adapt:
if args.training_with_video_token_length:
local_min_size = np.min(np.array([np.mean(np.array([np.shape(example["pixel_values"])[1], np.shape(example["pixel_values"])[2]])) for example in examples]))
# The video will be resized to a lower resolution than its own.
choice_list = [length for length in list(length_to_frame_num.keys()) if length < local_min_size * 1.25]
if len(choice_list) == 0:
choice_list = list(length_to_frame_num.keys())
local_video_sample_size = np.random.choice(choice_list)
batch_video_length = length_to_frame_num[local_video_sample_size]
random_downsample_ratio = args.video_sample_size / local_video_sample_size
else:
random_downsample_ratio = get_random_downsample_ratio(args.video_sample_size)
batch_video_length = args.video_sample_n_frames + sample_n_frames_bucket_interval
else:
random_downsample_ratio = 1
batch_video_length = args.video_sample_n_frames + sample_n_frames_bucket_interval
aspect_ratio_sample_size = {key : [x / 512 * args.video_sample_size / random_downsample_ratio for x in ASPECT_RATIO_512[key]] for key in ASPECT_RATIO_512.keys()}
aspect_ratio_random_crop_sample_size = {key : [x / 512 * args.video_sample_size / random_downsample_ratio for x in ASPECT_RATIO_RANDOM_CROP_512[key]] for key in ASPECT_RATIO_RANDOM_CROP_512.keys()}
if args.fix_sample_size is not None:
fix_sample_size = [int(x / spatial_compression_ratio / 2) * spatial_compression_ratio * 2 for x in args.fix_sample_size]
elif args.random_ratio_crop:
if rng is None:
random_sample_size = aspect_ratio_random_crop_sample_size[
np.random.choice(list(aspect_ratio_random_crop_sample_size.keys()), p = ASPECT_RATIO_RANDOM_CROP_PROB)
]
else:
random_sample_size = aspect_ratio_random_crop_sample_size[
rng.choice(list(aspect_ratio_random_crop_sample_size.keys()), p = ASPECT_RATIO_RANDOM_CROP_PROB)
]
random_sample_size = [int(x / spatial_compression_ratio / 2) * spatial_compression_ratio * 2 for x in random_sample_size]
else:
closest_size, closest_ratio = get_closest_ratio(h, w, ratios=aspect_ratio_sample_size)
closest_size = [int(x / spatial_compression_ratio / 2) * spatial_compression_ratio * 2 for x in closest_size]
min_example_length = min(
[example["pixel_values"].shape[0] for example in examples]
)
batch_video_length = int(min(batch_video_length, min_example_length))
# Magvae needs the number of frames to be 4n + 1.
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:
if args.fix_sample_size is not None:
# To 0~1
pixel_values = torch.from_numpy(example["pixel_values"]).permute(0, 3, 1, 2).contiguous()
pixel_values = pixel_values / 255.
# Get adapt hw for resize
fix_sample_size = list(map(lambda x: int(x), fix_sample_size))
transform = transforms.Compose([
transforms.Resize(fix_sample_size, interpolation=transforms.InterpolationMode.BILINEAR), # Image.BICUBIC
transforms.CenterCrop(fix_sample_size),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True),
])
elif args.random_ratio_crop:
# To 0~1
pixel_values = torch.from_numpy(example["pixel_values"]).permute(0, 3, 1, 2).contiguous()
pixel_values = pixel_values / 255.
# Get adapt hw for resize
b, c, h, w = pixel_values.size()
th, tw = random_sample_size
if th / tw > h / w:
nh = int(th)
nw = int(w / h * nh)
else:
nw = int(tw)
nh = int(h / w * nw)
transform = transforms.Compose([
transforms.Resize([nh, nw]),
transforms.CenterCrop([int(x) for x in random_sample_size]),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True),
])
else:
# To 0~1
pixel_values = torch.from_numpy(example["pixel_values"]).permute(0, 3, 1, 2).contiguous()
pixel_values = pixel_values / 255.
# Get adapt hw for resize
closest_size = list(map(lambda x: int(x), closest_size))
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), # Image.BICUBIC
transforms.CenterCrop(closest_size),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True),
])
new_examples["pixel_values"].append(transform(pixel_values)[:batch_video_length])
new_examples["text"].append(example["text"])
if args.train_mode != "normal":
mask = get_random_mask(new_examples["pixel_values"][-1].size(), image_start_only=True)
mask_pixel_values = new_examples["pixel_values"][-1] * (1 - mask)
# Wan 2.1 use 0 for masked pixels
# + torch.ones_like(new_examples["pixel_values"][-1]) * -1 * mask
new_examples["mask_pixel_values"].append(mask_pixel_values)
new_examples["mask"].append(mask)
clip_pixel_values = new_examples["pixel_values"][-1][0].permute(1, 2, 0).contiguous()
clip_pixel_values = (clip_pixel_values * 0.5 + 0.5) * 255
new_examples["clip_pixel_values"].append(clip_pixel_values)
# Limit the number of frames to the same
new_examples["pixel_values"] = torch.stack([example for example in new_examples["pixel_values"]])
if args.train_mode != "normal":
new_examples["mask_pixel_values"] = torch.stack([example for example in new_examples["mask_pixel_values"]])
new_examples["mask"] = torch.stack([example for example in new_examples["mask"]])
new_examples["clip_pixel_values"] = torch.stack([example for example in new_examples["clip_pixel_values"]])
# Encode prompts when enable_text_encoder_in_dataloader=True
if args.enable_text_encoder_in_dataloader:
prompt_ids = tokenizer(
new_examples['text'],
max_length=args.tokenizer_max_length,
padding="max_length",
add_special_tokens=True,
truncation=True,
return_tensors="pt"
)
encoder_hidden_states = text_encoder(
prompt_ids.input_ids
)[0]
new_examples['encoder_attention_mask'] = prompt_ids.attention_mask
new_examples['encoder_hidden_states'] = encoder_hidden_states
return new_examples
# DataLoaders creation:
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:
# DataLoaders creation:
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)
)
# Scheduler and math around the number of training steps.
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 everything with our `accelerator`.
transformer3d.network = network
transformer3d = transformer3d.to(dtype=weight_dtype)
transformer3d, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
transformer3d, optimizer, train_dataloader, lr_scheduler
)
# FSDP returns a new wrapper object; re-sync low/high_transformer3d references
# so that the training loop's `local_transformer` uses the FSDP-wrapped model.
if args.boundary_type != "full":
if args.boundary_type == "low":
low_transformer3d = transformer3d
else:
high_transformer3d = transformer3d
if fsdp_stage != 0 or zero_stage != 0:
from functools import partial
from videox_fun.dist import set_multi_gpus_devices, shard_model
shard_fn = partial(shard_model, device_id=accelerator.device, param_dtype=weight_dtype)
text_encoder = shard_fn(text_encoder)
shard_fn = partial(shard_model, device_id=accelerator.device, param_dtype=weight_dtype)
if args.boundary_type == "high":
low_transformer3d = shard_fn(low_transformer3d)
else:
high_transformer3d = shard_fn(high_transformer3d)
# Move text_encode and vae to gpu and cast to weight_dtype
vae.to(accelerator.device if not args.low_vram else "cpu", dtype=weight_dtype)
if args.boundary_type != "full":
if args.low_vram:
# In low_vram mode, only place the primary training transformer on GPU.
# The secondary model stays on CPU and is loaded on-demand in the denoising loop.
if args.boundary_type == "low":
low_transformer3d.to(accelerator.device, dtype=weight_dtype)
else:
high_transformer3d.to(accelerator.device, dtype=weight_dtype)
else:
high_transformer3d.to(accelerator.device, dtype=weight_dtype)
low_transformer3d.to(accelerator.device, dtype=weight_dtype)
else:
transformer3d.to(accelerator.device, dtype=weight_dtype)
text_encoder.to(accelerator.device if not args.low_vram else "cpu")
# We need to recalculate our total training steps as the size of the training dataloader may have changed.
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
# Afterwards we recalculate our number of training epochs
args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
# We need to initialize the trackers we use, and also store our configuration.
# The trackers initializes automatically on the main process.
if accelerator.is_main_process:
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)
print(f"Removed tracker_config['{k}']")
accelerator.init_trackers(args.tracker_project_name, tracker_config)
# Train!
total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps
logger.info("***** Running 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
# Potentially load in the weights and states from a previous save
if args.resume_from_checkpoint:
if args.resume_from_checkpoint != "latest":
path = os.path.basename(args.resume_from_checkpoint)
else:
# Get the most recent checkpoint
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
checkpoint_folder_path = os.path.join(args.output_dir, path)
pkl_path = os.path.join(checkpoint_folder_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
print(f"Load pkl from {pkl_path}. Get first_epoch = {first_epoch}.")
if zero_stage != 3 and not args.use_fsdp:
from safetensors.torch import load_file
state_dict = load_file(os.path.join(checkpoint_folder_path, "lora_diffusion_pytorch_model.safetensors"), device=str(accelerator.device))
m, u = accelerator.unwrap_model(network).load_state_dict(state_dict, strict=False)
print(f"missing keys: {len(m)}, unexpected keys: {len(u)}")
optimizer_file_pt = os.path.join(checkpoint_folder_path, "optimizer.pt")
optimizer_file_bin = os.path.join(checkpoint_folder_path, "optimizer.bin")
optimizer_file_to_load = None
if os.path.exists(optimizer_file_pt):
optimizer_file_to_load = optimizer_file_pt
elif os.path.exists(optimizer_file_bin):
optimizer_file_to_load = optimizer_file_bin
if optimizer_file_to_load:
try:
accelerator.print(f"Loading optimizer state from {optimizer_file_to_load}")
optimizer_state = torch.load(optimizer_file_to_load, map_location=accelerator.device)
optimizer.load_state_dict(optimizer_state)
accelerator.print("Optimizer state loaded successfully.")
except Exception as e:
accelerator.print(f"Failed to load optimizer state from {optimizer_file_to_load}: {e}")
scheduler_file_pt = os.path.join(checkpoint_folder_path, "scheduler.pt")
scheduler_file_bin = os.path.join(checkpoint_folder_path, "scheduler.bin")
scheduler_file_to_load = None
if os.path.exists(scheduler_file_pt):
scheduler_file_to_load = scheduler_file_pt
elif os.path.exists(scheduler_file_bin):
scheduler_file_to_load = scheduler_file_bin
if scheduler_file_to_load:
try:
accelerator.print(f"Loading scheduler state from {scheduler_file_to_load}")
scheduler_state = torch.load(scheduler_file_to_load, map_location=accelerator.device)
lr_scheduler.load_state_dict(scheduler_state)
accelerator.print("Scheduler state loaded successfully.")
except Exception as e:
accelerator.print(f"Failed to load scheduler state from {scheduler_file_to_load}: {e}")
if hasattr(accelerator, 'scaler') and accelerator.scaler is not None:
scaler_file = os.path.join(checkpoint_folder_path, "scaler.pt")
if os.path.exists(scaler_file):
try:
accelerator.print(f"Loading GradScaler state from {scaler_file}")
scaler_state = torch.load(scaler_file, map_location=accelerator.device)
accelerator.scaler.load_state_dict(scaler_state)
accelerator.print("GradScaler state loaded successfully.")
except Exception as e:
accelerator.print(f"Failed to load GradScaler state: {e}")
else:
accelerator.load_state(checkpoint_folder_path)
accelerator.print("accelerator.load_state() completed for zero_stage 3.")
else:
initial_global_step = 0
# function for saving/removing
def save_model(ckpt_file, unwrapped_nw):
os.makedirs(args.output_dir, exist_ok=True)
accelerator.print(f"\nsaving checkpoint: {ckpt_file}")
if isinstance(unwrapped_nw, dict):
from safetensors.torch import save_file
save_file(unwrapped_nw, ckpt_file, metadata={"format": "pt"})
return ckpt_file
unwrapped_nw.save_weights(ckpt_file, weight_dtype, None)
progress_bar = PauseAwareTqdm(
range(0, args.max_train_steps),
initial=initial_global_step,
desc="Steps",
# Only show the progress bar once on each machine.
disable=not accelerator.is_local_main_process,
)
boundary = config["transformer_additional_kwargs"].get("boundary", 0.900)
print(f"The boundary is {boundary} and the boundary_type is {args.boundary_type}.")
for epoch in range(first_epoch, args.num_train_epochs):
train_loss = 0.0
train_reward = 0.0
train_first_frame_mse = 0.0
# Per-reward monitoring (unweighted, keeps each reward's own scale) so a
# multi-reward run can tell which reward is actually moving.
train_reward_per_fn = {name: 0.0 for name in reward_fn_names}
train_loss_per_fn = {name: 0.0 for name in reward_fn_names}
batch_sampler.sampler.generator = torch.Generator().manual_seed(args.seed + epoch)
# Iterate over the dataset using DataLoader
for step, batch in enumerate(train_dataloader):
if (not text_only) and epoch == first_epoch and step == 0:
pixel_values, texts = batch['pixel_values'].cpu(), batch['text']
pixel_values = rearrange(pixel_values, "b f c h w -> b c f h w")
os.makedirs(os.path.join(args.output_dir, "sanity_check"), exist_ok=True)
for idx, (pixel_value, text) in enumerate(zip(pixel_values, texts)):
pixel_value = pixel_value[None, ...]
gif_name = '-'.join(text.replace('/', '').split()[:10]) if not text == '' else f'{global_step}-{idx}'
save_videos_grid(pixel_value, f"{args.output_dir}/sanity_check/{gif_name[:10]}.mp4", rescale=True)
if args.train_mode != "normal":
clip_pixel_values, mask_pixel_values, texts = batch['clip_pixel_values'].cpu(), batch['mask_pixel_values'].cpu(), batch['text']
mask_pixel_values = rearrange(mask_pixel_values, "b f c h w -> b c f h w")
for idx, (clip_pixel_value, pixel_value, text) in enumerate(zip(clip_pixel_values, mask_pixel_values, texts)):
pixel_value = pixel_value[None, ...]
save_videos_grid(pixel_value, f"{args.output_dir}/sanity_check/mask_{gif_name[:10] if not text == '' else f'{global_step}-{idx}'}.mp4", rescale=True)
# Extract text prompts from the batch
train_prompt = batch["text"]
logger.info(f"train_prompt: {train_prompt}")
# here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)
# of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`
# corresponds to doing no classifier free guidance.
do_classifier_free_guidance = args.guidance_scale > 1.0
# Prepare timesteps
# TODO: support other noise schedulers (FlowUniPCMultistepScheduler and FlowDPMSolverMultistepScheduler)
noise_scheduler.set_timesteps(args.num_inference_steps, device=accelerator.device, mu=1)
timesteps = noise_scheduler.timesteps
# Prepare latent variables
vae_scale_factor = spatial_compression_ratio
tc_ratio = vae.config.temporal_compression_ratio
# Get actual dimensions from data and align to VAE temporal constraint.
# Under --force_t2v the batch is prompt-only (no mask/pixels), so fall through
# to the else branch which derives the shape from args (like pure T2V).
if args.train_mode != "normal" and not args.force_t2v:
mask_pixel_values = batch["mask_pixel_values"].to(weight_dtype)
mask = batch["mask"].to(weight_dtype)
raw_n_frames = mask.shape[1] # [B, F, C, H, W]
actual_n_frames = ((raw_n_frames - 1) // tc_ratio) * tc_ratio + 1
if raw_n_frames > actual_n_frames:
mask_pixel_values = mask_pixel_values[:, :actual_n_frames]
mask = mask[:, :actual_n_frames]
# Single image cannot be trained directly (reward model needs generated frames).
# Pad to video_sample_n_frames so the model generates a full video from the image.
if actual_n_frames == 1:
target_n_frames = args.video_sample_n_frames
# Fix first frame: set mask=0 (condition) and restore pixel values.
mask[:, 0:1] = 0
mask_pixel_values[:, 0:1] = batch["pixel_values"][:, 0:1].to(weight_dtype)
# Pad remaining frames: mask=1 (generate), pixels=0.
pad_mask = torch.ones(
mask.shape[0], target_n_frames - 1, mask.shape[2], mask.shape[3], mask.shape[4],
dtype=mask.dtype, device=mask.device
)
mask = torch.cat([mask, pad_mask], dim=1)
pad_pixels = torch.zeros(
mask_pixel_values.shape[0], target_n_frames - 1,
mask_pixel_values.shape[2], mask_pixel_values.shape[3], mask_pixel_values.shape[4],
dtype=mask_pixel_values.dtype, device=mask_pixel_values.device
)
mask_pixel_values = torch.cat([mask_pixel_values, pad_pixels], dim=1)
actual_n_frames = target_n_frames
actual_h = mask.shape[3]
actual_w = mask.shape[4]
else:
actual_n_frames = args.video_sample_n_frames
actual_h = args.video_sample_size
actual_w = args.video_sample_size
# Make the inpaint latents to be zeros.
if args.train_mode != "normal" and not args.force_t2v:
t2v_flag = [(_mask == 1).all() for _mask in mask]
new_t2v_flag = []
for _mask in t2v_flag:
if _mask and np.random.rand() < 0.90:
new_t2v_flag.append(0)
else:
new_t2v_flag.append(1)
t2v_flag = torch.from_numpy(np.array(new_t2v_flag)).to(accelerator.device, dtype=weight_dtype)
latent_shape = [
args.train_batch_size,
vae.config.latent_channels,
int((actual_n_frames - 1) // tc_ratio + 1) if actual_n_frames != 1 else 1,
actual_h // vae_scale_factor,
actual_w // vae_scale_factor,
]
with accelerator.accumulate(transformer3d):
# Keep the cross-step accumulation state in float32 (like train_grpo_lora.py's
# sample_with_cfg, which clones latents to fp32). bf16 has ~3 decimal digits of
# mantissa, and with the aggressive shift=12 schedule the first ~19/30 steps sit in
# the very-high-noise region with large Euler strides, so storing x_t in bf16
# compounds round-off into visible banding/noise even at step 0 (identity LoRA).
# The model input is cast back to weight_dtype right before the forward below.
latents = torch.randn(*latent_shape, device=accelerator.device, dtype=torch.float32, generator=torch_rng)
bsz, channel, num_latent_frames, latent_h, latent_w = latents.size()
if args.low_vram:
torch.cuda.empty_cache()
vae.to(accelerator.device)
with torch.no_grad():
# This way is quicker when batch grows up
def _batch_encode_vae(pixel_values):
pixel_values = rearrange(pixel_values, "b f c h w -> b c f h w")
bs = args.vae_mini_batch
new_pixel_values = []
for i in range(0, pixel_values.shape[0], bs):
pixel_values_bs = pixel_values[i : i + bs]
pixel_values_bs = vae.encode(pixel_values_bs)[0]
pixel_values_bs = pixel_values_bs.sample()
new_pixel_values.append(pixel_values_bs)
return torch.cat(new_pixel_values, dim = 0)
if args.train_mode != "normal" and not args.force_t2v:
mask = rearrange(mask, "b f c h w -> b c f h w")
mask = torch.concat(
[
torch.repeat_interleave(mask[:, :, 0:1], repeats=4, dim=2),
mask[:, :, 1:]
], dim=2
)
mask = mask.view(mask.shape[0], mask.shape[2] // 4, 4, mask.shape[3], mask.shape[4])
mask = mask.transpose(1, 2)
mask_conditions = F.interpolate(mask[:, :1], size=latents.size()[-3:], mode='trilinear', align_corners=True).to(accelerator.device, weight_dtype)
# Encode inpaint latents.
masked_video_latents = _batch_encode_vae(mask_pixel_values).to(accelerator.device, weight_dtype)
if args.train_mode == "ti2v":
# ti2v: mask uses ti2v-style interpolation; masked_video_latents stays raw for post-step blending
mask_latents = mask_conditions
else:
# 14B (i2v): standard inpaint mask + masked_video_latents
mask = resize_mask(1 - mask, latents)
mask_latents = mask
inpaint_latents = torch.concat([mask, masked_video_latents], dim=1)
inpaint_latents = t2v_flag[:, None, None, None, None] * inpaint_latents
if args.low_vram:
vae.to('cpu')
torch.cuda.empty_cache()
# Reduce the vram by offload text encoders
if args.low_vram:
torch.cuda.empty_cache()
text_encoder.to(accelerator.device)
# Encode input prompt
prompt_embeds, negative_prompt_embeds = encode_prompt(
tokenizer,
text_encoder,
train_prompt,
negative_prompt=["色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"] * len(train_prompt),
device=accelerator.device,
dtype=weight_dtype,
do_classifier_free_guidance=do_classifier_free_guidance,
)
if do_classifier_free_guidance:
in_prompt_embeds = negative_prompt_embeds + prompt_embeds
else:
in_prompt_embeds = prompt_embeds
# Reduce the vram by offload text encoders
if args.low_vram:
text_encoder.to("cpu")
torch.cuda.empty_cache()
# Under --force_t2v, fall into the else branch below: mask_latents /
# masked_video_latents become all-zero, so I2V receives an all-zero `y`
# condition while keeping its 52-channel (4 mask + 48 latent) input.
if args.train_mode != "normal" and not args.force_t2v:
if args.train_mode != "ti2v":
# non-ti2v: extract from concatenated inpaint_latents
mask_latents = inpaint_latents[:, :4]
masked_video_latents = inpaint_latents[:, 4:]
# else: 5B (ti2v) — mask_latents and masked_video_latents already set above
else:
# T2V mode: all zeros (generate everything)
mask_latents = torch.tile(
torch.zeros_like(latents)[:, :1].to(accelerator.device, weight_dtype), [1, 4, 1, 1, 1]
)
masked_video_latents = torch.zeros_like(latents).to(accelerator.device, weight_dtype)
if args.train_mode == "ti2v":
mask_conditions = torch.ones_like(latents).to(accelerator.device, weight_dtype)[:, :1]
if args.train_mode == "ti2v":
mask_conditions[:, :, 1:, :, :] = 1
generator = torch.Generator(device=accelerator.device).manual_seed(args.seed)
# Prepare extra step kwargs.
extra_step_kwargs = prepare_extra_step_kwargs(noise_scheduler, generator, args.eta)
target_shape = (vae.latent_channels, num_latent_frames, latent_w, latent_h)
seq_len = math.ceil(
(target_shape[2] * target_shape[3]) /
(accelerator.unwrap_model(transformer3d).config.patch_size[1] * accelerator.unwrap_model(transformer3d).config.patch_size[2]) *
target_shape[1]
)
num_inference_steps_for_backprop = args.num_inference_steps
if args.boundary_type == "high":
# When training the high noise reward lora, the denoising steps in the high noise are eligible for backpropagation.
num_inference_steps_for_backprop = 0
for i, t in enumerate(noise_scheduler.timesteps):
if t >= boundary * noise_scheduler.config.num_train_timesteps:
num_inference_steps_for_backprop += 1
# Denoising loop
if args.backprop:
if args.backprop_step_list is None:
if args.backprop_strategy == "last":
backprop_step_list = [num_inference_steps_for_backprop - 1]
elif args.backprop_strategy == "tail":
backprop_step_list = list(range(num_inference_steps_for_backprop))[-args.backprop_num_steps:]
elif args.backprop_strategy == "uniform":
interval = num_inference_steps_for_backprop // args.backprop_num_steps
random_start = random.randint(0, interval)
backprop_step_list = [random_start + i * interval for i in range(args.backprop_num_steps)]
elif args.backprop_strategy == "random":
backprop_step_list = random.sample(
range(args.backprop_random_start_step, args.backprop_random_end_step + 1), args.backprop_num_steps
)
else:
raise ValueError(f"Invalid backprop strategy: {args.backprop_strategy}.")
else:
backprop_step_list = args.backprop_step_list
# Sanity check for backpropagation (of 14B models)
if args.boundary_type != "full":
high_step_list = []
low_step_list = []
for i, t in enumerate(timesteps):
if t >= boundary * noise_scheduler.config.num_train_timesteps:
high_step_list.append(i)
else:
low_step_list.append(i)
if args.boundary_type == "high":
assert all(step in high_step_list for step in backprop_step_list), \
f"{backprop_step_list} is not in {high_step_list}"
else:
assert all(step in low_step_list for step in backprop_step_list), \
f"{backprop_step_list} is not in {low_step_list}"
# Real denoising rollout: show the bar only on the local main process, otherwise
# every FSDP rank prints its own copy (the loop is a collective, so all ranks run it).
for i, t in enumerate(tqdm(timesteps, desc="Denoise", disable=not accelerator.is_local_main_process)):
# expand the latents if we are doing classifier free guidance
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
if hasattr(noise_scheduler, "scale_model_input"):
latent_model_input = noise_scheduler.scale_model_input(latent_model_input, t)
# Construct y (mask + masked_video_latents) for I2V/inpaint conditioning.
# Pure T2V (normal) and ti2v pass y=None: the T2V transformer's
# patch_embedding expects latent channels only, so concatenating the
# zero mask/video condition (mask_latents:4 + masked_video_latents:48)
# would inflate the channel dim (48 -> 100) and crash the forward.
if args.train_mode in ("ti2v", "normal"):
# ti2v: mask_conditions handles conditioning; normal: no condition
y = None
else:
mask_input = torch.cat([mask_latents] * 2) if do_classifier_free_guidance else mask_latents
masked_video_latents_input = (
torch.cat([masked_video_latents] * 2) if do_classifier_free_guidance else masked_video_latents
)
y = torch.cat([mask_input, masked_video_latents_input], dim=1).to(accelerator.device, weight_dtype)
# broadcast to batch dimension in a way that's compatible with ONNX/Core ML
# Under --force_t2v, ti2v degenerates to a plain T2V forward: skip the
# mask blending / per-token timestep and use the scalar broadcast timestep.
if args.train_mode == "ti2v" and not args.force_t2v:
mc_step = mask_conditions.clone()
if do_classifier_free_guidance:
mc_step = torch.cat([mc_step] * 2)
mask_conditions_bs = mc_step.size()[0]
if not mask_conditions[:, :, 0, :, :].any():
mvl_input = torch.cat([masked_video_latents] * 2) if do_classifier_free_guidance else masked_video_latents
latent_model_input = (1 - mc_step) * mvl_input + mc_step * latent_model_input
temp_ts = (mc_step[:, 0, :, ::2, ::2] * t).flatten(1)
timestep = torch.cat([temp_ts, temp_ts.new_ones(mask_conditions_bs, seq_len - temp_ts.size(1)) * t], dim = 1)
else:
timestep = mc_step.new_ones(mask_conditions_bs, seq_len) * t
else:
timestep = t.expand(latent_model_input.shape[0])
if args.boundary_type != "full":
if t >= boundary * noise_scheduler.config.num_train_timesteps:
if args.low_vram and args.boundary_type == "low":
torch.cuda.empty_cache()
high_transformer3d.to(accelerator.device)
if args.low_vram and args.boundary_type == "high":
# Unload low_transformer3d to CPU; high stays on GPU for backprop.
low_transformer3d.to("cpu")
torch.cuda.empty_cache()
local_transformer = high_transformer3d
else:
if args.low_vram and args.boundary_type == "low":
# high_transformer3d's noise_pred is detached at high t,
# safe to offload before backward.
high_transformer3d.to("cpu")
torch.cuda.empty_cache()
if args.low_vram and args.boundary_type == "high":
torch.cuda.empty_cache()
low_transformer3d.to(accelerator.device)
local_transformer = low_transformer3d
else:
local_transformer = transformer3d
# Whether to enable DRTune: https://arxiv.org/abs/2405.00760
if args.stop_latent_model_input_gradient:
latent_model_input = latent_model_input.detach()
# predict noise model_output
with torch.cuda.amp.autocast(dtype=weight_dtype), torch.cuda.device(device=accelerator.device):
noise_pred = local_transformer(
x=latent_model_input.to(weight_dtype),
context=in_prompt_embeds,
t=timestep,
seq_len=seq_len,
y=y
)
# Optimize the denoising results only for the specified steps.
if i in backprop_step_list:
noise_pred = noise_pred
else:
noise_pred = noise_pred.detach()
# perform guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond.float() + args.guidance_scale * (noise_pred_text.float() - noise_pred_uncond.float())
# compute the previous noisy sample x_t -> x_t-1
latents = noise_scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0]
if args.train_mode == "ti2v" and not args.force_t2v:
blend_mask = mask_conditions # [B, 1, T_lat, H_lat, W_lat], already in latent size
latents = (1 - blend_mask) * masked_video_latents + blend_mask * latents
# decode latents (tensor)
# Since the casual VAE decoding consumes a large amount of VRAM, and we need to keep the decoding
# operation within the computational graph. Thus, we only decode args.num_decoded_latents
# latent frames to calculate the reward.
# Uniformly sample across the full temporal range for better motion quality evaluation.
if args.low_vram:
vae.to(accelerator.device)
total_latent_frames = latents.shape[2]
local_num_decoded_latents = min(total_latent_frames, args.num_decoded_latents)
sampled_latent_indices = torch.linspace(0, local_num_decoded_latents - 1, local_num_decoded_latents).long().tolist()
sampled_latents = latents[:, :, sampled_latent_indices, :, :]
sampled_frames = vae.decode(sampled_latents.to(vae.device, vae.dtype))[0]
sampled_frames = sampled_frames.clamp(-1, 1)
sampled_frames = (sampled_frames / 2 + 0.5).clamp(0, 1) # [-1, 1] -> [0, 1]
# Image rewards (HPSv3/HPS/MPS/Aesthetic/PickScore) score individual frames, so the
# frame subsampling below is safe. Video rewards (VideoAlignReward, is_image_reward=False)
# need the full temporal sequence to evaluate motion quality; collapsing frames here would
# degrade the video to (near-)static input and break the MQ dimension. So derive a
# per-reward frame view, gating all subsampling on `is_image_reward`, and keep the inpaint
# first-frame drop for every reward. Matches train_grpo_lora.py's behavior.
def _prepare_reward_frames(_is_image_reward):
_frames = sampled_frames
if _is_image_reward and args.num_sampled_frames is not None:
num_frames = _frames.size(2) - 1
_idx = torch.linspace(0, num_frames, steps=args.num_sampled_frames).long()
_frames = _frames[:, :, _idx, :, :]
# In inpaint/ti2v mode, skip the first frame (condition image) for all rewards
# to prevent the reward model from incentivizing first-frame drift. Under
# --force_t2v there is no real condition frame, so keep all decoded frames.
if args.train_mode != "normal" and not args.force_t2v:
_frames = _frames[:, :, 1:]
# Randomly select frames for extra stochasticity - image rewards only; video
# rewards must keep all decoded frames (count bounded by --num_decoded_latents).
if _is_image_reward and args.num_reward_frames is not None and _frames.shape[2] >= args.num_reward_frames:
_ridx = torch.randperm(_frames.shape[2])[:args.num_reward_frames].sort().values
_frames = _frames[:, :, _ridx]
return _frames
# Combine each reward's differentiable loss by a weighted sum.
loss = None
reward = None
for _name in reward_fn_names:
_fn = loss_fns[_name]
_frames = _prepare_reward_frames(getattr(_fn, "is_image_reward", False))
_l, _r = _fn(_frames, train_prompt)
_w = multi_reward_weights.get(_name, 1.0 / len(reward_fn_names))
if loss is None:
loss, reward = _w * _l, _w * _r
else:
loss, reward = loss + _w * _l, reward + _w * _r
# Log each reward's raw (unweighted) loss/reward on its own scale.
_fn_avg_loss = accelerator.gather(_l.repeat(args.train_batch_size)).mean()
_fn_avg_reward = accelerator.gather(_r.repeat(args.train_batch_size)).mean()
train_loss_per_fn[_name] += _fn_avg_loss.item() / args.gradient_accumulation_steps
train_reward_per_fn[_name] += _fn_avg_reward.item() / args.gradient_accumulation_steps
# First-frame latent MSE loss to anchor the condition image.
first_frame_mse = torch.tensor(0.0, device=accelerator.device)
if args.train_mode != "normal" and args.first_frame_mse_weight > 0 and not args.force_t2v:
first_frame_mse = F.mse_loss(
latents[:, :, 0:1].float(),
masked_video_latents[:, :, 0:1].detach().float()
)
loss = loss + args.first_frame_mse_weight * first_frame_mse
# Gather the losses and rewards across all processes for logging (if we use distributed training).
avg_loss = accelerator.gather(loss.repeat(args.train_batch_size)).mean()
avg_reward = accelerator.gather(reward.repeat(args.train_batch_size)).mean()
avg_first_frame_mse = accelerator.gather(first_frame_mse.repeat(args.train_batch_size)).mean()
train_loss += avg_loss.item() / args.gradient_accumulation_steps
train_reward += avg_reward.item() / args.gradient_accumulation_steps
train_first_frame_mse += avg_first_frame_mse.item() / args.gradient_accumulation_steps
# Backpropagate
accelerator.backward(loss)
if accelerator.sync_gradients:
total_norm = accelerator.clip_grad_norm_(trainable_params, args.max_grad_norm)
# If use_deepspeed, `total_norm` cannot be logged by accelerator.
if not args.use_deepspeed:
accelerator.log({"total_norm": total_norm}, step=global_step)
else:
if hasattr(optimizer, "optimizer") and hasattr(optimizer.optimizer, "_global_grad_norm"):
accelerator.log({"total_norm": optimizer.optimizer._global_grad_norm}, step=global_step)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
# Save train sample after gradient update to avoid VRAM pressure
# between full-latent VAE decode and reward CUDA kernels.
# Decode ALL latent frames to preserve causal VAE cache correctness;
# temporal subsampling breaks the autoregressive cache.
if global_step % args.checkpointing_steps == 0:
# Non-training window: the full-latent VAE decode + encode-to-disk can take tens
# of seconds, so keep it out of the progress-bar rate via paused().
with progress_bar.paused():
torch.cuda.empty_cache()
if args.low_vram:
vae.to(accelerator.device)
with torch.no_grad():
all_frames = vae.decode(latents.to(vae.device, vae.dtype))[0]
all_frames = all_frames.clamp(-1, 1)
all_frames = (all_frames / 2 + 0.5).clamp(0, 1)
saved_file = f"sample-{global_step}-{accelerator.process_index}.mp4"
save_videos_grid(
all_frames.to(torch.float32).detach().cpu(),
os.path.join(args.output_dir, "train_sample", saved_file),
fps=16
)
del all_frames
if args.low_vram:
vae.to("cpu")
torch.cuda.empty_cache()
# Checks if the accelerator has performed an optimization step behind the scenes
if accelerator.sync_gradients:
progress_bar.update(1)
global_step += 1
log_dict = {"train_loss": train_loss, "train_reward": train_reward, "train_first_frame_mse": train_first_frame_mse}
for _name in reward_fn_names:
log_dict[f"train_reward_{_name}"] = train_reward_per_fn[_name]
log_dict[f"train_loss_{_name}"] = train_loss_per_fn[_name]
accelerator.log(log_dict, step=global_step)
train_loss = 0.0
train_reward = 0.0
train_first_frame_mse = 0.0
train_reward_per_fn = {name: 0.0 for name in reward_fn_names}
train_loss_per_fn = {name: 0.0 for name in reward_fn_names}
if global_step % args.checkpointing_steps == 0:
if args.use_deepspeed or args.use_fsdp or accelerator.is_main_process:
# _before_ saving state, check if this save would set us over the `checkpoints_total_limit`
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]))
# before we save the new checkpoint, we need to have at _most_ `checkpoints_total_limit - 1` checkpoints
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"{len(checkpoints)} checkpoints already exist, removing {len(removing_checkpoints)} checkpoints"
)
logger.info(f"removing checkpoints: {', '.join(removing_checkpoints)}")
for removing_checkpoint in removing_checkpoints:
removing_checkpoint = os.path.join(args.output_dir, removing_checkpoint)
shutil.rmtree(removing_checkpoint)
gc.collect()
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
# Keep the checkpoint save out of the progress-bar rate: a long save would
# otherwise land in the next step's interval and show up as a slow step.
with progress_bar.paused():
if not args.save_state:
safetensor_save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}.safetensors")
save_model(safetensor_save_path, accelerator.unwrap_model(network))
logger.info(f"Saved safetensor to {safetensor_save_path}")
else:
accelerator_save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
accelerator.save_state(accelerator_save_path)
logger.info(f"Saved state to {accelerator_save_path}")
# Run on ALL processes: under FSDP the pipeline forward triggers all-gather
# collectives, so a rank0-only call would deadlock the other ranks.
if args.validation_prompts is not None and global_step % args.validation_steps == 0:
with progress_bar.paused():
log_validation(
vae,
text_encoder,
tokenizer,
transformer3d,
network,
config,
args,
accelerator,
weight_dtype,
global_step,
other_transformer3d=(low_transformer3d if args.boundary_type == "high" else (high_transformer3d if args.boundary_type == "low" else None)),
)
logs = {"step_loss": loss.detach().item(), "step_reward": reward.detach().item(), "lr": lr_scheduler.get_last_lr()[0]}
progress_bar.set_postfix(**logs)
if global_step >= args.max_train_steps:
break
# Run on ALL processes (FSDP collective forward), matching the step-level call.
if args.validation_prompts is not None and epoch % args.validation_epochs == 0:
with progress_bar.paused():
log_validation(
vae,
text_encoder,
tokenizer,
transformer3d,
network,
config,
args,
accelerator,
weight_dtype,
global_step,
other_transformer3d=(low_transformer3d if args.boundary_type == "high" else (high_transformer3d if args.boundary_type == "low" else None)),
)
# Close the bar before the end-of-run checkpoint / summary window so its wall-clock is not
# attributed to the last training step.
progress_bar.close()
# Create the pipeline using the trained modules and save it.
accelerator.wait_for_everyone()
if args.use_deepspeed or args.use_fsdp or accelerator.is_main_process:
gc.collect()
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
if not args.save_state:
safetensor_save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}.safetensors")
save_model(safetensor_save_path, accelerator.unwrap_model(network))
else:
accelerator_save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
accelerator.save_state(accelerator_save_path)
logger.info(f"Saved state to {accelerator_save_path}")
accelerator.end_training()
if __name__ == "__main__":
main()