Files
FineTrainers-Conditioning/training/prepare_dataset.py
T
Aryan feb2e26547 Improve dataset preparation support + multiresolution prep (#39)
* update

* make style

* renormalize correctly

* apply suggestions from review

* apply suggestions from review

* update
2024-10-17 16:48:52 +05:30

640 lines
22 KiB
Python

#!/usr/bin/env python3
import argparse
import functools
import json
import os
import pathlib
import queue
import traceback
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Optional, Union
import torch
import torch.distributed as dist
from diffusers import AutoencoderKLCogVideoX
from diffusers.training_utils import set_seed
from diffusers.utils import export_to_video, get_logger
from torch.utils.data import DataLoader
from torchvision import transforms
from tqdm import tqdm
from transformers import T5EncoderModel, T5Tokenizer
import decord # isort:skip
from dataset import BucketSampler, VideoDatasetWithResizing, VideoDatasetWithResizeAndRectangleCrop # isort:skip
decord.bridge.set_bridge("torch")
logger = get_logger(__name__)
DTYPE_MAPPING = {
"fp32": torch.float32,
"fp16": torch.float16,
"bf16": torch.bfloat16,
}
def check_height(x: Any) -> int:
x = int(x)
if x % 16 != 0:
raise argparse.ArgumentTypeError(f"`--height_buckets` must be divisible by 16, but got {x} which does not fit criteria.")
return x
def check_width(x: Any) -> int:
x = int(x)
if x % 16 != 0:
raise argparse.ArgumentTypeError(f"`--width_buckets` must be divisible by 16, but got {x} which does not fit criteria.")
return x
def check_frames(x: Any) -> int:
x = int(x)
if x % 4 != 0 and x % 4 != 1:
raise argparse.ArgumentTypeError(f"`--frames_buckets` must be of form `4 * k` or `4 * k + 1`, but got {x} which does not fit criteria.")
return x
def get_args() -> Dict[str, Any]:
parser = argparse.ArgumentParser()
parser.add_argument(
"--model_id",
type=str,
default="THUDM/CogVideoX-2b",
help="Hugging Face model ID to use for tokenizer, text encoder and VAE.",
)
parser.add_argument("--data_root", type=str, required=True, help="Path to where training data is located.")
parser.add_argument(
"--dataset_file", type=str, default=None, help="Path to CSV file containing metadata about training data."
)
parser.add_argument(
"--caption_column",
type=str,
default="caption",
help="If using a CSV file via the `--dataset_file` argument, this should be the name of the column containing the captions. If using the folder structure format for data loading, this should be the name of the file containing line-separated captions (the file should be located in `--data_root`).",
)
parser.add_argument(
"--video_column",
type=str,
default="video",
help="If using a CSV file via the `--dataset_file` argument, this should be the name of the column containing the video paths. If using the folder structure format for data loading, this should be the name of the file containing line-separated video paths (the file should be located in `--data_root`).",
)
parser.add_argument(
"--id_token",
type=str,
default=None,
help="Identifier token appended to the start of each prompt if provided.",
)
parser.add_argument(
"--height_buckets",
nargs="+",
type=check_height,
default=[256, 320, 384, 480, 512, 576, 720, 768, 960, 1024, 1280, 1536],
)
parser.add_argument(
"--width_buckets",
nargs="+",
type=check_width,
default=[256, 320, 384, 480, 512, 576, 720, 768, 960, 1024, 1280, 1536],
)
parser.add_argument(
"--frame_buckets",
nargs="+",
type=check_frames,
default=[49],
)
parser.add_argument(
"--random_flip",
type=float,
default=None,
help="If random horizontal flip augmentation is to be used, this should be the flip probability.",
)
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(
"--pin_memory",
action="store_true",
help="Whether or not to use the pinned memory setting in pytorch dataloader.",
)
parser.add_argument(
"--video_reshape_mode",
type=str,
default=None,
help="All input videos are reshaped to this mode. Choose between ['center', 'random', 'none']",
)
parser.add_argument(
"--save_image_latents",
action="store_true",
help="Whether or not to encode and store image latents, which are required for image-to-video finetuning. The image latents are the first frame of input videos encoded with the VAE.",
)
parser.add_argument(
"--output_dir",
type=str,
required=True,
help="Path to output directory where preprocessed videos/latents/embeddings will be saved.",
)
parser.add_argument("--max_num_frames", type=int, default=49, help="Maximum number of frames in output video.")
parser.add_argument(
"--max_sequence_length", type=int, default=226, help="Max sequence length of prompt embeddings."
)
parser.add_argument(
"--target_fps", type=int, default=8, help="Frame rate of output videos if `--save_tensors` is unspecified."
)
parser.add_argument(
"--save_tensors",
action="store_true",
help="Whether to encode videos/captions to latents/embeddings and save them in pytorch serializable format.",
)
parser.add_argument(
"--use_slicing",
action="store_true",
help="Whether to enable sliced encoding/decoding in the VAE. Only used if `--save_tensors` is also used.",
)
parser.add_argument(
"--use_tiling",
action="store_true",
help="Whether to enable tiled encoding/decoding in the VAE. Only used if `--save_tensors` is also used.",
)
parser.add_argument("--batch_size", type=int, default=1, help="Number of videos to process at once in the VAE.")
parser.add_argument(
"--num_decode_threads",
type=int,
default=0,
help="Number of decoding threads for `decord` to use. The default `0` means to automatically determine required number of threads.",
)
parser.add_argument(
"--dtype",
type=str,
choices=["fp32", "fp16", "bf16"],
default="fp32",
help="Data type to use when generating latents and prompt embeddings.",
)
parser.add_argument("--seed", type=int, default=42, help="Seed for reproducibility.")
parser.add_argument(
"--num_artifact_workers", type=int, default=4, help="Number of worker threads for serializing artifacts."
)
return parser.parse_args()
def _get_t5_prompt_embeds(
tokenizer: T5Tokenizer,
text_encoder: T5EncoderModel,
prompt: Union[str, List[str]],
num_videos_per_prompt: int = 1,
max_sequence_length: int = 226,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
text_input_ids=None,
):
prompt = [prompt] if isinstance(prompt, str) else prompt
batch_size = len(prompt)
if tokenizer is not None:
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
else:
if text_input_ids is None:
raise ValueError("`text_input_ids` must be provided when the tokenizer is not specified.")
prompt_embeds = text_encoder(text_input_ids.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 prompt_embeds
def encode_prompt(
tokenizer: T5Tokenizer,
text_encoder: T5EncoderModel,
prompt: Union[str, List[str]],
num_videos_per_prompt: int = 1,
max_sequence_length: int = 226,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
text_input_ids=None,
):
prompt = [prompt] if isinstance(prompt, str) else prompt
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,
text_input_ids=text_input_ids,
)
return prompt_embeds
def compute_prompt_embeddings(
tokenizer: T5Tokenizer,
text_encoder: T5EncoderModel,
prompts: List[str],
max_sequence_length: int,
device: torch.device,
dtype: torch.dtype,
requires_grad: bool = False,
):
if requires_grad:
prompt_embeds = encode_prompt(
tokenizer,
text_encoder,
prompts,
num_videos_per_prompt=1,
max_sequence_length=max_sequence_length,
device=device,
dtype=dtype,
)
else:
with torch.no_grad():
prompt_embeds = encode_prompt(
tokenizer,
text_encoder,
prompts,
num_videos_per_prompt=1,
max_sequence_length=max_sequence_length,
device=device,
dtype=dtype,
)
return prompt_embeds
to_pil_image = transforms.ToPILImage(mode="RGB")
def save_image(image: torch.Tensor, path: pathlib.Path) -> None:
image = to_pil_image(image)
image.save(path)
def save_video(video: torch.Tensor, path: pathlib.Path, fps: int = 8) -> None:
video = [to_pil_image(frame) for frame in video]
export_to_video(video, path, fps=fps)
def save_prompt(prompt: str, path: pathlib.Path) -> None:
with open(path, "w", encoding="utf=8") as file:
file.write(prompt)
@torch.no_grad()
def serialize_artifacts(
batch_size: int,
fps: int,
images_dir: Optional[pathlib.Path] = None,
image_latents_dir: Optional[pathlib.Path] = None,
videos_dir: Optional[pathlib.Path] = None,
video_latents_dir: Optional[pathlib.Path] = None,
prompts_dir: Optional[pathlib.Path] = None,
prompt_embeds_dir: Optional[pathlib.Path] = None,
images: Optional[torch.Tensor] = None,
image_latents: Optional[torch.Tensor] = None,
videos: Optional[torch.Tensor] = None,
video_latents: Optional[torch.Tensor] = None,
prompts: Optional[List[str]] = None,
prompt_embeds: Optional[torch.Tensor] = None,
) -> None:
if images is not None:
images = (images.permute(0, 2, 1, 3, 4) + 1) / 2
if videos is not None:
videos = (videos.permute(0, 2, 1, 3, 4) + 1) / 2
data_folder_mapper_list = [
(images, images_dir, lambda img, path: save_image(img[0], path), "png"),
(image_latents, image_latents_dir, torch.save, "pt"),
(videos, videos_dir, functools.partial(save_video, fps=fps), "mp4"),
(video_latents, video_latents_dir, torch.save, "pt"),
(prompts, prompts_dir, save_prompt, "txt"),
(prompt_embeds, prompt_embeds_dir, torch.save, "pt"),
]
filenames = [uuid.uuid4() for _ in range(batch_size)]
for data, folder, save_fn, extension in data_folder_mapper_list:
if data is None:
continue
for slice, filename in zip(data, filenames):
if isinstance(slice, torch.Tensor):
slice = slice.clone()
path = folder.joinpath(f"{filename}.{extension}")
save_fn(slice, path)
def save_intermediates(output_queue: queue.Queue) -> None:
while True:
try:
item = output_queue.get(timeout=30)
if item is None:
break
serialize_artifacts(**item)
except queue.Empty:
continue
@torch.no_grad()
def main():
args = get_args()
set_seed(args.seed)
output_dir = pathlib.Path(args.output_dir)
tmp_dir = output_dir.joinpath("tmp")
output_dir.mkdir(parents=True, exist_ok=True)
tmp_dir.mkdir(parents=True, exist_ok=True)
# Create task queue for non-blocking serializing of artifacts
output_queue = queue.Queue()
save_thread = ThreadPoolExecutor(max_workers=args.num_artifact_workers)
save_future = save_thread.submit(save_intermediates, output_queue)
# Initialize distributed processing
if "LOCAL_RANK" in os.environ:
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="nccl")
world_size = dist.get_world_size()
rank = dist.get_rank()
else:
# Single GPU
local_rank = 0
world_size = 1
rank = 0
torch.cuda.set_device(rank)
# Create folders where intermediate tensors from each rank will be saved
images_dir = tmp_dir.joinpath(f"images/{rank}")
image_latents_dir = tmp_dir.joinpath(f"image_latents/{rank}")
videos_dir = tmp_dir.joinpath(f"videos/{rank}")
video_latents_dir = tmp_dir.joinpath(f"video_latents/{rank}")
prompts_dir = tmp_dir.joinpath(f"prompts/{rank}")
prompt_embeds_dir = tmp_dir.joinpath(f"prompt_embeds/{rank}")
images_dir.mkdir(parents=True, exist_ok=True)
image_latents_dir.mkdir(parents=True, exist_ok=True)
videos_dir.mkdir(parents=True, exist_ok=True)
video_latents_dir.mkdir(parents=True, exist_ok=True)
prompts_dir.mkdir(parents=True, exist_ok=True)
prompt_embeds_dir.mkdir(parents=True, exist_ok=True)
weight_dtype = DTYPE_MAPPING[args.dtype]
target_fps = args.target_fps
# 1. Dataset
dataset_init_kwargs = {
"data_root": args.data_root,
"dataset_file": args.dataset_file,
"caption_column": args.caption_column,
"video_column": args.video_column,
"max_num_frames": args.max_num_frames,
"id_token": args.id_token,
"height_buckets": args.height_buckets,
"width_buckets": args.width_buckets,
"frame_buckets": args.frame_buckets,
"load_tensors": False,
"random_flip": args.random_flip,
"image_to_video": args.save_image_latents,
}
if args.video_reshape_mode is None:
dataset = VideoDatasetWithResizing(**dataset_init_kwargs)
else:
dataset = VideoDatasetWithResizeAndRectangleCrop(
video_reshape_mode=args.video_reshape_mode, **dataset_init_kwargs
)
original_dataset_size = len(dataset)
# Split data among GPUs
if world_size > 1:
samples_per_gpu = original_dataset_size // world_size
start_index = rank * samples_per_gpu
end_index = start_index + samples_per_gpu
if rank == world_size - 1:
end_index = original_dataset_size # Make sure the last GPU gets the remaining data
# Slice the data
dataset.prompts = dataset.prompts[start_index:end_index]
dataset.video_paths = dataset.video_paths[start_index:end_index]
else:
pass
rank_dataset_size = len(dataset)
# 2. Dataloader
def collate_fn(data):
prompts = [x["prompt"] for x in data[0]]
images = [x["image"] for x in data[0]]
images = torch.stack(images).to(dtype=weight_dtype, non_blocking=True)
videos = [x["video"] for x in data[0]]
videos = torch.stack(videos).to(dtype=weight_dtype, non_blocking=True)
return {
"images": images,
"videos": videos,
"prompts": prompts,
}
dataloader = DataLoader(
dataset,
batch_size=1,
sampler=BucketSampler(dataset, batch_size=args.batch_size, shuffle=True, drop_last=False),
collate_fn=collate_fn,
num_workers=args.dataloader_num_workers,
pin_memory=args.pin_memory,
)
# 3. Prepare models
device = f"cuda:{rank}"
generator = torch.Generator(device).manual_seed(args.seed)
tokenizer = T5Tokenizer.from_pretrained(args.model_id, subfolder="tokenizer")
text_encoder = T5EncoderModel.from_pretrained(args.model_id, subfolder="text_encoder", torch_dtype=weight_dtype)
text_encoder = text_encoder.to(device)
vae = AutoencoderKLCogVideoX.from_pretrained(args.model_id, subfolder="vae", torch_dtype=weight_dtype)
vae = vae.to(device)
if args.use_slicing:
vae.enable_slicing()
if args.use_tiling:
vae.enable_tiling()
# 4. Compute latents and embeddings and save
if rank == 0:
iterator = tqdm(
dataloader, desc="Encoding", total=(rank_dataset_size + args.batch_size - 1) // args.batch_size
)
else:
iterator = dataloader
for step, batch in enumerate(iterator):
try:
images = None
if args.save_image_latents:
images = batch["images"].to(device, non_blocking=True)
videos = batch["videos"].to(device, non_blocking=True)
prompts = batch["prompts"]
# Encode videos & images
image_latents = None
if args.save_image_latents:
images = images.permute(0, 2, 1, 3, 4) # [B, C, F, H, W]
image_noise_sigma = torch.normal(
mean=-3.0, std=0.5, size=(images.size(0),), generator=generator, device=device, dtype=weight_dtype
)
image_noise_sigma = torch.exp(image_noise_sigma)
noisy_images = images + torch.empty_like(images).normal_(generator=generator) * image_noise_sigma[:, None, None, None, None]
image_latent_dist = vae.encode(noisy_images).latent_dist
image_latents = image_latent_dist.sample() * vae.config.scaling_factor
image_latents = image_latents.permute(0, 2, 1, 3, 4) # [B, F, C, H, W]
image_latents = image_latents.to(memory_format=torch.contiguous_format, dtype=weight_dtype)
videos = videos.permute(0, 2, 1, 3, 4) # [B, C, F, H, W]
latent_dist = vae.encode(videos).latent_dist
video_latents = latent_dist.sample(generator=generator) * vae.config.scaling_factor
video_latents = video_latents.permute(0, 2, 1, 3, 4) # [B, F, C, H, W]
video_latents = video_latents.to(memory_format=torch.contiguous_format, dtype=weight_dtype)
# Encode prompts
prompt_embeds = compute_prompt_embeddings(
tokenizer,
text_encoder,
prompts,
args.max_sequence_length,
device,
weight_dtype,
requires_grad=False,
)
output_queue.put(
{
"batch_size": prompt_embeds.shape[0],
"fps": target_fps,
"images_dir": images_dir,
"image_latents_dir": image_latents_dir,
"videos_dir": videos_dir,
"video_latents_dir": video_latents_dir,
"prompts_dir": prompts_dir,
"prompt_embeds_dir": prompt_embeds_dir,
"images": images,
"image_latents": image_latents,
"videos": videos,
"video_latents": video_latents,
"prompts": prompts,
"prompt_embeds": prompt_embeds,
}
)
except Exception:
print("-------------------------")
print(f"An exception occurred while processing data: {rank=}, {world_size=}, {step=}")
traceback.print_exc()
print("-------------------------")
# 5. Complete distributed processing
if world_size > 1:
dist.barrier()
dist.destroy_process_group()
output_queue.put(None)
save_thread.shutdown(wait=True)
save_future.result()
# 6. Combine results from each rank
if rank == 0:
print(
f"Completed preprocessing latents and embeddings. Temporary files from all ranks saved to `{tmp_dir.as_posix()}`"
)
# Move files from each rank to common directory
for subfolder, extension in [
("images", "png"),
("image_latents", "pt"),
("videos", "mp4"),
("video_latents", "pt"),
("prompts", "txt"),
("prompt_embeds", "pt"),
]:
tmp_subfolder = tmp_dir.joinpath(subfolder)
combined_subfolder = output_dir.joinpath(subfolder)
combined_subfolder.mkdir(parents=True, exist_ok=True)
pattern = f"*.{extension}"
for file in tmp_subfolder.rglob(pattern):
file.replace(combined_subfolder / file.name)
# Remove temporary directories
def rmdir_recursive(dir: pathlib.Path) -> None:
for child in dir.iterdir():
if child.is_file():
child.unlink()
else:
rmdir_recursive(child)
dir.rmdir()
rmdir_recursive(tmp_dir)
# Combine prompts and videos into individual text files and single jsonl
prompts_folder = output_dir.joinpath("prompts")
prompts = []
stems = []
for filename in prompts_folder.rglob("*.txt"):
with open(filename, "r") as file:
prompts.append(file.read().strip())
stems.append(filename.stem)
prompts_txt = output_dir.joinpath("prompts.txt")
videos_txt = output_dir.joinpath("videos.txt")
data_jsonl = output_dir.joinpath("data.jsonl")
with open(prompts_txt, "w") as file:
for prompt in prompts:
file.write(f"{prompt}\n")
with open(videos_txt, "w") as file:
for stem in stems:
file.write(f"videos/{stem}.pt\n")
with open(data_jsonl, "w") as file:
for prompt, stem in zip(prompts, stems):
data = {
"prompt": prompt,
"prompt_embed": f"prompt_embeds/{stem}.pt",
"image": f"images/{stem}.png",
"image_latent": f"image_latents/{stem}.pt",
"video": f"videos/{stem}.mp4",
"video_latent": f"video_latents/{stem}.pt",
}
file.write(json.dumps(data) + "\n")
print(
f"Completed preprocessing. All files saved to `{output_dir.as_posix()}`"
)
if __name__ == "__main__":
main()