Files
storyteller-ml/animation/StableAnimator/train_single.py
T

1671 lines
67 KiB
Python

import argparse
import random
import logging
import math
import os
import cv2
import shutil
from pathlib import Path
from urllib.parse import urlparse
import numpy as np
import PIL
from PIL import Image, ImageDraw
import torch
import torch.nn.functional as F
import torch.utils.checkpoint
from diffusers.models.attention_processor import XFormersAttnProcessor
from animation.dataset.animation_dataset import LargeScaleAnimationVideos
from animation.modules.attention_processor import AnimationAttnProcessor
from animation.modules.attention_processor_normalized import AnimationIDAttnNormalizedProcessor
from animation.modules.face_model import FaceModel
from animation.modules.id_encoder import FusionFaceId
from animation.modules.pose_net import PoseNet
from animation.modules.unet import UNetSpatioTemporalConditionModel
from animation.pipelines.validation_pipeline_animation import ValidationAnimationPipeline
import transformers
from accelerate import Accelerator, DistributedType
from accelerate.logging import get_logger
from accelerate.utils import ProjectConfiguration, set_seed
from huggingface_hub import create_repo, upload_folder
from packaging import version
from tqdm.auto import tqdm
from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection
from einops import rearrange
import datetime
import diffusers
from diffusers import AutoencoderKLTemporalDecoder, EulerDiscreteScheduler
from diffusers.image_processor import VaeImageProcessor
from diffusers.optimization import get_scheduler
from diffusers.training_utils import EMAModel
from diffusers.utils import check_min_version, deprecate, is_wandb_available, load_image
from diffusers.utils.import_utils import is_xformers_available
import warnings
import torch.nn as nn
from diffusers.utils.torch_utils import randn_tensor
# Will error if the minimal version of diffusers is not installed. Remove at your own risks.
check_min_version("0.24.0.dev0")
logger = get_logger(__name__, log_level="INFO")
# i should make a utility function file
def validate_and_convert_image(image, target_size=(256, 256)):
if image is None:
print("Encountered a None image")
return None
if isinstance(image, torch.Tensor):
# Convert PyTorch tensor to PIL Image
if image.ndim == 3 and image.shape[0] in [1, 3]: # Check for CxHxW format
if image.shape[0] == 1: # Convert single-channel grayscale to RGB
image = image.repeat(3, 1, 1)
image = image.mul(255).clamp(0, 255).byte().permute(1, 2, 0).cpu().numpy()
image = Image.fromarray(image)
else:
print(f"Invalid image tensor shape: {image.shape}")
return None
elif isinstance(image, Image.Image):
# Resize PIL Image
image = image.resize(target_size)
else:
print("Image is not a PIL Image or a PyTorch tensor")
return None
return image
def create_image_grid(images, rows, cols, target_size=(256, 256)):
valid_images = [validate_and_convert_image(img, target_size) for img in images]
valid_images = [img for img in valid_images if img is not None]
if not valid_images:
print("No valid images to create a grid")
return None
w, h = target_size
grid = Image.new('RGB', size=(cols * w, rows * h))
for i, image in enumerate(valid_images):
grid.paste(image, box=((i % cols) * w, (i // cols) * h))
return grid
def save_combined_frames(batch_output, validation_images, validation_control_images, output_folder):
# Flatten batch_output, which is a list of lists of PIL Images
flattened_batch_output = [img for sublist in batch_output for img in sublist]
# Combine frames into a list without converting (since they are already PIL Images)
combined_frames = validation_images + validation_control_images + flattened_batch_output
# Calculate rows and columns for the grid
num_images = len(combined_frames)
cols = 3 # adjust number of columns as needed
rows = (num_images + cols - 1) // cols
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
filename = f"combined_frames_{timestamp}.png"
# Create and save the grid image
grid = create_image_grid(combined_frames, rows, cols)
output_folder = os.path.join(output_folder, "validation_images")
os.makedirs(output_folder, exist_ok=True)
# Now define the full path for the file
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
filename = f"combined_frames_{timestamp}.png"
output_loc = os.path.join(output_folder, filename)
if grid is not None:
grid.save(output_loc)
else:
print("Failed to create image grid")
# def load_images_from_folder(folder):
# images = []
# valid_extensions = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff"} # Add or remove extensions as needed
#
# # Function to extract frame number from the filename
# def frame_number(filename):
# # First, try the pattern 'frame_x_7fps'
# new_pattern_match = re.search(r'frame_(\d+)_7fps', filename)
# if new_pattern_match:
# return int(new_pattern_match.group(1))
# # If the new pattern is not found, use the original digit extraction method
# matches = re.findall(r'\d+', filename)
# if matches:
# if matches[-1] == '0000' and len(matches) > 1:
# return int(matches[-2]) # Return the second-to-last sequence if the last is '0000'
# return int(matches[-1]) # Otherwise, return the last sequence
# return float('inf') # Return 'inf'
#
# # Sorting files based on frame number
# sorted_files = sorted(os.listdir(folder), key=frame_number)
#
# # Load images in sorted order
# for filename in sorted_files:
# ext = os.path.splitext(filename)[1].lower()
# if ext in valid_extensions:
# img = Image.open(os.path.join(folder, filename)).convert('RGB')
# images.append(img)
#
# return images
def load_images_from_folder(folder):
images = []
files = os.listdir(folder)
png_files = [f for f in files if f.endswith('.png')]
png_files.sort(key=lambda x: int(x.split('_')[1].split('.')[0]))
for filename in png_files:
img = Image.open(os.path.join(folder, filename)).convert('RGB')
images.append(img)
return images
# copy from https://github.com/crowsonkb/k-diffusion.git
def stratified_uniform(shape, group=0, groups=1, dtype=None, device=None):
"""Draws stratified samples from a uniform distribution."""
if groups <= 0:
raise ValueError(f"groups must be positive, got {groups}")
if group < 0 or group >= groups:
raise ValueError(f"group must be in [0, {groups})")
n = shape[-1] * groups
offsets = torch.arange(group, n, groups, dtype=dtype, device=device)
u = torch.rand(shape, dtype=dtype, device=device)
return (offsets + u) / n
def rand_cosine_interpolated(shape, image_d, noise_d_low, noise_d_high, sigma_data=1., min_value=1e-3, max_value=1e3,
device='cpu', dtype=torch.float32):
"""Draws samples from an interpolated cosine timestep distribution (from simple diffusion)."""
def logsnr_schedule_cosine(t, logsnr_min, logsnr_max):
t_min = math.atan(math.exp(-0.5 * logsnr_max))
t_max = math.atan(math.exp(-0.5 * logsnr_min))
return -2 * torch.log(torch.tan(t_min + t * (t_max - t_min)))
def logsnr_schedule_cosine_shifted(t, image_d, noise_d, logsnr_min, logsnr_max):
shift = 2 * math.log(noise_d / image_d)
return logsnr_schedule_cosine(t, logsnr_min - shift, logsnr_max - shift) + shift
def logsnr_schedule_cosine_interpolated(t, image_d, noise_d_low, noise_d_high, logsnr_min, logsnr_max):
logsnr_low = logsnr_schedule_cosine_shifted(
t, image_d, noise_d_low, logsnr_min, logsnr_max)
logsnr_high = logsnr_schedule_cosine_shifted(
t, image_d, noise_d_high, logsnr_min, logsnr_max)
return torch.lerp(logsnr_low, logsnr_high, t)
logsnr_min = -2 * math.log(min_value / sigma_data)
logsnr_max = -2 * math.log(max_value / sigma_data)
u = stratified_uniform(
shape, group=0, groups=1, dtype=dtype, device=device
)
logsnr = logsnr_schedule_cosine_interpolated(
u, image_d, noise_d_low, noise_d_high, logsnr_min, logsnr_max)
return torch.exp(-logsnr / 2) * sigma_data
def rand_log_normal(shape, loc=0., scale=1., device='cpu', dtype=torch.float32):
"""Draws samples from an lognormal distribution."""
u = torch.rand(shape, dtype=dtype, device=device) * (1 - 2e-7) + 1e-7
return torch.distributions.Normal(loc, scale).icdf(u).exp()
min_value = 0.002
max_value = 700
image_d = 64
noise_d_low = 32
noise_d_high = 64
sigma_data = 0.5
def _resize_with_antialiasing(input, size, interpolation="bicubic", align_corners=True):
h, w = input.shape[-2:]
factors = (h / size[0], w / size[1])
# First, we have to determine sigma
# Taken from skimage: https://github.com/scikit-image/scikit-image/blob/v0.19.2/skimage/transform/_warps.py#L171
sigmas = (
max((factors[0] - 1.0) / 2.0, 0.001),
max((factors[1] - 1.0) / 2.0, 0.001),
)
# Now kernel size. Good results are for 3 sigma, but that is kind of slow. Pillow uses 1 sigma
# https://github.com/python-pillow/Pillow/blob/master/src/libImaging/Resample.c#L206
# But they do it in the 2 passes, which gives better results. Let's try 2 sigmas for now
ks = int(max(2.0 * 2 * sigmas[0], 3)), int(max(2.0 * 2 * sigmas[1], 3))
# Make sure it is odd
if (ks[0] % 2) == 0:
ks = ks[0] + 1, ks[1]
if (ks[1] % 2) == 0:
ks = ks[0], ks[1] + 1
input = _gaussian_blur2d(input, ks, sigmas)
output = torch.nn.functional.interpolate(
input, size=size, mode=interpolation, align_corners=align_corners)
return output
def _compute_padding(kernel_size):
"""Compute padding tuple."""
# 4 or 6 ints: (padding_left, padding_right,padding_top,padding_bottom)
# https://pytorch.org/docs/stable/nn.html#torch.nn.functional.pad
if len(kernel_size) < 2:
raise AssertionError(kernel_size)
computed = [k - 1 for k in kernel_size]
# for even kernels we need to do asymmetric padding :(
out_padding = 2 * len(kernel_size) * [0]
for i in range(len(kernel_size)):
computed_tmp = computed[-(i + 1)]
pad_front = computed_tmp // 2
pad_rear = computed_tmp - pad_front
out_padding[2 * i + 0] = pad_front
out_padding[2 * i + 1] = pad_rear
return out_padding
def _filter2d(input, kernel):
# prepare kernel
b, c, h, w = input.shape
tmp_kernel = kernel[:, None, ...].to(
device=input.device, dtype=input.dtype)
tmp_kernel = tmp_kernel.expand(-1, c, -1, -1)
height, width = tmp_kernel.shape[-2:]
padding_shape: list[int] = _compute_padding([height, width])
input = torch.nn.functional.pad(input, padding_shape, mode="reflect")
# kernel and input tensor reshape to align element-wise or batch-wise params
tmp_kernel = tmp_kernel.reshape(-1, 1, height, width)
input = input.view(-1, tmp_kernel.size(0), input.size(-2), input.size(-1))
# convolve the tensor with the kernel.
output = torch.nn.functional.conv2d(
input, tmp_kernel, groups=tmp_kernel.size(0), padding=0, stride=1)
out = output.view(b, c, h, w)
return out
def _gaussian(window_size: int, sigma):
if isinstance(sigma, float):
sigma = torch.tensor([[sigma]])
batch_size = sigma.shape[0]
x = (torch.arange(window_size, device=sigma.device,
dtype=sigma.dtype) - window_size // 2).expand(batch_size, -1)
if window_size % 2 == 0:
x = x + 0.5
gauss = torch.exp(-x.pow(2.0) / (2 * sigma.pow(2.0)))
return gauss / gauss.sum(-1, keepdim=True)
def _gaussian_blur2d(input, kernel_size, sigma):
if isinstance(sigma, tuple):
sigma = torch.tensor([sigma], dtype=input.dtype)
else:
sigma = sigma.to(dtype=input.dtype)
ky, kx = int(kernel_size[0]), int(kernel_size[1])
bs = sigma.shape[0]
kernel_x = _gaussian(kx, sigma[:, 1].view(bs, 1))
kernel_y = _gaussian(ky, sigma[:, 0].view(bs, 1))
out_x = _filter2d(input, kernel_x[..., None, :])
out = _filter2d(out_x, kernel_y[..., None])
return out
def export_to_video(video_frames, output_video_path, fps):
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
h, w, _ = video_frames[0].shape
video_writer = cv2.VideoWriter(
output_video_path, fourcc, fps=fps, frameSize=(w, h))
for i in range(len(video_frames)):
img = cv2.cvtColor(video_frames[i], cv2.COLOR_RGB2BGR)
video_writer.write(img)
def export_to_gif(frames, output_gif_path, fps):
"""
Export a list of frames to a GIF.
Args:
- frames (list): List of frames (as numpy arrays or PIL Image objects).
- output_gif_path (str): Path to save the output GIF.
- duration_ms (int): Duration of each frame in milliseconds.
"""
# Convert numpy arrays to PIL Images if needed
pil_frames = [Image.fromarray(frame) if isinstance(
frame, np.ndarray) else frame for frame in frames]
pil_frames[0].save(output_gif_path.replace('.mp4', '.gif'),
format='GIF',
append_images=pil_frames[1:],
save_all=True,
duration=125,
loop=0)
def tensor_to_vae_latent(t, vae, scale=True):
t = t.to(vae.dtype)
if len(t.shape) == 5:
video_length = t.shape[1]
t = rearrange(t, "b f c h w -> (b f) c h w")
latents = vae.encode(t).latent_dist.sample()
latents = rearrange(latents, "(b f) c h w -> b f c h w", f=video_length)
elif len(t.shape) == 4:
latents = vae.encode(t).latent_dist.sample()
if scale:
latents = latents * vae.config.scaling_factor
return latents
def parse_args():
parser = argparse.ArgumentParser(
description="Script to train Stable Diffusion XL for InstructPix2Pix."
)
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(
"--num_frames",
type=int,
default=14,
)
parser.add_argument(
"--dataset_type",
type=str,
default='ubc',
)
parser.add_argument(
"--num_validation_images",
type=int,
default=1,
help="Number of images that should be generated during validation with `validation_prompt`.",
)
parser.add_argument(
"--validation_steps",
type=int,
default=500,
help=(
"Run fine-tuning validation every X epochs. The validation process consists of running the text/image prompt"
" multiple times: `args.num_validation_images`."
),
)
parser.add_argument(
"--output_dir",
type=str,
default="./outputs",
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(
"--per_gpu_batch_size",
type=int,
default=1,
help="Batch size (per device) for the training dataloader.",
)
parser.add_argument("--num_train_epochs", type=int, default=100)
parser.add_argument(
"--max_train_steps",
type=int,
default=None,
help="Total number of training steps to perform. If provided, overrides num_train_epochs.",
)
parser.add_argument(
"--gradient_accumulation_steps",
type=int,
default=1,
help="Number of updates steps to accumulate before performing a backward/update pass.",
)
parser.add_argument(
"--gradient_checkpointing",
action="store_true",
help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.",
)
parser.add_argument(
"--learning_rate",
type=float,
default=1e-4,
help="Initial learning rate (after the potential warmup period) to use.",
)
parser.add_argument(
"--scale_lr",
action="store_true",
default=False,
help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.",
)
parser.add_argument(
"--lr_scheduler",
type=str,
default="constant",
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(
"--conditioning_dropout_prob",
type=float,
default=0.1,
help="Conditioning dropout probability. Drops out the conditionings (image and edit prompt) used in training InstructPix2Pix. See section 3.2.1 in the paper: https://arxiv.org/abs/2211.09800.",
)
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(
"--num_workers",
type=int,
default=8,
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=1,
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(
"--enable_xformers_memory_efficient_attention",
action="store_true",
help="Whether or not to use xformers.",
)
parser.add_argument(
"--log_trainable_parameters",
action="store_true",
help="Whether to write the trainable parameters.",
)
parser.add_argument(
"--pretrain_unet",
type=str,
default=None,
help="use weight for unet block",
)
parser.add_argument(
"--rank",
type=int,
default=128,
help=("The dimension of the LoRA update matrices."),
)
parser.add_argument(
"--csv_path",
type=str,
default=None,
help=(
"path to the dataset csv"
),
)
parser.add_argument(
"--video_folder",
type=str,
default=None,
help=(
"path to the video folder"
),
)
parser.add_argument(
"--condition_folder",
type=str,
default=None,
help=(
"path to the depth folder"
),
)
parser.add_argument(
"--motion_folder",
type=str,
default=None,
help=(
"path to the depth folder"
),
)
parser.add_argument(
"--validation_prompt",
type=str,
default=None,
help=(
"A set of prompts evaluated every `--validation_steps` and logged to `--report_to`."
" Provide either a matching number of `--validation_image`s, a single `--validation_image`"
" to be used with all prompts, or a single prompt that will be used with all `--validation_image`s."
),
)
parser.add_argument(
"--validation_image_folder",
type=str,
default=None,
help=(
"A set of paths to the controlnext conditioning image be evaluated every `--validation_steps`"
" and logged to `--report_to`. Provide either a matching number of `--validation_prompt`s, a"
" a single `--validation_prompt` to be used with all `--validation_image`s, or a single"
" `--validation_image` that will be used with all `--validation_prompt`s."
),
)
parser.add_argument(
"--validation_image",
type=str,
default=None,
help=(
"A set of paths to the controlnext conditioning image be evaluated every `--validation_steps`"
" and logged to `--report_to`. Provide either a matching number of `--validation_prompt`s, a"
" a single `--validation_prompt` to be used with all `--validation_image`s, or a single"
" `--validation_image` that will be used with all `--validation_prompt`s."
),
)
parser.add_argument(
"--validation_control_folder",
type=str,
default=None,
help=(
"the validation control image"
),
)
parser.add_argument(
"--sample_n_frames",
type=int,
default=14,
help=(
"the sample_n_frames"
),
)
parser.add_argument(
"--ref_augment",
action="store_true",
help=(
"use augment for the reference image"
),
)
parser.add_argument(
"--train_stage",
type=int,
default=2,
help=(
"the training stage"
),
)
parser.add_argument(
"--posenet_model_name_or_path",
type=str,
default=None,
help="Path to pretrained posenet model",
)
parser.add_argument(
"--face_encoder_model_name_or_path",
type=str,
default=None,
help="Path to pretrained face encoder model",
)
parser.add_argument(
"--unet_model_name_or_path",
type=str,
default=None,
help="Path to pretrained unet model",
)
parser.add_argument(
"--data_root_path",
type=str,
default=None,
help="Path to the data root path",
)
parser.add_argument(
"--data_path",
type=str,
default=None,
help="Path to the data path",
)
parser.add_argument(
"--finetune_mode",
type=bool,
default=False,
help="Enable or disable the finetune mode (True/False).",
)
parser.add_argument(
"--posenet_model_finetune_path",
type=str,
default=None,
help="Path to the pretrained posenet model",
)
parser.add_argument(
"--face_encoder_finetune_path",
type=str,
default=None,
help="Path to the pretrained face encoder",
)
parser.add_argument(
"--unet_model_finetune_path",
type=str,
default=None,
help="Path to the pretrained unet model",
)
parser.add_argument(
"--dataset_width",
type=int,
default=512,
help="video dataset width",
)
parser.add_argument(
"--dataset_height",
type=int,
default=512,
help="video dataset height",
)
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 download_image(url):
original_image = (
lambda image_url_or_path: load_image(image_url_or_path)
if urlparse(image_url_or_path).scheme
else PIL.Image.open(image_url_or_path).convert("RGB")
)(url)
return original_image
# This is for training using deepspeed.
# Since now the DeepSpeed only supports trainging with only one model
# So we create a virtual wrapper to contail all the models
class DeepSpeedWrapperModel(nn.Module):
def __init__(self, **kwargs):
super().__init__()
for name, value in kwargs.items():
assert isinstance(value, nn.Module)
self.register_module(name, value)
def main():
warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=FutureWarning)
torch.multiprocessing.set_start_method('spawn')
args = parse_args()
if args.non_ema_revision is not None:
deprecate(
"non_ema_revision!=None",
"0.15.0",
message=(
"Downloading 'non_ema' weights from revision branches of the Hub is deprecated. Please make sure to"
" use `--variant=non_ema` instead."
),
)
logging_dir = os.path.join(args.output_dir, args.logging_dir)
accelerator_project_config = ProjectConfiguration(
project_dir=args.output_dir, logging_dir=logging_dir)
# ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True)
accelerator = Accelerator(
gradient_accumulation_steps=args.gradient_accumulation_steps,
mixed_precision=args.mixed_precision,
project_config=accelerator_project_config,
)
generator = torch.Generator(
device=accelerator.device).manual_seed(23123134)
if args.report_to == "wandb":
if not is_wandb_available():
raise ImportError(
"Make sure to install wandb if you want to use it for logging during training.")
import wandb
# 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:
transformers.utils.logging.set_verbosity_warning()
diffusers.utils.logging.set_verbosity_info()
else:
transformers.utils.logging.set_verbosity_error()
diffusers.utils.logging.set_verbosity_error()
# If passed along, set the training seed now.
if args.seed is not None:
set_seed(args.seed)
# Handle the repository creation
if accelerator.is_main_process:
if args.output_dir is not None:
os.makedirs(args.output_dir, exist_ok=True)
if args.push_to_hub:
repo_id = create_repo(
repo_id=args.hub_model_id or Path(args.output_dir).name, exist_ok=True, token=args.hub_token
).repo_id
# Load scheduler, tokenizer and models.
print(args.pretrained_model_name_or_path)
feature_extractor = CLIPImageProcessor.from_pretrained(args.pretrained_model_name_or_path,
subfolder="feature_extractor", revision=args.revision)
noise_scheduler = EulerDiscreteScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler")
image_encoder = CLIPVisionModelWithProjection.from_pretrained(
args.pretrained_model_name_or_path, subfolder="image_encoder", revision=args.revision
)
vae = AutoencoderKLTemporalDecoder.from_pretrained(
args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision, variant="fp16")
unet = UNetSpatioTemporalConditionModel.from_pretrained(
args.pretrained_model_name_or_path if args.pretrain_unet is None else args.pretrain_unet,
subfolder="unet",
low_cpu_mem_usage=True,
variant="fp16"
)
pose_net = PoseNet(noise_latent_channels=unet.config.block_out_channels[0])
face_encoder = FusionFaceId(
cross_attention_dim=1024,
id_embeddings_dim=512,
clip_embeddings_dim=1024,
num_tokens=4, )
face_model = FaceModel()
# init adapter modules
lora_rank = 128
attn_procs = {}
unet_svd = unet.state_dict()
for name in unet.attn_processors.keys():
if "transformer_blocks" in name and "temporal_transformer_blocks" not in name:
cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim
if name.startswith("mid_block"):
hidden_size = unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(reversed(unet.config.block_out_channels))[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = unet.config.block_out_channels[block_id]
if cross_attention_dim is None:
# print(f"This is AnimationAttnProcessor: {name}")
attn_procs[name] = AnimationAttnProcessor(hidden_size=hidden_size,
cross_attention_dim=cross_attention_dim, rank=lora_rank)
else:
# print(f"This is AnimationIDAttnNormalizedProcessor: {name}")
layer_name = name.split(".processor")[0]
weights = {
"to_k_ip.weight": unet_svd[layer_name + ".to_k.weight"],
"to_v_ip.weight": unet_svd[layer_name + ".to_v.weight"],
}
attn_procs[name] = AnimationIDAttnNormalizedProcessor(hidden_size=hidden_size,
cross_attention_dim=cross_attention_dim,
rank=lora_rank)
attn_procs[name].load_state_dict(weights, strict=False)
elif "temporal_transformer_blocks" in name:
cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim
if name.startswith("mid_block"):
hidden_size = unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(reversed(unet.config.block_out_channels))[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = unet.config.block_out_channels[block_id]
if cross_attention_dim is None:
attn_procs[name] = XFormersAttnProcessor()
else:
attn_procs[name] = XFormersAttnProcessor()
unet.set_attn_processor(attn_procs)
# triggering the finetune mode
if args.finetune_mode is True and args.posenet_model_finetune_path is not None and args.face_encoder_finetune_path is not None and args.unet_model_finetune_path is not None:
print("Loading existing posenet weights, face_encoder weights and unet weights.")
if args.posenet_model_finetune_path.endswith(".pth"):
pose_net_state_dict = torch.load(args.posenet_model_finetune_path, map_location="cpu")
pose_net.load_state_dict(pose_net_state_dict, strict=True)
else:
print("posenet weights loading fail")
print(1 / 0)
if args.face_encoder_finetune_path.endswith(".pth"):
face_encoder_state_dict = torch.load(args.face_encoder_finetune_path, map_location="cpu")
face_encoder.load_state_dict(face_encoder_state_dict, strict=True)
else:
print("face_encoder weights loading fail")
print(1 / 0)
if args.unet_model_finetune_path.endswith(".pth"):
unet_state_dict = torch.load(args.unet_model_finetune_path, map_location="cpu")
unet.load_state_dict(unet_state_dict, strict=True)
else:
print("unet weights loading fail")
print(1 / 0)
vae_scale_factor = 2 ** (len(vae.config.block_out_channels) - 1)
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor)
# Freeze vae and image_encoder
vae.requires_grad_(False)
image_encoder.requires_grad_(False)
unet.requires_grad_(False)
pose_net.requires_grad_(False)
face_encoder.requires_grad_(False)
weight_dtype = torch.float32
if accelerator.mixed_precision == "fp16":
weight_dtype = torch.float16
elif accelerator.mixed_precision == "bf16":
weight_dtype = torch.bfloat16
image_encoder.to(accelerator.device, dtype=weight_dtype)
vae.to(accelerator.device, dtype=weight_dtype)
if args.use_ema:
ema_unet = EMAModel(unet.parameters(
), model_cls=UNetSpatioTemporalConditionModel, model_config=unet.config)
if args.enable_xformers_memory_efficient_attention:
if is_xformers_available():
import xformers
xformers_version = version.parse(xformers.__version__)
if xformers_version == version.parse("0.0.16"):
logger.warn(
"xFormers 0.0.16 cannot be used for training in some GPUs. If you observe problems during training, please update xFormers to at least 0.0.17. See https://huggingface.co/docs/diffusers/main/en/optimization/xformers for more details."
)
unet.enable_xformers_memory_efficient_attention()
else:
raise ValueError(
"xformers is not available. Make sure it is installed correctly")
if args.gradient_checkpointing:
unet.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.per_gpu_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
else:
optimizer_cls = torch.optim.AdamW
# if accelerator.distributed_type == DistributedType.DEEPSPEED:
# ds_wrapper = DeepSpeedWrapperModel(
# unet=unet,
# controlnext=controlnext
# )
# unet = ds_wrapper.unet
# controlnext = ds_wrapper.controlnext
pose_net.requires_grad_(True)
face_encoder.requires_grad_(True)
parameters_list = []
for name, para in pose_net.named_parameters():
para.requires_grad = True
parameters_list.append({"params": para, "lr": args.learning_rate})
for name, para in face_encoder.named_parameters():
para.requires_grad = True
parameters_list.append({"params": para, "lr": args.learning_rate})
"""
For more details, please refer to: https://github.com/dvlab-research/ControlNeXt/issues/14#issuecomment-2290450333
This is the selective parameters part.
As presented in our paper, we only select a small subset of parameters, which is fully adapted to the SD1.5 and SDXL backbones. By training fewer than 100 million parameters, we still achieve excellent performance. But this is is not suitable for the SD3 and SVD training. This is because, after SDXL, Stability faced significant legal risks due to the generation of highly realistic human images. After that, they stopped refining their models on human-related data, such as SVD and SD3, to avoid potential risks.
To achieve optimal performance, it's necessary to first continue training SVD and SD3 on human-related data to develop a robust backbone before fine-tuning. Of course, you can also combine the continual pretraining and finetuning. So you can find that we direct provide the full SVD parameters.
We have experimented with two approaches: 1.Directly training the model from scratch on human dancing data. 2. Continual training using a pre-trained human generation backbone, followed by fine-tuning a selective small subset of parameters. Interestingly, we observed no significant difference in performance between these two methods.
"""
for name, para in unet.named_parameters():
if "attentions" in name:
para.requires_grad = True
parameters_list.append({"params": para})
else:
para.requires_grad = False
optimizer = optimizer_cls(
parameters_list,
lr=args.learning_rate,
betas=(args.adam_beta1, args.adam_beta2),
weight_decay=args.adam_weight_decay,
eps=args.adam_epsilon,
)
# check para
if accelerator.is_main_process and args.log_trainable_parameters:
rec_txt1 = open('rec_para.txt', 'w')
rec_txt2 = open('rec_para_train.txt', 'w')
for name, para in unet.named_parameters():
if para.requires_grad is False:
rec_txt1.write(f'{name}\n')
else:
rec_txt2.write(f'{name}\n')
rec_txt1.close()
rec_txt2.close()
# DataLoaders creation:
args.global_batch_size = args.per_gpu_batch_size * accelerator.num_processes
root_path = args.data_root_path
txt_path = args.data_path
train_dataset = LargeScaleAnimationVideos(
root_path=root_path,
txt_path=txt_path,
width=args.dataset_width,
height=args.dataset_height,
n_sample_frames=args.sample_n_frames,
sample_frame_rate=4,
app=face_model.app,
handler_ante=face_model.handler_ante,
face_helper=face_model.face_helper
)
train_dataloader = torch.utils.data.DataLoader(
train_dataset,
batch_size=args.per_gpu_batch_size,
num_workers=args.num_workers,
shuffle=True,
)
# 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,
)
unet, pose_net, face_encoder, optimizer, lr_scheduler, train_dataloader = accelerator.prepare(
unet, pose_net, face_encoder, optimizer, lr_scheduler, train_dataloader
)
if args.use_ema:
ema_unet.to(accelerator.device)
# 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:
accelerator.init_trackers("StableAnimator", config=vars(args))
# Train!
total_batch_size = args.per_gpu_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.per_gpu_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
def encode_image(pixel_values):
pixel_values = _resize_with_antialiasing(pixel_values, (224, 224))
pixel_values = (pixel_values + 1.0) / 2.0
pixel_values = pixel_values.to(torch.float32)
# Normalize the image with for CLIP input
pixel_values = feature_extractor(
images=pixel_values,
do_normalize=True,
do_center_crop=False,
do_resize=False,
do_rescale=False,
return_tensors="pt",
).pixel_values
pixel_values = pixel_values.to(
device=accelerator.device, dtype=image_encoder.dtype)
image_embeddings = image_encoder(pixel_values).image_embeds
image_embeddings = image_embeddings.unsqueeze(1)
return image_embeddings
def _get_add_time_ids(
fps,
motion_bucket_id,
noise_aug_strength,
dtype,
batch_size,
unet=None,
device=None
):
add_time_ids = [fps, motion_bucket_id, noise_aug_strength]
add_time_ids = torch.tensor([add_time_ids], dtype=dtype, device=device)
add_time_ids = add_time_ids.repeat(batch_size, 1)
return add_time_ids
# 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
else:
accelerator.print(f"Resuming from checkpoint {path}")
accelerator.load_state(os.path.join(args.output_dir, path))
global_step = int(path.split("-")[1])
resume_global_step = global_step * args.gradient_accumulation_steps
first_epoch = global_step // num_update_steps_per_epoch
resume_step = resume_global_step % (
num_update_steps_per_epoch * args.gradient_accumulation_steps)
# Only show the progress bar once on each machine.
progress_bar = tqdm(range(global_step, args.max_train_steps),
disable=not accelerator.is_local_main_process)
progress_bar.set_description("Steps")
for epoch in range(first_epoch, args.num_train_epochs):
pose_net.train()
face_encoder.train()
unet.train()
train_loss = 0.0
for step, batch in enumerate(train_dataloader):
# Skip steps until we reach the resumed step
if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step:
if step % args.gradient_accumulation_steps == 0:
progress_bar.update(1)
continue
with accelerator.accumulate(pose_net, face_encoder, unet):
with accelerator.autocast():
pixel_values = batch["pixel_values"].to(weight_dtype).to(
accelerator.device, non_blocking=True
)
conditional_pixel_values = batch["reference_image"].to(weight_dtype).to(
accelerator.device, non_blocking=True
)
latents = tensor_to_vae_latent(pixel_values, vae).to(dtype=weight_dtype)
# Get the text embedding for conditioning.
encoder_hidden_states = encode_image(conditional_pixel_values).to(dtype=weight_dtype)
image_embed = encoder_hidden_states.clone()
train_noise_aug = 0.02
conditional_pixel_values = conditional_pixel_values + train_noise_aug * \
randn_tensor(conditional_pixel_values.shape, generator=generator,
device=conditional_pixel_values.device,
dtype=conditional_pixel_values.dtype)
conditional_latents = tensor_to_vae_latent(conditional_pixel_values, vae, scale=False)
# Sample noise that we'll add to the latents
noise = torch.randn_like(latents)
bsz = latents.shape[0]
# Sample a random timestep for each image
sigmas = rand_cosine_interpolated(shape=[bsz, ], image_d=image_d, noise_d_low=noise_d_low,
noise_d_high=noise_d_high, sigma_data=sigma_data,
min_value=min_value, max_value=max_value).to(latents.device,
dtype=weight_dtype)
# sigmas = rand_log_normal(shape=[bsz,], loc=0.7, scale=1.6).to(latents)
# Add noise to the latents according to the noise magnitude at each timestep
# (this is the forward diffusion process)
sigmas_reshaped = sigmas.clone()
while len(sigmas_reshaped.shape) < len(latents.shape):
sigmas_reshaped = sigmas_reshaped.unsqueeze(-1)
noisy_latents = latents + noise * sigmas_reshaped
timesteps = torch.Tensor([0.25 * sigma.log() for sigma in sigmas]).to(latents.device,
dtype=weight_dtype)
inp_noisy_latents = noisy_latents / ((sigmas_reshaped ** 2 + 1) ** 0.5)
added_time_ids = _get_add_time_ids(
fps=6,
motion_bucket_id=127.0,
noise_aug_strength=train_noise_aug, # noise_aug_strength == 0.0
dtype=encoder_hidden_states.dtype,
batch_size=bsz,
unet=unet,
device=latents.device
)
added_time_ids = added_time_ids.to(latents.device)
# Conditioning dropout to support classifier-free guidance during inference. For more details
# check out the section 3.2.1 of the original paper https://arxiv.org/abs/2211.09800.
if args.conditioning_dropout_prob is not None:
random_p = torch.rand(
bsz, device=latents.device, generator=generator)
# Sample masks for the edit prompts.
prompt_mask = random_p < 2 * args.conditioning_dropout_prob
prompt_mask = prompt_mask.reshape(bsz, 1, 1)
# Final text conditioning.
null_conditioning = torch.zeros_like(encoder_hidden_states)
encoder_hidden_states = torch.where(
prompt_mask, null_conditioning, encoder_hidden_states)
# Sample masks for the original images.
image_mask_dtype = conditional_latents.dtype
image_mask = 1 - (
(random_p >= args.conditioning_dropout_prob).to(
image_mask_dtype)
* (random_p < 3 * args.conditioning_dropout_prob).to(image_mask_dtype)
)
image_mask = image_mask.reshape(bsz, 1, 1, 1)
# Final image conditioning.
conditional_latents = image_mask * conditional_latents
# Concatenate the `conditional_latents` with the `noisy_latents`.
conditional_latents = conditional_latents.unsqueeze(
1).repeat(1, noisy_latents.shape[1], 1, 1, 1)
pose_pixels = batch["pose_pixels"].to(
dtype=weight_dtype, device=accelerator.device, non_blocking=True
)
faceid_embeds = batch["faceid_embeds"].to(
dtype=weight_dtype, device=accelerator.device, non_blocking=True
)
pose_latents = pose_net(pose_pixels)
# print("This is faceid_latents calculation")
# print(faceid_embeds.size()) # [1, 512]
# print(image_embed.size()) # [1, 1, 1024]
faceid_latents = face_encoder(faceid_embeds, image_embed)
inp_noisy_latents = torch.cat(
[inp_noisy_latents, conditional_latents], dim=2)
target = latents
# print(f"the size of encoder_hidden_states: {encoder_hidden_states.size()}") # [1, 1, 1024]
# print(f"the size of face latents: {faceid_latents.size()}") # [1, 4, 1024]
encoder_hidden_states = torch.cat([encoder_hidden_states, faceid_latents], dim=1)
encoder_hidden_states = encoder_hidden_states.to(latents.dtype)
inp_noisy_latents = inp_noisy_latents.to(latents.dtype)
pose_latents = pose_latents.to(latents.dtype)
# Predict the noise residual
model_pred = unet(
inp_noisy_latents, timesteps, encoder_hidden_states,
added_time_ids=added_time_ids,
pose_latents=pose_latents,
).sample
sigmas = sigmas_reshaped
# Denoise the latents
c_out = -sigmas / ((sigmas ** 2 + 1) ** 0.5)
c_skip = 1 / (sigmas ** 2 + 1)
denoised_latents = model_pred * c_out + c_skip * noisy_latents
weighing = (1 + sigmas ** 2) * (sigmas ** -2.0)
tgt_face_masks = batch["tgt_face_masks"].to(
dtype=weight_dtype, device=accelerator.device, non_blocking=True
)
tgt_face_masks = rearrange(tgt_face_masks, "b f c h w -> (b f) c h w")
tgt_face_masks = F.interpolate(tgt_face_masks, size=(target.size()[-2], target.size()[-1]),
mode='nearest')
tgt_face_masks = rearrange(tgt_face_masks, "(b f) c h w -> b f c h w", f=args.sample_n_frames)
# MSE loss
loss = torch.mean(
(weighing.float() * (denoised_latents.float() -
target.float()) ** 2 * (1 + tgt_face_masks)).reshape(target.shape[0], -1),
dim=1,
)
loss = loss.mean()
# Gather the losses across all processes for logging (if we use distributed training).
avg_loss = accelerator.gather(
loss.repeat(args.per_gpu_batch_size)).mean()
train_loss += avg_loss.item() / args.gradient_accumulation_steps
# Backpropagate
accelerator.backward(loss)
# if accelerator.sync_gradients:
# accelerator.clip_grad_norm_(unet.parameters(), args.max_grad_norm)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
with torch.cuda.device(latents.device):
torch.cuda.empty_cache()
# Checks if the accelerator has performed an optimization step behind the scenes
if accelerator.sync_gradients:
if args.use_ema:
ema_unet.step(unet.parameters())
progress_bar.update(1)
global_step += 1
accelerator.log({"train_loss": train_loss}, step=global_step)
train_loss = 0.0
# save checkpoints!
# if global_step % args.checkpointing_steps == 0 and (accelerator.is_main_process or accelerator.distributed_type == DistributedType.DEEPSPEED):
if global_step % args.checkpointing_steps == 0 and 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 and accelerator.is_main_process:
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)
save_path = os.path.join(
args.output_dir, f"checkpoint-{global_step}")
accelerator.save_state(save_path)
unwrap_unet = accelerator.unwrap_model(unet)
unwrap_pose_net = accelerator.unwrap_model(pose_net)
unwrap_face_encoder = accelerator.unwrap_model(face_encoder)
unwrap_unet_state_dict = unwrap_unet.state_dict()
torch.save(unwrap_unet_state_dict,
os.path.join(args.output_dir, f"checkpoint-{global_step}", f"unet-{global_step}.pth"))
unwrap_pose_net_state_dict = unwrap_pose_net.state_dict()
torch.save(unwrap_pose_net_state_dict, os.path.join(args.output_dir, f"checkpoint-{global_step}",
f"pose_net-{global_step}.pth"))
unwrap_face_encoder_state_dict = unwrap_face_encoder.state_dict()
torch.save(unwrap_face_encoder_state_dict,
os.path.join(args.output_dir, f"checkpoint-{global_step}",
f"face_encoder-{global_step}.pth"))
logger.info(f"Saved state to {save_path}")
if accelerator.is_main_process:
# sample images!
if global_step % args.validation_steps == 0:
logger.info(
f"Running validation... \n Generating {args.num_validation_images} videos."
)
# create pipeline
if args.use_ema:
# Store the UNet parameters temporarily and load the EMA parameters to perform inference.
ema_unet.store(unet.parameters())
ema_unet.copy_to(unet.parameters())
log_validation(
vae=vae,
image_encoder=image_encoder,
unet=unet,
pose_net=pose_net,
face_encoder=face_encoder,
app=face_model.app,
face_helper=face_model.face_helper,
handler_ante=face_model.handler_ante,
scheduler=noise_scheduler,
accelerator=accelerator,
feature_extractor=feature_extractor,
width=512,
height=512,
torch_dtype=weight_dtype,
validation_image_folder=args.validation_image_folder,
validation_image=args.validation_image,
validation_control_folder=args.validation_control_folder,
output_dir=args.output_dir,
generator=generator,
global_step=global_step,
num_validation_cases=1,
)
if args.use_ema:
# Switch back to the original UNet parameters.
ema_unet.restore(unet.parameters())
with torch.cuda.device(latents.device):
torch.cuda.empty_cache()
logs = {"step_loss": loss.detach().item(
), "lr": lr_scheduler.get_last_lr()[0]}
progress_bar.set_postfix(**logs)
if global_step >= args.max_train_steps:
break
# save checkpoints!
# if accelerator.is_main_process or accelerator.distributed_type == DistributedType.DEEPSPEED:
if accelerator.is_main_process:
save_path = os.path.join(
args.output_dir, f"checkpoint-last")
accelerator.save_state(save_path)
logger.info(f"Saved state to {save_path}")
def log_validation(
vae,
image_encoder,
unet,
pose_net,
face_encoder,
app,
face_helper,
handler_ante,
scheduler,
accelerator,
feature_extractor,
width,
height,
torch_dtype,
validation_image_folder,
validation_image,
validation_control_folder,
output_dir,
generator,
global_step,
num_validation_cases=1,
):
logger.info("Running validation... ")
validation_unet = accelerator.unwrap_model(unet)
validation_image_encoder = accelerator.unwrap_model(image_encoder)
validation_vae = accelerator.unwrap_model(vae)
validation_pose_net = accelerator.unwrap_model(pose_net)
validation_face_encoder = accelerator.unwrap_model(face_encoder)
pipeline = ValidationAnimationPipeline(
vae=validation_vae,
image_encoder=validation_image_encoder,
unet=validation_unet,
scheduler=scheduler,
feature_extractor=feature_extractor,
pose_net=validation_pose_net,
face_encoder=validation_face_encoder,
)
pipeline = pipeline.to(accelerator.device)
validation_images = load_images_from_folder(validation_image_folder)
validation_image_path = validation_image
if validation_image is None:
validation_image = validation_images[0]
else:
validation_image = Image.open(validation_image).convert('RGB')
validation_control_images = load_images_from_folder(validation_control_folder)
val_save_dir = os.path.join(output_dir, "validation_images")
if not os.path.exists(val_save_dir):
os.makedirs(val_save_dir)
with accelerator.autocast():
for val_img_idx in range(num_validation_cases):
# num_frames = args.num_frames
num_frames = len(validation_control_images)
face_helper.clean_all()
validation_face = cv2.imread(validation_image_path)
# validation_image_bgr = cv2.cvtColor(validation_face, cv2.COLOR_RGB2BGR)
validation_image_face_info = app.get(validation_face)
if len(validation_image_face_info) > 0:
validation_image_face_info = sorted(validation_image_face_info,
key=lambda x: (x['bbox'][2] - x['bbox'][0]) * (
x['bbox'][3] - x['bbox'][1]))[-1]
validation_image_id_ante_embedding = validation_image_face_info['embedding']
else:
validation_image_id_ante_embedding = None
if validation_image_id_ante_embedding is None:
face_helper.read_image(validation_face)
face_helper.get_face_landmarks_5(only_center_face=True)
face_helper.align_warp_face()
if len(face_helper.cropped_faces) == 0:
validation_image_id_ante_embedding = np.zeros((512,))
else:
validation_image_align_face = face_helper.cropped_faces[0]
print('fail to detect face using insightface, extract embedding on align face')
validation_image_id_ante_embedding = handler_ante.get_feat(validation_image_align_face)
video_frames = pipeline(
image=validation_image,
image_pose=validation_control_images,
height=height,
width=width,
num_frames=num_frames,
tile_size=num_frames,
tile_overlap=4,
decode_chunk_size=4,
motion_bucket_id=127.,
fps=7,
min_guidance_scale=3,
max_guidance_scale=3,
noise_aug_strength=0.02,
num_inference_steps=25,
generator=generator,
output_type="pil",
validation_image_id_ante_embedding=validation_image_id_ante_embedding,
).frames[0]
# save_combined_frames(video_frames, validation_images, validation_control_images, val_save_dir)
out_file = os.path.join(
val_save_dir,
f"step_{global_step}_val_img_{val_img_idx}.mp4",
)
# print(video_frames.size()) # [16, 3, 512, 512]
for i in range(num_frames):
img = video_frames[i]
video_frames[i] = np.array(img)
export_to_gif(video_frames, out_file, 8)
del pipeline
torch.cuda.empty_cache()
if __name__ == "__main__":
main()