Merge branch 'main' of github.com:hao-ai-lab/FastVideo

This commit is contained in:
“BrianChen1129”
2025-07-27 03:31:20 +00:00
41 changed files with 593 additions and 70 deletions
+28
View File
@@ -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 }}
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+1 -1
View File
@@ -310,7 +310,7 @@
"value": -99999,
"cachedValue": "fp16"
},
"use_cpu_offload": {
"dit_cpu_offload": {
"isAuto": true,
"value": -99999,
"cachedValue": true
@@ -517,7 +517,7 @@
"value": "fp16",
"cachedValue": "fp16"
},
"use_cpu_offload": {
"dit_cpu_offload": {
"isAuto": true,
"value": true,
"cachedValue": true
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -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(
+4 -1
View File
@@ -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")
+18 -4
View File
@@ -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__":
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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,
@@ -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")
+145
View File
@@ -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")
+1 -1
View File
@@ -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",
+7
View File
@@ -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,
+17 -1
View File
@@ -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
+1 -1
View File
@@ -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",
@@ -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",
+13 -3
View File
@@ -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:
+74 -5
View File
@@ -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,73 @@ 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)
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,
len(prompts), batch_prompt[:100])
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)
# 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 +287,11 @@ class VideoGenerator:
output_path: {sampling_param.output_path}
""" # type: ignore[attr-defined]
logger.info(debug_str)
# Use prompt[:100] for video name
if sampling_param.output_video_name is None:
sampling_param.output_video_name = prompt[:100]
# Prepare batch
batch = ForwardBatch(
**shallow_asdict(sampling_param),
@@ -229,7 +299,6 @@ class VideoGenerator:
n_tokens=n_tokens,
VSA_sparsity=fastvideo_args.VSA_sparsity,
extra={},
output_video_name=kwargs.get("output_video_name", prompt[:100]),
)
# Run inference
+7
View File
@@ -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
+190 -13
View File
@@ -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.
@@ -67,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
@@ -88,6 +142,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: {
@@ -120,6 +177,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",
@@ -198,6 +273,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",
@@ -226,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.",
@@ -243,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,
@@ -276,6 +371,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 +383,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 +421,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 +440,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 +498,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 +667,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(
+13 -5
View File
@@ -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,
@@ -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
@@ -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),
}
+2 -2
View File
@@ -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()
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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"]
@@ -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
@@ -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}")
@@ -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
+1
View File
@@ -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)
+1
View File
@@ -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)
+1 -1
View File
@@ -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:
@@ -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)
+2 -2
View File
@@ -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)
+10
View File
@@ -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"
+5 -3
View File
@@ -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/
+23
View File
@@ -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"