Spaces:
Runtime error
Runtime error
#!/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.") | |
parser.add_argument( | |
"--save_latents_and_embeddings", | |
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_latents_and_embeddings` 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_latents_and_embeddings` 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 = image.to(dtype=torch.float32).clamp(-1, 1) | |
image = to_pil_image(image.float()) | |
image.save(path) | |
def save_video(video: torch.Tensor, path: pathlib.Path, fps: int = 8) -> None: | |
video = video.to(dtype=torch.float32).clamp(-1, 1) | |
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) | |
def save_metadata(metadata: Dict[str, Any], path: pathlib.Path) -> None: | |
with open(path, "w", encoding="utf-8") as file: | |
file.write(json.dumps(metadata)) | |
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: | |
num_frames, height, width = videos.size(1), videos.size(3), videos.size(4) | |
metadata = [{"num_frames": num_frames, "height": height, "width": width}] | |
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"), | |
(metadata, videos_dir, save_metadata, "txt"), | |
] | |
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().to("cpu") | |
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 | |
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 = None | |
if args.save_image_latents: | |
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}" | |
if args.save_latents_and_embeddings: | |
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 | |
image_latents = None | |
video_latents = None | |
prompt_embeds = None | |
if args.save_image_latents: | |
images = batch["images"].to(device, non_blocking=True) | |
images = images.permute(0, 2, 1, 3, 4) # [B, C, F, H, W] | |
videos = batch["videos"].to(device, non_blocking=True) | |
videos = videos.permute(0, 2, 1, 3, 4) # [B, C, F, H, W] | |
prompts = batch["prompts"] | |
# Encode videos & images | |
if args.save_latents_and_embeddings: | |
if args.use_slicing: | |
if args.save_image_latents: | |
encoded_slices = [vae._encode(image_slice) for image_slice in images.split(1)] | |
image_latents = torch.cat(encoded_slices) | |
image_latents = image_latents.to(memory_format=torch.contiguous_format, dtype=weight_dtype) | |
encoded_slices = [vae._encode(video_slice) for video_slice in videos.split(1)] | |
video_latents = torch.cat(encoded_slices) | |
else: | |
if args.save_image_latents: | |
image_latents = vae._encode(images) | |
image_latents = image_latents.to(memory_format=torch.contiguous_format, dtype=weight_dtype) | |
video_latents = vae._encode(videos) | |
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, | |
) | |
if images is not None: | |
images = (images.permute(0, 2, 1, 3, 4) + 1) / 2 | |
videos = (videos.permute(0, 2, 1, 3, 4) + 1) / 2 | |
output_queue.put( | |
{ | |
"batch_size": len(prompts), | |
"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"), | |
("videos", "txt"), | |
]: | |
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}.mp4\n") | |
with open(data_jsonl, "w") as file: | |
for prompt, stem in zip(prompts, stems): | |
video_metadata_txt = output_dir.joinpath(f"videos/{stem}.txt") | |
with open(video_metadata_txt, "r", encoding="utf-8") as metadata_file: | |
metadata = json.loads(metadata_file.read()) | |
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", | |
"metadata": metadata, | |
} | |
file.write(json.dumps(data) + "\n") | |
print(f"Completed preprocessing. All files saved to `{output_dir.as_posix()}`") | |
if __name__ == "__main__": | |
main() | |