# Adopted from https://github.com/guandeh17/Self-Forcing # SPDX-License-Identifier: Apache-2.0 import os import sys # torchrun no longer consistently prepends the script directory to sys.path, # which breaks absolute project imports when launched from another cwd. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # torchvision 0.27+ removed write_video/read_video. Several modules import the # symbols at module import time, so patch them before importing project code. import torchvision.io as _tv_io if not hasattr(_tv_io, "write_video"): import imageio.v2 as _imageio_v2 def _shim_write_video(filename, video_array, fps, **_unused): if hasattr(video_array, "detach"): video_array = video_array.detach().cpu().numpy() _imageio_v2.mimwrite(filename, video_array, fps=fps, codec="libx264", quality=8) _tv_io.write_video = _shim_write_video if not hasattr(_tv_io, "read_video"): import imageio.v3 as _imageio_v3 import torch as _torch_for_shim def _shim_read_video(filename, pts_unit="sec", output_format="THWC", **_unused): frames = _imageio_v3.imread(filename, plugin="pyav") tensor = _torch_for_shim.from_numpy(frames) if output_format == "TCHW": tensor = tensor.permute(0, 3, 1, 2) return tensor, None, {} _tv_io.read_video = _shim_read_video import argparse import torch from omegaconf import OmegaConf from tqdm import tqdm from torchvision.io import write_video from einops import rearrange import torch.distributed as dist from torch.utils.data import DataLoader, SequentialSampler from torch.utils.data.distributed import DistributedSampler from pipeline import CausalDiffusionInferencePipeline from utils.dataset import MultiTextConcatDataset, MultiVideoConcatDataset, eval_collate_fn, multi_video_collate_fn from utils.misc import set_seed from utils.config import normalize_config, section_get, wan_default_config from utils.nvfp4_checkpoint import ( clean_fsdp_state_dict_keys, drop_fouroversix_master_weights, is_nvfp4_state_dict, is_te_nvfp4_checkpoint, quantize_model_for_fouroversix_nvfp4, unwrap_generator_state_dict, ) from utils.memory import get_cuda_free_memory_gb, DynamicSwapInstaller def save_prompts_to_txt(prompts_for_sample, prompt_txt_path: str, is_main_process: bool): """Save per-block prompts alongside the video. Consecutive identical prompts are merged, e.g.: [0] a, [1] a, [2] b => [0,1] a\\n[2] b\\n """ try: with open(prompt_txt_path, "w", encoding="utf-8") as f: if len(prompts_for_sample) == 0: return current_prompt = prompts_for_sample[0] current_indices = [0] for seg_idx in range(1, len(prompts_for_sample)): p = prompts_for_sample[seg_idx] if p == current_prompt: current_indices.append(seg_idx) else: indices_str = ",".join(str(i) for i in current_indices) f.write(f"[{indices_str}] {current_prompt}\n") current_prompt = p current_indices = [seg_idx] indices_str = ",".join(str(i) for i in current_indices) f.write(f"[{indices_str}] {current_prompt}\n") except Exception as e: if is_main_process: print(f"Warning: failed to save prompts to {prompt_txt_path}: {e}") parser = argparse.ArgumentParser() parser.add_argument("--config_path", type=str, help="Path to the config file") te_quant_group = parser.add_mutually_exclusive_group() te_quant_group.add_argument( "--use_te_quant", dest="use_te_quant", action="store_true", help="Override config and enable TransformerEngine quantization", ) te_quant_group.add_argument( "--no_use_te_quant", dest="use_te_quant", action="store_false", help="Override config and disable TransformerEngine quantization", ) parser.set_defaults(use_te_quant=None) args = parser.parse_args() config = normalize_config(OmegaConf.load(args.config_path)) if args.use_te_quant is not None: config.model_quant_use_transformer_engine = args.use_te_quant if not hasattr(config, "sampling_steps") or config.sampling_steps is None: raise ValueError("sampling_steps must be defined in the inference config") if not hasattr(config, "guidance_scale") or config.guidance_scale is None: config.guidance_scale = 1.0 config.use_ema = section_get(config, "inference", "use_ema", getattr(config, "use_ema", False)) config.output_folder = section_get(config, "inference", "output_folder", getattr(config, "output_folder", "videos/longlive2")) config.num_samples = section_get(config, "inference", "num_samples", getattr(config, "num_samples", 1)) config.num_output_frames = getattr(config, "num_output_frames", config.image_or_video_shape[1]) config.save_with_index = getattr(config, "save_with_index", False) config.inference_iter = getattr(config, "inference_iter", -1) if bool(getattr(config, "fp8_quant", False)) and bool( getattr(config, "model_quant", False) ): raise ValueError("fp8_quant and model_quant (NVFP4) are mutually exclusive.") def _maybe_to_dict(value): if value is None: return None if OmegaConf.is_config(value): value = OmegaConf.to_container(value, resolve=True) return dict(value) def _config_bool(value, default=False): if value is None: return default if isinstance(value, str): return value.strip().lower() in {"1", "true", "yes", "y", "on"} return bool(value) def _expected_inference_samples(config): inference_iter = int(getattr(config, "inference_iter", -1)) if inference_iter >= 0: return inference_iter + 1 return None def _resolve_torch_compile(config): setting = getattr(config, "torch_compile", False) if isinstance(setting, str) and setting.strip().lower() == "auto": if not ( bool(getattr(config, "model_quant", False)) or bool(getattr(config, "fp8_quant", False)) ): return False, "auto disabled because quantization is false" min_samples = int(getattr(config, "torch_compile_min_samples", 2)) expected_samples = _expected_inference_samples(config) if expected_samples is not None and expected_samples < min_samples: return ( False, "auto disabled because expected samples " f"({expected_samples}) < torch_compile_min_samples ({min_samples})", ) return True, "auto enabled for repeated quantized inference" return _config_bool(setting, default=False), "explicit setting" def quantize_generator_model(model, config, keep_master_weights): from utils.quant import ( ModelQuantizationConfig, _materialize_mixed_quantized_weights_for_inference, _materialize_quantized_weights_for_inference, _materialize_transformer_engine_weights_for_inference, quantize_model_with_filter, ) use_transformer_engine = bool(getattr(config, "model_quant_use_transformer_engine", False)) te_inference_only = bool(getattr(config, "model_quant_te_inference_only", use_transformer_engine)) te_low_precision_weights = bool(getattr(config, "model_quant_te_low_precision_weights", te_inference_only)) te_fallback_to_fouroversix = bool(getattr(config, "model_quant_te_fallback_to_fouroversix", False)) quant_cfg = ModelQuantizationConfig( scale_rule=getattr(config, "model_quant_scale_rule", "static_6"), quantize_backend=getattr(config, "model_quant_backend", None), activation_scale_rule=getattr( config, "model_quant_activation_scale_rule", getattr(config, "model_quant_scale_rule", "static_6"), ), weight_scale_rule=getattr(config, "model_quant_weight_scale_rule", None), gradient_scale_rule=getattr(config, "model_quant_gradient_scale_rule", None), ) quant_cfg.keep_master_weights = keep_master_weights model, matched_modules = quantize_model_with_filter( model, quant_config=quant_cfg, filtered_modules=getattr(config, "model_quant_filtered_modules", None), use_default_filtered_modules=getattr(config, "model_quant_use_default_filtered_modules", True), cast_model_to_bf16=True, materialize_for_inference=False, use_transformer_engine=use_transformer_engine, te_inference_only=te_inference_only, te_low_precision_weights=te_low_precision_weights, te_recipe_kwargs=_maybe_to_dict(getattr(config, "model_quant_te_recipe_kwargs", None)), te_module_kwargs=_maybe_to_dict(getattr(config, "model_quant_te_module_kwargs", None)), te_fallback_to_fouroversix=te_fallback_to_fouroversix, verbose=True, ) materialize_fn = _materialize_quantized_weights_for_inference if use_transformer_engine and te_fallback_to_fouroversix: materialize_fn = _materialize_mixed_quantized_weights_for_inference elif use_transformer_engine: materialize_fn = _materialize_transformer_engine_weights_for_inference if local_rank == 0: print(f"[NVFP4] Generator quantized; {len(matched_modules)} modules excluded") return model, materialize_fn def materialize_quantized_generator(model, device, materialize_fn, stage_desc): mat_modules, master_bytes, quantized_bytes = materialize_fn( model, target_device=device, ) if local_rank == 0: print( f"[NVFP4] Materialized quantized generator weights {stage_desc}: " f"{len(mat_modules)} modules, " f"master_weight={master_bytes / (1024 ** 3):.3f} GiB, " f"quantized_weight={quantized_bytes / (1024 ** 3):.3f} GiB" ) def configure_generator_torch_compile(pipeline, config): compile_enabled, reason = _resolve_torch_compile(config) if not compile_enabled: if local_rank == 0 and str(getattr(config, "torch_compile", "false")).lower() == "auto": print(f"[torch.compile] skipped: {reason}") return target = str(getattr(config, "torch_compile_target", "generator_model")).lower() if target not in {"generator_model", "model"}: if local_rank == 0: print(f"[torch.compile][warn] Unsupported target={target}; expected generator_model") return if not hasattr(pipeline.generator, "configure_torch_compile"): if local_rank == 0: print("[torch.compile][warn] Current generator does not expose configure_torch_compile; skipping") return compiled = pipeline.generator.configure_torch_compile( backend=str(getattr(config, "torch_compile_backend", "inductor")), mode=getattr(config, "torch_compile_mode", "max-autotune-no-cudagraphs"), fullgraph=_config_bool(getattr(config, "torch_compile_fullgraph", False)), dynamic=_config_bool(getattr(config, "torch_compile_dynamic", False)), options=_maybe_to_dict(getattr(config, "torch_compile_options", None)), suppress_errors=_config_bool(getattr(config, "torch_compile_suppress_errors", True), default=True), ) if local_rank == 0: status = "enabled" if compiled else "not enabled" print(f"[torch.compile] {status}: target={target}") # Initialize distributed inference if "LOCAL_RANK" in os.environ: dist.init_process_group(backend='nccl') local_rank = int(os.environ["LOCAL_RANK"]) torch.cuda.set_device(local_rank) device = torch.device(f"cuda:{local_rank}") set_seed(config.seed + local_rank) config.distributed = True # Mark as distributed for pipeline else: local_rank = 0 device = torch.device("cuda") set_seed(config.seed) config.distributed = False # Mark as non-distributed print(f'Free VRAM {get_cuda_free_memory_gb(device)} GB') low_memory = get_cuda_free_memory_gb(device) < 40 torch.set_grad_enabled(False) # Initialize pipeline pipeline = CausalDiffusionInferencePipeline(config, device=device) # --------------------------- LoRA support (optional) --------------------------- from utils.lora_utils import configure_lora_for_model import peft merge_lora = bool(getattr(config, "merge_lora", False)) has_lora_adapter = bool(getattr(config, "adapter", None) and configure_lora_for_model is not None) if has_lora_adapter and ( bool(getattr(config, "model_quant", False)) or bool(getattr(config, "fp8_quant", False)) ) and not merge_lora: if local_rank == 0: print( "[quant][LoRA] merge_lora=false is unsupported with quantization; " "forcing merge_lora=true so the LoRA is folded into the BF16 base before quantization." ) merge_lora = True config.merge_lora = True materialize_quantized_weights_for_inference = None generator_checkpoint = None generator_lora_state = None generator_ckpt_path = getattr(config, "generator_ckpt", None) loaded_prequantized_generator = False prequantized_generator_backend = None if generator_ckpt_path: generator_checkpoint = torch.load(generator_ckpt_path, map_location="cpu") is_lora_only_checkpoint = ( isinstance(generator_checkpoint, dict) and "generator_lora" in generator_checkpoint and not any(key in generator_checkpoint for key in ("generator", "generator_ema", "model")) ) if is_lora_only_checkpoint: generator_lora_state = generator_checkpoint["generator_lora"] if local_rank == 0: print(f"Found LoRA generator weights in {generator_ckpt_path}") else: raw_gen_state_dict = unwrap_generator_state_dict(generator_checkpoint, use_ema=config.use_ema) if config.use_ema: raw_gen_state_dict = clean_fsdp_state_dict_keys(raw_gen_state_dict) if is_te_nvfp4_checkpoint(generator_checkpoint): raise ValueError( "This checkpoint was saved as a TransformerEngine module state_dict, " "which is not packed NVFP4 and is no longer a supported export format. " "Regenerate with `--backend transformer_engine` to save merged bf16 weights " "for TE runtime quantization, or use `--backend fouroversix` for a compact " "materialized NVFP4 checkpoint." ) elif is_nvfp4_state_dict(raw_gen_state_dict): if not getattr(config, "model_quant", False): raise ValueError( "generator_ckpt is a materialized NVFP4 checkpoint, but model_quant is false. " "Set model_quant: true in the inference yaml." ) if getattr(config, "model_quant_use_transformer_engine", False): raise ValueError( "Materialized NVFP4 generator checkpoints use FourOverSix modules. " "Set model_quant_use_transformer_engine: false when loading this checkpoint." ) if local_rank == 0: print(f"[NVFP4] Loading materialized generator checkpoint from {generator_ckpt_path}") pipeline.generator.model, matched_modules = quantize_model_for_fouroversix_nvfp4( pipeline.generator.model, config=config, keep_master_weights=False, verbose=(local_rank == 0), ) dropped_modules = drop_fouroversix_master_weights(pipeline.generator.model) pipeline.generator.load_state_dict(raw_gen_state_dict, strict=True) loaded_prequantized_generator = True prequantized_generator_backend = "fouroversix" if local_rank == 0: print( "[NVFP4] Prepared quantized generator architecture: " f"{len(dropped_modules)} materialized modules, " f"{len(matched_modules)} modules excluded" ) elif config.use_ema: missing, unexpected = pipeline.generator.load_state_dict(raw_gen_state_dict, strict=False) if local_rank == 0: if len(missing) > 0: print(f"[Warning] {len(missing)} parameters are missing when loading checkpoint: {missing[:8]} ...") if len(unexpected) > 0: print(f"[Warning] {len(unexpected)} unexpected parameters encountered when loading checkpoint: {unexpected[:8]} ...") else: print(f"Loading generator from {generator_ckpt_path}") pipeline.generator.load_state_dict(raw_gen_state_dict, strict=True) pipeline.is_lora_enabled = False pipeline.is_lora_merged = False if loaded_prequantized_generator: if has_lora_adapter or merge_lora or getattr(config, "lora_ckpt", None): if local_rank == 0: print("[NVFP4] Pre-quantized generator checkpoint is already saved with merged weights; skipping LoRA setup") has_lora_adapter = False merge_lora = False config.merge_lora = False if getattr(config, "model_quant", False) and not merge_lora and not loaded_prequantized_generator: pipeline.generator.model, materialize_quantized_weights_for_inference = quantize_generator_model( pipeline.generator.model, config=config, keep_master_weights=has_lora_adapter, ) if has_lora_adapter: if local_rank == 0: print(f"LoRA enabled with config: {config.adapter}") print("Applying LoRA to generator (inference)...") if merge_lora: print("LoRA weights will be merged into the base model before inference") # Apply LoRA to the generator transformer after loading base weights. pipeline.generator.model = configure_lora_for_model( pipeline.generator.model, model_name="generator", lora_config=config.adapter, is_main_process=(local_rank == 0), ) # Load LoRA weights from lora_ckpt. If omitted, fall back to generator_ckpt # when it directly contains generator_lora. lora_ckpt_path = getattr(config, "lora_ckpt", None) if lora_ckpt_path: if local_rank == 0: print(f"Loading LoRA weights from lora_ckpt: {lora_ckpt_path}") lora_checkpoint = torch.load(lora_ckpt_path, map_location="cpu") if isinstance(lora_checkpoint, dict) and "generator_lora" in lora_checkpoint: peft.set_peft_model_state_dict(pipeline.generator.model, lora_checkpoint["generator_lora"]) # type: ignore else: peft.set_peft_model_state_dict(pipeline.generator.model, lora_checkpoint) # type: ignore if local_rank == 0: print("LoRA weights loaded for generator") elif generator_lora_state is not None: if local_rank == 0: print(f"Loading LoRA weights from generator_ckpt: {generator_ckpt_path}") peft.set_peft_model_state_dict(pipeline.generator.model, generator_lora_state) # type: ignore if local_rank == 0: print("LoRA weights loaded for generator") else: if local_rank == 0: print("No LoRA checkpoint configured; using initialized LoRA adapters") if merge_lora: if local_rank == 0: print("Merging LoRA weights into generator before quantization / inference...") pipeline.generator.model = pipeline.generator.model.merge_and_unload(safe_merge=True) pipeline.is_lora_merged = True else: pipeline.is_lora_enabled = True elif merge_lora and local_rank == 0: print("merge_lora=True requested but no adapter config was found; continuing without LoRA merge") del generator_checkpoint # Move pipeline to appropriate dtype and device if loaded_prequantized_generator: pipeline.text_encoder.to(dtype=torch.bfloat16) pipeline.vae.to(dtype=torch.bfloat16) else: pipeline = pipeline.to(dtype=torch.bfloat16) if low_memory: DynamicSwapInstaller.install_model(pipeline.text_encoder, device=device) pipeline.generator.to(device=device) if getattr(config, "model_quant", False) and not loaded_prequantized_generator: if merge_lora: pipeline.generator.model, materialize_quantized_weights_for_inference = quantize_generator_model( pipeline.generator.model, config=config, keep_master_weights=False, ) stage_desc = "after LoRA merge" if pipeline.is_lora_merged else "for inference" else: stage_desc = "after LoRA wrapping" if pipeline.is_lora_enabled else "for inference" materialize_quantized_generator( pipeline.generator.model, device=device, materialize_fn=materialize_quantized_weights_for_inference, stage_desc=stage_desc, ) elif loaded_prequantized_generator and local_rank == 0: print(f"[NVFP4] Using pre-saved {prequantized_generator_backend} generator weights from checkpoint") pipeline.generator.model.eval().requires_grad_(False) if bool(getattr(config, "fp8_quant", False)): from utils.fp8 import quantize_model_fp8 quantize_model_fp8(pipeline.generator.model, verbose=(local_rank == 0)) configure_generator_torch_compile(pipeline, config) vae_device_str = getattr(config, "vae_device", None) use_dedicated_vae_device = bool(getattr(config, "streaming_vae", False)) and bool(vae_device_str) if use_dedicated_vae_device: vae_device = torch.device(vae_device_str) pipeline.vae.to(device="cpu") pipeline.vae.to(device=vae_device) if hasattr(pipeline.vae, "mean"): pipeline.vae.mean = pipeline.vae.mean.to(device=vae_device) pipeline.vae.std = pipeline.vae.std.to(device=vae_device) if local_rank == 0: print(f"[inference] VAE on {vae_device}, diffusion on {device}") else: pipeline.vae.to(device=device) if vae_device_str and local_rank == 0: print(f"[inference] Ignoring vae_device={vae_device_str} because streaming_vae is false") # Create dataset nfpb = getattr(config, 'num_frame_per_block', 8) data_path = config.data_path chunks_per_shot = getattr(config, 'chunks_per_shot', 0) scene_cut_prefix = getattr(config, 'scene_cut_prefix', "The scene transitions. ") if getattr(config, "i2v", False): model_name = config.model_kwargs.model_name frame_raw_height = list(config.image_or_video_shape)[3] * wan_default_config[model_name]["spatial_compression_ratio"] frame_raw_width = list(config.image_or_video_shape)[4] * wan_default_config[model_name]["spatial_compression_ratio"] temporal_compression_ratio = wan_default_config[model_name]["temporal_compression_ratio"] total_frames = (config.num_output_frames - 1) * temporal_compression_ratio + 1 dataset = MultiVideoConcatDataset( data_dir=data_path, video_size=(frame_raw_height, frame_raw_width), total_frames=total_frames, deterministic=True, num_frame_per_block=nfpb, temporal_compression_ratio=temporal_compression_ratio, target_fps=24 if "5B" in model_name else 16, allow_padding=getattr(config, "allow_padding", False), min_latent_frames=getattr(config, "min_latent_frames", 0), single_video_only=getattr(config, "uniform_prompt", False), independent_first_frame=getattr(config, "independent_first_frame", False), return_image=True, max_chunks_per_shot=getattr(config, "max_chunks_per_shot", 0), scene_cut_prefix=scene_cut_prefix, ) collate_fn = multi_video_collate_fn num_blocks = config.num_output_frames // nfpb else: num_blocks = config.num_output_frames // nfpb dataset = MultiTextConcatDataset( data_path=data_path, num_blocks=num_blocks, chunks_per_shot=chunks_per_shot, scene_cut_prefix=scene_cut_prefix, deterministic=True, ) collate_fn = eval_collate_fn if local_rank == 0: print(f"[data] data_path={data_path}, mode={getattr(dataset, '_mode', dataset.__class__.__name__)}, num_blocks={num_blocks}") num_prompts = len(dataset) print(f"Number of prompts: {num_prompts}") if dist.is_initialized(): sampler = DistributedSampler(dataset, shuffle=False, drop_last=True) else: sampler = SequentialSampler(dataset) dataloader = DataLoader(dataset, batch_size=1, sampler=sampler, num_workers=0, drop_last=False, collate_fn=collate_fn) # Create output directory (only on main process to avoid race conditions) if local_rank == 0: os.makedirs(config.output_folder, exist_ok=True) if dist.is_initialized(): dist.barrier() def encode(self, videos: torch.Tensor) -> torch.Tensor: device, dtype = videos[0].device, videos[0].dtype scale = [self.mean.to(device=device, dtype=dtype), 1.0 / self.std.to(device=device, dtype=dtype)] output = [ self.model.encode(u.unsqueeze(0), scale).float().squeeze(0) for u in videos ] output = torch.stack(output, dim=0) return output for i, batch_data in tqdm(enumerate(dataloader), disable=(local_rank != 0)): idx = batch_data['idx'].item() # For DataLoader batch_size=1, the batch_data is already a single item, but in a batch container # Unpack the batch data for convenience if isinstance(batch_data, dict): batch = batch_data elif isinstance(batch_data, list): batch = batch_data[0] # First (and only) item in the batch all_video = [] # MultiTextConcatDataset + eval_collate_fn: prompts[0] is List[str]. block_prompts = list(batch['prompts'][0]) prompt = block_prompts[0] # for filename prompts = [block_prompts] * config.num_samples shape = config.image_or_video_shape sampled_noise = torch.randn( [config.num_samples, config.num_output_frames, shape[2], shape[3], shape[4]], device=device, dtype=torch.bfloat16 ) initial_latent = None if getattr(config, "i2v", False): image = batch["image"].to(device=device, dtype=torch.bfloat16) if image.ndim == 4: image = image.unsqueeze(2) elif image.ndim != 5: raise ValueError(f"Expected i2v image with shape [B,C,H,W] or [B,C,T,H,W], got {tuple(image.shape)}") initial_latent = pipeline.vae.encode_to_latent(image).to(device=device, dtype=torch.bfloat16) if initial_latent.shape[0] != config.num_samples: initial_latent = initial_latent.repeat(config.num_samples, 1, 1, 1, 1) if config.num_output_frames <= initial_latent.shape[1]: raise ValueError( f"num_output_frames must exceed the i2v conditioning frames; " f"got {config.num_output_frames} and {initial_latent.shape[1]}" ) print("sampled_noise.device", sampled_noise.device) print("prompts", prompts) print('sampled_noise.shape', sampled_noise.shape, 'prompts', prompts) save_latents_only = section_get( config, "inference", "save_latents_only", getattr(config, "save_latents_only", getattr(config, "save_latent_only", False)), aliases=("save_latent_only", "return_latents"), ) inference_kwargs = dict( noise=sampled_noise, text_prompts=prompts, return_latents=save_latents_only, ) if initial_latent is not None: inference_kwargs["initial_latent"] = initial_latent with torch.inference_mode(): generated = pipeline.inference(**inference_kwargs) if not save_latents_only: current_video = rearrange(generated, 'b t c h w -> b t h w c').cpu() all_video.append(current_video) # Final output video video = 255.0 * torch.cat(all_video, dim=1) # Clear VAE cache pipeline.vae.model.clear_cache() else: latents = generated if dist.is_initialized(): rank = dist.get_rank() else: rank = 0 # Save the video if the current prompt is not a dummy prompt if idx < num_prompts: # Determine model type for filename if hasattr(pipeline, 'is_lora_enabled') and pipeline.is_lora_enabled: model_type = "lora" elif getattr(config, 'use_ema', False): model_type = "ema" else: model_type = "regular" for seed_idx in range(config.num_samples): if config.save_with_index: base_name = f'rank{rank}-{idx}-{seed_idx}_{model_type}' else: base_name = f'rank{rank}-{prompt[:100]}-{seed_idx}_{model_type}' if save_latents_only: latent_path = os.path.join(config.output_folder, f'{base_name}.pt') torch.save(latents[seed_idx].cpu(), latent_path) else: output_path = os.path.join(config.output_folder, f'{base_name}.mp4') fps = 24 if '5B' in config.model_kwargs.model_name else 16 write_video(output_path, video[seed_idx], fps=fps) prompt_txt_path = os.path.join(config.output_folder, f'{base_name}_prompts.txt') save_prompts_to_txt( prompts[seed_idx] if isinstance(prompts[seed_idx], list) else [prompts[seed_idx]], prompt_txt_path, is_main_process=(rank == 0), ) if config.inference_iter != -1 and i >= config.inference_iter: break