Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d4d815f8 | ||
|
|
42da276006 | ||
|
|
7b3c4fbdd1 | ||
|
|
f041bd3f49 | ||
|
|
fbb9a73ab4 | ||
|
|
daea023383 | ||
|
|
0422d18377 | ||
|
|
f46c6d923f | ||
|
|
79a31b0b45 | ||
|
|
ef98769b46 | ||
|
|
869c5f7370 | ||
|
|
74529f22a4 | ||
|
|
2a2d67e792 | ||
|
|
bb3367c9ac |
@@ -42,7 +42,6 @@ docs/_build/
|
||||
docs/source/getting_started/examples/
|
||||
docs/source/inference/examples/
|
||||
docs/source/training/examples/
|
||||
docs/source/distillation/examples/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
@@ -95,7 +95,7 @@ For a more detailed guide, please see our [inference quick start](https://hao-ai
|
||||
- [Contribution Guide](https://hao-ai-lab.github.io/FastVideo/getting_started/installation.html)
|
||||
|
||||
## Distillation and Finetuning
|
||||
- [Distillation Guide](https://hao-ai-lab.github.io/FastVideo/distillation/dmd.html)
|
||||
- [Distillation Guide](https://hao-ai-lab.github.io/FastVideo/training/distillation.html)
|
||||
- [Finetuning Guide](https://hao-ai-lab.github.io/FastVideo/training/finetune.html)
|
||||
|
||||
## 📑 Development Plan
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
(v0-data-preprocess)=
|
||||
|
||||
# 🧱 Data Preprocess for Distillation
|
||||
|
||||
For distillation, we use the same data preprocessing pipeline as training. Please refer to the [Training Data Preprocess](../training/data_preprocess.md) for general preprocessing steps.
|
||||
|
||||
## Distillation-Specific Datasets
|
||||
|
||||
### FastVideo 480P Synthetic Wan Dataset
|
||||
|
||||
For Wan2.1 T2V distillation, we use the **FastVideo 480P Synthetic Wan dataset** ([FastVideo/Wan-Syn_77x448x832_600k](https://huggingface.co/datasets/FastVideo/Wan-Syn_77x448x832_600k)) which contains 600k synthetic latents.
|
||||
|
||||
```bash
|
||||
# Download the preprocessed dataset
|
||||
python scripts/huggingface/download_hf.py \
|
||||
--repo_id "FastVideo/Wan-Syn_77x448x832_600k" \
|
||||
--local_dir "FastVideo/Wan-Syn_77x448x832_600k" \
|
||||
--repo_type "dataset"
|
||||
```
|
||||
|
||||
### Crush Smol Dataset
|
||||
|
||||
For Wan2.2 TI2V distillation, we use the crush_smol dataset which includes both raw videos and preprocessed latents.
|
||||
|
||||
```bash
|
||||
# Download dataset
|
||||
python scripts/huggingface/download_hf.py \
|
||||
--repo_id=FastVideo/mini_i2v_dataset \
|
||||
--local_dir=data/mini_i2v_dataset \
|
||||
--repo_type=dataset
|
||||
```
|
||||
|
||||
## Preprocessing for Distillation
|
||||
|
||||
The preprocessing steps are identical to training. Run the appropriate preprocessing script based on your model:
|
||||
|
||||
```bash
|
||||
# For Wan2.1 T2V
|
||||
bash scripts/preprocess/v1_preprocess_wan_data_t2v
|
||||
|
||||
# For Wan2.2 TI2V
|
||||
bash examples/distill/Wan2.2-TI2V-5B-Diffusers/crush_smol/preprocess_wan_data_ti2v_5b.sh
|
||||
```
|
||||
@@ -1,69 +0,0 @@
|
||||
# 🎯 Distillation
|
||||
|
||||
We introduce a new finetuning strategy - **Sparse-distill**, which jointly integrates **[DMD](https://arxiv.org/abs/2405.14867)** and **[VSA](https://arxiv.org/abs/2505.13389)** in a single training process. This approach combines the benefits of both distillation to shorten diffusion steps and sparse attention to reduce attention computations, enabling much faster video generation.
|
||||
|
||||
## 📊 Model Overview
|
||||
|
||||
We provide two distilled models:
|
||||
|
||||
- **[FastWan2.1-T2V-1.3B-Diffusers](https://huggingface.co/FastVideo/FastWan2.1-T2V-1.3B-Diffusers)**: 3-step inference, up to **20 FPS** on H100 GPU
|
||||
- **[FastWan2.1-T2V-14B-480P-Diffusers](https://huggingface.co/FastVideo/FastWan2.1-T2V-14B-480P-Diffusers)**: 3-step inference, up to **50x speed up** at 480P, **70x speed up** at 720P for denoising loop
|
||||
|
||||
Both models are trained on **61×448×832** resolution but support generating videos with **any resolution** (1.3B model mainly support 480P, 14B model support 480P and 720P, quality may degrade for different resolutions).
|
||||
|
||||
## 🗂️ Dataset
|
||||
|
||||
We use the **FastVideo 480P Synthetic Wan dataset** ([FastVideo/Wan-Syn_77x448x832_600k](https://huggingface.co/datasets/FastVideo/Wan-Syn_77x448x832_600k)) for distillation, which contains 600k synthetic latents.
|
||||
|
||||
### Download Dataset
|
||||
|
||||
```bash
|
||||
# Download the preprocessed dataset
|
||||
python scripts/huggingface/download_hf.py \
|
||||
--repo_id "FastVideo/Wan-Syn_77x448x832_600k" \
|
||||
--local_dir "FastVideo/Wan-Syn_77x448x832_600k" \
|
||||
--repo_type "dataset"
|
||||
```
|
||||
|
||||
## 🚀 Training Scripts
|
||||
|
||||
### 1.3B Model Sparse-Distill
|
||||
|
||||
For the 1.3B model, we use **4 nodes with 32 H200 GPUs** (8 GPUs per node):
|
||||
|
||||
```bash
|
||||
# Multi-node training (8 nodes, 64 GPUs total)
|
||||
sbatch examples/distill/Wan2.1-T2V/Wan-Syn-Data-480P/distill_dmd_VSA_t2v_1.3B.slurm
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
- Global batch size: 64
|
||||
- Gradient accumulation steps: 2
|
||||
- Learning rate: 1e-5
|
||||
- VSA attention sparsity: 0.8
|
||||
- Training steps: 4000 (~12 hours)
|
||||
|
||||
### 14B Model Sparse-Distill
|
||||
|
||||
For the 14B model, we use **8 nodes with 64 H200 GPUs** (8 GPUs per node):
|
||||
|
||||
```bash
|
||||
# Multi-node training (8 nodes, 64 GPUs total)
|
||||
sbatch examples/distill/Wan2.1-T2V/Wan-Syn-Data-480P/distill_dmd_VSA_t2v_14B.slurm
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
- Global batch size: 64
|
||||
- Sequence parallel size: 4
|
||||
- Gradient accumulation steps: 4
|
||||
- Learning rate: 1e-5
|
||||
- VSA attention sparsity: 0.9
|
||||
- Training steps: 3000 (~52 hours)
|
||||
- HSDP shard dim: 8
|
||||
|
||||
## ⚙️ Inference
|
||||
Set `MODEL_BASE` to your own model path and run:
|
||||
|
||||
```bash
|
||||
bash scripts/inference/v1_inference_wan_dmd.sh
|
||||
```
|
||||
@@ -287,35 +287,11 @@ def create_nested_structures(
|
||||
relative_path = example.path.relative_to(category_dir)
|
||||
path_parts = relative_path.parts
|
||||
|
||||
if example.category == "training":
|
||||
# For training examples like finetune/wan_i2v_14b_480p/crush_smol
|
||||
if len(path_parts) >= 3:
|
||||
method = path_parts[0] # e.g., "finetune"
|
||||
model = path_parts[1] # e.g., "wan_i2v_14b_480p"
|
||||
dataset = path_parts[2] # e.g., "crush_smol"
|
||||
|
||||
# Initialize nested structure
|
||||
if example.category not in nested_structures:
|
||||
nested_structures[example.category] = {}
|
||||
if method not in nested_structures[example.category]:
|
||||
nested_structures[example.category][method] = {}
|
||||
if model not in nested_structures[example.category][method]:
|
||||
nested_structures[example.category][method][model] = {}
|
||||
|
||||
# Store the nested structure
|
||||
nested_structures[
|
||||
example.category][method][model][dataset] = NestedStructure(
|
||||
category=example.category,
|
||||
method=method,
|
||||
model=model,
|
||||
dataset=dataset,
|
||||
example=example)
|
||||
|
||||
elif example.category == "distillation" and len(path_parts) >= 2:
|
||||
# For distillation examples like Wan2.1-T2V/Wan-Syn-Data-480P
|
||||
model = path_parts[0] # e.g., "Wan2.1-T2V"
|
||||
dataset = path_parts[1] # e.g., "Wan-Syn-Data-480P"
|
||||
method = "DMD" # Default method for distillation
|
||||
# For nested examples like finetune/wan_i2v_14b_480p/crush_smol
|
||||
if len(path_parts) >= 3:
|
||||
method = path_parts[0] # e.g., "finetune"
|
||||
model = path_parts[1] # e.g., "wan_i2v_14b_480p"
|
||||
dataset = path_parts[2] # e.g., "crush_smol"
|
||||
|
||||
# Initialize nested structure
|
||||
if example.category not in nested_structures:
|
||||
|
||||
@@ -33,7 +33,6 @@ With FastVideo's optimizations, you can achieve more than 3x inference improveme
|
||||
FastVideo has the following features:
|
||||
- State-of-the-art performance optimizations for inference
|
||||
- [Sliding Tile Attention](https://arxiv.org/pdf/2502.04507)
|
||||
- [Video Sparse Attention](https://arxiv.org/pdf/2505.13389)
|
||||
- [TeaCache](https://arxiv.org/pdf/2411.19108)
|
||||
- [Sage Attention](https://arxiv.org/abs/2410.02367)
|
||||
- Cutting edge models
|
||||
@@ -79,16 +78,18 @@ inference/add_pipeline
|
||||
|
||||
training/examples/examples_training_index
|
||||
training/data_preprocess
|
||||
training/distillation
|
||||
<!-- training/finetune -->
|
||||
:::
|
||||
|
||||
:::{toctree}
|
||||
<!-- :::{toctree}
|
||||
:caption: Distillation
|
||||
:maxdepth: 1
|
||||
|
||||
distillation/examples/examples_distillation_index
|
||||
distillation/data_preprocess
|
||||
distillation/dmd
|
||||
distillation/dmd -->
|
||||
<!-- training/finetune -->
|
||||
:::
|
||||
|
||||
% What is STA Kernel?
|
||||
@@ -101,16 +102,6 @@ sliding_tile_attention/installation
|
||||
sliding_tile_attention/demo
|
||||
:::
|
||||
|
||||
% What is VSA Kernel?
|
||||
|
||||
:::{toctree}
|
||||
:caption: Video Sparse Attention
|
||||
:maxdepth: 1
|
||||
|
||||
video_sparse_attention/installation
|
||||
video_sparse_attention/demo
|
||||
:::
|
||||
|
||||
:::{toctree}
|
||||
:caption: Design
|
||||
:maxdepth: 1
|
||||
|
||||
@@ -19,7 +19,6 @@ This page describes the various options for speeding up generation times in Fast
|
||||
- Torch SDPA: `FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA`
|
||||
- Flash Attention 2 and 3: `FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN`
|
||||
- Sliding Tile Attention: `FASTVIDEO_ATTENTION_BACKEND=SLIDING_TILE_ATTN`
|
||||
- Video Sparse Attention: `FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN`
|
||||
- Sage Attention: `FASTVIDEO_ATTENTION_BACKEND=SAGE_ATTN`
|
||||
|
||||
### Configuring Backends
|
||||
@@ -75,17 +74,6 @@ pip install st_attn==0.0.4
|
||||
|
||||
Please see [this page](#sta-installation) for more installation instructions.
|
||||
|
||||
(optimizations-vsa)=
|
||||
### Video Sparse Attention
|
||||
**`VIDEO_SPARSE_ATTN`**
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
python setup_vsa.py install
|
||||
```
|
||||
|
||||
Please see [this page](#vsa-installation) for more installation instructions.
|
||||
|
||||
(optimizations-sage)=
|
||||
### Sage Attention
|
||||
**`SAGE_ATTN`**
|
||||
|
||||
@@ -29,13 +29,13 @@ export CUDA_HOME=/usr/local/cuda-12.4
|
||||
export PATH=${CUDA_HOME}/bin:${PATH}
|
||||
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
|
||||
git submodule update --init --recursive
|
||||
python setup_sta.py install
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
# 🧪 Test
|
||||
|
||||
```bash
|
||||
python csrc/attn/tests/test_sta.py
|
||||
python test/test_sta.py
|
||||
```
|
||||
|
||||
# 📋 Usage
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
(v0-distill)=
|
||||
# 🎯 Distill
|
||||
Our distillation recipe is based on [Phased Consistency Model](https://github.com/G-U-N/Phased-Consistency-Model). We did not find significant improvement using multi-phase distillation, so we keep the one phase setup similar to the original latent consistency model's recipe.
|
||||
We use the [MixKit](https://huggingface.co/datasets/LanguageBind/Open-Sora-Plan-v1.1.0/tree/main/all_mixkit) dataset for distillation. To avoid running the text encoder and VAE during training, we prprocess all data to generate text embeddings and VAE latents.
|
||||
Preprocessing instructions can be found [data_preprocess.md](#v0-data-preprocess). For convenience, we also provide preprocessed data that can be downloaded directly using the following command:
|
||||
|
||||
```bash
|
||||
python scripts/huggingface/download_hf.py --repo_id=FastVideo/HD-Mixkit-Finetune-Hunyuan --local_dir=data/HD-Mixkit-Finetune-Hunyuan --repo_type=dataset
|
||||
```
|
||||
|
||||
Next, download the original model weights with:
|
||||
|
||||
```bash
|
||||
python scripts/huggingface/download_hf.py --repo_id=FastVideo/hunyuan --local_dir=data/hunyuan --repo_type=model # original hunyuan
|
||||
python scripts/huggingface/download_hf.py --repo_id=genmo/mochi-1-preview --local_dir=data/mochi --repo_type=model # original mochi
|
||||
```
|
||||
|
||||
To launch the distillation process, use the following commands:
|
||||
|
||||
```
|
||||
bash scripts/distill/distill_hunyuan.sh # for hunyuan
|
||||
bash scripts/distill/distill_mochi.sh # for mochi
|
||||
```
|
||||
|
||||
We also provide an optional script for distillation with adversarial loss, located at `fastvideo/distill_adv.py`. Although we tried adversarial loss, we did not observe significant improvements.
|
||||
@@ -7,7 +7,7 @@ Ensure your data is prepared and preprocessed in the format specified in [data_p
|
||||
python scripts/huggingface/download_hf.py --repo_id=FastVideo/Mochi-Black-Myth --local_dir=data/Mochi-Black-Myth --repo_type=dataset
|
||||
```
|
||||
|
||||
Download the original model weights as specified in the [Distillation Section](../distillation/dmd.md):
|
||||
Download the original model weights as specified in [Distill Section](#v0-distill):
|
||||
|
||||
Then you can run the finetune with:
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
(vsa-demo)=
|
||||
|
||||
# 🎬 Demo
|
||||
@@ -1,61 +0,0 @@
|
||||
(vsa-installation)=
|
||||
|
||||
# 🔧 Installation
|
||||
You can install the Video Sparse Attention package using
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
python setup_vsa.py install
|
||||
```
|
||||
|
||||
# Building from Source
|
||||
We support H100 (via ThunderKittens) and any other GPU (via Triton) for VSA.
|
||||
|
||||
First, install C++20 for ThunderKittens (if using H100):
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install gcc-11 g++-11
|
||||
|
||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
|
||||
|
||||
sudo apt update
|
||||
sudo apt install clang-11
|
||||
```
|
||||
|
||||
Set up CUDA environment (if using CUDA 12.4):
|
||||
|
||||
```bash
|
||||
export CUDA_HOME=/usr/local/cuda-12.4
|
||||
export PATH=${CUDA_HOME}/bin:${PATH}
|
||||
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
|
||||
```
|
||||
|
||||
Install VSA:
|
||||
|
||||
```bash
|
||||
cd csrc/attn/
|
||||
git submodule update --init --recursive
|
||||
python setup_vsa.py install
|
||||
```
|
||||
|
||||
# 🧪 Test
|
||||
|
||||
```bash
|
||||
python csrc/attn/tests/test_vsa.py
|
||||
```
|
||||
|
||||
# 📋 Usage
|
||||
|
||||
```python
|
||||
from vsa import video_sparse_attn
|
||||
|
||||
# q, k, v: [batch_size, num_heads, seq_len, head_dim]
|
||||
# variable_block_sizes: [num_blocks] - number of valid tokens in each block
|
||||
# topk: int - number of top-k blocks to attend to
|
||||
# block_size: int or tuple of 3 ints - size of each block (default: 64 tokens)
|
||||
# compress_attn_weight: optional weight for compressed attention branch
|
||||
|
||||
output = video_sparse_attn(q, k, v, variable_block_sizes, topk, block_size, compress_attn_weight)
|
||||
|
||||
```
|
||||
@@ -1,200 +0,0 @@
|
||||
A person reading a book with words that float off the pages and form pictures.
|
||||
A person diving into a pool of liquid crystal, creating ripples of light.
|
||||
A handheld shot chasing after a group of friends laughing and playing on the beach at sunset.
|
||||
A mysterious ancient temple hidden in the jungle.
|
||||
A high-speed train navigating a steep descent.
|
||||
a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a winter storm
|
||||
A cheetah accelerating to full speed while chasing its prey.
|
||||
A serene orchard is in full bloom, with trees heavy with blossoms and bees buzzing around, darting from flower to flower in a display of natural harmony.
|
||||
A little child let out a big yawn
|
||||
Subtle reflections of a woman on the window of a train moving at hyper-speed in a Japanese city.
|
||||
A truck left along the edge of a cliff, revealing the stunning coastal landscape below with waves crashing against the rocks.
|
||||
A red bird transforms into a flag
|
||||
A zoom-out from a single leaf on a tree to reveal the entire forest, showcasing the vastness and diversity of the woodland.
|
||||
A slow-motion video of a liquid droplet bouncing on a water-repellent surface.
|
||||
Static camera shot. A dinasour running near some lions and chasing them away.
|
||||
an adorable kangaroo wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a beautiful sunset
|
||||
A zoom-in on an artist's brush touching the canvas, highlighting the texture of the paint and the strokes being made.
|
||||
an old man wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a colorful festival
|
||||
A woman is ascending to the sky from the ground
|
||||
View out a window of a giant strange creature walking in rundown city at night, one single street lamp dimly lighting the area.
|
||||
An arc shot around a lone tree in a vast, foggy field at dawn, revealing the changing light and shadows.
|
||||
A person sculpting a statue out of a waterfall, the water solidifying under their touch.
|
||||
The person's forehead creased with concentration as she worked on a challenging puzzle.
|
||||
The person's cheeks flushed with pleasure as she savored a delicious meal.
|
||||
Hand-drawn simple line art, a young kid looking up into space with a wondrous expression on his face.
|
||||
A crab made of different jewlery is walking on the beach. As it walks, it drops different jewelry pieces like diamonds, pearls, etc
|
||||
Gold coins are falling out when elevator door opens
|
||||
the scene transitions from huge waves into a snowy mountain at sunset
|
||||
a giant cathedral is completely filled with cats. there are cats everywhere you look. a man enters the cathedral and bows before the giant cat king sitting on a throne.
|
||||
A mother dog gently picks up a piece of meat and carefully places it in her puppy's bowl, her eyes filled with warmth and care as she watches her little one eat.
|
||||
A soap bubble floating in the air, displaying iridescent colors that shift and change as it moves through different angles of light.
|
||||
A truck left alongside a train moving through the countryside, matching its speed and revealing the changing landscape.
|
||||
An astronaut walking between stone buildings.
|
||||
A close-up shot of the person's face reveals his fear and desperation as he navigates the ship through the storm.
|
||||
A frozen lake slowly cracking and thawing as spring arrives, with sheets of ice breaking apart and drifting across the surface.
|
||||
A FPV shot zooming through a tunnel into a vibrant underwater space.
|
||||
a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a colorful festival
|
||||
A person sips on a smoothie, the cool and fruity flavors refreshing her mouth.
|
||||
In a vibrant theater, a magician in dazzling attire stands center stage, pulling a comically oversized rubber chicken from an ornate, old-fashioned box. His costume shimmers under the stage lights, adding to the spectacle. The crowd erupts in laughter and applause, their faces filled with joy and amazement. The magician's expression hints at mischievous delight as he holds up the rubber chicken, his performance bringing cheer to the audience.
|
||||
A hamster running on a spinning wheel.
|
||||
A quaint village nestled in a valley is surrounded by blooming cherry blossoms, with petals drifting through the air as villagers go about their daily activities, adding life to the scene.
|
||||
In a tranquil forest clearing, a sparkling waterfall cascades down into a clear pool, surrounded by lush greenery and flowers, with occasional birds fluttering by.
|
||||
A woman beamed with pride as she watched her child perform on stage.
|
||||
an adorable kangaroo wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a winter storm
|
||||
A man is eating salad
|
||||
An Asian girl wearing a bright yellow T-shirt and white pants is Hip-Hop dancing
|
||||
nighttime footage of a hermit crab using an incandescent lightbulb as its shell
|
||||
a toy robot wearing a green dress and a sun hat taking a pleasant stroll in Antarctica during a beautiful sunset
|
||||
A goat operating a food truck, serving gourmet grilled cheese sandwiches to a line of animals.
|
||||
Macro shot. Man in an antique scuba helmet with dark glass walking out of a flower
|
||||
A bustling train station in the heart of a vibrant city.
|
||||
Light filtering through a canopy of autumn leaves, casting warm, dappled patterns of yellow, orange, and red onto the ground.
|
||||
Chimneys in the setting sun
|
||||
A longboarder accelerating downhill, carving through turns.
|
||||
A couple runs through a sudden downpour, laughing and splashing in puddles as they try to find shelter.
|
||||
A glass of iced coffee condensing water on the outside, with droplets forming and sliding down the glass in slow motion.
|
||||
macro shot of a leaf showing tiny trains moving through its veins
|
||||
A corgi wearing sunglasses walks on the beach of a tropical island
|
||||
Borneo wildlife on the Kinabatangan River
|
||||
A beautiful silhouette animation shows a wolf howling at the moon, feeling lonely, until it finds its pack.
|
||||
an adorable kangaroo wearing blue jeans and a white t shirt taking a pleasant stroll in Johannesburg South Africa during a colorful festival
|
||||
A green monster made of plants walks through an airport.
|
||||
A close up view of a glass sphere that has a zen garden within it. There is a small dwarf in the sphere who is raking the zen garden and creating patterns in the sand.
|
||||
A person on a scooter colliding with a park bench, the scooter tipping over.
|
||||
A tilt-up from a city street, ascending to show the skyline with its mix of modern and historic architecture.
|
||||
A chef tossing a pancake into the air and catching it.
|
||||
A woman whispering a secret into a friend's ear.
|
||||
A vulture circling high in the sky.
|
||||
A medieval castle overlooking a bustling renaissance fair.
|
||||
a toy robot wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a beautiful sunset
|
||||
A man standing in front of a burning building giving the 'thumbs up' sign.
|
||||
The person's cheeks flushed with embarrassment as he told a funny story.
|
||||
Llamas and Emus are playing chess
|
||||
A woman sipping a steaming cup of tea.
|
||||
A tree root bursting through the seat of an ancient, weathered bench, intertwining with the wood.
|
||||
Smoke rises from the chimney of a cozy log cabin nestled in the woods, with soft light glowing from the windows, suggesting a warm and inviting atmosphere.
|
||||
A close-up of sparkling water being poured into a glass, capturing the detailed flow and bubbles.
|
||||
a woman wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a beautiful sunset
|
||||
The Glenfinnan Viaduct is a historic railway bridge in Scotland, UK, that crosses over the west highland line between the towns of Mallaig and Fort William. It is a stunning sight as a steam train leaves the bridge, traveling over the arch-covered viaduct. The landscape is dotted with lush greenery and rocky mountains, creating a picturesque backdrop for the train journey. The sky is blue and the sun is shining, making for a beautiful day to explore this majestic spot.
|
||||
A piece of elastic fabric being pulled and stretched, then returning to its original size when the tension is released.
|
||||
a woman wearing a green dress and a sun hat taking a pleasant stroll in Antarctica during a beautiful sunset
|
||||
A video of a water jet cutting through metal, showing the powerful and precise movement of water.
|
||||
Car mirrors and sunsets
|
||||
Giant Pandas are eating hot noodles in a Chinese restaurant
|
||||
A rally car taking a fast turn on a track
|
||||
a toy robot wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a colorful festival
|
||||
A crystal-clear icicle slowly dripping as it melts in the warmth of the midday sun, each drop sparkling as it falls.
|
||||
A tilt-down from a chandelier in a grand hall, revealing the ornate decor and people mingling below.
|
||||
A man is playing the drums under the water
|
||||
A person playing an electric guitar made of lightning, with thunderous sound waves.
|
||||
A person floating in a bubble, drifting over a bustling cityscape.
|
||||
A tilt-down from a starry night sky, revealing a quiet forest clearing bathed in moonlight.
|
||||
A pan right through a dense jungle, moving past lush vegetation and exotic wildlife.
|
||||
Close-up of a man eating an apple.
|
||||
A low-angle shot of a dancer leaping gracefully into the air, making their movement appear even more dynamic and powerful.
|
||||
A woman is search her bag trying to find something.
|
||||
A bulldozer clears debris from a demolished building, making way for new construction.
|
||||
A man sighed in relief as the doctor delivered the good news.
|
||||
A tsunami coming through an alley in Bulgaria, dynamic movement.
|
||||
Blooming Flowers
|
||||
A push-in through a dense crowd at a festival, moving towards a performer on stage who is captivating the audience.
|
||||
A truck right through a tranquil garden, moving past blooming flowers, trees, and a small fountain.
|
||||
The person's eyes sparkled with excitement as he greeted a friend.
|
||||
A person playing chess with a robot on a floating platform above the ocean.
|
||||
A gentle breeze rustles the leaves as someone walks down a serene forest path, sunlight filtering through the trees and shifting patterns on the ground as branches sway.
|
||||
A rollercoaster ride from a city to a desert and then to an ice world
|
||||
A pan left across an ancient library, moving from shelf to shelf, showcasing rows of leather-bound books.
|
||||
A mother otter floating on her back in a river, cradling her pup on her stomach to keep it safe and warm in the gentle current.
|
||||
an adorable kangaroo wearing purple overalls and cowboy boots taking a pleasant stroll in Johannesburg South Africa during a colorful festival
|
||||
a woman wearing a green dress and a sun hat taking a pleasant stroll in Mumbai India during a colorful festival
|
||||
A delicate layer of morning frost melting off a flower petal, the tiny droplets glistening like diamonds in the light.
|
||||
A panda is cooking for her child, her child is next to her.
|
||||
Macro shot of a man wearing an antique diving helmet with dark glass and a jetpack walking on the veins of a leaf. Realistic style
|
||||
an old man wearing purple overalls and cowboy boots taking a pleasant stroll in Johannesburg South Africa during a beautiful sunset
|
||||
A girl is unfolding a birthday gift.
|
||||
A pencil drawing an architectural plan.
|
||||
A handheld camera following a dog running through a park, bouncing and tilting as it captures the dog's joyful exploration.
|
||||
A pan left across a serene beach at sunrise, moving from the darkened shore to the brightening horizon.
|
||||
A group of people are clapping to celebrate
|
||||
Vendors set up stalls at a bustling farmer’s market, displaying fresh fruits and vegetables, while people stroll through, selecting produce and enjoying the lively atmosphere.
|
||||
A police helicopter hovers above a high-speed chase, guiding officers on the ground to apprehend a suspect.
|
||||
A paper origami dragon riding a boat in waves. Realistic style.
|
||||
A close-up of a droplet of dew forming on a leaf, capturing the detailed surface tension.
|
||||
a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a beautiful sunset
|
||||
A dry rainbow rose is coming back to life.
|
||||
A glass falling off a table and shattering on the floor.
|
||||
A marathon runner crossing the finish line after a grueling race.
|
||||
A zoom-in on a drop of morning dew on a leaf, showing the reflection of the surrounding world within it.
|
||||
A child blowing on hot cocoa to cool it down.
|
||||
A squad of futsal players showcasing their skills on an indoor court.
|
||||
A princess is brushing her long golden hair in the garden.
|
||||
A close-up of a pair of eyes, revealing the subtle emotions and reflections within them.
|
||||
A tracking shot of a group of cyclists racing through a forest trail, with trees and foliage rushing by.
|
||||
A woman yawning widely at the end of a long day.
|
||||
an old man wearing a green dress and a sun hat taking a pleasant stroll in Johannesburg South Africa during a colorful festival
|
||||
Hidden within a garden, an ancient fountain trickles with water, surrounded by vibrant flowers and lush greenery that seem to whisper secrets of the past.
|
||||
A Chinese man sits at a table and eats noodles with chopsticks
|
||||
A pink pig running fast toward the camera in an alley in Tokyo.
|
||||
Strange creatures move through a mysterious, foggy marsh, their silhouettes barely visible through the dense mist as they navigate the eerie, otherworldly landscape.
|
||||
Tour of an art gallery with many beautiful works of art in different styles.
|
||||
FPV flying through a colorful coral lined streets of an underwater suburban neighborhood.
|
||||
Aerial view of Santorini during the blue hour, showcasing the stunning architecture of white Cycladic buildings with blue domes. The caldera views are breathtaking, and the lighting creates a beautiful, serene atmosphere.
|
||||
Camera zoom out. A couple walking along the beach as the sun sets over the ocean.
|
||||
an extreme close up shot of a woman's eye, with her iris appearing as earth
|
||||
a woman wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a colorful festival
|
||||
an old man wearing a green dress and a sun hat taking a pleasant stroll in Mumbai India during a winter storm
|
||||
an adorable kangaroo wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a winter storm
|
||||
A martial artist breaking a board with a powerful punch.
|
||||
People gather on a peaceful beach at sunset, a bonfire crackling as they sit around, enjoying the warmth and the sight of the sun dipping below the horizon.
|
||||
A close-up of a waterfall, showing the detailed movement of water as it crashes down.
|
||||
A child is blowing bubbles
|
||||
a woman wearing a green dress and a sun hat taking a pleasant stroll in Johannesburg South Africa during a winter storm
|
||||
A wide-angle perspective of a serene lake surrounded by mountains, reflecting the sky and creating a sense of infinite space.
|
||||
The person's eyebrows arched in skepticism as she listened to a dubious claim.
|
||||
an old man wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a beautiful sunset
|
||||
a woman wearing a green dress and a sun hat taking a pleasant stroll in Johannesburg South Africa during a colorful festival
|
||||
a woman wearing purple overalls and cowboy boots taking a pleasant stroll in Antarctica during a colorful festival
|
||||
a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a colorful festival
|
||||
A chef flips a pancake and puts cream on it.
|
||||
An astronaut runs on the surface of the moon, the low angle shot shows the vast background of the moon, the movement is smooth and appears lightweight
|
||||
A man's face lit up with happiness as he received a heartfelt compliment.
|
||||
A futuristic spaceport hums with activity as ships of various shapes and sizes take off and land on multiple platforms, their engines glowing with vibrant colors.
|
||||
A person knitting a scarf using beams of light instead of yarn.
|
||||
A pedestal up from the edge of a canyon, gradually revealing the expansive landscape and river below.
|
||||
a woman wearing purple overalls and cowboy boots taking a pleasant stroll in Johannesburg South Africa during a colorful festival
|
||||
an old man wearing blue jeans and a white t shirt taking a pleasant stroll in Johannesburg South Africa during a colorful festival
|
||||
A person walking up a staircase made of clouds leading to a floating castle.
|
||||
Monks meditate in a serene mountaintop temple, sitting in quiet reflection as the wind gently moves through the surrounding trees, creating a sense of peace and tranquility.
|
||||
An aerial shot of a bustling city intersection at rush hour, capturing the organized chaos of cars and pedestrians.
|
||||
A pair of hands skillfully knitting a colorful scarf, the yarn winding through their fingers with each stitch.
|
||||
Close-up, a Chinese child is eating dumplings
|
||||
A kite losing wind and falling to the ground.
|
||||
Bioluminescent waves gently wash ashore on a deserted beach, illuminating the sand with each cresting wave as a figure walks along the water's edge, leaving glowing footprints.
|
||||
A red panda taking a bite of a pizza
|
||||
A close-up shot of a young woman driving a car, looking thoughtful, blurred green forest visible through the rainy car window.
|
||||
A high-speed video of a splash created by a stone thrown into a pond.
|
||||
A metal rod being bent slightly by a force and then springing back to its original straight shape when the force is removed.
|
||||
A hedgehog in a knight's armor, riding a toy horse into a medieval castle.
|
||||
A bird made of fresh oranges rushes out of the orange
|
||||
A low altitude first person perspective camera tracking shot of a soccer player's feet dribbling the ball on the groud in a soccer field, Sports Videography, Motion Tracking camera shot
|
||||
A tranquil island retreat features swaying palm trees and hammocks strung between them, inviting guests to relax and enjoy the serene beauty of the surroundings.
|
||||
a spooky haunted mansion, with friendly jack o lanterns and ghost characters welcoming trick or treaters to the entrance, tilt shift photography
|
||||
A coconut tree made of dollar bills at sunset, with bills falling off like leaves.
|
||||
A motocross bike accelerating out of a tight turn on a dirt track.
|
||||
A tranquil Zen garden with a gently flowing stream and koi fish.
|
||||
A green monster made of leaves walks through the airport, carrying a suitcase.
|
||||
A time-lapse of a frost-covered leaf gradually thawing in the morning sunlight, with tiny water droplets forming and trickling down.
|
||||
A woman practicing her archery skills at a range.
|
||||
A slow-motion video of ink being injected into a tank of water, creating intricate and beautiful patterns.
|
||||
a woman wearing blue jeans and a white t shirt taking a pleasant stroll in Johannesburg South Africa during a winter storm
|
||||
The person's forehead creased with worry as he listened to bad news.
|
||||
An arc shot around a grand piano being played in an empty concert hall, the motion revealing the intricate details of the instrument.
|
||||
A person conducting a symphony of animals in a forest clearing.
|
||||
A truck right alongside a flowing river, capturing the movement of the water and the surrounding forest.
|
||||
A rocket blasting off from the launch pad, accelerating rapidly into the sky.
|
||||
Workers move through a picturesque vineyard during the harvest season, carefully picking grapes and placing them into baskets as the sun bathes the vines in a warm glow.
|
||||
A person is eating an ice cream.
|
||||
An over-the-shoulder perspective of a chef meticulously plating a dish in a bustling kitchen.
|
||||
A man looked away in shame when confronted with his wrongdoing.
|
||||
A person is savoring a slice of pizza at a pizzeria.
|
||||
@@ -2,32 +2,28 @@ from fastvideo import VideoGenerator
|
||||
|
||||
from fastvideo.configs.sample import SamplingParam
|
||||
|
||||
import os
|
||||
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
|
||||
OUTPUT_PATH = "video_samples"
|
||||
def main():
|
||||
# FastVideo will automatically use the optimal default arguments for the
|
||||
# model.
|
||||
# If a local path is provided, FastVideo will make a best effort
|
||||
# attempt to identify the optimal arguments.
|
||||
model_name = "FastVideo/FastWan2.1-T2V-14B-Diffusers"
|
||||
model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
model_name,
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=False,
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
|
||||
pin_cpu_memory=True,
|
||||
# image_encoder_cpu_offload=False,
|
||||
)
|
||||
|
||||
sampling_param = SamplingParam.from_pretrained(model_name)
|
||||
# sampling_param.image_path = "test.jpg"
|
||||
sampling_param.image_path = "test.jpg"
|
||||
# sampling_param.num_inference_steps = 0
|
||||
# Generate videos with the same simple API, regardless of GPU count
|
||||
i2v_prompt = "An astronaut hatching from an egg, on the surface of the moon, the darkness and depth of space realised in the background. High quality, ultrarealistic detail and breath-taking movie-like camera shot."
|
||||
@@ -38,13 +34,7 @@ def main():
|
||||
# "wide with interest. The playful yet serene atmosphere is complemented by soft "
|
||||
# "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
# )
|
||||
results = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param)
|
||||
stage_names = results["stage_names"]
|
||||
stage_execution_times = results["stage_execution_times"]
|
||||
# print(logging_info)
|
||||
print(stage_names)
|
||||
print(stage_execution_times)
|
||||
|
||||
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param)
|
||||
# video = generator.generate_video(prompt, sampling_param=sampling_param, output_path="wan_t2v_videos/")
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# FastVideo Gradio Demo
|
||||
|
||||
This is a Gradio-based web interface for generating videos using the FastVideo framework. The demo allows users to create videos from text prompts with various customization options.
|
||||
|
||||
## Overview
|
||||
|
||||
The demo uses the FastVideo framework to generate videos based on text prompts. It provides a simple web interface built with Gradio that allows users to:
|
||||
|
||||
- Enter text prompts to generate videos
|
||||
- Customize video parameters (dimensions, number of frames, etc.)
|
||||
- Use negative prompts to guide the generation process
|
||||
- Set or randomize seeds for reproducibility
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Run the demo with:
|
||||
|
||||
```bash
|
||||
python examples/inference/gradio/gradio_demo.py
|
||||
```
|
||||
|
||||
This will start a web server at `http://0.0.0.0:7860` where you can access the interface.
|
||||
|
||||
---
|
||||
|
||||
## Model Initialization
|
||||
|
||||
This demo initializes a `VideoGenerator` with the minimum required arguments for inference. Users can seamlessly adjust inference options between generations, including prompts, resolution, video length, or even the number of inference steps, *without ever needing to reload the model*.
|
||||
|
||||
## Video Generation
|
||||
|
||||
The core functionality is in the `generate_video` function, which:
|
||||
1. Processes user inputs
|
||||
2. Uses the FastVideo VideoGenerator from earlier to run inference (`generator.generate_video()`)
|
||||
3. Returns an output path that Gradio uses to display the generated video
|
||||
|
||||
## Gradio Interface
|
||||
|
||||
The interface is built with several components:
|
||||
- A text input for the prompt
|
||||
- A video display for the result
|
||||
- Inference options in a collapsible accordion:
|
||||
- Height and width sliders
|
||||
- Number of frames slider
|
||||
- Guidance scale slider
|
||||
- Inference steps slider
|
||||
- Negative prompt options
|
||||
- Seed controls
|
||||
|
||||
### Inference Options
|
||||
|
||||
- **Height/Width**: Control the resolution of the generated video
|
||||
- **Number of Frames**: Set how many frames to generate
|
||||
- **Guidance Scale**: Control how closely the generation follows the prompt
|
||||
- **Inference Steps**: More steps can improve quality but take longer
|
||||
- **Negative Prompt**: Specify what you don't want to see in the video
|
||||
- **Seed**: Control randomness for reproducible results
|
||||
@@ -0,0 +1,182 @@
|
||||
# FastVideo Dual Model Setup (T2V + I2V)
|
||||
|
||||
This document describes the dual model functionality that has been added to the FastVideo Gradio app, supporting both Text-to-Video (T2V) and Image-to-Video (I2V) generation modes with specialized models.
|
||||
|
||||
## Overview
|
||||
|
||||
The Gradio app now supports both Text-to-Video (T2V) and Image-to-Video (I2V) generation modes using specialized models:
|
||||
- **T2V Model**: `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` for text-to-video generation
|
||||
- **I2V Model**: `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` for image-to-video generation
|
||||
|
||||
Users can switch between modes using the tabbed interface and upload images for I2V generation.
|
||||
|
||||
## Model Configuration
|
||||
|
||||
### T2V Model
|
||||
- **Model**: `FastVideo/FastWan2.1-T2V-1.3B-Diffusers`
|
||||
- **Purpose**: Text-to-video generation
|
||||
- **Default Parameters**: Optimized for text prompts
|
||||
|
||||
### I2V Model
|
||||
- **Model**: `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers`
|
||||
- **Purpose**: Image-to-video generation
|
||||
- **Default Parameters**: Optimized for image animation
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Backend Changes (`ray_serve_backend.py`)
|
||||
|
||||
1. **Added dual model support**:
|
||||
- Separate model paths for T2V and I2V
|
||||
- Automatic model selection based on request type
|
||||
- Independent model initialization
|
||||
|
||||
2. **Updated `VideoGenerationRequest`**:
|
||||
- Added `model_type` field ("t2v" or "i2v")
|
||||
- Added `image_path` field for I2V input
|
||||
|
||||
3. **Enhanced `FastVideoAPI` class**:
|
||||
- Dual model initialization (`t2v_generator` and `i2v_generator`)
|
||||
- Separate default parameters for each model
|
||||
- Automatic model selection in `generate_video` method
|
||||
|
||||
### Frontend Changes (`gradio_frontend.py`)
|
||||
|
||||
1. **Tabbed interface**:
|
||||
- "Text-to-Video" tab for T2V generation
|
||||
- "Image-to-Video" tab for I2V generation
|
||||
|
||||
2. **Automatic model selection**:
|
||||
- T2V tab uses T2V model automatically
|
||||
- I2V tab uses I2V model automatically
|
||||
- Model type sent in API requests
|
||||
|
||||
3. **Separate event handlers**:
|
||||
- `handle_t2v_generation` for text-to-video
|
||||
- `handle_i2v_generation` for image-to-video
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting the Application
|
||||
|
||||
1. **Using the combined startup script (recommended)**:
|
||||
```bash
|
||||
python start_ray_serve_app.py
|
||||
```
|
||||
|
||||
2. **Manual startup**:
|
||||
```bash
|
||||
# Start backend
|
||||
python ray_serve_backend.py \
|
||||
--t2v_model_path "FastVideo/FastWan2.1-T2V-1.3B-Diffusers" \
|
||||
--i2v_model_path "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
|
||||
|
||||
# Start frontend
|
||||
python gradio_frontend.py --backend_url "http://localhost:8000"
|
||||
```
|
||||
|
||||
### Using T2V Mode
|
||||
|
||||
1. Navigate to the "Text-to-Video" tab
|
||||
2. Enter a text prompt describing the video you want to generate
|
||||
3. Adjust advanced parameters if needed
|
||||
4. Click "Run" to generate the video
|
||||
|
||||
### Using I2V Mode
|
||||
|
||||
1. Navigate to the "Image-to-Video" tab
|
||||
2. Upload an image using the image upload component
|
||||
3. Enter a prompt describing how the image should animate
|
||||
4. Adjust advanced parameters if needed
|
||||
5. Click "Run" to generate the video
|
||||
|
||||
### Example Prompts
|
||||
|
||||
**T2V Examples**:
|
||||
- "A hand enters the frame, pulling a sheet of plastic wrap over three balls of dough placed on a wooden surface."
|
||||
- "A vintage train snakes through the mountains, its plume of white steam rising dramatically against the jagged peaks."
|
||||
|
||||
**I2V Examples**:
|
||||
- "The image comes to life with subtle movement, the scene gently animating while maintaining the original composition and mood."
|
||||
- "The static image transforms into a dynamic scene with natural motion, preserving the original lighting and atmosphere."
|
||||
|
||||
## Testing
|
||||
|
||||
A comprehensive test script is provided to verify both T2V and I2V functionality:
|
||||
|
||||
```bash
|
||||
python test_i2v.py
|
||||
```
|
||||
|
||||
This script:
|
||||
- Tests backend health
|
||||
- Tests T2V functionality with text prompts
|
||||
- Tests I2V functionality with image uploads
|
||||
- Verifies response formats for both modes
|
||||
- Cleans up test files
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Backend API Changes
|
||||
|
||||
The `/generate_video` endpoint now accepts:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "Animation description",
|
||||
"model_type": "t2v", // or "i2v"
|
||||
"image_path": "/path/to/input/image.png", // for I2V
|
||||
// ... other parameters
|
||||
}
|
||||
```
|
||||
|
||||
### Model Selection Logic
|
||||
|
||||
- **T2V Mode**: Uses `FastVideo/FastWan2.1-T2V-1.3B-Diffusers`
|
||||
- **I2V Mode**: Uses `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers`
|
||||
- **Automatic Selection**: Based on presence of `image_path` and `model_type`
|
||||
|
||||
### Memory Management
|
||||
|
||||
- Both models are loaded independently
|
||||
- Automatic cleanup prevents memory leaks
|
||||
- Temporary files are cleaned up after processing
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Command Line Arguments
|
||||
|
||||
**Backend**:
|
||||
- `--t2v_model_path`: Path to T2V model
|
||||
- `--i2v_model_path`: Path to I2V model
|
||||
- `--output_path`: Output directory
|
||||
- `--host`, `--port`: Server configuration
|
||||
|
||||
**Frontend**:
|
||||
- `--backend_url`: Backend API URL
|
||||
- `--t2v_model_path`, `--i2v_model_path`: Model paths (for reference)
|
||||
- `--host`, `--port`: Server configuration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Model Loading Issues**: Ensure both models are accessible
|
||||
2. **Memory Issues**: The backend includes automatic cleanup
|
||||
3. **Image Upload Failures**: Check image format and size
|
||||
4. **Generation Failures**: Check backend logs for detailed errors
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Model Loading**: Both models are loaded at startup
|
||||
- **Memory Usage**: Higher memory requirements due to dual models
|
||||
- **Generation Time**: I2V may take longer due to larger model size
|
||||
- **GPU Requirements**: Ensure sufficient VRAM for both models
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- Model switching without restart
|
||||
- Batch processing for both modes
|
||||
- Advanced image preprocessing
|
||||
- Progress indicators
|
||||
- Result caching and history
|
||||
- Model-specific parameter optimization
|
||||
@@ -0,0 +1,222 @@
|
||||
# FastVideo with Ray Serve Backend and Gradio Frontend
|
||||
|
||||
This setup provides a scalable web application for FastVideo inference using Ray Serve as the backend and Gradio as the frontend.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Backend**: Ray Serve handles video generation requests with GPU acceleration
|
||||
- **Frontend**: Gradio provides a user-friendly web interface
|
||||
- **Communication**: HTTP REST API between frontend and backend
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Scalable backend with Ray Serve
|
||||
- ✅ GPU-accelerated video generation
|
||||
- ✅ User-friendly Gradio interface
|
||||
- ✅ Health monitoring and error handling
|
||||
- ✅ All original functionality preserved
|
||||
- ✅ Easy deployment and management
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install the additional dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements_ray_serve.txt
|
||||
```
|
||||
|
||||
2. Ensure you have the FastVideo model available (the default is `FastVideo/FastHunyuan-diffusers`)
|
||||
|
||||
## Usage
|
||||
|
||||
### Option 1: Start Both Services Together (Recommended)
|
||||
|
||||
Use the startup script to launch both backend and frontend:
|
||||
|
||||
```bash
|
||||
python start_ray_serve_app.py
|
||||
```
|
||||
|
||||
This will:
|
||||
- Start the Ray Serve backend on port 8000
|
||||
- Start the Gradio frontend on port 7860
|
||||
- Monitor both services and provide unified logging
|
||||
- Handle graceful shutdown with Ctrl+C
|
||||
|
||||
### Option 2: Start Services Separately
|
||||
|
||||
#### Start Backend Only
|
||||
|
||||
```bash
|
||||
python ray_serve_backend.py --model_path FastVideo/FastHunyuan-diffusers --output_path outputs
|
||||
```
|
||||
|
||||
#### Start Frontend Only
|
||||
|
||||
```bash
|
||||
python gradio_frontend.py --backend_url http://localhost:8000 --model_path FastVideo/FastHunyuan-diffusers
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Command Line Arguments
|
||||
|
||||
#### Startup Script (`start_ray_serve_app.py`)
|
||||
|
||||
- `--model_path`: Path to the FastVideo model (default: `FastVideo/FastHunyuan-diffusers`)
|
||||
- `--output_path`: Directory to save generated videos (default: `outputs`)
|
||||
- `--backend_host`: Backend host to bind to (default: `0.0.0.0`)
|
||||
- `--backend_port`: Backend port (default: `8000`)
|
||||
- `--frontend_host`: Frontend host to bind to (default: `0.0.0.0`)
|
||||
- `--frontend_port`: Frontend port (default: `7860`)
|
||||
- `--skip_backend_check`: Skip backend health check
|
||||
|
||||
#### Backend (`ray_serve_backend.py`)
|
||||
|
||||
- `--model_path`: Path to the FastVideo model
|
||||
- `--output_path`: Directory to save generated videos
|
||||
- `--host`: Host to bind to
|
||||
- `--port`: Port to bind to
|
||||
|
||||
#### Frontend (`gradio_frontend.py`)
|
||||
|
||||
- `--backend_url`: URL of the Ray Serve backend
|
||||
- `--model_path`: Path to the model (for default parameters)
|
||||
- `--host`: Host to bind to
|
||||
- `--port`: Port to bind to
|
||||
|
||||
### Environment Variables
|
||||
|
||||
You can also set these environment variables:
|
||||
|
||||
- `FASTVIDEO_MODEL_PATH`: Path to the FastVideo model
|
||||
- `FASTVIDEO_OUTPUT_PATH`: Directory to save generated videos
|
||||
- `RAY_SERVE_HOST`: Backend host
|
||||
- `RAY_SERVE_PORT`: Backend port
|
||||
- `GRADIO_HOST`: Frontend host
|
||||
- `GRADIO_PORT`: Frontend port
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Backend API (Ray Serve)
|
||||
|
||||
- `GET /health`: Health check endpoint
|
||||
- `POST /generate_video`: Video generation endpoint
|
||||
|
||||
#### Video Generation Request
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "A beautiful sunset over the ocean",
|
||||
"negative_prompt": "blurry, low quality",
|
||||
"use_negative_prompt": true,
|
||||
"seed": 42,
|
||||
"guidance_scale": 7.5,
|
||||
"num_frames": 21,
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 20,
|
||||
"randomize_seed": false
|
||||
}
|
||||
```
|
||||
|
||||
#### Video Generation Response
|
||||
|
||||
```json
|
||||
{
|
||||
"output_path": "/path/to/generated/video.mp4",
|
||||
"seed": 42,
|
||||
"success": true,
|
||||
"error_message": null
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Start the application:
|
||||
```bash
|
||||
python start_ray_serve_app.py
|
||||
```
|
||||
|
||||
2. Access the frontend at: `http://localhost:7860`
|
||||
3. Access the backend API at: `http://localhost:8000`
|
||||
|
||||
### Production Deployment
|
||||
|
||||
For production deployment, consider:
|
||||
|
||||
1. **Load Balancing**: Use a reverse proxy (nginx, traefik) in front of the services
|
||||
2. **Monitoring**: Add monitoring and logging (Prometheus, Grafana)
|
||||
3. **Scaling**: Configure Ray Serve for horizontal scaling
|
||||
4. **Security**: Add authentication and rate limiting
|
||||
5. **Storage**: Use shared storage for video outputs
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
Create a Dockerfile for containerized deployment:
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements_ray_serve.txt .
|
||||
RUN pip install -r requirements_ray_serve.txt
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8000 7860
|
||||
|
||||
# Start the application
|
||||
CMD ["python", "start_ray_serve_app.py"]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Backend not starting**: Check GPU availability and Ray installation
|
||||
2. **Frontend can't connect**: Verify backend URL and network connectivity
|
||||
3. **Video generation fails**: Check model path and GPU memory
|
||||
4. **Port conflicts**: Change ports using command line arguments
|
||||
|
||||
### Logs
|
||||
|
||||
- Backend logs are prefixed with `[BACKEND]`
|
||||
- Frontend logs are prefixed with `[FRONTEND]`
|
||||
- Use `--skip_backend_check` if you need to debug startup issues
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
- Adjust `num_replicas` in the Ray Serve deployment for scaling
|
||||
- Configure `max_concurrent_queries` based on GPU memory
|
||||
- Use multiple GPUs by modifying `ray_actor_options`
|
||||
|
||||
## Migration from Original Gradio Demo
|
||||
|
||||
The new setup maintains full compatibility with the original functionality:
|
||||
|
||||
1. All parameters and options are preserved
|
||||
2. The same example prompts are included
|
||||
3. The UI layout and behavior are identical
|
||||
4. Video generation quality is the same
|
||||
|
||||
The main differences are:
|
||||
- Backend processing is now handled by Ray Serve
|
||||
- Better error handling and status monitoring
|
||||
- Scalable architecture for production use
|
||||
- Separation of concerns between frontend and backend
|
||||
|
||||
## Contributing
|
||||
|
||||
To extend this setup:
|
||||
|
||||
1. Add new endpoints to `ray_serve_backend.py`
|
||||
2. Update the frontend in `gradio_frontend.py`
|
||||
3. Modify the startup script if needed
|
||||
4. Update this README with new features
|
||||
@@ -0,0 +1,501 @@
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import statistics
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from copy import deepcopy
|
||||
from typing import List, Dict, Any
|
||||
import threading
|
||||
|
||||
import torch
|
||||
|
||||
# All the prompts for stress testing
|
||||
STRESS_TEST_PROMPTS = [
|
||||
"A person reading a book with words that float off the pages and form pictures.",
|
||||
"A person diving into a pool of liquid crystal, creating ripples of light.",
|
||||
"A handheld shot chasing after a group of friends laughing and playing on the beach at sunset.",
|
||||
"A mysterious ancient temple hidden in the jungle.",
|
||||
"A high-speed train navigating a steep descent.",
|
||||
"a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a winter storm",
|
||||
"A cheetah accelerating to full speed while chasing its prey.",
|
||||
"A serene orchard is in full bloom, with trees heavy with blossoms and bees buzzing around, darting from flower to flower in a display of natural harmony.",
|
||||
"A little child let out a big yawn",
|
||||
"Subtle reflections of a woman on the window of a train moving at hyper-speed in a Japanese city.",
|
||||
"A truck left along the edge of a cliff, revealing the stunning coastal landscape below with waves crashing against the rocks.",
|
||||
"A red bird transforms into a flag",
|
||||
"A zoom-out from a single leaf on a tree to reveal the entire forest, showcasing the vastness and diversity of the woodland.",
|
||||
"A slow-motion video of a liquid droplet bouncing on a water-repellent surface.",
|
||||
"Static camera shot. A dinasour running near some lions and chasing them away.",
|
||||
"an adorable kangaroo wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a beautiful sunset",
|
||||
"A zoom-in on an artist's brush touching the canvas, highlighting the texture of the paint and the strokes being made.",
|
||||
"an old man wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a colorful festival",
|
||||
"A woman is ascending to the sky from the ground",
|
||||
"View out a window of a giant strange creature walking in rundown city at night, one single street lamp dimly lighting the area.",
|
||||
"An arc shot around a lone tree in a vast, foggy field at dawn, revealing the changing light and shadows.",
|
||||
"A person sculpting a statue out of a waterfall, the water solidifying under their touch.",
|
||||
"The person's forehead creased with concentration as she worked on a challenging puzzle.",
|
||||
"The person's cheeks flushed with pleasure as she savored a delicious meal.",
|
||||
"Hand-drawn simple line art, a young kid looking up into space with a wondrous expression on his face.",
|
||||
"A crab made of different jewlery is walking on the beach. As it walks, it drops different jewelry pieces like diamonds, pearls, etc",
|
||||
"Gold coins are falling out when elevator door opens",
|
||||
"the scene transitions from huge waves into a snowy mountain at sunset",
|
||||
"a giant cathedral is completely filled with cats. there are cats everywhere you look. a man enters the cathedral and bows before the giant cat king sitting on a throne.",
|
||||
"A mother dog gently picks up a piece of meat and carefully places it in her puppy's bowl, her eyes filled with warmth and care as she watches her little one eat.",
|
||||
"A soap bubble floating in the air, displaying iridescent colors that shift and change as it moves through different angles of light.",
|
||||
"A truck left alongside a train moving through the countryside, matching its speed and revealing the changing landscape.",
|
||||
"An astronaut walking between stone buildings.",
|
||||
"A close-up shot of the person's face reveals his fear and desperation as he navigates the ship through the storm.",
|
||||
"A frozen lake slowly cracking and thawing as spring arrives, with sheets of ice breaking apart and drifting across the surface.",
|
||||
"A FPV shot zooming through a tunnel into a vibrant underwater space.",
|
||||
"a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a colorful festival",
|
||||
"A person sips on a smoothie, the cool and fruity flavors refreshing her mouth.",
|
||||
"In a vibrant theater, a magician in dazzling attire stands center stage, pulling a comically oversized rubber chicken from an ornate, old-fashioned box. His costume shimmers under the stage lights, adding to the spectacle. The crowd erupts in laughter and applause, their faces filled with joy and amazement. The magician's expression hints at mischievous delight as he holds up the rubber chicken, his performance bringing cheer to the audience.",
|
||||
"A hamster running on a spinning wheel.",
|
||||
"A quaint village nestled in a valley is surrounded by blooming cherry blossoms, with petals drifting through the air as villagers go about their daily activities, adding life to the scene.",
|
||||
"In a tranquil forest clearing, a sparkling waterfall cascades down into a clear pool, surrounded by lush greenery and flowers, with occasional birds fluttering by.",
|
||||
"A woman beamed with pride as she watched her child perform on stage.",
|
||||
"an adorable kangaroo wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a winter storm",
|
||||
"A man is eating salad",
|
||||
"An Asian girl wearing a bright yellow T-shirt and white pants is Hip-Hop dancing",
|
||||
"nighttime footage of a hermit crab using an incandescent lightbulb as its shell",
|
||||
"a toy robot wearing a green dress and a sun hat taking a pleasant stroll in Antarctica during a beautiful sunset",
|
||||
"A goat operating a food truck, serving gourmet grilled cheese sandwiches to a line of animals.",
|
||||
"Macro shot. Man in an antique scuba helmet with dark glass walking out of a flower",
|
||||
"A bustling train station in the heart of a vibrant city.",
|
||||
"Light filtering through a canopy of autumn leaves, casting warm, dappled patterns of yellow, orange, and red onto the ground.",
|
||||
"Chimneys in the setting sun",
|
||||
"A longboarder accelerating downhill, carving through turns.",
|
||||
"A couple runs through a sudden downpour, laughing and splashing in puddles as they try to find shelter.",
|
||||
"A glass of iced coffee condensing water on the outside, with droplets forming and sliding down the glass in slow motion.",
|
||||
"macro shot of a leaf showing tiny trains moving through its veins",
|
||||
"A corgi wearing sunglasses walks on the beach of a tropical island",
|
||||
"Borneo wildlife on the Kinabatangan River",
|
||||
"A beautiful silhouette animation shows a wolf howling at the moon, feeling lonely, until it finds its pack.",
|
||||
"an adorable kangaroo wearing blue jeans and a white t shirt taking a pleasant stroll in Johannesburg South Africa during a colorful festival",
|
||||
"A green monster made of plants walks through an airport.",
|
||||
"A close up view of a glass sphere that has a zen garden within it. There is a small dwarf in the sphere who is raking the zen garden and creating patterns in the sand.",
|
||||
"A person on a scooter colliding with a park bench, the scooter tipping over.",
|
||||
"A tilt-up from a city street, ascending to show the skyline with its mix of modern and historic architecture.",
|
||||
"A chef tossing a pancake into the air and catching it.",
|
||||
"A woman whispering a secret into a friend's ear.",
|
||||
"A vulture circling high in the sky.",
|
||||
"A medieval castle overlooking a bustling renaissance fair.",
|
||||
"a toy robot wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a beautiful sunset",
|
||||
"A man standing in front of a burning building giving the 'thumbs up' sign.",
|
||||
"The person's cheeks flushed with embarrassment as he told a funny story.",
|
||||
"Llamas and Emus are playing chess",
|
||||
"A woman sipping a steaming cup of tea.",
|
||||
"A tree root bursting through the seat of an ancient, weathered bench, intertwining with the wood.",
|
||||
"Smoke rises from the chimney of a cozy log cabin nestled in the woods, with soft light glowing from the windows, suggesting a warm and inviting atmosphere.",
|
||||
"A close-up of sparkling water being poured into a glass, capturing the detailed flow and bubbles.",
|
||||
"a woman wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a beautiful sunset",
|
||||
"The Glenfinnan Viaduct is a historic railway bridge in Scotland, UK, that crosses over the west highland line between the towns of Mallaig and Fort William. It is a stunning sight as a steam train leaves the bridge, traveling over the arch-covered viaduct. The landscape is dotted with lush greenery and rocky mountains, creating a picturesque backdrop for the train journey. The sky is blue and the sun is shining, making for a beautiful day to explore this majestic spot.",
|
||||
"A piece of elastic fabric being pulled and stretched, then returning to its original size when the tension is released.",
|
||||
"a woman wearing a green dress and a sun hat taking a pleasant stroll in Antarctica during a beautiful sunset",
|
||||
"A video of a water jet cutting through metal, showing the powerful and precise movement of water.",
|
||||
"Car mirrors and sunsets",
|
||||
"Giant Pandas are eating hot noodles in a Chinese restaurant",
|
||||
"A rally car taking a fast turn on a track",
|
||||
"a toy robot wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a colorful festival",
|
||||
"A crystal-clear icicle slowly dripping as it melts in the warmth of the midday sun, each drop sparkling as it falls.",
|
||||
"A tilt-down from a chandelier in a grand hall, revealing the ornate decor and people mingling below.",
|
||||
"A man is playing the drums under the water",
|
||||
"A person playing an electric guitar made of lightning, with thunderous sound waves.",
|
||||
"A person floating in a bubble, drifting over a bustling cityscape.",
|
||||
"A tilt-down from a starry night sky, revealing a quiet forest clearing bathed in moonlight.",
|
||||
"A pan right through a dense jungle, moving past lush vegetation and exotic wildlife.",
|
||||
"Close-up of a man eating an apple.",
|
||||
"A low-angle shot of a dancer leaping gracefully into the air, making their movement appear even more dynamic and powerful.",
|
||||
"A woman is search her bag trying to find something.",
|
||||
"A bulldozer clears debris from a demolished building, making way for new construction.",
|
||||
"A man sighed in relief as the doctor delivered the good news.",
|
||||
"A tsunami coming through an alley in Bulgaria, dynamic movement.",
|
||||
"Blooming Flowers",
|
||||
"A push-in through a dense crowd at a festival, moving towards a performer on stage who is captivating the audience.",
|
||||
"A truck right through a tranquil garden, moving past blooming flowers, trees, and a small fountain.",
|
||||
"The person's eyes sparkled with excitement as he greeted a friend.",
|
||||
"A person playing chess with a robot on a floating platform above the ocean.",
|
||||
"A gentle breeze rustles the leaves as someone walks down a serene forest path, sunlight filtering through the trees and shifting patterns on the ground as branches sway.",
|
||||
"A rollercoaster ride from a city to a desert and then to an ice world",
|
||||
"A pan left across an ancient library, moving from shelf to shelf, showcasing rows of leather-bound books.",
|
||||
"A mother otter floating on her back in a river, cradling her pup on her stomach to keep it safe and warm in the gentle current.",
|
||||
"an adorable kangaroo wearing purple overalls and cowboy boots taking a pleasant stroll in Johannesburg South Africa during a colorful festival",
|
||||
"a woman wearing a green dress and a sun hat taking a pleasant stroll in Mumbai India during a colorful festival",
|
||||
"A delicate layer of morning frost melting off a flower petal, the tiny droplets glistening like diamonds in the light.",
|
||||
"A panda is cooking for her child, her child is next to her.",
|
||||
"Macro shot of a man wearing an antique diving helmet with dark glass and a jetpack walking on the veins of a leaf. Realistic style",
|
||||
"an old man wearing purple overalls and cowboy boots taking a pleasant stroll in Johannesburg South Africa during a beautiful sunset",
|
||||
"A girl is unfolding a birthday gift.",
|
||||
"A pencil drawing an architectural plan.",
|
||||
"A handheld camera following a dog running through a park, bouncing and tilting as it captures the dog's joyful exploration.",
|
||||
"A pan left across a serene beach at sunrise, moving from the darkened shore to the brightening horizon.",
|
||||
"A group of people are clapping to celebrate",
|
||||
"Vendors set up stalls at a bustling farmer's market, displaying fresh fruits and vegetables, while people stroll through, selecting produce and enjoying the lively atmosphere.",
|
||||
"A police helicopter hovers above a high-speed chase, guiding officers on the ground to apprehend a suspect.",
|
||||
"A paper origami dragon riding a boat in waves. Realistic style.",
|
||||
"A close-up of a droplet of dew forming on a leaf, capturing the detailed surface tension.",
|
||||
"a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a beautiful sunset",
|
||||
"A dry rainbow rose is coming back to life.",
|
||||
"A glass falling off a table and shattering on the floor.",
|
||||
"A marathon runner crossing the finish line after a grueling race.",
|
||||
"A zoom-in on a drop of morning dew on a leaf, showing the reflection of the surrounding world within it.",
|
||||
"A child blowing on hot cocoa to cool it down.",
|
||||
"A squad of futsal players showcasing their skills on an indoor court.",
|
||||
"A princess is brushing her long golden hair in the garden.",
|
||||
"A close-up of a pair of eyes, revealing the subtle emotions and reflections within them.",
|
||||
"A tracking shot of a group of cyclists racing through a forest trail, with trees and foliage rushing by.",
|
||||
"A woman yawning widely at the end of a long day.",
|
||||
"an old man wearing a green dress and a sun hat taking a pleasant stroll in Johannesburg South Africa during a colorful festival",
|
||||
"Hidden within a garden, an ancient fountain trickles with water, surrounded by vibrant flowers and lush greenery that seem to whisper secrets of the past.",
|
||||
"A Chinese man sits at a table and eats noodles with chopsticks",
|
||||
"A pink pig running fast toward the camera in an alley in Tokyo.",
|
||||
"Strange creatures move through a mysterious, foggy marsh, their silhouettes barely visible through the dense mist as they navigate the eerie, otherworldly landscape.",
|
||||
"Tour of an art gallery with many beautiful works of art in different styles.",
|
||||
"FPV flying through a colorful coral lined streets of an underwater suburban neighborhood.",
|
||||
"Aerial view of Santorini during the blue hour, showcasing the stunning architecture of white Cycladic buildings with blue domes. The caldera views are breathtaking, and the lighting creates a beautiful, serene atmosphere.",
|
||||
"Camera zoom out. A couple walking along the beach as the sun sets over the ocean.",
|
||||
"an extreme close up shot of a woman's eye, with her iris appearing as earth",
|
||||
"a woman wearing purple overalls and cowboy boots taking a pleasant stroll in Mumbai India during a colorful festival",
|
||||
"an old man wearing a green dress and a sun hat taking a pleasant stroll in Mumbai India during a winter storm",
|
||||
"an adorable kangaroo wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a winter storm",
|
||||
"A martial artist breaking a board with a powerful punch.",
|
||||
"People gather on a peaceful beach at sunset, a bonfire crackling as they sit around, enjoying the warmth and the sight of the sun dipping below the horizon.",
|
||||
"A close-up of a waterfall, showing the detailed movement of water as it crashes down.",
|
||||
"A child is blowing bubbles",
|
||||
"a woman wearing a green dress and a sun hat taking a pleasant stroll in Johannesburg South Africa during a winter storm",
|
||||
"A wide-angle perspective of a serene lake surrounded by mountains, reflecting the sky and creating a sense of infinite space.",
|
||||
"The person's eyebrows arched in skepticism as she listened to a dubious claim.",
|
||||
"an old man wearing blue jeans and a white t shirt taking a pleasant stroll in Mumbai India during a beautiful sunset",
|
||||
"a woman wearing a green dress and a sun hat taking a pleasant stroll in Johannesburg South Africa during a colorful festival",
|
||||
"a woman wearing purple overalls and cowboy boots taking a pleasant stroll in Antarctica during a colorful festival",
|
||||
"a toy robot wearing blue jeans and a white t shirt taking a pleasant stroll in Antarctica during a colorful festival",
|
||||
"A chef flips a pancake and puts cream on it.",
|
||||
"An astronaut runs on the surface of the moon, the low angle shot shows the vast background of the moon, the movement is smooth and appears lightweight",
|
||||
"A man's face lit up with happiness as he received a heartfelt compliment.",
|
||||
"A futuristic spaceport hums with activity as ships of various shapes and sizes take off and land on multiple platforms, their engines glowing with vibrant colors.",
|
||||
"A person knitting a scarf using beams of light instead of yarn.",
|
||||
"A pedestal up from the edge of a canyon, gradually revealing the expansive landscape and river below.",
|
||||
"a woman wearing purple overalls and cowboy boots taking a pleasant stroll in Johannesburg South Africa during a colorful festival",
|
||||
"an old man wearing blue jeans and a white t shirt taking a pleasant stroll in Johannesburg South Africa during a colorful festival",
|
||||
"A person walking up a staircase made of clouds leading to a floating castle.",
|
||||
"Monks meditate in a serene mountaintop temple, sitting in quiet reflection as the wind gently moves through the surrounding trees, creating a sense of peace and tranquility.",
|
||||
"An aerial shot of a bustling city intersection at rush hour, capturing the organized chaos of cars and pedestrians.",
|
||||
"A pair of hands skillfully knitting a colorful scarf, the yarn winding through their fingers with each stitch.",
|
||||
"Close-up, a Chinese child is eating dumplings",
|
||||
"A kite losing wind and falling to the ground.",
|
||||
"Bioluminescent waves gently wash ashore on a deserted beach, illuminating the sand with each cresting wave as a figure walks along the water's edge, leaving glowing footprints.",
|
||||
"A red panda taking a bite of a pizza",
|
||||
"A close-up shot of a young woman driving a car, looking thoughtful, blurred green forest visible through the rainy car window.",
|
||||
"A high-speed video of a splash created by a stone thrown into a pond.",
|
||||
"A metal rod being bent slightly by a force and then springing back to its original straight shape when the force is removed.",
|
||||
"A hedgehog in a knight's armor, riding a toy horse into a medieval castle.",
|
||||
"A bird made of fresh oranges rushes out of the orange",
|
||||
"A low altitude first person perspective camera tracking shot of a soccer player's feet dribbling the ball on the groud in a soccer field, Sports Videography, Motion Tracking camera shot",
|
||||
"A tranquil island retreat features swaying palm trees and hammocks strung between them, inviting guests to relax and enjoy the serene beauty of the surroundings.",
|
||||
"a spooky haunted mansion, with friendly jack o lanterns and ghost characters welcoming trick or treaters to the entrance, tilt shift photography",
|
||||
"A coconut tree made of dollar bills at sunset, with bills falling off like leaves.",
|
||||
"A motocross bike accelerating out of a tight turn on a dirt track.",
|
||||
"A tranquil Zen garden with a gently flowing stream and koi fish.",
|
||||
"A green monster made of leaves walks through the airport, carrying a suitcase.",
|
||||
"A time-lapse of a frost-covered leaf gradually thawing in the morning sunlight, with tiny water droplets forming and trickling down.",
|
||||
"A woman practicing her archery skills at a range.",
|
||||
"A slow-motion video of ink being injected into a tank of water, creating intricate and beautiful patterns.",
|
||||
"a woman wearing blue jeans and a white t shirt taking a pleasant stroll in Johannesburg South Africa during a winter storm",
|
||||
"The person's forehead creased with worry as he listened to bad news.",
|
||||
"An arc shot around a grand piano being played in an empty concert hall, the motion revealing the intricate details of the instrument.",
|
||||
"A person conducting a symphony of animals in a forest clearing.",
|
||||
"A truck right alongside a flowing river, capturing the movement of the water and the surrounding forest.",
|
||||
"A rocket blasting off from the launch pad, accelerating rapidly into the sky.",
|
||||
"Workers move through a picturesque vineyard during the harvest season, carefully picking grapes and placing them into baskets as the sun bathes the vines in a warm glow.",
|
||||
"A person is eating an ice cream.",
|
||||
"An over-the-shoulder perspective of a chef meticulously plating a dish in a bustling kitchen.",
|
||||
"A man looked away in shame when confronted with his wrongdoing.",
|
||||
"A person is savoring a slice of pizza at a pizzeria."
|
||||
]
|
||||
|
||||
class BackendStressTest:
|
||||
def __init__(self, output_path: str,
|
||||
server_url: str = "http://localhost:8000", max_concurrent: int = 50):
|
||||
self.output_path = output_path
|
||||
self.server_url = server_url
|
||||
self.max_concurrent = max_concurrent
|
||||
|
||||
# Results storage
|
||||
self.results = []
|
||||
self.lock = threading.Lock()
|
||||
|
||||
async def check_health(self) -> bool:
|
||||
"""Check if the Ray Serve backend is healthy"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.server_url}/health", timeout=aiohttp.ClientTimeout(total=5)) as response:
|
||||
return response.status == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def build_request_params(self, prompt: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Build request parameters for Ray Serve backend"""
|
||||
# Default parameters matching the Ray Serve backend
|
||||
default_params = {
|
||||
'prompt': prompt,
|
||||
'negative_prompt': None,
|
||||
'use_negative_prompt': False,
|
||||
'seed': 42,
|
||||
'guidance_scale': 7.5,
|
||||
'num_frames': 21,
|
||||
'height': 448,
|
||||
'width': 832,
|
||||
'num_inference_steps': 20,
|
||||
'randomize_seed': True,
|
||||
'return_frames': False # Don't return frames for stress testing to reduce overhead
|
||||
}
|
||||
|
||||
# Override with any provided kwargs
|
||||
for key, value in kwargs.items():
|
||||
if key in default_params:
|
||||
default_params[key] = value
|
||||
|
||||
# Randomize seed if requested
|
||||
if default_params.get('randomize_seed', True):
|
||||
default_params['seed'] = torch.randint(0, 1000000, (1,)).item()
|
||||
|
||||
# Handle negative prompt
|
||||
if not default_params.get('use_negative_prompt', False):
|
||||
default_params['negative_prompt'] = None
|
||||
|
||||
# NEW: Remove keys with None values to avoid sending nulls that may break validation
|
||||
clean_params = {k: v for k, v in default_params.items() if v is not None}
|
||||
return clean_params
|
||||
|
||||
async def test_single_request(self, session: aiohttp.ClientSession, prompt: str, request_id: int) -> Dict[str, Any]:
|
||||
"""Test a single request and measure latency"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Build request parameters
|
||||
request_params = self.build_request_params(prompt)
|
||||
|
||||
# Make request to Ray Serve backend
|
||||
async with session.post(
|
||||
f"{self.server_url}/generate_video",
|
||||
json=request_params,
|
||||
timeout=aiohttp.ClientTimeout(total=900) # 15 minute timeout for video generation
|
||||
) as response:
|
||||
|
||||
end_time = time.time()
|
||||
latency = end_time - start_time
|
||||
|
||||
if response.status == 200:
|
||||
response_data = await response.json()
|
||||
if response_data.get('success', False):
|
||||
result = {
|
||||
'request_id': request_id,
|
||||
'prompt': prompt,
|
||||
'latency': latency,
|
||||
'status': 'success',
|
||||
'response_time': latency, # Use our own timing
|
||||
'timestamp': start_time,
|
||||
'output_path': response_data.get('output_path', ''),
|
||||
'used_seed': response_data.get('seed', request_params['seed'])
|
||||
}
|
||||
else:
|
||||
result = {
|
||||
'request_id': request_id,
|
||||
'prompt': prompt,
|
||||
'latency': latency,
|
||||
'status': 'error',
|
||||
'error': response_data.get('error_message', 'Unknown backend error'),
|
||||
'timestamp': start_time
|
||||
}
|
||||
else:
|
||||
response_text = await response.text()
|
||||
result = {
|
||||
'request_id': request_id,
|
||||
'prompt': prompt,
|
||||
'latency': latency,
|
||||
'status': 'error',
|
||||
'error': f"HTTP {response.status}: {response_text}",
|
||||
'timestamp': start_time
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
end_time = time.time()
|
||||
latency = end_time - start_time
|
||||
result = {
|
||||
'request_id': request_id,
|
||||
'prompt': prompt,
|
||||
'latency': latency,
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'timestamp': start_time
|
||||
}
|
||||
|
||||
# Thread-safe result storage
|
||||
with self.lock:
|
||||
self.results.append(result)
|
||||
|
||||
return result
|
||||
|
||||
async def run_stress_test(self, num_iterations: int = 1, concurrent_requests: int = None):
|
||||
"""Run the stress test with multiple iterations and concurrent requests"""
|
||||
if concurrent_requests is None:
|
||||
concurrent_requests = self.max_concurrent
|
||||
|
||||
# Check backend health before starting
|
||||
print(f"Testing Ray Serve backend at {self.server_url}...")
|
||||
if not await self.check_health():
|
||||
print(f"❌ Backend is not healthy at {self.server_url}")
|
||||
print("Make sure the Ray Serve backend is running with:")
|
||||
print("python ray_serve_backend.py")
|
||||
return
|
||||
print("✅ Backend is healthy and ready for stress testing")
|
||||
|
||||
print(f"\nStarting stress test with {len(STRESS_TEST_PROMPTS)} prompts")
|
||||
print(f"Running {num_iterations} iteration(s) with {concurrent_requests} concurrent requests")
|
||||
print(f"Total requests: {len(STRESS_TEST_PROMPTS) * num_iterations}")
|
||||
print(f"Backend URL: {self.server_url}")
|
||||
print("-" * 80)
|
||||
|
||||
all_prompts = STRESS_TEST_PROMPTS * num_iterations
|
||||
request_id = 0
|
||||
|
||||
# Create semaphore to limit concurrent requests
|
||||
semaphore = asyncio.Semaphore(concurrent_requests)
|
||||
|
||||
async def limited_request(session: aiohttp.ClientSession, prompt: str, req_id: int):
|
||||
async with semaphore:
|
||||
return await self.test_single_request(session, prompt, req_id)
|
||||
|
||||
# Run concurrent requests using asyncio
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Create all tasks
|
||||
tasks = [
|
||||
limited_request(session, prompt, request_id + i)
|
||||
for i, prompt in enumerate(all_prompts)
|
||||
]
|
||||
|
||||
# Process completed requests as they finish
|
||||
completed = 0
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
try:
|
||||
result = await coro
|
||||
completed += 1
|
||||
prompt = result['prompt']
|
||||
status_icon = "✅" if result['status'] == 'success' else "❌"
|
||||
output_info = f" -> {result.get('output_path', 'N/A')}" if result['status'] == 'success' else ""
|
||||
print(f"{status_icon} [{completed}/{len(all_prompts)}] {result['latency']:.2f}s - {prompt[:50]}...{output_info}")
|
||||
except Exception as e:
|
||||
completed += 1
|
||||
print(f"❌ [{completed}/{len(all_prompts)}] Exception: {e}")
|
||||
|
||||
self.analyze_results()
|
||||
|
||||
def analyze_results(self):
|
||||
"""Analyze and print test results"""
|
||||
print("\n" + "=" * 80)
|
||||
print("STRESS TEST RESULTS")
|
||||
print("=" * 80)
|
||||
|
||||
successful_requests = [r for r in self.results if r['status'] == 'success']
|
||||
failed_requests = [r for r in self.results if r['status'] == 'error']
|
||||
|
||||
print(f"Total Requests: {len(self.results)}")
|
||||
print(f"Successful: {len(successful_requests)}")
|
||||
print(f"Failed: {len(failed_requests)}")
|
||||
print(f"Success Rate: {len(successful_requests)/len(self.results)*100:.1f}%")
|
||||
|
||||
if successful_requests:
|
||||
latencies = [r['latency'] for r in successful_requests]
|
||||
print(f"\nLatency Statistics (seconds):")
|
||||
print(f" Min: {min(latencies):.2f}")
|
||||
print(f" Max: {max(latencies):.2f}")
|
||||
print(f" Mean: {statistics.mean(latencies):.2f}")
|
||||
print(f" Median: {statistics.median(latencies):.2f}")
|
||||
print(f" Std Dev: {statistics.stdev(latencies):.2f}")
|
||||
|
||||
# Percentiles
|
||||
sorted_latencies = sorted(latencies)
|
||||
p50 = sorted_latencies[int(len(sorted_latencies) * 0.5)]
|
||||
p90 = sorted_latencies[int(len(sorted_latencies) * 0.9)]
|
||||
p95 = sorted_latencies[int(len(sorted_latencies) * 0.95)]
|
||||
p99 = sorted_latencies[int(len(sorted_latencies) * 0.99)]
|
||||
|
||||
print(f" P50: {p50:.2f}")
|
||||
print(f" P90: {p90:.2f}")
|
||||
print(f" P95: {p95:.2f}")
|
||||
print(f" P99: {p99:.2f}")
|
||||
|
||||
if failed_requests:
|
||||
print(f"\nFailed Requests ({len(failed_requests)}):")
|
||||
for req in failed_requests[:5]: # Show first 5 failures
|
||||
print(f" - {req['error']}")
|
||||
if len(failed_requests) > 5:
|
||||
print(f" ... and {len(failed_requests) - 5} more")
|
||||
|
||||
# Save detailed results
|
||||
results_file = os.path.join(self.output_path, "stress_test_results.json")
|
||||
os.makedirs(self.output_path, exist_ok=True)
|
||||
|
||||
with open(results_file, 'w') as f:
|
||||
json.dump({
|
||||
'summary': {
|
||||
'total_requests': len(self.results),
|
||||
'successful_requests': len(successful_requests),
|
||||
'failed_requests': len(failed_requests),
|
||||
'success_rate': len(successful_requests)/len(self.results)*100 if self.results else 0
|
||||
},
|
||||
'latency_stats': {
|
||||
'min': min(latencies) if successful_requests else 0,
|
||||
'max': max(latencies) if successful_requests else 0,
|
||||
'mean': statistics.mean(latencies) if successful_requests else 0,
|
||||
'median': statistics.median(latencies) if successful_requests else 0,
|
||||
'std_dev': statistics.stdev(latencies) if len(successful_requests) > 1 else 0
|
||||
},
|
||||
'detailed_results': self.results
|
||||
}, f, indent=2)
|
||||
|
||||
print(f"\nDetailed results saved to: {results_file}")
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="FastVideo Ray Serve Backend Stress Test")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs",
|
||||
help="Path to save test results")
|
||||
parser.add_argument("--server_url",
|
||||
type=str,
|
||||
default="http://localhost:8000",
|
||||
help="Ray Serve backend URL")
|
||||
parser.add_argument("--max_concurrent",
|
||||
type=int,
|
||||
default=50,
|
||||
help="Maximum concurrent requests")
|
||||
parser.add_argument("--iterations",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of iterations through all prompts")
|
||||
parser.add_argument("--concurrent_requests",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Number of concurrent requests (overrides max_concurrent)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create stress test instance
|
||||
stress_test = BackendStressTest(
|
||||
output_path=args.output_path,
|
||||
server_url=args.server_url,
|
||||
max_concurrent=args.max_concurrent
|
||||
)
|
||||
|
||||
# Run the stress test
|
||||
await stress_test.run_stress_test(
|
||||
num_iterations=args.iterations,
|
||||
concurrent_requests=args.concurrent_requests
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,169 @@
|
||||
import argparse
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
import gradio as gr
|
||||
import torch
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="FastVideo Gradio Demo")
|
||||
parser.add_argument("--model_path",
|
||||
type=str,
|
||||
default="FastVideo/FastHunyuan-diffusers",
|
||||
help="Path to the model")
|
||||
parser.add_argument("--num_gpus",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of GPUs to use")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs",
|
||||
help="Path to save generated videos")
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
# args = FastVideoArgs(model_path="FastVideo/FastHunyuan-Diffusers", num_gpus=2)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
model_path=parsed_args.model_path, num_gpus=parsed_args.num_gpus)
|
||||
|
||||
default_params = SamplingParam.from_pretrained(parsed_args.model_path)
|
||||
|
||||
def generate_video(
|
||||
prompt,
|
||||
negative_prompt,
|
||||
use_negative_prompt,
|
||||
seed,
|
||||
guidance_scale,
|
||||
num_frames,
|
||||
height,
|
||||
width,
|
||||
num_inference_steps,
|
||||
randomize_seed=False,
|
||||
):
|
||||
params = deepcopy(default_params)
|
||||
params.prompt = prompt
|
||||
params.negative_prompt = negative_prompt
|
||||
params.seed = seed
|
||||
params.guidance_scale = guidance_scale
|
||||
params.num_frames = num_frames
|
||||
params.height = height
|
||||
params.width = width
|
||||
params.num_inference_steps = num_inference_steps
|
||||
|
||||
if randomize_seed:
|
||||
params.seed = torch.randint(0, 1000000, (1, )).item()
|
||||
|
||||
if not use_negative_prompt:
|
||||
params.negative_prompt = None
|
||||
|
||||
generator.generate_video(prompt=prompt, sampling_param=params)
|
||||
|
||||
output_path = os.path.join(parsed_args.output_path,
|
||||
f"{params.prompt[:100]}.mp4")
|
||||
|
||||
return output_path, params.seed
|
||||
|
||||
examples = [
|
||||
"A hand enters the frame, pulling a sheet of plastic wrap over three balls of dough placed on a wooden surface. The plastic wrap is stretched to cover the dough more securely. The hand adjusts the wrap, ensuring that it is tight and smooth over the dough. The scene focuses on the hand’s movements as it secures the edges of the plastic wrap. No new objects appear, and the camera remains stationary, focusing on the action of covering the dough.",
|
||||
"A vintage train snakes through the mountains, its plume of white steam rising dramatically against the jagged peaks. The cars glint in the late afternoon sun, their deep crimson and gold accents lending a touch of elegance. The tracks carve a precarious path along the cliffside, revealing glimpses of a roaring river far below. Inside, passengers peer out the large windows, their faces lit with awe as the landscape unfolds.",
|
||||
"A crowded rooftop bar buzzes with energy, the city skyline twinkling like a field of stars in the background. Strings of fairy lights hang above, casting a warm, golden glow over the scene. Groups of people gather around high tables, their laughter blending with the soft rhythm of live jazz. The aroma of freshly mixed cocktails and charred appetizers wafts through the air, mingling with the cool night breeze.",
|
||||
]
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.Markdown("# FastVideo Inference Demo")
|
||||
|
||||
with gr.Group():
|
||||
with gr.Row():
|
||||
prompt = gr.Text(
|
||||
label="Prompt",
|
||||
show_label=False,
|
||||
max_lines=1,
|
||||
placeholder="Enter your prompt",
|
||||
container=False,
|
||||
)
|
||||
run_button = gr.Button("Run", scale=0)
|
||||
result = gr.Video(label="Result", show_label=False)
|
||||
|
||||
with gr.Accordion("Advanced options", open=False):
|
||||
with gr.Group():
|
||||
with gr.Row():
|
||||
height = gr.Slider(
|
||||
label="Height",
|
||||
minimum=256,
|
||||
maximum=1024,
|
||||
step=32,
|
||||
value=default_params.height,
|
||||
)
|
||||
width = gr.Slider(label="Width",
|
||||
minimum=256,
|
||||
maximum=1024,
|
||||
step=32,
|
||||
value=default_params.width)
|
||||
|
||||
with gr.Row():
|
||||
num_frames = gr.Slider(
|
||||
label="Number of Frames",
|
||||
minimum=21,
|
||||
maximum=163,
|
||||
value=default_params.num_frames,
|
||||
)
|
||||
guidance_scale = gr.Slider(
|
||||
label="Guidance Scale",
|
||||
minimum=1,
|
||||
maximum=12,
|
||||
value=default_params.guidance_scale,
|
||||
)
|
||||
num_inference_steps = gr.Slider(
|
||||
label="Inference Steps",
|
||||
minimum=4,
|
||||
maximum=100,
|
||||
value=default_params.num_inference_steps,
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
use_negative_prompt = gr.Checkbox(
|
||||
label="Use negative prompt", value=False)
|
||||
negative_prompt = gr.Text(
|
||||
label="Negative prompt",
|
||||
max_lines=1,
|
||||
placeholder="Enter a negative prompt",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
seed = gr.Slider(label="Seed",
|
||||
minimum=0,
|
||||
maximum=1000000,
|
||||
step=1,
|
||||
value=default_params.seed)
|
||||
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
||||
seed_output = gr.Number(label="Used Seed")
|
||||
|
||||
gr.Examples(examples=examples, inputs=prompt)
|
||||
|
||||
use_negative_prompt.change(
|
||||
fn=lambda x: gr.update(visible=x),
|
||||
inputs=use_negative_prompt,
|
||||
outputs=default_params.negative_prompt,
|
||||
)
|
||||
|
||||
run_button.click(
|
||||
fn=generate_video,
|
||||
inputs=[
|
||||
prompt,
|
||||
negative_prompt,
|
||||
use_negative_prompt,
|
||||
seed,
|
||||
guidance_scale,
|
||||
num_frames,
|
||||
height,
|
||||
width,
|
||||
num_inference_steps,
|
||||
randomize_seed,
|
||||
],
|
||||
outputs=[result, seed_output],
|
||||
)
|
||||
|
||||
demo.queue(max_size=20).launch(server_name="0.0.0.0", server_port=7860)
|
||||
@@ -0,0 +1,182 @@
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import signal
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root to the Python path
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def check_frontend_health(frontend_url: str, max_retries: int = 30) -> bool:
|
||||
"""Check if the frontend is healthy"""
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
response = requests.get(frontend_url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Frontend is healthy at {frontend_url}")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
if i < max_retries - 1:
|
||||
print(f"⏳ Waiting for frontend to start... ({i+1}/{max_retries})")
|
||||
time.sleep(2)
|
||||
|
||||
print(f"❌ Frontend failed to start within {max_retries * 2} seconds")
|
||||
return False
|
||||
|
||||
|
||||
def start_frontend_instance(args, instance_id: int, backend_url: str):
|
||||
"""Start a single frontend instance"""
|
||||
frontend_script = Path(__file__).parent / "gradio_frontend.py"
|
||||
frontend_port = args.frontend_base_port + instance_id
|
||||
|
||||
cmd = [
|
||||
sys.executable, str(frontend_script),
|
||||
"--backend_url", backend_url,
|
||||
"--t2v_model_path", args.t2v_model_path,
|
||||
"--i2v_model_path", args.i2v_model_path,
|
||||
"--host", args.frontend_host,
|
||||
"--port", str(frontend_port)
|
||||
]
|
||||
|
||||
print(f"🎨 Starting Frontend {instance_id + 1} on port {frontend_port}...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start the frontend process
|
||||
frontend_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor frontend output
|
||||
def monitor_frontend():
|
||||
for line in frontend_process.stdout:
|
||||
print(f"[FRONTEND-{instance_id + 1}] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_frontend, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return frontend_process, frontend_port
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="FastVideo Multi-Frontend Launcher")
|
||||
|
||||
# Model and output settings
|
||||
parser.add_argument("--t2v_model_path",
|
||||
type=str,
|
||||
default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
help="Path to the T2V model")
|
||||
parser.add_argument("--i2v_model_path",
|
||||
type=str,
|
||||
default="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
|
||||
help="Path to the I2V model")
|
||||
|
||||
# Frontend settings
|
||||
parser.add_argument("--frontend_host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Frontend host to bind to")
|
||||
parser.add_argument("--frontend_base_port",
|
||||
type=int,
|
||||
default=7860,
|
||||
help="Base port for frontend instances")
|
||||
parser.add_argument("--num_frontends",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Number of frontend instances to start")
|
||||
|
||||
# Backend settings
|
||||
parser.add_argument("--backend_url",
|
||||
type=str,
|
||||
default="http://localhost:8000",
|
||||
help="Backend URL for frontends to connect to")
|
||||
|
||||
# Other settings
|
||||
parser.add_argument("--skip_health_check",
|
||||
action="store_true",
|
||||
help="Skip frontend health check")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🎬 FastVideo Multi-Frontend Launcher")
|
||||
print("=" * 50)
|
||||
print(f"T2V Model: {args.t2v_model_path}")
|
||||
print(f"I2V Model: {args.i2v_model_path}")
|
||||
print(f"Backend URL: {args.backend_url}")
|
||||
print(f"Number of Frontends: {args.num_frontends}")
|
||||
print(f"Frontend Base Port: {args.frontend_base_port}")
|
||||
print("=" * 50)
|
||||
|
||||
# Start multiple frontend instances
|
||||
frontend_processes = []
|
||||
frontend_urls = []
|
||||
|
||||
for i in range(args.num_frontends):
|
||||
process, port = start_frontend_instance(args, i, args.backend_url)
|
||||
frontend_processes.append(process)
|
||||
frontend_urls.append(f"http://{args.frontend_host}:{port}")
|
||||
|
||||
# Wait for frontends to be ready
|
||||
if not args.skip_health_check:
|
||||
print("\n⏳ Waiting for frontends to start...")
|
||||
for i, url in enumerate(frontend_urls):
|
||||
if not check_frontend_health(url):
|
||||
print(f"❌ Frontend {i + 1} failed to start. Terminating...")
|
||||
for process in frontend_processes:
|
||||
process.terminate()
|
||||
sys.exit(1)
|
||||
|
||||
print("\n🎉 All frontend instances are starting up!")
|
||||
for i, url in enumerate(frontend_urls):
|
||||
print(f"📺 Frontend {i + 1}: {url}")
|
||||
print("\nPress Ctrl+C to stop all frontend instances...")
|
||||
|
||||
# Signal handler for graceful shutdown
|
||||
def signal_handler(signum, frame):
|
||||
print("\n🛑 Shutting down frontend instances...")
|
||||
for process in frontend_processes:
|
||||
process.terminate()
|
||||
|
||||
# Wait for processes to terminate
|
||||
try:
|
||||
for process in frontend_processes:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("⚠️ Force killing processes...")
|
||||
for process in frontend_processes:
|
||||
process.kill()
|
||||
|
||||
print("✅ Frontend instances stopped")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Monitor processes
|
||||
try:
|
||||
while True:
|
||||
# Check if processes are still running
|
||||
for i, process in enumerate(frontend_processes):
|
||||
if process.poll() is not None:
|
||||
print(f"❌ Frontend {i + 1} process died unexpectedly")
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
signal_handler(signal.SIGINT, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,181 @@
|
||||
# Nginx configuration for FastVideo load balancing
|
||||
# This configuration implements the architecture:
|
||||
# ngrok -> nginx reverse proxy -> frontend1/frontend2 -> backend1×8/backend2×8
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Basic settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 100M; # Allow large video uploads
|
||||
|
||||
# Logging
|
||||
access_log /mnt/fast-disks/nfs/hao_lab/FastVideo/outputs/nginx_access.log;
|
||||
error_log /mnt/fast-disks/nfs/hao_lab/FastVideo/outputs/nginx_error.log;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# Upstream for frontend load balancing
|
||||
upstream frontend_servers {
|
||||
# Round-robin load balancing between frontends
|
||||
server 127.0.0.1:7860 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:7861 weight=1 max_fails=3 fail_timeout=30s;
|
||||
upstream frontend_servers {
|
||||
# Round-robin load balancing between frontends
|
||||
server 127.0.0.1:7860 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:7861 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
# Health check
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Upstream for backend1 load balancing
|
||||
upstream backend1_servers {
|
||||
server 127.0.0.1:8000 weight=1 max_fails=3 fail_timeout=30s; (8 replicas)
|
||||
upstream backend1_servers {
|
||||
# Round-robin load balancing for backend1 replicas
|
||||
server 127.0.0.1:8000 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8001 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8002 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8003 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8004 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8005 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8006 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8007 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Upstream for backend2 load balancing
|
||||
upstream backend2_servers {
|
||||
server 127.0.0.1:8000 weight=1 max_fails=3 fail_timeout=30s; (8 replicas)
|
||||
upstream backend2_servers {
|
||||
# Round-robin load balancing for backend2 replicas
|
||||
server 127.0.0.1:8010 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8011 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8012 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8013 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8014 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8015 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8016 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8017 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Main server block
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Frontend routes (Gradio interfaces)
|
||||
location / {
|
||||
proxy_pass http://frontend_servers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support for Gradio
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# Buffer settings
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
}
|
||||
|
||||
# Backend API routes for frontend1
|
||||
location /api/frontend1/ {
|
||||
# Strip the /api/frontend1/ prefix
|
||||
rewrite ^/api/frontend1/(.*) /$1 break;
|
||||
proxy_pass http://backend1_servers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Timeouts for video generation
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# Buffer settings for large responses
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
}
|
||||
|
||||
# Backend API routes for frontend2
|
||||
location /api/frontend2/ {
|
||||
# Strip the /api/frontend2/ prefix
|
||||
rewrite ^/api/frontend2/(.*) /$1 break;
|
||||
proxy_pass http://backend2_servers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Timeouts for video generation
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# Buffer settings for large responses
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Static files (if needed)
|
||||
location /static/ {
|
||||
alias /var/www/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
# Nginx configuration for FastVideo load balancing
|
||||
# This configuration implements the architecture:
|
||||
# ngrok -> nginx reverse proxy -> frontend1/frontend2 -> backend1×8/backend2×8
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Basic settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 100M; # Allow large video uploads
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# Upstream for frontend load balancing
|
||||
upstream frontend_servers {
|
||||
# Round-robin load balancing between frontends
|
||||
server 127.0.0.1:7860 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:7861 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
# Health check
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Upstream for backend1 load balancing (8 replicas)
|
||||
upstream backend1_servers {
|
||||
# Round-robin load balancing for backend1 replicas
|
||||
server 127.0.0.1:8000 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8001 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8002 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8003 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8004 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8005 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8006 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8007 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Upstream for backend2 load balancing (8 replicas)
|
||||
upstream backend2_servers {
|
||||
# Round-robin load balancing for backend2 replicas
|
||||
server 127.0.0.1:8010 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8011 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8012 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8013 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8014 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8015 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8016 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:8017 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Main server block
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Frontend routes (Gradio interfaces)
|
||||
location / {
|
||||
proxy_pass http://frontend_servers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support for Gradio
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# Buffer settings
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
}
|
||||
|
||||
# Backend API routes for frontend1
|
||||
location /api/frontend1/ {
|
||||
# Strip the /api/frontend1/ prefix
|
||||
rewrite ^/api/frontend1/(.*) /$1 break;
|
||||
proxy_pass http://backend1_servers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Timeouts for video generation
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# Buffer settings for large responses
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
}
|
||||
|
||||
# Backend API routes for frontend2
|
||||
location /api/frontend2/ {
|
||||
# Strip the /api/frontend2/ prefix
|
||||
rewrite ^/api/frontend2/(.*) /$1 break;
|
||||
proxy_pass http://backend2_servers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Timeouts for video generation
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# Buffer settings for large responses
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Static files (if needed)
|
||||
location /static/ {
|
||||
alias /var/www/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,45 +5,16 @@ import base64
|
||||
import io
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Any, Optional, List
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import ray
|
||||
from ray import serve
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi import FastAPI, Request
|
||||
from pydantic import BaseModel
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
import imageio
|
||||
from ray.serve.handle import DeploymentHandle
|
||||
from prometheus_client import Counter, Histogram, generate_latest
|
||||
|
||||
NUM_GPUS = 16
|
||||
DEFAULT_FPS = 16
|
||||
SEED_RANGE_MAX = 1_000_000
|
||||
SUPPORTED_MODELS = [
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
"FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
|
||||
]
|
||||
|
||||
MODEL_CONFIGS = {
|
||||
"1.3B": {
|
||||
"num_cpus": 2,
|
||||
"text_encoder_cpu_offload": False,
|
||||
"dit_cpu_offload": False,
|
||||
"vae_cpu_offload": False,
|
||||
"VSA_sparsity": 0.8,
|
||||
},
|
||||
"14B": {
|
||||
"num_cpus": 16,
|
||||
"text_encoder_cpu_offload": True,
|
||||
"dit_cpu_offload": True,
|
||||
"vae_cpu_offload": False,
|
||||
"VSA_sparsity": 0.9,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class VideoGenerationRequest(BaseModel):
|
||||
@@ -55,297 +26,284 @@ class VideoGenerationRequest(BaseModel):
|
||||
num_frames: int = 21
|
||||
height: int = 448
|
||||
width: int = 832
|
||||
num_inference_steps: int = 20
|
||||
randomize_seed: bool = False
|
||||
return_frames: bool = False
|
||||
model_path: Optional[str] = None
|
||||
return_frames: bool = False # Whether to return base64 encoded frames
|
||||
image_path: Optional[str] = None # Path to input image for I2V
|
||||
model_type: str = "t2v" # "t2v" or "i2v" to specify which model to use
|
||||
|
||||
|
||||
class VideoGenerationResponse(BaseModel):
|
||||
video_data: Optional[str] = None
|
||||
output_path: str
|
||||
seed: int
|
||||
success: bool
|
||||
error_message: Optional[str] = None
|
||||
generation_time: Optional[float] = None
|
||||
model_load_time: Optional[float] = None
|
||||
inference_time: Optional[float] = None
|
||||
encoding_time: Optional[float] = None
|
||||
total_time: Optional[float] = None
|
||||
stage_names: Optional[List[str]] = None
|
||||
stage_execution_times: Optional[List[float]] = None
|
||||
frames: Optional[List[str]] = None # Base64 encoded frames
|
||||
|
||||
|
||||
def encode_video_to_base64(frames: List[np.ndarray], fps: int = DEFAULT_FPS) -> str:
|
||||
def encode_frames_to_base64(frames: List[np.ndarray]) -> List[str]:
|
||||
"""Convert numpy frames (0-255) to base64-encoded PNG images"""
|
||||
if not frames:
|
||||
return ""
|
||||
return []
|
||||
|
||||
try:
|
||||
buffer = io.BytesIO()
|
||||
imageio.mimsave(buffer, frames, fps=fps, format="mp4")
|
||||
buffer.seek(0)
|
||||
|
||||
video_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
return f"data:video/mp4;base64,{video_base64}"
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to encode video: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def setup_model_environment(model_path: str) -> None:
|
||||
if "fullattn" in model_path.lower():
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
||||
else:
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
os.environ["FASTVIDEO_STAGE_LOGGING"] = "1"
|
||||
|
||||
|
||||
def process_generation_result(result: Any) -> tuple[List[np.ndarray], float, List[str], List[float]]:
|
||||
frames = result if isinstance(result, list) else result.get("frames", [])
|
||||
generation_time = result.get("generation_time", 0.0) if isinstance(result, dict) else 0.0
|
||||
encoded_frames = []
|
||||
|
||||
logging_info = result.get("logging_info", None)
|
||||
if logging_info:
|
||||
stage_names = logging_info.get_execution_order()
|
||||
stage_execution_times = [
|
||||
logging_info.get_stage_info(stage_name).get("execution_time", 0.0)
|
||||
for stage_name in stage_names
|
||||
]
|
||||
else:
|
||||
stage_names = []
|
||||
stage_execution_times = []
|
||||
for i, frame in enumerate(frames):
|
||||
try:
|
||||
# Ensure frame is numpy array
|
||||
if not isinstance(frame, np.ndarray):
|
||||
print(f"Warning: Frame {i} is not a numpy array, skipping")
|
||||
continue
|
||||
|
||||
# Ensure frame is uint8
|
||||
if frame.dtype != np.uint8:
|
||||
# Clip values to 0-255 range and convert to uint8
|
||||
frame = np.clip(frame, 0, 255).astype(np.uint8)
|
||||
|
||||
# Convert numpy array to PIL Image
|
||||
if len(frame.shape) == 3 and frame.shape[2] == 3:
|
||||
# RGB image
|
||||
pil_image = Image.fromarray(frame, mode='RGB')
|
||||
elif len(frame.shape) == 3 and frame.shape[2] == 4:
|
||||
# RGBA image
|
||||
pil_image = Image.fromarray(frame, mode='RGBA')
|
||||
elif len(frame.shape) == 2:
|
||||
# Grayscale image
|
||||
pil_image = Image.fromarray(frame, mode='L')
|
||||
else:
|
||||
print(f"Warning: Frame {i} has unsupported shape {frame.shape}, skipping")
|
||||
continue
|
||||
|
||||
# Save to bytes buffer as PNG
|
||||
buffer = io.BytesIO()
|
||||
pil_image.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
|
||||
# Encode to base64
|
||||
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
encoded_frames.append(f"data:image/png;base64,{img_base64}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to encode frame {i}: {e}")
|
||||
continue
|
||||
|
||||
return frames, generation_time, stage_names, stage_execution_times
|
||||
|
||||
|
||||
def prepare_sampling_params(video_request: VideoGenerationRequest, default_params: Any) -> Any:
|
||||
params = deepcopy(default_params)
|
||||
params.prompt = video_request.prompt
|
||||
|
||||
if video_request.use_negative_prompt:
|
||||
params.negative_prompt = video_request.negative_prompt
|
||||
|
||||
params.seed = (video_request.seed if not video_request.randomize_seed
|
||||
else torch.randint(0, SEED_RANGE_MAX, (1,)).item())
|
||||
params.randomize_seed = video_request.randomize_seed
|
||||
params.guidance_scale = video_request.guidance_scale
|
||||
params.num_frames = video_request.num_frames
|
||||
params.height = video_request.height
|
||||
params.width = video_request.width
|
||||
params.save_video = False
|
||||
params.return_frames = False
|
||||
|
||||
return params
|
||||
|
||||
|
||||
class BaseModelDeployment:
|
||||
def __init__(self, model_path: str, output_path: str = "outputs"):
|
||||
self.model_path = model_path
|
||||
self.output_path = output_path
|
||||
self.generator = None
|
||||
self.default_params = None
|
||||
|
||||
os.makedirs(self.output_path, exist_ok=True)
|
||||
setup_model_environment(self.model_path)
|
||||
|
||||
def _initialize_generator(self, config: Dict[str, Any]) -> None:
|
||||
from fastvideo.entrypoints.video_generator import VideoGenerator
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
print(f"Initializing model: {self.model_path}")
|
||||
self.generator = VideoGenerator.from_pretrained(
|
||||
model_path=self.model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
text_encoder_cpu_offload=config["text_encoder_cpu_offload"],
|
||||
dit_cpu_offload=config["dit_cpu_offload"],
|
||||
vae_cpu_offload=config["vae_cpu_offload"],
|
||||
VSA_sparsity=config["VSA_sparsity"],
|
||||
enable_stage_verification=False,
|
||||
)
|
||||
self.default_params = SamplingParam.from_pretrained(self.model_path)
|
||||
|
||||
def generate_video(self, video_request: VideoGenerationRequest) -> VideoGenerationResponse:
|
||||
total_start_time = time.time()
|
||||
|
||||
params = prepare_sampling_params(video_request, self.default_params)
|
||||
|
||||
inference_start_time = time.time()
|
||||
result = self.generator.generate_video(
|
||||
prompt=video_request.prompt,
|
||||
sampling_param=params,
|
||||
save_video=False,
|
||||
return_frames=False,
|
||||
)
|
||||
inference_time = time.time() - inference_start_time
|
||||
|
||||
frames, generation_time, stage_names, stage_execution_times = process_generation_result(result)
|
||||
|
||||
encoding_start_time = time.time()
|
||||
video_data = encode_video_to_base64(frames, fps=DEFAULT_FPS)
|
||||
encoding_time = time.time() - encoding_start_time
|
||||
|
||||
total_time = time.time() - total_start_time
|
||||
|
||||
return VideoGenerationResponse(
|
||||
video_data=video_data,
|
||||
seed=params.seed,
|
||||
success=True,
|
||||
generation_time=generation_time,
|
||||
inference_time=inference_time,
|
||||
encoding_time=encoding_time,
|
||||
total_time=total_time,
|
||||
stage_names=stage_names,
|
||||
stage_execution_times=stage_execution_times,
|
||||
)
|
||||
|
||||
|
||||
@serve.deployment(
|
||||
ray_actor_options={"num_cpus": 2, "num_gpus": 1, "runtime_env": {"conda": "fv"}},
|
||||
)
|
||||
class T2VModelDeployment(BaseModelDeployment):
|
||||
def __init__(self, t2v_model_path: str, output_path: str = "outputs"):
|
||||
super().__init__(t2v_model_path, output_path)
|
||||
self._initialize_generator(MODEL_CONFIGS["1.3B"])
|
||||
print("✅ T2V model initialized successfully")
|
||||
|
||||
|
||||
@serve.deployment(
|
||||
ray_actor_options={"num_cpus": 16, "num_gpus": 1, "runtime_env": {"conda": "fv"}},
|
||||
)
|
||||
class T2V14BModelDeployment(BaseModelDeployment):
|
||||
def __init__(self, t2v_14b_model_path: str, output_path: str = "outputs"):
|
||||
super().__init__(t2v_14b_model_path, output_path)
|
||||
# Override environment for 14B model
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
self._initialize_generator(MODEL_CONFIGS["14B"])
|
||||
print("✅ T2V 14B model initialized successfully")
|
||||
return encoded_frames
|
||||
|
||||
|
||||
# Create FastAPI app with rate limiting
|
||||
app = FastAPI()
|
||||
|
||||
# Initialize rate limiter
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
|
||||
@serve.deployment(num_replicas=50, ray_actor_options={"num_cpus": 2})
|
||||
@serve.deployment(
|
||||
num_replicas=8,
|
||||
# ray_actor_options={"num_cpus": 10, "num_gpus": 1, "runtime_env": {"conda": "fv", "working_dir": "/mnt/fast-disks/nfs/hao_lab/FastVideo"}},
|
||||
ray_actor_options={"num_cpus": 10, "num_gpus": 1, "runtime_env": {"conda": "fv"}},
|
||||
)
|
||||
@serve.ingress(app)
|
||||
class FastVideoAPI:
|
||||
|
||||
def __init__(self, t2v_deployments: Dict[str, DeploymentHandle]):
|
||||
self.t2v_deployments = t2v_deployments
|
||||
def __init__(self, t2v_model_path: str, i2v_model_path: str, output_path: str):
|
||||
self.t2v_model_path = t2v_model_path
|
||||
self.i2v_model_path = i2v_model_path
|
||||
self.output_path = output_path
|
||||
|
||||
# Initialize Prometheus metrics
|
||||
self.request_count = Counter('fastvideo_requests_total', 'Total FastVideo requests', ['model_type', 'status'])
|
||||
self.request_duration = Histogram('fastvideo_request_duration_seconds', 'FastVideo request duration', ['model_type'])
|
||||
self.video_generation_time = Histogram('fastvideo_video_generation_seconds', 'Video generation time', ['model_type'])
|
||||
|
||||
def _get_model_name(self, model_path: Optional[str]) -> str:
|
||||
return model_path.split('/')[-1] if model_path else "unknown"
|
||||
|
||||
def _record_metrics(self, model_name: str, status: str, duration: float, response: Optional[VideoGenerationResponse] = None) -> None:
|
||||
self.request_count.labels(model_type=model_name, status=status).inc()
|
||||
self.request_duration.labels(model_type=model_name).observe(duration)
|
||||
# Initialize the video generators
|
||||
self.t2v_generator = None # Initialize to None
|
||||
self.i2v_generator = None # Initialize to None
|
||||
self.t2v_default_params = None # Initialize to None
|
||||
self.i2v_default_params = None # Initialize to None
|
||||
|
||||
if response and hasattr(response, 'generation_time') and response.generation_time:
|
||||
self.video_generation_time.labels(model_type=model_name).observe(response.generation_time)
|
||||
# Ensure output directory exists
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
time.sleep(10)
|
||||
self._initialize_models() # Ensure models are initialized
|
||||
|
||||
def _initialize_models(self):
|
||||
# Set VSA environment variable
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
# os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
||||
|
||||
# Import only when needed - use direct imports to avoid module-level execution
|
||||
from fastvideo.entrypoints.video_generator import VideoGenerator
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
# Initialize T2V model
|
||||
if self.t2v_generator is None:
|
||||
print(f"Initializing T2V model: {self.t2v_model_path}")
|
||||
self.t2v_generator = VideoGenerator.from_pretrained(
|
||||
model_path=self.t2v_model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
# Adjust these offload parameters if you have < 32GB of VRAM
|
||||
text_encoder_cpu_offload=False,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=False,
|
||||
VSA_sparsity=0.8,
|
||||
# master_port=port,
|
||||
)
|
||||
self.t2v_default_params = SamplingParam.from_pretrained(self.t2v_model_path)
|
||||
print("✅ T2V model initialized successfully")
|
||||
|
||||
# Initialize I2V model
|
||||
# if self.i2v_generator is None:
|
||||
if False:
|
||||
print(f"Initializing I2V model: {self.i2v_model_path}")
|
||||
self.i2v_generator = VideoGenerator.from_pretrained(
|
||||
model_path=self.i2v_model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
# Adjust these offload parameters if you have < 32GB of VRAM
|
||||
text_encoder_cpu_offload=False,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=False,
|
||||
VSA_sparsity=0.8,
|
||||
# master_port=port,
|
||||
)
|
||||
self.i2v_default_params = SamplingParam.from_pretrained(self.i2v_model_path)
|
||||
print("✅ I2V model initialized successfully")
|
||||
|
||||
@app.post("/generate_video", response_model=VideoGenerationResponse)
|
||||
@limiter.limit("10/minute")
|
||||
@limiter.limit("50/minute") # Allow 2 requests per minute per IP
|
||||
async def generate_video(self, request: Request, video_request: VideoGenerationRequest) -> VideoGenerationResponse:
|
||||
"""Route the request to the appropriate model deployment based on model_path."""
|
||||
start_time = time.time()
|
||||
model_name = self._get_model_name(video_request.model_path)
|
||||
|
||||
try:
|
||||
if video_request.model_path not in self.t2v_deployments:
|
||||
raise ValueError(f"Model {video_request.model_path} not found")
|
||||
# Select the appropriate model and parameters based on model_type
|
||||
if video_request.model_type.lower() == "i2v":
|
||||
generator = self.i2v_generator
|
||||
params = deepcopy(self.i2v_default_params)
|
||||
print(f"Using I2V model for generation")
|
||||
else:
|
||||
generator = self.t2v_generator
|
||||
params = deepcopy(self.t2v_default_params)
|
||||
print(f"Using T2V model for generation")
|
||||
|
||||
response_ref = self.t2v_deployments[video_request.model_path].generate_video.remote(video_request)
|
||||
response = await response_ref
|
||||
|
||||
self._record_metrics(model_name, "success", time.time() - start_time, response)
|
||||
return response
|
||||
# Update parameters with request values
|
||||
params.prompt = video_request.prompt
|
||||
# Only override negative prompt if user explicitly opts in
|
||||
if video_request.use_negative_prompt:
|
||||
params.negative_prompt = video_request.negative_prompt
|
||||
|
||||
except Exception as e:
|
||||
self._record_metrics(model_name, "error", time.time() - start_time)
|
||||
params.seed = video_request.seed
|
||||
params.guidance_scale = video_request.guidance_scale
|
||||
params.num_frames = video_request.num_frames
|
||||
params.height = video_request.height
|
||||
params.width = video_request.width
|
||||
params.num_inference_steps = video_request.num_inference_steps
|
||||
|
||||
# Handle seed randomization
|
||||
if video_request.randomize_seed:
|
||||
params.seed = torch.randint(0, 1000000, (1,)).item()
|
||||
|
||||
# Ensure negative_prompt is a non-None string; FastVideo validation disallows None
|
||||
if params.negative_prompt is None:
|
||||
params.negative_prompt = "" # empty string satisfies validator
|
||||
|
||||
# Set up output path and video saving
|
||||
params.save_video = True
|
||||
params.output_path = self.output_path
|
||||
# params.return_frames = False # avoid keeping frames in memory
|
||||
|
||||
# Create a clean filename from the prompt
|
||||
safe_prompt = video_request.prompt[:100].replace(' ', '_').replace('/', '_').replace('\\', '_')
|
||||
|
||||
# Store desired video name inside the SamplingParam to avoid unknown kwarg errors
|
||||
setattr(params, "output_video_name", safe_prompt)
|
||||
|
||||
# Handle image_path for I2V
|
||||
if video_request.image_path:
|
||||
params.image_path = video_request.image_path
|
||||
|
||||
# Generate the video with proper output path and filename
|
||||
result = generator.generate_video(
|
||||
prompt=video_request.prompt,
|
||||
sampling_param=params,
|
||||
save_video=True, # Match the params.save_video setting
|
||||
)
|
||||
|
||||
# The actual output path where the video was saved
|
||||
output_path = os.path.join(self.output_path, f"{safe_prompt}.mp4")
|
||||
|
||||
# Verify the file exists
|
||||
if not os.path.exists(output_path):
|
||||
raise FileNotFoundError(f"Video was not saved to expected location: {output_path}")
|
||||
|
||||
frames = result.get("frames", [])
|
||||
|
||||
# Encode frames to base64 for web transmission only if requested
|
||||
encoded_frames = None
|
||||
if video_request.return_frames and frames:
|
||||
try:
|
||||
encoded_frames = encode_frames_to_base64(frames)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to encode frames: {e}")
|
||||
encoded_frames = None
|
||||
|
||||
response = VideoGenerationResponse(
|
||||
output_path=output_path,
|
||||
frames=encoded_frames,
|
||||
seed=params.seed,
|
||||
success=True
|
||||
)
|
||||
|
||||
# Memory cleanup to avoid OOM in repeated generations
|
||||
import gc
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
return VideoGenerationResponse(
|
||||
video_data=None,
|
||||
output_path="",
|
||||
seed=video_request.seed,
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
generation_time=0,
|
||||
inference_time=0,
|
||||
encoding_time=0,
|
||||
total_time=0,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
@limiter.limit("10/minute")
|
||||
async def health_check(self, request: Request) -> Dict[str, str]:
|
||||
@limiter.limit("10/minute") # Allow 10 health checks per minute per IP
|
||||
async def health_check(self, request: Request):
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.get("/metrics")
|
||||
async def metrics(self) -> Response:
|
||||
return Response(generate_latest(), media_type="text/plain")
|
||||
|
||||
|
||||
def validate_configuration(model_paths: List[str], replicas: List[int]) -> None:
|
||||
assert len(model_paths) == len(replicas), "Number of models and replicas must match"
|
||||
assert sum(replicas) <= NUM_GPUS, f"Total replicas ({sum(replicas)}) must be <= {NUM_GPUS}"
|
||||
|
||||
for model, replica_count in zip(model_paths, replicas):
|
||||
assert model in SUPPORTED_MODELS, f"Model {model} not supported"
|
||||
assert replica_count > 0, f"Replicas must be greater than 0"
|
||||
|
||||
|
||||
def start_ray_serve(
|
||||
*,
|
||||
t2v_model_paths: str,
|
||||
t2v_model_replicas: str,
|
||||
t2v_model_path: str = "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
|
||||
i2v_model_path: str = "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
|
||||
output_path: str = "outputs",
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8000,
|
||||
) -> None:
|
||||
port: int = 8000
|
||||
):
|
||||
"""Start the Ray Serve backend"""
|
||||
# Initialize Ray
|
||||
if not ray.is_initialized():
|
||||
ray.init()
|
||||
|
||||
model_paths = t2v_model_paths.split(",")
|
||||
replicas = [int(r) for r in t2v_model_replicas.split(",")]
|
||||
validate_configuration(model_paths, replicas)
|
||||
|
||||
t2v_deps = {}
|
||||
for model_path, replica_count in zip(model_paths, replicas):
|
||||
t2v_dep = T2VModelDeployment.options(num_replicas=replica_count).bind(model_path, output_path)
|
||||
t2v_deps[model_path] = t2v_dep
|
||||
|
||||
api = FastVideoAPI.bind(t2v_deps)
|
||||
serve.run(api, route_prefix="/", name="fast_video")
|
||||
|
||||
|
||||
# Deploy the API
|
||||
api = FastVideoAPI.bind(t2v_model_path, i2v_model_path, output_path)
|
||||
serve.run(api, route_prefix="/", name="fast_video") # detach
|
||||
|
||||
print(f"Ray Serve backend started at http://{host}:{port}")
|
||||
for model_path, replica_count in zip(model_paths, replicas):
|
||||
print(f"T2V Model: {model_path} | Replicas: {replica_count}")
|
||||
print(f"T2V Model: {t2v_model_path}")
|
||||
print(f"I2V Model: {i2v_model_path}")
|
||||
print(f"Health check: http://{host}:{port}/health")
|
||||
print(f"Video generation endpoint: http://{host}:{port}/generate_video")
|
||||
|
||||
|
||||
def setup_signal_handlers() -> None:
|
||||
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="FastVideo Ray Serve Backend")
|
||||
parser.add_argument("--t2v_model_paths",
|
||||
parser.add_argument("--t2v_model_path",
|
||||
type=str,
|
||||
default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers,FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
|
||||
help="Comma separated list of paths to the T2V model(s)")
|
||||
parser.add_argument("--t2v_model_replicas",
|
||||
default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
help="Path to the T2V model")
|
||||
parser.add_argument("--i2v_model_path",
|
||||
type=str,
|
||||
default="4,4",
|
||||
help="Comma separated list of number of replicas for the T2V model(s)")
|
||||
default="Wan-AI/Wan2.2-TI2V-5B-Diffusers",
|
||||
help="Path to the I2V model")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs",
|
||||
@@ -360,20 +318,20 @@ if __name__ == "__main__":
|
||||
help="Port to bind to")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
model_paths = args.t2v_model_paths.split(",")
|
||||
replicas = [int(r) for r in args.t2v_model_replicas.split(",")]
|
||||
validate_configuration(model_paths, replicas)
|
||||
|
||||
start_ray_serve(
|
||||
t2v_model_paths=args.t2v_model_paths,
|
||||
t2v_model_replicas=args.t2v_model_replicas,
|
||||
t2v_model_path=args.t2v_model_path,
|
||||
i2v_model_path=args.i2v_model_path,
|
||||
output_path=args.output_path,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
)
|
||||
|
||||
setup_signal_handlers()
|
||||
# ---- keep the process alive ---------------------------------
|
||||
import signal, sys, time
|
||||
signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) # Ctrl-C
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) # docker stop etc.
|
||||
|
||||
print("✅ FastVideo backend is running. Press Ctrl-C to stop.")
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
@@ -0,0 +1,334 @@
|
||||
import time
|
||||
import os
|
||||
import torch
|
||||
import base64
|
||||
import io
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
import ray
|
||||
from ray import serve
|
||||
from fastapi import FastAPI, Request
|
||||
from pydantic import BaseModel
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
|
||||
|
||||
class VideoGenerationRequest(BaseModel):
|
||||
prompt: str
|
||||
negative_prompt: Optional[str] = None
|
||||
use_negative_prompt: bool = False
|
||||
seed: int = 42
|
||||
guidance_scale: float = 7.5
|
||||
num_frames: int = 21
|
||||
height: int = 448
|
||||
width: int = 832
|
||||
num_inference_steps: int = 20
|
||||
randomize_seed: bool = False
|
||||
return_frames: bool = False # Whether to return base64 encoded frames
|
||||
image_path: Optional[str] = None # Path to input image for I2V
|
||||
model_type: str = "t2v" # "t2v" or "i2v" to specify which model to use
|
||||
|
||||
|
||||
class VideoGenerationResponse(BaseModel):
|
||||
output_path: str
|
||||
seed: int
|
||||
success: bool
|
||||
error_message: Optional[str] = None
|
||||
frames: Optional[List[str]] = None # Base64 encoded frames
|
||||
|
||||
|
||||
def encode_frames_to_base64(frames: List[np.ndarray]) -> List[str]:
|
||||
"""Convert numpy frames (0-255) to base64-encoded PNG images"""
|
||||
if not frames:
|
||||
return []
|
||||
|
||||
encoded_frames = []
|
||||
|
||||
for i, frame in enumerate(frames):
|
||||
try:
|
||||
# Ensure frame is numpy array
|
||||
if not isinstance(frame, np.ndarray):
|
||||
print(f"Warning: Frame {i} is not a numpy array, skipping")
|
||||
continue
|
||||
|
||||
# Ensure frame is uint8
|
||||
if frame.dtype != np.uint8:
|
||||
# Clip values to 0-255 range and convert to uint8
|
||||
frame = np.clip(frame, 0, 255).astype(np.uint8)
|
||||
|
||||
# Convert numpy array to PIL Image
|
||||
if len(frame.shape) == 3 and frame.shape[2] == 3:
|
||||
# RGB image
|
||||
pil_image = Image.fromarray(frame, mode='RGB')
|
||||
elif len(frame.shape) == 3 and frame.shape[2] == 4:
|
||||
# RGBA image
|
||||
pil_image = Image.fromarray(frame, mode='RGBA')
|
||||
elif len(frame.shape) == 2:
|
||||
# Grayscale image
|
||||
pil_image = Image.fromarray(frame, mode='L')
|
||||
else:
|
||||
print(f"Warning: Frame {i} has unsupported shape {frame.shape}, skipping")
|
||||
continue
|
||||
|
||||
# Save to bytes buffer as PNG
|
||||
buffer = io.BytesIO()
|
||||
pil_image.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
|
||||
# Encode to base64
|
||||
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
encoded_frames.append(f"data:image/png;base64,{img_base64}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to encode frame {i}: {e}")
|
||||
continue
|
||||
|
||||
return encoded_frames
|
||||
|
||||
|
||||
# Create FastAPI app with rate limiting
|
||||
app = FastAPI()
|
||||
|
||||
# Initialize rate limiter
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
|
||||
@serve.deployment(
|
||||
num_replicas=3, # Set to 3 for cluster 0
|
||||
ray_actor_options={
|
||||
"num_cpus": 10,
|
||||
"num_gpus": 1,
|
||||
"runtime_env": {"conda": "fv"},
|
||||
},
|
||||
)
|
||||
@serve.ingress(app)
|
||||
class FastVideoMultiGPUAPI:
|
||||
def __init__(self, t2v_model_path: str, i2v_model_path: str, output_path: str, gpu_id: int = 0):
|
||||
self.t2v_model_path = t2v_model_path
|
||||
self.i2v_model_path = i2v_model_path
|
||||
self.output_path = output_path
|
||||
self.gpu_id = gpu_id
|
||||
|
||||
# Initialize the video generators
|
||||
self.t2v_generator = None
|
||||
self.i2v_generator = None
|
||||
self.t2v_default_params = None
|
||||
self.i2v_default_params = None
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
time.sleep(10)
|
||||
self._initialize_models()
|
||||
|
||||
def _initialize_models(self):
|
||||
# Set VSA environment variable
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
||||
|
||||
# Import only when needed
|
||||
from fastvideo.entrypoints.video_generator import VideoGenerator
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
# Initialize T2V model
|
||||
if False: # Disabled for now
|
||||
print(f"Initializing T2V model on GPU {self.gpu_id}: {self.t2v_model_path}")
|
||||
self.t2v_generator = VideoGenerator.from_pretrained(
|
||||
model_path=self.t2v_model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
text_encoder_cpu_offload=False,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=False,
|
||||
VSA_sparsity=0.8,
|
||||
)
|
||||
self.t2v_default_params = SamplingParam.from_pretrained(self.t2v_model_path)
|
||||
print(f"✅ T2V model initialized successfully on GPU {self.gpu_id}")
|
||||
|
||||
# Initialize I2V model
|
||||
if self.i2v_generator is None:
|
||||
print(f"Initializing I2V model on GPU {self.gpu_id}: {self.i2v_model_path}")
|
||||
self.i2v_generator = VideoGenerator.from_pretrained(
|
||||
model_path=self.i2v_model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
text_encoder_cpu_offload=False,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=False,
|
||||
VSA_sparsity=0.8,
|
||||
)
|
||||
self.i2v_default_params = SamplingParam.from_pretrained(self.i2v_model_path)
|
||||
print(f"✅ I2V model initialized successfully on GPU {self.gpu_id}")
|
||||
|
||||
@app.post("/generate_video", response_model=VideoGenerationResponse)
|
||||
@limiter.limit("2/minute") # Allow 2 requests per minute per IP
|
||||
async def generate_video(self, request: Request, video_request: VideoGenerationRequest) -> VideoGenerationResponse:
|
||||
try:
|
||||
# Select the appropriate model and parameters based on model_type
|
||||
if video_request.model_type.lower() == "i2v":
|
||||
generator = self.i2v_generator
|
||||
params = deepcopy(self.i2v_default_params)
|
||||
print(f"Using I2V model for generation on GPU {self.gpu_id}")
|
||||
else:
|
||||
generator = self.t2v_generator
|
||||
params = deepcopy(self.t2v_default_params)
|
||||
print(f"Using T2V model for generation on GPU {self.gpu_id}")
|
||||
|
||||
# Update parameters with request values
|
||||
params.prompt = video_request.prompt
|
||||
|
||||
# Handle seed randomization
|
||||
if video_request.randomize_seed:
|
||||
params.seed = torch.randint(0, 1000000, (1,)).item()
|
||||
|
||||
# Ensure negative_prompt is a non-None string
|
||||
if params.negative_prompt is None:
|
||||
params.negative_prompt = ""
|
||||
|
||||
# Set up output path and video saving
|
||||
params.save_video = True
|
||||
params.output_path = self.output_path
|
||||
|
||||
# Create a clean filename from the prompt
|
||||
safe_prompt = video_request.prompt[:100].replace(' ', '_').replace('/', '_').replace('\\', '_')
|
||||
setattr(params, "output_video_name", safe_prompt)
|
||||
|
||||
# Handle image_path for I2V
|
||||
if video_request.image_path:
|
||||
params.image_path = video_request.image_path
|
||||
|
||||
# Generate the video
|
||||
result = generator.generate_video(
|
||||
prompt=video_request.prompt,
|
||||
sampling_param=params,
|
||||
save_video=True,
|
||||
)
|
||||
|
||||
frames = result.get("frames", [])
|
||||
|
||||
# Encode frames to base64 for web transmission only if requested
|
||||
encoded_frames = None
|
||||
if video_request.return_frames and frames:
|
||||
try:
|
||||
encoded_frames = encode_frames_to_base64(frames)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to encode frames: {e}")
|
||||
encoded_frames = None
|
||||
|
||||
response = VideoGenerationResponse(
|
||||
output_path="",
|
||||
frames=encoded_frames,
|
||||
seed=params.seed,
|
||||
success=True
|
||||
)
|
||||
|
||||
# Memory cleanup to avoid OOM in repeated generations
|
||||
import gc
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
return VideoGenerationResponse(
|
||||
output_path="",
|
||||
seed=video_request.seed,
|
||||
success=False,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
@limiter.limit("10/minute") # Allow 10 health checks per minute per IP
|
||||
async def health_check(self, request: Request):
|
||||
return {"status": "healthy", "gpu_id": self.gpu_id}
|
||||
|
||||
|
||||
def start_ray_serve_multi_gpu(
|
||||
t2v_model_path: str = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
i2v_model_path: str = "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
|
||||
output_path: str = "outputs",
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8000,
|
||||
num_gpus: int = 8,
|
||||
cluster_id: int = 0
|
||||
):
|
||||
"""Start the Ray Serve backend with multiple GPU replicas"""
|
||||
# Initialize Ray
|
||||
if not ray.is_initialized():
|
||||
ray.init()
|
||||
|
||||
# Use unique application name based on cluster_id
|
||||
app_name = f"fast_video_cluster_{cluster_id}"
|
||||
|
||||
# Deploy the API
|
||||
api = FastVideoMultiGPUAPI.bind(t2v_model_path, i2v_model_path, output_path)
|
||||
serve.run(api, route_prefix=f"/cluster_{cluster_id}", name=app_name)
|
||||
|
||||
print(f"Ray Serve multi-GPU backend started at http://{host}:{port}")
|
||||
print(f"T2V Model: {t2v_model_path}")
|
||||
print(f"I2V Model: {i2v_model_path}")
|
||||
print(f"Number of GPU replicas: {num_gpus}")
|
||||
print(f"Cluster ID: {cluster_id}")
|
||||
print(f"Application name: {app_name}")
|
||||
print(f"Health check: http://{host}:{port}/cluster_{cluster_id}/health")
|
||||
print(f"Video generation endpoint: http://{host}:{port}/cluster_{cluster_id}/generate_video")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="FastVideo Ray Serve Multi-GPU Backend")
|
||||
parser.add_argument("--t2v_model_path",
|
||||
type=str,
|
||||
default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
help="Path to the T2V model")
|
||||
parser.add_argument("--i2v_model_path",
|
||||
type=str,
|
||||
default="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
|
||||
help="Path to the I2V model")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs",
|
||||
help="Path to save generated videos")
|
||||
parser.add_argument("--host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Host to bind to")
|
||||
parser.add_argument("--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port to bind to")
|
||||
parser.add_argument("--num_gpus",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Number of GPU replicas")
|
||||
parser.add_argument("--cluster_id",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Cluster ID for unique naming")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
start_ray_serve_multi_gpu(
|
||||
t2v_model_path=args.t2v_model_path,
|
||||
i2v_model_path=args.i2v_model_path,
|
||||
output_path=args.output_path,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
num_gpus=args.num_gpus,
|
||||
cluster_id=args.cluster_id,
|
||||
)
|
||||
|
||||
# Keep the process alive
|
||||
import signal, sys, time
|
||||
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
|
||||
print("✅ FastVideo multi-GPU backend is running. Press Ctrl-C to stop.")
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
@@ -1,3 +0,0 @@
|
||||
python examples/inference/gradio/start_ray_serve_app.py \
|
||||
--t2v_model_paths "FastVideo/FastWan2.1-T2V-1.3B-Diffusers,FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers" \
|
||||
--t2v_model_replicas "4,4"
|
||||
@@ -12,245 +12,220 @@ import threading
|
||||
import signal
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Add the project root to the Python path
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
DEFAULT_BACKEND_HOST = "0.0.0.0"
|
||||
DEFAULT_BACKEND_PORT = 8000
|
||||
DEFAULT_FRONTEND_HOST = "0.0.0.0"
|
||||
DEFAULT_FRONTEND_PORT = 7860
|
||||
DEFAULT_OUTPUT_PATH = "outputs"
|
||||
DEFAULT_T2V_MODELS = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers,FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers"
|
||||
DEFAULT_T2V_REPLICAS = "4,4"
|
||||
|
||||
HEALTH_CHECK_TIMEOUT = 5
|
||||
HEALTH_CHECK_MAX_RETRIES = 100
|
||||
HEALTH_CHECK_INTERVAL = 2
|
||||
PROCESS_SHUTDOWN_TIMEOUT = 5
|
||||
PROCESS_MONITOR_INTERVAL = 1
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
class ServiceManager:
|
||||
def check_backend_health(backend_url: str, max_retries: int = 100) -> bool:
|
||||
"""Check if the backend is healthy"""
|
||||
health_url = f"{backend_url}/health"
|
||||
|
||||
def __init__(self, args: argparse.Namespace):
|
||||
self.args = args
|
||||
self.backend_process: Optional[subprocess.Popen] = None
|
||||
self.frontend_process: Optional[subprocess.Popen] = None
|
||||
self.backend_url = f"http://{args.backend_host}:{args.backend_port}"
|
||||
|
||||
def check_backend_health(self, max_retries: int = HEALTH_CHECK_MAX_RETRIES) -> bool:
|
||||
health_url = f"{self.backend_url}/health"
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.get(health_url, timeout=HEALTH_CHECK_TIMEOUT)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Backend is healthy at {self.backend_url}")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
print(f"⏳ Waiting for backend to start... ({attempt + 1}/{max_retries})")
|
||||
time.sleep(HEALTH_CHECK_INTERVAL)
|
||||
|
||||
print(f"❌ Backend failed to start within {max_retries * HEALTH_CHECK_INTERVAL} seconds")
|
||||
return False
|
||||
|
||||
def _create_monitor_thread(self, process: subprocess.Popen, service_name: str) -> threading.Thread:
|
||||
def monitor():
|
||||
if process.stdout:
|
||||
for line in process.stdout:
|
||||
print(f"[{service_name}] {line.rstrip()}")
|
||||
|
||||
thread = threading.Thread(target=monitor, daemon=True)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
def _start_service(self, script_name: str, args_dict: Dict[str, Any], service_name: str) -> subprocess.Popen:
|
||||
script_path = Path(__file__).parent / script_name
|
||||
|
||||
cmd = [sys.executable, str(script_path)]
|
||||
for key, value in args_dict.items():
|
||||
cmd.extend([f"--{key}", str(value)])
|
||||
|
||||
print(f"🚀 Starting {service_name}...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
self._create_monitor_thread(process, service_name.upper())
|
||||
return process
|
||||
|
||||
def start_backend(self) -> subprocess.Popen:
|
||||
backend_args = {
|
||||
"t2v_model_paths": self.args.t2v_model_paths,
|
||||
"t2v_model_replicas": self.args.t2v_model_replicas,
|
||||
"output_path": self.args.output_path,
|
||||
"host": self.args.backend_host,
|
||||
"port": self.args.backend_port
|
||||
}
|
||||
|
||||
self.backend_process = self._start_service("ray_serve_backend.py", backend_args, "backend")
|
||||
return self.backend_process
|
||||
|
||||
def start_frontend(self) -> subprocess.Popen:
|
||||
frontend_args = {
|
||||
"backend_url": self.backend_url,
|
||||
"t2v_model_paths": self.args.t2v_model_paths,
|
||||
"host": self.args.frontend_host,
|
||||
"port": self.args.frontend_port
|
||||
}
|
||||
|
||||
self.frontend_process = self._start_service("gradio_frontend.py", frontend_args, "frontend")
|
||||
return self.frontend_process
|
||||
|
||||
def shutdown_services(self) -> None:
|
||||
print("\n🛑 Shutting down services...")
|
||||
|
||||
processes = []
|
||||
if self.frontend_process:
|
||||
self.frontend_process.terminate()
|
||||
processes.append(("frontend", self.frontend_process))
|
||||
|
||||
if self.backend_process:
|
||||
self.backend_process.terminate()
|
||||
processes.append(("backend", self.backend_process))
|
||||
|
||||
for name, process in processes:
|
||||
try:
|
||||
process.wait(timeout=PROCESS_SHUTDOWN_TIMEOUT)
|
||||
print(f"✅ {name.capitalize()} stopped gracefully")
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"⚠️ Force killing {name} process...")
|
||||
process.kill()
|
||||
|
||||
print("✅ All services stopped")
|
||||
|
||||
def monitor_processes(self) -> None:
|
||||
if not self.backend_process or not self.frontend_process:
|
||||
print("❌ Processes not properly initialized")
|
||||
return
|
||||
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
while True:
|
||||
if self.frontend_process.poll() is not None:
|
||||
print("❌ Frontend process died unexpectedly")
|
||||
break
|
||||
|
||||
if self.backend_process.poll() is not None:
|
||||
print("❌ Backend process died unexpectedly")
|
||||
break
|
||||
|
||||
time.sleep(PROCESS_MONITOR_INTERVAL)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
response = requests.get(health_url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Backend is healthy at {backend_url}")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
self.shutdown_services()
|
||||
|
||||
|
||||
def setup_signal_handlers(service_manager: ServiceManager) -> None:
|
||||
def signal_handler(signum: int, frame: Any) -> None:
|
||||
service_manager.shutdown_services()
|
||||
sys.exit(0)
|
||||
if i < max_retries - 1:
|
||||
print(f"⏳ Waiting for backend to start... ({i+1}/{max_retries})")
|
||||
time.sleep(2)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
print(f"❌ Backend failed to start within {max_retries * 2} seconds")
|
||||
return False
|
||||
|
||||
|
||||
def print_startup_info(args: argparse.Namespace) -> None:
|
||||
def start_backend(args):
|
||||
"""Start the Ray Serve backend"""
|
||||
backend_script = Path(__file__).parent / "ray_serve_backend.py"
|
||||
|
||||
cmd = [
|
||||
sys.executable, str(backend_script),
|
||||
"--t2v_model_path", args.t2v_model_path,
|
||||
"--i2v_model_path", args.i2v_model_path,
|
||||
"--output_path", args.output_path,
|
||||
"--host", args.backend_host,
|
||||
"--port", str(args.backend_port)
|
||||
]
|
||||
|
||||
print(f"🚀 Starting Ray Serve backend...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start the backend process
|
||||
backend_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor backend output
|
||||
def monitor_backend():
|
||||
for line in backend_process.stdout:
|
||||
print(f"[BACKEND] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_backend, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return backend_process
|
||||
|
||||
|
||||
def start_frontend(args):
|
||||
"""Start the Gradio frontend"""
|
||||
frontend_script = Path(__file__).parent / "gradio_frontend.py"
|
||||
backend_url = f"http://{args.backend_host}:{args.backend_port}"
|
||||
|
||||
cmd = [
|
||||
sys.executable, str(frontend_script),
|
||||
"--backend_url", backend_url,
|
||||
"--t2v_model_path", args.t2v_model_path,
|
||||
"--i2v_model_path", args.i2v_model_path,
|
||||
"--host", args.frontend_host,
|
||||
"--port", str(args.frontend_port)
|
||||
]
|
||||
|
||||
print(f"🎨 Starting Gradio frontend...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start the frontend process
|
||||
frontend_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor frontend output
|
||||
def monitor_frontend():
|
||||
for line in frontend_process.stdout:
|
||||
print(f"[FRONTEND] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_frontend, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return frontend_process
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="FastVideo Ray Serve App")
|
||||
|
||||
# Model and output settings
|
||||
parser.add_argument("--t2v_model_path",
|
||||
type=str,
|
||||
default="Wan-AI/Wan2.2-TI2V-5BDiffusers",
|
||||
help="Path to the T2V model")
|
||||
parser.add_argument("--i2v_model_path",
|
||||
type=str,
|
||||
default="Wan-AI/Wan2.2-TI2V-5B-Diffusers",
|
||||
help="Path to the I2V model")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs",
|
||||
help="Path to save generated videos")
|
||||
|
||||
# Backend settings
|
||||
parser.add_argument("--backend_host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Backend host to bind to")
|
||||
parser.add_argument("--backend_port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Backend port to bind to")
|
||||
|
||||
# Frontend settings
|
||||
parser.add_argument("--frontend_host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Frontend host to bind to")
|
||||
parser.add_argument("--frontend_port",
|
||||
type=int,
|
||||
default=7860,
|
||||
help="Frontend port to bind to")
|
||||
|
||||
# Other settings
|
||||
parser.add_argument("--skip_backend_check",
|
||||
action="store_true",
|
||||
help="Skip backend health check")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(args.output_path, exist_ok=True)
|
||||
|
||||
print("🎬 FastVideo Ray Serve App")
|
||||
print("=" * 50)
|
||||
print(f"T2V Models: {args.t2v_model_paths}")
|
||||
print(f"T2V Model Replicas: {args.t2v_model_replicas}")
|
||||
print(f"T2V Model: {args.t2v_model_path}")
|
||||
print(f"I2V Model: {args.i2v_model_path}")
|
||||
print(f"Output: {args.output_path}")
|
||||
print(f"Backend: http://{args.backend_host}:{args.backend_port}")
|
||||
print(f"Frontend: http://{args.frontend_host}:{args.frontend_port}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="FastVideo Ray Serve App")
|
||||
|
||||
parser.add_argument("--t2v_model_paths",
|
||||
type=str,
|
||||
default=DEFAULT_T2V_MODELS,
|
||||
help="Comma separated list of paths to the T2V model(s)")
|
||||
parser.add_argument("--t2v_model_replicas",
|
||||
type=str,
|
||||
default=DEFAULT_T2V_REPLICAS,
|
||||
help="Comma separated list of number of replicas for the T2V model(s)")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default=DEFAULT_OUTPUT_PATH,
|
||||
help="Path to save generated videos")
|
||||
# Start backend
|
||||
backend_process = start_backend(args)
|
||||
|
||||
parser.add_argument("--backend_host",
|
||||
type=str,
|
||||
default=DEFAULT_BACKEND_HOST,
|
||||
help="Backend host to bind to")
|
||||
parser.add_argument("--backend_port",
|
||||
type=int,
|
||||
default=DEFAULT_BACKEND_PORT,
|
||||
help="Backend port to bind to")
|
||||
# Wait for backend to be ready
|
||||
backend_url = f"http://{args.backend_host}:{args.backend_port}"
|
||||
|
||||
parser.add_argument("--frontend_host",
|
||||
type=str,
|
||||
default=DEFAULT_FRONTEND_HOST,
|
||||
help="Frontend host to bind to")
|
||||
parser.add_argument("--frontend_port",
|
||||
type=int,
|
||||
default=DEFAULT_FRONTEND_PORT,
|
||||
help="Frontend port to bind to")
|
||||
if not args.skip_backend_check:
|
||||
if not check_backend_health(backend_url):
|
||||
print("❌ Backend failed to start. Terminating...")
|
||||
backend_process.terminate()
|
||||
sys.exit(1)
|
||||
|
||||
parser.add_argument("--skip_backend_check",
|
||||
action="store_true",
|
||||
help="Skip backend health check")
|
||||
# Start frontend
|
||||
frontend_process = start_frontend(args)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_arguments()
|
||||
print("\n🎉 Both services are starting up!")
|
||||
print(f"📺 Frontend will be available at: http://{args.frontend_host}:{args.frontend_port}")
|
||||
print(f"🔧 Backend API will be available at: {backend_url}")
|
||||
print("\nPress Ctrl+C to stop both services...")
|
||||
# return
|
||||
|
||||
os.makedirs(args.output_path, exist_ok=True)
|
||||
print_startup_info(args)
|
||||
# Signal handler for graceful shutdown
|
||||
def signal_handler(signum, frame):
|
||||
print("\n🛑 Shutting down services...")
|
||||
frontend_process.terminate()
|
||||
backend_process.terminate()
|
||||
|
||||
# Wait for processes to terminate
|
||||
try:
|
||||
frontend_process.wait(timeout=5)
|
||||
backend_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("⚠️ Force killing processes...")
|
||||
frontend_process.kill()
|
||||
backend_process.kill()
|
||||
|
||||
print("✅ Services stopped")
|
||||
sys.exit(0)
|
||||
|
||||
service_manager = ServiceManager(args)
|
||||
setup_signal_handlers(service_manager)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Monitor processes
|
||||
try:
|
||||
service_manager.start_backend()
|
||||
|
||||
if not args.skip_backend_check:
|
||||
if not service_manager.check_backend_health():
|
||||
print("❌ Backend failed to start. Terminating...")
|
||||
service_manager.shutdown_services()
|
||||
sys.exit(1)
|
||||
|
||||
service_manager.start_frontend()
|
||||
|
||||
print("\n🎉 Both services are starting up!")
|
||||
print(f"📺 Frontend will be available at: http://{args.frontend_host}:{args.frontend_port}")
|
||||
print(f"🔧 Backend API will be available at: http://{args.backend_host}:{args.backend_port}")
|
||||
print("\nPress Ctrl+C to stop both services...")
|
||||
|
||||
service_manager.monitor_processes()
|
||||
while True:
|
||||
# Check if processes are still running
|
||||
if frontend_process.poll() is not None:
|
||||
print("❌ Frontend process died unexpectedly")
|
||||
break
|
||||
|
||||
if backend_process.poll() is not None:
|
||||
print("❌ Backend process died unexpectedly")
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
service_manager.shutdown_services()
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
signal_handler(signal.SIGINT, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
"""
|
||||
Startup script for FastVideo scalable architecture:
|
||||
ngrok -> nginx reverse proxy -> frontend1/frontend2 -> backend1×8/backend2×8
|
||||
|
||||
This script starts:
|
||||
1. Multiple backend instances (8 GPU replicas each)
|
||||
2. Multiple frontend instances (2 instances)
|
||||
3. Nginx reverse proxy
|
||||
4. Optional ngrok tunnel
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import signal
|
||||
import requests
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root to the Python path
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def check_service_health(url: str, max_retries: int = 50) -> bool:
|
||||
"""Check if a service is healthy"""
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Service is healthy at {url}")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
if i < max_retries - 1:
|
||||
print(f"⏳ Waiting for service to start... ({i+1}/{max_retries})")
|
||||
time.sleep(2)
|
||||
|
||||
print(f"❌ Service failed to start within {max_retries * 2} seconds")
|
||||
return False
|
||||
|
||||
|
||||
def start_backend_cluster(args, cluster_id: int):
|
||||
"""Start one backend cluster (Ray-Serve application)."""
|
||||
backend_script = Path(__file__).parent / "ray_serve_backend_scalable.py"
|
||||
|
||||
# All Ray Serve apps share the same HTTP server (default 8000).
|
||||
# We still forward the port flag for completeness, but keep it
|
||||
# identical for every cluster.
|
||||
base_port = args.backend_base_port
|
||||
|
||||
cmd = [
|
||||
sys.executable, str(backend_script),
|
||||
"--t2v_model_path", args.t2v_model_path,
|
||||
"--i2v_model_path", args.i2v_model_path,
|
||||
"--output_path", args.output_path,
|
||||
"--host", args.backend_host,
|
||||
"--port", str(base_port),
|
||||
"--num_gpus", str(args.num_gpus_per_cluster),
|
||||
"--cluster_id", str(cluster_id),
|
||||
]
|
||||
|
||||
print(f"🚀 Starting Backend Cluster {cluster_id + 1} (HTTP port {base_port})...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start the backend process
|
||||
backend_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor backend output
|
||||
def monitor_backend():
|
||||
for line in backend_process.stdout:
|
||||
print(f"[BACKEND-{cluster_id + 1}] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_backend, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return backend_process, base_port
|
||||
|
||||
|
||||
def start_frontend_instance(args, instance_id: int, backend_url: str):
|
||||
"""Start a single frontend instance"""
|
||||
frontend_script = Path(__file__).parent / "gradio_frontend.py"
|
||||
frontend_port = args.frontend_base_port + instance_id
|
||||
|
||||
# Update backend URL to include cluster-specific path
|
||||
cluster_id = instance_id % args.num_backend_clusters
|
||||
backend_url_with_cluster = f"{backend_url}/cluster_{cluster_id}"
|
||||
|
||||
cmd = [
|
||||
sys.executable, str(frontend_script),
|
||||
"--backend_url", backend_url_with_cluster,
|
||||
"--t2v_model_path", args.t2v_model_path,
|
||||
"--i2v_model_path", args.i2v_model_path,
|
||||
"--host", args.frontend_host,
|
||||
"--port", str(frontend_port)
|
||||
]
|
||||
|
||||
print(f"🎨 Starting Frontend {instance_id + 1} on port {frontend_port}...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start the frontend process
|
||||
frontend_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor frontend output
|
||||
def monitor_frontend():
|
||||
for line in frontend_process.stdout:
|
||||
print(f"[FRONTEND-{instance_id + 1}] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_frontend, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return frontend_process, frontend_port
|
||||
|
||||
|
||||
def start_nginx(args):
|
||||
"""Start nginx reverse proxy"""
|
||||
nginx_conf = Path(__file__).parent / "nginx.conf"
|
||||
|
||||
# Update nginx configuration with actual ports
|
||||
update_nginx_config(args)
|
||||
|
||||
cmd = [
|
||||
"nginx",
|
||||
"-c", str(nginx_conf),
|
||||
"-g", "daemon off;"
|
||||
]
|
||||
|
||||
print(f"🌐 Starting Nginx reverse proxy...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start nginx process
|
||||
nginx_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor nginx output
|
||||
def monitor_nginx():
|
||||
for line in nginx_process.stdout:
|
||||
print(f"[NGINX] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_nginx, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return nginx_process
|
||||
|
||||
|
||||
def update_nginx_config(args):
|
||||
"""Rewrite nginx.conf with the correct ports – NO “/cluster_X” in upstreams."""
|
||||
nginx_conf = Path(__file__).parent / "nginx.conf"
|
||||
nginx_conf_backup = Path(__file__).parent / "nginx.conf.backup"
|
||||
|
||||
if not nginx_conf_backup.exists():
|
||||
nginx_conf_backup.write_text(nginx_conf.read_text())
|
||||
|
||||
config_content = nginx_conf_backup.read_text()
|
||||
|
||||
# ── 1. front-end pool ───────────────────────────────────────────────
|
||||
frontend_servers = "\n ".join(
|
||||
f"server 127.0.0.1:{args.frontend_base_port + i} "
|
||||
f"weight=1 max_fails=3 fail_timeout=30s;"
|
||||
for i in range(args.num_frontends)
|
||||
)
|
||||
config_content = config_content.replace(
|
||||
"# Upstream for frontend load balancing",
|
||||
f"# Upstream for frontend load balancing\n upstream frontend_servers {{\n"
|
||||
f" # Round-robin load balancing between frontends\n {frontend_servers}"
|
||||
)
|
||||
|
||||
# Shared Ray-Serve HTTP port
|
||||
backend_port = args.backend_base_port # default 8000
|
||||
backend_line = (f"server 127.0.0.1:{backend_port} "
|
||||
f"weight=1 max_fails=3 fail_timeout=30s;")
|
||||
|
||||
# ── 2. backend-1 pool ───────────────────────────────────────────────
|
||||
config_content = config_content.replace(
|
||||
"# Upstream for backend1 load balancing",
|
||||
f"# Upstream for backend1 load balancing\n upstream backend1_servers {{\n"
|
||||
f" {backend_line}"
|
||||
)
|
||||
|
||||
# ── 3. backend-2 pool ───────────────────────────────────────────────
|
||||
config_content = config_content.replace(
|
||||
"# Upstream for backend2 load balancing",
|
||||
f"# Upstream for backend2 load balancing\n upstream backend2_servers {{\n"
|
||||
f" {backend_line}"
|
||||
)
|
||||
|
||||
# ── 4. strip any stray “/cluster_X” fragments ───────────────────────
|
||||
config_content = config_content.replace("/cluster_0", "").replace("/cluster_1", "")
|
||||
|
||||
# ── 5. use user-writable log directory ---------------------------------
|
||||
log_dir = Path(args.output_path).resolve()
|
||||
config_content = config_content.replace(
|
||||
"access_log /var/log/nginx/access.log;",
|
||||
f"access_log {log_dir}/nginx_access.log;")
|
||||
config_content = config_content.replace(
|
||||
"error_log /var/log/nginx/error.log;",
|
||||
f"error_log {log_dir}/nginx_error.log;")
|
||||
|
||||
nginx_conf.write_text(config_content)
|
||||
print("✅ nginx.conf updated (no path suffixes & custom log paths)")
|
||||
|
||||
|
||||
def start_ngrok(args):
|
||||
"""Start ngrok tunnel"""
|
||||
if not args.use_ngrok:
|
||||
return None
|
||||
|
||||
cmd = [
|
||||
"ngrok",
|
||||
"http",
|
||||
str(args.nginx_port),
|
||||
"--log=stdout"
|
||||
]
|
||||
|
||||
print(f"🌍 Starting ngrok tunnel to port {args.nginx_port}...")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
# Start ngrok process
|
||||
ngrok_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor ngrok output
|
||||
def monitor_ngrok():
|
||||
for line in ngrok_process.stdout:
|
||||
print(f"[NGROK] {line.rstrip()}")
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_ngrok, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return ngrok_process
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="FastVideo Scalable Architecture Launcher")
|
||||
|
||||
# Model and output settings
|
||||
parser.add_argument("--t2v_model_path",
|
||||
type=str,
|
||||
default="FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
help="Path to the T2V model")
|
||||
parser.add_argument("--i2v_model_path",
|
||||
type=str,
|
||||
default="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
|
||||
help="Path to the I2V model")
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs",
|
||||
help="Path to save generated videos")
|
||||
|
||||
# Backend settings
|
||||
parser.add_argument("--backend_host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Backend host to bind to")
|
||||
parser.add_argument("--backend_base_port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Base port for backend clusters")
|
||||
parser.add_argument("--num_backend_clusters",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Number of backend clusters")
|
||||
parser.add_argument("--num_gpus_per_cluster",
|
||||
type=int,
|
||||
default=3, # Changed from 8 to 3 (3+3=6 GPUs total, leaving 1 GPU buffer)
|
||||
help="Number of GPUs per backend cluster")
|
||||
|
||||
# Frontend settings
|
||||
parser.add_argument("--frontend_host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Frontend host to bind to")
|
||||
parser.add_argument("--frontend_base_port",
|
||||
type=int,
|
||||
default=7860,
|
||||
help="Base port for frontend instances")
|
||||
parser.add_argument("--num_frontends",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Number of frontend instances")
|
||||
|
||||
# Nginx settings
|
||||
parser.add_argument("--nginx_port",
|
||||
type=int,
|
||||
default=80,
|
||||
help="Port for nginx reverse proxy")
|
||||
|
||||
# Ngrok settings
|
||||
parser.add_argument("--use_ngrok",
|
||||
action="store_true",
|
||||
help="Start ngrok tunnel")
|
||||
|
||||
# Other settings
|
||||
parser.add_argument("--skip_health_check",
|
||||
action="store_true",
|
||||
help="Skip health checks")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(args.output_path, exist_ok=True)
|
||||
|
||||
print(" FastVideo Scalable Architecture")
|
||||
print("=" * 60)
|
||||
print(f"Architecture: ngrok -> nginx -> frontend1/frontend2 -> backend1×{args.num_gpus_per_cluster}/backend2×{args.num_gpus_per_cluster}")
|
||||
print(f"T2V Model: {args.t2v_model_path}")
|
||||
print(f"I2V Model: {args.i2v_model_path}")
|
||||
print(f"Output: {args.output_path}")
|
||||
print(f"Backend Clusters: {args.num_backend_clusters}")
|
||||
print(f"GPUs per Cluster: {args.num_gpus_per_cluster}")
|
||||
print(f"Total GPUs needed: {args.num_backend_clusters * args.num_gpus_per_cluster}")
|
||||
print(f"Frontend Instances: {args.num_frontends}")
|
||||
print(f"Nginx Port: {args.nginx_port}")
|
||||
print(f"Use Ngrok: {args.use_ngrok}")
|
||||
print("=" * 60)
|
||||
|
||||
# Start backend clusters
|
||||
backend_processes = []
|
||||
backend_urls = []
|
||||
|
||||
for i in range(args.num_backend_clusters):
|
||||
process, _ = start_backend_cluster(args, i)
|
||||
backend_processes.append(process)
|
||||
backend_urls.append(f"http://{args.backend_host}:{args.backend_base_port}")
|
||||
|
||||
# Wait for backends to be ready
|
||||
if not args.skip_health_check:
|
||||
print("\n⏳ Waiting for backend clusters to start...")
|
||||
for i, url in enumerate(backend_urls):
|
||||
if not check_service_health(f"{url}/cluster_{i}/health"):
|
||||
print(f"❌ Backend cluster {i + 1} failed to start. Terminating...")
|
||||
for process in backend_processes:
|
||||
process.terminate()
|
||||
sys.exit(1)
|
||||
|
||||
# Start frontend instances
|
||||
frontend_processes = []
|
||||
frontend_urls = []
|
||||
|
||||
for i in range(args.num_frontends):
|
||||
# Each frontend connects to a different backend cluster
|
||||
backend_url = backend_urls[i % len(backend_urls)]
|
||||
process, port = start_frontend_instance(args, i, backend_url)
|
||||
frontend_processes.append(process)
|
||||
frontend_urls.append(f"http://{args.frontend_host}:{port}")
|
||||
|
||||
# Wait for frontends to be ready
|
||||
if not args.skip_health_check:
|
||||
print("\n⏳ Waiting for frontend instances to start...")
|
||||
for i, url in enumerate(frontend_urls):
|
||||
if not check_service_health(url):
|
||||
print(f"❌ Frontend {i + 1} failed to start. Terminating...")
|
||||
for process in backend_processes + frontend_processes:
|
||||
process.terminate()
|
||||
sys.exit(1)
|
||||
|
||||
# Start nginx reverse proxy
|
||||
nginx_process = start_nginx(args)
|
||||
|
||||
# Wait for nginx to be ready
|
||||
if not args.skip_health_check:
|
||||
print("\n⏳ Waiting for nginx to start...")
|
||||
if not check_service_health(f"http://localhost:{args.nginx_port}/health"):
|
||||
print("❌ Nginx failed to start. Terminating...")
|
||||
for process in backend_processes + frontend_processes + [nginx_process]:
|
||||
process.terminate()
|
||||
sys.exit(1)
|
||||
|
||||
# Start ngrok tunnel (optional)
|
||||
ngrok_process = start_ngrok(args)
|
||||
|
||||
print("\n🎉 All services are starting up!")
|
||||
print(f"🌐 Nginx reverse proxy: http://localhost:{args.nginx_port}")
|
||||
for i, url in enumerate(frontend_urls):
|
||||
print(f"📺 Frontend {i + 1}: {url}")
|
||||
for i, url in enumerate(backend_urls):
|
||||
print(f" Backend Cluster {i + 1}: {url}")
|
||||
if args.use_ngrok:
|
||||
print("🌍 Ngrok tunnel is starting...")
|
||||
print("\nPress Ctrl+C to stop all services...")
|
||||
|
||||
# Signal handler for graceful shutdown
|
||||
def signal_handler(signum, frame):
|
||||
print("\n🛑 Shutting down all services...")
|
||||
all_processes = backend_processes + frontend_processes + [nginx_process]
|
||||
if ngrok_process:
|
||||
all_processes.append(ngrok_process)
|
||||
|
||||
for process in all_processes:
|
||||
if process:
|
||||
process.terminate()
|
||||
|
||||
# Wait for processes to terminate
|
||||
try:
|
||||
for process in all_processes:
|
||||
if process:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("⚠️ Force killing processes...")
|
||||
for process in all_processes:
|
||||
if process:
|
||||
process.kill()
|
||||
|
||||
print("✅ All services stopped")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Monitor processes
|
||||
try:
|
||||
while True:
|
||||
# Check if processes are still running
|
||||
for i, process in enumerate(backend_processes):
|
||||
if process.poll() is not None:
|
||||
print(f"❌ Backend cluster {i + 1} process died unexpectedly")
|
||||
break
|
||||
|
||||
for i, process in enumerate(frontend_processes):
|
||||
if process.poll() is not None:
|
||||
print(f"❌ Frontend {i + 1} process died unexpectedly")
|
||||
break
|
||||
|
||||
if nginx_process and nginx_process.poll() is not None:
|
||||
print("❌ Nginx process died unexpectedly")
|
||||
break
|
||||
|
||||
if ngrok_process and ngrok_process.poll() is not None:
|
||||
print("❌ Ngrok process died unexpectedly")
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
signal_handler(signal.SIGINT, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for T2V and I2V functionality in FastVideo Gradio app.
|
||||
This script tests both the backend and frontend modifications.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
def test_backend_t2v():
|
||||
"""Test the backend T2V functionality directly"""
|
||||
backend_url = "http://localhost:8000"
|
||||
|
||||
try:
|
||||
# Test T2V request data
|
||||
request_data = {
|
||||
"prompt": "A beautiful sunset over the ocean with gentle waves",
|
||||
"negative_prompt": "",
|
||||
"use_negative_prompt": False,
|
||||
"seed": 42,
|
||||
"guidance_scale": 7.5,
|
||||
"num_frames": 21,
|
||||
"height": 448,
|
||||
"width": 832,
|
||||
"num_inference_steps": 20,
|
||||
"randomize_seed": False,
|
||||
"return_frames": True,
|
||||
"image_path": None,
|
||||
"model_type": "t2v"
|
||||
}
|
||||
|
||||
# Send request to backend
|
||||
response = requests.post(
|
||||
f"{backend_url}/generate_video",
|
||||
json=request_data,
|
||||
timeout=300 # 5 minutes timeout
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ Backend T2V test successful!")
|
||||
print(f"Success: {result.get('success')}")
|
||||
print(f"Seed used: {result.get('seed')}")
|
||||
if result.get('frames'):
|
||||
print(f"Frames returned: {len(result.get('frames'))}")
|
||||
else:
|
||||
print("No frames returned")
|
||||
else:
|
||||
print(f"❌ Backend T2V test failed with status {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Backend T2V test failed with exception: {e}")
|
||||
|
||||
def test_backend_i2v():
|
||||
"""Test the backend I2V functionality directly"""
|
||||
backend_url = "http://localhost:8000"
|
||||
|
||||
# Create a simple test image
|
||||
test_image = Image.new('RGB', (256, 256), color='red')
|
||||
temp_image_path = "test_image.png"
|
||||
test_image.save(temp_image_path)
|
||||
|
||||
try:
|
||||
# Test I2V request data
|
||||
request_data = {
|
||||
"prompt": "The red square gently animates with subtle movement",
|
||||
"negative_prompt": "",
|
||||
"use_negative_prompt": False,
|
||||
"seed": 42,
|
||||
"guidance_scale": 7.5,
|
||||
"num_frames": 21,
|
||||
"height": 448,
|
||||
"width": 832,
|
||||
"num_inference_steps": 20,
|
||||
"randomize_seed": False,
|
||||
"return_frames": True,
|
||||
"image_path": temp_image_path,
|
||||
"model_type": "i2v"
|
||||
}
|
||||
|
||||
# Send request to backend
|
||||
response = requests.post(
|
||||
f"{backend_url}/generate_video",
|
||||
json=request_data,
|
||||
timeout=300 # 5 minutes timeout
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ Backend I2V test successful!")
|
||||
print(f"Success: {result.get('success')}")
|
||||
print(f"Seed used: {result.get('seed')}")
|
||||
if result.get('frames'):
|
||||
print(f"Frames returned: {len(result.get('frames'))}")
|
||||
else:
|
||||
print("No frames returned")
|
||||
else:
|
||||
print(f"❌ Backend I2V test failed with status {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Backend I2V test failed with exception: {e}")
|
||||
|
||||
finally:
|
||||
# Clean up test image
|
||||
if os.path.exists(temp_image_path):
|
||||
os.remove(temp_image_path)
|
||||
|
||||
def test_backend_health():
|
||||
"""Test if the backend is running"""
|
||||
backend_url = "http://localhost:8000"
|
||||
|
||||
try:
|
||||
response = requests.get(f"{backend_url}/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
print("✅ Backend is healthy")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Backend health check failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Backend health check failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🧪 Testing FastVideo T2V and I2V functionality...")
|
||||
print("=" * 50)
|
||||
|
||||
# Test backend health first
|
||||
if test_backend_health():
|
||||
# Test T2V functionality
|
||||
print("\n📝 Testing T2V functionality...")
|
||||
test_backend_t2v()
|
||||
|
||||
# Test I2V functionality
|
||||
print("\n🖼️ Testing I2V functionality...")
|
||||
test_backend_i2v()
|
||||
else:
|
||||
print("⚠️ Backend is not running. Please start the backend first.")
|
||||
print("You can start it with: python start_ray_serve_app.py")
|
||||
|
||||
print("=" * 50)
|
||||
print("Test completed!")
|
||||
@@ -7,7 +7,7 @@ export TOKENIZERS_PARALLELISM=false
|
||||
|
||||
MODEL_PATH="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
|
||||
DATA_DIR="data/crush-smol_processed_i2v/combined_parquet_dataset/"
|
||||
VALIDATION_DATASET_FILE="$(dirname "$0")/validation.json"
|
||||
VALIDATION_DATASET_FILE="examples/training/finetune/wan_i2v_14b_480p/crush_smol/validation.json"
|
||||
NUM_GPUS=8
|
||||
# export CUDA_VISIBLE_DEVICES=4,5
|
||||
# IP=[MASTER NODE IP]
|
||||
|
||||
@@ -7,7 +7,7 @@ export TOKENIZERS_PARALLELISM=false
|
||||
|
||||
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
|
||||
DATA_DIR="data/crush-smol_processed_t2v/combined_parquet_dataset/"
|
||||
VALIDATION_DATASET_FILE="$(dirname "$0")/validation.json"
|
||||
VALIDATION_DATASET_FILE="examples/training/finetune/wan_t2v_1_3b/crush_smol/validation.json"
|
||||
NUM_GPUS=4
|
||||
# export CUDA_VISIBLE_DEVICES=4,5
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export WANDB_MODE=online
|
||||
|
||||
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
|
||||
DATA_DIR="data/crush-smol_processed_t2v/combined_parquet_dataset/"
|
||||
VALIDATION_DATASET_FILE="$(dirname "$0")/validation.json"
|
||||
VALIDATION_DATASET_FILE="examples/training/finetune/wan_t2v_1_3b/crush_smol/validation.json"
|
||||
NUM_GPUS=2
|
||||
# export CUDA_VISIBLE_DEVICES=4,5
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.691925)" fill="white"/>
|
||||
<path d="M86.4467 27.9646L64.2778 97.1314H84.6732L91.7672 71.4156L124.445 71.4156L134.898 57.2275L96.201 57.2275L100.635 43.9262H146.844L159.278 27.9646H86.4467Z" fill="#0C0D6F"/>
|
||||
<path d="M158.423 107.852H131.821L91.6798 160.17L84.5858 107.852H64.4273L73.2948 174.359H95.4637L158.423 107.852Z" fill="#0C0D6F"/>
|
||||
<path d="M86.4467 27.9646L64.2778 97.1314H84.6732L91.7672 71.4156L124.445 71.4156L134.898 57.2275L96.201 57.2275L100.635 43.9262H146.844L159.278 27.9646H86.4467Z" stroke="#0C0D6F" stroke-width="1.77351"/>
|
||||
<path d="M158.423 107.852H131.821L91.6798 160.17L84.5858 107.852H64.4273L73.2948 174.359H95.4637L158.423 107.852Z" stroke="#0C0D6F" stroke-width="1.77351"/>
|
||||
<path d="M158.297 107.923H131.694L166.713 34.6841L97.1109 126.545H118.393L95.3374 174.429L158.297 107.923Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.77351" stroke-miterlimit="16"/>
|
||||
<path d="M53.6055 107.773L62.473 174.279L67.7935 174.279L58.926 107.772L53.6055 107.773Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.77351"/>
|
||||
<path d="M42.9648 107.773L51.8324 174.279L53.6059 174.279L44.7384 107.772L42.9648 107.773Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.77351"/>
|
||||
<path d="M32.3232 107.772L41.1908 174.279L42.0775 174.279L33.21 107.772L32.3232 107.772Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="0.886754"/>
|
||||
<path d="M53.6055 97.1314L75.7743 27.9646H81.0949L58.926 97.1314H53.6055Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.77351"/>
|
||||
<path d="M42.9648 97.1314L65.1337 27.9646H66.9072L44.7384 97.1314H42.9648Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.77351"/>
|
||||
<path d="M32.3232 97.1315L54.4921 27.9646H55.3789L33.21 97.1315H32.3232Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="0.886754"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.308105)" fill="white"/>
|
||||
<path d="M49.8511 145.66L78.6319 55.8637H85.5394L56.7585 145.66H49.8511Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="2.30244"/>
|
||||
<path d="M36.0376 145.66L64.8185 55.8637H67.1209L38.3401 145.66H36.0376Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="2.30244"/>
|
||||
<path d="M22.2222 145.66L51.003 55.8637H52.1543L23.3734 145.66H22.2222Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.15122"/>
|
||||
<path d="M92.4465 55.8648L63.666 145.66H90.144L99.3538 112.275H144.251L150.007 93.855H105.11L110.866 76.5868H173.032L178.788 55.8648H92.4465Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="2.30244"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 779 B |
@@ -1,8 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.308105)" fill="white"/>
|
||||
<path d="M49.8511 145.66L78.6319 55.8637H85.5394L56.7585 145.66H49.8511Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="2.30244"/>
|
||||
<path d="M36.0376 145.66L64.8185 55.8637H67.1209L38.3401 145.66H36.0376Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="2.30244"/>
|
||||
<path d="M22.2222 145.66L51.003 55.8637H52.1543L23.3734 145.66H22.2222Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.15122"/>
|
||||
<path d="M92.4465 55.8648L63.666 145.66H90.144L99.3538 112.275H144.251L150.007 93.855H105.11L110.866 76.5868H173.032L178.788 55.8648H92.4465Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="2.30244"/>
|
||||
<path d="M131.799 93.8999H104.705L134.343 30.1061L69.483 112.866H91.1583L67.6768 161.635L131.799 93.8999Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.80627" stroke-miterlimit="16"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 966 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.691925)" fill="white"/>
|
||||
<path d="M89.2368 40.0858L70.8901 97.3273H87.769L93.64 76.0452L120.684 76.0453L129.334 64.3034L97.3093 64.3034L100.979 53.2954H139.22L149.511 40.0858H89.2368Z" fill="#0C0D6F"/>
|
||||
<path d="M148.804 106.199H126.788L93.5676 149.498L87.6967 106.199H71.0138L78.3525 161.239H96.6991L148.804 106.199Z" fill="#0C0D6F"/>
|
||||
<path d="M89.2368 40.0858L70.8901 97.3273H87.769L93.64 76.0452L120.684 76.0453L129.334 64.3034L97.3093 64.3034L100.979 53.2954H139.22L149.511 40.0858H89.2368Z" stroke="#0C0D6F" stroke-width="1.46773"/>
|
||||
<path d="M148.804 106.199H126.788L93.5676 149.498L87.6967 106.199H71.0138L78.3525 161.239H96.6991L148.804 106.199Z" stroke="#0C0D6F" stroke-width="1.46773"/>
|
||||
<path d="M148.699 106.258H126.683L155.664 45.6468L98.062 121.669H115.675L96.5942 161.298L148.699 106.258Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.46773" stroke-miterlimit="16"/>
|
||||
<path d="M62.0576 106.134L69.3963 161.174L73.7995 161.174L66.4608 106.134L62.0576 106.134Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.46773"/>
|
||||
<path d="M53.2515 106.134L60.5901 161.174L62.0579 161.174L54.7192 106.134L53.2515 106.134Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.46773"/>
|
||||
<path d="M44.4443 106.134L51.783 161.174L52.5169 161.174L45.1782 106.134L44.4443 106.134Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="0.733866"/>
|
||||
<path d="M62.0576 97.3273L80.4043 40.0858H84.8075L66.4608 97.3273H62.0576Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.46773"/>
|
||||
<path d="M53.2515 97.3273L71.5981 40.0858H73.0658L54.7192 97.3273H53.2515Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="1.46773"/>
|
||||
<path d="M44.4443 97.3274L62.791 40.0859H63.5248L45.1782 97.3274H44.4443Z" fill="#0C0D6F" stroke="#0C0D6F" stroke-width="0.733866"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,18 +0,0 @@
|
||||
<svg width="252" height="105" viewBox="0 0 252 105" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M89.4843 55.5457H101.361L87.7028 101H74.638L89.4843 55.5457Z" fill="#356CFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M96.0167 1.00057H112.645L118.583 48.273H104.924L103.737 39.7882H79.9827L67.5117 55.5457H85.3273L43.1638 101H28.3174L22.3789 55.5457H33.6621L38.4129 91.3031L58.604 68.2729H44.3515L96.0167 1.00057ZM100.768 13.1217L87.7028 29.4852H103.143L100.768 13.1217Z" fill="#356CFF"/>
|
||||
<path d="M37.2252 1.00057L22.3789 48.273H36.0375L40.7884 30.6974L62.6727 30.6974L69.6727 21.0005L43.7576 21.0004L46.7269 11.9096L77.6727 11.9096L86 1.00057L37.2252 1.00057Z" fill="#356CFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.488 55.5457L94.2351 101C94.2351 101 105.518 101 120.959 101C136.399 101 144.078 93.0133 148.276 79.788C152.432 68.0157 153.027 55.5457 136.399 55.5457C119.771 55.5457 108.488 55.5457 108.488 55.5457ZM109.081 90.697L116.802 65.8487C116.802 65.8487 120.959 65.8487 132.242 65.8487C143.525 65.8487 137.586 78.5759 135.211 84.0304C133.307 88.4021 127.491 90.697 122.74 90.697C117.989 90.697 109.081 90.697 109.081 90.697Z" fill="#356CFF"/>
|
||||
<path d="M173.188 1.00056L168.625 11.9096C168.625 11.9096 149.386 11.9092 142.525 11.9095C135.664 11.9098 136.586 20.3944 141.337 20.3944H159.747C168.654 20.3944 166.961 33.6899 163.904 38.5761C160.467 44.0675 157.371 48.273 148.463 48.273L125.188 48.273L124 37.97L147.87 37.97C153.808 37.97 156.184 29.4852 151.433 29.4852H131.836C120.142 29.4852 125.897 1.00043 141.337 1.00043L173.188 1.00056Z" fill="#356CFF"/>
|
||||
<path d="M179.938 1.00056L175.688 11.9096L191.221 11.9096L179.938 48.273H192.409L203.692 11.9096L219.132 11.9095L223.289 1.00043L179.938 1.00056Z" fill="#356CFF"/>
|
||||
<path d="M161.341 55.5457H202.845L198.5 65.8487H169.654L167.279 73.7268H188.5L184.749 82.8177H164.31L161.934 90.697H190.251L186.624 101H146.494L161.341 55.5457Z" fill="#356CFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M230.821 54.9391C255.169 54.9391 251.776 67.0602 249.231 77.9692C246.686 88.8783 240.917 101 217.757 101C194.596 101 195.606 88.8783 199.347 77.9692C203.089 67.0602 206.473 54.9391 230.821 54.9391ZM237.948 77.9692C239.984 70.6965 240.917 65.242 228.446 65.242C215.975 65.242 211.818 71.9087 210.037 77.9692C208.255 84.0298 208.255 91.3025 219.538 91.3025C230.821 91.3025 235.911 85.2419 237.948 77.9692Z" fill="#356CFF"/>
|
||||
<path d="M173.188 1.00056L168.625 11.9096C168.625 11.9096 149.386 11.9092 142.525 11.9095C135.664 11.9098 136.586 20.3944 141.337 20.3944M173.188 1.00056C173.188 1.00056 156.777 1.00043 141.337 1.00043M173.188 1.00056L141.337 1.00043M141.337 20.3944C146.088 20.3944 150.839 20.3944 159.747 20.3944M141.337 20.3944H159.747M159.747 20.3944C168.654 20.3944 166.961 33.6899 163.904 38.5761C160.467 44.0675 157.371 48.273 148.463 48.273M148.463 48.273C139.556 48.273 125.188 48.273 125.188 48.273M148.463 48.273L125.188 48.273M125.188 48.273L124 37.97M124 37.97C124 37.97 141.931 37.97 147.87 37.97M124 37.97L147.87 37.97M147.87 37.97C153.808 37.97 156.184 29.4852 151.433 29.4852M151.433 29.4852C146.682 29.4852 138.962 29.4852 131.836 29.4852M151.433 29.4852H131.836M131.836 29.4852C120.142 29.4852 125.897 1.00043 141.337 1.00043M37.2252 1.00057L22.3789 48.273H36.0375L40.7884 30.6974L62.6727 30.6974L69.6727 21.0005L43.7576 21.0004L46.7269 11.9096L77.6727 11.9096L86 1.00057L37.2252 1.00057ZM96.0167 1.00057H112.645L118.583 48.273H104.924L103.737 39.7882H79.9827L67.5117 55.5457H85.3273L43.1638 101H28.3174L22.3789 55.5457H33.6621L38.4129 91.3031L58.604 68.2729H44.3515L96.0167 1.00057ZM87.7028 29.4852L100.768 13.1217L103.143 29.4852H87.7028ZM89.4843 55.5457H101.361L87.7028 101H74.638L89.4843 55.5457ZM108.488 55.5457L94.2351 101C94.2351 101 105.518 101 120.959 101C136.399 101 144.078 93.0133 148.276 79.788C152.432 68.0157 153.027 55.5457 136.399 55.5457C119.771 55.5457 108.488 55.5457 108.488 55.5457ZM116.802 65.8487L109.081 90.697C109.081 90.697 117.989 90.697 122.74 90.697C127.491 90.697 133.307 88.4021 135.211 84.0304C137.586 78.5759 143.525 65.8487 132.242 65.8487C120.959 65.8487 116.802 65.8487 116.802 65.8487ZM179.938 1.00056L175.688 11.9096L191.221 11.9096L179.938 48.273H192.409L203.692 11.9096L219.132 11.9095L223.289 1.00043L179.938 1.00056ZM161.341 55.5457H202.845L198.5 65.8487H169.654L167.279 73.7268H188.5L184.749 82.8177H164.31L161.934 90.697H190.251L186.624 101H146.494L161.341 55.5457ZM230.821 54.9391C255.169 54.9391 251.776 67.0602 249.231 77.9692C246.686 88.8783 240.917 101 217.757 101C194.596 101 195.606 88.8783 199.347 77.9692C203.089 67.0602 206.473 54.9391 230.821 54.9391ZM228.446 65.242C240.917 65.242 239.984 70.6965 237.948 77.9692C235.911 85.2419 230.821 91.3025 219.538 91.3025C208.255 91.3025 208.255 84.0298 210.037 77.9692C211.818 71.9087 215.975 65.242 228.446 65.242Z" stroke="#356CFF" stroke-width="1.18771"/>
|
||||
<path d="M15.2524 55.5451L21.191 100.999L24.7541 100.999L18.8156 55.5451L15.2524 55.5451Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.18771"/>
|
||||
<path d="M8.12646 55.5451L14.065 100.999L15.2527 100.999L9.31417 55.5451L8.12646 55.5451Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.18771"/>
|
||||
<path d="M1 55.5451L6.93853 100.999L7.53239 100.999L1.59385 55.5451L1 55.5451Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.593853"/>
|
||||
<path d="M15.2524 48.2724L30.0988 1H33.6619L18.8156 48.2724H15.2524Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.18771"/>
|
||||
<path d="M8.12646 48.2724L22.9728 1H24.1605L9.31417 48.2724H8.12646Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.18771"/>
|
||||
<path d="M1 48.2724L15.8463 1H16.4402L1.59385 48.2724H1Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.593853"/>
|
||||
<path d="M85.3271 55.5457H67.5116L87 12.7363L44.3513 68.2729H58.6038L43.1636 101L85.3271 55.5457Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.18771" stroke-miterlimit="16"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.691925)" fill="black"/>
|
||||
<path d="M86.4467 27.9646L64.2778 97.1314H84.6732L91.7672 71.4156L124.445 71.4156L134.898 57.2275L96.201 57.2275L100.635 43.9262H146.844L159.278 27.9646H86.4467Z" fill="#356CFF"/>
|
||||
<path d="M158.423 107.852H131.821L91.6798 160.17L84.5858 107.852H64.4273L73.2948 174.359H95.4637L158.423 107.852Z" fill="#356CFF"/>
|
||||
<path d="M86.4467 27.9646L64.2778 97.1314H84.6732L91.7672 71.4156L124.445 71.4156L134.898 57.2275L96.201 57.2275L100.635 43.9262H146.844L159.278 27.9646H86.4467Z" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M158.423 107.852H131.821L91.6798 160.17L84.5858 107.852H64.4273L73.2948 174.359H95.4637L158.423 107.852Z" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M158.297 107.923H131.694L166.713 34.6841L97.1109 126.545H118.393L95.3374 174.429L158.297 107.923Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.77351" stroke-miterlimit="16"/>
|
||||
<path d="M53.6055 107.773L62.473 174.279L67.7935 174.279L58.926 107.772L53.6055 107.773Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M42.9648 107.773L51.8324 174.279L53.6059 174.279L44.7384 107.772L42.9648 107.773Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M32.3232 107.772L41.1908 174.279L42.0775 174.279L33.21 107.772L32.3232 107.772Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.886754"/>
|
||||
<path d="M53.6055 97.1314L75.7743 27.9646H81.0949L58.926 97.1314H53.6055Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M42.9648 97.1314L65.1337 27.9646H66.9072L44.7384 97.1314H42.9648Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M32.3232 97.1315L54.4921 27.9646H55.3789L33.21 97.1315H32.3232Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.886754"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.308105)" fill="black"/>
|
||||
<path d="M49.8511 145.66L78.6319 55.8637H85.5394L56.7585 145.66H49.8511Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M36.0376 145.66L64.8185 55.8637H67.1209L38.3401 145.66H36.0376Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M22.2222 145.66L51.003 55.8637H52.1543L23.3734 145.66H22.2222Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.15122"/>
|
||||
<path d="M92.4465 55.8648L63.666 145.66H90.144L99.3538 112.275H144.251L150.007 93.855H105.11L110.866 76.5868H173.032L178.788 55.8648H92.4465Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 779 B |
@@ -1,8 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.308105)" fill="black"/>
|
||||
<path d="M49.8511 145.66L78.6319 55.8637H85.5394L56.7585 145.66H49.8511Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M36.0376 145.66L64.8185 55.8637H67.1209L38.3401 145.66H36.0376Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M22.2222 145.66L51.003 55.8637H52.1543L23.3734 145.66H22.2222Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.15122"/>
|
||||
<path d="M92.4465 55.8648L63.666 145.66H90.144L99.3538 112.275H144.251L150.007 93.855H105.11L110.866 76.5868H173.032L178.788 55.8648H92.4465Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M131.799 93.8999H104.705L134.343 30.1061L69.483 112.866H91.1583L67.6768 161.635L131.799 93.8999Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.80627" stroke-miterlimit="16"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 966 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="160" height="144" viewBox="0 0 160 144" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.8511 122.66L57.6319 32.8637H64.5394L35.7585 122.66H28.8511Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M15.0376 122.66L43.8185 32.8637H46.1209L17.3401 122.66H15.0376Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M1.22217 122.66L30.003 32.8637H31.1543L2.3734 122.66H1.22217Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.15122"/>
|
||||
<path d="M71.4465 32.8648L42.666 122.66H69.144L78.3538 89.2745H123.251L129.007 70.855H84.1099L89.866 53.5868H152.032L157.788 32.8648H71.4465Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M110.799 70.8999H83.7053L113.343 7.10608L48.483 89.8657H70.1583L46.6768 138.635L110.799 70.8999Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.80627" stroke-miterlimit="16"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 885 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="160" height="93" viewBox="0 0 160 93" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.8511 91.66L57.6319 1.86368H64.5394L35.7585 91.66H28.8511Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M15.0376 91.66L43.8185 1.86368H46.1209L17.3401 91.66H15.0376Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
<path d="M1.22217 91.66L30.003 1.86366H31.1543L2.3734 91.66H1.22217Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.15122"/>
|
||||
<path d="M71.4465 1.86483L42.666 91.6599H69.144L78.3538 58.2746H123.251L129.007 39.855H84.1099L89.866 22.5868H152.032L157.788 1.86483H71.4465Z" fill="#356CFF" stroke="#356CFF" stroke-width="2.30244"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 691 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="200" height="201" viewBox="0 0 200 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" transform="translate(0 0.691925)" fill="black"/>
|
||||
<path d="M89.2368 40.0858L70.8901 97.3273H87.769L93.64 76.0452L120.684 76.0453L129.334 64.3034L97.3093 64.3034L100.979 53.2954H139.22L149.511 40.0858H89.2368Z" fill="#356CFF"/>
|
||||
<path d="M148.804 106.199H126.788L93.5676 149.498L87.6967 106.199H71.0138L78.3525 161.239H96.6991L148.804 106.199Z" fill="#356CFF"/>
|
||||
<path d="M89.2368 40.0858L70.8901 97.3273H87.769L93.64 76.0452L120.684 76.0453L129.334 64.3034L97.3093 64.3034L100.979 53.2954H139.22L149.511 40.0858H89.2368Z" stroke="#356CFF" stroke-width="1.46773"/>
|
||||
<path d="M148.804 106.199H126.788L93.5676 149.498L87.6967 106.199H71.0138L78.3525 161.239H96.6991L148.804 106.199Z" stroke="#356CFF" stroke-width="1.46773"/>
|
||||
<path d="M148.699 106.258H126.683L155.664 45.6468L98.062 121.669H115.675L96.5942 161.298L148.699 106.258Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.46773" stroke-miterlimit="16"/>
|
||||
<path d="M62.0576 106.134L69.3963 161.174L73.7995 161.174L66.4608 106.134L62.0576 106.134Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.46773"/>
|
||||
<path d="M53.2515 106.134L60.5901 161.174L62.0579 161.174L54.7192 106.134L53.2515 106.134Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.46773"/>
|
||||
<path d="M44.4443 106.134L51.783 161.174L52.5169 161.174L45.1782 106.134L44.4443 106.134Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.733866"/>
|
||||
<path d="M62.0576 97.3273L80.4043 40.0858H84.8075L66.4608 97.3273H62.0576Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.46773"/>
|
||||
<path d="M53.2515 97.3273L71.5981 40.0858H73.0658L54.7192 97.3273H53.2515Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.46773"/>
|
||||
<path d="M44.4443 97.3274L62.791 40.0859H63.5248L45.1782 97.3274H44.4443Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.733866"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg width="141" height="153" viewBox="0 0 141 153" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M55.4467 0.964615L33.2778 70.1314H53.6732L60.7672 44.4156L93.4453 44.4156L103.898 30.2275L65.201 30.2275L69.6347 16.9262H115.844L128.278 0.964615H55.4467Z" fill="#356CFF"/>
|
||||
<path d="M127.423 80.852H100.821L60.6798 133.17L53.5858 80.852H33.4273L42.2948 147.359H64.4637L127.423 80.852Z" fill="#356CFF"/>
|
||||
<path d="M55.4467 0.964615L33.2778 70.1314H53.6732L60.7672 44.4156L93.4453 44.4156L103.898 30.2275L65.201 30.2275L69.6347 16.9262H115.844L128.278 0.964615H55.4467Z" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M127.423 80.852H100.821L60.6798 133.17L53.5858 80.852H33.4273L42.2948 147.359H64.4637L127.423 80.852Z" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M127.297 80.9227H100.694L135.713 7.68411L66.1109 99.5445H87.393L64.3374 147.429L127.297 80.9227Z" fill="#FDC717" stroke="#FDC717" stroke-width="1.77351" stroke-miterlimit="16"/>
|
||||
<path d="M22.6055 80.7725L31.473 147.279L36.7935 147.279L27.926 80.7725L22.6055 80.7725Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M11.9648 80.7725L20.8324 147.279L22.6059 147.279L13.7384 80.7725L11.9648 80.7725Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M1.32324 80.7725L10.1908 147.279L11.0775 147.279L2.21 80.7725L1.32324 80.7725Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.886754"/>
|
||||
<path d="M22.6055 70.1314L44.7743 0.964615H50.0949L27.926 70.1314H22.6055Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M11.9648 70.1314L34.1337 0.964615H35.9072L13.7384 70.1314H11.9648Z" fill="#356CFF" stroke="#356CFF" stroke-width="1.77351"/>
|
||||
<path d="M1.32324 70.1315L23.4921 0.964645H24.3789L2.21 70.1315H1.32324Z" fill="#356CFF" stroke="#356CFF" stroke-width="0.886754"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -55,7 +55,6 @@ class DistributedAttention(nn.Module):
|
||||
self.backend = backend_name_to_enum(attn_backend.get_name())
|
||||
self.dtype = dtype
|
||||
|
||||
@torch.compiler.disable
|
||||
def forward(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
@@ -137,7 +136,6 @@ class DistributedAttention_VSA(DistributedAttention):
|
||||
"""Distributed attention layer with VSA support.
|
||||
"""
|
||||
|
||||
@torch.compiler.disable
|
||||
def forward(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
|
||||
@@ -7,8 +7,8 @@ from collections.abc import Callable
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
from fastvideo.configs.pipelines.hunyuan import FastHunyuanConfig, HunyuanConfig
|
||||
from fastvideo.configs.pipelines.stepvideo import StepVideoT2VConfig
|
||||
from fastvideo.configs.pipelines.wan import (FastWan2_1_T2V_480P_Config,
|
||||
FastWan2_2_TI2V_5B_Config,
|
||||
from fastvideo.configs.pipelines.wan import (FastWanT2V480PConfig,
|
||||
Wan2_2_TI2V_5B_Config,
|
||||
WanI2V480PConfig, WanI2V720PConfig,
|
||||
WanT2V480PConfig, WanT2V720PConfig)
|
||||
from fastvideo.logger import init_logger
|
||||
@@ -26,14 +26,13 @@ PIPE_NAME_TO_CONFIG: dict[str, type[PipelineConfig]] = {
|
||||
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": WanI2V480PConfig,
|
||||
"Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": WanI2V720PConfig,
|
||||
"Wan-AI/Wan2.1-T2V-14B-Diffusers": WanT2V720PConfig,
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers": FastWan2_1_T2V_480P_Config,
|
||||
"FastVideo/FastWan2.1-T2V-14B-480P-Diffusers": FastWan2_1_T2V_480P_Config,
|
||||
"FastVideo/FastWan2.2-TI2V-5B-Diffusers": FastWan2_2_TI2V_5B_Config,
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers": FastWanT2V480PConfig,
|
||||
"FastVideo/FastWan2.1-T2V-14B-480P-Diffusers": FastWanT2V480PConfig,
|
||||
"FastVideo/stepvideo-t2v-diffusers": StepVideoT2VConfig,
|
||||
"FastVideo/Wan2.1-VSA-T2V-14B-720P-Diffusers": WanT2V720PConfig,
|
||||
"Wan-AI/Wan2.2-TI2V-5B-Diffusers": WanT2V720PConfig,
|
||||
"Wan-AI/Wan2.2-T2V-A14B-Diffusers": WanT2V480PConfig,
|
||||
"Wan-AI/Wan2.2-I2V-A14B-Diffusers": WanI2V480PConfig,
|
||||
"Wan-AI/Wan2.2-TI2V-5B-Diffusers": Wan2_2_TI2V_5B_Config,
|
||||
# "Wan-AI/Wan2.2-T2V-A14B-Diffusers": Wan2_2_T2V_A14B_Config,
|
||||
# "Wan-AI/Wan2.2-I2V-A14B-Diffusers": Wan2_2_I2V_A14B_Config,
|
||||
# Add other specific weight variants
|
||||
}
|
||||
|
||||
@@ -54,7 +53,7 @@ PIPELINE_FALLBACK_CONFIG: dict[str, type[PipelineConfig]] = {
|
||||
"wanpipeline":
|
||||
WanT2V480PConfig, # Base Wan config as fallback for any Wan variant
|
||||
"wanimagetovideo": WanI2V480PConfig,
|
||||
"wandmdpipeline": FastWan2_1_T2V_480P_Config,
|
||||
"wandmdpipeline": FastWanT2V480PConfig,
|
||||
"stepvideo": StepVideoT2VConfig
|
||||
# Other fallbacks by architecture
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class WanI2V720PConfig(WanI2V480PConfig):
|
||||
|
||||
|
||||
@dataclass
|
||||
class FastWan2_1_T2V_480P_Config(WanT2V480PConfig):
|
||||
class FastWanT2V480PConfig(WanT2V480PConfig):
|
||||
"""Base configuration for FastWan T2V 1.3B 480P pipeline architecture with DMD"""
|
||||
|
||||
# WanConfig-specific parameters with defaults
|
||||
@@ -115,6 +115,9 @@ class FastWan2_1_T2V_480P_Config(WanT2V480PConfig):
|
||||
|
||||
@dataclass
|
||||
class Wan2_2_TI2V_5B_Config(WanT2V480PConfig):
|
||||
"""Base configuration for FastWan T2V 1.3B 480P pipeline architecture with DMD"""
|
||||
|
||||
# Denoising stage
|
||||
flow_shift: int = 5
|
||||
ti2v_task: bool = True
|
||||
|
||||
@@ -123,13 +126,6 @@ class Wan2_2_TI2V_5B_Config(WanT2V480PConfig):
|
||||
self.vae_config.load_decoder = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class FastWan2_2_TI2V_5B_Config(Wan2_2_TI2V_5B_Config):
|
||||
flow_shift: int = 5
|
||||
dmd_denoising_steps: list[int] | None = field(
|
||||
default_factory=lambda: [1000, 757, 522])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wan2_2_T2V_A14B_Config(WanT2V480PConfig):
|
||||
pass
|
||||
|
||||
@@ -6,19 +6,12 @@ from typing import Any
|
||||
from fastvideo.configs.sample.hunyuan import (FastHunyuanSamplingParam,
|
||||
HunyuanSamplingParam)
|
||||
from fastvideo.configs.sample.stepvideo import StepVideoT2VSamplingParam
|
||||
|
||||
# isort: off
|
||||
from fastvideo.configs.sample.wan import (
|
||||
FastWanT2V480PConfig,
|
||||
Wan2_2_I2V_A14B_SamplingParam,
|
||||
Wan2_2_T2V_A14B_SamplingParam,
|
||||
Wan2_2_TI2V_5B_SamplingParam,
|
||||
WanI2V_14B_480P_SamplingParam,
|
||||
WanI2V_14B_720P_SamplingParam,
|
||||
WanT2V_1_3B_SamplingParam,
|
||||
WanT2V_14B_SamplingParam,
|
||||
)
|
||||
# isort: on
|
||||
from fastvideo.configs.sample.wan import (Wan2_1_Fun_1_3B_InP_SamplingParam,
|
||||
Wan2_2_TI2V_5B_SamplingParam,
|
||||
WanI2V_14B_480P_SamplingParam,
|
||||
WanI2V_14B_720P_SamplingParam,
|
||||
WanT2V_1_3B_SamplingParam,
|
||||
WanT2V_14B_SamplingParam)
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.utils import (maybe_download_model_index,
|
||||
verify_model_config_and_directory)
|
||||
@@ -32,12 +25,18 @@ SAMPLING_PARAM_REGISTRY: dict[str, Any] = {
|
||||
"Wan-AI/Wan2.1-T2V-14B-Diffusers": WanT2V_14B_SamplingParam,
|
||||
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": WanI2V_14B_480P_SamplingParam,
|
||||
"Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": WanI2V_14B_720P_SamplingParam,
|
||||
"weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers":
|
||||
Wan2_1_Fun_1_3B_InP_SamplingParam,
|
||||
"FastVideo/stepvideo-t2v-diffusers": StepVideoT2VSamplingParam,
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers": FastWanT2V480PConfig,
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers":
|
||||
Wan2_1_Fun_1_3B_InP_SamplingParam,
|
||||
"FastVideo/FastWan2.1-T2V-14B-Diffusers":
|
||||
Wan2_1_Fun_1_3B_InP_SamplingParam,
|
||||
"Wan-AI/Wan2.2-TI2V-5B-Diffusers": Wan2_2_TI2V_5B_SamplingParam,
|
||||
"FastVideo/FastWan2.2-TI2V-5B-Diffusers": Wan2_2_TI2V_5B_SamplingParam,
|
||||
"Wan-AI/Wan2.2-T2V-A14B-Diffusers": Wan2_2_T2V_A14B_SamplingParam,
|
||||
"Wan-AI/Wan2.2-I2V-A14B-Diffusers": Wan2_2_I2V_A14B_SamplingParam,
|
||||
# "Wan-AI/Wan2.2-T2V-A14B-Diffusers":
|
||||
# Wan2_2_T2V_A14B_SamplingParam,
|
||||
# "Wan-AI/Wan2.2-I2V-A14B-Diffusers":
|
||||
# Wan2_2_I2V_A14B_SamplingParam,
|
||||
# Add other specific weight variants
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,21 @@ class FastWanT2V480PConfig(WanT2V_1_3B_SamplingParam):
|
||||
fps: int = 16
|
||||
|
||||
|
||||
# =============================================
|
||||
# ============= Wan2.1 Fun Models =============
|
||||
# =============================================
|
||||
@dataclass
|
||||
class Wan2_1_Fun_1_3B_InP_SamplingParam(SamplingParam):
|
||||
"""Sampling parameters for Wan2.1 Fun 1.3B InP model."""
|
||||
height: int = 480
|
||||
width: int = 832
|
||||
num_frames: int = 81
|
||||
fps: int = 16
|
||||
negative_prompt: str | None = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
|
||||
guidance_scale: float = 6.0
|
||||
num_inference_steps: int = 50
|
||||
|
||||
|
||||
# =============================================
|
||||
# ============= Wan2.2 TI2V Models =============
|
||||
# =============================================
|
||||
@@ -129,15 +144,9 @@ class Wan2_2_TI2V_5B_SamplingParam(Wan2_2_Base_SamplingParam):
|
||||
|
||||
@dataclass
|
||||
class Wan2_2_T2V_A14B_SamplingParam(Wan2_2_Base_SamplingParam):
|
||||
guidance_scale: float = 4.0
|
||||
guidance_scale_2: float = 3.0
|
||||
num_inference_steps: int = 40
|
||||
fps: int = 16
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wan2_2_I2V_A14B_SamplingParam(Wan2_2_Base_SamplingParam):
|
||||
guidance_scale: float = 3.5
|
||||
guidance_scale_2: float = 3.5
|
||||
num_inference_steps: int = 40
|
||||
fps: int = 16
|
||||
pass
|
||||
@@ -307,9 +307,8 @@ class VideoGenerator:
|
||||
|
||||
# Run inference
|
||||
start_time = time.perf_counter()
|
||||
output_batch = self.executor.execute_forward(batch, fastvideo_args)
|
||||
samples = output_batch.output
|
||||
logging_info = output_batch.logging_info
|
||||
output_batch = self.executor.execute_forward(batch, fastvideo_args)
|
||||
samples = output_batch
|
||||
|
||||
gen_time = time.perf_counter() - start_time
|
||||
logger.info("Generated successfully in %.2f seconds", gen_time)
|
||||
@@ -342,8 +341,7 @@ class VideoGenerator:
|
||||
"frames": frames,
|
||||
"prompts": prompt,
|
||||
"size": (target_height, target_width, batch.num_frames),
|
||||
"generation_time": gen_time,
|
||||
"logging_info": logging_info,
|
||||
"generation_time": gen_time
|
||||
}
|
||||
|
||||
def set_lora_adapter(self,
|
||||
|
||||
@@ -308,9 +308,8 @@ class FastVideoArgs:
|
||||
parser.add_argument(
|
||||
"--enable-torch-compile",
|
||||
action=StoreBoolean,
|
||||
default=FastVideoArgs.enable_torch_compile,
|
||||
help="Use torch.compile to speed up DiT inference." +
|
||||
"However, will likely cause precision drifts. See (https://github.com/pytorch/pytorch/issues/145213)",
|
||||
help=
|
||||
"Use torch.compile for speeding up STA inference without teacache",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
@@ -379,6 +378,7 @@ class FastVideoArgs:
|
||||
default=FastVideoArgs.enable_stage_verification,
|
||||
help="Enable input/output verification for pipeline stages",
|
||||
)
|
||||
|
||||
# Add pipeline configuration arguments
|
||||
PipelineConfig.add_cli_args(parser)
|
||||
|
||||
@@ -497,6 +497,12 @@ class FastVideoArgs:
|
||||
if self.num_gpus < max(self.tp_size, self.sp_size):
|
||||
self.num_gpus = max(self.tp_size, self.sp_size)
|
||||
|
||||
if self.enable_torch_compile and self.num_gpus > 1:
|
||||
logger.warning(
|
||||
"Currently torch compile does not work with multi-gpu. Setting enable_torch_compile to False"
|
||||
)
|
||||
self.enable_torch_compile = False
|
||||
|
||||
if self.pipeline_config is None:
|
||||
raise ValueError("pipeline_config is not set in FastVideoArgs")
|
||||
|
||||
@@ -635,7 +641,6 @@ class TrainingArgs(FastVideoArgs):
|
||||
num_euler_timesteps: int = 0
|
||||
lr_num_cycles: int = 0
|
||||
lr_power: float = 0.0
|
||||
min_lr_ratio: float = 0.5 # minimum learning rate ratio for cosine_with_min_lr scheduler
|
||||
not_apply_cfg_solver: bool = False
|
||||
distill_cfg: float = 0.0
|
||||
scheduler_type: str = ""
|
||||
@@ -665,13 +670,8 @@ class TrainingArgs(FastVideoArgs):
|
||||
min_timestep_ratio: float = 0.2
|
||||
max_timestep_ratio: float = 0.98
|
||||
real_score_guidance_scale: float = 3.5
|
||||
fake_score_learning_rate: float = 0.0 # separate learning rate for fake_score_transformer, if 0.0, use learning_rate
|
||||
fake_score_lr_scheduler: str = "constant" # separate lr scheduler for fake_score_transformer, if not set, use lr_scheduler
|
||||
training_state_checkpointing_steps: int = 0 # for resuming training
|
||||
weight_only_checkpointing_steps: int = 0 # for inference
|
||||
log_visualization: bool = False
|
||||
# simulate student forward to match inference
|
||||
simulate_student_forward: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_cli_args(cls, args: argparse.Namespace) -> "TrainingArgs":
|
||||
@@ -933,11 +933,6 @@ class TrainingArgs(FastVideoArgs):
|
||||
parser.add_argument("--lr-power",
|
||||
type=float,
|
||||
help="Learning rate power")
|
||||
parser.add_argument(
|
||||
"--min-lr-ratio",
|
||||
type=float,
|
||||
default=TrainingArgs.min_lr_ratio,
|
||||
help="Minimum learning rate ratio for cosine_with_min_lr scheduler")
|
||||
parser.add_argument("--not-apply-cfg-solver",
|
||||
action=StoreBoolean,
|
||||
help="Whether to not apply CFG solver")
|
||||
@@ -1003,22 +998,6 @@ class TrainingArgs(FastVideoArgs):
|
||||
type=float,
|
||||
default=TrainingArgs.real_score_guidance_scale,
|
||||
help="Teacher guidance scale")
|
||||
parser.add_argument("--fake-score-learning-rate",
|
||||
type=float,
|
||||
default=TrainingArgs.fake_score_learning_rate,
|
||||
help="Learning rate for fake score transformer")
|
||||
parser.add_argument(
|
||||
"--fake-score-lr-scheduler",
|
||||
type=str,
|
||||
default=TrainingArgs.fake_score_lr_scheduler,
|
||||
help="Learning rate scheduler for fake score transformer")
|
||||
parser.add_argument("--log-visualization",
|
||||
action=StoreBoolean,
|
||||
help="Whether to log visualization")
|
||||
parser.add_argument(
|
||||
"--simulate-student-forward",
|
||||
action=StoreBoolean,
|
||||
help="Whether to simulate student forward to match inference")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -86,12 +86,14 @@ class TimestepEmbedder(nn.Module):
|
||||
dtype=dtype)
|
||||
self.freq_dtype = freq_dtype
|
||||
|
||||
def forward(self, t: torch.Tensor) -> torch.Tensor:
|
||||
def forward(self, t: torch.Tensor, timestep_seq_len: int | None = None) -> torch.Tensor:
|
||||
t_freq = timestep_embedding(t,
|
||||
self.frequency_embedding_size,
|
||||
self.max_period,
|
||||
dtype=self.freq_dtype).to(
|
||||
self.mlp.fc_in.weight.dtype)
|
||||
if timestep_seq_len is not None:
|
||||
t_freq = t_freq.unflatten(0, (1, timestep_seq_len))
|
||||
# t_freq = t_freq.to(self.mlp.fc_in.weight.dtype)
|
||||
t_emb = self.mlp(t_freq)
|
||||
return t_emb
|
||||
|
||||
@@ -81,9 +81,9 @@ class WanTimeTextImageEmbedding(nn.Module):
|
||||
timestep: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor,
|
||||
encoder_hidden_states_image: torch.Tensor | None = None,
|
||||
timestep_seq_len: int | None = None,
|
||||
):
|
||||
logger.info(f"WTF timestep shape: {timestep.shape}")
|
||||
temb = self.time_embedder(timestep)
|
||||
temb = self.time_embedder(timestep, timestep_seq_len)
|
||||
timestep_proj = self.time_modulation(temb)
|
||||
|
||||
encoder_hidden_states = self.text_embedder(encoder_hidden_states)
|
||||
@@ -308,9 +308,24 @@ class WanTransformerBlock(nn.Module):
|
||||
bs, seq_length, _ = hidden_states.shape
|
||||
orig_dtype = hidden_states.dtype
|
||||
# assert orig_dtype != torch.float32
|
||||
e = self.scale_shift_table + temb.float()
|
||||
shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = e.chunk(
|
||||
6, dim=1)
|
||||
|
||||
if temb.dim() == 4:
|
||||
# temb: batch_size, seq_len, 6, inner_dim (wan2.2 ti2v)
|
||||
shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = (
|
||||
self.scale_shift_table.unsqueeze(0) + temb.float()
|
||||
).chunk(6, dim=2)
|
||||
# batch_size, seq_len, 1, inner_dim
|
||||
shift_msa = shift_msa.squeeze(2)
|
||||
scale_msa = scale_msa.squeeze(2)
|
||||
gate_msa = gate_msa.squeeze(2)
|
||||
c_shift_msa = c_shift_msa.squeeze(2)
|
||||
c_scale_msa = c_scale_msa.squeeze(2)
|
||||
c_gate_msa = c_gate_msa.squeeze(2)
|
||||
else:
|
||||
# temb: batch_size, 6, inner_dim (wan2.1/wan2.2 14B)
|
||||
e = self.scale_shift_table + temb.float()
|
||||
shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = e.chunk(
|
||||
6, dim=1)
|
||||
assert shift_msa.dtype == torch.float32
|
||||
|
||||
# 1. Self-attention
|
||||
@@ -638,9 +653,21 @@ class WanTransformer3DModel(CachableDiT):
|
||||
hidden_states = self.patch_embedding(hidden_states)
|
||||
hidden_states = hidden_states.flatten(2).transpose(1, 2)
|
||||
|
||||
# timestep shape: batch_size, or batch_size, seq_len (wan 2.2 ti2v)
|
||||
if timestep.dim() == 2:
|
||||
ts_seq_len = timestep.shape[1]
|
||||
timestep = timestep.flatten() # batch_size * seq_len
|
||||
else:
|
||||
ts_seq_len = None
|
||||
|
||||
temb, timestep_proj, encoder_hidden_states, encoder_hidden_states_image = self.condition_embedder(
|
||||
timestep, encoder_hidden_states, encoder_hidden_states_image)
|
||||
timestep_proj = timestep_proj.unflatten(1, (6, -1))
|
||||
timestep, encoder_hidden_states, encoder_hidden_states_image, timestep_seq_len=ts_seq_len)
|
||||
if ts_seq_len is not None:
|
||||
# batch_size, seq_len, 6, inner_dim
|
||||
timestep_proj = timestep_proj.unflatten(2, (6, -1))
|
||||
else:
|
||||
# batch_size, 6, inner_dim
|
||||
timestep_proj = timestep_proj.unflatten(1, (6, -1))
|
||||
|
||||
if encoder_hidden_states_image is not None:
|
||||
encoder_hidden_states = torch.concat(
|
||||
@@ -679,8 +706,15 @@ class WanTransformer3DModel(CachableDiT):
|
||||
self.maybe_cache_states(hidden_states, original_hidden_states)
|
||||
|
||||
# 5. Output norm, projection & unpatchify
|
||||
shift, scale = (self.scale_shift_table + temb.unsqueeze(1)).chunk(2,
|
||||
dim=1)
|
||||
if temb.dim() == 3:
|
||||
# batch_size, seq_len, inner_dim (wan 2.2 ti2v)
|
||||
shift, scale = (self.scale_shift_table.unsqueeze(0) + temb.unsqueeze(2)).chunk(2, dim=2)
|
||||
shift = shift.squeeze(2)
|
||||
scale = scale.squeeze(2)
|
||||
else:
|
||||
# batch_size, inner_dim
|
||||
shift, scale = (self.scale_shift_table + temb.unsqueeze(1)).chunk(2, dim=1)
|
||||
|
||||
hidden_states = self.norm_out(hidden_states, shift, scale)
|
||||
hidden_states = self.proj_out(hidden_states)
|
||||
|
||||
@@ -783,4 +817,4 @@ class WanTransformer3DModel(CachableDiT):
|
||||
if self.is_even:
|
||||
return hidden_states + self.previous_residual_even
|
||||
else:
|
||||
return hidden_states + self.previous_residual_odd
|
||||
return hidden_states + self.previous_residual_odd
|
||||
@@ -458,7 +458,25 @@ class TransformerLoader(ComponentLoader):
|
||||
reduce_dtype=torch.float32,
|
||||
output_dtype=None,
|
||||
training_mode=fastvideo_args.training_mode)
|
||||
|
||||
if fastvideo_args.enable_torch_compile:
|
||||
logger.info("Torch Compile enabled for DiT")
|
||||
for n, m in reversed(list(model.named_modules())):
|
||||
if any([
|
||||
compile_condition(n, m)
|
||||
for compile_condition in model._compile_conditions
|
||||
]):
|
||||
parts = n.split(".")
|
||||
parent = model
|
||||
attr = parts[-1]
|
||||
for part in parts[:-1]:
|
||||
if part.isdigit():
|
||||
parent = parent[int(part)]
|
||||
else:
|
||||
parent = getattr(parent, part)
|
||||
if attr.isdigit():
|
||||
parent[int(attr)] = torch.compile(m)
|
||||
else:
|
||||
setattr(parent, attr, torch.compile(m))
|
||||
|
||||
total_params = sum(p.numel() for p in model.parameters())
|
||||
logger.info("Loaded model with %.2fB parameters", total_params / 1e9)
|
||||
|
||||
@@ -33,7 +33,7 @@ class WanDMDPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
]
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
|
||||
|
||||
# We use UniPCMScheduler from Wan2.1 official repo, not the one in diffusers.
|
||||
self.modules["scheduler"] = FlowMatchEulerDiscreteScheduler(
|
||||
shift=fastvideo_args.pipeline_config.flow_shift)
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ class WanPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
self.add_stage(stage_name="denoising_stage",
|
||||
stage=DenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
transformer_2=self.get_module("transformer_2", None),
|
||||
scheduler=self.get_module("scheduler"),
|
||||
vae=self.get_module("vae"),
|
||||
pipeline=self))
|
||||
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
|
||||
@@ -97,10 +97,6 @@ class ComposedPipelineBase(ABC):
|
||||
self.initialize_validation_pipeline(self.training_args)
|
||||
|
||||
self.initialize_pipeline(self.fastvideo_args)
|
||||
if self.fastvideo_args.enable_torch_compile:
|
||||
self.modules["transformer"] = torch.compile(
|
||||
self.modules["transformer"])
|
||||
logger.info("Torch Compile enabled for DiT")
|
||||
|
||||
if not self.fastvideo_args.training_mode:
|
||||
logger.info("Creating pipeline stages...")
|
||||
|
||||
@@ -16,41 +16,6 @@ import torch
|
||||
|
||||
from fastvideo.attention import AttentionMetadata
|
||||
from fastvideo.configs.sample.teacache import TeaCacheParams, WanTeaCacheParams
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, List
|
||||
import time
|
||||
|
||||
class PipelineLoggingInfo:
|
||||
"""Simple approach using OrderedDict to track stage metrics."""
|
||||
|
||||
def __init__(self):
|
||||
# OrderedDict preserves insertion order and allows easy access
|
||||
self.stages: OrderedDict[str, Dict[str, Any]] = OrderedDict()
|
||||
|
||||
def add_stage_execution_time(self, stage_name: str, execution_time: float):
|
||||
"""Add execution time for a stage."""
|
||||
if stage_name not in self.stages:
|
||||
self.stages[stage_name] = {}
|
||||
self.stages[stage_name]['execution_time'] = execution_time
|
||||
self.stages[stage_name]['timestamp'] = time.time()
|
||||
|
||||
def add_stage_metric(self, stage_name: str, metric_name: str, value: Any):
|
||||
"""Add any metric for a stage."""
|
||||
if stage_name not in self.stages:
|
||||
self.stages[stage_name] = {}
|
||||
self.stages[stage_name][metric_name] = value
|
||||
|
||||
def get_stage_info(self, stage_name: str) -> Dict[str, Any]:
|
||||
"""Get all info for a specific stage."""
|
||||
return self.stages.get(stage_name, {})
|
||||
|
||||
def get_execution_order(self) -> List[str]:
|
||||
"""Get stages in execution order."""
|
||||
return list(self.stages.keys())
|
||||
|
||||
def get_total_execution_time(self) -> float:
|
||||
"""Get total pipeline execution time."""
|
||||
return sum(stage.get('execution_time', 0) for stage in self.stages.values())
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -163,9 +128,6 @@ class ForwardBatch:
|
||||
# VSA parameters
|
||||
VSA_sparsity: float = 0.0
|
||||
|
||||
# Logging info
|
||||
logging_info: PipelineLoggingInfo = field(default_factory=PipelineLoggingInfo)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize dependent fields after dataclass initialization."""
|
||||
|
||||
@@ -225,6 +187,3 @@ class TrainingBatch:
|
||||
# Distillation losses
|
||||
generator_loss: float = 0.0
|
||||
fake_score_loss: float = 0.0
|
||||
|
||||
dmd_latent_vis_dict: dict[str, Any] = field(default_factory=dict)
|
||||
fake_score_latent_vis_dict: dict[str, Any] = field(default_factory=dict)
|
||||
@@ -21,6 +21,7 @@ _PIPELINE_NAME_TO_ARCHITECTURE_NAME: dict[str, str] = {
|
||||
"WanPipeline": "wan",
|
||||
"WanDMDPipeline": "wan",
|
||||
"WanImageToVideoPipeline": "wan",
|
||||
"WanDMDPipeline": "wan",
|
||||
"StepVideoPipeline": "stepvideo",
|
||||
"HunyuanVideoPipeline": "hunyuan",
|
||||
}
|
||||
@@ -65,7 +66,7 @@ class _PipelineRegistry:
|
||||
arch = _PIPELINE_NAME_TO_ARCHITECTURE_NAME[pipeline_name_in_config]
|
||||
return set(self.pipelines[pipeline_type.value][arch].keys())
|
||||
|
||||
def _load_preprocess_pipeline_cls(
|
||||
def _load_preprocessing_pipeline_cls(
|
||||
self, workload_type: WorkloadType,
|
||||
arch: str) -> type[ComposedPipelineBase] | None:
|
||||
if workload_type == WorkloadType.I2V:
|
||||
@@ -90,7 +91,7 @@ class _PipelineRegistry:
|
||||
return None
|
||||
|
||||
if pipeline_type == PipelineType.PREPROCESS:
|
||||
return self._load_preprocess_pipeline_cls(workload_type, arch)
|
||||
return self._load_preprocessing_pipeline_cls(workload_type, arch)
|
||||
elif pipeline_type == PipelineType.BASIC:
|
||||
return self.pipelines[
|
||||
pipeline_type.value][arch][pipeline_name_in_config]
|
||||
@@ -131,7 +132,7 @@ def import_pipeline_classes(
|
||||
Import pipeline classes based on the pipeline type and workload type.
|
||||
|
||||
Args:
|
||||
pipeline_types: The pipeline types to load (basic, preprocess, training).
|
||||
pipeline_types: The pipeline types to load (basic, preprocessing, training).
|
||||
If None, loads all types.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -155,7 +155,6 @@ class PipelineStage(ABC):
|
||||
execution_time = time.perf_counter() - start_time
|
||||
logger.info("[%s] Execution completed in %s ms", stage_name,
|
||||
execution_time * 1000)
|
||||
batch.logging_info.add_stage_execution_time(stage_name, execution_time)
|
||||
except Exception as e:
|
||||
execution_time = time.perf_counter() - start_time
|
||||
logger.error("[%s] Error during execution after %s ms: %s",
|
||||
|
||||
@@ -245,20 +245,21 @@ class DenoisingStage(PipelineStage):
|
||||
logger.info(
|
||||
f"mask[0][0][:, ::2, ::2] shape: {mask2[0][0][:, ::2, ::2].shape}"
|
||||
)
|
||||
temp_ts = (mask2[0][0][:, ::2, ::2] * timestep).flatten()
|
||||
temp_ts = (mask2[0][0][:, ::2, ::2] * timestep)
|
||||
logger.info(f"temp_ts shape before flatten: {temp_ts.shape}")
|
||||
temp_ts = temp_ts.flatten()
|
||||
logger.info(f"temp_ts: {temp_ts}")
|
||||
logger.info(f"temp_ts shape: {temp_ts.shape}")
|
||||
temp_ts = torch.cat([
|
||||
temp_ts,
|
||||
temp_ts.new_ones(seq_len - temp_ts.size(0)) * timestep
|
||||
])
|
||||
# timestep = temp_ts.unsqueeze(0)
|
||||
timestep = temp_ts
|
||||
timestep = temp_ts.unsqueeze(0)
|
||||
logger.info(f"after ti2v timestep: {timestep}")
|
||||
t = timestep
|
||||
# else:
|
||||
t_expand = t.repeat(latent_model_input.shape[0])
|
||||
# logger.info(f"t_expand shape: {t_expand.shape}")
|
||||
t_expand = timestep.repeat(latent_model_input.shape[0], 1)
|
||||
else:
|
||||
t_expand = t.repeat(latent_model_input.shape[0])
|
||||
logger.info(f"t_expand shape: {t_expand.shape}")
|
||||
# logger.info(f"t_expand: {t_expand}")
|
||||
|
||||
assert torch.isnan(latent_model_input).sum() == 0
|
||||
@@ -320,6 +321,8 @@ class DenoisingStage(PipelineStage):
|
||||
# fastvideo_args=fastvideo_args
|
||||
):
|
||||
# Run transformer
|
||||
cuda_memory_before = torch.cuda.memory_allocated()
|
||||
logger.info(f"cuda memory before transformer: {cuda_memory_before / 1024 / 1024 / 1024} GB")
|
||||
noise_pred = self.transformer(
|
||||
latent_model_input,
|
||||
prompt_embeds,
|
||||
@@ -782,8 +785,10 @@ class DmdDenoisingStage(DenoisingStage):
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
if (vsa_available and self.attn_backend
|
||||
== VideoSparseAttentionBackend):
|
||||
if (st_attn_available
|
||||
and self.attn_backend == SlidingTileAttentionBackend
|
||||
) or (vsa_available and self.attn_backend
|
||||
== VideoSparseAttentionBackend):
|
||||
self.attn_metadata_builder_cls = self.attn_backend.get_builder_cls(
|
||||
)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def rocm_platform_plugin() -> str | None:
|
||||
finally:
|
||||
amdsmi.amdsmi_shut_down()
|
||||
except Exception as e:
|
||||
logger.info("ROCm platform is unavailable: %s", e)
|
||||
logger.info("ROCm detection failed: %s", e)
|
||||
|
||||
return "fastvideo.platforms.rocm.RocmPlatform" if is_rocm else None
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torchvision
|
||||
from diffusers.optimization import get_scheduler
|
||||
from einops import rearrange
|
||||
from torch.utils.data import DataLoader
|
||||
from torchdata.stateful_dataloader import StatefulDataLoader
|
||||
@@ -35,7 +36,7 @@ from fastvideo.training.activation_checkpoint import (
|
||||
apply_activation_checkpointing)
|
||||
from fastvideo.training.training_pipeline import TrainingPipeline
|
||||
from fastvideo.training.training_utils import (
|
||||
clip_grad_norm_while_handling_failing_dtensor_cases, get_scheduler,
|
||||
clip_grad_norm_while_handling_failing_dtensor_cases,
|
||||
load_distillation_checkpoint, pred_noise_to_pred_video,
|
||||
save_distillation_checkpoint, shift_timestep)
|
||||
from fastvideo.utils import is_vsa_available, set_random_seed
|
||||
@@ -106,28 +107,21 @@ class DistillationPipeline(TrainingPipeline):
|
||||
fake_score_params = list(
|
||||
filter(lambda p: p.requires_grad,
|
||||
self.fake_score_transformer.parameters()))
|
||||
|
||||
# Use separate learning rate for fake_score_transformer if specified
|
||||
fake_score_lr = training_args.fake_score_learning_rate
|
||||
if fake_score_lr == 0.0:
|
||||
fake_score_lr = training_args.learning_rate
|
||||
|
||||
self.fake_score_optimizer = torch.optim.AdamW(
|
||||
fake_score_params,
|
||||
lr=fake_score_lr,
|
||||
lr=training_args.learning_rate,
|
||||
betas=(0.9, 0.999),
|
||||
weight_decay=training_args.weight_decay,
|
||||
eps=1e-8,
|
||||
)
|
||||
|
||||
self.fake_score_lr_scheduler = get_scheduler(
|
||||
training_args.fake_score_lr_scheduler,
|
||||
training_args.lr_scheduler,
|
||||
optimizer=self.fake_score_optimizer,
|
||||
num_warmup_steps=training_args.lr_warmup_steps,
|
||||
num_training_steps=training_args.max_train_steps,
|
||||
num_warmup_steps=training_args.lr_warmup_steps * self.world_size,
|
||||
num_training_steps=training_args.max_train_steps * self.world_size,
|
||||
num_cycles=training_args.lr_num_cycles,
|
||||
power=training_args.lr_power,
|
||||
min_lr_ratio=training_args.min_lr_ratio,
|
||||
last_epoch=self.init_steps - 1,
|
||||
)
|
||||
|
||||
@@ -185,7 +179,8 @@ class DistillationPipeline(TrainingPipeline):
|
||||
"timestep": timestep,
|
||||
"return_dict": False,
|
||||
}
|
||||
|
||||
training_batch.noise_latents = noise_input
|
||||
# After setting noise_latents, it's guaranteed to be not None
|
||||
return training_batch
|
||||
|
||||
def _generator_forward(self, training_batch: TrainingBatch) -> torch.Tensor:
|
||||
@@ -197,7 +192,6 @@ class DistillationPipeline(TrainingPipeline):
|
||||
device=self.device,
|
||||
dtype=torch.long)
|
||||
timestep = self.denoising_step_list[index]
|
||||
training_batch.dmd_latent_vis_dict["generator_timestep"] = timestep
|
||||
|
||||
noise = torch.randn(self.video_latent_shape,
|
||||
device=self.device,
|
||||
@@ -216,101 +210,18 @@ class DistillationPipeline(TrainingPipeline):
|
||||
training_batch = self._build_distill_input_kwargs(
|
||||
noisy_latent, timestep, training_batch.conditional_dict,
|
||||
training_batch)
|
||||
|
||||
pred_noise = self.transformer(**training_batch.input_kwargs).permute(
|
||||
0, 2, 1, 3, 4)
|
||||
|
||||
pred_video = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noisy_latent.flatten(0, 1),
|
||||
noise_input_latent=training_batch.noise_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self.noise_scheduler).unflatten(0, pred_noise.shape[:2])
|
||||
|
||||
return pred_video
|
||||
|
||||
def _generator_multi_step_simulation_forward(
|
||||
self, training_batch: TrainingBatch) -> torch.Tensor:
|
||||
"""Forward pass through student transformer matching inference procedure."""
|
||||
latents = training_batch.latents
|
||||
dtype = latents.dtype
|
||||
|
||||
# Step 1: Randomly sample a target timestep index from denoising_step_list
|
||||
target_timestep_idx = torch.randint(0,
|
||||
len(self.denoising_step_list), [1],
|
||||
device=self.device,
|
||||
dtype=torch.long)
|
||||
target_timestep = self.denoising_step_list[target_timestep_idx]
|
||||
|
||||
# Step 2: Simulate the multi-step inference process up to the target timestep
|
||||
# Start from pure noise like in inference
|
||||
current_noise_latents = torch.randn(self.video_latent_shape,
|
||||
device=self.device,
|
||||
dtype=dtype)
|
||||
if self.sp_world_size > 1:
|
||||
current_noise_latents = rearrange(
|
||||
current_noise_latents,
|
||||
"b (n t) c h w -> b n t c h w",
|
||||
n=self.sp_world_size).contiguous()
|
||||
current_noise_latents = current_noise_latents[:, self.
|
||||
rank_in_sp_group, :, :, :, :]
|
||||
|
||||
# Only run intermediate steps if target_timestep_idx > 0
|
||||
max_target_idx = len(self.denoising_step_list) - 1
|
||||
if max_target_idx > 0:
|
||||
# Run student model for all steps before the target timestep
|
||||
with torch.no_grad():
|
||||
for step_idx in range(max_target_idx):
|
||||
current_timestep = self.denoising_step_list[step_idx]
|
||||
current_timestep_tensor = current_timestep * torch.ones(
|
||||
1, device=self.device, dtype=torch.long)
|
||||
# Run student model to get flow prediction
|
||||
training_batch_temp = self._build_distill_input_kwargs(
|
||||
current_noise_latents, current_timestep_tensor,
|
||||
training_batch.conditional_dict, training_batch)
|
||||
pred_flow = self.transformer(
|
||||
**training_batch_temp.input_kwargs).permute(
|
||||
0, 2, 1, 3, 4)
|
||||
pred_clean = pred_noise_to_pred_video(
|
||||
pred_noise=pred_flow.flatten(0, 1),
|
||||
noise_input_latent=current_noise_latents.flatten(0, 1),
|
||||
timestep=current_timestep_tensor,
|
||||
scheduler=self.noise_scheduler).unflatten(
|
||||
0, pred_flow.shape[:2])
|
||||
|
||||
# Add noise for the next timestep
|
||||
next_timestep = self.denoising_step_list[step_idx + 1]
|
||||
next_timestep_tensor = next_timestep * torch.ones(
|
||||
1, device=self.device, dtype=torch.long)
|
||||
noise = torch.randn(self.video_latent_shape,
|
||||
device=self.device,
|
||||
dtype=pred_clean.dtype)
|
||||
if self.sp_world_size > 1:
|
||||
noise = rearrange(noise,
|
||||
"b (n t) c h w -> b n t c h w",
|
||||
n=self.sp_world_size).contiguous()
|
||||
noise = noise[:, self.rank_in_sp_group, :, :, :, :]
|
||||
current_noise_latents = self.noise_scheduler.add_noise(
|
||||
pred_clean.flatten(0, 1), noise.flatten(0, 1),
|
||||
next_timestep_tensor).unflatten(0, pred_clean.shape[:2])
|
||||
|
||||
# Step 3: Use the simulated noisy input for the final training step
|
||||
# For timestep index 0, this is pure noise
|
||||
# For timestep index k > 0, this is the result after k denoising steps + noise at target level
|
||||
noisy_input = current_noise_latents
|
||||
|
||||
# Step 4: Final student prediction (this is what we train on)
|
||||
training_batch = self._build_distill_input_kwargs(
|
||||
noisy_input, target_timestep, training_batch.conditional_dict,
|
||||
training_batch)
|
||||
pred_noise = self.transformer(**training_batch.input_kwargs).permute(
|
||||
0, 2, 1, 3, 4)
|
||||
pred_video = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noisy_input.flatten(0, 1),
|
||||
timestep=target_timestep,
|
||||
scheduler=self.noise_scheduler).unflatten(0, pred_noise.shape[:2])
|
||||
training_batch.dmd_latent_vis_dict[
|
||||
"generator_timestep"] = target_timestep.float().detach()
|
||||
return pred_video
|
||||
|
||||
def _dmd_forward(self, generator_pred_video: torch.Tensor,
|
||||
training_batch: TrainingBatch) -> torch.Tensor:
|
||||
"""Compute DMD (Diffusion Model Distillation) loss."""
|
||||
@@ -347,9 +258,9 @@ class DistillationPipeline(TrainingPipeline):
|
||||
fake_score_pred_noise = self.fake_score_transformer(
|
||||
**training_batch.input_kwargs).permute(0, 2, 1, 3, 4)
|
||||
|
||||
faker_score_pred_video = pred_noise_to_pred_video(
|
||||
pred_fake_video = pred_noise_to_pred_video(
|
||||
pred_noise=fake_score_pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noisy_latent.flatten(0, 1),
|
||||
noise_input_latent=training_batch.noise_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self.noise_scheduler).unflatten(
|
||||
0, fake_score_pred_noise.shape[:2])
|
||||
@@ -363,7 +274,7 @@ class DistillationPipeline(TrainingPipeline):
|
||||
|
||||
pred_real_video_cond = pred_noise_to_pred_video(
|
||||
pred_noise=real_score_pred_noise_cond.flatten(0, 1),
|
||||
noise_input_latent=noisy_latent.flatten(0, 1),
|
||||
noise_input_latent=training_batch.noise_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self.noise_scheduler).unflatten(
|
||||
0, real_score_pred_noise_cond.shape[:2])
|
||||
@@ -377,7 +288,7 @@ class DistillationPipeline(TrainingPipeline):
|
||||
|
||||
pred_real_video_uncond = pred_noise_to_pred_video(
|
||||
pred_noise=real_score_pred_noise_uncond.flatten(0, 1),
|
||||
noise_input_latent=noisy_latent.flatten(0, 1),
|
||||
noise_input_latent=training_batch.noise_latents.flatten(0, 1),
|
||||
timestep=timestep,
|
||||
scheduler=self.noise_scheduler).unflatten(
|
||||
0, real_score_pred_noise_uncond.shape[:2])
|
||||
@@ -386,7 +297,7 @@ class DistillationPipeline(TrainingPipeline):
|
||||
pred_real_video_cond -
|
||||
pred_real_video_uncond) * self.real_score_guidance_scale
|
||||
|
||||
grad = (faker_score_pred_video - real_score_pred_video) / torch.abs(
|
||||
grad = (pred_fake_video - real_score_pred_video) / torch.abs(
|
||||
generator_pred_video - real_score_pred_video).mean()
|
||||
grad = torch.nan_to_num(grad)
|
||||
|
||||
@@ -394,19 +305,6 @@ class DistillationPipeline(TrainingPipeline):
|
||||
generator_pred_video.float(),
|
||||
(generator_pred_video.float() - grad.float()).detach())
|
||||
|
||||
training_batch.dmd_latent_vis_dict.update({
|
||||
"training_batch_dmd_fwd_clean_latent":
|
||||
training_batch.latents,
|
||||
"generator_pred_video":
|
||||
generator_pred_video,
|
||||
"real_score_pred_video":
|
||||
real_score_pred_video,
|
||||
"faker_score_pred_video":
|
||||
faker_score_pred_video,
|
||||
"dmd_timestep":
|
||||
timestep,
|
||||
})
|
||||
|
||||
return dmd_loss
|
||||
|
||||
def faker_score_forward(
|
||||
@@ -415,11 +313,7 @@ class DistillationPipeline(TrainingPipeline):
|
||||
with torch.no_grad(), set_forward_context(
|
||||
current_timestep=training_batch.timesteps,
|
||||
attn_metadata=training_batch.attn_metadata_vsa):
|
||||
if self.training_args.simulate_student_forward:
|
||||
generator_pred_video = self._generator_multi_step_simulation_forward(
|
||||
training_batch)
|
||||
else:
|
||||
generator_pred_video = self._generator_forward(training_batch)
|
||||
generator_pred_video = self._generator_forward(training_batch)
|
||||
|
||||
fake_score_timestep = torch.randint(0,
|
||||
self.num_train_timestep, [1],
|
||||
@@ -459,16 +353,9 @@ class DistillationPipeline(TrainingPipeline):
|
||||
**training_batch.input_kwargs).permute(0, 2, 1, 3, 4)
|
||||
|
||||
target = fake_score_noise - generator_pred_video
|
||||
flow_matching_loss = torch.mean((fake_score_pred_noise - target)**2)
|
||||
denoising_loss = torch.mean((fake_score_pred_noise - target)**2)
|
||||
|
||||
training_batch.fake_score_latent_vis_dict = {
|
||||
"training_batch_fakerscore_fwd_clean_latent":
|
||||
training_batch.latents,
|
||||
"generator_pred_video": generator_pred_video,
|
||||
"fake_score_timestep": fake_score_timestep,
|
||||
}
|
||||
|
||||
return training_batch, flow_matching_loss
|
||||
return training_batch, denoising_loss
|
||||
|
||||
def _clip_model_grad_norm_(self, training_batch: TrainingBatch,
|
||||
transformer) -> TrainingBatch:
|
||||
@@ -502,9 +389,6 @@ class DistillationPipeline(TrainingPipeline):
|
||||
"encoder_attention_mask": self.negative_prompt_attention_mask,
|
||||
}
|
||||
|
||||
training_batch.dmd_latent_vis_dict = {}
|
||||
training_batch.fake_score_latent_vis_dict = {}
|
||||
|
||||
training_batch.conditional_dict = conditional_dict
|
||||
training_batch.unconditional_dict = unconditional_dict
|
||||
training_batch.raw_latent_shape = training_batch.latents.shape
|
||||
@@ -541,35 +425,29 @@ class DistillationPipeline(TrainingPipeline):
|
||||
|
||||
self.optimizer.zero_grad()
|
||||
total_dmd_loss = 0.0
|
||||
dmd_latent_vis_dict = {}
|
||||
fake_score_latent_vis_dict = {}
|
||||
if (self.current_trainstep % self.generator_update_interval == 0):
|
||||
for batch in batches:
|
||||
batch_gen = copy.deepcopy(batch)
|
||||
batch_stu = copy.deepcopy(batch)
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=batch_gen.timesteps,
|
||||
attn_metadata=batch_gen.attn_metadata_vsa):
|
||||
if self.training_args.simulate_student_forward:
|
||||
generator_pred_video = self._generator_multi_step_simulation_forward(
|
||||
batch_gen)
|
||||
else:
|
||||
generator_pred_video = self._generator_forward(
|
||||
batch_gen)
|
||||
current_timestep=batch_stu.timesteps,
|
||||
attn_metadata=batch_stu.attn_metadata_vsa):
|
||||
generator_pred_video = self._generator_forward(batch_stu)
|
||||
|
||||
with set_forward_context(current_timestep=batch_gen.timesteps,
|
||||
attn_metadata=batch_gen.attn_metadata):
|
||||
with set_forward_context(current_timestep=batch_stu.timesteps,
|
||||
attn_metadata=batch_stu.attn_metadata):
|
||||
dmd_loss = self._dmd_forward(
|
||||
generator_pred_video=generator_pred_video,
|
||||
training_batch=batch_gen)
|
||||
training_batch=batch_stu)
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=batch_gen.timesteps,
|
||||
attn_metadata=batch_gen.attn_metadata_vsa):
|
||||
current_timestep=batch_stu.timesteps,
|
||||
attn_metadata=batch_stu.attn_metadata_vsa):
|
||||
(dmd_loss / gradient_accumulation_steps).backward()
|
||||
total_dmd_loss += dmd_loss.detach().item()
|
||||
self._clip_model_grad_norm_(batch_gen, self.transformer)
|
||||
self._clip_model_grad_norm_(batch_stu, self.transformer)
|
||||
self.optimizer.step()
|
||||
self.lr_scheduler.step()
|
||||
self.optimizer.zero_grad(set_to_none=True)
|
||||
avg_dmd_loss = torch.tensor(total_dmd_loss /
|
||||
gradient_accumulation_steps,
|
||||
@@ -578,25 +456,22 @@ class DistillationPipeline(TrainingPipeline):
|
||||
world_group.all_reduce(avg_dmd_loss,
|
||||
op=torch.distributed.ReduceOp.AVG)
|
||||
training_batch.generator_loss = avg_dmd_loss.item()
|
||||
dmd_latent_vis_dict = batch_gen.dmd_latent_vis_dict
|
||||
else:
|
||||
training_batch.generator_loss = 0.0
|
||||
|
||||
self.fake_score_optimizer.zero_grad()
|
||||
total_fake_score_loss = 0.0
|
||||
for batch in batches:
|
||||
batch_fake = copy.deepcopy(batch)
|
||||
batch_fake, fake_score_loss = self.faker_score_forward(batch_fake)
|
||||
with set_forward_context(current_timestep=batch_fake.timesteps,
|
||||
attn_metadata=batch_fake.attn_metadata):
|
||||
batch_critic = copy.deepcopy(batch)
|
||||
batch_critic, fake_score_loss = self.faker_score_forward(
|
||||
batch_critic)
|
||||
with set_forward_context(current_timestep=batch_critic.timesteps,
|
||||
attn_metadata=batch_critic.attn_metadata):
|
||||
(fake_score_loss / gradient_accumulation_steps).backward()
|
||||
total_fake_score_loss += fake_score_loss.detach().item()
|
||||
fake_score_latent_vis_dict.update(
|
||||
batch_fake.fake_score_latent_vis_dict)
|
||||
self._clip_model_grad_norm_(batch_fake, self.fake_score_transformer)
|
||||
self._clip_model_grad_norm_(batch_critic, self.fake_score_transformer)
|
||||
self.fake_score_optimizer.step()
|
||||
self.fake_score_lr_scheduler.step()
|
||||
self.lr_scheduler.step()
|
||||
self.fake_score_optimizer.zero_grad(set_to_none=True)
|
||||
avg_fake_score_loss = torch.tensor(total_fake_score_loss /
|
||||
gradient_accumulation_steps,
|
||||
@@ -605,8 +480,6 @@ class DistillationPipeline(TrainingPipeline):
|
||||
world_group.all_reduce(avg_fake_score_loss,
|
||||
op=torch.distributed.ReduceOp.AVG)
|
||||
training_batch.fake_score_loss = avg_fake_score_loss.item()
|
||||
training_batch.dmd_latent_vis_dict = dmd_latent_vis_dict
|
||||
training_batch.fake_score_latent_vis_dict = batch_fake.fake_score_latent_vis_dict
|
||||
|
||||
training_batch.total_loss = training_batch.generator_loss + training_batch.fake_score_loss
|
||||
return training_batch
|
||||
@@ -792,82 +665,6 @@ class DistillationPipeline(TrainingPipeline):
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def visualize_intermediate_latents(self, training_batch: TrainingBatch,
|
||||
training_args: TrainingArgs, step: int):
|
||||
"""Add visualization data to wandb logging and save frames to disk."""
|
||||
wandb_loss_dict = {}
|
||||
torch.cuda.empty_cache()
|
||||
dmd_latents_vis_dict = training_batch.dmd_latent_vis_dict
|
||||
fake_score_latents_vis_dict = training_batch.fake_score_latent_vis_dict
|
||||
fake_score_log_keys = ['generator_pred_video']
|
||||
dmd_log_keys = ['faker_score_pred_video']
|
||||
|
||||
for latent_key in fake_score_log_keys:
|
||||
latents = fake_score_latents_vis_dict[latent_key]
|
||||
latents = latents.permute(0, 2, 1, 3, 4)
|
||||
|
||||
if isinstance(self.vae.scaling_factor, torch.Tensor):
|
||||
latents = latents / self.vae.scaling_factor.to(
|
||||
latents.device, latents.dtype)
|
||||
else:
|
||||
latents = latents / self.vae.scaling_factor
|
||||
|
||||
# Apply shifting if needed
|
||||
if (hasattr(self.vae, "shift_factor")
|
||||
and self.vae.shift_factor is not None):
|
||||
if isinstance(self.vae.shift_factor, torch.Tensor):
|
||||
latents += self.vae.shift_factor.to(latents.device,
|
||||
latents.dtype)
|
||||
else:
|
||||
latents += self.vae.shift_factor
|
||||
with torch.autocast("cuda", dtype=torch.bfloat16):
|
||||
video = self.vae.decode(latents)
|
||||
video = (video / 2 + 0.5).clamp(0, 1)
|
||||
video = video.cpu().float()
|
||||
video = video.permute(0, 2, 1, 3, 4)
|
||||
video = (video * 255).numpy().astype(np.uint8)
|
||||
wandb_loss_dict[latent_key] = wandb.Video(
|
||||
video, fps=24, format="mp4") # change to 16 for Wan2.1
|
||||
# Clean up references
|
||||
del video, latents
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# Process DMD training data if available - use decode_stage instead of self.vae.decode
|
||||
if 'generator_pred_video' in dmd_latents_vis_dict:
|
||||
for latent_key in dmd_log_keys:
|
||||
latents = dmd_latents_vis_dict[latent_key]
|
||||
latents = latents.permute(0, 2, 1, 3, 4)
|
||||
# decoded_latent = decode_stage(ForwardBatch(data_type="video", latents=latents), training_args)
|
||||
if isinstance(self.vae.scaling_factor, torch.Tensor):
|
||||
latents = latents / self.vae.scaling_factor.to(
|
||||
latents.device, latents.dtype)
|
||||
else:
|
||||
latents = latents / self.vae.scaling_factor
|
||||
|
||||
# Apply shifting if needed
|
||||
if (hasattr(self.vae, "shift_factor")
|
||||
and self.vae.shift_factor is not None):
|
||||
if isinstance(self.vae.shift_factor, torch.Tensor):
|
||||
latents += self.vae.shift_factor.to(
|
||||
latents.device, latents.dtype)
|
||||
else:
|
||||
latents += self.vae.shift_factor
|
||||
with torch.autocast("cuda", dtype=torch.bfloat16):
|
||||
video = self.vae.decode(latents)
|
||||
video = (video / 2 + 0.5).clamp(0, 1)
|
||||
video = video.cpu().float()
|
||||
video = video.permute(0, 2, 1, 3, 4)
|
||||
video = (video * 255).numpy().astype(np.uint8)
|
||||
wandb_loss_dict[latent_key] = wandb.Video(
|
||||
video, fps=24, format="mp4") # change to 16 for Wan2.1
|
||||
# Clean up references
|
||||
del video, latents
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# Log to wandb
|
||||
if self.global_rank == 0:
|
||||
wandb.log(wandb_loss_dict, step=step)
|
||||
|
||||
def train(self) -> None:
|
||||
"""Main training loop with distillation-specific logging."""
|
||||
assert self.training_args.seed is not None, "seed must be set"
|
||||
@@ -959,45 +756,18 @@ class DistillationPipeline(TrainingPipeline):
|
||||
if self.global_rank == 0:
|
||||
# Prepare logging data
|
||||
log_data = {
|
||||
"train_total_loss":
|
||||
total_loss,
|
||||
"train_fake_score_loss":
|
||||
fake_score_loss,
|
||||
"learning_rate":
|
||||
self.lr_scheduler.get_last_lr()[0],
|
||||
"fake_score_learning_rate":
|
||||
self.fake_score_lr_scheduler.get_last_lr()[0],
|
||||
"step_time":
|
||||
step_time,
|
||||
"avg_step_time":
|
||||
avg_step_time,
|
||||
"grad_norm":
|
||||
grad_norm,
|
||||
"train_total_loss": total_loss,
|
||||
"train_fake_score_loss": fake_score_loss,
|
||||
"learning_rate": self.lr_scheduler.get_last_lr()[0],
|
||||
"step_time": step_time,
|
||||
"avg_step_time": avg_step_time,
|
||||
"grad_norm": grad_norm,
|
||||
}
|
||||
# Only log generator loss when generator is actually trained
|
||||
if (step % self.generator_update_interval == 0):
|
||||
log_data["train_generator_loss"] = generator_loss
|
||||
if use_vsa:
|
||||
log_data["VSA_train_sparsity"] = current_vsa_sparsity
|
||||
|
||||
if training_batch.dmd_latent_vis_dict:
|
||||
dmd_additional_logs = {
|
||||
"generator_timestep":
|
||||
training_batch.
|
||||
dmd_latent_vis_dict["generator_timestep"].item(),
|
||||
"dmd_timestep":
|
||||
training_batch.dmd_latent_vis_dict["dmd_timestep"].item(
|
||||
),
|
||||
}
|
||||
log_data.update(dmd_additional_logs)
|
||||
|
||||
faker_score_additional_logs = {
|
||||
"fake_score_timestep":
|
||||
training_batch.
|
||||
fake_score_latent_vis_dict["fake_score_timestep"].item(),
|
||||
}
|
||||
log_data.update(faker_score_additional_logs)
|
||||
|
||||
wandb.log(log_data, step=step)
|
||||
|
||||
# Save training state checkpoint (for resuming training)
|
||||
@@ -1031,10 +801,6 @@ class DistillationPipeline(TrainingPipeline):
|
||||
only_save_generator_weight=True)
|
||||
|
||||
if self.training_args.log_validation and step % self.training_args.validation_steps == 0:
|
||||
if self.training_args.log_visualization:
|
||||
self.visualize_intermediate_latents(training_batch,
|
||||
self.training_args,
|
||||
step)
|
||||
self._log_validation(self.transformer, self.training_args, step)
|
||||
|
||||
wandb.finish()
|
||||
|
||||
@@ -14,6 +14,7 @@ import torch
|
||||
import torch.distributed as dist
|
||||
import torchvision
|
||||
from diffusers import FlowMatchEulerDiscreteScheduler
|
||||
from diffusers.optimization import get_scheduler
|
||||
from einops import rearrange
|
||||
from torch.utils.data import DataLoader
|
||||
from torchdata.stateful_dataloader import StatefulDataLoader
|
||||
@@ -38,9 +39,8 @@ from fastvideo.training.activation_checkpoint import (
|
||||
apply_activation_checkpointing)
|
||||
from fastvideo.training.training_utils import (
|
||||
clip_grad_norm_while_handling_failing_dtensor_cases,
|
||||
compute_density_for_timestep_sampling, get_scheduler, get_sigmas,
|
||||
load_checkpoint, normalize_dit_input, save_checkpoint,
|
||||
shard_latents_across_sp)
|
||||
compute_density_for_timestep_sampling, get_sigmas, load_checkpoint,
|
||||
normalize_dit_input, save_checkpoint, shard_latents_across_sp)
|
||||
from fastvideo.utils import is_vsa_available, set_random_seed, shallow_asdict
|
||||
|
||||
import wandb # isort: skip
|
||||
@@ -130,11 +130,10 @@ class TrainingPipeline(LoRAPipeline, ABC):
|
||||
self.lr_scheduler = get_scheduler(
|
||||
training_args.lr_scheduler,
|
||||
optimizer=self.optimizer,
|
||||
num_warmup_steps=training_args.lr_warmup_steps,
|
||||
num_training_steps=training_args.max_train_steps,
|
||||
num_warmup_steps=training_args.lr_warmup_steps * self.world_size,
|
||||
num_training_steps=training_args.max_train_steps * self.world_size,
|
||||
num_cycles=training_args.lr_num_cycles,
|
||||
power=training_args.lr_power,
|
||||
min_lr_ratio=training_args.min_lr_ratio,
|
||||
last_epoch=self.init_steps - 1,
|
||||
)
|
||||
|
||||
@@ -210,9 +209,9 @@ class TrainingPipeline(LoRAPipeline, ABC):
|
||||
def _normalize_dit_input(self,
|
||||
training_batch: TrainingBatch) -> TrainingBatch:
|
||||
# TODO(will): support other models
|
||||
training_batch.latents = normalize_dit_input('wan',
|
||||
training_batch.latents,
|
||||
self.get_module("vae"))
|
||||
training_batch.latents = normalize_dit_input(
|
||||
'wan', training_batch.latents,
|
||||
self.validation_pipeline.get_module("vae"))
|
||||
return training_batch
|
||||
|
||||
def _prepare_dit_inputs(self,
|
||||
|
||||
@@ -3,8 +3,7 @@ import json
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Callable, Iterator
|
||||
from enum import Enum
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
@@ -12,8 +11,6 @@ import torch.distributed as dist
|
||||
import torch.distributed.checkpoint as dcp
|
||||
from einops import rearrange
|
||||
from safetensors.torch import save_file
|
||||
from torch.optim import Optimizer
|
||||
from torch.optim.lr_scheduler import LambdaLR
|
||||
|
||||
from fastvideo.distributed.parallel_state import (get_sp_parallel_rank,
|
||||
get_sp_world_size)
|
||||
@@ -855,448 +852,3 @@ def shift_timestep(timestep: torch.Tensor, shift: float,
|
||||
t = timestep / num_train_timestep
|
||||
denominator = 1 + (shift - 1) * t
|
||||
return num_train_timestep * (shift * t / denominator)
|
||||
|
||||
|
||||
# coding=utf-8
|
||||
# Copyright 2025 The HuggingFace Inc. team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Modified from diffusers/src/diffusers/optimization.py
|
||||
"""PyTorch optimization for diffusion models."""
|
||||
|
||||
|
||||
class SchedulerType(Enum):
|
||||
LINEAR = "linear"
|
||||
COSINE = "cosine"
|
||||
COSINE_WITH_RESTARTS = "cosine_with_restarts"
|
||||
COSINE_WITH_MIN_LR = "cosine_with_min_lr"
|
||||
POLYNOMIAL = "polynomial"
|
||||
CONSTANT = "constant"
|
||||
CONSTANT_WITH_WARMUP = "constant_with_warmup"
|
||||
PIECEWISE_CONSTANT = "piecewise_constant"
|
||||
|
||||
|
||||
def get_constant_schedule(optimizer: Optimizer,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a constant learning rate, using the learning rate set in optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
return LambdaLR(optimizer, lambda _: 1, last_epoch=last_epoch)
|
||||
|
||||
|
||||
def get_constant_schedule_with_warmup(optimizer: Optimizer,
|
||||
num_warmup_steps: int,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a constant learning rate preceded by a warmup period during which the learning rate
|
||||
increases linearly between 0 and the initial lr set in the optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
num_warmup_steps (`int`):
|
||||
The number of steps for the warmup phase.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
|
||||
def lr_lambda(current_step: int):
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1.0, num_warmup_steps))
|
||||
return 1.0
|
||||
|
||||
return LambdaLR(optimizer, lr_lambda, last_epoch=last_epoch)
|
||||
|
||||
|
||||
def get_piecewise_constant_schedule(optimizer: Optimizer,
|
||||
step_rules: str,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a constant learning rate, using the learning rate set in optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
step_rules (`string`):
|
||||
The rules for the learning rate. ex: rule_steps="1:10,0.1:20,0.01:30,0.005" it means that the learning rate
|
||||
if multiple 1 for the first 10 steps, multiple 0.1 for the next 20 steps, multiple 0.01 for the next 30
|
||||
steps and multiple 0.005 for the other steps.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
|
||||
rules_dict = {}
|
||||
rule_list = step_rules.split(",")
|
||||
for rule_str in rule_list[:-1]:
|
||||
value_str, steps_str = rule_str.split(":")
|
||||
steps = int(steps_str)
|
||||
value = float(value_str)
|
||||
rules_dict[steps] = value
|
||||
last_lr_multiple = float(rule_list[-1])
|
||||
|
||||
def create_rules_function(
|
||||
rules_dict: dict,
|
||||
last_lr_multiple: float) -> Callable[[int], float]:
|
||||
|
||||
def rule_func(steps: int) -> float:
|
||||
for step_threshold, lr_multiple in sorted(rules_dict.items()):
|
||||
if steps < step_threshold:
|
||||
return lr_multiple
|
||||
return last_lr_multiple
|
||||
|
||||
return rule_func
|
||||
|
||||
rules_func = create_rules_function(rules_dict, last_lr_multiple)
|
||||
|
||||
return LambdaLR(optimizer, rules_func, last_epoch=last_epoch)
|
||||
|
||||
|
||||
def get_linear_schedule_with_warmup(optimizer: Optimizer,
|
||||
num_warmup_steps: int,
|
||||
num_training_steps: int,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a learning rate that decreases linearly from the initial lr set in the optimizer to 0, after
|
||||
a warmup period during which it increases linearly from 0 to the initial lr set in the optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
num_warmup_steps (`int`):
|
||||
The number of steps for the warmup phase.
|
||||
num_training_steps (`int`):
|
||||
The total number of training steps.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
|
||||
def lr_lambda(current_step: int):
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1, num_warmup_steps))
|
||||
return max(
|
||||
0.0,
|
||||
float(num_training_steps - current_step) /
|
||||
float(max(1, num_training_steps - num_warmup_steps)))
|
||||
|
||||
return LambdaLR(optimizer, lr_lambda, last_epoch)
|
||||
|
||||
|
||||
def get_cosine_schedule_with_warmup(optimizer: Optimizer,
|
||||
num_warmup_steps: int,
|
||||
num_training_steps: int,
|
||||
num_cycles: float = 0.5,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a learning rate that decreases following the values of the cosine function between the
|
||||
initial lr set in the optimizer to 0, after a warmup period during which it increases linearly between 0 and the
|
||||
initial lr set in the optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
num_warmup_steps (`int`):
|
||||
The number of steps for the warmup phase.
|
||||
num_training_steps (`int`):
|
||||
The total number of training steps.
|
||||
num_periods (`float`, *optional*, defaults to 0.5):
|
||||
The number of periods of the cosine function in a schedule (the default is to just decrease from the max
|
||||
value to 0 following a half-cosine).
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
|
||||
def lr_lambda(current_step):
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1, num_warmup_steps))
|
||||
progress = float(current_step - num_warmup_steps) / float(
|
||||
max(1, num_training_steps - num_warmup_steps))
|
||||
return max(
|
||||
0.0, 0.5 *
|
||||
(1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress)))
|
||||
|
||||
return LambdaLR(optimizer, lr_lambda, last_epoch)
|
||||
|
||||
|
||||
def get_cosine_schedule_with_min_lr(optimizer: Optimizer,
|
||||
num_warmup_steps: int,
|
||||
num_training_steps: int,
|
||||
min_lr_ratio: float = 0.1,
|
||||
num_cycles: float = 0.5,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a learning rate that decreases following the values of the cosine function between the
|
||||
initial lr set in the optimizer to a minimum lr (min_lr_ratio * initial_lr), after a warmup period during which
|
||||
it increases linearly between 0 and the initial lr set in the optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
num_warmup_steps (`int`):
|
||||
The number of steps for the warmup phase.
|
||||
num_training_steps (`int`):
|
||||
The total number of training steps.
|
||||
min_lr_ratio (`float`, *optional*, defaults to 0.1):
|
||||
The ratio of minimum learning rate to initial learning rate.
|
||||
num_cycles (`float`, *optional*, defaults to 0.5):
|
||||
The number of periods of the cosine function in a schedule.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
|
||||
def lr_lambda(current_step):
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1, num_warmup_steps))
|
||||
progress = float(current_step - num_warmup_steps) / float(
|
||||
max(1, num_training_steps - num_warmup_steps))
|
||||
# Cosine decay from 1.0 to min_lr_ratio over num_cycles periods
|
||||
# Use the same formula as standard cosine but scale the output to [min_lr_ratio, 1.0]
|
||||
cosine_value = 0.5 * (
|
||||
1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))
|
||||
# Scale from [0, 1] to [min_lr_ratio, 1.0]
|
||||
return min_lr_ratio + (1.0 - min_lr_ratio) * cosine_value
|
||||
|
||||
return LambdaLR(optimizer, lr_lambda, last_epoch)
|
||||
|
||||
|
||||
def get_cosine_with_hard_restarts_schedule_with_warmup(
|
||||
optimizer: Optimizer,
|
||||
num_warmup_steps: int,
|
||||
num_training_steps: int,
|
||||
num_cycles: int = 1,
|
||||
last_epoch: int = -1) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a learning rate that decreases following the values of the cosine function between the
|
||||
initial lr set in the optimizer to 0, with several hard restarts, after a warmup period during which it increases
|
||||
linearly between 0 and the initial lr set in the optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
num_warmup_steps (`int`):
|
||||
The number of steps for the warmup phase.
|
||||
num_training_steps (`int`):
|
||||
The total number of training steps.
|
||||
num_cycles (`int`, *optional*, defaults to 1):
|
||||
The number of hard restarts to use.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
"""
|
||||
|
||||
def lr_lambda(current_step):
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1, num_warmup_steps))
|
||||
progress = float(current_step - num_warmup_steps) / float(
|
||||
max(1, num_training_steps - num_warmup_steps))
|
||||
if progress >= 1.0:
|
||||
return 0.0
|
||||
return max(
|
||||
0.0, 0.5 * (1.0 + math.cos(math.pi *
|
||||
((float(num_cycles) * progress) % 1.0))))
|
||||
|
||||
return LambdaLR(optimizer, lr_lambda, last_epoch)
|
||||
|
||||
|
||||
def get_polynomial_decay_schedule_with_warmup(
|
||||
optimizer: Optimizer,
|
||||
num_warmup_steps: int,
|
||||
num_training_steps: int,
|
||||
lr_end: float = 1e-7,
|
||||
power: float = 1.0,
|
||||
last_epoch: int = -1,
|
||||
) -> LambdaLR:
|
||||
"""
|
||||
Create a schedule with a learning rate that decreases as a polynomial decay from the initial lr set in the
|
||||
optimizer to end lr defined by *lr_end*, after a warmup period during which it increases linearly from 0 to the
|
||||
initial lr set in the optimizer.
|
||||
|
||||
Args:
|
||||
optimizer ([`~torch.optim.Optimizer`]):
|
||||
The optimizer for which to schedule the learning rate.
|
||||
num_warmup_steps (`int`):
|
||||
The number of steps for the warmup phase.
|
||||
num_training_steps (`int`):
|
||||
The total number of training steps.
|
||||
lr_end (`float`, *optional*, defaults to 1e-7):
|
||||
The end LR.
|
||||
power (`float`, *optional*, defaults to 1.0):
|
||||
Power factor.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
|
||||
Note: *power* defaults to 1.0 as in the fairseq implementation, which in turn is based on the original BERT
|
||||
implementation at
|
||||
https://github.com/google-research/bert/blob/f39e881b169b9d53bea03d2d341b31707a6c052b/optimization.py#L37
|
||||
|
||||
Return:
|
||||
`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
|
||||
|
||||
"""
|
||||
|
||||
lr_init = optimizer.defaults["lr"]
|
||||
if not (lr_init > lr_end):
|
||||
raise ValueError(
|
||||
f"lr_end ({lr_end}) must be smaller than initial lr ({lr_init})")
|
||||
|
||||
def lr_lambda(current_step: int):
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1, num_warmup_steps))
|
||||
elif current_step > num_training_steps:
|
||||
return lr_end / lr_init # as LambdaLR multiplies by lr_init
|
||||
else:
|
||||
lr_range = lr_init - lr_end
|
||||
decay_steps = num_training_steps - num_warmup_steps
|
||||
pct_remaining = 1 - (current_step - num_warmup_steps) / decay_steps
|
||||
decay = lr_range * pct_remaining**power + lr_end
|
||||
return decay / lr_init # as LambdaLR multiplies by lr_init
|
||||
|
||||
return LambdaLR(optimizer, lr_lambda, last_epoch)
|
||||
|
||||
|
||||
# Type alias for scheduler functions
|
||||
SchedulerFunction = Callable[..., LambdaLR]
|
||||
|
||||
TYPE_TO_SCHEDULER_FUNCTION: dict[SchedulerType, SchedulerFunction] = {
|
||||
SchedulerType.LINEAR: get_linear_schedule_with_warmup,
|
||||
SchedulerType.COSINE: get_cosine_schedule_with_warmup,
|
||||
SchedulerType.COSINE_WITH_RESTARTS:
|
||||
get_cosine_with_hard_restarts_schedule_with_warmup,
|
||||
SchedulerType.COSINE_WITH_MIN_LR: get_cosine_schedule_with_min_lr,
|
||||
SchedulerType.POLYNOMIAL: get_polynomial_decay_schedule_with_warmup,
|
||||
SchedulerType.CONSTANT: get_constant_schedule,
|
||||
SchedulerType.CONSTANT_WITH_WARMUP: get_constant_schedule_with_warmup,
|
||||
SchedulerType.PIECEWISE_CONSTANT: get_piecewise_constant_schedule,
|
||||
}
|
||||
|
||||
|
||||
def get_scheduler(
|
||||
name: str | SchedulerType,
|
||||
optimizer: Optimizer,
|
||||
step_rules: str | None = None,
|
||||
num_warmup_steps: int | None = None,
|
||||
num_training_steps: int | None = None,
|
||||
num_cycles: int = 1,
|
||||
power: float = 1.0,
|
||||
min_lr_ratio: float = 0.1,
|
||||
last_epoch: int = -1,
|
||||
) -> LambdaLR:
|
||||
"""
|
||||
Unified API to get any scheduler from its name.
|
||||
|
||||
Args:
|
||||
name (`str` or `SchedulerType`):
|
||||
The name of the scheduler to use.
|
||||
optimizer (`torch.optim.Optimizer`):
|
||||
The optimizer that will be used during training.
|
||||
step_rules (`str`, *optional*):
|
||||
A string representing the step rules to use. This is only used by the `PIECEWISE_CONSTANT` scheduler.
|
||||
num_warmup_steps (`int`, *optional*):
|
||||
The number of warmup steps to do. This is not required by all schedulers (hence the argument being
|
||||
optional), the function will raise an error if it's unset and the scheduler type requires it.
|
||||
num_training_steps (`int``, *optional*):
|
||||
The number of training steps to do. This is not required by all schedulers (hence the argument being
|
||||
optional), the function will raise an error if it's unset and the scheduler type requires it.
|
||||
num_cycles (`int`, *optional*):
|
||||
The number of hard restarts used in `COSINE_WITH_RESTARTS` scheduler.
|
||||
power (`float`, *optional*, defaults to 1.0):
|
||||
Power factor. See `POLYNOMIAL` scheduler
|
||||
min_lr_ratio (`float`, *optional*, defaults to 0.1):
|
||||
The ratio of minimum learning rate to initial learning rate. Used in `COSINE_WITH_MIN_LR` scheduler.
|
||||
last_epoch (`int`, *optional*, defaults to -1):
|
||||
The index of the last epoch when resuming training.
|
||||
"""
|
||||
name = SchedulerType(name)
|
||||
schedule_func = TYPE_TO_SCHEDULER_FUNCTION[name]
|
||||
if name == SchedulerType.CONSTANT:
|
||||
return schedule_func(optimizer, last_epoch=last_epoch)
|
||||
|
||||
if name == SchedulerType.PIECEWISE_CONSTANT:
|
||||
return schedule_func(optimizer,
|
||||
step_rules=step_rules,
|
||||
last_epoch=last_epoch)
|
||||
|
||||
# All other schedulers require `num_warmup_steps`
|
||||
if num_warmup_steps is None:
|
||||
raise ValueError(
|
||||
f"{name} requires `num_warmup_steps`, please provide that argument."
|
||||
)
|
||||
|
||||
if name == SchedulerType.CONSTANT_WITH_WARMUP:
|
||||
return schedule_func(optimizer,
|
||||
num_warmup_steps=num_warmup_steps,
|
||||
last_epoch=last_epoch)
|
||||
|
||||
# All other schedulers require `num_training_steps`
|
||||
if num_training_steps is None:
|
||||
raise ValueError(
|
||||
f"{name} requires `num_training_steps`, please provide that argument."
|
||||
)
|
||||
|
||||
if name == SchedulerType.COSINE_WITH_RESTARTS:
|
||||
return schedule_func(
|
||||
optimizer,
|
||||
num_warmup_steps=num_warmup_steps,
|
||||
num_training_steps=num_training_steps,
|
||||
num_cycles=num_cycles,
|
||||
last_epoch=last_epoch,
|
||||
)
|
||||
|
||||
if name == SchedulerType.POLYNOMIAL:
|
||||
return schedule_func(
|
||||
optimizer,
|
||||
num_warmup_steps=num_warmup_steps,
|
||||
num_training_steps=num_training_steps,
|
||||
power=power,
|
||||
last_epoch=last_epoch,
|
||||
)
|
||||
|
||||
if name == SchedulerType.COSINE_WITH_MIN_LR:
|
||||
return schedule_func(
|
||||
optimizer,
|
||||
num_warmup_steps=num_warmup_steps,
|
||||
num_training_steps=num_training_steps,
|
||||
min_lr_ratio=min_lr_ratio,
|
||||
last_epoch=last_epoch,
|
||||
)
|
||||
|
||||
return schedule_func(optimizer,
|
||||
num_warmup_steps=num_warmup_steps,
|
||||
num_training_steps=num_training_steps,
|
||||
last_epoch=last_epoch)
|
||||
|
||||
@@ -27,7 +27,7 @@ class WanI2VTrainingPipeline(TrainingPipeline):
|
||||
"""
|
||||
A training pipeline for Wan.
|
||||
"""
|
||||
_required_config_modules = ["scheduler", "transformer", "vae"]
|
||||
_required_config_modules = ["scheduler", "transformer"]
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
|
||||
self.modules["scheduler"] = FlowUniPCMultistepScheduler(
|
||||
|
||||
@@ -21,7 +21,6 @@ from fastvideo.pipelines import ForwardBatch, build_pipeline
|
||||
from fastvideo.platforms import current_platform
|
||||
from fastvideo.utils import (get_exception_traceback,
|
||||
kill_itself_when_parent_died)
|
||||
import fastvideo.envs as envs
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -141,10 +140,7 @@ class Worker:
|
||||
fastvideo_args = recv_rpc['kwargs']['fastvideo_args']
|
||||
output_batch = self.execute_forward(forward_batch,
|
||||
fastvideo_args)
|
||||
logging_info = None
|
||||
if envs.FASTVIDEO_STAGE_LOGGING:
|
||||
logging_info = output_batch.logging_info
|
||||
self.pipe.send({"output_batch": output_batch.output.cpu(), "logging_info": logging_info})
|
||||
self.pipe.send({"output_batch": output_batch.output.cpu()})
|
||||
elif method_name == 'set_lora_adapter':
|
||||
lora_nickname = recv_rpc['kwargs']['lora_nickname']
|
||||
lora_path = recv_rpc['kwargs']['lora_path']
|
||||
|
||||
@@ -15,7 +15,6 @@ from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.worker.executor import Executor
|
||||
from fastvideo.worker.gpu_worker import run_worker_process
|
||||
import fastvideo.envs as envs
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -82,21 +81,7 @@ class MultiprocExecutor(Executor):
|
||||
"forward_batch": forward_batch,
|
||||
"fastvideo_args": fastvideo_args
|
||||
})
|
||||
output = responses[0]["output_batch"]
|
||||
|
||||
logging_info = None
|
||||
if envs.FASTVIDEO_STAGE_LOGGING:
|
||||
logging_info = responses[0]["logging_info"]
|
||||
else:
|
||||
logging_info = None
|
||||
|
||||
result_batch = ForwardBatch(
|
||||
data_type=forward_batch.data_type,
|
||||
output=output,
|
||||
logging_info=logging_info
|
||||
)
|
||||
|
||||
return result_batch
|
||||
return cast(ForwardBatch, responses[0]["output_batch"])
|
||||
|
||||
def set_lora_adapter(self,
|
||||
lora_nickname: str,
|
||||
|
||||