From 3a38f7dfdc245ff952d6cb4446dc2b67f4f2de4c Mon Sep 17 00:00:00 2001 From: Jinzhe Pan <48981407+Eigensystem@users.noreply.github.com> Date: Sat, 26 Jul 2025 05:27:12 +0800 Subject: [PATCH 1/7] [1/3][Preprocess] refactor preprocessing configs (#638) --- fastvideo/configs/configs.py | 145 +++++++++++++++++++++++++++++ fastvideo/configs/utils.py | 18 +++- fastvideo/fastvideo_args.py | 171 +++++++++++++++++++++++++++++++++-- 3 files changed, 323 insertions(+), 11 deletions(-) create mode 100644 fastvideo/configs/configs.py diff --git a/fastvideo/configs/configs.py b/fastvideo/configs/configs.py new file mode 100644 index 000000000..5bc22aa35 --- /dev/null +++ b/fastvideo/configs/configs.py @@ -0,0 +1,145 @@ +import dataclasses +from typing import Any, Optional + +from fastvideo.configs.utils import update_config_from_args +from fastvideo.utils import FlexibleArgumentParser, StoreBoolean + + +@dataclasses.dataclass +class PreprocessConfig: + """Configuration for preprocessing operations.""" + + # Model and dataset configuration + model_path: str = "" + dataset_path: str = "" + dataset_output_dir: str = "./output" + + # Dataloader configuration + dataloader_num_workers: int = 1 + preprocess_video_batch_size: int = 2 + + # Saver configuration + samples_per_file: int = 64 + flush_frequency: int = 256 + + # Video processing parameters + max_height: int = 480 + max_width: int = 848 + num_frames: int = 163 + video_length_tolerance_range: float = 2.0 + train_fps: int = 30 + speed_factor: float = 1.0 + drop_short_ratio: float = 1.0 + do_temporal_sample: bool = False + + # Model configuration + training_cfg_rate: float = 0.0 + + @staticmethod + def add_cli_args(parser: FlexibleArgumentParser, + prefix: str = "preprocess") -> FlexibleArgumentParser: + """Add preprocessing configuration arguments to the parser.""" + prefix_with_dot = f"{prefix}." if (prefix.strip() != "") else "" + + preprocess_args = parser.add_argument_group("Preprocessing Arguments") + # Model & Dataset + preprocess_args.add_argument(f"--{prefix_with_dot}model-path", + type=str, + default=PreprocessConfig.model_path, + help="Path to the model for preprocessing") + preprocess_args.add_argument( + f"--{prefix_with_dot}dataset-path", + type=str, + default=PreprocessConfig.dataset_path, + help="Path to the dataset directory for preprocessing") + preprocess_args.add_argument( + f"--{prefix_with_dot}dataset-output-dir", + type=str, + default=PreprocessConfig.dataset_output_dir, + help="The output directory where the dataset will be written.") + + # Dataloader + preprocess_args.add_argument( + f"--{prefix_with_dot}dataloader-num-workers", + type=int, + default=PreprocessConfig.dataloader_num_workers, + help= + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ) + preprocess_args.add_argument( + f"--{prefix_with_dot}preprocess-video-batch-size", + type=int, + default=PreprocessConfig.preprocess_video_batch_size, + help="Batch size (per device) for the training dataloader.") + + # Saver + preprocess_args.add_argument(f"--{prefix_with_dot}samples-per-file", + type=int, + default=PreprocessConfig.samples_per_file, + help="Number of samples per output file") + preprocess_args.add_argument(f"--{prefix_with_dot}flush-frequency", + type=int, + default=PreprocessConfig.flush_frequency, + help="How often to save to parquet files") + + # Video processing parameters + preprocess_args.add_argument(f"--{prefix_with_dot}max-height", + type=int, + default=PreprocessConfig.max_height, + help="Maximum height for video processing") + preprocess_args.add_argument(f"--{prefix_with_dot}max-width", + type=int, + default=PreprocessConfig.max_width, + help="Maximum width for video processing") + preprocess_args.add_argument(f"--{prefix_with_dot}num-frames", + type=int, + default=PreprocessConfig.num_frames, + help="Number of frames to process") + preprocess_args.add_argument( + f"--{prefix_with_dot}video-length-tolerance-range", + type=float, + default=PreprocessConfig.video_length_tolerance_range, + help="Video length tolerance range") + preprocess_args.add_argument(f"--{prefix_with_dot}train-fps", + type=int, + default=PreprocessConfig.train_fps, + help="Training FPS") + preprocess_args.add_argument(f"--{prefix_with_dot}speed-factor", + type=float, + default=PreprocessConfig.speed_factor, + help="Speed factor for video processing") + preprocess_args.add_argument(f"--{prefix_with_dot}drop-short-ratio", + type=float, + default=PreprocessConfig.drop_short_ratio, + help="Ratio for dropping short videos") + preprocess_args.add_argument( + f"--{prefix_with_dot}do-temporal-sample", + action=StoreBoolean, + default=PreprocessConfig.do_temporal_sample, + help="Whether to do temporal sampling") + + # Model Training configuration + preprocess_args.add_argument(f"--{prefix_with_dot}training-cfg-rate", + type=float, + default=PreprocessConfig.training_cfg_rate, + help="Training CFG rate") + + return parser + + @classmethod + def from_kwargs(cls, kwargs: dict[str, + Any]) -> Optional["PreprocessConfig"]: + """Create PreprocessConfig from keyword arguments.""" + preprocess_config = cls() + if not update_config_from_args( + preprocess_config, kwargs, prefix="preprocess", pop_args=True): + return None + return preprocess_config + + def check_preprocess_config(self) -> None: + if self.dataset_path == "": + raise ValueError("dataset_path must be set for preprocessing mode") + if self.samples_per_file <= 0: + raise ValueError("samples_per_file must be greater than 0") + if self.flush_frequency <= 0: + raise ValueError("flush_frequency must be greater than 0") diff --git a/fastvideo/configs/utils.py b/fastvideo/configs/utils.py index c6d8047db..a2d47b306 100644 --- a/fastvideo/configs/utils.py +++ b/fastvideo/configs/utils.py @@ -1,10 +1,11 @@ +import argparse from typing import Any def update_config_from_args(config: Any, args_dict: dict[str, Any], prefix: str = "", - pop_args: bool = False) -> None: + pop_args: bool = False) -> bool: """ Update configuration object from arguments dictionary. @@ -43,3 +44,18 @@ def update_config_from_args(config: Any, for key in args_to_remove: if key not in args_not_to_remove: args_dict.pop(key) + + return len(args_to_remove) > 0 + + +def clean_cli_args(args: argparse.Namespace) -> dict[str, Any]: + """ + Clean the arguments by removing the ones that not explicitly provided by the user. + """ + provided_args = {} + for k, v in vars(args).items(): + if (v is not None and hasattr(args, '_provided') + and k in args._provided): + provided_args[k] = v + + return provided_args diff --git a/fastvideo/fastvideo_args.py b/fastvideo/fastvideo_args.py index 8e11fb366..d1aeaa3bf 100644 --- a/fastvideo/fastvideo_args.py +++ b/fastvideo/fastvideo_args.py @@ -6,9 +6,12 @@ import argparse import dataclasses from contextlib import contextmanager from dataclasses import field +from enum import Enum from typing import Any +from fastvideo.configs.configs import PreprocessConfig from fastvideo.configs.pipelines.base import PipelineConfig, STA_Mode +from fastvideo.configs.utils import clean_cli_args from fastvideo.logger import init_logger from fastvideo.platforms import current_platform from fastvideo.utils import FlexibleArgumentParser, StoreBoolean @@ -16,17 +19,58 @@ from fastvideo.utils import FlexibleArgumentParser, StoreBoolean logger = init_logger(__name__) -def clean_cli_args(args: argparse.Namespace) -> dict[str, Any]: +class ExecutionMode(str, Enum): """ - Clean the arguments by removing the ones that not explicitly provided by the user. + Enumeration for different pipeline modes. + + Inherits from str to allow string comparison for backward compatibility. """ - provided_args = {} - for k, v in vars(args).items(): - if (v is not None and hasattr(args, '_provided') - and k in args._provided): - provided_args[k] = v + INFERENCE = "inference" + PREPROCESSING = "preprocessing" + FINETUNING = "finetuning" + DISTILLATION = "distillation" - return provided_args + @classmethod + def from_string(cls, value: str) -> "ExecutionMode": + """Convert string to ExecutionMode enum.""" + try: + return cls(value.lower()) + except ValueError: + raise ValueError( + f"Invalid mode: {value}. Must be one of: {', '.join([m.value for m in cls])}" + ) from None + + @classmethod + def choices(cls) -> list[str]: + """Get all available choices as strings for argparse.""" + return [mode.value for mode in cls] + + +class WorkloadType(str, Enum): + """ + Enumeration for different workload types. + + Inherits from str to allow string comparison for backward compatibility. + """ + I2V = "i2v" # Image to Video + T2V = "t2v" # Text to Video + T2I = "t2i" # Text to Image + I2I = "i2i" # Image to Image + + @classmethod + def from_string(cls, value: str) -> "WorkloadType": + """Convert string to WorkloadType enum.""" + try: + return cls(value.lower()) + except ValueError: + raise ValueError( + f"Invalid workload type: {value}. Must be one of: {', '.join([m.value for m in cls])}" + ) from None + + @classmethod + def choices(cls) -> list[str]: + """Get all available choices as strings for argparse.""" + return [workload.value for workload in cls] # args for fastvideo framework @@ -35,6 +79,12 @@ class FastVideoArgs: # Model and path configuration (for convenience) model_path: str + # Running mode + mode: ExecutionMode = ExecutionMode.INFERENCE + + # Workload type + workload_type: WorkloadType = WorkloadType.T2V + # Cache strategy cache_strategy: str = "none" @@ -56,6 +106,7 @@ class FastVideoArgs: dist_timeout: int | None = None # timeout for torch.distributed pipeline_config: PipelineConfig = field(default_factory=PipelineConfig) + preprocess_config: PreprocessConfig | None = None # LoRA parameters # (Wenxuan) prefer to keep it here instead of in pipeline config to not make it complicated. @@ -120,6 +171,24 @@ class FastVideoArgs: help="Directory containing StepVideo model", ) + # Running mode + parser.add_argument( + "--mode", + type=str, + choices=ExecutionMode.choices(), + default=FastVideoArgs.mode.value, + help="The mode to run FastVideo", + ) + + # Workload type + parser.add_argument( + "--workload-type", + type=str, + choices=WorkloadType.choices(), + default=FastVideoArgs.workload_type.value, + help="The workload type", + ) + # distributed_executor_backend parser.add_argument( "--distributed-executor-backend", @@ -276,6 +345,9 @@ class FastVideoArgs: # Add pipeline configuration arguments PipelineConfig.add_cli_args(parser) + # Add preprocessing configuration arguments + PreprocessConfig.add_cli_args(parser) + return parser @classmethod @@ -285,11 +357,26 @@ class FastVideoArgs: attrs = [attr.name for attr in dataclasses.fields(cls)] # Create a dictionary of attribute values, with defaults for missing attributes - kwargs = {} + kwargs: dict[str, Any] = {} for attr in attrs: if attr == 'pipeline_config': pipeline_config = PipelineConfig.from_kwargs(provided_args) - kwargs[attr] = pipeline_config + kwargs['pipeline_config'] = pipeline_config + elif attr == 'preprocess_config': + preprocess_config = PreprocessConfig.from_kwargs(provided_args) + kwargs['preprocess_config'] = preprocess_config + elif attr == 'mode': + # Convert string to ExecutionMode enum + mode_value = getattr(args, attr, FastVideoArgs.mode.value) + kwargs['mode'] = ExecutionMode.from_string( + mode_value) if isinstance(mode_value, str) else mode_value + elif attr == 'workload_type': + # Convert string to WorkloadType enum + workload_type_value = getattr(args, 'workload_type', + FastVideoArgs.workload_type.value) + kwargs['workload_type'] = WorkloadType.from_string( + workload_type_value) if isinstance( + workload_type_value, str) else workload_type_value # Use getattr with default value from the dataclass for potentially missing attributes else: # Get the field to check if it has a default_factory @@ -308,7 +395,18 @@ class FastVideoArgs: @classmethod def from_kwargs(cls, **kwargs: Any) -> "FastVideoArgs": + # Convert mode string to enum if necessary + if 'mode' in kwargs and isinstance(kwargs['mode'], str): + kwargs['mode'] = ExecutionMode.from_string(kwargs['mode']) + + # Convert workload_type string to enum if necessary + if 'workload_type' in kwargs and isinstance(kwargs['workload_type'], + str): + kwargs['workload_type'] = WorkloadType.from_string( + kwargs['workload_type']) + kwargs['pipeline_config'] = PipelineConfig.from_kwargs(kwargs) + kwargs['preprocess_config'] = PreprocessConfig.from_kwargs(kwargs) return cls(**kwargs) def check_fastvideo_args(self) -> None: @@ -316,6 +414,34 @@ class FastVideoArgs: if current_platform.is_mps(): self.use_fsdp_inference = False + # Validate mode and inference_mode consistency + assert isinstance( + self.mode, ExecutionMode + ), f"Mode must be an ExecutionMode enum, got {type(self.mode)}" + assert self.mode in ExecutionMode.choices( + ), f"Invalid execution mode: {self.mode}" + + # Validate workload type + assert isinstance( + self.workload_type, WorkloadType + ), f"Workload type must be a WorkloadType enum, got {type(self.workload_type)}" + assert self.workload_type in WorkloadType.choices( + ), f"Invalid workload type: {self.workload_type}" + + if self.mode in [ExecutionMode.DISTILLATION, ExecutionMode.FINETUNING + ] and self.inference_mode: + logger.warning( + "Mode is 'training' but inference_mode is True. Setting inference_mode to False." + ) + self.inference_mode = False + elif self.mode in [ + ExecutionMode.INFERENCE, ExecutionMode.PREPROCESSING + ] and not self.inference_mode: + logger.warning( + "Mode is '%s' but inference_mode is False. Setting inference_mode to True.", + self.mode) + self.inference_mode = True + if not self.inference_mode: assert self.hsdp_replicate_dim != -1, "hsdp_replicate_dim must be set for training" assert self.hsdp_shard_dim != -1, "hsdp_shard_dim must be set for training" @@ -346,6 +472,18 @@ class FastVideoArgs: self.pipeline_config.check_pipeline_config() + # Add preprocessing config validation if needed + if self.mode == ExecutionMode.PREPROCESSING: + if self.preprocess_config is None: + raise ValueError( + "preprocess_config is not set in FastVideoArgs when mode is PREPROCESSING" + ) + if self.preprocess_config.model_path == "": + self.preprocess_config.model_path = self.model_path + if not self.pipeline_config.vae_config.load_encoder: + self.pipeline_config.vae_config.load_encoder = True + self.preprocess_config.check_preprocess_config() + _current_fastvideo_args = None @@ -503,6 +641,19 @@ class TrainingArgs(FastVideoArgs): if attr == 'pipeline_config': pipeline_config = PipelineConfig.from_kwargs(provided_args) kwargs[attr] = pipeline_config + elif attr == 'mode': + # Convert string to ExecutionMode enum + mode_value = getattr(args, attr, ExecutionMode.FINETUNING.value) + kwargs[attr] = ExecutionMode.from_string( + mode_value) if isinstance(mode_value, str) else mode_value + elif attr == 'workload_type': + # Convert string to WorkloadType enum + workload_type_value = getattr(args, 'workload_type', + WorkloadType.T2V.value) + kwargs[attr] = WorkloadType.from_string( + workload_type_value) if isinstance( + workload_type_value, str) else workload_type_value + # Use getattr with default value from the dataclass for potentially missing attributes else: # Get the field to check its default value field = dataclasses.fields(cls)[next( From 744e0555c08fa8e0796e6b0c106640915fc6e4d4 Mon Sep 17 00:00:00 2001 From: William Lin Date: Fri, 25 Jul 2025 16:20:53 -0700 Subject: [PATCH 2/7] [misc] Use FASTVIDEO_STAGE_LOGGING for perf timing of stage (#644) --- fastvideo/envs.py | 7 +++++++ fastvideo/pipelines/stages/base.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fastvideo/envs.py b/fastvideo/envs.py index fa735457e..25444854f 100644 --- a/fastvideo/envs.py +++ b/fastvideo/envs.py @@ -27,6 +27,7 @@ if TYPE_CHECKING: CMAKE_BUILD_TYPE: str | None = None VERBOSE: bool = False FASTVIDEO_SERVER_DEV_MODE: bool = False + FASTVIDEO_STAGE_LOGGING: bool = False def get_default_cache_root() -> str: @@ -172,6 +173,7 @@ environment_variables: dict[str, Callable[[], Any]] = { # - "TORCH_SDPA": use torch.nn.MultiheadAttention # - "FLASH_ATTN": use FlashAttention # - "SLIDING_TILE_ATTN" : use Sliding Tile Attention + # - "VIDEO_SPARSE_ATTN": use Video Sparse Attention # - "SAGE_ATTN": use Sage Attention "FASTVIDEO_ATTENTION_BACKEND": lambda: os.getenv("FASTVIDEO_ATTENTION_BACKEND", None), @@ -199,6 +201,11 @@ environment_variables: dict[str, Callable[[], Any]] = { # e.g. `/reset_prefix_cache` "FASTVIDEO_SERVER_DEV_MODE": lambda: bool(int(os.getenv("FASTVIDEO_SERVER_DEV_MODE", "0"))), + + # If set, fastvideo will enable stage logging, which will print the time + # taken for each stage + "FASTVIDEO_STAGE_LOGGING": + lambda: bool(int(os.getenv("FASTVIDEO_STAGE_LOGGING", "0"))), } # end-env-vars-definition diff --git a/fastvideo/pipelines/stages/base.py b/fastvideo/pipelines/stages/base.py index 43e6c9ac5..10b4f690d 100644 --- a/fastvideo/pipelines/stages/base.py +++ b/fastvideo/pipelines/stages/base.py @@ -12,6 +12,7 @@ from abc import ABC, abstractmethod import torch +import fastvideo.envs as envs from fastvideo.fastvideo_args import FastVideoArgs from fastvideo.logger import init_logger from fastvideo.pipelines.pipeline_batch_info import ForwardBatch @@ -145,8 +146,7 @@ class PipelineStage(ABC): raise # Execute the actual stage logic - # envs.ENABLE_STAGE_LOGGING - if False: + if envs.FASTVIDEO_STAGE_LOGGING: logger.info("[%s] Starting execution", stage_name) start_time = time.perf_counter() From d3c0c5220848577f9c07fcfdb3af592c99ff9527 Mon Sep 17 00:00:00 2001 From: Yongqi Chen <144848849+BrianChen1129@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:44:10 -0400 Subject: [PATCH 3/7] [Feature] Add prompt_txt support for CLI inference; Add DMD CLI inference (#646) --- fastvideo/entrypoints/cli/generate.py | 16 ++++- fastvideo/entrypoints/video_generator.py | 72 +++++++++++++++++++++-- fastvideo/fastvideo_args.py | 12 ++++ scripts/inference/v1_inference_wan.sh | 8 ++- scripts/inference/v1_inference_wan_dmd.sh | 23 ++++++++ 5 files changed, 120 insertions(+), 11 deletions(-) create mode 100755 scripts/inference/v1_inference_wan_dmd.sh diff --git a/fastvideo/entrypoints/cli/generate.py b/fastvideo/entrypoints/cli/generate.py index e7494c580..67058f76d 100644 --- a/fastvideo/entrypoints/cli/generate.py +++ b/fastvideo/entrypoints/cli/generate.py @@ -58,9 +58,18 @@ class GenerateSubcommand(CLISubcommand): "model_path must be provided either in config file or via --model-path" ) - if 'prompt' not in merged_args or not merged_args['prompt']: + # Check if either prompt or prompt_txt is provided + has_prompt = 'prompt' in merged_args and merged_args['prompt'] + has_prompt_txt = 'prompt_txt' in merged_args and merged_args[ + 'prompt_txt'] + + if not (has_prompt or has_prompt_txt): + raise ValueError("Either prompt or prompt_txt must be provided") + + if has_prompt and has_prompt_txt: raise ValueError( - "prompt must be provided either in config file or via --prompt") + "Cannot provide both 'prompt' and 'prompt_txt'. Use only one of them." + ) init_args = { k: v @@ -73,11 +82,12 @@ class GenerateSubcommand(CLISubcommand): } model_path = init_args.pop('model_path') - prompt = generation_args.pop('prompt') + prompt = generation_args.pop('prompt', None) generator = VideoGenerator.from_pretrained(model_path=model_path, **init_args) + # Call generate_video - it handles both single and batch modes generator.generate_video(prompt=prompt, **generation_args) def validate(self, args: argparse.Namespace) -> None: diff --git a/fastvideo/entrypoints/video_generator.py b/fastvideo/entrypoints/video_generator.py index 5051a9b7e..3e6581fe5 100644 --- a/fastvideo/entrypoints/video_generator.py +++ b/fastvideo/entrypoints/video_generator.py @@ -98,15 +98,15 @@ class VideoGenerator: def generate_video( self, - prompt: str, + prompt: str | None = None, sampling_param: SamplingParam | None = None, **kwargs, - ) -> dict[str, Any] | list[np.ndarray]: + ) -> dict[str, Any] | list[np.ndarray] | list[dict[str, Any]]: """ Generate a video based on the given prompt. Args: - prompt: The prompt to use for generation + prompt: The prompt to use for generation (optional if prompt_txt is provided) negative_prompt: The negative prompt to use (overrides the one in fastvideo_args) output_path: Path to save the video (overrides the one in fastvideo_args) output_video_name: Name of the video file to save. Default is the first 100 characters of the prompt. @@ -123,8 +123,66 @@ class VideoGenerator: callback_steps: Number of steps between each callback Returns: - Either the output dictionary or the list of frames depending on return_frames + Either the output dictionary, list of frames, or list of results for batch processing """ + # Handle batch processing from text file + if self.fastvideo_args.prompt_txt is not None: + prompt_txt_path = self.fastvideo_args.prompt_txt + if not os.path.exists(prompt_txt_path): + raise FileNotFoundError( + f"Prompt text file not found: {prompt_txt_path}") + + # Read prompts from file + with open(prompt_txt_path, encoding='utf-8') as f: + prompts = [line.strip() for line in f if line.strip()] + + if not prompts: + raise ValueError(f"No prompts found in file: {prompt_txt_path}") + + logger.info("Found %d prompts in %s", len(prompts), prompt_txt_path) + + results = [] + for i, batch_prompt in enumerate(prompts): + logger.info("Processing prompt %d/%d: %s...", i + 1, + len(prompts), batch_prompt[:100]) + + try: + # Generate video for this prompt using the same logic below + result = self._generate_single_video( + batch_prompt, sampling_param, **kwargs) + + # Add prompt info to result + if isinstance(result, dict): + result["prompt_index"] = i + result["prompt"] = batch_prompt + + results.append(result) + logger.info("Successfully generated video for prompt %d", + i + 1) + + except Exception as e: + logger.error("Failed to generate video for prompt %d: %s", + i + 1, e) + continue + + logger.info( + "Completed batch processing. Generated %d videos successfully.", + len(results)) + return results + + # Single prompt generation (original behavior) + if prompt is None: + raise ValueError("Either prompt or prompt_txt must be provided") + + return self._generate_single_video(prompt, sampling_param, **kwargs) + + def _generate_single_video( + self, + prompt: str, + sampling_param: SamplingParam | None = None, + **kwargs, + ) -> dict[str, Any] | list[np.ndarray]: + """Internal method for single video generation""" # Create a copy of inference args to avoid modifying the original fastvideo_args = self.fastvideo_args pipeline_config = fastvideo_args.pipeline_config @@ -222,6 +280,10 @@ class VideoGenerator: output_path: {sampling_param.output_path} """ # type: ignore[attr-defined] logger.info(debug_str) + + # Use prompt[:100] for video name + output_video_name = kwargs.get("output_video_name", prompt[:100]) + # Prepare batch batch = ForwardBatch( **shallow_asdict(sampling_param), @@ -229,7 +291,7 @@ class VideoGenerator: n_tokens=n_tokens, VSA_sparsity=fastvideo_args.VSA_sparsity, extra={}, - output_video_name=kwargs.get("output_video_name", prompt[:100]), + output_video_name=output_video_name, ) # Run inference diff --git a/fastvideo/fastvideo_args.py b/fastvideo/fastvideo_args.py index d1aeaa3bf..50a0e64d9 100644 --- a/fastvideo/fastvideo_args.py +++ b/fastvideo/fastvideo_args.py @@ -139,6 +139,9 @@ class FastVideoArgs: # Stage verification enable_stage_verification: bool = True + # Prompt text file for batch processing + prompt_txt: str | None = None + # model paths for correct deallocation model_paths: dict[str, str] = field(default_factory=dict) model_loaded: dict[str, bool] = field(default_factory=lambda: { @@ -267,6 +270,15 @@ class FastVideoArgs: help="Output type for the generated video", ) + # Prompt text file for batch processing + parser.add_argument( + "--prompt-txt", + type=str, + default=FastVideoArgs.prompt_txt, + help= + "Path to a text file containing prompts (one per line) for batch processing", + ) + # STA (Sliding Tile Attention) parameters parser.add_argument( "--STA-mode", diff --git a/scripts/inference/v1_inference_wan.sh b/scripts/inference/v1_inference_wan.sh index 6bd73234b..60f96843e 100755 --- a/scripts/inference/v1_inference_wan.sh +++ b/scripts/inference/v1_inference_wan.sh @@ -1,9 +1,10 @@ #!/bin/bash -num_gpus=2 +num_gpus=1 export FASTVIDEO_ATTENTION_BACKEND= export MODEL_BASE=Wan-AI/Wan2.1-T2V-1.3B-Diffusers # export MODEL_BASE=hunyuanvideo-community/HunyuanVideo +# You can either use --prompt or --prompt-txt, but not both. fastvideo generate \ --model-path $MODEL_BASE \ --sp-size $num_gpus \ @@ -14,8 +15,9 @@ fastvideo generate \ --num-frames 77 \ --num-inference-steps 50 \ --fps 16 \ - --guidance-scale 3.0 \ - --prompt "A beautiful woman in a red dress walking down a street" \ + --guidance-scale 6.0 \ + --flow-shift 8.0 \ + --prompt-txt assets/prompt.txt \ --negative-prompt "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards" \ --seed 1024 \ --output-path outputs_video/ \ No newline at end of file diff --git a/scripts/inference/v1_inference_wan_dmd.sh b/scripts/inference/v1_inference_wan_dmd.sh new file mode 100755 index 000000000..a38b5b77f --- /dev/null +++ b/scripts/inference/v1_inference_wan_dmd.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +num_gpus=1 +export FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN +export MODEL_BASE=FastVideo/FastWan2.1-T2V-1.3B-Diffusers +# export MODEL_BASE=hunyuanvideo-community/HunyuanVideo +# You can either use --prompt or --prompt-txt, but not both. +fastvideo generate \ + --model-path $MODEL_BASE \ + --sp-size $num_gpus \ + --tp-size 1 \ + --num-gpus $num_gpus \ + --height 448 \ + --width 832 \ + --num-frames 61 \ + --num-inference-steps 3 \ + --fps 16 \ + --prompt-txt assets/prompt.txt \ + --negative-prompt "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards" \ + --seed 1024 \ + --output-path outputs_video_dmd/ \ + --VSA-sparsity 0.8 \ + --dmd-denoising-steps "1000,757,522" From 81c9df629c4d7728493703d9ca84d3d802df73a7 Mon Sep 17 00:00:00 2001 From: William Lin Date: Fri, 25 Jul 2025 17:55:03 -0700 Subject: [PATCH 4/7] [core] Add offloading for vae and image encoder and rename offloading args (#643) --- comfyui/examples/FastHunyuan-diffusers.json | 2 +- .../Wan2.1-I2V-14B-480P-Diffusers.json | 2 +- comfyui/video_generator/video_generator.py | 8 +++---- comfyui/web/js/FastVideo.core.js | 2 +- docs/source/inference/configuration.md | 2 +- examples/inference/basic/basic.py | 5 ++++- examples/inference/basic/basic_dmd.py | 22 +++++++++++++++---- examples/inference/basic/basic_mps.py | 4 ++-- examples/inference/basic/test.py | 4 ++-- .../optimizations/teacache_example.py | 2 +- fastvideo/configs/fasthunyuan_t2v.json | 2 +- fastvideo/configs/wan_1.3B_t2v_pipeline.json | 2 +- .../configs/wan_14B_i2v_480p_pipeline.json | 2 +- fastvideo/fastvideo_args.py | 20 ++++++++++++++--- fastvideo/models/loader/component_loader.py | 18 ++++++++++----- fastvideo/pipelines/composed_pipeline_base.py | 2 +- .../pipelines/preprocess/v1_preprocess.py | 2 +- fastvideo/pipelines/stages/decoding.py | 3 ++- fastvideo/pipelines/stages/encoding.py | 3 ++- fastvideo/pipelines/stages/image_encoding.py | 2 +- .../lora/test_lora_inference_similarity.py | 6 ++--- .../tests/ssim/test_inference_similarity.py | 2 +- .../transformers/test_hunyuanvideo_load.py | 2 +- fastvideo/tests/transformers/test_wanvideo.py | 2 +- fastvideo/tests/vaes/test_hunyuan_vae.py | 1 + fastvideo/tests/vaes/test_wan_vae.py | 1 + fastvideo/training/training_pipeline.py | 2 +- .../training/wan_i2v_training_pipeline.py | 6 ++--- fastvideo/training/wan_training_pipeline.py | 4 ++-- 29 files changed, 89 insertions(+), 46 deletions(-) diff --git a/comfyui/examples/FastHunyuan-diffusers.json b/comfyui/examples/FastHunyuan-diffusers.json index b5731ee1e..db4a71d13 100644 --- a/comfyui/examples/FastHunyuan-diffusers.json +++ b/comfyui/examples/FastHunyuan-diffusers.json @@ -310,7 +310,7 @@ "value": -99999, "cachedValue": "fp16" }, - "use_cpu_offload": { + "dit_cpu_offload": { "isAuto": true, "value": -99999, "cachedValue": true diff --git a/comfyui/examples/Wan2.1-I2V-14B-480P-Diffusers.json b/comfyui/examples/Wan2.1-I2V-14B-480P-Diffusers.json index e5a7087ee..1cb01d7e9 100644 --- a/comfyui/examples/Wan2.1-I2V-14B-480P-Diffusers.json +++ b/comfyui/examples/Wan2.1-I2V-14B-480P-Diffusers.json @@ -517,7 +517,7 @@ "value": "fp16", "cachedValue": "fp16" }, - "use_cpu_offload": { + "dit_cpu_offload": { "isAuto": true, "value": true, "cachedValue": true diff --git a/comfyui/video_generator/video_generator.py b/comfyui/video_generator/video_generator.py index 87bf6d945..259993b8e 100644 --- a/comfyui/video_generator/video_generator.py +++ b/comfyui/video_generator/video_generator.py @@ -105,7 +105,7 @@ class VideoGenerator: "precision": (["fp16", "bf16"], { "default": "fp16" }), - "use_cpu_offload": ([True, False], { + "dit_cpu_offload": ([True, False], { "default": False }), } @@ -204,7 +204,7 @@ class VideoGenerator: vae_config=None, text_encoder_config=None, dit_config=None, - use_cpu_offload=None, + dit_cpu_offload=None, ): print('Running FastVideo inference') @@ -259,8 +259,8 @@ class VideoGenerator: raw_generation_args['tp_size'] = tp_size if sp_size is not None: raw_generation_args['sp_size'] = sp_size - if use_cpu_offload is not None: - raw_generation_args['use_cpu_offload'] = use_cpu_offload + if dit_cpu_offload is not None: + raw_generation_args['dit_cpu_offload'] = dit_cpu_offload generation_args = { k: v diff --git a/comfyui/web/js/FastVideo.core.js b/comfyui/web/js/FastVideo.core.js index 6be0dcbc5..87f4009d7 100644 --- a/comfyui/web/js/FastVideo.core.js +++ b/comfyui/web/js/FastVideo.core.js @@ -552,7 +552,7 @@ app.registerExtension({ ] const floatWidgetNames = ["embedded_cfg_scale", "guidance_scale"] const comboWidgetNames = ["vae_tiling", "vae_precision", "vae_sp", "text_encoder_precision", "precision", - "load_encoder", "load_decoder", "use_tiling", "use_temporal_tiling", "use_parallel_tiling", "use_cpu_offload", "enable_teacache" + "load_encoder", "load_decoder", "use_tiling", "use_temporal_tiling", "use_parallel_tiling", "dit_cpu_offload", "enable_teacache" ] const stringWidgetNames = ["prefix", "quant_config", "lora_config", "image_path"] diff --git a/docs/source/inference/configuration.md b/docs/source/inference/configuration.md index 581fb151e..6462931d0 100644 --- a/docs/source/inference/configuration.md +++ b/docs/source/inference/configuration.md @@ -27,7 +27,7 @@ def main(): model_name = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" config = PipelineConfig.from_pretrained(model_name) config.vae_precision = "fp16" - config.use_cpu_offload = True + config.dit_cpu_offload = True # Create the generator generator = VideoGenerator.from_pretrained( diff --git a/examples/inference/basic/basic.py b/examples/inference/basic/basic.py index c21ccfe8b..37f15b516 100644 --- a/examples/inference/basic/basic.py +++ b/examples/inference/basic/basic.py @@ -13,7 +13,10 @@ def main(): # FastVideo will automatically handle distributed setup num_gpus=2, use_fsdp_inference=True, - use_cpu_offload=False + dit_cpu_offload=False, + vae_cpu_offload=False, + text_encoder_cpu_offload=False, + # image_encoder_cpu_offload=False, ) # sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers") diff --git a/examples/inference/basic/basic_dmd.py b/examples/inference/basic/basic_dmd.py index 3579d592b..9bf7bc243 100644 --- a/examples/inference/basic/basic_dmd.py +++ b/examples/inference/basic/basic_dmd.py @@ -16,8 +16,9 @@ def main(): num_gpus=1, use_fsdp_inference=True, # Adjust these offload parameters if you have < 32GB of VRAM - text_encoder_offload=False, - use_cpu_offload=False, + text_encoder_cpu_offload=False, + dit_cpu_offload=False, + vae_cpu_offload=False, VSA_sparsity=0.8, ) load_end_time = time.perf_counter() @@ -35,11 +36,24 @@ def main(): video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param) end_time = time.perf_counter() gen_time = end_time - start_time - e2e_gen_time = end_time - start_time + + # Generate another video with a different prompt, without reloading the + # model! + prompt2 = ( + "A majestic lion strides across the golden savanna, its powerful frame " + "glistening under the warm afternoon sun. The tall grass ripples gently in " + "the breeze, enhancing the lion's commanding presence. The tone is vibrant, " + "embodying the raw energy of the wild. Low angle, steady tracking shot, " + "cinematic.") + start_time = time.perf_counter() + video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True) + end_time = time.perf_counter() + gen_time2 = end_time - start_time + print(f"Time taken to load model: {load_time} seconds") print(f"Time taken to generate video: {gen_time} seconds") - print(f"Time taken for e2e generation: {e2e_gen_time} seconds") + print(f"Time taken to generate video2: {gen_time2} seconds") if __name__ == "__main__": diff --git a/examples/inference/basic/basic_mps.py b/examples/inference/basic/basic_mps.py index 6a819969b..ca53be02c 100644 --- a/examples/inference/basic/basic_mps.py +++ b/examples/inference/basic/basic_mps.py @@ -9,8 +9,8 @@ def main(): "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", pipeline_config=config, use_fsdp_inference=False, # Disable FSDP for MPS - use_cpu_offload=True, - text_encoder_offload=True, + dit_cpu_offload=True, + text_encoder_cpu_offload=True, pin_cpu_memory=True, disable_autocast=False, num_gpus=1, diff --git a/examples/inference/basic/test.py b/examples/inference/basic/test.py index f8f55e326..050da2584 100644 --- a/examples/inference/basic/test.py +++ b/examples/inference/basic/test.py @@ -16,8 +16,8 @@ def main(): # if num_gpus > 1, FastVideo will automatically handle distributed setup pipeline_config=pipeline_config, use_fsdp_inference=False, # Disable FSDP for MPS - use_cpu_offload=True, - text_encoder_offload=True, + dit_cpu_offload=True, + text_encoder_cpu_offload=True, pin_cpu_memory=True, disable_autocast=False, num_gpus=1, diff --git a/examples/inference/optimizations/teacache_example.py b/examples/inference/optimizations/teacache_example.py index a50630f70..1dab0f92e 100644 --- a/examples/inference/optimizations/teacache_example.py +++ b/examples/inference/optimizations/teacache_example.py @@ -9,7 +9,7 @@ def main(): gen = VideoGenerator.from_pretrained( model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers", num_gpus=1, - use_cpu_offload=False, + dit_cpu_offload=False, ) load_time = time.perf_counter() - start_time print(f"Model loading time: {load_time:.2f} seconds") diff --git a/fastvideo/configs/fasthunyuan_t2v.json b/fastvideo/configs/fasthunyuan_t2v.json index e58aa0d49..6eb29ae44 100644 --- a/fastvideo/configs/fasthunyuan_t2v.json +++ b/fastvideo/configs/fasthunyuan_t2v.json @@ -1,7 +1,7 @@ { "embedded_cfg_scale": 6, "flow_shift": 17, - "use_cpu_offload": false, + "dit_cpu_offload": false, "disable_autocast": false, "precision": "bf16", "vae_precision": "fp32", diff --git a/fastvideo/configs/wan_1.3B_t2v_pipeline.json b/fastvideo/configs/wan_1.3B_t2v_pipeline.json index a2d959b8c..2ed1b3bfe 100644 --- a/fastvideo/configs/wan_1.3B_t2v_pipeline.json +++ b/fastvideo/configs/wan_1.3B_t2v_pipeline.json @@ -1,7 +1,7 @@ { "embedded_cfg_scale": 6.0, "flow_shift": 3, - "use_cpu_offload": true, + "dit_cpu_offload": true, "disable_autocast": false, "precision": "bf16", "vae_precision": "fp32", diff --git a/fastvideo/configs/wan_14B_i2v_480p_pipeline.json b/fastvideo/configs/wan_14B_i2v_480p_pipeline.json index 1035eefe5..21743d66b 100644 --- a/fastvideo/configs/wan_14B_i2v_480p_pipeline.json +++ b/fastvideo/configs/wan_14B_i2v_480p_pipeline.json @@ -1,7 +1,7 @@ { "embedded_cfg_scale": 6.0, "flow_shift": 3, - "use_cpu_offload": true, + "dit_cpu_offload": true, "disable_autocast": false, "precision": "bf16", "vae_precision": "fp32", diff --git a/fastvideo/fastvideo_args.py b/fastvideo/fastvideo_args.py index 50a0e64d9..fc7d253b8 100644 --- a/fastvideo/fastvideo_args.py +++ b/fastvideo/fastvideo_args.py @@ -118,9 +118,12 @@ class FastVideoArgs: output_type: str = "pil" - use_cpu_offload: bool = True # For DiT + # CPU offload parameters + dit_cpu_offload: bool = True use_fsdp_inference: bool = True - text_encoder_offload: bool = True + text_encoder_cpu_offload: bool = True + image_encoder_cpu_offload: bool = True + vae_cpu_offload: bool = True pin_cpu_memory: bool = True # STA (Sliding Tile Attention) parameters @@ -307,7 +310,7 @@ class FastVideoArgs: ) parser.add_argument( - "--use-cpu-offload", + "--dit-cpu-offload", action=StoreBoolean, help= "Use CPU offload for DiT inference. Enable if run out of memory with FSDP.", @@ -324,6 +327,17 @@ class FastVideoArgs: help= "Use CPU offload for text encoder. Enable if run out of memory.", ) + parser.add_argument( + "--image-encoder-cpu-offload", + action=StoreBoolean, + help= + "Use CPU offload for image encoder. Enable if run out of memory.", + ) + parser.add_argument( + "--vae-cpu-offload", + action=StoreBoolean, + help="Use CPU offload for VAE. Enable if run out of memory.", + ) parser.add_argument( "--pin-cpu-memory", action=StoreBoolean, diff --git a/fastvideo/models/loader/component_loader.py b/fastvideo/models/loader/component_loader.py index 5ad5fc03d..dc451bbf8 100644 --- a/fastvideo/models/loader/component_loader.py +++ b/fastvideo/models/loader/component_loader.py @@ -247,10 +247,10 @@ class TextEncoderLoader(ComponentLoader): target_device: torch.device, fastvideo_args: FastVideoArgs, dtype: str = "fp16"): - use_cpu_offload = fastvideo_args.text_encoder_offload and len( + use_cpu_offload = fastvideo_args.text_encoder_cpu_offload and len( getattr(model_config, "_fsdp_shard_conditions", [])) > 0 - if fastvideo_args.text_encoder_offload: + if fastvideo_args.text_encoder_cpu_offload: target_device = torch.device( "mps") if current_platform.is_mps() else torch.device("cpu") @@ -324,7 +324,10 @@ class ImageEncoderLoader(TextEncoderLoader): encoder_config = fastvideo_args.pipeline_config.image_encoder_config encoder_config.update_model_arch(model_config) - target_device = get_local_torch_device() + if fastvideo_args.image_encoder_cpu_offload: + target_device = torch.device("mps") if current_platform.is_mps() else torch.device("cpu") + else: + target_device = get_local_torch_device() # TODO(will): add support for other dtypes return self.load_model( model_path, encoder_config, target_device, fastvideo_args, @@ -375,10 +378,15 @@ class VAELoader(ComponentLoader): vae_config = fastvideo_args.pipeline_config.vae_config vae_config.update_model_arch(config) + if fastvideo_args.vae_cpu_offload: + target_device = torch.device("mps") if current_platform.is_mps() else torch.device("cpu") + else: + target_device = get_local_torch_device() + with set_default_torch_dtype(PRECISION_TO_TYPE[ fastvideo_args.pipeline_config.vae_precision]): vae_cls, _ = ModelRegistry.resolve_model_cls(class_name) - vae = vae_cls(vae_config).to(get_local_torch_device()) + vae = vae_cls(vae_config).to(target_device) # Find all safetensors files safetensors_list = glob.glob( @@ -441,7 +449,7 @@ class TransformerLoader(ComponentLoader): device=get_local_torch_device(), hsdp_replicate_dim=fastvideo_args.hsdp_replicate_dim, hsdp_shard_dim=fastvideo_args.hsdp_shard_dim, - cpu_offload=fastvideo_args.use_cpu_offload, + cpu_offload=fastvideo_args.dit_cpu_offload, fsdp_inference=fastvideo_args.use_fsdp_inference, # TODO(will): make these configurable param_dtype=torch.bfloat16, diff --git a/fastvideo/pipelines/composed_pipeline_base.py b/fastvideo/pipelines/composed_pipeline_base.py index 89caedc44..1aa7cc55f 100644 --- a/fastvideo/pipelines/composed_pipeline_base.py +++ b/fastvideo/pipelines/composed_pipeline_base.py @@ -139,7 +139,7 @@ class ComposedPipelineBase(ABC): for key, value in kwargs.items(): setattr(fastvideo_args, key, value) - fastvideo_args.use_cpu_offload = False + fastvideo_args.dit_cpu_offload = False # we hijack the precision to be the master weight type so that the # model is loaded with the correct precision. Subsequently we will # use FSDP2's MixedPrecisionPolicy to set the precision for the diff --git a/fastvideo/pipelines/preprocess/v1_preprocess.py b/fastvideo/pipelines/preprocess/v1_preprocess.py index b0465fd56..56640bd8d 100644 --- a/fastvideo/pipelines/preprocess/v1_preprocess.py +++ b/fastvideo/pipelines/preprocess/v1_preprocess.py @@ -23,7 +23,7 @@ def main(args) -> None: assert num_gpus == 1, "Only support 1 GPU" pipeline_config = PipelineConfig.from_pretrained(args.model_path) kwargs = { - "use_cpu_offload": False, + "dit_cpu_offload": False, "vae_precision": "fp32", "vae_config": WanVAEConfig(load_encoder=True, load_decoder=False), } diff --git a/fastvideo/pipelines/stages/decoding.py b/fastvideo/pipelines/stages/decoding.py index 44de981d4..8e692c06a 100644 --- a/fastvideo/pipelines/stages/decoding.py +++ b/fastvideo/pipelines/stages/decoding.py @@ -133,7 +133,8 @@ class DecodingStage(PipelineStage): if hasattr(self, 'maybe_free_model_hooks'): self.maybe_free_model_hooks() - self.vae.to("cpu") + if fastvideo_args.vae_cpu_offload: + self.vae.to("cpu") if torch.backends.mps.is_available(): del self.vae diff --git a/fastvideo/pipelines/stages/encoding.py b/fastvideo/pipelines/stages/encoding.py index 54c20bce7..c8cdfab36 100644 --- a/fastvideo/pipelines/stages/encoding.py +++ b/fastvideo/pipelines/stages/encoding.py @@ -135,7 +135,8 @@ class EncodingStage(PipelineStage): if hasattr(self, 'maybe_free_model_hooks'): self.maybe_free_model_hooks() - self.vae.to("cpu") + if fastvideo_args.vae_cpu_offload: + self.vae.to("cpu") return batch diff --git a/fastvideo/pipelines/stages/image_encoding.py b/fastvideo/pipelines/stages/image_encoding.py index fa7991e83..889c01198 100644 --- a/fastvideo/pipelines/stages/image_encoding.py +++ b/fastvideo/pipelines/stages/image_encoding.py @@ -67,7 +67,7 @@ class ImageEncodingStage(PipelineStage): batch.image_embeds.append(image_embeds) - if fastvideo_args.use_cpu_offload: + if fastvideo_args.image_encoder_cpu_offload: self.image_encoder.to('cpu') return batch diff --git a/fastvideo/tests/inference/lora/test_lora_inference_similarity.py b/fastvideo/tests/inference/lora/test_lora_inference_similarity.py index ae4159ee2..2033e7903 100644 --- a/fastvideo/tests/inference/lora/test_lora_inference_similarity.py +++ b/fastvideo/tests/inference/lora/test_lora_inference_similarity.py @@ -33,7 +33,7 @@ WAN_LORA_PARAMS = { "fps": 24, "neg_prompt": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走", "text-encoder-precision": ("fp32",), - "use_cpu_offload": True, + "dit_cpu_offload": True, } # LoRA configurations for testing @@ -68,7 +68,7 @@ def test_merge_lora_weights(model_id): lora_path = lora_config["lora_path"] args = FastVideoArgs.from_kwargs( model_path=model_id, - use_cpu_offload=True, + dit_cpu_offload=True, dit_precision="bf16", ) pipe = build_pipeline(args) @@ -113,7 +113,7 @@ def test_lora_inference_similarity(ATTENTION_BACKEND, model_id): init_kwargs = { "num_gpus": BASE_PARAMS["num_gpus"], "flow_shift": BASE_PARAMS["flow_shift"], - "use_cpu_offload": BASE_PARAMS["use_cpu_offload"], + "dit_cpu_offload": BASE_PARAMS["dit_cpu_offload"], } if "text-encoder-precision" in BASE_PARAMS: init_kwargs["text_encoder_precisions"] = BASE_PARAMS["text-encoder-precision"] diff --git a/fastvideo/tests/ssim/test_inference_similarity.py b/fastvideo/tests/ssim/test_inference_similarity.py index 457a308dc..8c223026c 100644 --- a/fastvideo/tests/ssim/test_inference_similarity.py +++ b/fastvideo/tests/ssim/test_inference_similarity.py @@ -228,7 +228,7 @@ def test_inference_similarity(prompt, ATTENTION_BACKEND, model_id): "flow_shift": BASE_PARAMS["flow_shift"], "sp_size": BASE_PARAMS["sp_size"], "tp_size": BASE_PARAMS["tp_size"], - "use_cpu_offload": True, + "dit_cpu_offload": True, } if BASE_PARAMS.get("vae_sp"): init_kwargs["vae_sp"] = True diff --git a/fastvideo/tests/transformers/test_hunyuanvideo_load.py b/fastvideo/tests/transformers/test_hunyuanvideo_load.py index c0bfade23..5d83032e9 100644 --- a/fastvideo/tests/transformers/test_hunyuanvideo_load.py +++ b/fastvideo/tests/transformers/test_hunyuanvideo_load.py @@ -62,7 +62,7 @@ def test_hunyuanvideo_distributed(): precision_str = "bf16" args = FastVideoArgs(model_path=TRANSFORMER_PATH, - use_cpu_offload=True, + dit_cpu_offload=True, pipeline_config=PipelineConfig(dit_config=HunyuanVideoConfig(), dit_precision=precision_str)) args.device = torch.device(f"cuda:{LOCAL_RANK}") diff --git a/fastvideo/tests/transformers/test_wanvideo.py b/fastvideo/tests/transformers/test_wanvideo.py index ab4fdd60a..60b0a8626 100644 --- a/fastvideo/tests/transformers/test_wanvideo.py +++ b/fastvideo/tests/transformers/test_wanvideo.py @@ -34,7 +34,7 @@ def test_wan_transformer(): precision = torch.bfloat16 precision_str = "bf16" args = FastVideoArgs(model_path=TRANSFORMER_PATH, - use_cpu_offload=True, + dit_cpu_offload=True, pipeline_config=PipelineConfig(dit_config=WanVideoConfig(), dit_precision=precision_str)) args.device = device diff --git a/fastvideo/tests/vaes/test_hunyuan_vae.py b/fastvideo/tests/vaes/test_hunyuan_vae.py index 2dc914ec5..4e77a4f65 100644 --- a/fastvideo/tests/vaes/test_hunyuan_vae.py +++ b/fastvideo/tests/vaes/test_hunyuan_vae.py @@ -43,6 +43,7 @@ def test_hunyuan_vae(): precision_str = "bf16" args = FastVideoArgs(model_path=VAE_PATH, pipeline_config=PipelineConfig(vae_config=HunyuanVAEConfig(), vae_precision=precision_str)) args.device = device + args.vae_cpu_offload = False loader = VAELoader() model = loader.load(VAE_PATH, args) diff --git a/fastvideo/tests/vaes/test_wan_vae.py b/fastvideo/tests/vaes/test_wan_vae.py index cec0e439e..d0873ed3a 100644 --- a/fastvideo/tests/vaes/test_wan_vae.py +++ b/fastvideo/tests/vaes/test_wan_vae.py @@ -32,6 +32,7 @@ def test_wan_vae(): precision_str = "bf16" args = FastVideoArgs(model_path=VAE_PATH, pipeline_config=PipelineConfig(vae_config=WanVAEConfig(), vae_precision=precision_str)) args.device = device + args.vae_cpu_offload = False loader = VAELoader() model2 = loader.load(VAE_PATH, args) diff --git a/fastvideo/training/training_pipeline.py b/fastvideo/training/training_pipeline.py index 1135ed3ff..10d11885a 100644 --- a/fastvideo/training/training_pipeline.py +++ b/fastvideo/training/training_pipeline.py @@ -619,7 +619,7 @@ class TrainingPipeline(LoRAPipeline, ABC): """ assert training_args is not None training_args.inference_mode = True - training_args.use_cpu_offload = True + training_args.dit_cpu_offload = True if not training_args.log_validation: return if self.validation_pipeline is None: diff --git a/fastvideo/training/wan_i2v_training_pipeline.py b/fastvideo/training/wan_i2v_training_pipeline.py index ca3c16d9e..303ee20a4 100644 --- a/fastvideo/training/wan_i2v_training_pipeline.py +++ b/fastvideo/training/wan_i2v_training_pipeline.py @@ -46,7 +46,7 @@ class WanI2VTrainingPipeline(TrainingPipeline): args_copy = deepcopy(training_args) args_copy.inference_mode = True - args_copy.use_cpu_offload = True + args_copy.dit_cpu_offload = True # args_copy.pipeline_config.vae_config.load_encoder = False # validation_pipeline = WanImageToVideoValidationPipeline.from_pretrained( self.validation_pipeline = WanImageToVideoPipeline.from_pretrained( @@ -59,7 +59,7 @@ class WanI2VTrainingPipeline(TrainingPipeline): tp_size=training_args.tp_size, sp_size=training_args.sp_size, num_gpus=training_args.num_gpus, - use_cpu_offload=True) + dit_cpu_offload=True) def _get_next_batch(self, training_batch: TrainingBatch) -> TrainingBatch: assert self.training_args is not None @@ -225,5 +225,5 @@ if __name__ == "__main__": parser = TrainingArgs.add_cli_args(parser) parser = FastVideoArgs.add_cli_args(parser) args = parser.parse_args() - args.use_cpu_offload = False + args.dit_cpu_offload = False main(args) diff --git a/fastvideo/training/wan_training_pipeline.py b/fastvideo/training/wan_training_pipeline.py index 4ca04379b..d3c2578ab 100644 --- a/fastvideo/training/wan_training_pipeline.py +++ b/fastvideo/training/wan_training_pipeline.py @@ -36,7 +36,7 @@ class WanTrainingPipeline(TrainingPipeline): args_copy = deepcopy(training_args) args_copy.inference_mode = True - args_copy.use_cpu_offload = True + args_copy.dit_cpu_offload = True args_copy.pipeline_config.vae_config.load_encoder = False validation_pipeline = WanPipeline.from_pretrained( training_args.model_path, @@ -70,5 +70,5 @@ if __name__ == "__main__": parser = TrainingArgs.add_cli_args(parser) parser = FastVideoArgs.add_cli_args(parser) args = parser.parse_args() - args.use_cpu_offload = False + args.dit_cpu_offload = False main(args) From cc703eca51a1ce9b75fee9b49cbc8c4892ea3a31 Mon Sep 17 00:00:00 2001 From: Kevin Lin <42618777+kevin314@users.noreply.github.com> Date: Fri, 25 Jul 2025 20:39:05 -0500 Subject: [PATCH 5/7] [CI] Add publish workflow for ComfyUI (#647) --- .github/workflows/publish-comfyui.yml | 28 ++++++++++++++++++++++++++ comfyui/assets/logo.png | Bin 0 -> 31646 bytes pyproject.toml | 10 +++++++++ 3 files changed, 38 insertions(+) create mode 100644 .github/workflows/publish-comfyui.yml create mode 100644 comfyui/assets/logo.png diff --git a/.github/workflows/publish-comfyui.yml b/.github/workflows/publish-comfyui.yml new file mode 100644 index 000000000..031d52b3d --- /dev/null +++ b/.github/workflows/publish-comfyui.yml @@ -0,0 +1,28 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + - master + paths: + - "pyproject.toml" + +permissions: + issues: write + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'hao-ai-lab' }} + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + submodules: true + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@v1 + with: + ## Add your own personal access token to your Github Repository secrets and reference it here. + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} \ No newline at end of file diff --git a/comfyui/assets/logo.png b/comfyui/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3063b7b4e97b7947b90740804503ce7c4bce5a20 GIT binary patch literal 31646 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 8d08fb6fa..18807ca9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,3 +139,13 @@ column_limit = 80 line_length = 80 use_parentheses = true skip_gitignore = true + + +[project.urls] +Repository = "https://github.com/hao-ai-lab/FastVideo" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "FastVideo" +DisplayName = "FastVideo" +Icon = "https://raw.githubusercontent.com/hao-ai-lab/FastVideo/main/comfyui/assets/logo.png" From d8bb5abc462a16151448c426ebed2dc9e1b866f2 Mon Sep 17 00:00:00 2001 From: Kevin Lin <42618777+kevin314@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:02:52 -0500 Subject: [PATCH 6/7] [CI] Fix ComfyUI publisher ID (#648) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 18807ca9e..3b1369d09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,6 @@ Repository = "https://github.com/hao-ai-lab/FastVideo" # Used by Comfy Registry https://comfyregistry.org [tool.comfy] -PublisherId = "FastVideo" +PublisherId = "fastvideo" DisplayName = "FastVideo" Icon = "https://raw.githubusercontent.com/hao-ai-lab/FastVideo/main/comfyui/assets/logo.png" From cb320dfa3a13c4c260ffa052a902a74dc7f4d1ea Mon Sep 17 00:00:00 2001 From: William Lin Date: Sat, 26 Jul 2025 19:46:26 -0700 Subject: [PATCH 7/7] [bugfix] VideoGenerator improperly extracts output_video_name (#649) --- fastvideo/configs/sample/base.py | 7 +++++++ fastvideo/entrypoints/video_generator.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/fastvideo/configs/sample/base.py b/fastvideo/configs/sample/base.py index 934497507..2da5f05d7 100644 --- a/fastvideo/configs/sample/base.py +++ b/fastvideo/configs/sample/base.py @@ -23,6 +23,7 @@ class SamplingParam: negative_prompt: str | None = None prompt_path: str | None = None output_path: str = "outputs/" + output_video_name: str | None = None # Batch info num_videos_per_prompt: int = 1 @@ -106,6 +107,12 @@ class SamplingParam: default=SamplingParam.output_path, help="Path to save the generated video", ) + parser.add_argument( + "--output-video-name", + type=str, + default=SamplingParam.output_video_name, + help="Name of the output video", + ) parser.add_argument( "--num-videos-per-prompt", type=int, diff --git a/fastvideo/entrypoints/video_generator.py b/fastvideo/entrypoints/video_generator.py index 3e6581fe5..2933b2f05 100644 --- a/fastvideo/entrypoints/video_generator.py +++ b/fastvideo/entrypoints/video_generator.py @@ -141,6 +141,11 @@ class VideoGenerator: logger.info("Found %d prompts in %s", len(prompts), prompt_txt_path) + if sampling_param is not None: + original_output_video_name = sampling_param.output_video_name + else: + original_output_video_name = None + results = [] for i, batch_prompt in enumerate(prompts): logger.info("Processing prompt %d/%d: %s...", i + 1, @@ -148,6 +153,8 @@ class VideoGenerator: try: # Generate video for this prompt using the same logic below + if sampling_param is not None and original_output_video_name is not None: + sampling_param.output_video_name = original_output_video_name + f"_{i}" result = self._generate_single_video( batch_prompt, sampling_param, **kwargs) @@ -282,7 +289,8 @@ class VideoGenerator: logger.info(debug_str) # Use prompt[:100] for video name - output_video_name = kwargs.get("output_video_name", prompt[:100]) + if sampling_param.output_video_name is None: + sampling_param.output_video_name = prompt[:100] # Prepare batch batch = ForwardBatch( @@ -291,7 +299,6 @@ class VideoGenerator: n_tokens=n_tokens, VSA_sparsity=fastvideo_args.VSA_sparsity, extra={}, - output_video_name=output_video_name, ) # Run inference