Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8a1d39de3 | ||
|
|
b4edcceb90 | ||
|
|
464f6abff5 | ||
|
|
def36d9973 | ||
|
|
5939883d31 | ||
|
|
7188170277 | ||
|
|
b7f69c2c1d | ||
|
|
23a4531491 | ||
|
|
7d52ad0118 | ||
|
|
4d7bf35fa3 | ||
|
|
a6a9c9ca07 | ||
|
|
f4704847c2 | ||
|
|
d9c996310b | ||
|
|
d6651afd2e | ||
|
|
cf67618cad | ||
|
|
2f0a2b3c57 | ||
|
|
e7748d9952 | ||
|
|
8eb3140b2f | ||
|
|
d6ddcea682 | ||
|
|
3559ba2377 | ||
|
|
61e63ea0d7 | ||
|
|
4ce4ac4734 | ||
|
|
e7f6db9bd1 | ||
|
|
d83f45a6a0 | ||
|
|
dd91542cd1 | ||
|
|
581e8115fe | ||
|
|
dea69cf651 | ||
|
|
60ac6537df | ||
|
|
5285116e73 |
@@ -61,7 +61,7 @@ steps:
|
||||
- "pyproject.toml"
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 45m .buildkite/scripts/pr_test.sh"
|
||||
command: "timeout 60m .buildkite/scripts/pr_test.sh"
|
||||
label: "SSIM Tests"
|
||||
env:
|
||||
- TEST_TYPE=ssim
|
||||
@@ -76,7 +76,7 @@ steps:
|
||||
- "pyproject.toml"
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
command: "timeout 20m .buildkite/scripts/pr_test.sh"
|
||||
label: "LoRA Inference Tests"
|
||||
env:
|
||||
- TEST_TYPE=inference_lora
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
<div align="center">
|
||||
<img src=assets/logos/logo.svg width="30%"/>
|
||||
</div>
|
||||
**FastVideo is a unified post-training and inference framework for accelerated video generation.**
|
||||
|
||||
FastVideo features an end-to-end unified pipeline for accelerating diffusion models, starting from data preprocessing to model training, finetuning, distillation, and inference. FastVideo is designed to be modular and extensible, allowing users to easily add new optimizations and techniques. Whether it is training-free optimizations or post-training optimizations, FastVideo has you covered.
|
||||
|
||||
<p align="center">
|
||||
| 🕹️ <a href="https://fastwan.fastvideo.org/"<b>Online Demo</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo"><b>Documentation</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start/"><b> Quick Start</b></a> | 🤗 <a href="https://huggingface.co/collections/FastVideo/fastwan-6886a305d9799c8cd1496408" target="_blank"><b>FastWan</b></a> | 🟣💬 <a href="https://join.slack.com/t/fastvideo/shared_invite/zt-3f4lao1uq-u~Ipx6Lt4J27AlD2y~IdLQ" target="_blank"> <b>Slack</b> </a> | 🟣💬 <a href="https://ibb.co/c7g1qdD" target="_blank"> <b> WeChat </b> </a> |
|
||||
| <a href="https://hao-ai-lab.github.io/FastVideo"><b>Documentation</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start/"><b> Quick Start</b></a> | <a href="https://github.com/hao-ai-lab/FastVideo/discussions/982" target="_blank"><b>Weekly Dev Meeting</b></a> | 🟣💬 <a href="https://join.slack.com/t/fastvideo/shared_invite/zt-3f4lao1uq-u~Ipx6Lt4J27AlD2y~IdLQ" target="_blank"> <b>Slack</b> </a> | 🟣💬 <a href="https://ibb.co/sv3MMKyv" target="_blank"> <b> WeChat </b> </a> |
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<img src=assets/fastwan.png width="90%"/>
|
||||
</div>
|
||||
**FastVideo is a unified post-training and inference framework for accelerated video generation.**
|
||||
|
||||
## NEWS
|
||||
- ```2025/11/19```: Release [CausalWan2.2 I2V A14B Preview](https://huggingface.co/FastVideo/CausalWan2.2-I2V-A14B-Preview-Diffusers) models, [Blog](https://hao-ai-lab.github.io/blogs/fastvideo_causalwan_preview/) and [Inference Code!](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_self_forcing_causal_wan2_2_i2v.py)
|
||||
- ```2025/08/04```: Release [FastWan](https://hao-ai-lab.github.io/FastVideo/distillation/dmd) models and [Sparse-Distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/).
|
||||
|
||||
<details>
|
||||
<summary>More</summary>
|
||||
|
||||
- ```2025/06/14```: Release finetuning and inference code for [VSA](https://arxiv.org/pdf/2505.13389)
|
||||
- ```2025/04/24```: [FastVideo V1](https://hao-ai-lab.github.io/blogs/fastvideo/) is released!
|
||||
- ```2025/02/18```: Release the inference code for [Sliding Tile Attention](https://hao-ai-lab.github.io/blogs/sta/).
|
||||
|
||||
</details>
|
||||
|
||||
## Key Features
|
||||
|
||||
FastVideo has the following features:
|
||||
- End-to-end post-training support:
|
||||
- [Sparse distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/) for Wan2.1 and Wan2.2 to achineve >50x denoising speedup
|
||||
- Data preprocessing pipeline for video data
|
||||
- End-to-end post-training support for bidirectional and autoregressive models:
|
||||
- Support full finetuning and LoRA finetuning for state-of-the-art open video DiTs
|
||||
- Scalable training with FSDP2, sequence parallelism, and selective activation checkpointing, with near linear scaling to 64 GPUs
|
||||
- Data preprocessing pipeline for video, image, and text data
|
||||
- Distribution Matching Distillation (DMD2) stepwise distillation.
|
||||
- Sparse attention with [Video Sparse Attention](https://arxiv.org/pdf/2505.13389)
|
||||
- [Sparse distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/) to achineve >50x denoising speedup
|
||||
- Scalable training with FSDP2, sequence parallelism, and selective activation checkpointing.
|
||||
- Causal distillation through Self-Forcing
|
||||
- See this [page](https://hao-ai-lab.github.io/FastVideo/training/overview/) for full list of supported models and recipes.
|
||||
- State-of-the-art performance optimizations for inference
|
||||
- [Video Sparse Attention](https://arxiv.org/pdf/2505.13389)
|
||||
- [Sliding Tile Attention](https://arxiv.org/pdf/2502.04507)
|
||||
- [TeaCache](https://arxiv.org/pdf/2411.19108)
|
||||
- [Sage Attention](https://arxiv.org/abs/2410.02367)
|
||||
- Sequence Parallelism for distributed inference
|
||||
- Multiple state-of-the-art attention backends
|
||||
- User-friendly CLI and Python API
|
||||
- See this [page](https://hao-ai-lab.github.io/FastVideo/inference/optimizations/) for full list of supported optimizations.
|
||||
- Diverse hardware and OS support
|
||||
- Support H100, A100, 4090
|
||||
- Support Linux, Windows, MacOS
|
||||
- See this [page](https://hao-ai-lab.github.io/FastVideo/inference/hardware_support/) for full list of supported hardware and OS.
|
||||
|
||||
## Getting Started
|
||||
We recommend using an environment manager such as `Conda` to create a clean environment:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 490 KiB |
Binary file not shown.
@@ -50,21 +50,25 @@ class MyNewAttnBackend(AttentionBackend):
|
||||
FastVideo uses a `ForwardContext` to pass global metadata (like current timestep, batch info, or custom attention configurations) to attention backends without changing the `forward` signature of every layer. **This is optional and only required if your backend needs dynamic per-step information.**
|
||||
|
||||
To use this:
|
||||
|
||||
1. **Set Context**: In your pipeline or generation loop, use the `set_forward_context` context manager.
|
||||
2. **Access Context**: Inside your attention backend, use `get_forward_context()`.
|
||||
|
||||
See `docs/attention/sta/index.md` (Sliding Tile Attention) for an example of how complex configuration (window sizes) is passed this way.
|
||||
See [`docs/attention/sta/index.md`](../sta/index.md) (Sliding Tile Attention) for an example of how complex configuration (window sizes) is passed this way.
|
||||
|
||||
## 3. Adding Compiled Kernels (C++/CUDA)
|
||||
|
||||
If your backend requires custom CUDA kernels, you need to add them to the `fastvideo-kernel` package.
|
||||
|
||||
### A. Add Source Files
|
||||
|
||||
Place your kernel implementation files in `fastvideo-kernel/csrc/attention/`.
|
||||
|
||||
* `mynew_attn.cu` (CUDA implementation)
|
||||
* `mynew_attn.h` (Optional headers)
|
||||
|
||||
### B. Register in Extension
|
||||
|
||||
Update `fastvideo-kernel/csrc/common_extension.cpp` to expose your function to Python.
|
||||
|
||||
```cpp
|
||||
@@ -84,6 +88,7 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
```
|
||||
|
||||
### C. Update CMakeLists.txt
|
||||
|
||||
Update `fastvideo-kernel/CMakeLists.txt` to compile your new files.
|
||||
|
||||
**Case 1: General CUDA Kernel (Runs on all GPUs)**
|
||||
@@ -111,6 +116,7 @@ endif()
|
||||
```
|
||||
|
||||
### D. Expose in Python Ops
|
||||
|
||||
Update `fastvideo-kernel/python/fastvideo_kernel/ops.py` to make the function importable and handle fallbacks gracefully.
|
||||
|
||||
```python
|
||||
@@ -133,6 +139,7 @@ def my_compiled_attn_func(q, k, v):
|
||||
```
|
||||
|
||||
### E. Expose in Package Init
|
||||
|
||||
Update `fastvideo-kernel/python/fastvideo_kernel/__init__.py` to export the function.
|
||||
|
||||
```python
|
||||
|
||||
@@ -41,7 +41,7 @@ Clone the repository and build the kernel:
|
||||
|
||||
```bash
|
||||
# Clone recursively to get ThunderKittens submodule
|
||||
git clone --recursive https://github.com/hao-ai-lab/FastVideo.git
|
||||
git clone https://github.com/hao-ai-lab/FastVideo.git
|
||||
cd FastVideo/fastvideo-kernel
|
||||
|
||||
# Build and install
|
||||
|
||||
+35
-14
@@ -4,25 +4,26 @@ This document outlines FastVideo's architecture for developers interested in fra
|
||||
|
||||
## Table of Contents - Directory Structure and Files
|
||||
|
||||
- [`fastvideo/pipelines/`](#design-pipeline-system) - Core diffusion pipeline components
|
||||
- [`fastvideo/models/`](#design-model-components) - Model implementations
|
||||
- [`dits/`](#design-transformer-models) - Transformer-based diffusion models
|
||||
- [`vaes/`](#design-vae-variational-auto-encoder) - Variational autoencoders
|
||||
- [`encoders/`](#design-text-and-image-encoders) - Text and image encoders
|
||||
- [`schedulers/`](#design-schedulers) - Diffusion schedulers
|
||||
- [`fastvideo/attention/`](#design-optimized-attention) - Optimized attention implementations
|
||||
- [`fastvideo/distributed/`](#design-distributed-processing) - Distributed computing utilities
|
||||
- [`fastvideo/layers/`](#design-tensor-parallelism) - Custom neural network layers
|
||||
- [`fastvideo/platforms/`](#design-platforms) - Hardware platform abstractions
|
||||
- [`fastvideo/worker/`](#design-executor-and-worker-abstractions) - Multi-GPU process management
|
||||
- [`fastvideo/fastvideo_args.py`](#design-fastvideo-args) - Argument handling
|
||||
- [`fastvideo/forward_context.py`](#design-forwardcontext) - Forward pass context management
|
||||
- [`fastvideo/pipelines/`](#pipeline-system) - Core diffusion pipeline components
|
||||
- [`fastvideo/models/`](#model-components) - Model implementations
|
||||
- [`dits/`](#transformer-models) - Transformer-based diffusion models
|
||||
- [`vaes/`](#vae-variational-auto-encoder) - Variational autoencoders
|
||||
- [`encoders/`](#text-and-image-encoders) - Text and image encoders
|
||||
- [`schedulers/`](#schedulers) - Diffusion schedulers
|
||||
- [`fastvideo/attention/`](#optimized-attention) - Optimized attention implementations
|
||||
- [`fastvideo/distributed/`](#distributed-processing) - Distributed computing utilities
|
||||
- [`fastvideo/layers/`](#tensor-parallelism) - Custom neural network layers
|
||||
- [`fastvideo/platforms/`](#platforms) - Hardware platform abstractions
|
||||
- [`fastvideo/worker/`](#executor-and-worker-system) - Multi-GPU process management
|
||||
- [`fastvideo/fastvideo_args.py`](#fastvideoargs) - Argument handling
|
||||
- [`fastvideo/forward_context.py`](#forward-context-management) - Forward pass context management
|
||||
- `fastvideo/utils.py` - Utility functions
|
||||
- [`fastvideo/logger.py`](#design-logger) - Logging infrastructure
|
||||
- [`fastvideo/logger.py`](#logger) - Logging infrastructure
|
||||
|
||||
## Core Architecture
|
||||
|
||||
FastVideo separates model components from execution logic with these principles:
|
||||
|
||||
- **Component Isolation**: Models (encoders, VAEs, transformers) are isolated from execution (pipelines, stages, distributed processing)
|
||||
- **Modular Design**: Components can be independently replaced
|
||||
- **Distributed Execution**: Supports various parallelism strategies (Tensor, Sequence)
|
||||
@@ -34,12 +35,14 @@ FastVideo separates model components from execution logic with these principles:
|
||||
The `FastVideoArgs` class in `fastvideo/fastvideo_args.py` serves as the central configuration system for FastVideo. It contains all parameters needed to control model loading, inference configuration, performance optimization settings, and more.
|
||||
|
||||
Key features include:
|
||||
|
||||
- **Command-line Interface**: Automatic conversion between CLI arguments and dataclass fields
|
||||
- **Configuration Groups**: Organized by functional areas (model loading, video params, optimization settings)
|
||||
- **Context Management**: Global access to current settings via `get_current_fastvideo_args()`
|
||||
- **Parameter Validation**: Ensures valid combinations of settings
|
||||
|
||||
Common configuration areas:
|
||||
|
||||
- **Model paths and loading options**: `model_path`, `trust_remote_code`, `revision`
|
||||
- **Distributed execution settings**: `num_gpus`, `tp_size`, `sp_size`
|
||||
- **Video generation parameters**: `height`, `width`, `num_frames`, `num_inference_steps`
|
||||
@@ -90,7 +93,9 @@ class MyCustomPipeline(ComposedPipelineBase):
|
||||
```
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
Each stage handles a specific diffusion process component:
|
||||
|
||||
- **Input Validation**: Parameter verification
|
||||
- **Text Encoding**: CLIP, LLaMA, or T5-based encoding
|
||||
- **Image Encoding**: Image input processing
|
||||
@@ -133,6 +138,7 @@ Transformer networks perform the actual denoising during diffusion:
|
||||
- `HunyuanVideoTransformer3DModel`
|
||||
|
||||
Features include:
|
||||
|
||||
- Text/image conditioning
|
||||
- Standardized interface for model-specific optimizations
|
||||
|
||||
@@ -161,6 +167,7 @@ VAEs handle conversion between pixel space and latent space:
|
||||
These models compress image/video data to a more efficient latent representation (typically 4x-8x smaller in each dimension).
|
||||
|
||||
FastVideo's VAE implementations include:
|
||||
|
||||
- Efficient video batch processing
|
||||
- Memory optimization
|
||||
- Optional tiling for large frames
|
||||
@@ -179,6 +186,7 @@ Encoders process conditioning inputs into embeddings:
|
||||
- `CLIPVisionModel`
|
||||
|
||||
FastVideo implements optimizations such as:
|
||||
|
||||
- Vocab parallelism for distributed processing
|
||||
- Caching for common prompts
|
||||
- Precision-tuned computation
|
||||
@@ -193,6 +201,7 @@ Schedulers manage the diffusion sampling process:
|
||||
- `FlowMatchEulerDiscreteScheduler`
|
||||
|
||||
These components control:
|
||||
|
||||
- Diffusion timestep sequences
|
||||
- Noise prediction to latent update conversions
|
||||
- Quality/speed trade-offs
|
||||
@@ -219,7 +228,9 @@ This diagram shows how models are discovered, validated, and loaded across entry
|
||||
The `fastvideo/attention/` directory contains optimized attention implementations crucial for efficient video diffusion:
|
||||
|
||||
### Attention Backends
|
||||
|
||||
Multiple implementations with automatic selection:
|
||||
|
||||
- **FLASH_ATTN**: Optimized for supporting hardware
|
||||
- **TORCH_SDPA**: Built-in PyTorch scaled dot-product attention
|
||||
- **SLIDING_TILE_ATTN**: For very long sequences
|
||||
@@ -240,7 +251,9 @@ self.attn = LocalAttention(
|
||||

|
||||
|
||||
### Attention Patterns
|
||||
|
||||
Supports various patterns with memory optimization techniques:
|
||||
|
||||
- **Cross/Self/Temporal/Global-Local Attention**
|
||||
- Chunking, progressive computation, optimized masking
|
||||
|
||||
@@ -296,6 +309,7 @@ self.attn = DistributedAttention(
|
||||
```
|
||||
|
||||
### Communication Primitives
|
||||
|
||||
Efficient distributed operations via AllGather, AllReduce, and synchronization mechanisms.
|
||||
|
||||
Efficient communication primitives minimize distributed overhead:
|
||||
@@ -314,6 +328,7 @@ Defined in `fastvideo/forward_context.py`, `ForwardContext` manages execution-sp
|
||||
- **Profiling Data**: Potential hooks for performance metrics collection
|
||||
|
||||
This context-based approach enables:
|
||||
|
||||
- Dynamic optimization based on execution state (e.g., attention backend selection)
|
||||
- Step-specific customizations within model components
|
||||
|
||||
@@ -339,12 +354,14 @@ FastVideo implements a flexible execution model for distributed processing:
|
||||
- **GPU Workers**: Handle actual model execution on individual GPUs
|
||||
|
||||
The MultiProcExecutor implementation:
|
||||
|
||||
1. Spawns worker processes for each GPU
|
||||
2. Establishes communication channels via pipes
|
||||
3. Coordinates distributed operations across workers
|
||||
4. Handles graceful startup and shutdown of the process group
|
||||
|
||||
Each GPU worker:
|
||||
|
||||
1. Initializes the distributed environment
|
||||
2. Builds the pipeline for the specified model
|
||||
3. Executes requested operations on its assigned GPU
|
||||
@@ -359,11 +376,13 @@ The `fastvideo/platforms/` directory provides hardware platform abstractions tha
|
||||
### Platform Abstraction
|
||||
|
||||
FastVideo's platform abstraction layer enables:
|
||||
|
||||
- **Hardware Detection**: Automatic detection of available hardware
|
||||
- **Backend Selection**: Appropriate selection of compute kernels
|
||||
- **Memory Management**: Efficient utilization of hardware-specific memory features
|
||||
|
||||
The primary components include:
|
||||
|
||||
- **Platform Interface**: Defines the common API for all platform implementations
|
||||
- **CUDA Platform**: Optimized implementation for NVIDIA GPUs
|
||||
- **Backend Enum**: Used throughout the codebase for feature selection
|
||||
@@ -383,6 +402,7 @@ else:
|
||||
The platform system is designed to be extensible for future hardware targets.
|
||||
|
||||
## Logger
|
||||
|
||||
See [PR](https://github.com/hao-ai-lab/FastVideo/pull/356)
|
||||
|
||||
*TODO*: (help wanted) Add an environment variable that disables process-aware logging.
|
||||
@@ -397,6 +417,7 @@ If you're a new contributor, here are some common areas to explore:
|
||||
4. **Hardware support**: Extend the `platforms` module for new hardware targets
|
||||
|
||||
When adding code, follow these practices:
|
||||
|
||||
- Use type hints for better code readability
|
||||
- Add appropriate docstrings
|
||||
- Maintain the separation between model components and execution logic
|
||||
|
||||
@@ -13,7 +13,7 @@ We provide two distilled models:
|
||||
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).
|
||||
|
||||
## ⚙️ Inference
|
||||
First install [VSA](https://hao-ai-lab.github.io/FastVideo/video_sparse_attention/installation). Set `MODEL_BASE` to your own model path and run:
|
||||
First install [VSA](../attention/vsa/index.md). Set `MODEL_BASE` to your own model path and run:
|
||||
|
||||
```bash
|
||||
bash scripts/inference/v1_inference_wan_dmd.sh
|
||||
|
||||
@@ -11,15 +11,13 @@ FastVideo supports the following hardware platforms:
|
||||
### Using pip
|
||||
|
||||
```bash
|
||||
# Create and activate a new conda environment
|
||||
conda create -n fastvideo python=3.12
|
||||
conda activate fastvideo
|
||||
|
||||
pip install fastvideo
|
||||
```
|
||||
|
||||
### Using conda
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge fastvideo
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
```bash
|
||||
@@ -28,6 +26,12 @@ cd FastVideo
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Also optionally install flash-attn:
|
||||
|
||||
```bash
|
||||
pip install flash-attn --no-build-isolation
|
||||
```
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
- **NVIDIA GPUs**: CUDA 11.8+ with compute capability 7.0+
|
||||
@@ -38,4 +42,4 @@ pip install -e .
|
||||
|
||||
- [Quick Start Guide](quick_start.md) - Get started with your first video generation
|
||||
- [Configuration](../inference/configuration.md) - Learn about configuration options
|
||||
- [Examples](../inference/examples/) - Explore example scripts and notebooks
|
||||
- [Examples](../inference/examples/examples_inference_index.md) - Explore example scripts and notebooks
|
||||
|
||||
@@ -84,12 +84,12 @@ pip install flash-attn --no-build-isolation
|
||||
|
||||
## Set up using Docker
|
||||
We also have prebuilt docker images with FastVideo dependencies pre-installed:
|
||||
[Docker Images](#docker)
|
||||
[Docker Images](../../contributing/developer_env/docker.md)
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
If you're planning to contribute to FastVideo please see the following page:
|
||||
[Contributor Guide](#developer-overview)
|
||||
[Contributor Guide](../../contributing/overview.md)
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ uv pip install -e .
|
||||
## Development Environment Setup
|
||||
|
||||
If you're planning to contribute to FastVideo please see the following page:
|
||||
[Contributor Guide](#developer-overview)
|
||||
[Contributor Guide](../../contributing/overview.md)
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ conda activate fastvideo
|
||||
pip install fastvideo
|
||||
```
|
||||
|
||||
Also optionally install flash-attn:
|
||||
|
||||
```bash
|
||||
pip install flash-attn --no-build-isolation
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Text-to-Video Generation
|
||||
|
||||
@@ -45,6 +45,7 @@ FastVideo uses the Hugging Face Diffusers format for model organization:
|
||||
### Implementing Modules
|
||||
|
||||
Place new modules in the appropriate directories:
|
||||
|
||||
- Encoders: `fastvideo/models/encoders/`
|
||||
- VAEs: `fastvideo/models/vaes/`
|
||||
- Transformer models: `fastvideo/models/dits/`
|
||||
@@ -53,12 +54,15 @@ Place new modules in the appropriate directories:
|
||||
### Adapting Model Layers
|
||||
|
||||
#### Layer Replacements
|
||||
|
||||
Replace standard PyTorch layers with FastVideo optimized versions:
|
||||
|
||||
- nn.LayerNorm → fastvideo.layers.layernorm.RMSNorm
|
||||
- Embedding layers → fastvideo.layers.vocab_parallel_embedding modules
|
||||
- Activation functions → versions from fastvideo.layers.activation
|
||||
|
||||
#### Distributed Linear Layers
|
||||
|
||||
Use appropriate parallel layers for distribution:
|
||||
|
||||
```python
|
||||
@@ -91,6 +95,7 @@ self.out_proj = RowParallelLinear(
|
||||
```
|
||||
|
||||
### Attention Layers
|
||||
|
||||
Replace standard attention with FastVideo's optimized attention:
|
||||
|
||||
```python
|
||||
@@ -163,6 +168,10 @@ Pipelines are composed of stages, each handling a specific part of the diffusion
|
||||
- **DenoisingStage**: Performs denoising diffusion
|
||||
- **DecodingStage**: Converts latents to pixels
|
||||
|
||||
Note: `DenoisingStage` uses the unified denoising engine under the hood. You
|
||||
can inject a custom strategy via `strategy_cls` if a pipeline needs specialized
|
||||
denoising behavior.
|
||||
|
||||
### Creating Your Pipeline
|
||||
|
||||
```python
|
||||
@@ -225,7 +234,7 @@ class MyCustomPipeline(ComposedPipelineBase):
|
||||
stage_name="denoising_stage",
|
||||
stage=DenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler")
|
||||
scheduler=self.get_module("scheduler"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -240,6 +249,24 @@ class MyCustomPipeline(ComposedPipelineBase):
|
||||
EntryClass = MyCustomPipeline
|
||||
```
|
||||
|
||||
### Customizing Denoising Strategies
|
||||
|
||||
If your model requires a custom denoising loop, pass a strategy class:
|
||||
|
||||
```python
|
||||
from fastvideo.pipelines.stages.denoising_cosmos_strategy import (
|
||||
CosmosStrategy)
|
||||
|
||||
self.add_stage(
|
||||
stage_name="denoising_stage",
|
||||
stage=DenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler"),
|
||||
strategy_cls=CosmosStrategy,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Creating Custom Stages (Optional)
|
||||
|
||||
If existing stages don't meet your needs, create custom ones:
|
||||
@@ -304,6 +331,7 @@ EntryClass = [MyCustomPipeline, MyOtherPipeline]
|
||||
```
|
||||
|
||||
The registry will automatically:
|
||||
|
||||
1. Scan all packages under `fastvideo/pipelines/`
|
||||
2. Look for `EntryClass` variables
|
||||
3. Register pipelines using their class names as identifiers
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# FastVideo CLI Inference
|
||||
|
||||
The FastVideo CLI provides a quick way to access the FastVideo inference pipeline for video generation. For more advanced usage,
|
||||
see the Python interface [here](https://hao-ai-lab.github.io/FastVideo/inference/examples/basic.html).
|
||||
see the Python interface [here](examples/basic.md).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
|
||||
@@ -74,4 +74,4 @@ if __name__ == '__main__':
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
For configuring optimizations, please see our [optimizations guide](#inference-optimizations)
|
||||
For configuring optimizations, please see our [optimizations guide](optimizations.md)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This page contains step-by-step instructions to get you quickly started with video generation using FastVideo.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **OS**: Linux (Tested on Ubuntu 22.04+)
|
||||
- **Python**: 3.10-3.12
|
||||
- **CUDA**: 12.8
|
||||
@@ -21,9 +22,10 @@ conda activate fastvideo
|
||||
pip install fastvideo
|
||||
```
|
||||
|
||||
For advanced installation options, see the [Installation Guide](installation.md).
|
||||
For advanced installation options, see the [Installation Guide](../getting_started/installation.md).
|
||||
|
||||
## Generating Your First Video
|
||||
|
||||
Here's a minimal example to generate a video using the default settings. Create a file called `example.py` with the following code:
|
||||
|
||||
```python
|
||||
@@ -60,9 +62,10 @@ python example.py
|
||||
The generated video will be saved in the current directory under `my_videos/`
|
||||
|
||||
More inference example scripts can be found in `scripts/inference/`
|
||||
|
||||
## Available Models
|
||||
|
||||
Please see the [support matrix](#support-matrix) for the list of supported models and their available optimizations.
|
||||
Please see the [support matrix](support_matrix.md) for the list of supported models and their available optimizations.
|
||||
|
||||
## Image-to-Video Generation
|
||||
|
||||
@@ -96,20 +99,28 @@ if __name__ == '__main__':
|
||||
Common issues and their solutions:
|
||||
|
||||
### Out of Memory Errors
|
||||
|
||||
If you encounter CUDA out of memory errors:
|
||||
|
||||
- Reduce `num_frames` or video resolution
|
||||
- Enable memory optimization with `enable_model_cpu_offload`
|
||||
- Try a smaller model or use distilled versions
|
||||
- Use `num_gpus` > 1 if multiple GPUs are available
|
||||
- Try enabling FSDP inference with `use_fsdp_inference=True` (may slow down generation)
|
||||
- Try enabling DiT layerwise offload with `dit_layerwise_offload=True` (now only a few models support this, but may introduce less overhead than FSDP)
|
||||
|
||||
### Slow Generation
|
||||
|
||||
To speed up generation:
|
||||
|
||||
- Reduce `num_inference_steps` (20-30 is usually sufficient)
|
||||
- Use half precision (`fp16`) for the VAE
|
||||
- Use multiple GPUs if available
|
||||
|
||||
### Unexpected Results
|
||||
|
||||
If the generated video doesn't match your prompt:
|
||||
|
||||
- Try increasing `guidance_scale` (7.0-9.0 works well)
|
||||
- Make your prompt more detailed and specific
|
||||
- Experiment with different random seeds
|
||||
@@ -117,8 +128,8 @@ If the generated video doesn't match your prompt:
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [Advanced Inference Configurations](#inference-configuration)
|
||||
- Learn about using [Optimizations](#inference-optimizations)
|
||||
- See [Examples](../examples/examples_inference_index.md) for more usage scenarios
|
||||
- Learn about [Advanced Inference Configurations](configuration.md)
|
||||
- Learn about using [Optimizations](optimizations.md)
|
||||
- See [Examples](examples/examples_inference_index.md) for more usage scenarios
|
||||
- Join our [Community Discord](https://discord.gg/JA7cksDz86).
|
||||
- Join our [Community Slack](https://join.slack.com/t/fastvideo/shared_invite/zt-38u6p1jqe-yDI1QJOCEnbtkLoaI5bjZQ).
|
||||
|
||||
@@ -7,13 +7,13 @@ This page describes the various options for speeding up generation times in Fast
|
||||
|
||||
- Optimized Attention Backends
|
||||
|
||||
- [Flash Attention](#optimizations-flash)
|
||||
- [Sliding Tile Attention](#optimizations-sta)
|
||||
- [Sage Attention](#optimizations-sage)
|
||||
- [Sage Attention 3](#optimizations-sage3)
|
||||
- [Flash Attention](#flash-attention)
|
||||
- [Sliding Tile Attention](#sliding-tile-attention)
|
||||
- [Sage Attention](#sage-attention)
|
||||
- [Sage Attention 3](#sage-attention-3)
|
||||
|
||||
- Caching Techniques
|
||||
- [TeaCache](#optimizations-teacache)
|
||||
- [TeaCache](#teacache)
|
||||
|
||||
## Attention Backends
|
||||
|
||||
@@ -74,7 +74,7 @@ python setup.py install
|
||||
pip install st_attn==0.0.4
|
||||
```
|
||||
|
||||
Please see [this page](#sta-installation) for more installation instructions.
|
||||
Please see [this page](../attention/sta/index.md) for more installation instructions.
|
||||
|
||||
### Video Sparse Attention
|
||||
|
||||
@@ -85,7 +85,7 @@ git submodule update --init --recursive
|
||||
python setup_vsa.py install
|
||||
```
|
||||
|
||||
Please see [this page](#vsa-installation) for more installation instructions.
|
||||
Please see [this page](../attention/vsa/index.md) for more installation instructions.
|
||||
|
||||
### Sage Attention
|
||||
|
||||
|
||||
@@ -40,20 +40,26 @@ The `HuggingFace Model ID` can be directly pass to `from_pretrained()` methods a
|
||||
}
|
||||
</style>
|
||||
|
||||
| Model Name | HuggingFace Model ID | Resolutions | TeaCache | Sliding Tile Attn | Sage Attn | VSA |
|
||||
|------------|---------------------|-------------|----------|-------------------|-----------|-----|
|
||||
| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ✅ |
|
||||
| FastWan2.2 TI2V 5B Full Attn* | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P | ⭕ | ⭕ | ⭕ | ✅ |
|
||||
| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P | ⭕ | ⭕ | ✅ | ⭕ |
|
||||
| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P<br>720P | ❌ | ❌ | ✅ | ⭕ |
|
||||
| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P<br>720P | ❌ | ❌ | ✅ | ⭕ |
|
||||
| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p<br>544px960p | ❌ | ✅ | ✅ | ⭕ |
|
||||
| FastHunyuan | `FastVideo/FastHunyuan-diffusers` | 720px1280p<br>544px960p | ❌ | ✅ | ✅ | ⭕ |
|
||||
| Wan2.1 T2V 1.3B | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | 480P | ✅ | ✅* | ✅ | ⭕ |
|
||||
| Wan2.1 T2V 14B | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | 480P, 720P | ✅ | ✅* | ✅ | ⭕ |
|
||||
| Wan2.1 I2V 480P | `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` | 480P | ✅ | ✅* | ✅ | ⭕ |
|
||||
| Wan2.1 I2V 720P | `Wan-AI/Wan2.1-I2V-14B-720P-Diffusers` | 720P | ✅ | ✅ | ✅ | ⭕ |
|
||||
| StepVideo T2V | `FastVideo/stepvideo-t2v-diffusers` | 768px768px204f<br>544px992px204f<br>544px992px136f | ❌ | ❌ | ✅ | ⭕ |
|
||||
| Model Name | HuggingFace Model ID | Resolutions | TeaCache | Sliding Tile Attn | Sage Attn | VSA | BSA |
|
||||
|------------|---------------------|-------------|----------|-------------------|-----------|-----|-----|
|
||||
| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ✅ | ⭕ |
|
||||
| FastWan2.2 TI2V 5B Full Attn* | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P | ⭕ | ⭕ | ⭕ | ✅ | ⭕ |
|
||||
| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P | ⭕ | ⭕ | ✅ | ⭕ | ⭕ |
|
||||
| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P<br>720P | ❌ | ❌ | ✅ | ⭕ | ⭕ |
|
||||
| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P<br>720P | ❌ | ❌ | ✅ | ⭕ | ⭕ |
|
||||
| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p<br>544px960p | ❌ | ✅ | ✅ | ⭕ | ⭕ |
|
||||
| FastHunyuan | `FastVideo/FastHunyuan-diffusers` | 720px1280p<br>544px960p | ❌ | ✅ | ✅ | ⭕ | ⭕ |
|
||||
| Wan2.1 T2V 1.3B | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | 480P | ✅ | ✅* | ✅ | ⭕ | ⭕ |
|
||||
| Wan2.1 T2V 14B | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | 480P, 720P | ✅ | ✅* | ✅ | ⭕ | ⭕ |
|
||||
| Wan2.1 I2V 480P | `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` | 480P | ✅ | ✅* | ✅ | ⭕ | ⭕ |
|
||||
| Wan2.1 I2V 720P | `Wan-AI/Wan2.1-I2V-14B-720P-Diffusers` | 720P | ✅ | ✅ | ✅ | ⭕ | ⭕ |
|
||||
| StepVideo T2V | `FastVideo/stepvideo-t2v-diffusers` | 768px768px204f<br>544px992px204f<br>544px992px136f | ❌ | ❌ | ✅ | ⭕ | ⭕ |
|
||||
| TurboWan2.1 T2V 1.3B | `loayrashid/TurboWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| TurboWan2.1 T2V 14B | `loayrashid/TurboWan2.1-T2V-14B-Diffusers` | 480P, 720P | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| LongCat T2V 13.6B | See note** | 480P<br>720P | ❌ | ❌ | ❌ | ⭕ | ✅ |
|
||||
| Matrix Game 2.0 Base | `FastVideo/Matrix-Game-2.0-Base-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| Matrix Game 2.0 GTA | `FastVideo/Matrix-Game-2.0-GTA-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| Matrix Game 2.0 TempleRun | `FastVideo/Matrix-Game-2.0-TempleRun-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
|
||||
**Note**: Wan2.2 TI2V 5B has some quality issues when performing I2V generation. We are working on fixing this issue.
|
||||
|
||||
@@ -64,3 +70,13 @@ The `HuggingFace Model ID` can be directly pass to `from_pretrained()` methods a
|
||||
|
||||
### Sliding Tile Attention
|
||||
- Currently only Hopper GPUs (H100s) are supported.
|
||||
|
||||
### TurboWan2.1 (TurboDiffusion)
|
||||
- Uses TurboDiffusionPipeline with RCM scheduler for 1-4 step generation
|
||||
- Requires SLA attention backend: `export FASTVIDEO_ATTENTION_BACKEND=SLA_ATTN`
|
||||
- Uses `guidance_scale=1.0` (no classifier-free guidance)
|
||||
|
||||
### Matrix Game 2.0
|
||||
- Image-to-video game world models with keyboard/mouse control input
|
||||
- Three variants available: Base (universal), GTA, and TempleRun
|
||||
- Each variant has different keyboard dimensions for control inputs
|
||||
|
||||
@@ -1,45 +1,130 @@
|
||||
# 🧱 Data Preprocessing
|
||||
|
||||
# 🧱 Data Preprocess
|
||||
To save GPU memory during training, FastVideo precomputes text embeddings and VAE latents. This eliminates the need to load the text encoder and VAE during training.
|
||||
|
||||
To save GPU memory, we precompute text embeddings and VAE latents to eliminate the need to load the text encoder and VAE during training.
|
||||
## Quick Start
|
||||
|
||||
We provide a sample dataset to help you get started. Download the source media using the following command:
|
||||
Download the sample dataset and run preprocessing:
|
||||
|
||||
```bash
|
||||
python scripts/huggingface/download_hf.py --repo_id=FastVideo/mini_i2v_dataset --local_dir=data/mini_i2v_dataset --repo_type=dataset
|
||||
# Download the crush-smol dataset
|
||||
python scripts/huggingface/download_hf.py \
|
||||
--repo_id "wlsaidhi/crush-smol-merged" \
|
||||
--local_dir "data/crush-smol" \
|
||||
--repo_type "dataset"
|
||||
|
||||
# Run preprocessing
|
||||
bash examples/training/finetune/wan_t2v_1.3B/crush_smol/preprocess_wan_data_t2v_new.sh
|
||||
```
|
||||
|
||||
The folder `crush-smol_raw/` contains raw videos and captions for testing preprocessing, while `crush-smol_preprocessed/` contains latents prepared for testing training.
|
||||
## Preprocessing Pipeline
|
||||
|
||||
To preprocess the dataset for fine-tuning or distillation, run:
|
||||
The new preprocessing pipeline supports multiple dataset formats and video loaders:
|
||||
|
||||
```
|
||||
bash scripts/preprocess/v1_preprocess_wan_data_t2v # for wan
|
||||
```bash
|
||||
GPU_NUM=2
|
||||
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
|
||||
DATASET_PATH="data/crush-smol/"
|
||||
OUTPUT_DIR="data/crush-smol_processed_t2v/"
|
||||
|
||||
torchrun --nproc_per_node=$GPU_NUM \
|
||||
-m fastvideo.pipelines.preprocess.v1_preprocessing_new \
|
||||
--model_path $MODEL_PATH \
|
||||
--mode preprocess \
|
||||
--workload_type t2v \
|
||||
--preprocess.video_loader_type torchvision \
|
||||
--preprocess.dataset_type merged \
|
||||
--preprocess.dataset_path $DATASET_PATH \
|
||||
--preprocess.dataset_output_dir $OUTPUT_DIR \
|
||||
--preprocess.preprocess_video_batch_size 2 \
|
||||
--preprocess.dataloader_num_workers 0 \
|
||||
--preprocess.max_height 480 \
|
||||
--preprocess.max_width 832 \
|
||||
--preprocess.num_frames 77 \
|
||||
--preprocess.train_fps 16 \
|
||||
--preprocess.samples_per_file 8 \
|
||||
--preprocess.flush_frequency 8 \
|
||||
--preprocess.video_length_tolerance_range 5
|
||||
```
|
||||
|
||||
## Process your own dataset
|
||||
### Key Parameters
|
||||
|
||||
If you wish to create your own dataset for finetuning or distillation, please refer `mini_i2v_dataset/crush-smol_raw/` to structure you video dataset in the following format:
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `--workload_type` | Task type: `t2v` (text-to-video) or `i2v` (image-to-video) |
|
||||
| `--preprocess.dataset_type` | Input format: `hf` (HuggingFace) or `merged` (local folder) |
|
||||
| `--preprocess.dataset_path` | Path to dataset (HF repo ID or local folder) |
|
||||
| `--preprocess.dataset_output_dir` | Output directory for Parquet files |
|
||||
| `--preprocess.video_loader_type` | Video decoder: `torchcodec` or `torchvision` |
|
||||
| `--preprocess.max_height` / `max_width` | Target resolution for videos |
|
||||
| `--preprocess.num_frames` | Number of frames to extract per video |
|
||||
| `--preprocess.train_fps` | Target FPS for frame extraction |
|
||||
|
||||
## Dataset Formats
|
||||
|
||||
### Merged Dataset (Local Folder)
|
||||
|
||||
Structure your dataset as follows:
|
||||
|
||||
```
|
||||
path_to_your_dataset_folder/
|
||||
your_dataset/
|
||||
├── videos/
|
||||
│ ├── video_001.mp4
|
||||
│ ├── video_002.mp4
|
||||
│ └── ...
|
||||
└── videos2caption.json
|
||||
```
|
||||
|
||||
The `videos2caption.json` maps video filenames to captions:
|
||||
|
||||
```json
|
||||
[
|
||||
{"path": "video_001.mp4", "cap": "A cat playing with yarn..."},
|
||||
{"path": "video_002.mp4", "cap": "Ocean waves at sunset..."}
|
||||
]
|
||||
```
|
||||
|
||||
### HuggingFace Dataset
|
||||
|
||||
Use `--preprocess.dataset_type hf` and point `--preprocess.dataset_path` to a HuggingFace dataset with `video` and `caption` columns.
|
||||
|
||||
## Creating Your Own Dataset
|
||||
|
||||
If you have raw videos and captions in separate files, generate the `videos2caption.json`:
|
||||
|
||||
```bash
|
||||
python scripts/dataset_preparation/prepare_json_file.py \
|
||||
--data_folder path/to/your_raw_data/ \
|
||||
--output path/to/output_folder
|
||||
```
|
||||
|
||||
Your raw data folder should contain:
|
||||
|
||||
```
|
||||
your_raw_data/
|
||||
├── videos/
|
||||
│ ├── 0.mp4
|
||||
│ ├── 1.mp4
|
||||
├── videos.txt
|
||||
└── prompt.txt
|
||||
│ └── ...
|
||||
├── videos.txt # list of video filenames
|
||||
└── prompt.txt # corresponding captions (one per line)
|
||||
```
|
||||
|
||||
To generate the `videos2caption.json` and `merge.txt`, run
|
||||
## Output Format
|
||||
|
||||
``` python
|
||||
python scripts/dataset_preparation/prepare_json_file.py --data_folder mini_i2v_dataset/crush-smol_raw/ --output your_output_folder
|
||||
```
|
||||
Preprocessing outputs Parquet files in the `combined_parquet_dataset/` subdirectory containing:
|
||||
|
||||
Adjust the `DATA_MERGE_PATH` and `OUTPUT_DIR` in `scripts/preprocess/v1_preprocess_****.sh` accordingly and run:
|
||||
- `vae_latent_bytes` — VAE-encoded video latent
|
||||
- `text_embedding_bytes` — text encoder output
|
||||
- `clip_feature_bytes` — CLIP image features (I2V only)
|
||||
- `first_frame_latent_bytes` — first frame latent (I2V only)
|
||||
- Metadata: shapes, dtypes, and sample identifiers
|
||||
|
||||
```
|
||||
bash scripts/preprocess/v1_preprocess_****.sh
|
||||
```
|
||||
## Examples
|
||||
|
||||
The preprocessed data will be put into the `OUTPUT_DIR` and the `videos2caption.json` can be used in finetune and distill scripts.
|
||||
See ready-to-run preprocessing scripts in the training examples:
|
||||
|
||||
- **T2V**: `examples/training/finetune/wan_t2v_1.3B/crush_smol/preprocess_wan_data_t2v_new.sh`
|
||||
- **I2V**: `examples/training/finetune/wan_i2v_14B_480p/crush_smol/preprocess_wan_data_i2v_new.sh`
|
||||
|
||||
**→ [Browse all training examples](examples/examples_training_index.md)**
|
||||
|
||||
+153
-55
@@ -1,78 +1,176 @@
|
||||
# 🧠 Finetuning
|
||||
|
||||
# 🧠 Finetune
|
||||
## ⚡ Full Finetune
|
||||
Ensure your data is prepared and preprocessed in the format specified in [data_preprocess.md](#v0-data-preprocess). For convenience, we also provide a mochi preprocessed Black Myth Wukong data that can be downloaded directly:
|
||||
This guide covers finetuning video diffusion models with FastVideo, including full finetuning and LoRA.
|
||||
|
||||
## Training Arguments
|
||||
|
||||
FastVideo training scripts use several argument groups:
|
||||
|
||||
### Training Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--max_train_steps` | Total training steps |
|
||||
| `--train_batch_size` | Batch size per GPU |
|
||||
| `--gradient_accumulation_steps` | Steps to accumulate before optimizer update |
|
||||
| `--num_latent_t` | Temporal latent dimension (reduce to save memory) |
|
||||
| `--num_height` / `--num_width` | Video resolution |
|
||||
| `--num_frames` | Number of frames per video |
|
||||
| `--output_dir` | Directory for checkpoints |
|
||||
|
||||
### Parallelism Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--num_gpus` | Total number of GPUs |
|
||||
| `--sp_size` | Sequence parallel size (increase to reduce memory per GPU) |
|
||||
| `--tp_size` | Tensor parallel size |
|
||||
| `--hsdp_replicate_dim` | HSDP replication dimension |
|
||||
| `--hsdp_shard_dim` | HSDP sharding dimension |
|
||||
|
||||
### Optimizer Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--learning_rate` | Base learning rate |
|
||||
| `--mixed_precision` | Precision mode (`bf16` recommended) |
|
||||
| `--weight_decay` | Weight decay for regularization |
|
||||
| `--max_grad_norm` | Gradient clipping threshold |
|
||||
|
||||
### Validation Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--log_validation` | Enable validation logging |
|
||||
| `--validation_dataset_file` | JSON file with validation prompts |
|
||||
| `--validation_steps` | Run validation every N steps |
|
||||
| `--validation_sampling_steps` | Inference steps for validation |
|
||||
| `--validation_guidance_scale` | CFG scale for validation |
|
||||
|
||||
## Full Finetuning
|
||||
|
||||
Full finetuning updates all model weights. This provides the best quality but requires more GPU memory.
|
||||
|
||||
```bash
|
||||
python scripts/huggingface/download_hf.py --repo_id=FastVideo/Mochi-Black-Myth --local_dir=data/Mochi-Black-Myth --repo_type=dataset
|
||||
# Example: Wan2.1 T2V 1.3B full finetune (4 GPUs)
|
||||
bash examples/training/finetune/wan_t2v_1.3B/crush_smol/finetune_t2v.sh
|
||||
```
|
||||
|
||||
Download the original model weights as specified in the [Distillation Section](../distillation/dmd.md):
|
||||
**Typical settings:**
|
||||
|
||||
Then you can run the finetune with:
|
||||
- Learning rate: `1e-5` to `5e-5`
|
||||
- Gradient checkpointing: `--enable_gradient_checkpointing_type "full"`
|
||||
- Memory scaling: Increase `--sp_size` or reduce `--num_latent_t` to fit in memory
|
||||
|
||||
```
|
||||
bash scripts/finetune/finetune_mochi.sh # for mochi
|
||||
```
|
||||
## LoRA Finetuning
|
||||
|
||||
**Note that for finetuning, we did not tune the hyperparameters in the provided script.**
|
||||
## ⚡ Finetune with VSA
|
||||
Follow [data_preprocess.md](#v0-data-preprocess) to get parquet files for preproccessed latent, and then run:
|
||||
LoRA (Low-Rank Adaptation) trains lightweight adapters while keeping the base model frozen. This significantly reduces memory usage and training time.
|
||||
|
||||
### LoRA-Specific Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--lora_training True` | Enable LoRA mode |
|
||||
| `--lora_rank` | Rank of LoRA adapters (16, 32, 64, 128) |
|
||||
|
||||
### Learning Rate for LoRA
|
||||
|
||||
**Important:** LoRA typically requires a **10–20× higher learning rate** than full finetuning because only the low-rank adapters are being trained while the base model is frozen.
|
||||
|
||||
| Training Mode | Recommended Learning Rate |
|
||||
|---------------|---------------------------|
|
||||
| Full finetune | `1e-5` to `5e-5` |
|
||||
| LoRA | `1e-4` to `2e-4` |
|
||||
|
||||
### Example LoRA Training
|
||||
|
||||
```bash
|
||||
bash scripts/finetune/finetune_v1_VSA.sh
|
||||
# Example: Wan2.1 T2V 1.3B LoRA finetune (1 GPU)
|
||||
bash examples/training/finetune/wan_t2v_1.3B/crush_smol/finetune_t2v_lora.sh
|
||||
```
|
||||
|
||||
## ⚡ Lora Finetune
|
||||
Key differences from full finetune:
|
||||
|
||||
Hunyuan supports Lora fine-tuning of videos up to 720p. Demos and prompts of Black-Myth-Wukong can be found in [here](https://huggingface.co/FastVideo/Hunyuan-Black-Myth-Wukong-lora-weight). You can download the Lora weight through:
|
||||
- Add `--lora_training True --lora_rank 32`
|
||||
- Use higher learning rate (10–20× full finetune)
|
||||
- Can run on fewer GPUs (even single GPU)
|
||||
- Outputs adapter weights instead of full model
|
||||
|
||||
## LoRA Extraction and Merging
|
||||
|
||||
FastVideo provides tools to extract LoRA adapters from finetuned models and merge them back.
|
||||
|
||||
### Extract LoRA Adapter
|
||||
|
||||
Extract a LoRA adapter by comparing a finetuned model to its base:
|
||||
|
||||
```bash
|
||||
python scripts/huggingface/download_hf.py --repo_id=FastVideo/Hunyuan-Black-Myth-Wukong-lora-weight --local_dir=data/Hunyuan-Black-Myth-Wukong-lora-weight --repo_type=model
|
||||
python scripts/lora_extraction/extract_lora.py \
|
||||
--base Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
|
||||
--ft path/to/your/finetuned_model \
|
||||
--out adapter_r32.safetensors \
|
||||
--rank 32
|
||||
```
|
||||
|
||||
### Minimum Hardware Requirement
|
||||
- 40 GB GPU memory each for 2 GPUs with lora.
|
||||
- 30 GB GPU memory each for 2 GPUs with CPU offload and lora.
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--base` | Base model (HuggingFace ID or local path) |
|
||||
| `--ft` | Finetuned model path |
|
||||
| `--out` | Output adapter file (.safetensors) |
|
||||
| `--rank` | LoRA rank (16, 32, 64, 128) |
|
||||
| `--full-rank` | Extract full-rank adapter (optional) |
|
||||
|
||||
Currently, both Mochi and Hunyuan models support Lora finetuning through diffusers. To generate personalized videos from your own dataset, you'll need to follow three main steps: dataset preparation, finetuning, and inference.
|
||||
### Merge LoRA Adapter
|
||||
|
||||
### Dataset Preparation
|
||||
We provide scripts to better help you get started to train on your own characters!
|
||||
You can run this to organize your dataset to get the videos2caption.json before preprocess. Specify your video folder and corresponding caption folder (caption files should be .txt files and have the same name with its video):
|
||||
|
||||
```
|
||||
python scripts/dataset_preparation/prepare_json_file.py --video_dir data/input_videos/ --prompt_dir data/captions/ --output_path data/output_folder/videos2caption.json --verbose
|
||||
```
|
||||
|
||||
Also, we provide script to resize your videos:
|
||||
|
||||
```
|
||||
python scripts/data_preprocess/resize_videos.py
|
||||
```
|
||||
|
||||
### Finetuning
|
||||
After basic dataset preparation and preprocess, you can start to finetune your model using Lora:
|
||||
|
||||
```
|
||||
bash scripts/finetune/finetune_hunyuan_hf_lora.sh
|
||||
```
|
||||
|
||||
### Inference
|
||||
For inference with Lora checkpoint, you can run the following scripts with additional parameter `--lora_checkpoint_dir`:
|
||||
|
||||
```
|
||||
bash scripts/inference/inference_hunyuan_hf.sh
|
||||
```
|
||||
|
||||
**We also provide scripts for Mochi in the same directory.**
|
||||
|
||||
### Finetune with Both Image and Video
|
||||
Our codebase support finetuning with both image and video.
|
||||
Merge an adapter back into a base model:
|
||||
|
||||
```bash
|
||||
bash scripts/finetune/finetune_hunyuan.sh
|
||||
bash scripts/finetune/finetune_mochi_lora_mix.sh
|
||||
python scripts/lora_extraction/merge_lora.py \
|
||||
--base Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
|
||||
--adapter adapter_r32.safetensors \
|
||||
--ft path/to/your/finetuned_model \
|
||||
--output merged_model
|
||||
```
|
||||
|
||||
For Image-Video Mixture Fine-tuning, make sure to enable the `--group_frame` option in your script.
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--base` | Base model path |
|
||||
| `--adapter` | LoRA adapter file |
|
||||
| `--ft` | Finetuned model (for config reference) |
|
||||
| `--output` | Output directory for merged model |
|
||||
|
||||
### Validate Merged Model
|
||||
|
||||
Compare the merged model against the original finetuned model:
|
||||
|
||||
```bash
|
||||
python scripts/lora_extraction/lora_inference_comparison.py \
|
||||
--base merged_model \
|
||||
--ft path/to/your/finetuned_model \
|
||||
--adapter NONE \
|
||||
--output-dir results \
|
||||
--prompt "A cat sitting on a windowsill" \
|
||||
--compute-ssim \
|
||||
--compute-lpips
|
||||
```
|
||||
|
||||
## Training Examples
|
||||
|
||||
Ready-to-run training scripts are available for multiple models:
|
||||
|
||||
**→ [Browse all training examples](examples/examples_training_index.md)**
|
||||
|
||||
| Model | Type | Example |
|
||||
|-------|------|---------|
|
||||
| Wan2.1 T2V 1.3B | T2V | `examples/training/finetune/wan_t2v_1.3B/crush_smol/` |
|
||||
| Wan2.1 I2V 14B | I2V | `examples/training/finetune/wan_i2v_14B_480p/crush_smol/` |
|
||||
| Wan2.1-Fun 1.3B InP | I2V | `examples/training/finetune/Wan2.1-Fun-1.3B-InP/crush_smol/` |
|
||||
| Wan2.1 VSA | T2V/I2V | `examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/` |
|
||||
|
||||
Each example includes:
|
||||
|
||||
- `download_dataset.sh` — download sample data
|
||||
- `preprocess_*.sh` — run preprocessing
|
||||
- `finetune_*.sh` — full finetune launcher
|
||||
- `finetune_*_lora.sh` — LoRA finetune launcher
|
||||
- `validation.json` — validation prompts
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Training Overview
|
||||
|
||||
FastVideo supports finetuning video diffusion models on custom datasets. This page explains what data you need and how to get started.
|
||||
|
||||
## Data Requirements
|
||||
|
||||
To save GPU memory during training, FastVideo precomputes embeddings and latents ahead of time. This eliminates the need to load the text encoder and VAE during training, significantly reducing memory usage.
|
||||
|
||||
### Text-to-Video (T2V) Finetuning
|
||||
|
||||
For T2V models, you need:
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| **Text embeddings** | Precomputed embeddings from the model's text encoder (e.g., T5 or LLaMA). Stored as numpy arrays in Parquet files. |
|
||||
| **Video latents** | VAE-encoded representations of your training videos. Each video is encoded into a compressed latent tensor. |
|
||||
|
||||
### Image-to-Video (I2V) Finetuning
|
||||
|
||||
For I2V models, you need everything from T2V plus additional image conditioning. Note that not all I2V architectures require encoded images—this depends on how the model conditions on the input frame. Wan2.1 and Wan2.2 A14B I2V models do require these additional components:
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| **Text embeddings** | Same as T2V—precomputed from the text encoder. |
|
||||
| **Video latents** | Same as T2V—VAE-encoded video representations. |
|
||||
| **First frame latent** | VAE-encoded representation of the first frame, used as the conditioning image. |
|
||||
| **CLIP features** | Image embeddings from a CLIP vision encoder for the conditioning frame. |
|
||||
|
||||
## Preprocessing
|
||||
|
||||
Before training, you need to preprocess your raw videos and captions into Parquet files containing precomputed latents and embeddings.
|
||||
|
||||
FastVideo supports two input formats:
|
||||
|
||||
- **HuggingFace datasets** — load directly from HF Hub or local HF datasets
|
||||
- **Merged datasets** — local folder with videos and a `videos2caption.json` metadata file
|
||||
|
||||
**→ See [Data Preprocessing](data_preprocess.md) for full details and examples.**
|
||||
|
||||
## Training Examples
|
||||
|
||||
Ready-to-run examples with preprocessing scripts, training launchers, and validation configs are available for multiple models and datasets:
|
||||
|
||||
**→ [Browse all training examples](examples/examples_training_index.md)**
|
||||
|
||||
Each example includes:
|
||||
|
||||
- `download_dataset.sh` — download sample data
|
||||
- `preprocess_*.sh` — run preprocessing
|
||||
- `finetune_*.sh` — launch training (full finetune or LoRA)
|
||||
- `validation.json` — validation prompts for checkpoints
|
||||
|
||||
## Training Methods
|
||||
|
||||
FastVideo supports several training approaches:
|
||||
|
||||
| Method | Use Case |
|
||||
|--------|----------|
|
||||
| **Full finetune** | Adapt entire model to a new domain or style |
|
||||
| **LoRA finetune** | Lightweight adaptation with frozen base weights |
|
||||
| **VSA finetune** | Finetune with Variable Sparse Attention for efficiency |
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Get started**: Pick an example from the [training examples index](examples/examples_training_index.md)
|
||||
2. **Prepare data**: Follow [data preprocessing](data_preprocess.md) for your own dataset
|
||||
3. **Run inference**: After training, see [inference examples](../inference/examples/examples_inference_index.md)
|
||||
@@ -0,0 +1,71 @@
|
||||
# LoRA Extraction and Merging
|
||||
|
||||
Tools for extracting and merging LoRA adapters for FastVideo models.
|
||||
|
||||
## Extract LoRA Adapter
|
||||
|
||||
```bash
|
||||
python scripts/lora_extraction/extract_lora.py \
|
||||
--base Wan-AI/Wan2.2-TI2V-5B-Diffusers \
|
||||
--ft FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers \
|
||||
--out adapter_r32.safetensors \
|
||||
--rank 32
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--base`: Base model (HuggingFace ID or local path)
|
||||
- `--ft`: Fine-tuned model (HuggingFace ID or local path)
|
||||
- `--out`: Output adapter file
|
||||
- `--rank`: LoRA rank (16, 32, 64, 128)
|
||||
- `--full-rank`: Extract full-rank adapter (optional)
|
||||
|
||||
## Merge Adapter
|
||||
|
||||
```bash
|
||||
python scripts/lora_extraction/merge_lora.py \
|
||||
--base Wan-AI/Wan2.2-TI2V-5B-Diffusers \
|
||||
--adapter adapter_r32.safetensors \
|
||||
--ft FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers \
|
||||
--output merged_model
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--base`: Base model (HuggingFace ID or local path)
|
||||
- `--adapter`: LoRA adapter file (.safetensors)
|
||||
- `--ft`: Fine-tuned model (for configuration)
|
||||
- `--output`: Output directory
|
||||
|
||||
## Validate Quality (Optional)
|
||||
|
||||
```bash
|
||||
python scripts/lora_extraction/lora_inference_comparison.py \
|
||||
--base merged_model \
|
||||
--ft FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers \
|
||||
--adapter NONE \
|
||||
--output-dir results \
|
||||
--prompt "A cat sitting on a windowsill" \
|
||||
--seed 42 \
|
||||
--height 480 \
|
||||
--width 480 \
|
||||
--num-frames 49 \
|
||||
--num-inference-steps 32 \
|
||||
--compute-ssim \
|
||||
--compute-lpips
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--base`: Merged model or base model path
|
||||
- `--ft`: Fine-tuned model (reference)
|
||||
- `--adapter`: Path to adapter or NONE
|
||||
- `--output-dir`: Output directory
|
||||
- `--prompt`: Text prompt (default: "A cat sitting on a windowsill")
|
||||
- `--seed`: Random seed (default: 42)
|
||||
- `--height`: Video height (default: 480)
|
||||
- `--width`: Video width (default: 832)
|
||||
- `--num-frames`: Number of frames (default: 49)
|
||||
- `--num-inference-steps`: Inference steps (default: 32)
|
||||
- `--compute-ssim`: Compute SSIM metric
|
||||
- `--compute-lpips`: Compute LPIPS metric
|
||||
@@ -0,0 +1,19 @@
|
||||
# Self-Forcing Distillation for SFWan2.1 T2V 1.3B
|
||||
|
||||
These scripts demonstrate self-forcing distillation (SFwan) for the causal Wan2.1 T2V 1.3B model. The workflow mirrors DMD2 while injecting self-forcing blocks so the student can autoregressively refine later frames.
|
||||
|
||||
## Run the recipe
|
||||
1. Download the preprocessed text-video dataset:
|
||||
```bash
|
||||
bash examples/distill/SFWan2.1-T2V/download_dataset.sh
|
||||
```
|
||||
2. (Optional) Regenerate parquet shards locally:
|
||||
```bash
|
||||
bash examples/distill/SFWan2.1-T2V/preprocess_data.sh
|
||||
```
|
||||
3. Launch self-forcing distillation with your cluster settings:
|
||||
```bash
|
||||
sbatch examples/distill/SFWan2.1-T2V/distill_dmd_t2v_1.3B.sh
|
||||
```
|
||||
|
||||
Update the dataset paths and wandb credentials inside the script before running on your environment.
|
||||
@@ -12,7 +12,7 @@ def main():
|
||||
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
model_name,
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
# Adjust these offload parameters if you have < 32GB of VRAM
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
|
||||
|
||||
@@ -12,7 +12,7 @@ def main():
|
||||
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
LongCat Image-to-Video (I2V) Example Script
|
||||
|
||||
This script demonstrates LongCat I2V inference using the FastVideo Python API.
|
||||
LongCat I2V takes an input image and generates a video from it.
|
||||
|
||||
It runs both basic generation (50 steps) and distill+refine generation
|
||||
(16 steps distill + 50 steps refinement to 720p with BSA).
|
||||
|
||||
Usage:
|
||||
python examples/inference/basic/basic_longcat_i2v.py
|
||||
|
||||
Note:
|
||||
Refinement uses 768x768 dimensions where latent (48x48) is divisible by 8,
|
||||
compatible with BSA chunks [4, 4, 8].
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
# Common prompts and settings matching the shell script examples
|
||||
PROMPT = (
|
||||
"A woman sits at a wooden table by the window in a cozy café. She reaches out "
|
||||
"with her right hand, picks up the white coffee cup from the saucer, and gently "
|
||||
"brings it to her lips to take a sip. After drinking, she places the cup back on "
|
||||
"the table and looks out the window, enjoying the peaceful atmosphere."
|
||||
)
|
||||
|
||||
NEGATIVE_PROMPT = (
|
||||
"Bright tones, overexposed, static, blurred details, subtitles, style, works, "
|
||||
"paintings, images, static, overall gray, worst quality, low quality, JPEG compression "
|
||||
"residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, "
|
||||
"deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, "
|
||||
"three legs, many people in the background, walking backwards"
|
||||
)
|
||||
|
||||
# Input image path
|
||||
IMAGE_PATH = "assets/girl.png"
|
||||
|
||||
SEED = 42
|
||||
|
||||
|
||||
def basic_generation():
|
||||
"""
|
||||
Run basic LongCat I2V generation (50 steps at 480p).
|
||||
|
||||
This uses the full 50-step denoising process for highest quality.
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("LongCat I2V: Basic Generation (50 steps, 480p)")
|
||||
print("=" * 60)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-I2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=False,
|
||||
)
|
||||
|
||||
output_path = "outputs_video/longcat_i2v_basic"
|
||||
|
||||
generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
image_path=IMAGE_PATH,
|
||||
output_path=output_path,
|
||||
save_video=True,
|
||||
height=480,
|
||||
width=480, # Square
|
||||
num_frames=93,
|
||||
num_inference_steps=50,
|
||||
fps=15,
|
||||
guidance_scale=4.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"\nBasic generation complete! Video saved to: {output_path}")
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
def distill_refine_generation():
|
||||
"""
|
||||
Run LongCat I2V with distill+refine pipeline (16 steps + refinement to 768p).
|
||||
|
||||
This uses the distilled LoRA for fast 480p generation (16 steps),
|
||||
then refines to 768p using the refinement LoRA with BSA enabled.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("LongCat I2V: Distill + Refine Pipeline")
|
||||
print("=" * 60)
|
||||
|
||||
# Stage 1: Distilled generation (16 steps at 480p)
|
||||
print("\n[Stage 1] Distilled generation (16 steps, 480p)")
|
||||
print("-" * 40)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-I2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=False,
|
||||
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
|
||||
lora_nickname="distilled",
|
||||
)
|
||||
|
||||
distill_output_path = "outputs_video/longcat_i2v_distill"
|
||||
|
||||
generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
image_path=IMAGE_PATH,
|
||||
output_path=distill_output_path,
|
||||
save_video=True,
|
||||
height=480,
|
||||
width=480, # Square
|
||||
num_frames=93,
|
||||
num_inference_steps=16,
|
||||
fps=15,
|
||||
guidance_scale=1.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"Distilled generation complete! Video saved to: {distill_output_path}")
|
||||
generator.shutdown()
|
||||
|
||||
# Stage 2: Refinement (480p -> 768p)
|
||||
print("\n[Stage 2] Refinement (480p -> 768p with BSA)")
|
||||
print("-" * 40)
|
||||
|
||||
# Find the actual saved video file from stage 1
|
||||
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
|
||||
if not video_files:
|
||||
raise FileNotFoundError(f"No video file found in {distill_output_path}")
|
||||
# Use the most recently created video file
|
||||
distill_video_path = max(video_files, key=os.path.getmtime)
|
||||
print(f"Using stage 1 video: {distill_video_path}")
|
||||
|
||||
# Create a new generator with refinement LoRA and BSA enabled
|
||||
# Note: Refinement uses the T2V model (not I2V) since it's upscaling the generated video
|
||||
# For BSA [4, 4, 8]: latent must be divisible by 8
|
||||
# 768x768: latent 48x48, 48%8=0 ✓
|
||||
refine_generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-T2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=True,
|
||||
bsa_sparsity=0.875,
|
||||
bsa_chunk_q=[4, 4, 4],
|
||||
bsa_chunk_k=[4, 4, 4],
|
||||
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
|
||||
lora_nickname="refinement",
|
||||
)
|
||||
|
||||
refine_output_path = "outputs_video/longcat_i2v_refine_720p"
|
||||
|
||||
refine_generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
output_path=refine_output_path,
|
||||
save_video=True,
|
||||
refine_from=distill_video_path,
|
||||
t_thresh=0.5,
|
||||
spatial_refine_only=False,
|
||||
num_cond_frames=0,
|
||||
height=720,
|
||||
width=720,
|
||||
num_inference_steps=50,
|
||||
fps=30,
|
||||
guidance_scale=1.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"Refinement complete! Video saved to: {refine_output_path}")
|
||||
refine_generator.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run both basic and distill+refine generation pipelines."""
|
||||
print("\n" + "=" * 60)
|
||||
print("LongCat Image-to-Video Example")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
# Run basic generation
|
||||
basic_generation()
|
||||
|
||||
# Run distill+refine pipeline
|
||||
distill_refine_generation()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All generations complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
LongCat Text-to-Video (T2V) Example Script
|
||||
|
||||
This script demonstrates LongCat T2V inference using the FastVideo Python API.
|
||||
It runs both basic generation (50 steps) and distill+refine generation
|
||||
(16 steps distill + 50 steps refinement to 720p).
|
||||
|
||||
Usage:
|
||||
python examples/inference/basic/basic_longcat_t2v.py
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
# Common prompts and settings matching the shell script examples
|
||||
PROMPT = (
|
||||
"In a realistic photography style, a white boy around seven or eight years old "
|
||||
"sits on a park bench, wearing a light blue T-shirt, denim shorts, and white sneakers. "
|
||||
"He holds an ice cream cone with vanilla and chocolate flavors, and beside him is a "
|
||||
"medium-sized golden Labrador. Smiling, the boy offers the ice cream to the dog, "
|
||||
"who eagerly licks it with its tongue. The sun is shining brightly, and the background "
|
||||
"features a green lawn and several tall trees, creating a warm and loving scene."
|
||||
)
|
||||
|
||||
NEGATIVE_PROMPT = (
|
||||
"Bright tones, overexposed, static, blurred details, subtitles, style, works, "
|
||||
"paintings, images, static, overall gray, worst quality, low quality, JPEG compression "
|
||||
"residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, "
|
||||
"deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, "
|
||||
"three legs, many people in the background, walking backwards"
|
||||
)
|
||||
|
||||
SEED = 42
|
||||
|
||||
|
||||
def basic_generation():
|
||||
"""
|
||||
Run basic LongCat T2V generation (50 steps at 480p).
|
||||
|
||||
This uses the full 50-step denoising process for highest quality.
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("LongCat T2V: Basic Generation (50 steps, 480p)")
|
||||
print("=" * 60)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-T2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=False,
|
||||
)
|
||||
|
||||
output_path = "outputs_video/longcat_t2v_basic"
|
||||
|
||||
generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
output_path=output_path,
|
||||
save_video=True,
|
||||
height=480,
|
||||
width=832,
|
||||
num_frames=93,
|
||||
num_inference_steps=50,
|
||||
fps=15,
|
||||
guidance_scale=4.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"\nBasic generation complete! Video saved to: {output_path}")
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
def distill_refine_generation():
|
||||
"""
|
||||
Run LongCat T2V with distill+refine pipeline (16 steps + refinement to 720p).
|
||||
|
||||
This uses the distilled LoRA for fast 480p generation (16 steps),
|
||||
then refines to 720p using the refinement LoRA with BSA enabled.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("LongCat T2V: Distill + Refine Pipeline")
|
||||
print("=" * 60)
|
||||
|
||||
# Stage 1: Distilled generation (16 steps at 480p)
|
||||
print("\n[Stage 1] Distilled generation (16 steps, 480p)")
|
||||
print("-" * 40)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-T2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=False,
|
||||
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
|
||||
lora_nickname="distilled",
|
||||
)
|
||||
|
||||
distill_output_path = "outputs_video/longcat_t2v_distill"
|
||||
|
||||
generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
output_path=distill_output_path,
|
||||
save_video=True,
|
||||
height=480,
|
||||
width=832,
|
||||
num_frames=93,
|
||||
num_inference_steps=16,
|
||||
fps=15,
|
||||
guidance_scale=1.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"Distilled generation complete! Video saved to: {distill_output_path}")
|
||||
generator.shutdown()
|
||||
|
||||
# Stage 2: Refinement (480p -> 720p)
|
||||
print("\n[Stage 2] Refinement (480p -> 720p with BSA)")
|
||||
print("-" * 40)
|
||||
|
||||
# Find the actual saved video file from stage 1
|
||||
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
|
||||
if not video_files:
|
||||
raise FileNotFoundError(f"No video file found in {distill_output_path}")
|
||||
# Use the most recently created video file
|
||||
distill_video_path = max(video_files, key=os.path.getmtime)
|
||||
print(f"Using stage 1 video: {distill_video_path}")
|
||||
|
||||
# Create a new generator with refinement LoRA and BSA enabled
|
||||
refine_generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-T2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=True,
|
||||
bsa_sparsity=0.875,
|
||||
bsa_chunk_q=[4, 4, 8],
|
||||
bsa_chunk_k=[4, 4, 8],
|
||||
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
|
||||
lora_nickname="refinement",
|
||||
)
|
||||
|
||||
refine_output_path = "outputs_video/longcat_t2v_refine_720p"
|
||||
|
||||
refine_generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
output_path=refine_output_path,
|
||||
save_video=True,
|
||||
refine_from=distill_video_path,
|
||||
t_thresh=0.5,
|
||||
spatial_refine_only=False,
|
||||
num_cond_frames=0,
|
||||
height=720,
|
||||
width=1280,
|
||||
num_inference_steps=50,
|
||||
fps=30,
|
||||
guidance_scale=1.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"Refinement complete! Video saved to: {refine_output_path}")
|
||||
refine_generator.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run both basic and distill+refine generation pipelines."""
|
||||
print("\n" + "=" * 60)
|
||||
print("LongCat Text-to-Video Example")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
# Run basic generation
|
||||
basic_generation()
|
||||
|
||||
# Run distill+refine pipeline
|
||||
distill_refine_generation()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All generations complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
LongCat Video Continuation (VC) Example Script
|
||||
|
||||
This script demonstrates LongCat VC inference using the FastVideo Python API.
|
||||
LongCat VC takes an input video and generates a continuation of it.
|
||||
|
||||
It runs both basic generation (50 steps) and distill+refine generation
|
||||
(16 steps distill + 50 steps refinement to 720p).
|
||||
|
||||
Usage:
|
||||
python examples/inference/basic/basic_longcat_vc.py
|
||||
|
||||
Prerequisites:
|
||||
- Ensure the input video exists at assets/motorcycle.mp4
|
||||
(or provide your own video)
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
# Common prompts and settings matching the shell script examples
|
||||
PROMPT = (
|
||||
"A person rides a motorcycle along a long, straight road that stretches between "
|
||||
"a body of water and a forested hillside. The rider steadily accelerates, keeping "
|
||||
"the motorcycle centered between the guardrails, while the scenery passes by on "
|
||||
"both sides. The video captures the journey from the rider's perspective, emphasizing "
|
||||
"the sense of motion and adventure."
|
||||
)
|
||||
|
||||
NEGATIVE_PROMPT = (
|
||||
"Bright tones, overexposed, static, blurred details, subtitles, style, works, "
|
||||
"paintings, images, static, overall gray, worst quality, low quality, JPEG compression "
|
||||
"residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, "
|
||||
"deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, "
|
||||
"three legs, many people in the background, walking backwards"
|
||||
)
|
||||
|
||||
# Input video path
|
||||
VIDEO_PATH = "assets/motorcycle.mp4"
|
||||
|
||||
# Number of conditioning frames from the input video
|
||||
NUM_COND_FRAMES = 13
|
||||
|
||||
SEED = 42
|
||||
|
||||
|
||||
def basic_generation():
|
||||
"""
|
||||
Run basic LongCat VC generation (50 steps at 480p).
|
||||
|
||||
This uses the full 50-step denoising process for highest quality.
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("LongCat VC: Basic Generation (50 steps, 480p)")
|
||||
print("=" * 60)
|
||||
|
||||
# Check if video exists
|
||||
if not os.path.exists(VIDEO_PATH):
|
||||
raise FileNotFoundError(
|
||||
f"Video not found at {VIDEO_PATH}. "
|
||||
"Please provide a valid video path."
|
||||
)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-VC-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=False,
|
||||
)
|
||||
|
||||
output_path = "outputs_video/longcat_vc_basic"
|
||||
|
||||
generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
video_path=VIDEO_PATH,
|
||||
num_cond_frames=NUM_COND_FRAMES,
|
||||
output_path=output_path,
|
||||
save_video=True,
|
||||
height=480,
|
||||
width=832,
|
||||
num_frames=93,
|
||||
num_inference_steps=50,
|
||||
fps=15,
|
||||
guidance_scale=4.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"\nBasic generation complete! Video saved to: {output_path}")
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
def distill_refine_generation():
|
||||
"""
|
||||
Run LongCat VC with distill+refine pipeline (16 steps + refinement to 720p).
|
||||
|
||||
This uses the distilled LoRA for fast 480p generation (16 steps),
|
||||
then refines to 720p using the refinement LoRA with BSA enabled.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("LongCat VC: Distill + Refine Pipeline")
|
||||
print("=" * 60)
|
||||
|
||||
# Check if video exists
|
||||
if not os.path.exists(VIDEO_PATH):
|
||||
raise FileNotFoundError(
|
||||
f"Video not found at {VIDEO_PATH}. "
|
||||
"Please provide a valid video path."
|
||||
)
|
||||
|
||||
# Stage 1: Distilled generation (16 steps at 480p)
|
||||
print("\n[Stage 1] Distilled generation (16 steps, 480p)")
|
||||
print("-" * 40)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-VC-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=False,
|
||||
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
|
||||
lora_nickname="distilled",
|
||||
)
|
||||
|
||||
distill_output_path = "outputs_video/longcat_vc_distill"
|
||||
|
||||
generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
video_path=VIDEO_PATH,
|
||||
num_cond_frames=NUM_COND_FRAMES,
|
||||
output_path=distill_output_path,
|
||||
save_video=True,
|
||||
height=480,
|
||||
width=832,
|
||||
num_frames=93,
|
||||
num_inference_steps=16,
|
||||
fps=15,
|
||||
guidance_scale=1.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"Distilled generation complete! Video saved to: {distill_output_path}")
|
||||
generator.shutdown()
|
||||
|
||||
# Stage 2: Refinement (480p -> 720p)
|
||||
print("\n[Stage 2] Refinement (480p -> 720p with BSA)")
|
||||
print("-" * 40)
|
||||
|
||||
# Find the actual saved video file from stage 1
|
||||
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
|
||||
if not video_files:
|
||||
raise FileNotFoundError(f"No video file found in {distill_output_path}")
|
||||
# Use the most recently created video file
|
||||
distill_video_path = max(video_files, key=os.path.getmtime)
|
||||
print(f"Using stage 1 video: {distill_video_path}")
|
||||
|
||||
# Create a new generator with refinement LoRA and BSA enabled
|
||||
# Note: Refinement uses the T2V model (not VC) since it's upscaling the generated video
|
||||
refine_generator = VideoGenerator.from_pretrained(
|
||||
"FastVideo/LongCat-Video-T2V-Diffusers",
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=False,
|
||||
enable_bsa=True,
|
||||
bsa_sparsity=0.875,
|
||||
bsa_chunk_q=[4, 4, 8],
|
||||
bsa_chunk_k=[4, 4, 8],
|
||||
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
|
||||
lora_nickname="refinement",
|
||||
)
|
||||
|
||||
refine_output_path = "outputs_video/longcat_vc_refine_720p"
|
||||
|
||||
refine_generator.generate_video(
|
||||
prompt=PROMPT,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
output_path=refine_output_path,
|
||||
save_video=True,
|
||||
refine_from=distill_video_path,
|
||||
t_thresh=0.5,
|
||||
spatial_refine_only=False,
|
||||
num_cond_frames=0, # For refinement, no conditioning frames
|
||||
height=720,
|
||||
width=1280,
|
||||
num_inference_steps=50,
|
||||
fps=30,
|
||||
guidance_scale=1.0,
|
||||
seed=SEED,
|
||||
)
|
||||
|
||||
print(f"Refinement complete! Video saved to: {refine_output_path}")
|
||||
refine_generator.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run both basic and distill+refine generation pipelines."""
|
||||
print("\n" + "=" * 60)
|
||||
print("LongCat Video Continuation Example")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
# Run basic generation
|
||||
basic_generation()
|
||||
|
||||
# Run distill+refine pipeline
|
||||
distill_refine_generation()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All generations complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def main():
|
||||
config["model_path"],
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator
|
||||
from fastvideo.models.dits.matrix_game.utils import get_current_action_async, expand_action_to_frames
|
||||
|
||||
import torch
|
||||
import asyncio
|
||||
|
||||
# Available variants: "base_distilled_model", "gta_distilled_model", "templerun_distilled_model"
|
||||
# Each variant has different keyboard_dim:
|
||||
# - base_distilled_model: keyboard_dim=4
|
||||
# - gta_distilled_model: keyboard_dim=2
|
||||
# - templerun_distilled_model: keyboard_dim=7 (keyboard only, no mouse)
|
||||
MODEL_VARIANT = "base_distilled_model"
|
||||
|
||||
# Variant-specific settings
|
||||
VARIANT_CONFIG = {
|
||||
"base_distilled_model": {
|
||||
"model_path": "FastVideo/Matrix-Game-2.0-Base-Diffusers",
|
||||
"keyboard_dim": 4,
|
||||
"mode": "universal",
|
||||
"image_url": "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-2/demo_images/universal/0000.png",
|
||||
},
|
||||
"gta_distilled_model": {
|
||||
"model_path": "FastVideo/Matrix-Game-2.0-GTA-Diffusers",
|
||||
"keyboard_dim": 2,
|
||||
"mode": "gta_drive",
|
||||
"image_url": "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-2/demo_images/gta_drive/0000.png",
|
||||
},
|
||||
"templerun_distilled_model": {
|
||||
"model_path": "FastVideo/Matrix-Game-2.0-TempleRun-Diffusers",
|
||||
"keyboard_dim": 7,
|
||||
"mode": "templerun",
|
||||
"image_url": "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-2/demo_images/temple_run/0000.png",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
OUTPUT_PATH = "video_samples_matrixgame2"
|
||||
async 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.
|
||||
config = VARIANT_CONFIG[MODEL_VARIANT]
|
||||
|
||||
generator = StreamingVideoGenerator.from_pretrained(
|
||||
config["model_path"],
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
vae_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,
|
||||
)
|
||||
|
||||
max_blocks = 50
|
||||
num_frames = 597
|
||||
actions = {
|
||||
"keyboard": torch.zeros((num_frames, config["keyboard_dim"])),
|
||||
"mouse": torch.zeros((num_frames, 2))
|
||||
}
|
||||
grid_sizes = torch.tensor([150, 44, 80])
|
||||
mode = config["mode"]
|
||||
|
||||
generator.reset(
|
||||
prompt="",
|
||||
image_path=config["image_url"],
|
||||
mouse_cond=actions["mouse"].unsqueeze(0),
|
||||
keyboard_cond=actions["keyboard"].unsqueeze(0),
|
||||
grid_sizes=grid_sizes,
|
||||
num_frames=num_frames,
|
||||
height=352,
|
||||
width=640,
|
||||
num_inference_steps=50,
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
)
|
||||
print("Initialization complete.")
|
||||
|
||||
for block_id in range(max_blocks):
|
||||
print(f"\n=== Block {block_id + 1}/{max_blocks} ===")
|
||||
|
||||
action = await get_current_action_async(mode)
|
||||
keyboard_cond, mouse_cond = expand_action_to_frames(action, 12)
|
||||
await generator.step_async(keyboard_cond, mouse_cond)
|
||||
|
||||
if (await asyncio.to_thread(input, "\nContinue? (y/n): ")).lower() == 'n':
|
||||
break
|
||||
|
||||
# Save final video
|
||||
generator.finalize()
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -13,7 +13,7 @@ def main():
|
||||
model_name,
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
text_encoder_cpu_offload=False,
|
||||
dit_cpu_offload=False,
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
"FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
dit_precision="fp32",
|
||||
vae_cpu_offload=False,
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
"rand0nmr/SFWan2.2-T2V-A14B-Diffusers",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import os
|
||||
|
||||
# Set SLA attention backend BEFORE fastvideo imports
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
OUTPUT_PATH = "video_samples_turbodiffusion"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# TurboDiffusion: 1-4 step video generation using RCM scheduler + SLA attention
|
||||
# FastVideo will automatically use TurboDiffusionPipeline when specified
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"loayrashid/TurboWan2.1-T2V-1.3B-Diffusers",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
|
||||
# set to false if using RTX 4090
|
||||
# pin_cpu_memory=False,
|
||||
)
|
||||
|
||||
# Generate videos with the same simple API, regardless of GPU count
|
||||
# TurboDiffusion defaults: guidance_scale=1.0 and num_inference_steps=4 (from config)
|
||||
prompt = (
|
||||
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
|
||||
"wide with interest. The playful yet serene atmosphere is complemented by soft "
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
video = generator.generate_video(
|
||||
prompt,
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
# Generate another video with a different prompt, without reloading the model!
|
||||
prompt2 = (
|
||||
"A majestic lion strides across the golden savanna, its powerful frame "
|
||||
"glistening under the warm afternoon sun. The tall grass ripples gently in "
|
||||
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
|
||||
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
|
||||
"cinematic."
|
||||
)
|
||||
video2 = generator.generate_video(
|
||||
prompt2,
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
|
||||
# Set SLA attention backend BEFORE fastvideo imports
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
OUTPUT_PATH = "video_samples_turbodiffusion_14B"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# TurboDiffusion 14B: 1-4 step video generation using RCM scheduler + SLA attention
|
||||
# FastVideo will automatically use TurboDiffusionPipeline when specified
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"loayrashid/TurboWan2.1-T2V-14B-Diffusers",
|
||||
# 14B model needs more GPUs
|
||||
num_gpus=2,
|
||||
)
|
||||
|
||||
prompt = (
|
||||
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
|
||||
"wide with interest. The playful yet serene atmosphere is complemented by soft "
|
||||
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
|
||||
)
|
||||
video = generator.generate_video(
|
||||
prompt,
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
# Generate another video with a different prompt, without reloading the model!
|
||||
prompt2 = (
|
||||
"A majestic lion strides across the golden savanna, its powerful frame "
|
||||
"glistening under the warm afternoon sun. The tall grass ripples gently in "
|
||||
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
|
||||
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
|
||||
"cinematic."
|
||||
)
|
||||
video2 = generator.generate_video(
|
||||
prompt2,
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
|
||||
# Set SLA attention backend BEFORE fastvideo imports
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
# Use local model path
|
||||
MODEL_PATH = "loayrashid/TurboWan2.2-I2V-A14B-Diffusers"
|
||||
OUTPUT_PATH = "video_samples_turbodiffusion_i2v"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# TurboDiffusion I2V: 1-4 step image-to-video generation
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
MODEL_PATH,
|
||||
num_gpus=2,
|
||||
)
|
||||
|
||||
# Example prompt and image for I2V
|
||||
prompt = ("Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside.")
|
||||
|
||||
# Use an example image path
|
||||
image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG"
|
||||
|
||||
|
||||
video = generator.generate_video(
|
||||
prompt,
|
||||
image_path=image_path,
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,7 +12,7 @@ def main():
|
||||
"Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
# "alibaba-pai/Wan2.2-Fun-A14B-Control",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -12,7 +12,7 @@ def main():
|
||||
"Wan-AI/Wan2.2-I2V-A14B-Diffusers",
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True, # DiT need to be offloaded for MoE
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -11,7 +11,7 @@ def main():
|
||||
model_name,
|
||||
# FastVideo will automatically handle distributed setup
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
use_fsdp_inference=False, # set to True if GPU is out of memory
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
|
||||
import gradio as gr
|
||||
import torch
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
|
||||
from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator
|
||||
from fastvideo.models.dits.matrix_game.utils import expand_action_to_frames
|
||||
|
||||
|
||||
VARIANT_CONFIG = {
|
||||
"Matrix-Game-2.0-Base": {
|
||||
"model_path": "FastVideo/Matrix-Game-2.0-Base-Diffusers",
|
||||
"keyboard_dim": 4,
|
||||
"mode": "universal",
|
||||
"image_url": "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-2/demo_images/universal/0000.png",
|
||||
},
|
||||
"Matrix-Game-2.0-GTA": {
|
||||
"model_path": "FastVideo/Matrix-Game-2.0-GTA-Diffusers",
|
||||
"keyboard_dim": 2,
|
||||
"mode": "gta_drive",
|
||||
"image_url": "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-2/demo_images/gta_drive/0000.png",
|
||||
},
|
||||
"Matrix-Game-2.0-TempleRun": {
|
||||
"model_path": "FastVideo/Matrix-Game-2.0-TempleRun-Diffusers",
|
||||
"keyboard_dim": 7,
|
||||
"mode": "templerun",
|
||||
"image_url": "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-2/demo_images/temple_run/0000.png",
|
||||
},
|
||||
}
|
||||
|
||||
MODEL_PATH_MAPPING = {
|
||||
name: config["model_path"] for name, config in VARIANT_CONFIG.items()
|
||||
}
|
||||
|
||||
|
||||
CAM_VALUE = 0.1
|
||||
KEYBOARD_MAP_UNIVERSAL = {
|
||||
"W (Forward)": [1, 0, 0, 0],
|
||||
"S (Back)": [0, 1, 0, 0],
|
||||
"A (Left)": [0, 0, 1, 0],
|
||||
"D (Right)": [0, 0, 0, 1],
|
||||
"Q (Stop)": [0, 0, 0, 0],
|
||||
}
|
||||
KEYBOARD_MAP_GTA = {
|
||||
"W (Forward)": [1, 0],
|
||||
"S (Back)": [0, 1],
|
||||
"Q (Stop)": [0, 0],
|
||||
}
|
||||
KEYBOARD_MAP_TEMPLERUN = {
|
||||
"Q (Run)": [1, 0, 0, 0, 0, 0, 0],
|
||||
"W (Jump)": [0, 1, 0, 0, 0, 0, 0],
|
||||
"S (Slide)": [0, 0, 1, 0, 0, 0, 0],
|
||||
"Z (Turn Left)": [0, 0, 0, 1, 0, 0, 0],
|
||||
"C (Turn Right)": [0, 0, 0, 0, 1, 0, 0],
|
||||
"A (Left)": [0, 0, 0, 0, 0, 1, 0],
|
||||
"D (Right)": [0, 0, 0, 0, 0, 0, 1],
|
||||
}
|
||||
|
||||
|
||||
CAMERA_MAP_UNIVERSAL = {
|
||||
"U (Center)": [0, 0],
|
||||
"I (Up)": [CAM_VALUE, 0],
|
||||
"K (Down)": [-CAM_VALUE, 0],
|
||||
"J (Left)": [0, -CAM_VALUE],
|
||||
"L (Right)": [0, CAM_VALUE],
|
||||
}
|
||||
CAMERA_MAP_GTA = {
|
||||
"Q (Straight)": [0, 0],
|
||||
"A (Steer Left)": [0, -CAM_VALUE],
|
||||
"D (Steer Right)": [0, CAM_VALUE],
|
||||
}
|
||||
|
||||
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_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
||||
os.environ["FASTVIDEO_STAGE_LOGGING"] = "1"
|
||||
|
||||
def create_timing_display(inference_time, total_time, stage_execution_times, num_frames):
|
||||
dit_denoising_time = f"{stage_execution_times[5]:.2f}s" if len(stage_execution_times) > 5 else "N/A"
|
||||
|
||||
timing_html = f"""
|
||||
<div style="margin: 10px 0;">
|
||||
<h3 style="text-align: center; margin-bottom: 10px;">⏱️ Timing Breakdown</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 10px;">
|
||||
<div class="timing-card timing-card-highlight">
|
||||
<div style="font-size: 20px;">🚀</div>
|
||||
<div style="font-weight: bold; margin: 3px 0; font-size: 14px;">DiT Denoising</div>
|
||||
<div style="font-size: 16px; color: #ffa200; font-weight: bold;">{dit_denoising_time}</div>
|
||||
</div>
|
||||
<div class="timing-card">
|
||||
<div style="font-size: 20px;">🧠</div>
|
||||
<div style="font-weight: bold; margin: 3px 0; font-size: 14px;">E2E (w. vae/text encoder)</div>
|
||||
<div style="font-size: 16px; color: #2563eb;">{inference_time:.2f}s</div>
|
||||
</div>
|
||||
<div class="timing-card">
|
||||
<div style="font-size: 20px;">🎬</div>
|
||||
<div style="font-weight: bold; margin: 3px 0; font-size: 14px;">Video Encoding</div>
|
||||
<div style="font-size: 16px; color: #dc2626;">N/A</div>
|
||||
</div>
|
||||
<div class="timing-card">
|
||||
<div style="font-size: 20px;">🌐</div>
|
||||
<div style="font-weight: bold; margin: 3px 0; font-size: 14px;">Network Transfer</div>
|
||||
<div style="font-size: 16px; color: #059669;">N/A</div>
|
||||
</div>
|
||||
<div class="timing-card">
|
||||
<div style="font-size: 20px;">📊</div>
|
||||
<div style="font-weight: bold; margin: 3px 0; font-size: 14px;">Total Processing</div>
|
||||
<div style="font-size: 18px; color: #0277bd;">{total_time:.2f}s</div>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
if inference_time > 0:
|
||||
fps = num_frames / inference_time
|
||||
timing_html += f"""
|
||||
<div class="performance-card" style="margin-top: 15px;">
|
||||
<span style="font-weight: bold;">Generation Speed: </span>
|
||||
<span style="font-size: 18px; color: #6366f1; font-weight: bold;">{fps:.1f} frames/second</span>
|
||||
</div>"""
|
||||
|
||||
return timing_html + "</div>"
|
||||
|
||||
def get_action_tensors(mode: str, keyboard_key: str, mouse_key: str | None):
|
||||
if mode == "universal":
|
||||
keyboard = torch.tensor(KEYBOARD_MAP_UNIVERSAL.get(keyboard_key, [0, 0, 0, 0])).cuda()
|
||||
mouse = torch.tensor(CAMERA_MAP_UNIVERSAL.get(mouse_key, [0, 0])).cuda()
|
||||
elif mode == "gta_drive":
|
||||
keyboard = torch.tensor(KEYBOARD_MAP_GTA.get(keyboard_key, [0, 0])).cuda()
|
||||
mouse = torch.tensor(CAMERA_MAP_GTA.get(mouse_key, [0, 0])).cuda()
|
||||
elif mode == "templerun":
|
||||
keyboard = torch.tensor(KEYBOARD_MAP_TEMPLERUN.get(keyboard_key, [1, 0, 0, 0, 0, 0, 0])).cuda()
|
||||
mouse = None
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
return {"keyboard": keyboard, "mouse": mouse}
|
||||
|
||||
def create_gradio_interface(generators: dict[str, StreamingVideoGenerator], loaded_model_name: str):
|
||||
initial_config = VARIANT_CONFIG.get(loaded_model_name, VARIANT_CONFIG["Matrix-Game-2.0-Base"])
|
||||
initial_mode = initial_config["mode"]
|
||||
|
||||
if initial_mode == "universal":
|
||||
initial_kb_choices = list(KEYBOARD_MAP_UNIVERSAL.keys())
|
||||
initial_mouse_choices = list(CAMERA_MAP_UNIVERSAL.keys())
|
||||
initial_mouse_visible = True
|
||||
elif initial_mode == "gta_drive":
|
||||
initial_kb_choices = list(KEYBOARD_MAP_GTA.keys())
|
||||
initial_mouse_choices = list(CAMERA_MAP_GTA.keys())
|
||||
initial_mouse_visible = True
|
||||
else: # templerun
|
||||
initial_kb_choices = list(KEYBOARD_MAP_TEMPLERUN.keys())
|
||||
initial_mouse_choices = []
|
||||
initial_mouse_visible = False
|
||||
|
||||
theme = gr.themes.Base().set(
|
||||
button_primary_background_fill="#2563eb",
|
||||
button_primary_background_fill_hover="#1d4ed8",
|
||||
button_primary_text_color="white",
|
||||
slider_color="#2563eb",
|
||||
checkbox_background_color_selected="#2563eb",
|
||||
)
|
||||
|
||||
with gr.Blocks(title="FastVideo - Matrix Game 2.0", theme=theme) as demo:
|
||||
game_state = gr.State({
|
||||
"initialized": False,
|
||||
"current_model": None,
|
||||
"block_idx": 0,
|
||||
"max_blocks": 50,
|
||||
})
|
||||
|
||||
# Header
|
||||
gr.Image("assets/full.svg", show_label=False, container=False, height=80)
|
||||
|
||||
gr.HTML("""
|
||||
<div style="text-align: center; margin-bottom: 10px;">
|
||||
<p style="font-size: 18px;"> Make Video Generation Go Blurrrrrrr </p>
|
||||
<p style="font-size: 18px;"> <a href="https://github.com/hao-ai-lab/FastVideo/tree/main" target="_blank">Code</a> | <a href="https://hao-ai-lab.github.io/blogs/fastvideo_post_training/" target="_blank">Blog</a> | <a href="https://hao-ai-lab.github.io/FastVideo/" target="_blank">Docs</a> </p>
|
||||
</div>
|
||||
""")
|
||||
|
||||
with gr.Accordion("🎥 What Is FastVideo?", open=False):
|
||||
gr.HTML("""
|
||||
<div style="padding: 20px; line-height: 1.6;">
|
||||
<p style="font-size: 16px; margin-bottom: 15px;">
|
||||
FastVideo is an inference and post-training framework for diffusion models. It features an end-to-end unified pipeline for accelerating diffusion models, starting from data preprocessing to model training, finetuning, distillation, and inference. FastVideo is designed to be modular and extensible, allowing users to easily add new optimizations and techniques. Whether it is training-free optimizations or post-training optimizations, FastVideo has you covered.
|
||||
</p>
|
||||
</div>
|
||||
""")
|
||||
|
||||
# Model Selection
|
||||
with gr.Row():
|
||||
model_selection = gr.Dropdown(
|
||||
choices=[loaded_model_name],
|
||||
value=loaded_model_name,
|
||||
label="Select Model",
|
||||
interactive=False
|
||||
)
|
||||
|
||||
|
||||
# Main Layout
|
||||
with gr.Row(equal_height=True, elem_classes="main-content-row"):
|
||||
with gr.Column(scale=1, elem_classes="advanced-options-column"):
|
||||
with gr.Group():
|
||||
gr.HTML("<div style='margin: 0 0 15px 0; text-align: center; font-size: 16px;'>Game Controls</div>")
|
||||
|
||||
with gr.Group():
|
||||
gr.HTML("<div style='font-size: 14px; margin-bottom: 5px; font-weight: bold;'>🎮 Keyboard Control</div>")
|
||||
keyboard_action = gr.Radio(
|
||||
choices=initial_kb_choices,
|
||||
value=initial_kb_choices[0] if initial_kb_choices else None,
|
||||
label="Movement",
|
||||
show_label=False,
|
||||
interactive=True
|
||||
)
|
||||
|
||||
with gr.Group(visible=initial_mouse_visible) as mouse_group:
|
||||
gr.HTML("<div style='font-size: 14px; margin-bottom: 5px; font-weight: bold;'>🖱️ Mouse/Camera Control</div>")
|
||||
mouse_action = gr.Radio(
|
||||
choices=initial_mouse_choices if initial_mouse_visible else [],
|
||||
value=initial_mouse_choices[0] if initial_mouse_choices else None,
|
||||
label="Camera",
|
||||
show_label=False,
|
||||
interactive=True
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
action_btn = gr.Button("Start", variant="primary")
|
||||
stop_btn = gr.Button("Stop", variant="stop")
|
||||
|
||||
gr.HTML("<div style='margin-top: 15px;'></div>")
|
||||
|
||||
seed = gr.Slider(
|
||||
label="Seed",
|
||||
minimum=0,
|
||||
maximum=1000000,
|
||||
step=1,
|
||||
value=1024,
|
||||
)
|
||||
randomize_seed = gr.Checkbox(label="Randomize seed", value=False)
|
||||
seed_output = gr.Number(label="Used Seed")
|
||||
|
||||
block_counter = gr.Textbox(label="Progress", value="Block: 0 / 50", interactive=False, lines=1)
|
||||
|
||||
|
||||
# Right Column: Video Output
|
||||
with gr.Column(scale=1, elem_classes="video-column"):
|
||||
video_output = gr.Video(
|
||||
label="Generated Video",
|
||||
show_label=True,
|
||||
height=466,
|
||||
width=600,
|
||||
container=True,
|
||||
elem_classes="video-component",
|
||||
autoplay=True
|
||||
)
|
||||
|
||||
# Styles
|
||||
gr.HTML("""
|
||||
<style>
|
||||
.center-button {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
height: 100% !important;
|
||||
padding-top: 1.4em !important;
|
||||
}
|
||||
|
||||
.gradio-container {
|
||||
max-width: 1200px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 1200px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.gr-form, .gr-box, .gr-group {
|
||||
max-width: 1200px !important;
|
||||
}
|
||||
|
||||
.gr-video {
|
||||
max-width: 500px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.main-content-row {
|
||||
display: flex !important;
|
||||
align-items: flex-start !important;
|
||||
min-height: 500px !important;
|
||||
gap: 20px !important;
|
||||
}
|
||||
|
||||
.advanced-options-column,
|
||||
.video-column {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex: 1 !important;
|
||||
min-height: 400px !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.video-column > * {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.video-column .gr-video,
|
||||
.video-component {
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.video-column .gr-video .gr-form {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.advanced-options-column .gr-group,
|
||||
.video-column .gr-video {
|
||||
margin-top: 0 !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.advanced-options-column > *:last-child,
|
||||
.video-column > *:last-child {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.main-content-row {
|
||||
min-height: 600px !important;
|
||||
}
|
||||
|
||||
.advanced-options-column,
|
||||
.video-column {
|
||||
min-height: 600px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-content-row {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.advanced-options-column,
|
||||
.video-column {
|
||||
min-height: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.timing-card {
|
||||
background: var(--background-fill-secondary) !important;
|
||||
border: 1px solid var(--border-color-primary) !important;
|
||||
color: var(--body-text-color) !important;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.timing-card-highlight {
|
||||
background: var(--background-fill-primary) !important;
|
||||
border: 2px solid var(--color-accent) !important;
|
||||
}
|
||||
|
||||
.performance-card {
|
||||
background: var(--background-fill-secondary) !important;
|
||||
border: 1px solid var(--border-color-primary) !important;
|
||||
color: var(--body-text-color) !important;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gr-number input[readonly] {
|
||||
background-color: var(--background-fill-secondary) !important;
|
||||
border: 1px solid var(--border-color-primary) !important;
|
||||
color: var(--body-text-color-subdued) !important;
|
||||
cursor: default !important;
|
||||
text-align: center !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
</style>
|
||||
""")
|
||||
|
||||
# UI update based on model selection
|
||||
def on_model_change(model_name):
|
||||
config = VARIANT_CONFIG.get(model_name, VARIANT_CONFIG["Matrix-Game-2.0-Base"])
|
||||
mode = config["mode"]
|
||||
|
||||
if mode == "universal":
|
||||
kb_choices = list(KEYBOARD_MAP_UNIVERSAL.keys())
|
||||
mouse_choices = list(CAMERA_MAP_UNIVERSAL.keys())
|
||||
mouse_visible = True
|
||||
elif mode == "gta_drive":
|
||||
kb_choices = list(KEYBOARD_MAP_GTA.keys())
|
||||
mouse_choices = list(CAMERA_MAP_GTA.keys())
|
||||
mouse_visible = True
|
||||
else: # templerun
|
||||
kb_choices = list(KEYBOARD_MAP_TEMPLERUN.keys())
|
||||
mouse_choices = []
|
||||
mouse_visible = False
|
||||
|
||||
return (
|
||||
gr.update(choices=kb_choices, value=kb_choices[0] if kb_choices else None),
|
||||
gr.update(choices=mouse_choices, value=mouse_choices[0] if mouse_choices else None, visible=mouse_visible),
|
||||
gr.update(visible=mouse_visible),
|
||||
)
|
||||
|
||||
model_selection.change(
|
||||
fn=on_model_change,
|
||||
inputs=model_selection,
|
||||
outputs=[keyboard_action, mouse_action, mouse_group]
|
||||
)
|
||||
|
||||
def start_game(model_name, seed_val, randomize, state):
|
||||
if randomize:
|
||||
seed_val = torch.randint(0, 1000000, (1,)).item()
|
||||
|
||||
config = VARIANT_CONFIG.get(model_name)
|
||||
if not config:
|
||||
return state, seed_val, "Block: 0 / 50", None, "", gr.update(), gr.update()
|
||||
|
||||
generator = generators.get(config["model_path"])
|
||||
if not generator:
|
||||
return state, seed_val, "Block: 0 / 50", None, "", gr.update(), gr.update()
|
||||
|
||||
# If already initialized, clean up first
|
||||
if state.get("initialized"):
|
||||
try:
|
||||
# Clear accumulated frames without saving
|
||||
generator.accumulated_frames = []
|
||||
generator.executor.execute_streaming_clear()
|
||||
except Exception as e:
|
||||
print(f"Warning: cleanup error: {e}")
|
||||
|
||||
# Streaming parameters
|
||||
num_latent_frames_per_block = 3
|
||||
max_blocks = 50
|
||||
total_latent_frames = num_latent_frames_per_block * max_blocks
|
||||
num_frames = (total_latent_frames - 1) * 4 + 1
|
||||
|
||||
actions = {
|
||||
"keyboard": torch.zeros((num_frames, config["keyboard_dim"])),
|
||||
"mouse": torch.zeros((num_frames, 2))
|
||||
}
|
||||
grid_sizes = torch.tensor([150, 44, 80])
|
||||
|
||||
output_dir = os.path.abspath("outputs/matrixgame")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
video_path = os.path.join(output_dir, f"video_{int(time.time())}.mp4")
|
||||
|
||||
generator.reset(
|
||||
prompt="",
|
||||
image_path=config["image_url"],
|
||||
mouse_cond=actions["mouse"].unsqueeze(0),
|
||||
keyboard_cond=actions["keyboard"].unsqueeze(0),
|
||||
grid_sizes=grid_sizes,
|
||||
num_frames=num_frames,
|
||||
height=352,
|
||||
width=640,
|
||||
num_inference_steps=50,
|
||||
output_path=video_path,
|
||||
)
|
||||
|
||||
new_state = {
|
||||
"initialized": True,
|
||||
"current_model": model_name,
|
||||
"block_idx": 0,
|
||||
"max_blocks": max_blocks,
|
||||
"video_path": video_path,
|
||||
"frames_per_block": num_latent_frames_per_block * 4,
|
||||
"mode": config["mode"],
|
||||
"seed": seed_val,
|
||||
}
|
||||
|
||||
return new_state, seed_val, "Block: 0 / 50", None, gr.update(value="Step"), gr.update(interactive=True)
|
||||
|
||||
async def step_game(keyboard_key, mouse_key, model_name, state):
|
||||
if not state.get("initialized"):
|
||||
return state, state.get("seed", 0), "Block: 0 / 50", None, gr.update(), gr.update()
|
||||
|
||||
# total_start_time = time.time()
|
||||
config = VARIANT_CONFIG.get(model_name)
|
||||
generator = generators.get(config["model_path"])
|
||||
mode = state["mode"]
|
||||
frames_per_block = state["frames_per_block"]
|
||||
|
||||
# Parse inputs to tensors
|
||||
action = get_action_tensors(mode, keyboard_key, mouse_key)
|
||||
keyboard_cond, mouse_cond = expand_action_to_frames(action, frames_per_block)
|
||||
|
||||
# run step async
|
||||
# inference_start_time = time.time()
|
||||
frames, block_future = await generator.step_async(keyboard_cond, mouse_cond)
|
||||
# inference_time = time.time() - inference_start_time
|
||||
|
||||
# wait for block file to be written
|
||||
block_path = await asyncio.to_thread(block_future.result) if block_future else None
|
||||
state["block_idx"] = generator.block_idx
|
||||
block_str = f"Block: {state['block_idx']} / {state['max_blocks']}"
|
||||
|
||||
# total_time = time.time() - total_start_time
|
||||
|
||||
# Timing breakdown
|
||||
# timing_html = create_timing_display(inference_time, total_time, [], frames_per_block)
|
||||
|
||||
return state, state.get("seed", 0), block_str, block_path, gr.update(), gr.update()
|
||||
|
||||
def stop_game(model_name, state):
|
||||
if not state.get("initialized"):
|
||||
return {"initialized": False}, 0, "Block: 0 / 50", None, gr.update(value="Start"), gr.update(interactive=False)
|
||||
|
||||
config = VARIANT_CONFIG.get(model_name)
|
||||
generator = generators.get(config["model_path"])
|
||||
|
||||
final_path = state.get("video_path")
|
||||
generator.finalize(final_path)
|
||||
|
||||
return {"initialized": False}, state.get("seed", 0), "Block: 0 / 50", final_path, gr.update(value="Start"), gr.update(interactive=False)
|
||||
|
||||
async def handle_action(keyboard_key, mouse_key, model_name, seed_val, randomize, state):
|
||||
if not state.get("initialized"):
|
||||
return start_game(model_name, seed_val, randomize, state)
|
||||
else:
|
||||
return await step_game(keyboard_key, mouse_key, model_name, state)
|
||||
|
||||
action_btn.click(
|
||||
fn=handle_action,
|
||||
inputs=[keyboard_action, mouse_action, model_selection, seed, randomize_seed, game_state],
|
||||
outputs=[game_state, seed_output, block_counter, video_output, action_btn, stop_btn]
|
||||
)
|
||||
|
||||
stop_btn.click(
|
||||
fn=stop_game,
|
||||
inputs=[model_selection, game_state],
|
||||
outputs=[game_state, seed_output, block_counter, video_output, action_btn, stop_btn]
|
||||
)
|
||||
|
||||
gr.HTML("""
|
||||
<div style="text-align: center; margin-top: 10px; margin-bottom: 15px;">
|
||||
<p style="font-size: 16px; margin: 0;">Note that this demo is meant to showcase Matrix Game's quality and that under a large number of requests, generation speed may be affected.</p>
|
||||
</div>
|
||||
""")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Matrix Game Gradio Demo")
|
||||
parser.add_argument("--model", type=str, default="Matrix-Game-2.0-Base",
|
||||
choices=list(VARIANT_CONFIG.keys()),
|
||||
help="Model variant to load")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=7860)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load the selected model
|
||||
config = VARIANT_CONFIG[args.model]
|
||||
model_path = config["model_path"]
|
||||
|
||||
print(f"Loading model: {model_path}")
|
||||
setup_model_environment(model_path)
|
||||
generator = StreamingVideoGenerator.from_pretrained(
|
||||
model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=True,
|
||||
dit_cpu_offload=True,
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=True,
|
||||
)
|
||||
|
||||
generators = {model_path: generator}
|
||||
|
||||
demo = create_gradio_interface(generators, args.model)
|
||||
|
||||
print(f"Starting Gradio at http://{args.host}:{args.port}")
|
||||
|
||||
# FastAPI Wrapper
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/logo.png")
|
||||
def get_logo():
|
||||
return FileResponse(
|
||||
"assets/full.svg",
|
||||
media_type="image/svg+xml",
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/favicon.ico")
|
||||
def get_favicon():
|
||||
favicon_path = "assets/icon-simple.svg"
|
||||
|
||||
if os.path.exists(favicon_path):
|
||||
return FileResponse(
|
||||
favicon_path,
|
||||
media_type="image/svg+xml",
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Favicon not found")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request):
|
||||
base_url = str(request.base_url).rstrip('/')
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>FastVideo - Matrix Game 2.0</title>
|
||||
<meta name="title" content="MatrixGame2.0">
|
||||
<meta name="description" content="Make video generation go blurrrrrrr">
|
||||
<meta name="keywords" content="FastVideo, video generation, AI, machine learning, Matrix Game 2.0">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{base_url}/">
|
||||
<meta property="og:title" content="FastVideo - Matrix Game 2.0">
|
||||
<meta property="og:description" content="Make video generation go blurrrrrrr">
|
||||
<meta property="og:image" content="{base_url}/logo.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:site_name" content="MatrixGame2.0">
|
||||
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="{base_url}/">
|
||||
<meta property="twitter:title" content="MatrixGame2.0">
|
||||
<meta property="twitter:description" content="Make video generation go blurrrrrrr">
|
||||
<meta property="twitter:image" content="{base_url}/logo.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/favicon.ico">
|
||||
<style>
|
||||
body, html {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}}
|
||||
iframe {{
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
border: none;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="/gradio" width="100%" height="100%" style="border: none;"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
app = gr.mount_gradio_app(
|
||||
app,
|
||||
demo,
|
||||
path="/gradio",
|
||||
allowed_paths=[os.path.abspath("outputs"), os.path.abspath("fastvideo-logos")]
|
||||
)
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
from fastvideo import VideoGenerator
|
||||
import argparse
|
||||
|
||||
OUTPUT_PATH = "video_samples_wan2_2_5B_ti2v"
|
||||
|
||||
|
||||
def main(text_encoder_path: str):
|
||||
# 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 = "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=True,
|
||||
vae_cpu_offload=False,
|
||||
text_encoder_cpu_offload=False,
|
||||
# AbsMaxFP8 is the quantization method used by ComfyUI;
|
||||
# check fastvideo/layers/quantization/* for more quantization methods
|
||||
override_text_encoder_quant="AbsMaxFP8",
|
||||
# for Wan 2.2, this is the path to "umt5_xxl_fp8_e4m3fn_scaled.safetensors"
|
||||
override_text_encoder_safetensors=text_encoder_path,
|
||||
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
|
||||
)
|
||||
|
||||
# I2V is triggered just by passing in an image_path argument
|
||||
prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside."
|
||||
image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG"
|
||||
video = generator.generate_video(
|
||||
prompt, output_path=OUTPUT_PATH, save_video=True, image_path=image_path
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--text_encoder_path",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to the quantized text encoder safetensors file.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
main(args.text_encoder_path)
|
||||
@@ -9,7 +9,7 @@ build-backend = "scikit_build_core.build"
|
||||
|
||||
[project]
|
||||
name = "fastvideo-kernel"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "Unified CUDA kernels for FastVideo"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Adapted from TurboDiffusion SLA implementation
|
||||
# Copyright (c) 2025 by SLA team.
|
||||
#
|
||||
# Citation:
|
||||
# @article{zhang2025sla,
|
||||
# title={SLA: Beyond Sparsity in Diffusion Transformers via Fine-Tunable Sparse-Linear Attention},
|
||||
# author={Jintao Zhang and Haoxu Wang and Kai Jiang and Shuo Yang and Kaiwen Zheng and
|
||||
# Haocheng Xi and Ziteng Wang and Hongzhou Zhu and Min Zhao and Ion Stoica and
|
||||
# Joseph E. Gonzalez and Jun Zhu and Jianfei Chen},
|
||||
# journal={arXiv preprint arXiv:2509.24006},
|
||||
# year={2025}
|
||||
# }
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _attn_fwd(
|
||||
Q, K, V,
|
||||
qk_scale: tl.constexpr,
|
||||
topk: tl.constexpr,
|
||||
LUT, LSE, OS,
|
||||
L: tl.constexpr,
|
||||
M_BLOCKS: tl.constexpr,
|
||||
D: tl.constexpr,
|
||||
BLOCK_M: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
):
|
||||
idx_m = tl.program_id(0).to(tl.int64)
|
||||
idx_bh = tl.program_id(1).to(tl.int64)
|
||||
|
||||
qkv_offset = idx_bh * L * D
|
||||
lut_offset = (idx_bh * M_BLOCKS + idx_m) * topk
|
||||
lse_offset = idx_bh * L
|
||||
offs_m = idx_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||
offs_n = tl.arange(0, BLOCK_N)
|
||||
offs_d = tl.arange(0, D)
|
||||
|
||||
Q_ptrs = Q + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
K_ptrs = K + qkv_offset + offs_n[None, :] * D + offs_d[:, None]
|
||||
V_ptrs = V + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
OS_ptrs = OS + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
LUT_ptr = LUT + lut_offset
|
||||
LSE_ptrs = LSE + lse_offset + offs_m
|
||||
|
||||
m_i = tl.full([BLOCK_M], -float('inf'), dtype=tl.float32)
|
||||
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
|
||||
o_s = tl.zeros([BLOCK_M, D], dtype=tl.float32)
|
||||
|
||||
q = tl.load(Q_ptrs, mask=offs_m[:, None] < L)
|
||||
for block_idx in tl.range(topk):
|
||||
idx_n = tl.load(LUT_ptr + block_idx)
|
||||
n_mask = offs_n < L - idx_n * BLOCK_N
|
||||
|
||||
k = tl.load(K_ptrs + idx_n * BLOCK_N * D, mask=n_mask[None, :])
|
||||
qk = tl.dot(q, k) * (qk_scale * 1.4426950408889634) # = 1 / ln(2)
|
||||
if L - idx_n * BLOCK_N < BLOCK_N:
|
||||
qk = tl.where(n_mask[None, :], qk, float("-inf"))
|
||||
|
||||
v = tl.load(V_ptrs + idx_n * BLOCK_N * D, mask=n_mask[:, None])
|
||||
local_m = tl.max(qk, 1)
|
||||
new_m = tl.maximum(m_i, local_m)
|
||||
qk = qk - new_m[:, None]
|
||||
|
||||
p = tl.math.exp2(qk)
|
||||
l_ij = tl.sum(p, 1)
|
||||
alpha = tl.math.exp2(m_i - new_m)
|
||||
o_s = o_s * alpha[:, None]
|
||||
o_s += tl.dot(p.to(v.dtype), v)
|
||||
|
||||
l_i = l_i * alpha + l_ij
|
||||
m_i = new_m
|
||||
|
||||
o_s = o_s / l_i[:, None]
|
||||
tl.store(OS_ptrs, o_s.to(OS.type.element_ty), mask=offs_m[:, None] < L)
|
||||
|
||||
m_i += tl.math.log2(l_i)
|
||||
tl.store(LSE_ptrs, m_i, mask=offs_m < L)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _attn_bwd_preprocess(
|
||||
OS, DOS, DELTAS,
|
||||
L,
|
||||
D: tl.constexpr,
|
||||
BLOCK_M: tl.constexpr,
|
||||
):
|
||||
idx_m = tl.program_id(0).to(tl.int64)
|
||||
idx_bh = tl.program_id(1).to(tl.int64)
|
||||
|
||||
OS += idx_bh * L * D
|
||||
DOS += idx_bh * L * D
|
||||
DELTAS += idx_bh * L
|
||||
|
||||
offs_m = idx_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||
offs_d = tl.arange(0, D)
|
||||
|
||||
o_s = tl.load(OS + offs_m[:, None] * D + offs_d[None, :], mask=offs_m[:, None] < L)
|
||||
do_s = tl.load(DOS + offs_m[:, None] * D + offs_d[None, :], mask=offs_m[:, None] < L)
|
||||
|
||||
delta_s = tl.sum(o_s * do_s, axis=1).to(DELTAS.type.element_ty)
|
||||
tl.store(DELTAS + offs_m, delta_s, mask=offs_m < L)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _attn_bwd_dq(
|
||||
Q, K, V, LSE, DELTAS,
|
||||
DOS, DQ, LUT,
|
||||
qk_scale: tl.constexpr,
|
||||
topk: tl.constexpr,
|
||||
L: tl.constexpr,
|
||||
M_BLOCKS: tl.constexpr,
|
||||
D: tl.constexpr,
|
||||
BLOCK_M: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
):
|
||||
idx_m = tl.program_id(0).to(tl.int64)
|
||||
idx_bh = tl.program_id(1).to(tl.int64)
|
||||
|
||||
offs_m = idx_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||
offs_n = tl.arange(0, BLOCK_N)
|
||||
offs_d = tl.arange(0, D)
|
||||
|
||||
qkv_offset = idx_bh * L * D
|
||||
lse_offset = idx_bh * L
|
||||
lut_offset = (idx_bh * M_BLOCKS + idx_m) * topk
|
||||
|
||||
Q_ptrs = Q + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
K_ptrs = K + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
V_ptrs = V + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
DQ_ptrs = DQ + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
DOS_ptrs = DOS + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
LSE_ptrs = LSE + lse_offset + offs_m
|
||||
DELTAS_ptrs = DELTAS + lse_offset + offs_m
|
||||
LUT_ptr = LUT + lut_offset
|
||||
|
||||
q = tl.load(Q_ptrs, mask=offs_m[:, None] < L)
|
||||
do_s = tl.load(DOS_ptrs, mask=offs_m[:, None] < L)
|
||||
delta_s = tl.load(DELTAS_ptrs, mask=offs_m < L)
|
||||
lse = tl.load(LSE_ptrs, mask=offs_m < L, other=float("inf"))
|
||||
|
||||
dq = tl.zeros([BLOCK_M, D], dtype=tl.float32)
|
||||
for block_idx in tl.range(topk, num_stages=2):
|
||||
idx_n = tl.load(LUT_ptr + block_idx)
|
||||
n_mask = offs_n < L - idx_n * BLOCK_N
|
||||
|
||||
k = tl.load(K_ptrs + idx_n * BLOCK_N * D, mask=n_mask[:, None])
|
||||
v = tl.load(V_ptrs + idx_n * BLOCK_N * D, mask=n_mask[:, None])
|
||||
qk = tl.dot(q, k.T) * (qk_scale * 1.4426950408889634)
|
||||
p = tl.math.exp2(qk - lse[:, None])
|
||||
p = tl.where(n_mask[None, :], p, 0.0)
|
||||
|
||||
dp = tl.dot(do_s, v.T).to(tl.float32)
|
||||
ds = p * (dp - delta_s[:, None])
|
||||
dq += tl.dot(ds.to(k.dtype), k)
|
||||
tl.store(DQ_ptrs, dq * qk_scale, mask=offs_m[:, None] < L)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _attn_bwd_dkdv(
|
||||
Q, K, V, DOS, DK, DV,
|
||||
qk_scale, KBID, LSE, DELTAS,
|
||||
L: tl.constexpr,
|
||||
M_BLOCKS: tl.constexpr,
|
||||
N_BLOCKS: tl.constexpr,
|
||||
D: tl.constexpr,
|
||||
BLOCK_M: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
BLOCK_SLICE_FACTOR: tl.constexpr,
|
||||
):
|
||||
BLOCK_M2: tl.constexpr = BLOCK_M // BLOCK_SLICE_FACTOR
|
||||
|
||||
idx_n = tl.program_id(0).to(tl.int64)
|
||||
idx_bh = tl.program_id(1).to(tl.int64)
|
||||
|
||||
offs_n = idx_n * BLOCK_N + tl.arange(0, BLOCK_N)
|
||||
offs_m = tl.arange(0, BLOCK_M2)
|
||||
offs_d = tl.arange(0, D)
|
||||
|
||||
qkv_offset = idx_bh * L * D
|
||||
kbid_offset = idx_bh * M_BLOCKS * N_BLOCKS
|
||||
lse_offset = idx_bh * L
|
||||
|
||||
Q_ptrs = Q + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
K_ptrs = K + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
V_ptrs = V + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
DOS_ptrs = DOS + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
|
||||
DK_ptrs = DK + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
DV_ptrs = DV + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
|
||||
LSE_ptrs = LSE + lse_offset + offs_m
|
||||
DELTAS_ptrs = DELTAS + lse_offset + offs_m
|
||||
KBID_ptr = KBID + kbid_offset + idx_n
|
||||
|
||||
k = tl.load(K_ptrs, mask=offs_n[:, None] < L)
|
||||
v = tl.load(V_ptrs, mask=offs_n[:, None] < L)
|
||||
|
||||
dk = tl.zeros([BLOCK_N, D], dtype=tl.float32)
|
||||
dv = tl.zeros([BLOCK_N, D], dtype=tl.float32)
|
||||
for idx_m in tl.range(0, L, BLOCK_M2):
|
||||
kbid = tl.load(KBID_ptr)
|
||||
if kbid == 1:
|
||||
m_mask = offs_m < L - idx_m
|
||||
q = tl.load(Q_ptrs, mask=m_mask[:, None])
|
||||
lse = tl.load(LSE_ptrs, mask=m_mask, other=float("inf"))
|
||||
qkT = tl.dot(k, q.T) * (qk_scale * 1.4426950408889634)
|
||||
pT = tl.math.exp2(qkT - lse[None, :])
|
||||
pT = tl.where(offs_n[:, None] < L, pT, 0.0)
|
||||
|
||||
do = tl.load(DOS_ptrs, mask=m_mask[:, None])
|
||||
dv += tl.dot(pT.to(do.dtype), do)
|
||||
delta = tl.load(DELTAS_ptrs, mask=m_mask)
|
||||
dpT = tl.dot(v, tl.trans(do))
|
||||
dsT = pT * (dpT - delta[None, :])
|
||||
dk += tl.dot(dsT.to(q.dtype), q)
|
||||
|
||||
Q_ptrs += BLOCK_M2 * D
|
||||
DOS_ptrs += BLOCK_M2 * D
|
||||
LSE_ptrs += BLOCK_M2
|
||||
DELTAS_ptrs += BLOCK_M2
|
||||
if (idx_m + BLOCK_M2) % BLOCK_M == 0:
|
||||
KBID_ptr += N_BLOCKS
|
||||
|
||||
tl.store(DK_ptrs, dk * qk_scale, mask=offs_n[:, None] < L)
|
||||
tl.store(DV_ptrs, dv, mask=offs_n[:, None] < L)
|
||||
|
||||
|
||||
class _attention(torch.autograd.Function):
|
||||
"""Sparse attention forward/backward with autograd support."""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, q, k, v, k_block_id, lut, topk, BLOCK_M, BLOCK_N, qk_scale=None):
|
||||
assert q.is_contiguous() and k.is_contiguous() and v.is_contiguous()
|
||||
assert k_block_id.is_contiguous() and lut.is_contiguous()
|
||||
|
||||
assert BLOCK_M == 64 or BLOCK_M == 128
|
||||
assert BLOCK_N == 64
|
||||
|
||||
B, H, L, D = q.shape
|
||||
if qk_scale is None:
|
||||
qk_scale = D**-0.5
|
||||
|
||||
M_BLOCKS = triton.cdiv(L, BLOCK_M)
|
||||
|
||||
o_s = torch.empty_like(v)
|
||||
lse = torch.empty(q.shape[:-1], device=q.device, dtype=torch.float32)
|
||||
|
||||
grid = (M_BLOCKS, B * H)
|
||||
_attn_fwd[grid](
|
||||
q, k, v, qk_scale, topk,
|
||||
lut, lse, o_s,
|
||||
L, M_BLOCKS,
|
||||
D, BLOCK_M, BLOCK_N,
|
||||
num_warps=4 if q.shape[-1] == 64 else 8,
|
||||
num_stages=3
|
||||
)
|
||||
|
||||
ctx.save_for_backward(q, k, v, k_block_id, lut, lse, o_s)
|
||||
ctx.qk_scale = qk_scale
|
||||
ctx.topk = topk
|
||||
ctx.BLOCK_M = BLOCK_M
|
||||
ctx.BLOCK_N = BLOCK_N
|
||||
return o_s
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, do_s):
|
||||
q, k, v, k_block_id, lut, lse, o_s = ctx.saved_tensors
|
||||
do_s = do_s.contiguous()
|
||||
|
||||
BLOCK_M, BLOCK_N = ctx.BLOCK_M, ctx.BLOCK_N
|
||||
B, H, L, D = q.shape
|
||||
|
||||
M_BLOCKS = triton.cdiv(L, BLOCK_M)
|
||||
N_BLOCKS = triton.cdiv(L, BLOCK_N)
|
||||
|
||||
dq = torch.empty_like(q)
|
||||
dk = torch.empty_like(k)
|
||||
dv = torch.empty_like(v)
|
||||
delta_s = torch.empty_like(lse)
|
||||
|
||||
grid = (M_BLOCKS, B * H)
|
||||
_attn_bwd_preprocess[grid](
|
||||
o_s, do_s, delta_s,
|
||||
L, D, BLOCK_M,
|
||||
)
|
||||
|
||||
grid = (M_BLOCKS, B * H)
|
||||
_attn_bwd_dq[grid](
|
||||
q, k, v, lse, delta_s,
|
||||
do_s, dq, lut,
|
||||
ctx.qk_scale, ctx.topk,
|
||||
L, M_BLOCKS,
|
||||
D, BLOCK_M, BLOCK_N,
|
||||
num_warps=4 if q.shape[-1] == 64 else 8,
|
||||
num_stages=4 if q.shape[-1] == 64 else 5
|
||||
)
|
||||
|
||||
grid = (N_BLOCKS, B * H)
|
||||
_attn_bwd_dkdv[grid](
|
||||
q, k, v, do_s, dk, dv,
|
||||
ctx.qk_scale, k_block_id, lse, delta_s,
|
||||
L, M_BLOCKS, N_BLOCKS,
|
||||
D, BLOCK_M, BLOCK_N,
|
||||
BLOCK_SLICE_FACTOR=BLOCK_M // 64,
|
||||
num_warps=4 if q.shape[-1] == 64 else 8,
|
||||
num_stages=4 if q.shape[-1] == 64 else 5
|
||||
)
|
||||
|
||||
return dq, dk, dv, None, None, None, None, None, None
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.2.2"
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SLA (Sparse-Linear Attention) backend for FastVideo
|
||||
# Adapted from TurboDiffusion SLA implementation
|
||||
#
|
||||
# Copyright (c) 2025 by SLA team.
|
||||
# Citation:
|
||||
# @article{zhang2025sla,
|
||||
# title={SLA: Beyond Sparsity in Diffusion Transformers via Fine-Tunable Sparse-Linear Attention},
|
||||
# author={Jintao Zhang and Haoxu Wang and Kai Jiang and Shuo Yang and Kaiwen Zheng and
|
||||
# Haocheng Xi and Ziteng Wang and Hongzhou Zhu and Min Zhao and Ion Stoica and
|
||||
# Joseph E. Gonzalez and Jun Zhu and Jianfei Chen},
|
||||
# journal={arXiv preprint arXiv:2509.24006},
|
||||
# year={2025}
|
||||
# }
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from fastvideo.attention.backends.abstract import (
|
||||
AttentionBackend,
|
||||
AttentionImpl,
|
||||
AttentionMetadata,
|
||||
AttentionMetadataBuilder,
|
||||
)
|
||||
from fastvideo_kernel.triton_kernels.sla_triton import _attention
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# ============================================================================
|
||||
# SLA Utility functions (moved from sla_kernels/utils.py)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@triton.jit
|
||||
def compress_kernel(
|
||||
X,
|
||||
XM,
|
||||
L: tl.constexpr,
|
||||
D: tl.constexpr,
|
||||
BLOCK_L: tl.constexpr,
|
||||
):
|
||||
idx_l = tl.program_id(0)
|
||||
idx_bh = tl.program_id(1)
|
||||
|
||||
offs_l = idx_l * BLOCK_L + tl.arange(0, BLOCK_L)
|
||||
offs_d = tl.arange(0, D)
|
||||
|
||||
x_offset = idx_bh * L * D
|
||||
xm_offset = idx_bh * ((L + BLOCK_L - 1) // BLOCK_L) * D
|
||||
x = tl.load(X + x_offset + offs_l[:, None] * D + offs_d[None, :],
|
||||
mask=offs_l[:, None] < L)
|
||||
|
||||
nx = min(BLOCK_L, L - idx_l * BLOCK_L)
|
||||
x_mean = tl.sum(x, axis=0, dtype=tl.float32) / nx
|
||||
tl.store(XM + xm_offset + idx_l * D + offs_d,
|
||||
x_mean.to(XM.dtype.element_ty))
|
||||
|
||||
|
||||
def mean_pool(x: torch.Tensor, BLK: int) -> torch.Tensor:
|
||||
"""Mean pool tensor along sequence dimension with block size BLK."""
|
||||
assert x.is_contiguous()
|
||||
|
||||
B, H, L, D = x.shape
|
||||
L_BLOCKS = (L + BLK - 1) // BLK
|
||||
x_mean = torch.empty((B, H, L_BLOCKS, D), device=x.device, dtype=x.dtype)
|
||||
|
||||
grid = (L_BLOCKS, B * H)
|
||||
compress_kernel[grid](x, x_mean, L, D, BLK)
|
||||
return x_mean
|
||||
|
||||
|
||||
def get_block_map(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
topk_ratio: float,
|
||||
BLKQ: int = 64,
|
||||
BLKK: int = 64,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, int]:
|
||||
"""Compute sparse block map for attention based on QK similarity.
|
||||
|
||||
Args:
|
||||
q: Query tensor of shape (B, H, L, D)
|
||||
k: Key tensor of shape (B, H, L, D)
|
||||
topk_ratio: Ratio of key blocks to attend to (0-1)
|
||||
BLKQ: Query block size
|
||||
BLKK: Key block size
|
||||
|
||||
Returns:
|
||||
sparse_map: Binary mask of shape (B, H, num_q_blocks, num_k_blocks)
|
||||
lut: Top-k indices of shape (B, H, num_q_blocks, topk)
|
||||
topk: Number of key blocks selected
|
||||
"""
|
||||
arg_k = k - torch.mean(
|
||||
k, dim=-2, keepdim=True) # smooth-k technique from SageAttention
|
||||
pooled_qblocks = mean_pool(q, BLKQ)
|
||||
pooled_kblocks = mean_pool(arg_k, BLKK)
|
||||
pooled_score = pooled_qblocks @ pooled_kblocks.transpose(-1, -2)
|
||||
|
||||
K = pooled_score.shape[-1]
|
||||
topk = min(K, int(topk_ratio * K))
|
||||
lut = torch.topk(pooled_score, topk, dim=-1, sorted=False).indices
|
||||
|
||||
sparse_map = torch.zeros_like(pooled_score, dtype=torch.int8)
|
||||
sparse_map.scatter_(-1, lut, 1)
|
||||
return sparse_map, lut, topk
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SLA Backend classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class SLAAttentionBackend(AttentionBackend):
|
||||
"""Sparse-Linear Attention backend."""
|
||||
|
||||
accept_output_buffer: bool = True
|
||||
|
||||
@staticmethod
|
||||
def get_supported_head_sizes() -> list[int]:
|
||||
return [64, 128]
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "SLA_ATTN"
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls() -> type["SLAAttentionImpl"]:
|
||||
return SLAAttentionImpl
|
||||
|
||||
@staticmethod
|
||||
def get_metadata_cls() -> type["SLAAttentionMetadata"]:
|
||||
return SLAAttentionMetadata
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["SLAAttentionMetadataBuilder"]:
|
||||
return SLAAttentionMetadataBuilder
|
||||
|
||||
|
||||
@dataclass
|
||||
class SLAAttentionMetadata(AttentionMetadata):
|
||||
"""Metadata for SLA attention."""
|
||||
current_timestep: int
|
||||
topk_ratio: float = 0.5 # Ratio of key blocks to attend to
|
||||
|
||||
|
||||
class SLAAttentionMetadataBuilder(AttentionMetadataBuilder):
|
||||
"""Builder for SLA attention metadata."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def prepare(self) -> None:
|
||||
pass
|
||||
|
||||
def build(
|
||||
self,
|
||||
current_timestep: int,
|
||||
topk_ratio: float = 0.5,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> SLAAttentionMetadata:
|
||||
return SLAAttentionMetadata(
|
||||
current_timestep=current_timestep,
|
||||
topk_ratio=topk_ratio,
|
||||
)
|
||||
|
||||
|
||||
class SLAAttentionImpl(AttentionImpl, nn.Module):
|
||||
"""SLA attention implementation with learnable linear projection.
|
||||
|
||||
This implementation combines sparse attention with linear attention,
|
||||
using a learnable projection to blend the outputs. The sparse attention
|
||||
uses a block-sparse pattern determined by QK similarity.
|
||||
|
||||
Args:
|
||||
num_heads: Number of attention heads
|
||||
head_size: Dimension of each head
|
||||
topk_ratio: Ratio of key blocks to attend to (0-1), default 0.5
|
||||
feature_map: Feature map for linear attention ('softmax', 'elu', 'relu')
|
||||
BLKQ: Query block size for sparse attention
|
||||
BLKK: Key block size for sparse attention
|
||||
use_bf16: Whether to use bfloat16 for computation
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
head_size: int,
|
||||
causal: bool = False,
|
||||
softmax_scale: float | None = None,
|
||||
num_kv_heads: int | None = None,
|
||||
prefix: str = "",
|
||||
# SLA-specific parameters - matched to TurboDiffusion defaults
|
||||
topk_ratio: float = 0.1, # TurboDiffusion uses topk=0.1
|
||||
feature_map: str = "softmax",
|
||||
BLKQ: int = 128, # TurboDiffusion uses BLKQ=128
|
||||
BLKK: int = 64, # TurboDiffusion uses BLKK=64
|
||||
use_bf16: bool = True,
|
||||
**extra_impl_args,
|
||||
) -> None:
|
||||
nn.Module.__init__(self)
|
||||
|
||||
self.num_heads = num_heads
|
||||
self.head_size = head_size
|
||||
self.softmax_scale = softmax_scale if softmax_scale else head_size**-0.5
|
||||
self.causal = causal
|
||||
self.prefix = prefix
|
||||
|
||||
# SLA-specific config
|
||||
self.topk_ratio = topk_ratio
|
||||
self.BLKQ = BLKQ
|
||||
self.BLKK = BLKK
|
||||
self.dtype = torch.bfloat16 if use_bf16 else torch.float16
|
||||
|
||||
# Learnable linear projection for combining sparse + linear attention
|
||||
self.proj_l = nn.Linear(head_size, head_size, dtype=torch.float32)
|
||||
|
||||
# Feature map for linear attention
|
||||
# Type annotation for callables
|
||||
self.feature_map_q: Callable[[torch.Tensor], torch.Tensor]
|
||||
self.feature_map_k: Callable[[torch.Tensor], torch.Tensor]
|
||||
if feature_map == "elu":
|
||||
self.feature_map_q = lambda x: F.elu(x) + 1
|
||||
self.feature_map_k = lambda x: F.elu(x) + 1
|
||||
elif feature_map == "relu":
|
||||
self.feature_map_q = F.relu
|
||||
self.feature_map_k = F.relu
|
||||
elif feature_map == "softmax":
|
||||
self.feature_map_q = lambda x: F.softmax(x, dim=-1)
|
||||
self.feature_map_k = lambda x: F.softmax(x, dim=-1)
|
||||
else:
|
||||
raise ValueError(f"Unknown feature map: {feature_map}")
|
||||
|
||||
self._init_weights()
|
||||
|
||||
def _init_weights(self) -> None:
|
||||
"""Initialize projection weights to zero for residual-like behavior."""
|
||||
with torch.no_grad():
|
||||
nn.init.zeros_(self.proj_l.weight)
|
||||
nn.init.zeros_(self.proj_l.bias) # type: ignore[arg-type]
|
||||
|
||||
def _calc_linear_attention(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Compute linear attention: (Q @ K^T @ V) / normalizer.
|
||||
|
||||
Args:
|
||||
q: Query tensor (B, H, L, D) after feature map
|
||||
k: Key tensor (B, H, L, D) after feature map
|
||||
v: Value tensor (B, H, L, D)
|
||||
|
||||
Returns:
|
||||
Linear attention output (B, H, L, D)
|
||||
"""
|
||||
kvsum = k.transpose(-1, -2) @ v # (B, H, D, D)
|
||||
ksum = torch.sum(k, dim=-2, keepdim=True) # (B, H, 1, D)
|
||||
return (q @ kvsum) / (1e-5 + (q * ksum).sum(dim=-1, keepdim=True))
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata,
|
||||
) -> torch.Tensor:
|
||||
"""Forward pass for SLA attention.
|
||||
|
||||
Input tensors are in FastVideo format: (B, L, H, D)
|
||||
Internally converted to SLA format: (B, H, L, D)
|
||||
|
||||
Args:
|
||||
query: Query tensor (B, L, H, D)
|
||||
key: Key tensor (B, L, H, D)
|
||||
value: Value tensor (B, L, H, D)
|
||||
attn_metadata: Attention metadata
|
||||
|
||||
Returns:
|
||||
Output tensor (B, L, H, D)
|
||||
"""
|
||||
original_dtype = query.dtype
|
||||
|
||||
# Convert from FastVideo format (B, L, H, D) to SLA format (B, H, L, D)
|
||||
q = query.transpose(1, 2).contiguous()
|
||||
k = key.transpose(1, 2).contiguous()
|
||||
v = value.transpose(1, 2).contiguous()
|
||||
|
||||
# Get topk ratio from metadata if available
|
||||
topk_ratio = self.topk_ratio
|
||||
if hasattr(attn_metadata, 'topk_ratio'):
|
||||
topk_ratio = attn_metadata.topk_ratio # type: ignore[union-attr]
|
||||
|
||||
# Compute block-sparse attention pattern
|
||||
sparse_map, lut, real_topk = get_block_map(q,
|
||||
k,
|
||||
topk_ratio=topk_ratio,
|
||||
BLKQ=self.BLKQ,
|
||||
BLKK=self.BLKK)
|
||||
|
||||
# Convert to compute dtype
|
||||
q = q.to(self.dtype)
|
||||
k = k.to(self.dtype)
|
||||
v = v.to(self.dtype)
|
||||
|
||||
# Sparse attention
|
||||
o_s = _attention.apply(q, k, v, sparse_map, lut, real_topk, self.BLKQ,
|
||||
self.BLKK)
|
||||
|
||||
# Linear attention with feature maps
|
||||
q_linear = self.feature_map_q(q).contiguous().to(self.dtype)
|
||||
k_linear = self.feature_map_k(k).contiguous().to(self.dtype)
|
||||
o_l = self._calc_linear_attention(q_linear, k_linear, v)
|
||||
|
||||
# Project linear attention output and combine
|
||||
with torch.amp.autocast('cuda', dtype=self.dtype):
|
||||
o_l = self.proj_l(o_l)
|
||||
|
||||
# Combine sparse and linear outputs
|
||||
output = (o_s + o_l).to(original_dtype)
|
||||
|
||||
# Convert back to FastVideo format (B, L, H, D)
|
||||
output = output.transpose(1, 2)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
# Check if spas_sage_attn is available for SageSLA
|
||||
SAGESLA_ENABLED = True
|
||||
try:
|
||||
import spas_sage_attn._qattn as qattn
|
||||
import spas_sage_attn._fused as fused
|
||||
from spas_sage_attn.utils import get_vanilla_qk_quant, block_map_lut_triton
|
||||
except ImportError:
|
||||
SAGESLA_ENABLED = False
|
||||
|
||||
SAGE2PP_ENABLED = True
|
||||
try:
|
||||
from spas_sage_attn._qattn import qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold
|
||||
except ImportError:
|
||||
SAGE2PP_ENABLED = False
|
||||
|
||||
|
||||
class SageSLAAttentionBackend(AttentionBackend):
|
||||
"""Quantized Sparse-Linear Attention backend using SageAttention kernels."""
|
||||
|
||||
accept_output_buffer: bool = True
|
||||
|
||||
@staticmethod
|
||||
def get_supported_head_sizes() -> list[int]:
|
||||
return [64, 128]
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "SAGE_SLA_ATTN"
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls() -> type["SageSLAAttentionImpl"]:
|
||||
return SageSLAAttentionImpl
|
||||
|
||||
@staticmethod
|
||||
def get_metadata_cls() -> type["SLAAttentionMetadata"]:
|
||||
return SLAAttentionMetadata
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["SLAAttentionMetadataBuilder"]:
|
||||
return SLAAttentionMetadataBuilder
|
||||
|
||||
|
||||
def _get_cuda_arch(device_index: int) -> str:
|
||||
"""Get CUDA architecture string for the given device."""
|
||||
major, minor = torch.cuda.get_device_capability(device_index)
|
||||
return f"sm{major}{minor}"
|
||||
|
||||
|
||||
class SageSLAAttentionImpl(AttentionImpl, nn.Module):
|
||||
"""SageSLA attention implementation using quantized SageAttention kernels.
|
||||
|
||||
This uses INT8 quantization for Q/K and FP8 for V to achieve better performance
|
||||
while maintaining accuracy. Requires spas_sage_attn package.
|
||||
|
||||
Args:
|
||||
num_heads: Number of attention heads
|
||||
head_size: Dimension of each head (must be 64 or 128)
|
||||
topk_ratio: Ratio of key blocks to attend to (0-1), default 0.5
|
||||
feature_map: Feature map for linear attention ('softmax', 'elu', 'relu')
|
||||
use_bf16: Whether to use bfloat16 for computation
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
head_size: int,
|
||||
causal: bool = False,
|
||||
softmax_scale: float | None = None,
|
||||
num_kv_heads: int | None = None,
|
||||
prefix: str = "",
|
||||
# SageSLA-specific parameters
|
||||
topk_ratio: float = 0.5,
|
||||
feature_map: str = "softmax",
|
||||
use_bf16: bool = True,
|
||||
**extra_impl_args,
|
||||
) -> None:
|
||||
nn.Module.__init__(self)
|
||||
|
||||
if not SAGESLA_ENABLED:
|
||||
raise ImportError(
|
||||
"SageSLA requires spas_sage_attn. "
|
||||
"Install with: pip install git+https://github.com/thu-ml/SpargeAttn.git"
|
||||
)
|
||||
|
||||
assert head_size in [
|
||||
64, 128
|
||||
], f"SageSLA requires head_size in [64, 128], got {head_size}"
|
||||
|
||||
self.num_heads = num_heads
|
||||
self.head_size = head_size
|
||||
self.softmax_scale = softmax_scale if softmax_scale else head_size**-0.5
|
||||
self.causal = causal
|
||||
self.prefix = prefix
|
||||
|
||||
# SageSLA-specific config
|
||||
self.topk_ratio = topk_ratio
|
||||
self.dtype = torch.bfloat16 if use_bf16 else torch.float16
|
||||
|
||||
# Learnable linear projection for combining sparse + linear attention
|
||||
self.proj_l = nn.Linear(head_size, head_size, dtype=torch.float32)
|
||||
|
||||
# Feature map for linear attention
|
||||
# Type annotation for callables
|
||||
self.feature_map_q: Callable[[torch.Tensor], torch.Tensor]
|
||||
self.feature_map_k: Callable[[torch.Tensor], torch.Tensor]
|
||||
if feature_map == "elu":
|
||||
self.feature_map_q = lambda x: F.elu(x) + 1
|
||||
self.feature_map_k = lambda x: F.elu(x) + 1
|
||||
elif feature_map == "relu":
|
||||
self.feature_map_q = F.relu
|
||||
self.feature_map_k = F.relu
|
||||
elif feature_map == "softmax":
|
||||
self.feature_map_q = lambda x: F.softmax(x, dim=-1)
|
||||
self.feature_map_k = lambda x: F.softmax(x, dim=-1)
|
||||
else:
|
||||
raise ValueError(f"Unknown feature map: {feature_map}")
|
||||
|
||||
self._init_weights()
|
||||
|
||||
def _init_weights(self) -> None:
|
||||
"""Initialize projection weights to zero for residual-like behavior."""
|
||||
with torch.no_grad():
|
||||
nn.init.zeros_(self.proj_l.weight)
|
||||
nn.init.zeros_(self.proj_l.bias) # type: ignore[arg-type]
|
||||
|
||||
def _calc_linear_attention(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Compute linear attention: (Q @ K^T @ V) / normalizer."""
|
||||
kvsum = k.transpose(-1, -2) @ v
|
||||
ksum = torch.sum(k, dim=-2, keepdim=True)
|
||||
return (q @ kvsum) / (1e-5 + (q * ksum).sum(dim=-1, keepdim=True))
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata,
|
||||
) -> torch.Tensor:
|
||||
"""Forward pass for SageSLA attention with quantized kernels.
|
||||
|
||||
Input tensors are in FastVideo format: (B, L, H, D)
|
||||
|
||||
Args:
|
||||
query: Query tensor (B, L, H, D)
|
||||
key: Key tensor (B, L, H, D)
|
||||
value: Value tensor (B, L, H, D)
|
||||
attn_metadata: Attention metadata
|
||||
|
||||
Returns:
|
||||
Output tensor (B, L, H, D)
|
||||
"""
|
||||
original_dtype = query.dtype
|
||||
|
||||
# Convert from FastVideo format (B, L, H, D) to SLA format (B, H, L, D)
|
||||
q = query.transpose(1, 2).contiguous()
|
||||
k = key.transpose(1, 2).contiguous()
|
||||
v = value.transpose(1, 2).contiguous()
|
||||
|
||||
# Get topk ratio from metadata if available
|
||||
topk_ratio = self.topk_ratio
|
||||
if hasattr(attn_metadata, 'topk_ratio'):
|
||||
topk_ratio = attn_metadata.topk_ratio # type: ignore[union-attr]
|
||||
|
||||
# Determine block sizes based on GPU architecture
|
||||
arch = _get_cuda_arch(q.device.index)
|
||||
if arch == "sm90":
|
||||
BLKQ, BLKK = 64, 128
|
||||
else:
|
||||
BLKQ, BLKK = 128, 64
|
||||
|
||||
# Compute block-sparse attention pattern
|
||||
sparse_map, lut, real_topk = get_block_map(q,
|
||||
k,
|
||||
topk_ratio=topk_ratio,
|
||||
BLKQ=BLKQ,
|
||||
BLKK=BLKK)
|
||||
|
||||
# Convert to compute dtype
|
||||
q = q.to(self.dtype)
|
||||
k = k.to(self.dtype)
|
||||
v = v.to(self.dtype)
|
||||
|
||||
# ========== SPARGE QUANTIZED ATTENTION ==========
|
||||
km = k.mean(dim=-2, keepdim=True)
|
||||
headdim = q.size(-1)
|
||||
scale = 1.0 / (headdim**0.5)
|
||||
|
||||
# Quantize Q, K to INT8
|
||||
q_int8, q_scale, k_int8, k_scale = get_vanilla_qk_quant(
|
||||
q, k, km, BLKQ, BLKK)
|
||||
lut_triton, valid_block_num = block_map_lut_triton(sparse_map)
|
||||
|
||||
# Quantize V to FP8
|
||||
b, h_kv, kv_len, head_dim = v.shape
|
||||
padded_len = (kv_len + 127) // 128 * 128
|
||||
v_transposed_permutted = torch.empty((b, h_kv, head_dim, padded_len),
|
||||
dtype=v.dtype,
|
||||
device=v.device)
|
||||
fused.transpose_pad_permute_cuda(v, v_transposed_permutted, 1)
|
||||
v_fp8 = torch.empty(v_transposed_permutted.shape,
|
||||
dtype=torch.float8_e4m3fn,
|
||||
device=v.device)
|
||||
v_scale = torch.empty((b, h_kv, head_dim),
|
||||
dtype=torch.float32,
|
||||
device=v.device)
|
||||
fused.scale_fuse_quant_cuda(v_transposed_permutted, v_fp8, v_scale,
|
||||
kv_len, 2.25, 1)
|
||||
|
||||
# Sparse attention with quantized kernels
|
||||
o_s = torch.empty_like(q)
|
||||
if arch == "sm90":
|
||||
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90(
|
||||
q_int8, k_int8, v_fp8, o_s, lut_triton, valid_block_num,
|
||||
q_scale, k_scale, v_scale, 1, False, 1, scale)
|
||||
else:
|
||||
pvthreshold = torch.full((q.shape[-3], ),
|
||||
1e6,
|
||||
dtype=torch.float32,
|
||||
device=q.device)
|
||||
if SAGE2PP_ENABLED:
|
||||
qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold(
|
||||
q_int8, k_int8, v_fp8, o_s, lut_triton, valid_block_num,
|
||||
pvthreshold, q_scale, k_scale, v_scale, 1, False, 1, scale,
|
||||
0)
|
||||
else:
|
||||
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold(
|
||||
q_int8, k_int8, v_fp8, o_s, lut_triton, valid_block_num,
|
||||
pvthreshold, q_scale, k_scale, v_scale, 1, False, 1, scale,
|
||||
0)
|
||||
# ========== END SPARGE ==========
|
||||
|
||||
# Linear attention with feature maps
|
||||
q_linear = self.feature_map_q(q).contiguous().to(self.dtype)
|
||||
k_linear = self.feature_map_k(k).contiguous().to(self.dtype)
|
||||
o_l = self._calc_linear_attention(q_linear, k_linear, v)
|
||||
|
||||
# Project linear attention output and combine
|
||||
with torch.amp.autocast('cuda', dtype=self.dtype):
|
||||
o_l = self.proj_l(o_l)
|
||||
|
||||
# Combine sparse and linear outputs
|
||||
output = (o_s + o_l).to(original_dtype)
|
||||
|
||||
# Convert back to FastVideo format (B, L, H, D)
|
||||
output = output.transpose(1, 2)
|
||||
|
||||
return output
|
||||
@@ -50,6 +50,10 @@ class DistributedAttention(nn.Module):
|
||||
num_kv_heads=num_kv_heads,
|
||||
prefix=f"{prefix}.impl",
|
||||
**extra_impl_args)
|
||||
# Register attn_impl as submodule if it has learnable parameters (e.g., SLA's proj_l)
|
||||
# This ensures its parameters are included in state_dict() for saving/loading
|
||||
if isinstance(self.attn_impl, nn.Module):
|
||||
self.add_module('attn_impl', self.attn_impl)
|
||||
self.num_heads = num_heads
|
||||
self.head_size = head_size
|
||||
self.num_kv_heads = num_kv_heads
|
||||
|
||||
@@ -18,7 +18,8 @@ class DiTArchConfig(ArchConfig):
|
||||
AttentionBackendEnum.SLIDING_TILE_ATTN, AttentionBackendEnum.SAGE_ATTN,
|
||||
AttentionBackendEnum.FLASH_ATTN, AttentionBackendEnum.TORCH_SDPA,
|
||||
AttentionBackendEnum.VIDEO_SPARSE_ATTN, AttentionBackendEnum.VMOBA_ATTN,
|
||||
AttentionBackendEnum.SAGE_ATTN_THREE)
|
||||
AttentionBackendEnum.SAGE_ATTN_THREE, AttentionBackendEnum.SLA_ATTN,
|
||||
AttentionBackendEnum.SAGE_SLA_ATTN)
|
||||
|
||||
hidden_size: int = 0
|
||||
num_attention_heads: int = 0
|
||||
|
||||
@@ -26,7 +26,6 @@ class LongCatVideoArchConfig(DiTArchConfig):
|
||||
default_factory=lambda: [is_longcat_blocks])
|
||||
|
||||
# Parameter name mapping for weight conversion
|
||||
# Maps original LongCat third_party names -> native FastVideo names
|
||||
param_names_mapping: dict = field(
|
||||
default_factory=lambda: {
|
||||
# Embedders
|
||||
|
||||
@@ -17,11 +17,7 @@ from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
@dataclass
|
||||
class LongCatDiTArchConfig(DiTArchConfig):
|
||||
"""Extended DiTArchConfig with LongCat-specific fields.
|
||||
|
||||
NOTE: This is for Phase 1 wrapper compatibility. For native model (Phase 2),
|
||||
use LongCatVideoConfig from fastvideo.configs.models.dits.longcat instead.
|
||||
"""
|
||||
"""Extended DiTArchConfig with LongCat-specific fields."""
|
||||
# LongCat-specific architecture parameters
|
||||
adaln_tembed_dim: int = 512
|
||||
caption_channels: int = 4096
|
||||
@@ -88,20 +84,16 @@ def umt5_postprocess_text(outputs: BaseEncoderOutput) -> torch.Tensor:
|
||||
|
||||
@dataclass
|
||||
class LongCatT2V480PConfig(PipelineConfig):
|
||||
"""Configuration for LongCat pipeline (480p) aligned to LongCat-Video modules.
|
||||
"""Configuration for LongCat pipeline (480p).
|
||||
|
||||
Components expected by loaders:
|
||||
- tokenizer: AutoTokenizer
|
||||
- text_encoder: UMT5EncoderModel
|
||||
- transformer: LongCatVideoTransformer3DModel (Phase 1 wrapper)
|
||||
OR LongCatTransformer3DModel (Phase 2 native)
|
||||
- transformer: LongCatTransformer3DModel
|
||||
- vae: AutoencoderKLWan (Wan VAE, 4x8 compression)
|
||||
- scheduler: FlowMatchEulerDiscreteScheduler
|
||||
"""
|
||||
|
||||
# DiT config with LongCat-specific arch_config
|
||||
# NOTE: For Phase 1 wrapper, uses LongCatDiTArchConfig
|
||||
# For Phase 2 native model, can use LongCatVideoConfig directly
|
||||
dit_config: DiTConfig = field(
|
||||
default_factory=lambda: DiTConfig(arch_config=LongCatDiTArchConfig()))
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ from fastvideo.configs.pipelines.hunyuan import FastHunyuanConfig, HunyuanConfig
|
||||
from fastvideo.configs.pipelines.hunyuan15 import Hunyuan15T2V480PConfig, Hunyuan15T2V720PConfig
|
||||
from fastvideo.configs.pipelines.stepvideo import StepVideoT2VConfig
|
||||
from fastvideo.configs.pipelines.longcat import LongCatT2V480PConfig
|
||||
from fastvideo.configs.pipelines.turbodiffusion import (
|
||||
TurboDiffusionT2V_1_3B_Config, TurboDiffusionT2V_14B_Config,
|
||||
TurboDiffusionI2V_A14B_Config)
|
||||
|
||||
# isort: off
|
||||
from fastvideo.configs.pipelines.wan import (
|
||||
@@ -55,11 +58,25 @@ PIPE_NAME_TO_CONFIG: dict[str, type[PipelineConfig]] = {
|
||||
"FastVideo/Matrix-Game-2.0-Base-Diffusers": MatrixGameI2V480PConfig,
|
||||
"FastVideo/Matrix-Game-2.0-GTA-Diffusers": MatrixGameI2V480PConfig,
|
||||
"FastVideo/Matrix-Game-2.0-TempleRun-Diffusers": MatrixGameI2V480PConfig,
|
||||
# LongCat Video models
|
||||
"FastVideo/LongCat-Video-T2V-Diffusers": LongCatT2V480PConfig,
|
||||
"FastVideo/LongCat-Video-I2V-Diffusers": LongCatT2V480PConfig,
|
||||
"FastVideo/LongCat-Video-VC-Diffusers": LongCatT2V480PConfig,
|
||||
# TurboDiffusion models
|
||||
"loayrashid/TurboWan2.1-T2V-1.3B-Diffusers": TurboDiffusionT2V_1_3B_Config,
|
||||
"loayrashid/TurboWan2.1-T2V-14B-Diffusers": TurboDiffusionT2V_14B_Config,
|
||||
"loayrashid/TurboWan2.2-I2V-A14B-Diffusers": TurboDiffusionI2V_A14B_Config,
|
||||
# Add other specific weight variants
|
||||
}
|
||||
|
||||
# For determining pipeline type from model ID
|
||||
PIPELINE_DETECTOR: dict[str, Callable[[str], bool]] = {
|
||||
"longcatimagetovideo":
|
||||
lambda id: "longcatimagetovideo" in id.lower(),
|
||||
"longcatvideocontinuation":
|
||||
lambda id: "longcatvideocontinuation" in id.lower(),
|
||||
"longcat":
|
||||
lambda id: "longcat" in id.lower(),
|
||||
"hunyuan":
|
||||
lambda id: "hunyuan" in id.lower(),
|
||||
"hunyuan15":
|
||||
@@ -78,13 +95,15 @@ PIPELINE_DETECTOR: dict[str, Callable[[str], bool]] = {
|
||||
lambda id: "stepvideo" in id.lower(),
|
||||
"cosmos":
|
||||
lambda id: "cosmos" in id.lower(),
|
||||
"longcat":
|
||||
lambda id: "longcat" in id.lower(),
|
||||
"turbodiffusion":
|
||||
lambda id: "turbodiffusion" in id.lower() or "turbowan" in id.lower(),
|
||||
# Add other pipeline architecture detectors
|
||||
}
|
||||
|
||||
# Fallback configs when exact match isn't found but architecture is detected
|
||||
PIPELINE_FALLBACK_CONFIG: dict[str, type[PipelineConfig]] = {
|
||||
"longcatimagetovideo": LongCatT2V480PConfig,
|
||||
"longcatvideocontinuation": LongCatT2V480PConfig,
|
||||
"longcat": LongCatT2V480PConfig,
|
||||
"hunyuan":
|
||||
HunyuanConfig, # Base Hunyuan config as fallback for any Hunyuan variant
|
||||
@@ -96,7 +115,8 @@ PIPELINE_FALLBACK_CONFIG: dict[str, type[PipelineConfig]] = {
|
||||
"wanimagetovideo": WanI2V480PConfig,
|
||||
"wandmdpipeline": FastWan2_1_T2V_480P_Config,
|
||||
"wancausaldmdpipeline": SelfForcingWanT2V480PConfig,
|
||||
"stepvideo": StepVideoT2VConfig
|
||||
"stepvideo": StepVideoT2VConfig,
|
||||
"turbodiffusion": TurboDiffusionT2V_1_3B_Config,
|
||||
# Other fallbacks by architecture
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
TurboDiffusion pipeline configurations.
|
||||
|
||||
TurboDiffusion uses RCM (recurrent Consistency Model) scheduler with
|
||||
SLA (Sparse-Linear Attention) for fast 1-4 step video generation.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from fastvideo.configs.models import DiTConfig, EncoderConfig, VAEConfig
|
||||
from fastvideo.configs.models.dits import WanVideoConfig
|
||||
from fastvideo.configs.models.encoders import CLIPVisionConfig
|
||||
from fastvideo.configs.models.vaes import WanVAEConfig
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
from fastvideo.configs.pipelines.wan import t5_postprocess_text, T5Config, BaseEncoderOutput
|
||||
|
||||
import torch
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionT2VConfig(PipelineConfig):
|
||||
"""Base configuration for TurboDiffusion T2V pipeline.
|
||||
|
||||
Uses RCM scheduler with sigma_max=80 for 1-4 step generation.
|
||||
No boundary_ratio (single model, no switching).
|
||||
"""
|
||||
# DiT
|
||||
dit_config: DiTConfig = field(default_factory=WanVideoConfig)
|
||||
# VAE
|
||||
vae_config: VAEConfig = field(default_factory=WanVAEConfig)
|
||||
vae_tiling: bool = False
|
||||
vae_sp: bool = False
|
||||
|
||||
# Denoising stage
|
||||
flow_shift: float | None = 3.0
|
||||
|
||||
# No boundary_ratio for T2V (single model)
|
||||
boundary_ratio: float | None = None
|
||||
|
||||
# Text encoding stage
|
||||
text_encoder_configs: tuple[EncoderConfig, ...] = field(
|
||||
default_factory=lambda: (T5Config(), ))
|
||||
postprocess_text_funcs: tuple[Callable[[BaseEncoderOutput], torch.Tensor],
|
||||
...] = field(default_factory=lambda:
|
||||
(t5_postprocess_text, ))
|
||||
|
||||
# Precision for each component
|
||||
precision: str = "bf16"
|
||||
vae_precision: str = "fp32"
|
||||
text_encoder_precisions: tuple[str, ...] = field(
|
||||
default_factory=lambda: ("fp32", ))
|
||||
|
||||
# self-forcing params
|
||||
warp_denoising_step: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
self.vae_config.load_encoder = False
|
||||
self.vae_config.load_decoder = True
|
||||
# Ensure no boundary_ratio is set in dit_config
|
||||
self.dit_config.boundary_ratio = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionT2V_1_3B_Config(TurboDiffusionT2VConfig):
|
||||
"""Configuration for TurboDiffusion T2V 1.3B model."""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionT2V_14B_Config(TurboDiffusionT2VConfig):
|
||||
"""Configuration for TurboDiffusion T2V 14B model.
|
||||
|
||||
Uses same config as 1.3B but with higher flow_shift for 14B model.
|
||||
"""
|
||||
flow_shift: float | None = 5.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionI2VConfig(PipelineConfig):
|
||||
"""Base configuration for TurboDiffusion I2V pipeline.
|
||||
|
||||
Uses RCM scheduler with sigma_max=200 for 1-4 step generation.
|
||||
Uses boundary_ratio=0.9 for high-noise to low-noise model switching.
|
||||
"""
|
||||
# DiT
|
||||
dit_config: DiTConfig = field(default_factory=WanVideoConfig)
|
||||
# VAE
|
||||
vae_config: VAEConfig = field(default_factory=WanVAEConfig)
|
||||
vae_tiling: bool = False
|
||||
vae_sp: bool = False
|
||||
|
||||
# Denoising stage
|
||||
flow_shift: float | None = 5.0
|
||||
|
||||
boundary_ratio: float | None = 0.9
|
||||
|
||||
# Text encoding stage
|
||||
text_encoder_configs: tuple[EncoderConfig, ...] = field(
|
||||
default_factory=lambda: (T5Config(), ))
|
||||
postprocess_text_funcs: tuple[Callable[[BaseEncoderOutput], torch.Tensor],
|
||||
...] = field(default_factory=lambda:
|
||||
(t5_postprocess_text, ))
|
||||
|
||||
# Image encoder for I2V
|
||||
image_encoder_config: EncoderConfig = field(
|
||||
default_factory=CLIPVisionConfig)
|
||||
image_encoder_precision: str = "fp32"
|
||||
|
||||
# Precision for each component
|
||||
precision: str = "bf16"
|
||||
vae_precision: str = "fp32"
|
||||
text_encoder_precisions: tuple[str, ...] = field(
|
||||
default_factory=lambda: ("fp32", ))
|
||||
|
||||
# self-forcing params
|
||||
warp_denoising_step: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
self.vae_config.load_encoder = True
|
||||
self.vae_config.load_decoder = True
|
||||
self.dit_config.boundary_ratio = self.boundary_ratio
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionI2V_A14B_Config(TurboDiffusionI2VConfig):
|
||||
"""Configuration for TurboDiffusion I2V A14B model."""
|
||||
pass
|
||||
@@ -223,7 +223,7 @@ class SamplingParam:
|
||||
help="Path to input image for image-to-video generation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--video_path",
|
||||
"--video-path",
|
||||
type=str,
|
||||
default=SamplingParam.video_path,
|
||||
help="Path to input video for video-to-video generation",
|
||||
|
||||
@@ -26,6 +26,11 @@ from fastvideo.configs.sample.wan import (
|
||||
SelfForcingWan2_2_T2V_A14B_480P_SamplingParam,
|
||||
MatrixGame2_SamplingParam,
|
||||
)
|
||||
from fastvideo.configs.sample.turbodiffusion import (
|
||||
TurboDiffusionT2V_1_3B_SamplingParam,
|
||||
TurboDiffusionT2V_14B_SamplingParam,
|
||||
TurboDiffusionI2V_A14B_SamplingParam,
|
||||
)
|
||||
# isort: on
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.utils import (maybe_download_model_index,
|
||||
@@ -34,36 +39,48 @@ from fastvideo.utils import (maybe_download_model_index,
|
||||
logger = init_logger(__name__)
|
||||
# Registry maps specific model weights to their config classes
|
||||
SAMPLING_PARAM_REGISTRY: dict[str, Any] = {
|
||||
"FastVideo/FastHunyuan-diffusers": FastHunyuanSamplingParam,
|
||||
"hunyuanvideo-community/HunyuanVideo": HunyuanSamplingParam,
|
||||
"FastVideo/FastHunyuan-diffusers":
|
||||
FastHunyuanSamplingParam,
|
||||
"hunyuanvideo-community/HunyuanVideo":
|
||||
HunyuanSamplingParam,
|
||||
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v":
|
||||
Hunyuan15_480P_SamplingParam,
|
||||
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v":
|
||||
Hunyuan15_720P_SamplingParam,
|
||||
"FastVideo/stepvideo-t2v-diffusers": StepVideoT2VSamplingParam,
|
||||
"FastVideo/stepvideo-t2v-diffusers":
|
||||
StepVideoT2VSamplingParam,
|
||||
|
||||
# Wan2.1
|
||||
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers": WanT2V_1_3B_SamplingParam,
|
||||
"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,
|
||||
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers":
|
||||
WanT2V_1_3B_SamplingParam,
|
||||
"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,
|
||||
"IRMChen/Wan2.1-Fun-1.3B-Control-Diffusers":
|
||||
Wan2_1_Fun_1_3B_Control_SamplingParam,
|
||||
|
||||
# Wan2.2
|
||||
"Wan-AI/Wan2.2-TI2V-5B-Diffusers": Wan2_2_TI2V_5B_SamplingParam,
|
||||
"Wan-AI/Wan2.2-TI2V-5B-Diffusers":
|
||||
Wan2_2_TI2V_5B_SamplingParam,
|
||||
"FastVideo/FastWan2.2-TI2V-5B-FullAttn-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,
|
||||
|
||||
# FastWan2.1
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers": FastWanT2V480P_SamplingParam,
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers":
|
||||
FastWanT2V480P_SamplingParam,
|
||||
|
||||
# FastWan2.2
|
||||
"FastVideo/FastWan2.2-TI2V-5B-Diffusers": Wan2_2_TI2V_5B_SamplingParam,
|
||||
"FastVideo/FastWan2.2-TI2V-5B-Diffusers":
|
||||
Wan2_2_TI2V_5B_SamplingParam,
|
||||
|
||||
# Causal Self-Forcing Wan2.1
|
||||
"wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers":
|
||||
@@ -80,9 +97,20 @@ SAMPLING_PARAM_REGISTRY: dict[str, Any] = {
|
||||
Cosmos_Predict2_2B_Video2World_SamplingParam,
|
||||
|
||||
# MatrixGame2.0 models
|
||||
"FastVideo/Matrix-Game-2.0-Base-Diffusers": MatrixGame2_SamplingParam,
|
||||
"FastVideo/Matrix-Game-2.0-GTA-Diffusers": MatrixGame2_SamplingParam,
|
||||
"FastVideo/Matrix-Game-2.0-TempleRun-Diffusers": MatrixGame2_SamplingParam,
|
||||
"FastVideo/Matrix-Game-2.0-Base-Diffusers":
|
||||
MatrixGame2_SamplingParam,
|
||||
"FastVideo/Matrix-Game-2.0-GTA-Diffusers":
|
||||
MatrixGame2_SamplingParam,
|
||||
"FastVideo/Matrix-Game-2.0-TempleRun-Diffusers":
|
||||
MatrixGame2_SamplingParam,
|
||||
|
||||
# TurboDiffusion models
|
||||
"loayrashid/TurboWan2.1-T2V-1.3B-Diffusers":
|
||||
TurboDiffusionT2V_1_3B_SamplingParam,
|
||||
"loayrashid/TurboWan2.1-T2V-14B-Diffusers":
|
||||
TurboDiffusionT2V_14B_SamplingParam,
|
||||
"loayrashid/TurboWan2.2-I2V-A14B-Diffusers":
|
||||
TurboDiffusionI2V_A14B_SamplingParam,
|
||||
|
||||
# Add other specific weight variants
|
||||
}
|
||||
@@ -105,6 +133,8 @@ SAMPLING_PARAM_DETECTOR: dict[str, Callable[[str], bool]] = {
|
||||
lambda id: "wancausaldmdpipeline" in id.lower(),
|
||||
"matrixgame":
|
||||
lambda id: "matrixgame" in id.lower() or "matrix-game" in id.lower(),
|
||||
"turbodiffusion":
|
||||
lambda id: "turbodiffusion" in id.lower() or "turbowan" in id.lower(),
|
||||
# Add other pipeline architecture detectors
|
||||
}
|
||||
|
||||
@@ -121,6 +151,8 @@ SAMPLING_FALLBACK_PARAM: dict[str, Any] = {
|
||||
"wancausaldmdpipeline": SelfForcingWan2_1_T2V_1_3B_480P_SamplingParam,
|
||||
"stepvideo": StepVideoT2VSamplingParam,
|
||||
"matrixgame": MatrixGame2_SamplingParam,
|
||||
"turbodiffusion":
|
||||
TurboDiffusionT2V_1_3B_SamplingParam, # Default to T2V for fallback
|
||||
# Other fallbacks by architecture
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
TurboDiffusion sampling parameters.
|
||||
|
||||
TurboDiffusion uses RCM (recurrent Consistency Model) scheduler for
|
||||
1-4 step video generation with no classifier-free guidance.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionT2V_1_3B_SamplingParam(SamplingParam):
|
||||
"""Sampling parameters for TurboDiffusion T2V 1.3B model.
|
||||
|
||||
Uses 4-step RCM sampling with guidance_scale=1.0 (no CFG).
|
||||
"""
|
||||
# Video parameters
|
||||
height: int = 480
|
||||
width: int = 832
|
||||
num_frames: int = 81
|
||||
fps: int = 16
|
||||
|
||||
# Denoising stage - TurboDiffusion uses 1-4 steps with no CFG
|
||||
guidance_scale: float = 1.0
|
||||
num_inference_steps: int = 4
|
||||
|
||||
# No negative prompt needed for TurboDiffusion (no CFG)
|
||||
negative_prompt: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionT2V_14B_SamplingParam(SamplingParam):
|
||||
"""Sampling parameters for TurboDiffusion T2V 14B model.
|
||||
|
||||
Uses 4-step RCM sampling with guidance_scale=1.0 (no CFG).
|
||||
"""
|
||||
# Video parameters (720p for 14B)
|
||||
height: int = 720
|
||||
width: int = 1280
|
||||
num_frames: int = 81
|
||||
fps: int = 16
|
||||
|
||||
# Denoising stage - TurboDiffusion uses 1-4 steps with no CFG
|
||||
guidance_scale: float = 1.0
|
||||
num_inference_steps: int = 4
|
||||
|
||||
# No negative prompt needed for TurboDiffusion (no CFG)
|
||||
negative_prompt: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurboDiffusionI2V_A14B_SamplingParam(SamplingParam):
|
||||
"""Sampling parameters for TurboDiffusion I2V A14B model.
|
||||
|
||||
Uses 4-step RCM sampling with dual-model switching (high/low noise).
|
||||
"""
|
||||
# Video parameters (720p for A14B I2V)
|
||||
height: int = 720
|
||||
width: int = 1280
|
||||
num_frames: int = 81
|
||||
fps: int = 16
|
||||
|
||||
# Denoising stage - TurboDiffusion uses 1-4 steps with no CFG
|
||||
guidance_scale: float = 1.0
|
||||
num_inference_steps: int = 4
|
||||
|
||||
# Note: boundary_ratio is set in the pipeline config (TurboDiffusionI2VConfig),
|
||||
# not here. This keeps sampling params and pipeline config separate.
|
||||
|
||||
# No negative prompt needed for TurboDiffusion (no CFG)
|
||||
negative_prompt: str | None = None
|
||||
@@ -0,0 +1,288 @@
|
||||
import asyncio
|
||||
import os
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
|
||||
import imageio
|
||||
import numpy as np
|
||||
import torch
|
||||
import torchvision
|
||||
from einops import rearrange
|
||||
|
||||
from fastvideo.configs.sample import SamplingParam
|
||||
from fastvideo.entrypoints.video_generator import VideoGenerator
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines import ForwardBatch
|
||||
from fastvideo.utils import align_to, shallow_asdict
|
||||
from fastvideo.worker.executor import Executor
|
||||
from fastvideo.worker.multiproc_executor import MultiprocExecutor
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class IncrementalVideoWriter:
|
||||
|
||||
def __init__(self, path: str, fps: int = 24, block_dir: str | None = None):
|
||||
self._executor = ThreadPoolExecutor(max_workers=2,
|
||||
thread_name_prefix="video_write_")
|
||||
self._path = path
|
||||
self._writer = imageio.get_writer(path, fps=fps, format="mp4")
|
||||
self._pending_main: Future | None = None
|
||||
self._block_dir = block_dir
|
||||
self._block_idx = 0
|
||||
self._fps = fps
|
||||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
return self._path
|
||||
|
||||
def add_frames(self, frames: list[np.ndarray]) -> Future | None:
|
||||
# Wait for previous main video write to complete
|
||||
if self._pending_main is not None:
|
||||
self._pending_main.result()
|
||||
|
||||
# Copy frames to avoid race conditions
|
||||
frames_copy = [f.copy() for f in frames]
|
||||
self._pending_main = self._executor.submit(self._write_frames,
|
||||
frames_copy)
|
||||
|
||||
# Write block file if block_dir is set
|
||||
block_future = None
|
||||
if self._block_dir:
|
||||
self._block_idx += 1
|
||||
block_path = os.path.join(self._block_dir,
|
||||
f"b{self._block_idx}.mp4")
|
||||
block_future = self._executor.submit(self._write_block, frames_copy,
|
||||
block_path)
|
||||
return block_future
|
||||
|
||||
def _write_frames(self, frames: list[np.ndarray]) -> None:
|
||||
for frame in frames:
|
||||
self._writer.append_data(frame)
|
||||
|
||||
def _write_block(self, frames: list[np.ndarray], path: str) -> str:
|
||||
imageio.mimsave(path, frames, fps=self._fps)
|
||||
return path
|
||||
|
||||
def close(self) -> None:
|
||||
if self._pending_main is not None:
|
||||
self._pending_main.result()
|
||||
self._pending_main = None
|
||||
if self._writer:
|
||||
self._writer.close()
|
||||
self._writer = None
|
||||
self._executor.shutdown(wait=True)
|
||||
|
||||
|
||||
class StreamingVideoGenerator(VideoGenerator):
|
||||
"""
|
||||
This class extends VideoGenerator with streaming capabilities,
|
||||
allowing incremental video generation with step-by-step control.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
executor_class: type[Executor],
|
||||
log_stats: bool,
|
||||
use_queue_mode: bool = True):
|
||||
super().__init__(fastvideo_args, executor_class, log_stats)
|
||||
self.accumulated_frames: list[np.ndarray] = []
|
||||
self.sampling_param: SamplingParam | None = None
|
||||
self.batch: ForwardBatch | None = None
|
||||
self._use_queue_mode = use_queue_mode and isinstance(
|
||||
self.executor, MultiprocExecutor)
|
||||
self.writer: IncrementalVideoWriter | None = None
|
||||
self.block_dir: str | None = None
|
||||
self.block_idx: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_fastvideo_args(
|
||||
cls, fastvideo_args: FastVideoArgs) -> "StreamingVideoGenerator":
|
||||
executor_class = Executor.get_class(fastvideo_args)
|
||||
return cls(
|
||||
fastvideo_args=fastvideo_args,
|
||||
executor_class=executor_class,
|
||||
log_stats=False,
|
||||
)
|
||||
|
||||
def reset(
|
||||
self,
|
||||
prompt: str = "A gameplay video of a cyberpunk city",
|
||||
image_path: str | None = None,
|
||||
num_frames: int = 120, # Default max frames
|
||||
**kwargs):
|
||||
self.accumulated_frames = []
|
||||
self.block_idx = 0
|
||||
self.block_dir = None
|
||||
if self.writer:
|
||||
self.writer.close()
|
||||
self.writer = None
|
||||
self.executor.execute_streaming_clear()
|
||||
|
||||
# Handle batch processing from text file
|
||||
if self.sampling_param is None:
|
||||
self.sampling_param = SamplingParam.from_pretrained(
|
||||
self.fastvideo_args.model_path)
|
||||
|
||||
self.sampling_param.update(kwargs)
|
||||
self.sampling_param.prompt = prompt
|
||||
if image_path:
|
||||
self.sampling_param.image_path = image_path
|
||||
self.sampling_param.num_frames = num_frames
|
||||
|
||||
if "output_path" in kwargs:
|
||||
output_path = self._prepare_output_path(kwargs["output_path"],
|
||||
prompt)
|
||||
# Create block directory for individual block files
|
||||
block_dir = output_path.replace(".mp4", "")
|
||||
os.makedirs(block_dir, exist_ok=True)
|
||||
self.block_dir = block_dir
|
||||
self.writer = IncrementalVideoWriter(output_path,
|
||||
fps=24,
|
||||
block_dir=block_dir)
|
||||
|
||||
fastvideo_args = self.fastvideo_args
|
||||
|
||||
self.sampling_param.height = align_to(self.sampling_param.height, 16)
|
||||
self.sampling_param.width = align_to(self.sampling_param.width, 16)
|
||||
|
||||
latents_size = [(self.sampling_param.num_frames - 1) // 4 + 1,
|
||||
self.sampling_param.height // 8,
|
||||
self.sampling_param.width // 8]
|
||||
n_tokens = latents_size[0] * latents_size[1] * latents_size[2]
|
||||
|
||||
self.sampling_param.return_frames = True
|
||||
self.sampling_param.save_video = False
|
||||
|
||||
self.batch = ForwardBatch(
|
||||
**shallow_asdict(self.sampling_param),
|
||||
eta=0.0,
|
||||
n_tokens=n_tokens,
|
||||
VSA_sparsity=fastvideo_args.VSA_sparsity,
|
||||
)
|
||||
|
||||
if self._use_queue_mode:
|
||||
self.executor.submit_reset(self.batch, fastvideo_args)
|
||||
result = self.executor.wait_result()
|
||||
if result.error:
|
||||
raise result.error
|
||||
else:
|
||||
self.executor.execute_streaming_reset(self.batch, fastvideo_args)
|
||||
|
||||
def step(
|
||||
self, keyboard_cond: torch.Tensor,
|
||||
mouse_cond: torch.Tensor) -> tuple[list[np.ndarray], Future | None]:
|
||||
if self.batch is None:
|
||||
raise RuntimeError("Call reset() before step()")
|
||||
|
||||
if self._use_queue_mode and self.executor._streaming_enabled:
|
||||
self.executor.submit_step(keyboard_cond, mouse_cond)
|
||||
result = self.executor.wait_result()
|
||||
if result.error:
|
||||
raise result.error
|
||||
output_batch = result.output_batch
|
||||
else:
|
||||
# Fallback to RPC-based
|
||||
output_batch = self.executor.execute_streaming_step(
|
||||
keyboard_action=keyboard_cond, mouse_action=mouse_cond)
|
||||
|
||||
frames = self._process_output_batch(output_batch)
|
||||
block_future = None
|
||||
if len(frames) > 0:
|
||||
self.accumulated_frames.extend(frames)
|
||||
self.block_idx += 1
|
||||
if self.writer:
|
||||
# Returns Future for block file, or None if no block_dir
|
||||
block_future = self.writer.add_frames(frames)
|
||||
|
||||
return frames, block_future
|
||||
|
||||
async def step_async(
|
||||
self, keyboard_cond: torch.Tensor,
|
||||
mouse_cond: torch.Tensor) -> tuple[list[np.ndarray], Future | None]:
|
||||
if self.batch is None:
|
||||
raise RuntimeError("Call reset() before step_async()")
|
||||
|
||||
if self._use_queue_mode and self.executor._streaming_enabled:
|
||||
self.executor.submit_step(keyboard_cond, mouse_cond)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
result = await loop.run_in_executor(None, self.executor.wait_result)
|
||||
|
||||
if result.error:
|
||||
raise result.error
|
||||
output_batch = result.output_batch
|
||||
else:
|
||||
# Fallback to RPC-based
|
||||
output_batch = await self.executor.execute_streaming_step_async(
|
||||
keyboard_action=keyboard_cond,
|
||||
mouse_action=mouse_cond,
|
||||
)
|
||||
|
||||
frames = self._process_output_batch(output_batch)
|
||||
block_future = None
|
||||
if len(frames) > 0:
|
||||
self.accumulated_frames.extend(frames)
|
||||
self.block_idx += 1
|
||||
if self.writer:
|
||||
block_future = self.writer.add_frames(frames)
|
||||
|
||||
return frames, block_future
|
||||
|
||||
def finalize(self,
|
||||
output_path: str = "streaming_output.mp4",
|
||||
fps: int = 24) -> str:
|
||||
if not self.accumulated_frames:
|
||||
logger.warning("No frames to save.")
|
||||
return ""
|
||||
|
||||
if self.writer:
|
||||
output_path = self.writer.path
|
||||
self.writer.close()
|
||||
self.writer = None
|
||||
logger.info("Saved video to %s", output_path)
|
||||
else:
|
||||
imageio.mimsave(output_path,
|
||||
self.accumulated_frames,
|
||||
fps=fps,
|
||||
format="mp4")
|
||||
logger.info("Saved video to %s", output_path)
|
||||
|
||||
if self._use_queue_mode and self.executor._streaming_enabled:
|
||||
self.executor.submit_clear()
|
||||
else:
|
||||
self.executor.execute_streaming_clear()
|
||||
self.accumulated_frames = []
|
||||
return output_path
|
||||
|
||||
def _process_output_batch(self,
|
||||
output_batch: ForwardBatch) -> list[np.ndarray]:
|
||||
if output_batch.output is None:
|
||||
return []
|
||||
|
||||
samples = output_batch.output
|
||||
# [B, C, T, H, W] or [1, C, T, H, W]
|
||||
if len(samples.shape) == 5:
|
||||
# Rearrange to [T, B, C, H, W] for processing loop
|
||||
videos = rearrange(samples, "b c t h w -> t b c h w")
|
||||
else:
|
||||
logger.warning("Unexpected output shape: %s", samples.shape)
|
||||
return []
|
||||
|
||||
frames = []
|
||||
for x in videos:
|
||||
x = torchvision.utils.make_grid(x, nrow=1)
|
||||
x = x.transpose(0, 1).transpose(1, 2).squeeze(-1)
|
||||
frames.append((x * 255).cpu().numpy().astype(np.uint8))
|
||||
|
||||
return frames
|
||||
|
||||
def shutdown(self):
|
||||
if self.writer:
|
||||
self.writer.close()
|
||||
self.writer = None
|
||||
|
||||
if self._use_queue_mode and self.executor._streaming_enabled:
|
||||
self.executor.disable_streaming()
|
||||
|
||||
super().shutdown()
|
||||
@@ -38,8 +38,10 @@ if TYPE_CHECKING:
|
||||
FASTVIDEO_TORCH_PROFILE_REGIONS: str = ""
|
||||
FASTVIDEO_SERVER_DEV_MODE: bool = False
|
||||
FASTVIDEO_STAGE_LOGGING: bool = False
|
||||
FASTVIDEO_DENOISING_PERF_LOGGING: bool = False
|
||||
FASTVIDEO_HOST_IP: str = ""
|
||||
FASTVIDEO_LOOPBACK_IP: str = ""
|
||||
FASTVIDEO_DISABLE_PIN_MEMORY: str | None = None
|
||||
|
||||
|
||||
def get_default_cache_root() -> str:
|
||||
@@ -136,6 +138,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
"FASTVIDEO_LOOPBACK_IP":
|
||||
lambda: os.getenv("FASTVIDEO_LOOPBACK_IP", ""),
|
||||
|
||||
# Disable pinned memory (e.g., on platforms that do not support it)
|
||||
"FASTVIDEO_DISABLE_PIN_MEMORY":
|
||||
lambda: os.getenv("FASTVIDEO_DISABLE_PIN_MEMORY", None),
|
||||
|
||||
# Number of GPUs per worker in Ray, if it is set to be a fraction,
|
||||
# it allows ray to schedule multiple actors on a single GPU,
|
||||
# so that users can colocate other actors on the same GPUs as FastVideo.
|
||||
@@ -275,6 +281,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
# taken for each stage
|
||||
"FASTVIDEO_STAGE_LOGGING":
|
||||
lambda: bool(int(os.getenv("FASTVIDEO_STAGE_LOGGING", "0"))),
|
||||
|
||||
# Enable per-step denoising perf logging hooks
|
||||
"FASTVIDEO_DENOISING_PERF_LOGGING":
|
||||
lambda: bool(int(os.getenv("FASTVIDEO_DENOISING_PERF_LOGGING", "0"))),
|
||||
}
|
||||
|
||||
# end-env-vars-definition
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Any, TYPE_CHECKING
|
||||
from fastvideo.configs.configs import PreprocessConfig
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig, STA_Mode
|
||||
from fastvideo.configs.utils import clean_cli_args
|
||||
from fastvideo.layers.quantization import QUANTIZATION_METHODS, QuantizationMethods
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.utils import FlexibleArgumentParser, StoreBoolean
|
||||
|
||||
@@ -131,7 +132,8 @@ class FastVideoArgs:
|
||||
|
||||
# CPU offload parameters
|
||||
dit_cpu_offload: bool = True
|
||||
use_fsdp_inference: bool = True
|
||||
use_fsdp_inference: bool = False
|
||||
dit_layerwise_offload: bool = False
|
||||
text_encoder_cpu_offload: bool = True
|
||||
image_encoder_cpu_offload: bool = True
|
||||
vae_cpu_offload: bool = True
|
||||
@@ -170,6 +172,10 @@ class FastVideoArgs:
|
||||
"transformer": True,
|
||||
"vae": True,
|
||||
})
|
||||
|
||||
override_text_encoder_safetensors: str | None = None # path to safetensors file for text encoder override
|
||||
override_text_encoder_quant: QuantizationMethods = None
|
||||
|
||||
override_transformer_cls_name: str | None = None
|
||||
init_weights_from_safetensors: str = "" # path to safetensors file for initial weight loading
|
||||
init_weights_from_safetensors_2: str = "" # path to safetensors file for initial weight loading for transformer_2
|
||||
@@ -416,11 +422,18 @@ class FastVideoArgs:
|
||||
help=
|
||||
"Use CPU offload for DiT inference. Enable if run out of memory with FSDP.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dit-layerwise-offload",
|
||||
action=StoreBoolean,
|
||||
help="Enable layerwise CPU offload with async H2D prefetch overlap.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-fsdp-inference",
|
||||
action=StoreBoolean,
|
||||
help=
|
||||
"Use FSDP for inference by sharding the model weights. Latency is very low due to prefetch--enable if run out of memory.",
|
||||
"Use FSDP for inference by sharding the model weights. FSDP helps reduce GPU memory usage but may introduce"
|
||||
+
|
||||
" weight transfer overhead depending on the specific setup. Enable if run out of memory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text-encoder-cpu-offload",
|
||||
@@ -476,6 +489,19 @@ class FastVideoArgs:
|
||||
default=FastVideoArgs.enable_stage_verification,
|
||||
help="Enable input/output verification for pipeline stages",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-text-encoder-safetensors",
|
||||
type=str,
|
||||
default=FastVideoArgs.override_text_encoder_safetensors,
|
||||
help="Path to safetensors file for text encoder override",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-text-encoder-quant",
|
||||
type=str,
|
||||
choices=QUANTIZATION_METHODS,
|
||||
default=FastVideoArgs.override_text_encoder_quant,
|
||||
help="Quantization method for text encoder override",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-transformer-cls-name",
|
||||
type=str,
|
||||
@@ -582,9 +608,23 @@ class FastVideoArgs:
|
||||
def check_fastvideo_args(self) -> None:
|
||||
"""Validate inference arguments for consistency"""
|
||||
from fastvideo.platforms import current_platform
|
||||
from fastvideo.pin_memory import is_pin_memory_available
|
||||
|
||||
if current_platform.is_mps():
|
||||
self.use_fsdp_inference = False
|
||||
self.dit_layerwise_offload = False
|
||||
|
||||
if self.dit_layerwise_offload:
|
||||
if self.use_fsdp_inference:
|
||||
logger.warning(
|
||||
"dit_layerwise_offload is enabled, automatically disabling use_fsdp_inference."
|
||||
)
|
||||
self.use_fsdp_inference = False
|
||||
if self.dit_cpu_offload:
|
||||
logger.warning(
|
||||
"dit_layerwise_offload is enabled, automatically disabling dit_cpu_offload."
|
||||
)
|
||||
self.dit_cpu_offload = False
|
||||
|
||||
# Validate mode and inference_mode consistency
|
||||
assert isinstance(
|
||||
@@ -649,6 +689,11 @@ class FastVideoArgs:
|
||||
self.pipeline_config.vae_config.load_encoder = True
|
||||
self.preprocess_config.check_preprocess_config()
|
||||
|
||||
if self.pin_cpu_memory and not is_pin_memory_available():
|
||||
logger.warning("Pinned memory is unavailable on this system; "
|
||||
"disabling pin_cpu_memory.")
|
||||
self.pin_cpu_memory = False
|
||||
|
||||
|
||||
_current_fastvideo_args = None
|
||||
|
||||
|
||||
+286
-190
@@ -7,13 +7,20 @@ import torch
|
||||
import torch.nn.functional as F
|
||||
from torch.nn.parameter import Parameter
|
||||
|
||||
from fastvideo.distributed import (divide, get_tp_rank, get_tp_world_size,
|
||||
split_tensor_along_last_dim,
|
||||
tensor_model_parallel_all_gather,
|
||||
tensor_model_parallel_all_reduce)
|
||||
from fastvideo.layers.quantization.base_config import (QuantizationConfig,
|
||||
QuantizeMethodBase)
|
||||
from fastvideo.distributed import (
|
||||
divide,
|
||||
get_tp_rank,
|
||||
get_tp_world_size,
|
||||
split_tensor_along_last_dim,
|
||||
tensor_model_parallel_all_gather,
|
||||
tensor_model_parallel_all_reduce,
|
||||
)
|
||||
from fastvideo.layers.quantization.base_config import (
|
||||
QuantizationConfig,
|
||||
QuantizeMethodBase,
|
||||
)
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
# yapf: disable
|
||||
from fastvideo.models.parameter import (BasevLLMParameter,
|
||||
BlockQuantScaleParameter,
|
||||
@@ -27,12 +34,22 @@ from fastvideo.models.utils import set_weight_attrs
|
||||
logger = init_logger(__name__)
|
||||
|
||||
WEIGHT_LOADER_V2_SUPPORTED = [
|
||||
"CompressedTensorsLinearMethod", "AWQMarlinLinearMethod", "AWQLinearMethod",
|
||||
"GPTQMarlinLinearMethod", "Fp8LinearMethod", "MarlinLinearMethod",
|
||||
"QQQLinearMethod", "GPTQMarlin24LinearMethod", "TPUInt8LinearMethod",
|
||||
"GPTQLinearMethod", "FBGEMMFp8LinearMethod", "ModelOptFp8LinearMethod",
|
||||
"IPEXAWQLinearMethod", "IPEXGPTQLinearMethod", "HQQMarlinMethod",
|
||||
"QuarkLinearMethod"
|
||||
"CompressedTensorsLinearMethod",
|
||||
"AWQMarlinLinearMethod",
|
||||
"AWQLinearMethod",
|
||||
"GPTQMarlinLinearMethod",
|
||||
"Fp8LinearMethod",
|
||||
"MarlinLinearMethod",
|
||||
"QQQLinearMethod",
|
||||
"GPTQMarlin24LinearMethod",
|
||||
"TPUInt8LinearMethod",
|
||||
"GPTQLinearMethod",
|
||||
"FBGEMMFp8LinearMethod",
|
||||
"ModelOptFp8LinearMethod",
|
||||
"IPEXAWQLinearMethod",
|
||||
"IPEXGPTQLinearMethod",
|
||||
"HQQMarlinMethod",
|
||||
"QuarkLinearMethod",
|
||||
]
|
||||
|
||||
|
||||
@@ -41,8 +58,8 @@ def adjust_scalar_to_fused_array(
|
||||
shard_id: str | int) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""For fused modules (QKV and MLP) we have an array of length
|
||||
N that holds 1 scale for each "logical" matrix. So the param
|
||||
is an array of length N. The loaded_weight corresponds to
|
||||
one of the shards on disk. Here, we slice the param based on
|
||||
is an array of length N. The loaded_weight corresponds to
|
||||
one of the shards on disk. Here, we slice the param based on
|
||||
the shard_id for loading.
|
||||
"""
|
||||
qkv_idxs = {"q": 0, "k": 1, "v": 2}
|
||||
@@ -65,18 +82,23 @@ class LinearMethodBase(QuantizeMethodBase):
|
||||
"""Base class for different (maybe quantized) linear methods."""
|
||||
|
||||
@abstractmethod
|
||||
def create_weights(self, layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: list[int], input_size: int,
|
||||
output_size: int, params_dtype: torch.dtype,
|
||||
**extra_weight_attrs) -> None:
|
||||
"""Create weights for a linear layer.
|
||||
def create_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: list[int],
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
params_dtype: torch.dtype,
|
||||
**extra_weight_attrs,
|
||||
) -> None:
|
||||
"""Create weights for a linear layer.
|
||||
The weights will be set as attributes of the layer.
|
||||
|
||||
Args:
|
||||
layer: The layer that is using the LinearMethodBase factory.
|
||||
input_size_per_partition: Size of the weight input dim on rank X.
|
||||
output_partition_sizes: Sizes of the output dim of each logical
|
||||
output_partition_sizes: Sizes of the output dim of each logical
|
||||
weight on rank X. E.g., output_partition_sizes for QKVLinear
|
||||
is a list contains the width of Wq, Wk, Wv on rank X.
|
||||
input_size: Size of the input dim of the weight across all ranks.
|
||||
@@ -86,10 +108,12 @@ class LinearMethodBase(QuantizeMethodBase):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def apply(self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None) -> torch.Tensor:
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Apply the weights in layer to the input tensor.
|
||||
Expects create_weights to have been called before on the layer."""
|
||||
raise NotImplementedError
|
||||
@@ -98,28 +122,37 @@ class LinearMethodBase(QuantizeMethodBase):
|
||||
class UnquantizedLinearMethod(LinearMethodBase):
|
||||
"""Linear method without quantization."""
|
||||
|
||||
def create_weights(self, layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: list[int], input_size: int,
|
||||
output_size: int, params_dtype: torch.dtype,
|
||||
**extra_weight_attrs) -> None:
|
||||
weight = Parameter(torch.empty(
|
||||
sum(output_partition_sizes),
|
||||
input_size_per_partition,
|
||||
dtype=params_dtype,
|
||||
),
|
||||
requires_grad=False)
|
||||
def create_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: list[int],
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
params_dtype: torch.dtype,
|
||||
**extra_weight_attrs,
|
||||
) -> None:
|
||||
weight = Parameter(
|
||||
torch.empty(
|
||||
sum(output_partition_sizes),
|
||||
input_size_per_partition,
|
||||
dtype=params_dtype,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
set_weight_attrs(weight, {"input_dim": 1, "output_dim": 0})
|
||||
layer.register_parameter("weight", weight)
|
||||
set_weight_attrs(weight, extra_weight_attrs)
|
||||
|
||||
def apply(self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None) -> torch.Tensor:
|
||||
output = F.linear(x, layer.weight, bias) if torch.cuda.is_available(
|
||||
) or bias is None else F.linear(
|
||||
x, layer.weight, bias.to(x.dtype)
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
output = (
|
||||
F.linear(x, layer.weight, bias) if torch.cuda.is_available()
|
||||
or bias is None else F.linear(x, layer.weight, bias.to(x.dtype))
|
||||
) # NOTE: this line assumes that we are using amp when using cuda and is needed to account for the fact that amp isn't supported in mps
|
||||
return output
|
||||
|
||||
@@ -157,8 +190,8 @@ class LinearBase(torch.nn.Module):
|
||||
self.quant_config = quant_config
|
||||
self.prefix = prefix
|
||||
if quant_config is None:
|
||||
self.quant_method: QuantizeMethodBase | None = UnquantizedLinearMethod(
|
||||
)
|
||||
self.quant_method: QuantizeMethodBase | None = (
|
||||
UnquantizedLinearMethod())
|
||||
else:
|
||||
self.quant_method = quant_config.get_quant_method(self,
|
||||
prefix=prefix)
|
||||
@@ -181,29 +214,36 @@ class ReplicatedLinear(LinearBase):
|
||||
(e.g. model.layers.0.qkv_proj)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
bias: bool = True,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
super().__init__(input_size,
|
||||
output_size,
|
||||
skip_bias_add,
|
||||
params_dtype,
|
||||
quant_config,
|
||||
prefix=prefix)
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
bias: bool = True,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(
|
||||
input_size,
|
||||
output_size,
|
||||
skip_bias_add,
|
||||
params_dtype,
|
||||
quant_config,
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
# All the linear layer supports quant method.
|
||||
assert self.quant_method is not None
|
||||
self.quant_method.create_weights(self,
|
||||
self.input_size, [self.output_size],
|
||||
self.input_size,
|
||||
self.output_size,
|
||||
self.params_dtype,
|
||||
weight_loader=self.weight_loader)
|
||||
self.quant_method.create_weights(
|
||||
self,
|
||||
self.input_size,
|
||||
[self.output_size],
|
||||
self.input_size,
|
||||
self.output_size,
|
||||
self.params_dtype,
|
||||
weight_loader=self.weight_loader,
|
||||
)
|
||||
|
||||
if bias:
|
||||
self.bias = Parameter(
|
||||
@@ -211,10 +251,13 @@ class ReplicatedLinear(LinearBase):
|
||||
self.output_size,
|
||||
dtype=self.params_dtype,
|
||||
))
|
||||
set_weight_attrs(self.bias, {
|
||||
"output_dim": 0,
|
||||
"weight_loader": self.weight_loader,
|
||||
})
|
||||
set_weight_attrs(
|
||||
self.bias,
|
||||
{
|
||||
"output_dim": 0,
|
||||
"weight_loader": self.weight_loader,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.register_parameter("bias", None)
|
||||
|
||||
@@ -265,19 +308,21 @@ class ColumnParallelLinear(LinearBase):
|
||||
output_sizes: list of output sizes packed into one output, like for QKV
|
||||
the list would be size 3.
|
||||
prefix: The name of the layer in the state dict, including all parents
|
||||
(e.g. model.layers.0.qkv_proj)
|
||||
(e.g. model.layers.0.qkv_proj)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
bias: bool = True,
|
||||
gather_output: bool = False,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
output_sizes: list[int] | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
bias: bool = True,
|
||||
gather_output: bool = False,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
output_sizes: list[int] | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
# Divide the weight matrix along the last dimension.
|
||||
self.tp_size = get_tp_world_size()
|
||||
self.input_size_per_partition = input_size
|
||||
@@ -290,8 +335,14 @@ class ColumnParallelLinear(LinearBase):
|
||||
for output_size in self.output_sizes
|
||||
]
|
||||
|
||||
super().__init__(input_size, output_size, skip_bias_add, params_dtype,
|
||||
quant_config, prefix)
|
||||
super().__init__(
|
||||
input_size,
|
||||
output_size,
|
||||
skip_bias_add,
|
||||
params_dtype,
|
||||
quant_config,
|
||||
prefix,
|
||||
)
|
||||
|
||||
self.gather_output = gather_output
|
||||
|
||||
@@ -308,17 +359,21 @@ class ColumnParallelLinear(LinearBase):
|
||||
params_dtype=self.params_dtype,
|
||||
weight_loader=(
|
||||
self.weight_loader_v2 if self.quant_method.__class__.__name__
|
||||
in WEIGHT_LOADER_V2_SUPPORTED else self.weight_loader))
|
||||
in WEIGHT_LOADER_V2_SUPPORTED else self.weight_loader),
|
||||
)
|
||||
if bias:
|
||||
self.bias = Parameter(
|
||||
torch.empty(
|
||||
self.output_size_per_partition,
|
||||
dtype=params_dtype,
|
||||
))
|
||||
set_weight_attrs(self.bias, {
|
||||
"output_dim": 0,
|
||||
"weight_loader": self.weight_loader,
|
||||
})
|
||||
set_weight_attrs(
|
||||
self.bias,
|
||||
{
|
||||
"output_dim": 0,
|
||||
"weight_loader": self.weight_loader,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.register_parameter("bias", None)
|
||||
|
||||
@@ -401,32 +456,37 @@ class MergedColumnParallelLinear(ColumnParallelLinear):
|
||||
(e.g. model.layers.0.qkv_proj)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
input_size: int,
|
||||
output_sizes: list[int],
|
||||
bias: bool = True,
|
||||
gather_output: bool = False,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_sizes: list[int],
|
||||
bias: bool = True,
|
||||
gather_output: bool = False,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
self.output_sizes = output_sizes
|
||||
tp_size = get_tp_world_size()
|
||||
assert all(output_size % tp_size == 0 for output_size in output_sizes)
|
||||
super().__init__(input_size=input_size,
|
||||
output_size=sum(output_sizes),
|
||||
bias=bias,
|
||||
gather_output=gather_output,
|
||||
skip_bias_add=skip_bias_add,
|
||||
params_dtype=params_dtype,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix)
|
||||
|
||||
def weight_loader(self,
|
||||
param: Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: int | None = None) -> None:
|
||||
super().__init__(
|
||||
input_size=input_size,
|
||||
output_size=sum(output_sizes),
|
||||
bias=bias,
|
||||
gather_output=gather_output,
|
||||
skip_bias_add=skip_bias_add,
|
||||
params_dtype=params_dtype,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
def weight_loader(
|
||||
self,
|
||||
param: Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: int | None = None,
|
||||
) -> None:
|
||||
param_data = param.data
|
||||
output_dim = getattr(param, "output_dim", None)
|
||||
# Special case for AQLM codebooks.
|
||||
@@ -518,20 +578,22 @@ class MergedColumnParallelLinear(ColumnParallelLinear):
|
||||
# Special case for Quantization.
|
||||
# If quantized, we need to adjust the offset and size to account
|
||||
# for the packing.
|
||||
if isinstance(param, PackedColumnParameter | PackedvLLMParameter
|
||||
) and param.packed_dim == param.output_dim:
|
||||
shard_size, shard_offset = \
|
||||
if (isinstance(param, PackedColumnParameter | PackedvLLMParameter)
|
||||
and param.packed_dim == param.output_dim):
|
||||
shard_size, shard_offset = (
|
||||
param.adjust_shard_indexes_for_packing(
|
||||
shard_size=shard_size, shard_offset=shard_offset)
|
||||
shard_size=shard_size, shard_offset=shard_offset))
|
||||
|
||||
loaded_weight_shard = loaded_weight.narrow(param.output_dim,
|
||||
shard_offset, shard_size)
|
||||
self.weight_loader_v2(param, loaded_weight_shard, shard_id)
|
||||
|
||||
def weight_loader_v2(self,
|
||||
param: BasevLLMParameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: int | None = None) -> None:
|
||||
def weight_loader_v2(
|
||||
self,
|
||||
param: BasevLLMParameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: int | None = None,
|
||||
) -> None:
|
||||
if loaded_shard_id is None:
|
||||
if isinstance(param, PerTensorScaleParameter):
|
||||
param.load_merged_column_weight(loaded_weight=loaded_weight,
|
||||
@@ -568,10 +630,12 @@ class MergedColumnParallelLinear(ColumnParallelLinear):
|
||||
shard_offset = sum(self.output_sizes[:loaded_shard_id]) // tp_size
|
||||
shard_size = self.output_sizes[loaded_shard_id] // tp_size
|
||||
|
||||
param.load_merged_column_weight(loaded_weight=loaded_weight,
|
||||
shard_id=loaded_shard_id,
|
||||
shard_offset=shard_offset,
|
||||
shard_size=shard_size)
|
||||
param.load_merged_column_weight(
|
||||
loaded_weight=loaded_weight,
|
||||
shard_id=loaded_shard_id,
|
||||
shard_offset=shard_offset,
|
||||
shard_size=shard_size,
|
||||
)
|
||||
|
||||
|
||||
class QKVParallelLinear(ColumnParallelLinear):
|
||||
@@ -600,16 +664,18 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
(e.g. model.layers.0.qkv_proj)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
hidden_size: int,
|
||||
head_size: int,
|
||||
total_num_heads: int,
|
||||
total_num_kv_heads: int | None = None,
|
||||
bias: bool = True,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
head_size: int,
|
||||
total_num_heads: int,
|
||||
total_num_kv_heads: int | None = None,
|
||||
bias: bool = True,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
self.hidden_size = hidden_size
|
||||
self.head_size = head_size
|
||||
self.total_num_heads = total_num_heads
|
||||
@@ -626,29 +692,31 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
self.num_kv_heads = divide(self.total_num_kv_heads, tp_size)
|
||||
self.num_kv_head_replicas = 1
|
||||
input_size = self.hidden_size
|
||||
output_size = (self.num_heads +
|
||||
2 * self.num_kv_heads) * tp_size * self.head_size
|
||||
output_size = ((self.num_heads + 2 * self.num_kv_heads) * tp_size *
|
||||
self.head_size)
|
||||
self.output_sizes = [
|
||||
self.num_heads * self.head_size * tp_size, # q_proj
|
||||
self.num_kv_heads * self.head_size * tp_size, # k_proj
|
||||
self.num_kv_heads * self.head_size * tp_size, # v_proj
|
||||
self.num_kv_heads * self.head_size * tp_size, # v_proj
|
||||
]
|
||||
|
||||
super().__init__(input_size=input_size,
|
||||
output_size=output_size,
|
||||
bias=bias,
|
||||
gather_output=False,
|
||||
skip_bias_add=skip_bias_add,
|
||||
params_dtype=params_dtype,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix)
|
||||
super().__init__(
|
||||
input_size=input_size,
|
||||
output_size=output_size,
|
||||
bias=bias,
|
||||
gather_output=False,
|
||||
skip_bias_add=skip_bias_add,
|
||||
params_dtype=params_dtype,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
def _get_shard_offset_mapping(self, loaded_shard_id: str) -> int | None:
|
||||
shard_offset_mapping = {
|
||||
"q": 0,
|
||||
"k": self.num_heads * self.head_size,
|
||||
"v": (self.num_heads + self.num_kv_heads) * self.head_size,
|
||||
"total": (self.num_heads + 2 * self.num_kv_heads) * self.head_size
|
||||
"total": (self.num_heads + 2 * self.num_kv_heads) * self.head_size,
|
||||
}
|
||||
return shard_offset_mapping.get(loaded_shard_id)
|
||||
|
||||
@@ -663,7 +731,7 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
def _load_fused_module_from_checkpoint(self, param: BasevLLMParameter,
|
||||
loaded_weight: torch.Tensor):
|
||||
"""
|
||||
Handle special case for models where QKV layers are already
|
||||
Handle special case for models where QKV layers are already
|
||||
fused on disk. In this case, we have no shard id. This function
|
||||
determmines the shard id by splitting these layers and then calls
|
||||
the weight loader using the shard id.
|
||||
@@ -674,31 +742,39 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
shard_offsets = [
|
||||
# (shard_id, shard_offset, shard_size)
|
||||
("q", 0, self.total_num_heads * self.head_size),
|
||||
("k", self.total_num_heads * self.head_size,
|
||||
self.total_num_kv_heads * self.head_size),
|
||||
("v",
|
||||
(self.total_num_heads + self.total_num_kv_heads) * self.head_size,
|
||||
self.total_num_kv_heads * self.head_size),
|
||||
(
|
||||
"k",
|
||||
self.total_num_heads * self.head_size,
|
||||
self.total_num_kv_heads * self.head_size,
|
||||
),
|
||||
(
|
||||
"v",
|
||||
(self.total_num_heads + self.total_num_kv_heads) *
|
||||
self.head_size,
|
||||
self.total_num_kv_heads * self.head_size,
|
||||
),
|
||||
]
|
||||
|
||||
for shard_id, shard_offset, shard_size in shard_offsets:
|
||||
# Special case for Quantization.
|
||||
# If quantized, we need to adjust the offset and size to account
|
||||
# for the packing.
|
||||
if isinstance(param, PackedColumnParameter | PackedvLLMParameter
|
||||
) and param.packed_dim == param.output_dim:
|
||||
shard_size, shard_offset = \
|
||||
if (isinstance(param, PackedColumnParameter | PackedvLLMParameter)
|
||||
and param.packed_dim == param.output_dim):
|
||||
shard_size, shard_offset = (
|
||||
param.adjust_shard_indexes_for_packing(
|
||||
shard_size=shard_size, shard_offset=shard_offset)
|
||||
shard_size=shard_size, shard_offset=shard_offset))
|
||||
|
||||
loaded_weight_shard = loaded_weight.narrow(param.output_dim,
|
||||
shard_offset, shard_size)
|
||||
self.weight_loader_v2(param, loaded_weight_shard, shard_id)
|
||||
|
||||
def weight_loader_v2(self,
|
||||
param: BasevLLMParameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: str | None = None):
|
||||
def weight_loader_v2(
|
||||
self,
|
||||
param: BasevLLMParameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: str | None = None,
|
||||
):
|
||||
if loaded_shard_id is None: # special case for certain models
|
||||
if isinstance(param, PerTensorScaleParameter):
|
||||
param.load_qkv_weight(loaded_weight=loaded_weight, shard_id=0)
|
||||
@@ -715,17 +791,20 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
shard_offset = self._get_shard_offset_mapping(loaded_shard_id)
|
||||
shard_size = self._get_shard_size_mapping(loaded_shard_id)
|
||||
|
||||
param.load_qkv_weight(loaded_weight=loaded_weight,
|
||||
num_heads=self.num_kv_head_replicas,
|
||||
shard_id=loaded_shard_id,
|
||||
shard_offset=shard_offset,
|
||||
shard_size=shard_size)
|
||||
|
||||
def weight_loader(self,
|
||||
param: Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: str | None = None):
|
||||
param.load_qkv_weight(
|
||||
loaded_weight=loaded_weight,
|
||||
num_heads=self.num_kv_head_replicas,
|
||||
shard_id=loaded_shard_id,
|
||||
shard_offset=shard_offset,
|
||||
shard_size=shard_size,
|
||||
)
|
||||
|
||||
def weight_loader(
|
||||
self,
|
||||
param: Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: str | None = None,
|
||||
):
|
||||
param_data = param.data
|
||||
output_dim = getattr(param, "output_dim", None)
|
||||
# Special case for AQLM codebooks.
|
||||
@@ -748,14 +827,20 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
shard_offsets = [
|
||||
# (shard_id, shard_offset, shard_size)
|
||||
("q", 0, self.total_num_heads * self.head_size),
|
||||
("k", self.total_num_heads * self.head_size,
|
||||
self.total_num_kv_heads * self.head_size),
|
||||
("v", (self.total_num_heads + self.total_num_kv_heads) *
|
||||
self.head_size, self.total_num_kv_heads * self.head_size),
|
||||
(
|
||||
"k",
|
||||
self.total_num_heads * self.head_size,
|
||||
self.total_num_kv_heads * self.head_size,
|
||||
),
|
||||
(
|
||||
"v",
|
||||
(self.total_num_heads + self.total_num_kv_heads) *
|
||||
self.head_size,
|
||||
self.total_num_kv_heads * self.head_size,
|
||||
),
|
||||
]
|
||||
|
||||
for shard_id, shard_offset, shard_size in shard_offsets:
|
||||
|
||||
loaded_weight_shard = loaded_weight.narrow(
|
||||
output_dim, shard_offset, shard_size)
|
||||
self.weight_loader(param, loaded_weight_shard, shard_id)
|
||||
@@ -843,16 +928,18 @@ class RowParallelLinear(LinearBase):
|
||||
quant_config: Quantization configure.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
bias: bool = True,
|
||||
input_is_parallel: bool = True,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
reduce_results: bool = True,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
bias: bool = True,
|
||||
input_is_parallel: bool = True,
|
||||
skip_bias_add: bool = False,
|
||||
params_dtype: torch.dtype | None = None,
|
||||
reduce_results: bool = True,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
# Divide the weight matrix along the first dimension.
|
||||
self.tp_rank = get_tp_rank()
|
||||
self.tp_size = get_tp_world_size()
|
||||
@@ -860,8 +947,14 @@ class RowParallelLinear(LinearBase):
|
||||
self.output_size_per_partition = output_size
|
||||
self.output_partition_sizes = [output_size]
|
||||
|
||||
super().__init__(input_size, output_size, skip_bias_add, params_dtype,
|
||||
quant_config, prefix)
|
||||
super().__init__(
|
||||
input_size,
|
||||
output_size,
|
||||
skip_bias_add,
|
||||
params_dtype,
|
||||
quant_config,
|
||||
prefix,
|
||||
)
|
||||
|
||||
self.input_is_parallel = input_is_parallel
|
||||
self.reduce_results = reduce_results
|
||||
@@ -876,7 +969,8 @@ class RowParallelLinear(LinearBase):
|
||||
params_dtype=self.params_dtype,
|
||||
weight_loader=(
|
||||
self.weight_loader_v2 if self.quant_method.__class__.__name__
|
||||
in WEIGHT_LOADER_V2_SUPPORTED else self.weight_loader))
|
||||
in WEIGHT_LOADER_V2_SUPPORTED else self.weight_loader),
|
||||
)
|
||||
if not reduce_results and (bias and not skip_bias_add):
|
||||
raise ValueError("When not reduce the results, adding bias to the "
|
||||
"results can lead to incorrect results")
|
||||
@@ -884,10 +978,13 @@ class RowParallelLinear(LinearBase):
|
||||
if bias:
|
||||
self.bias = Parameter(
|
||||
torch.empty(self.output_size, dtype=params_dtype))
|
||||
set_weight_attrs(self.bias, {
|
||||
"output_dim": 0,
|
||||
"weight_loader": self.weight_loader,
|
||||
})
|
||||
set_weight_attrs(
|
||||
self.bias,
|
||||
{
|
||||
"output_dim": 0,
|
||||
"weight_loader": self.weight_loader,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.register_parameter("bias", None)
|
||||
|
||||
@@ -916,7 +1013,6 @@ class RowParallelLinear(LinearBase):
|
||||
|
||||
def weight_loader_v2(self, param: BasevLLMParameter,
|
||||
loaded_weight: torch.Tensor):
|
||||
|
||||
# Special case for loading scales off disk, which often do not
|
||||
# have a shape (such as in the case of AutoFP8).
|
||||
if len(loaded_weight.shape) == 0:
|
||||
|
||||
@@ -18,6 +18,7 @@ from fastvideo.layers.linear import (ColumnParallelLinear, LinearBase,
|
||||
QKVParallelLinear, ReplicatedLinear,
|
||||
RowParallelLinear)
|
||||
from fastvideo.layers.vocab_parallel_embedding import VocabParallelEmbedding
|
||||
from fastvideo.pin_memory import is_pin_memory_available
|
||||
from fastvideo.utils import get_mixed_precision_state
|
||||
|
||||
torch._dynamo.config.recompile_limit = 16
|
||||
@@ -155,8 +156,11 @@ class BaseLayerWithLoRA(nn.Module):
|
||||
get_local_torch_device(),
|
||||
non_blocking=True).full_tensor().to(current_device))
|
||||
|
||||
offload_policy = CPUOffloadPolicy() if "cpu" in str(
|
||||
current_device) else OffloadPolicy()
|
||||
if "cpu" in str(current_device):
|
||||
offload_policy = CPUOffloadPolicy(
|
||||
pin_memory=is_pin_memory_available())
|
||||
else:
|
||||
offload_policy = OffloadPolicy()
|
||||
mp_policy = get_mixed_precision_state().mp_policy
|
||||
|
||||
self.base_layer = fully_shard(unsharded_base_layer,
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Literal, get_args
|
||||
|
||||
from fastvideo.layers.quantization.base_config import QuantizationConfig
|
||||
|
||||
QuantizationMethods = Literal[None]
|
||||
QuantizationMethods = Literal[None, "AbsMaxFP8"]
|
||||
|
||||
QUANTIZATION_METHODS: list[str] = list(get_args(QuantizationMethods))
|
||||
|
||||
@@ -50,7 +50,12 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
|
||||
if quantization not in QUANTIZATION_METHODS:
|
||||
raise ValueError(f"Invalid quantization method: {quantization}")
|
||||
|
||||
method_to_config: dict[str, type[QuantizationConfig]] = {}
|
||||
# lazy import to avoid triggering `torch.compile` too early
|
||||
from .absmax_fp8 import AbsMaxFP8Config
|
||||
|
||||
method_to_config: dict[str, type[QuantizationConfig]] = {
|
||||
"AbsMaxFP8": AbsMaxFP8Config,
|
||||
}
|
||||
# Update the `method_to_config` with customized quantization methods.
|
||||
method_to_config.update(_CUSTOMIZED_METHOD_TO_QUANT_CONFIG)
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
from typing import Any
|
||||
import torch
|
||||
from fastvideo.distributed.parallel_state import get_tp_world_size
|
||||
from fastvideo.layers.linear import (
|
||||
LinearBase,
|
||||
LinearMethodBase,
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
)
|
||||
from fastvideo.layers.quantization import QuantizationMethods
|
||||
from fastvideo.layers.quantization.base_config import (
|
||||
QuantizationConfig,
|
||||
QuantizeMethodBase,
|
||||
)
|
||||
from fastvideo.models.utils import set_weight_attrs
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class AbsMaxFP8Config(QuantizationConfig):
|
||||
"""
|
||||
Config class for absmax float8_e4m3fn quantization.
|
||||
Currently only support per-tensor quantization.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_config_filenames() -> list[str]:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict[str, Any]) -> "QuantizationConfig":
|
||||
return cls()
|
||||
|
||||
def get_name(self) -> QuantizationMethods:
|
||||
return "AbsMaxFP8"
|
||||
|
||||
def get_supported_act_dtypes(self) -> list[torch.dtype]:
|
||||
return [torch.bfloat16, torch.float16, torch.float32]
|
||||
|
||||
@classmethod
|
||||
def get_min_capability(cls) -> int:
|
||||
return 75
|
||||
|
||||
def get_quant_method(self, layer: torch.nn.Module,
|
||||
prefix: str) -> QuantizeMethodBase | None:
|
||||
if isinstance(layer, LinearBase):
|
||||
return AbsMaxFP8LinearMethod()
|
||||
return None
|
||||
|
||||
|
||||
class AbsMaxFP8Parameter(nn.Parameter):
|
||||
|
||||
def weight_loader(
|
||||
self,
|
||||
param: nn.Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
_share_id: str | None = None,
|
||||
) -> None:
|
||||
if len(loaded_weight.shape) == 0:
|
||||
loaded_weight = loaded_weight.reshape(1)
|
||||
|
||||
assert param.size() == loaded_weight.size(), (
|
||||
f"Tried to load weights of size {loaded_weight.size()}"
|
||||
f"to a parameter of size {param.size()}")
|
||||
param.data.copy_(loaded_weight)
|
||||
|
||||
|
||||
class AbsMaxFP8MergedParameter(nn.Parameter):
|
||||
|
||||
def weight_loader(
|
||||
self,
|
||||
param: nn.Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
share_id: str | int | None = None,
|
||||
) -> None:
|
||||
# currently only support QKVParallelLinear and MergedColumnParallelLinear
|
||||
output_partition_sizes: list[int] = self.output_partition_sizes
|
||||
if share_id is None:
|
||||
share_id = 0
|
||||
if isinstance(share_id, str) and share_id in ["q", "k", "v"]:
|
||||
# QKVParallelLinear case
|
||||
share_idx = ["q", "k", "v"].index(share_id)
|
||||
start_idx = sum(output_partition_sizes[:share_idx])
|
||||
end_idx = start_idx + output_partition_sizes[share_idx]
|
||||
elif isinstance(share_id, int):
|
||||
# MergedColumnParallelLinear case
|
||||
tp_size = get_tp_world_size()
|
||||
if tp_size > 1:
|
||||
# TODO: support this case
|
||||
raise NotImplementedError(
|
||||
"AbsMaxFP8MergedParameter with integer share_id is not supported in tensor parallelism greater than 1 yet."
|
||||
)
|
||||
start_idx = sum(output_partition_sizes[:share_id])
|
||||
end_idx = start_idx + output_partition_sizes[share_id]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"AbsMaxFP8MergedParameter requires share_id to be ['q', 'k', 'v'] or int, got {share_id}."
|
||||
)
|
||||
if len(loaded_weight.shape) == 0:
|
||||
loaded_weight = loaded_weight.reshape(1)
|
||||
assert loaded_weight.numel() == 1
|
||||
# fill in the corresponding partition by repeating the val
|
||||
param.data[start_idx:end_idx].fill_(loaded_weight.item())
|
||||
|
||||
|
||||
class AbsMaxFP8LinearMethod(LinearMethodBase):
|
||||
"""Linear method with AbsMax FP8 quantization."""
|
||||
|
||||
@staticmethod
|
||||
def _convert_scale(scale: Any) -> torch.nn.Parameter:
|
||||
if scale is None:
|
||||
scale = torch.tensor([1.0], dtype=torch.float32)
|
||||
if not isinstance(scale, torch.Tensor):
|
||||
scale = torch.tensor([scale], dtype=torch.float32)
|
||||
if scale.dtype != torch.float32:
|
||||
raise NotImplementedError("Only float32 scale is supported")
|
||||
return AbsMaxFP8Parameter(scale, requires_grad=False)
|
||||
|
||||
@staticmethod
|
||||
def _merged_placeholder(
|
||||
output_partition_sizes: list[int], ) -> torch.nn.Parameter:
|
||||
scale = torch.ones(
|
||||
sum(output_partition_sizes),
|
||||
dtype=torch.float32,
|
||||
)
|
||||
para = AbsMaxFP8MergedParameter(
|
||||
scale,
|
||||
False,
|
||||
)
|
||||
set_weight_attrs(
|
||||
para,
|
||||
{
|
||||
"output_partition_sizes": output_partition_sizes,
|
||||
},
|
||||
)
|
||||
return para
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
input_size_per_partition: int,
|
||||
output_partition_sizes: list[int],
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
params_dtype: torch.dtype,
|
||||
**extra_weight_attrs,
|
||||
) -> None:
|
||||
assert params_dtype in [
|
||||
torch.bfloat16, torch.float16, torch.float32
|
||||
], (f"AbsMaxFP8LinearMethod only supports bfloat16, float16, or float32 original dtype, got {params_dtype}."
|
||||
)
|
||||
weight = nn.Parameter(
|
||||
torch.empty(
|
||||
sum(output_partition_sizes),
|
||||
input_size_per_partition,
|
||||
dtype=torch.float8_e4m3fn,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
if isinstance(layer, QKVParallelLinear | MergedColumnParallelLinear):
|
||||
scale_weight = self._merged_placeholder(output_partition_sizes, )
|
||||
else:
|
||||
scale_weight = self._convert_scale(
|
||||
extra_weight_attrs.get("scale_weight"))
|
||||
scale_input = self._convert_scale(extra_weight_attrs.get("scale_input"))
|
||||
|
||||
set_weight_attrs(weight, {"input_dim": 1, "output_dim": 0})
|
||||
layer.register_parameter("weight", weight)
|
||||
layer.register_parameter("scale_weight", scale_weight)
|
||||
layer.register_parameter("scale_input", scale_input)
|
||||
set_weight_attrs(
|
||||
weight,
|
||||
{
|
||||
"output_dtype": params_dtype,
|
||||
},
|
||||
)
|
||||
set_weight_attrs(weight, extra_weight_attrs)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
weight_quant = layer.weight
|
||||
output_dtype: torch.dtype = weight_quant.output_dtype
|
||||
scale_weight: torch.Tensor = layer.scale_weight.data.to(output_dtype)
|
||||
scale_input: torch.Tensor = layer.scale_input.data.to(output_dtype)
|
||||
weight_output_type = weight_quant.to(dtype=output_dtype)
|
||||
weight_final = weight_output_type * scale_weight.unsqueeze(1)
|
||||
x_final = x.to(dtype=output_dtype) * scale_input
|
||||
|
||||
return nn.functional.linear(x_final, weight_final,
|
||||
bias=bias).to(dtype=output_dtype)
|
||||
@@ -1,9 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Native LongCat Video DiT implementation using FastVideo conventions.
|
||||
|
||||
This is a Phase 2 reimplementation that replaces the third_party wrapper
|
||||
with native FastVideo layers for better performance and integration.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
@@ -129,7 +126,7 @@ class TimestepEmbedder(nn.Module):
|
||||
# Sinusoidal embedding in FP32
|
||||
t_freq = self.timestep_embedding(t.flatten(), self.frequency_embedding_size)
|
||||
|
||||
# Cast to model dtype before MLP
|
||||
# Cast to model dtype before MLP (matching original LongCat)
|
||||
# Handle LoRA wrapper if present
|
||||
linear_layer = self.linear_1.base_layer if hasattr(self.linear_1, 'base_layer') else self.linear_1
|
||||
target_dtype = linear_layer.weight.dtype
|
||||
@@ -166,13 +163,14 @@ class CaptionEmbedder(nn.Module):
|
||||
self.text_tokens_zero_pad = text_tokens_zero_pad
|
||||
|
||||
# Two-layer MLP using ReplicatedLinear
|
||||
# CRITICAL: Original LongCat uses GELU(approximate="tanh"), NOT SiLU!
|
||||
self.linear_1 = ReplicatedLinear(
|
||||
caption_channels,
|
||||
hidden_size,
|
||||
bias=True,
|
||||
params_dtype=dtype,
|
||||
)
|
||||
self.act = nn.SiLU()
|
||||
self.act = nn.GELU(approximate="tanh") # Match original LongCat
|
||||
self.linear_2 = ReplicatedLinear(
|
||||
hidden_size,
|
||||
hidden_size,
|
||||
@@ -268,10 +266,19 @@ class LongCatSelfAttention(nn.Module):
|
||||
self,
|
||||
x: torch.Tensor, # [B, N, C]
|
||||
latent_shape: tuple, # (T, H, W)
|
||||
num_cond_latents: int = 0, # Number of conditioning latent frames (for I2V)
|
||||
return_kv: bool = False, # Return K/V for caching
|
||||
**kwargs
|
||||
) -> torch.Tensor:
|
||||
) -> torch.Tensor | tuple:
|
||||
"""
|
||||
Forward pass with 3D RoPE and optional BSA.
|
||||
|
||||
For I2V mode (num_cond_latents > 0):
|
||||
- Conditioned tokens only attend to themselves
|
||||
- Noise tokens attend to ALL tokens (cond + noise)
|
||||
|
||||
Args:
|
||||
return_kv: If True, return (output, (k_cache, v_cache)) for KV caching
|
||||
"""
|
||||
B, N, C = x.shape
|
||||
T, H, W = latent_shape
|
||||
@@ -290,6 +297,12 @@ class LongCatSelfAttention(nn.Module):
|
||||
q = self.q_norm(q)
|
||||
k = self.k_norm(k)
|
||||
|
||||
# Save pre-RoPE K/V for cache if requested (before RoPE is applied)
|
||||
if return_kv:
|
||||
# [B, N, num_heads, head_dim] -> [B, num_heads, N, head_dim]
|
||||
k_cache = k.transpose(1, 2).clone()
|
||||
v_cache = v.transpose(1, 2).clone()
|
||||
|
||||
# For RoPE: need [B, num_heads, N, head_dim]
|
||||
q_rope = q.transpose(1, 2)
|
||||
k_rope = k.transpose(1, 2)
|
||||
@@ -301,6 +314,50 @@ class LongCatSelfAttention(nn.Module):
|
||||
q = q_rope.transpose(1, 2)
|
||||
k = k_rope.transpose(1, 2)
|
||||
|
||||
# === I2V Split Attention ===
|
||||
# For I2V, conditioned tokens and noise tokens are processed separately
|
||||
if num_cond_latents > 0:
|
||||
# Calculate number of conditioned tokens (cond_latents * spatial_tokens_per_frame)
|
||||
num_cond_tokens = num_cond_latents * (N // T)
|
||||
|
||||
# Conditioned tokens: only attend to themselves (same seq length, use self.attn)
|
||||
q_cond = q[:, :num_cond_tokens].contiguous()
|
||||
k_cond = k[:, :num_cond_tokens].contiguous()
|
||||
v_cond = v[:, :num_cond_tokens].contiguous()
|
||||
out_cond, _ = self.attn(q_cond, k_cond, v_cond)
|
||||
|
||||
# Noise tokens: attend to ALL tokens (different seq lengths!)
|
||||
# Need to use flash attention directly since q has different length than k/v
|
||||
q_noise = q[:, num_cond_tokens:].contiguous() # [B, N_noise, num_heads, head_dim]
|
||||
# k, v are full: [B, N, num_heads, head_dim]
|
||||
|
||||
# Transpose for flash attention: [B, num_heads, seq, head_dim]
|
||||
q_noise_t = q_noise.transpose(1, 2)
|
||||
k_t = k.transpose(1, 2)
|
||||
v_t = v.transpose(1, 2)
|
||||
|
||||
# Use scaled dot product attention (handles different q/kv lengths)
|
||||
out_noise_t = torch.nn.functional.scaled_dot_product_attention(
|
||||
q_noise_t, k_t, v_t,
|
||||
attn_mask=None,
|
||||
dropout_p=0.0,
|
||||
is_causal=False
|
||||
) # [B, num_heads, N_noise, head_dim]
|
||||
|
||||
# Transpose back: [B, N_noise, num_heads, head_dim]
|
||||
out_noise = out_noise_t.transpose(1, 2)
|
||||
|
||||
# Merge conditioned and noise outputs
|
||||
out = torch.cat([out_cond, out_noise], dim=1)
|
||||
|
||||
# Reshape and project out
|
||||
out = out.reshape(B, N, C)
|
||||
out, _ = self.to_out(out)
|
||||
|
||||
if return_kv:
|
||||
return out, (k_cache, v_cache)
|
||||
return out
|
||||
|
||||
# === Attention: BSA or standard ===
|
||||
if self.enable_bsa and T > 1: # Only use BSA for multi-frame videos
|
||||
# BSA expects [B, H, S, D] format
|
||||
@@ -348,6 +405,96 @@ class LongCatSelfAttention(nn.Module):
|
||||
out = out.reshape(B, N, C)
|
||||
out, _ = self.to_out(out)
|
||||
|
||||
if return_kv:
|
||||
return out, (k_cache, v_cache)
|
||||
return out
|
||||
|
||||
def forward_with_kv_cache(
|
||||
self,
|
||||
x: torch.Tensor, # [B, N_noise, C] - only noise tokens
|
||||
latent_shape: tuple, # (T_noise, H, W) - shape for noise only
|
||||
num_cond_latents: int, # Number of conditioning latent frames
|
||||
kv_cache: tuple, # (k_cond, v_cond) - [B, heads, N_cond, head_dim]
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Forward using cached K/V from conditioning frames.
|
||||
|
||||
x contains only NOISE tokens.
|
||||
kv_cache contains pre-computed K/V for CONDITIONING tokens.
|
||||
|
||||
CRITICAL: RoPE positions for noise tokens must start AFTER conditioning.
|
||||
We achieve this by padding Q with dummy tokens for conditioning positions,
|
||||
applying RoPE to the full sequence, then extracting only noise token Q.
|
||||
"""
|
||||
B, N, C = x.shape
|
||||
T, H, W = latent_shape
|
||||
|
||||
k_cache, v_cache = kv_cache
|
||||
|
||||
# Handle batch size mismatch (cache might be smaller for CFG)
|
||||
# When using CFG, latent_model_input is doubled [neg, pos], but cache is for original batch
|
||||
if k_cache.shape[0] != B:
|
||||
# Expand cache to match input batch size
|
||||
# For CFG: repeat the cache for both negative and positive branches
|
||||
repeat_factor = B // k_cache.shape[0]
|
||||
k_cache = k_cache.repeat(repeat_factor, 1, 1, 1)
|
||||
v_cache = v_cache.repeat(repeat_factor, 1, 1, 1)
|
||||
|
||||
# Project to Q/K/V for noise tokens
|
||||
q, _ = self.to_q(x)
|
||||
k, _ = self.to_k(x)
|
||||
v, _ = self.to_v(x)
|
||||
|
||||
# Reshape to heads: [B, N, num_heads, head_dim]
|
||||
q = q.view(B, N, self.num_heads, self.head_dim)
|
||||
k = k.view(B, N, self.num_heads, self.head_dim)
|
||||
v = v.view(B, N, self.num_heads, self.head_dim)
|
||||
|
||||
# Per-head RMS normalization
|
||||
q = self.q_norm(q)
|
||||
k = self.k_norm(k)
|
||||
|
||||
# Transpose for RoPE: [B, heads, N, head_dim]
|
||||
q_rope = q.transpose(1, 2)
|
||||
k_rope = k.transpose(1, 2)
|
||||
v = v.transpose(1, 2)
|
||||
|
||||
# CRITICAL: Apply RoPE with correct positional offset
|
||||
# Noise frame queries need positions starting from num_cond_latents
|
||||
# Following the original LongCat approach:
|
||||
# 1. Pad Q with dummy tokens matching k_cache shape
|
||||
# 2. Apply RoPE to full sequence (T_cond + T_noise)
|
||||
# 3. Extract only the noise portion of Q
|
||||
|
||||
# Create dummy Q padding to fill conditioning positions
|
||||
# k_cache shape: [B, heads, N_cond, head_dim]
|
||||
q_padding = torch.cat([torch.empty_like(k_cache), q_rope], dim=2).contiguous()
|
||||
|
||||
# Concatenate cached K with noise K for RoPE
|
||||
k_full = torch.cat([k_cache, k_rope], dim=2)
|
||||
v_full = torch.cat([v_cache, v], dim=2)
|
||||
|
||||
# Apply RoPE to full sequence (includes both cond and noise positions)
|
||||
# Grid size: (T_cond + T_noise, H, W)
|
||||
full_T = num_cond_latents + T
|
||||
q_padding, k_full = self.rope_3d(q_padding, k_full, grid_size=(full_T, H, W))
|
||||
|
||||
# Extract only the noise portion of Q (last N tokens)
|
||||
q_rope = q_padding[:, :, -N:].contiguous()
|
||||
|
||||
# Run attention: Q_noise attends to full K/V (cond + noise)
|
||||
out = torch.nn.functional.scaled_dot_product_attention(
|
||||
q_rope, k_full, v_full,
|
||||
attn_mask=None,
|
||||
dropout_p=0.0,
|
||||
is_causal=False
|
||||
) # [B, heads, N_noise, head_dim]
|
||||
|
||||
# Transpose back: [B, N_noise, heads, head_dim]
|
||||
out = out.transpose(1, 2)
|
||||
out = out.reshape(B, N, C)
|
||||
out, _ = self.to_out(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -394,6 +541,8 @@ class LongCatCrossAttention(nn.Module):
|
||||
self,
|
||||
x: torch.Tensor, # [B, N_img, C]
|
||||
context: torch.Tensor, # [B, N_text, C]
|
||||
latent_shape: tuple = None, # (T, H, W) - needed for I2V
|
||||
num_cond_latents: int = 0, # Number of conditioning latent frames (for I2V)
|
||||
**kwargs
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
@@ -402,9 +551,57 @@ class LongCatCrossAttention(nn.Module):
|
||||
Args:
|
||||
x: Image tokens [B, N_img, C]
|
||||
context: Text tokens [B, N_text, C] (standard padded format)
|
||||
latent_shape: (T, H, W) - needed for calculating num_cond_tokens
|
||||
num_cond_latents: Number of conditioning latent frames (for I2V)
|
||||
|
||||
For I2V mode (num_cond_latents > 0):
|
||||
- Conditioned tokens get ZERO cross-attention output
|
||||
- Only noise tokens get cross-attention with text
|
||||
"""
|
||||
B, N_img, C = x.shape
|
||||
|
||||
# === I2V: Only noise tokens get cross-attention ===
|
||||
if num_cond_latents > 0 and latent_shape is not None:
|
||||
T, H, W = latent_shape
|
||||
num_cond_tokens = num_cond_latents * (N_img // T)
|
||||
|
||||
# Only process noise tokens
|
||||
x_noise = x[:, num_cond_tokens:] # [B, N_noise, C]
|
||||
|
||||
# Project Q, K, V for noise tokens only
|
||||
q, _ = self.to_q(x_noise)
|
||||
k, _ = self.to_k(context)
|
||||
v, _ = self.to_v(context)
|
||||
|
||||
N_text = context.shape[1]
|
||||
N_noise = x_noise.shape[1]
|
||||
|
||||
# Reshape to heads
|
||||
q = q.view(B, N_noise, self.num_heads, self.head_dim)
|
||||
k = k.view(B, N_text, self.num_heads, self.head_dim)
|
||||
v = v.view(B, N_text, self.num_heads, self.head_dim)
|
||||
|
||||
# Per-head RMS normalization
|
||||
q = self.q_norm(q)
|
||||
k = self.k_norm(k)
|
||||
|
||||
# Run cross-attention
|
||||
out_noise = self.attn(q, k, v) # [B, N_noise, num_heads, head_dim]
|
||||
out_noise = out_noise.reshape(B, N_noise, C)
|
||||
out_noise, _ = self.to_out(out_noise)
|
||||
|
||||
# Conditioned tokens get zero output
|
||||
out_cond = torch.zeros(
|
||||
(B, num_cond_tokens, C),
|
||||
dtype=out_noise.dtype,
|
||||
device=out_noise.device
|
||||
)
|
||||
|
||||
# Merge
|
||||
out = torch.cat([out_cond, out_noise], dim=1)
|
||||
return out
|
||||
|
||||
# === Standard cross-attention ===
|
||||
# Project Q, K, V (standard cross-attention like WanVideo/StepVideo/Cosmos)
|
||||
q, _ = self.to_q(x)
|
||||
k, _ = self.to_k(context)
|
||||
@@ -475,19 +672,19 @@ class LongCatSwiGLUFFN(nn.Module):
|
||||
|
||||
def modulate_fp32(norm: nn.Module, x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Apply modulation in FP32 for numerical stability (matching original LongCat).
|
||||
Apply modulation in FP32 for numerical stability.
|
||||
|
||||
shift and scale should already be FP32 from torch.amp.autocast context.
|
||||
Converts inputs to FP32 for the modulation operation, then casts back.
|
||||
"""
|
||||
# Ensure modulation params are FP32 (should be from autocast)
|
||||
assert shift.dtype == torch.float32 and scale.dtype == torch.float32, \
|
||||
f"shift and scale must be FP32, got {shift.dtype} and {scale.dtype}"
|
||||
|
||||
orig_dtype = x.dtype
|
||||
|
||||
# Convert to FP32 for numerical stability
|
||||
shift_fp32 = shift.float()
|
||||
scale_fp32 = scale.float()
|
||||
|
||||
# Normalize and modulate in FP32
|
||||
x_norm = norm(x.to(torch.float32))
|
||||
x_mod = x_norm * (scale + 1) + shift
|
||||
x_mod = x_norm * (scale_fp32 + 1) + shift_fp32
|
||||
|
||||
return x_mod.to(orig_dtype)
|
||||
|
||||
@@ -568,10 +765,21 @@ class LongCatTransformerBlock(nn.Module):
|
||||
context: torch.Tensor, # [B, N_text, C]
|
||||
t: torch.Tensor, # [B, T, C_t]
|
||||
latent_shape: tuple, # (T, H, W)
|
||||
num_cond_latents: int = 0, # Number of conditioning latent frames (for I2V)
|
||||
return_kv: bool = False, # Return K/V for caching
|
||||
kv_cache: tuple | None = None, # Pre-computed K/V cache
|
||||
skip_crs_attn: bool = False, # Skip cross-attention (for cache init)
|
||||
**kwargs
|
||||
) -> torch.Tensor:
|
||||
) -> torch.Tensor | tuple:
|
||||
"""
|
||||
Forward pass with AdaLN modulation.
|
||||
|
||||
Args:
|
||||
num_cond_latents: For I2V, number of conditioning latent frames.
|
||||
These frames use split attention behavior.
|
||||
return_kv: If True, return (x, (k_cache, v_cache))
|
||||
kv_cache: Pre-computed K/V from conditioning frames
|
||||
skip_crs_attn: If True, skip cross-attention (used during cache init)
|
||||
"""
|
||||
B, N, C = x.shape
|
||||
T, H, W = latent_shape
|
||||
@@ -592,17 +800,47 @@ class LongCatTransformerBlock(nn.Module):
|
||||
x_norm = modulate_fp32(self.norm_attn, x.view(B, T, -1, C), shift_msa, scale_msa)
|
||||
x_norm = x_norm.view(B, N, C)
|
||||
|
||||
attn_out = self.self_attn(x_norm, latent_shape=latent_shape)
|
||||
# Handle KV cache
|
||||
if kv_cache is not None:
|
||||
# Move cache to device if offloaded
|
||||
kv_cache = (kv_cache[0].to(x.device), kv_cache[1].to(x.device))
|
||||
attn_out = self.self_attn.forward_with_kv_cache(
|
||||
x_norm,
|
||||
latent_shape=latent_shape,
|
||||
num_cond_latents=num_cond_latents,
|
||||
kv_cache=kv_cache,
|
||||
)
|
||||
kv_cache_new = None # Don't return cache when using cache
|
||||
else:
|
||||
attn_result = self.self_attn(
|
||||
x_norm,
|
||||
latent_shape=latent_shape,
|
||||
num_cond_latents=num_cond_latents,
|
||||
return_kv=return_kv,
|
||||
)
|
||||
if return_kv:
|
||||
attn_out, kv_cache_new = attn_result
|
||||
else:
|
||||
attn_out = attn_result
|
||||
kv_cache_new = None
|
||||
|
||||
# Residual with gating (CRITICAL: FP32 like original, then cast back)
|
||||
with torch.amp.autocast(device_type='cuda', dtype=torch.float32):
|
||||
x = x + (gate_msa * attn_out.view(B, T, -1, C)).view(B, N, C)
|
||||
x = x.to(x_orig_dtype)
|
||||
|
||||
# === Cross-Attention ===
|
||||
x_norm_cross = self.norm_cross(x)
|
||||
cross_out = self.cross_attn(x_norm_cross, context)
|
||||
x = x + cross_out
|
||||
# === Cross-Attention (skip if requested) ===
|
||||
if not skip_crs_attn:
|
||||
x_norm_cross = self.norm_cross(x)
|
||||
# When using KV cache, no need for num_cond_latents in cross-attn
|
||||
cross_num_cond = 0 if kv_cache is not None else num_cond_latents
|
||||
cross_out = self.cross_attn(
|
||||
x_norm_cross,
|
||||
context,
|
||||
latent_shape=latent_shape,
|
||||
num_cond_latents=cross_num_cond
|
||||
)
|
||||
x = x + cross_out
|
||||
|
||||
# === FFN ===
|
||||
x_norm_ffn = modulate_fp32(self.norm_ffn, x.view(B, T, -1, C), shift_mlp, scale_mlp)
|
||||
@@ -615,6 +853,8 @@ class LongCatTransformerBlock(nn.Module):
|
||||
x = x + (gate_mlp * ffn_out.view(B, T, -1, C)).view(B, N, C)
|
||||
x = x.to(x_orig_dtype)
|
||||
|
||||
if return_kv:
|
||||
return x, kv_cache_new
|
||||
return x
|
||||
|
||||
|
||||
@@ -670,16 +910,12 @@ class FinalLayer(nn.Module):
|
||||
B, N, C = x.shape
|
||||
T, _, _ = latent_shape
|
||||
|
||||
# AdaLN modulation (FP32 for stability like original)
|
||||
with torch.amp.autocast(device_type='cuda', dtype=torch.float32):
|
||||
t_mod = self.adaln_act(t)
|
||||
mod_params, _ = self.adaln_linear(t_mod)
|
||||
# Ensure FP32 output (needed when LoRA is applied)
|
||||
if mod_params.dtype != torch.float32:
|
||||
mod_params = mod_params.float()
|
||||
shift, scale = mod_params.unsqueeze(2).chunk(2, dim=-1)
|
||||
# AdaLN modulation
|
||||
t_mod = self.adaln_act(t)
|
||||
mod_params, _ = self.adaln_linear(t_mod)
|
||||
shift, scale = mod_params.unsqueeze(2).chunk(2, dim=-1)
|
||||
|
||||
# Modulate
|
||||
# Modulate (converts to FP32 internally for stability)
|
||||
x = modulate_fp32(self.norm, x.view(B, T, -1, C), shift, scale)
|
||||
x = x.reshape(B, N, C)
|
||||
|
||||
@@ -696,8 +932,6 @@ class FinalLayer(nn.Module):
|
||||
class LongCatTransformer3DModel(CachableDiT):
|
||||
"""
|
||||
Native LongCat Video Transformer using FastVideo layers.
|
||||
|
||||
This is a Phase 2 implementation that replaces third_party dependencies.
|
||||
"""
|
||||
|
||||
# FSDP sharding: shard at each transformer block
|
||||
@@ -789,13 +1023,28 @@ class LongCatTransformer3DModel(CachableDiT):
|
||||
encoder_attention_mask: torch.Tensor | None = None, # [B, N_text]
|
||||
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
|
||||
guidance: float | None = None, # Unused, for API compatibility
|
||||
num_cond_latents: int = 0, # For I2V: number of conditioning latent frames
|
||||
# === KV Cache Parameters ===
|
||||
return_kv: bool = False, # If True, return (output, kv_cache_dict)
|
||||
kv_cache_dict: dict | None = None, # Pre-computed {block_idx: (k, v)}
|
||||
skip_crs_attn: bool = False, # Skip cross-attention (for cache init)
|
||||
offload_kv_cache: bool = False, # Move cache to CPU after compute
|
||||
**kwargs
|
||||
) -> torch.Tensor:
|
||||
) -> torch.Tensor | tuple[torch.Tensor, dict]:
|
||||
"""
|
||||
Forward pass with FastVideo parameter ordering.
|
||||
|
||||
NOTE: This follows FastVideo convention:
|
||||
(hidden_states, encoder_hidden_states, timestep)
|
||||
|
||||
Args:
|
||||
num_cond_latents: For I2V, number of conditioning latent frames.
|
||||
These frames are treated as "clean" (timestep=0)
|
||||
and use split attention behavior.
|
||||
return_kv: If True, return (output, kv_cache_dict)
|
||||
kv_cache_dict: Pre-computed K/V cache {block_idx: (k, v)}
|
||||
skip_crs_attn: If True, skip cross-attention (for cache init)
|
||||
offload_kv_cache: If True, move cache to CPU after compute
|
||||
"""
|
||||
B, _, T, H, W = hidden_states.shape
|
||||
|
||||
@@ -825,12 +1074,31 @@ class LongCatTransformer3DModel(CachableDiT):
|
||||
encoder_attention_mask=encoder_attention_mask
|
||||
) # [B, N_text, C]
|
||||
|
||||
# 4. Transformer blocks
|
||||
# 4. Transformer blocks with optional KV cache
|
||||
kv_cache_dict_ret = {} if return_kv else None
|
||||
|
||||
for i, block in enumerate(self.blocks):
|
||||
x = block(
|
||||
# Get cache for this block if available
|
||||
block_kv_cache = kv_cache_dict.get(i, None) if kv_cache_dict else None
|
||||
|
||||
block_out = block(
|
||||
x, context, t,
|
||||
latent_shape=(N_t, N_h, N_w)
|
||||
latent_shape=(N_t, N_h, N_w),
|
||||
num_cond_latents=num_cond_latents,
|
||||
return_kv=return_kv,
|
||||
kv_cache=block_kv_cache,
|
||||
skip_crs_attn=skip_crs_attn,
|
||||
)
|
||||
|
||||
if return_kv:
|
||||
x, kv_cache = block_out
|
||||
# Store cache
|
||||
if offload_kv_cache:
|
||||
kv_cache_dict_ret[i] = (kv_cache[0].cpu(), kv_cache[1].cpu())
|
||||
else:
|
||||
kv_cache_dict_ret[i] = (kv_cache[0].contiguous(), kv_cache[1].contiguous())
|
||||
else:
|
||||
x = block_out
|
||||
|
||||
# 5. Output projection
|
||||
output = self.final_layer(x, t, latent_shape=(N_t, N_h, N_w))
|
||||
@@ -841,6 +1109,8 @@ class LongCatTransformer3DModel(CachableDiT):
|
||||
# Cast to float32 for better accuracy (as per original)
|
||||
output = output.to(torch.float32)
|
||||
|
||||
if return_kv:
|
||||
return output, kv_cache_dict_ret
|
||||
return output
|
||||
|
||||
def unpatchify(self, x: torch.Tensor, N_t: int, N_h: int, N_w: int) -> torch.Tensor:
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
|
||||
# import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from diffusers.utils import export_to_video
|
||||
from PIL import Image
|
||||
|
||||
from fastvideo.distributed.parallel_state import get_local_torch_device
|
||||
from fastvideo.utils import logger
|
||||
|
||||
|
||||
@@ -36,6 +35,120 @@ KEYBOARD_MAP_7 = { # templerun_distilled_model: still/w/s/left/right/a/d
|
||||
}
|
||||
KEYBOARD_MAP = KEYBOARD_MAP_4 # Default for backward compatibility
|
||||
|
||||
def expand_action_to_frames(action: dict, num_frames: int) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
result = {}
|
||||
for key, tensor in action.items():
|
||||
if tensor is not None:
|
||||
# Expand to [num_frames, D] then unsqueeze to [1, num_frames, D]
|
||||
result[key] = tensor.unsqueeze(0).repeat(num_frames, 1).unsqueeze(0)
|
||||
else:
|
||||
result[key] = None
|
||||
|
||||
if "mouse" not in result or result["mouse"] is None:
|
||||
# keyboard device if available, otherwise default
|
||||
device = result.get("keyboard", torch.tensor([])).device if result.get("keyboard") is not None else get_local_torch_device()
|
||||
result["mouse"] = torch.zeros(1, num_frames, 2, device=device)
|
||||
|
||||
return result["keyboard"], result["mouse"]
|
||||
|
||||
def get_current_action(mode="universal"):
|
||||
|
||||
CAM_VALUE = 0.1
|
||||
if mode == 'universal':
|
||||
logger.info("")
|
||||
logger.info('-'*30)
|
||||
logger.info("PRESS [I, K, J, L, U] FOR CAMERA TRANSFORM\n (I: up, K: down, J: left, L: right, U: no move)")
|
||||
logger.info("PRESS [W, S, A, D, Q] FOR MOVEMENT\n (W: forward, S: back, A: left, D: right, Q: no move)")
|
||||
logger.info('-'*30)
|
||||
CAMERA_VALUE_MAP = {
|
||||
"i": [CAM_VALUE, 0],
|
||||
"k": [-CAM_VALUE, 0],
|
||||
"j": [0, -CAM_VALUE],
|
||||
"l": [0, CAM_VALUE],
|
||||
"u": [0, 0]
|
||||
}
|
||||
KEYBOARD_IDX = {
|
||||
"w": [1, 0, 0, 0], "s": [0, 1, 0, 0], "a": [0, 0, 1, 0], "d": [0, 0, 0, 1],
|
||||
"q": [0, 0, 0, 0]
|
||||
}
|
||||
flag = 0
|
||||
while flag != 1:
|
||||
try:
|
||||
idx_mouse = input('Please input the mouse action (e.g. `U`):\n').strip().lower()
|
||||
idx_keyboard = input('Please input the keyboard action (e.g. `W`):\n').strip().lower()
|
||||
if idx_mouse in CAMERA_VALUE_MAP and idx_keyboard in KEYBOARD_IDX:
|
||||
flag = 1
|
||||
except Exception:
|
||||
pass
|
||||
mouse_cond = torch.tensor(CAMERA_VALUE_MAP[idx_mouse]).cuda()
|
||||
keyboard_cond = torch.tensor(KEYBOARD_IDX[idx_keyboard]).cuda()
|
||||
elif mode == 'gta_drive':
|
||||
logger.info("")
|
||||
logger.info('-'*30)
|
||||
logger.info("PRESS [W, S, A, D, Q] FOR MOVEMENT\n (W: forward, S: back, A: left, D: right, Q: no move)")
|
||||
logger.info('-'*30)
|
||||
CAMERA_VALUE_MAP = {
|
||||
"a": [0, -CAM_VALUE],
|
||||
"d": [0, CAM_VALUE],
|
||||
"q": [0, 0]
|
||||
}
|
||||
KEYBOARD_IDX = {
|
||||
"w": [1, 0], "s": [0, 1],
|
||||
"q": [0, 0]
|
||||
}
|
||||
flag = 0
|
||||
while flag != 1:
|
||||
try:
|
||||
indexes = input('Please input the actions (split with ` `):\n(e.g. `W` for forward, `W A` for forward and left)\n').strip().lower().split(' ')
|
||||
idx_mouse = []
|
||||
idx_keyboard = []
|
||||
for i in indexes:
|
||||
if i in CAMERA_VALUE_MAP.keys():
|
||||
idx_mouse += [i]
|
||||
elif i in KEYBOARD_IDX.keys():
|
||||
idx_keyboard += [i]
|
||||
if len(idx_mouse) == 0:
|
||||
idx_mouse += ['q']
|
||||
if len(idx_keyboard) == 0:
|
||||
idx_keyboard += ['q']
|
||||
assert idx_mouse in [['a'], ['d'], ['q']] and idx_keyboard in [['q'], ['w'], ['s']]
|
||||
flag = 1
|
||||
except Exception:
|
||||
pass
|
||||
mouse_cond = torch.tensor(CAMERA_VALUE_MAP[idx_mouse[0]]).cuda()
|
||||
keyboard_cond = torch.tensor(KEYBOARD_IDX[idx_keyboard[0]]).cuda()
|
||||
elif mode == 'templerun':
|
||||
logger.info("")
|
||||
logger.info('-'*30)
|
||||
logger.info("PRESS [W, S, A, D, Z, C, Q] FOR ACTIONS\n (W: jump, S: slide, A: left side, D: right side, Z: turn left, C: turn right, Q: no move)")
|
||||
logger.info('-'*30)
|
||||
KEYBOARD_IDX = {
|
||||
"w": [0, 1, 0, 0, 0, 0, 0], "s": [0, 0, 1, 0, 0, 0, 0],
|
||||
"a": [0, 0, 0, 0, 0, 1, 0], "d": [0, 0, 0, 0, 0, 0, 1],
|
||||
"z": [0, 0, 0, 1, 0, 0, 0], "c": [0, 0, 0, 0, 1, 0, 0],
|
||||
"q": [1, 0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
flag = 0
|
||||
while flag != 1:
|
||||
try:
|
||||
idx_keyboard = input('Please input the action: \n(e.g. `W` for forward, `Z` for turning left)\n').strip().lower()
|
||||
if idx_keyboard in KEYBOARD_IDX.keys():
|
||||
flag = 1
|
||||
except Exception:
|
||||
pass
|
||||
keyboard_cond = torch.tensor(KEYBOARD_IDX[idx_keyboard]).cuda()
|
||||
|
||||
if mode != 'templerun':
|
||||
return {
|
||||
"mouse": mouse_cond,
|
||||
"keyboard": keyboard_cond
|
||||
}
|
||||
return {
|
||||
"keyboard": keyboard_cond
|
||||
}
|
||||
|
||||
async def get_current_action_async(mode="universal"):
|
||||
return await asyncio.to_thread(get_current_action, mode)
|
||||
|
||||
def load_initial_image(image_path: str = None) -> Image.Image:
|
||||
if image_path and os.path.exists(image_path):
|
||||
@@ -43,7 +156,6 @@ def load_initial_image(image_path: str = None) -> Image.Image:
|
||||
logger.warning("No image provided, creating placeholder...")
|
||||
return Image.new("RGB", (640, 352), (128, 128, 128))
|
||||
|
||||
|
||||
def create_action_presets(num_frames: int, keyboard_dim: int = 4, seed: int = None):
|
||||
if keyboard_dim not in (2, 4, 7):
|
||||
raise ValueError(f"keyboard_dim must be 2, 4, or 7, got {keyboard_dim}")
|
||||
@@ -145,7 +257,6 @@ def create_action_presets(num_frames: int, keyboard_dim: int = 4, seed: int = No
|
||||
|
||||
return {"keyboard": keyboard_condition, "mouse": mouse_condition}
|
||||
|
||||
|
||||
def parse_config(config, mode="universal"):
|
||||
assert mode in ['universal', 'gta_drive', 'templerun']
|
||||
key_data = {}
|
||||
@@ -183,7 +294,6 @@ def parse_config(config, mode="universal"):
|
||||
)
|
||||
return key_data, mouse_data
|
||||
|
||||
|
||||
# NOTE: drawing functions are commented out to avoid cv2/libGL dependency.
|
||||
#
|
||||
# def draw_rounded_rectangle(image, top_left, bottom_right, color, radius=10, alpha=0.5):
|
||||
@@ -199,7 +309,6 @@ def parse_config(config, mode="universal"):
|
||||
# cv2.ellipse(overlay, (x2 - radius, y2 - radius), (radius, radius), 0, 0, 90, color, -1)
|
||||
# cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0, image)
|
||||
#
|
||||
#
|
||||
# def draw_keys_on_frame(frame, keys, key_size=(80, 50), spacing=20, bottom_margin=30, mode='universal'):
|
||||
# h, w, _ = frame.shape
|
||||
# horison_shift = 90
|
||||
@@ -244,7 +353,6 @@ def parse_config(config, mode="universal"):
|
||||
# text_y = y + (key_size[1] + text_size[1]) // 2
|
||||
# cv2.putText(frame, key_icon[key], (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 2)
|
||||
#
|
||||
#
|
||||
# def overlay_icon(frame, icon, position, scale=1.0, rotation=0):
|
||||
# x, y = position
|
||||
# h, w, _ = icon.shape
|
||||
@@ -282,7 +390,6 @@ def parse_config(config, mode="universal"):
|
||||
# frame_region[:, :, c] = (1 - alpha) * frame_region[:, :, c] + alpha * icon_rgb[:, :, c]
|
||||
# frame[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = frame_region
|
||||
#
|
||||
#
|
||||
# def process_video(input_video, output_video, config, mouse_icon_path,
|
||||
# mouse_scale=1.0, mouse_rotation=0, process_icon=True, mode='universal'):
|
||||
# key_data, mouse_data = parse_config(config, mode=mode)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import math
|
||||
from contextlib import nullcontext
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
@@ -733,9 +734,19 @@ class WanTransformer3DModel(CachableDiT):
|
||||
block, hidden_states, encoder_hidden_states,
|
||||
timestep_proj, freqs_cis, attention_mask)
|
||||
else:
|
||||
for block in self.blocks:
|
||||
hidden_states = block(hidden_states, encoder_hidden_states,
|
||||
timestep_proj, freqs_cis, attention_mask)
|
||||
offload_mgr = getattr(self, "_layerwise_offload_manager", None)
|
||||
use_offload = offload_mgr is not None and getattr(offload_mgr, "enabled", False)
|
||||
|
||||
for i, block in enumerate(self.blocks):
|
||||
scope = offload_mgr.layer_scope(
|
||||
prefetch_layer_idx=i + 1 if i + 1 < len(self.blocks) else None,
|
||||
release_layer_idx=i,
|
||||
non_blocking=True,
|
||||
) if use_offload else nullcontext()
|
||||
|
||||
with scope:
|
||||
hidden_states = block(hidden_states, encoder_hidden_states,
|
||||
timestep_proj, freqs_cis, attention_mask)
|
||||
# if teacache is enabled, we need to cache the original hidden states
|
||||
|
||||
if enable_teacache:
|
||||
|
||||
+218
-172
@@ -31,8 +31,11 @@ from fastvideo.configs.models.encoders import BaseEncoderOutput, T5Config
|
||||
from fastvideo.distributed import get_tp_rank, get_tp_world_size
|
||||
from fastvideo.layers.activation import get_act_fn
|
||||
from fastvideo.layers.layernorm import RMSNorm
|
||||
from fastvideo.layers.linear import (MergedColumnParallelLinear,
|
||||
QKVParallelLinear, RowParallelLinear)
|
||||
from fastvideo.layers.linear import (
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from fastvideo.layers.quantization import QuantizationConfig
|
||||
from fastvideo.layers.vocab_parallel_embedding import VocabParallelEmbedding
|
||||
from fastvideo.models.encoders.base import TextEncoder
|
||||
@@ -44,6 +47,7 @@ class AttentionType:
|
||||
Attention type.
|
||||
Use string to be compatible with `torch.compile`.
|
||||
"""
|
||||
|
||||
# Decoder attention between previous layer Q/K/V
|
||||
DECODER = "decoder"
|
||||
# Encoder attention between previous layer Q/K/V for encoder-decoder
|
||||
@@ -60,17 +64,16 @@ class AttentionMetadata:
|
||||
|
||||
|
||||
class T5DenseActDense(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config: T5Config,
|
||||
quant_config: QuantizationConfig | None = None):
|
||||
def __init__(
|
||||
self, config: T5Config, quant_config: QuantizationConfig | None = None
|
||||
):
|
||||
super().__init__()
|
||||
self.wi = MergedColumnParallelLinear(config.d_model, [config.d_ff],
|
||||
bias=False)
|
||||
self.wo = RowParallelLinear(config.d_ff,
|
||||
config.d_model,
|
||||
bias=False,
|
||||
quant_config=quant_config)
|
||||
self.wi = MergedColumnParallelLinear(
|
||||
config.d_model, [config.d_ff], bias=False
|
||||
)
|
||||
self.wo = RowParallelLinear(
|
||||
config.d_ff, config.d_model, bias=False, quant_config=quant_config
|
||||
)
|
||||
self.act = get_act_fn(config.dense_act_fn)
|
||||
|
||||
def forward(self, hidden_states) -> torch.Tensor:
|
||||
@@ -81,23 +84,21 @@ class T5DenseActDense(nn.Module):
|
||||
|
||||
|
||||
class T5DenseGatedActDense(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config: T5Config,
|
||||
quant_config: QuantizationConfig | None = None):
|
||||
def __init__(
|
||||
self, config: T5Config, quant_config: QuantizationConfig | None = None
|
||||
):
|
||||
super().__init__()
|
||||
self.wi_0 = MergedColumnParallelLinear(config.d_model, [config.d_ff],
|
||||
bias=False,
|
||||
quant_config=quant_config)
|
||||
self.wi_1 = MergedColumnParallelLinear(config.d_model, [config.d_ff],
|
||||
bias=False,
|
||||
quant_config=quant_config)
|
||||
self.wi_0 = MergedColumnParallelLinear(
|
||||
config.d_model, [config.d_ff], bias=False, quant_config=quant_config
|
||||
)
|
||||
self.wi_1 = MergedColumnParallelLinear(
|
||||
config.d_model, [config.d_ff], bias=False, quant_config=quant_config
|
||||
)
|
||||
# Should not run in fp16 unless mixed-precision is used,
|
||||
# see https://github.com/huggingface/transformers/issues/20287.
|
||||
self.wo = RowParallelLinear(config.d_ff,
|
||||
config.d_model,
|
||||
bias=False,
|
||||
quant_config=quant_config)
|
||||
self.wo = RowParallelLinear(
|
||||
config.d_ff, config.d_model, bias=False, quant_config=quant_config
|
||||
)
|
||||
self.act = get_act_fn(config.dense_act_fn)
|
||||
|
||||
def forward(self, hidden_states) -> torch.Tensor:
|
||||
@@ -109,17 +110,18 @@ class T5DenseGatedActDense(nn.Module):
|
||||
|
||||
|
||||
class T5LayerFF(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config: T5Config,
|
||||
quant_config: QuantizationConfig | None = None):
|
||||
def __init__(
|
||||
self, config: T5Config, quant_config: QuantizationConfig | None = None
|
||||
):
|
||||
super().__init__()
|
||||
if config.is_gated_act:
|
||||
self.DenseReluDense = T5DenseGatedActDense(
|
||||
config, quant_config=quant_config)
|
||||
config, quant_config=quant_config
|
||||
)
|
||||
else:
|
||||
self.DenseReluDense = T5DenseActDense(config,
|
||||
quant_config=quant_config)
|
||||
self.DenseReluDense = T5DenseActDense(
|
||||
config, quant_config=quant_config
|
||||
)
|
||||
|
||||
self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
||||
|
||||
@@ -132,39 +134,41 @@ class T5LayerFF(nn.Module):
|
||||
|
||||
# T5 has attn_bias and does not use softmax scaling
|
||||
class T5MultiHeadAttention(nn.Module):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def forward(self, q, k, v, attn_bias=None):
|
||||
b, _, n, c = q.shape
|
||||
attn = torch.einsum('binc,bjnc->bnij', q, k)
|
||||
attn = torch.einsum("binc,bjnc->bnij", q, k)
|
||||
if attn_bias is not None:
|
||||
attn += attn_bias
|
||||
|
||||
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
|
||||
x = torch.einsum('bnij,bjnc->binc', attn, v)
|
||||
x = torch.einsum("bnij,bjnc->binc", attn, v)
|
||||
x = x.reshape(b, -1, n * c)
|
||||
return x
|
||||
|
||||
|
||||
class T5Attention(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config: T5Config,
|
||||
attn_type: str,
|
||||
has_relative_attention_bias=False,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
config: T5Config,
|
||||
attn_type: str,
|
||||
has_relative_attention_bias=False,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
self.attn_type = attn_type
|
||||
# Cross-attention has no relative pos encoding anyway
|
||||
self.is_decoder = attn_type == AttentionType.DECODER
|
||||
self.has_relative_attention_bias = has_relative_attention_bias
|
||||
self.relative_attention_num_buckets = \
|
||||
self.relative_attention_num_buckets = (
|
||||
config.relative_attention_num_buckets
|
||||
self.relative_attention_max_distance = \
|
||||
)
|
||||
self.relative_attention_max_distance = (
|
||||
config.relative_attention_max_distance
|
||||
)
|
||||
self.d_model = config.d_model
|
||||
self.key_value_proj_dim = config.d_kv
|
||||
self.total_num_heads = self.total_num_kv_heads = config.num_heads
|
||||
@@ -191,12 +195,13 @@ class T5Attention(nn.Module):
|
||||
self.attn = T5MultiHeadAttention()
|
||||
|
||||
if self.has_relative_attention_bias:
|
||||
self.relative_attention_bias = \
|
||||
VocabParallelEmbedding(self.relative_attention_num_buckets,
|
||||
self.total_num_heads,
|
||||
org_num_embeddings=self.relative_attention_num_buckets,
|
||||
padding_size=self.relative_attention_num_buckets,
|
||||
quant_config=quant_config)
|
||||
self.relative_attention_bias = VocabParallelEmbedding(
|
||||
self.relative_attention_num_buckets,
|
||||
self.total_num_heads,
|
||||
org_num_embeddings=self.relative_attention_num_buckets,
|
||||
padding_size=self.relative_attention_num_buckets,
|
||||
quant_config=quant_config,
|
||||
)
|
||||
self.o = RowParallelLinear(
|
||||
self.total_num_heads * self.key_value_proj_dim,
|
||||
self.d_model,
|
||||
@@ -206,21 +211,20 @@ class T5Attention(nn.Module):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _relative_position_bucket(relative_position,
|
||||
bidirectional=True,
|
||||
num_buckets=32,
|
||||
max_distance=128) -> torch.Tensor:
|
||||
def _relative_position_bucket(
|
||||
relative_position, bidirectional=True, num_buckets=32, max_distance=128
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Adapted from Mesh Tensorflow:
|
||||
https://github.com/tensorflow/mesh/blob/0cb87fe07da627bf0b7e60475d59f95ed6b5be3d/mesh_tensorflow/transformer/transformer_layers.py#L593
|
||||
Translate relative position to a bucket number for relative attention.
|
||||
The relative position is defined as memory_position - query_position,
|
||||
i.e. the distance in tokens from the attending position to the
|
||||
attended-to position. If bidirectional=False, then positive relative
|
||||
positions are invalid. We use smaller buckets for small absolute
|
||||
relative_position and larger buckets for larger absolute
|
||||
Translate relative position to a bucket number for relative attention.
|
||||
The relative position is defined as memory_position - query_position,
|
||||
i.e. the distance in tokens from the attending position to the
|
||||
attended-to position. If bidirectional=False, then positive relative
|
||||
positions are invalid. We use smaller buckets for small absolute
|
||||
relative_position and larger buckets for larger absolute
|
||||
relative_positions. All relative positions >=max_distance map to the
|
||||
same bucket. All relative positions <=-max_distance map to the same
|
||||
same bucket. All relative positions <=-max_distance map to the same
|
||||
bucket. This should allow for more graceful generalization to longer
|
||||
sequences than the model has been trained on
|
||||
Args:
|
||||
@@ -231,16 +235,18 @@ class T5Attention(nn.Module):
|
||||
Returns:
|
||||
a Tensor with the same shape as relative_position, containing int32
|
||||
values in the range [0, num_buckets)
|
||||
"""# noqa: E501
|
||||
""" # noqa: E501
|
||||
relative_buckets = 0
|
||||
if bidirectional:
|
||||
num_buckets //= 2
|
||||
relative_buckets += (relative_position > 0).to(
|
||||
torch.long) * num_buckets
|
||||
torch.long
|
||||
) * num_buckets
|
||||
relative_position = torch.abs(relative_position)
|
||||
else:
|
||||
relative_position = -torch.min(relative_position,
|
||||
torch.zeros_like(relative_position))
|
||||
relative_position = -torch.min(
|
||||
relative_position, torch.zeros_like(relative_position)
|
||||
)
|
||||
# now relative_position is in the range [0, inf)
|
||||
|
||||
# half of the buckets are for exact increments in positions
|
||||
@@ -250,30 +256,32 @@ class T5Attention(nn.Module):
|
||||
# The other half of the buckets are for logarithmically bigger bins
|
||||
# in positions up to max_distance
|
||||
relative_position_if_large = max_exact + (
|
||||
torch.log(relative_position.float() / max_exact) /
|
||||
math.log(max_distance / max_exact) *
|
||||
(num_buckets - max_exact)).to(torch.long)
|
||||
torch.log(relative_position.float() / max_exact)
|
||||
/ math.log(max_distance / max_exact)
|
||||
* (num_buckets - max_exact)
|
||||
).to(torch.long)
|
||||
relative_position_if_large = torch.min(
|
||||
relative_position_if_large,
|
||||
torch.full_like(relative_position_if_large, num_buckets - 1))
|
||||
torch.full_like(relative_position_if_large, num_buckets - 1),
|
||||
)
|
||||
|
||||
relative_buckets += torch.where(is_small, relative_position,
|
||||
relative_position_if_large)
|
||||
relative_buckets += torch.where(
|
||||
is_small, relative_position, relative_position_if_large
|
||||
)
|
||||
return relative_buckets
|
||||
|
||||
def compute_bias(self,
|
||||
query_length,
|
||||
key_length,
|
||||
device=None) -> torch.Tensor:
|
||||
def compute_bias(
|
||||
self, query_length, key_length, device=None
|
||||
) -> torch.Tensor:
|
||||
"""Compute binned relative position bias"""
|
||||
if device is None:
|
||||
device = self.relative_attention_bias.weight.device
|
||||
context_position = torch.arange(query_length,
|
||||
dtype=torch.long,
|
||||
device=device)[:, None]
|
||||
memory_position = torch.arange(key_length,
|
||||
dtype=torch.long,
|
||||
device=device)[None, :]
|
||||
context_position = torch.arange(
|
||||
query_length, dtype=torch.long, device=device
|
||||
)[:, None]
|
||||
memory_position = torch.arange(
|
||||
key_length, dtype=torch.long, device=device
|
||||
)[None, :]
|
||||
# max_seq_len, nh
|
||||
relative_position = memory_position - context_position
|
||||
relative_position_bucket = self._relative_position_bucket(
|
||||
@@ -286,7 +294,8 @@ class T5Attention(nn.Module):
|
||||
relative_position_bucket
|
||||
) # shape (query_length, key_length, num_heads)
|
||||
x = values.permute([2, 0, 1]).unsqueeze(
|
||||
0) # shape (1, num_heads, query_length, key_length)
|
||||
0
|
||||
) # shape (1, num_heads, query_length, key_length)
|
||||
return x
|
||||
|
||||
def forward(
|
||||
@@ -314,8 +323,9 @@ class T5Attention(nn.Module):
|
||||
# The bias term is computed on longest sequence in batch. Biases
|
||||
# for shorter sequences are slices of the longest.
|
||||
assert self.attn_type == AttentionType.ENCODER
|
||||
attn_bias = self.compute_bias(seq_len,
|
||||
seq_len).repeat(num_seqs, 1, 1, 1)
|
||||
attn_bias = self.compute_bias(seq_len, seq_len).repeat(
|
||||
num_seqs, 1, 1, 1
|
||||
)
|
||||
attn_metadata.attn_bias = attn_bias
|
||||
else:
|
||||
# Encoder/Decoder Self-Attention Layer, attn bias already cached.
|
||||
@@ -324,24 +334,27 @@ class T5Attention(nn.Module):
|
||||
from fastvideo.platforms import current_platform
|
||||
|
||||
if attention_mask is not None:
|
||||
attention_mask = attention_mask.view(
|
||||
bs, 1, 1,
|
||||
-1) if attention_mask.ndim == 2 else attention_mask.unsqueeze(1)
|
||||
mask_val = -1e4 if current_platform.is_mps() else torch.finfo(
|
||||
q.dtype).min
|
||||
attention_mask = (
|
||||
attention_mask.view(bs, 1, 1, -1)
|
||||
if attention_mask.ndim == 2
|
||||
else attention_mask.unsqueeze(1)
|
||||
)
|
||||
mask_val = (
|
||||
-1e4 if current_platform.is_mps() else torch.finfo(q.dtype).min
|
||||
)
|
||||
attn_bias.masked_fill_(attention_mask == 0, mask_val)
|
||||
|
||||
if get_tp_world_size() > 1:
|
||||
rank = get_tp_rank()
|
||||
attn_bias = attn_bias[:, rank * self.n_heads:(rank + 1) *
|
||||
self.n_heads, :, :]
|
||||
attn_bias = attn_bias[
|
||||
:, rank * self.n_heads : (rank + 1) * self.n_heads, :, :
|
||||
]
|
||||
attn_output = self.attn(q, k, v, attn_bias)
|
||||
output, _ = self.o(attn_output)
|
||||
return output
|
||||
|
||||
|
||||
class T5LayerSelfAttention(nn.Module):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
@@ -353,10 +366,12 @@ class T5LayerSelfAttention(nn.Module):
|
||||
self.SelfAttention = T5Attention(
|
||||
config,
|
||||
AttentionType.DECODER
|
||||
if "decoder" in prefix else AttentionType.ENCODER,
|
||||
if "decoder" in prefix
|
||||
else AttentionType.ENCODER,
|
||||
has_relative_attention_bias=has_relative_attention_bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.SelfAttention")
|
||||
prefix=f"{prefix}.SelfAttention",
|
||||
)
|
||||
self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
||||
|
||||
def forward(
|
||||
@@ -376,17 +391,20 @@ class T5LayerSelfAttention(nn.Module):
|
||||
|
||||
|
||||
class T5LayerCrossAttention(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
self.EncDecAttention = T5Attention(config,
|
||||
AttentionType.ENCODER_DECODER,
|
||||
has_relative_attention_bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.EncDecAttention")
|
||||
self.EncDecAttention = T5Attention(
|
||||
config,
|
||||
AttentionType.ENCODER_DECODER,
|
||||
has_relative_attention_bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.EncDecAttention",
|
||||
)
|
||||
self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
||||
|
||||
def forward(
|
||||
@@ -404,13 +422,14 @@ class T5LayerCrossAttention(nn.Module):
|
||||
|
||||
|
||||
class T5Block(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config: T5Config,
|
||||
is_decoder: bool,
|
||||
has_relative_attention_bias=False,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
config: T5Config,
|
||||
is_decoder: bool,
|
||||
has_relative_attention_bias=False,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
self.is_decoder = is_decoder
|
||||
self.layer = nn.ModuleList()
|
||||
@@ -419,13 +438,18 @@ class T5Block(nn.Module):
|
||||
config,
|
||||
has_relative_attention_bias=has_relative_attention_bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.self_attn"))
|
||||
prefix=f"{prefix}.self_attn",
|
||||
)
|
||||
)
|
||||
|
||||
if self.is_decoder:
|
||||
self.layer.append(
|
||||
T5LayerCrossAttention(config,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.cross_attn"))
|
||||
T5LayerCrossAttention(
|
||||
config,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.cross_attn",
|
||||
)
|
||||
)
|
||||
|
||||
self.layer.append(T5LayerFF(config, quant_config=quant_config))
|
||||
|
||||
@@ -435,13 +459,15 @@ class T5Block(nn.Module):
|
||||
attention_mask: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None = None,
|
||||
) -> torch.Tensor:
|
||||
|
||||
hidden_states = self.layer[0](hidden_states=hidden_states,
|
||||
attention_mask=attention_mask,
|
||||
attn_metadata=attn_metadata)
|
||||
hidden_states = self.layer[0](
|
||||
hidden_states=hidden_states,
|
||||
attention_mask=attention_mask,
|
||||
attn_metadata=attn_metadata,
|
||||
)
|
||||
if self.is_decoder:
|
||||
hidden_states = self.layer[1](hidden_states=hidden_states,
|
||||
attn_metadata=attn_metadata)
|
||||
hidden_states = self.layer[1](
|
||||
hidden_states=hidden_states, attn_metadata=attn_metadata
|
||||
)
|
||||
|
||||
# Apply Feed Forward layer
|
||||
hidden_states = self.layer[2](hidden_states)
|
||||
@@ -451,37 +477,49 @@ class T5Block(nn.Module):
|
||||
|
||||
|
||||
class T5Stack(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
config: T5Config,
|
||||
is_decoder: bool,
|
||||
n_layers: int,
|
||||
embed_tokens=None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
is_umt5: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
config: T5Config,
|
||||
is_decoder: bool,
|
||||
n_layers: int,
|
||||
embed_tokens=None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
is_umt5: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.embed_tokens = embed_tokens
|
||||
self.is_umt5 = is_umt5
|
||||
if is_umt5:
|
||||
self.block = nn.ModuleList([
|
||||
T5Block(config,
|
||||
self.block = nn.ModuleList(
|
||||
[
|
||||
T5Block(
|
||||
config,
|
||||
is_decoder=is_decoder,
|
||||
has_relative_attention_bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.blocks.{i}") for i in range(n_layers)
|
||||
])
|
||||
prefix=f"{prefix}.blocks.{i}",
|
||||
)
|
||||
for i in range(n_layers)
|
||||
]
|
||||
)
|
||||
else:
|
||||
# Only the first block has relative positional encoding.
|
||||
self.block = nn.ModuleList([
|
||||
T5Block(config,
|
||||
self.block = nn.ModuleList(
|
||||
[
|
||||
T5Block(
|
||||
config,
|
||||
is_decoder=is_decoder,
|
||||
has_relative_attention_bias=i == 0,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.blocks.{i}") for i in range(n_layers)
|
||||
])
|
||||
self.final_layer_norm = RMSNorm(config.d_model,
|
||||
eps=config.layer_norm_epsilon)
|
||||
prefix=f"{prefix}.blocks.{i}",
|
||||
)
|
||||
for i in range(n_layers)
|
||||
]
|
||||
)
|
||||
self.final_layer_norm = RMSNorm(
|
||||
config.d_model, eps=config.layer_norm_epsilon
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
@@ -502,24 +540,24 @@ class T5Stack(nn.Module):
|
||||
|
||||
|
||||
class T5EncoderModel(TextEncoder):
|
||||
|
||||
def __init__(self, config: T5Config, prefix: str = ""):
|
||||
super().__init__(config)
|
||||
|
||||
quant_config = None
|
||||
|
||||
self.shared = VocabParallelEmbedding(
|
||||
config.vocab_size,
|
||||
config.d_model,
|
||||
org_num_embeddings=config.vocab_size)
|
||||
org_num_embeddings=config.vocab_size,
|
||||
)
|
||||
|
||||
self.encoder = T5Stack(config,
|
||||
False,
|
||||
config.num_layers,
|
||||
self.shared,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.encoder",
|
||||
is_umt5=False)
|
||||
self.encoder = T5Stack(
|
||||
config,
|
||||
False,
|
||||
config.num_layers,
|
||||
self.shared,
|
||||
quant_config=config.quant_config,
|
||||
prefix=f"{prefix}.encoder",
|
||||
is_umt5=False,
|
||||
)
|
||||
|
||||
def get_input_embeddings(self):
|
||||
return self.shared
|
||||
@@ -545,8 +583,9 @@ class T5EncoderModel(TextEncoder):
|
||||
attention_mask=attention_mask,
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str,
|
||||
torch.Tensor]]) -> set[str]:
|
||||
def load_weights(
|
||||
self, weights: Iterable[tuple[str, torch.Tensor]]
|
||||
) -> set[str]:
|
||||
stacked_params_mapping = [
|
||||
# (param_name, shard_name, shard_id)
|
||||
(".qkv_proj", ".q", "q"),
|
||||
@@ -584,32 +623,33 @@ class T5EncoderModel(TextEncoder):
|
||||
continue
|
||||
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader",
|
||||
default_weight_loader)
|
||||
weight_loader = getattr(
|
||||
param, "weight_loader", default_weight_loader
|
||||
)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(name)
|
||||
return loaded_params
|
||||
|
||||
|
||||
class UMT5EncoderModel(TextEncoder):
|
||||
|
||||
def __init__(self, config: T5Config, prefix: str = ""):
|
||||
super().__init__(config)
|
||||
|
||||
quant_config = None
|
||||
|
||||
self.shared = VocabParallelEmbedding(
|
||||
config.vocab_size,
|
||||
config.d_model,
|
||||
org_num_embeddings=config.vocab_size)
|
||||
org_num_embeddings=config.vocab_size,
|
||||
)
|
||||
|
||||
self.encoder = T5Stack(config,
|
||||
False,
|
||||
config.num_layers,
|
||||
self.shared,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.encoder",
|
||||
is_umt5=True)
|
||||
self.encoder = T5Stack(
|
||||
config,
|
||||
False,
|
||||
config.num_layers,
|
||||
self.shared,
|
||||
quant_config=config.quant_config,
|
||||
prefix=f"{prefix}.encoder",
|
||||
is_umt5=True,
|
||||
)
|
||||
|
||||
def get_input_embeddings(self):
|
||||
return self.shared
|
||||
@@ -635,15 +675,20 @@ class UMT5EncoderModel(TextEncoder):
|
||||
attention_mask=attention_mask,
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str,
|
||||
torch.Tensor]]) -> set[str]:
|
||||
def load_weights(
|
||||
self, weights: Iterable[tuple[str, torch.Tensor]]
|
||||
) -> set[str]:
|
||||
params_dict = dict(self.named_parameters())
|
||||
loaded_params: set[str] = set()
|
||||
for name, loaded_weight in weights:
|
||||
loaded = False
|
||||
if "decoder" in name or "lm_head" in name:
|
||||
continue
|
||||
for param_name, weight_name, shard_id in self.config.arch_config.stacked_params_mapping:
|
||||
for (
|
||||
param_name,
|
||||
weight_name,
|
||||
shard_id,
|
||||
) in self.config.arch_config.stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
continue
|
||||
name = name.replace(weight_name, param_name)
|
||||
@@ -668,8 +713,9 @@ class UMT5EncoderModel(TextEncoder):
|
||||
continue
|
||||
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader",
|
||||
default_weight_loader)
|
||||
weight_loader = getattr(
|
||||
param, "weight_loader", default_weight_loader
|
||||
)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(name)
|
||||
return loaded_params
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Inspired by SGLang's layerwise offload implementation:
|
||||
# https://github.com/sgl-project/sglang/pull/15511
|
||||
#
|
||||
# This implementation provides a lightweight layerwise CPU offload manager
|
||||
# with async H2D prefetch using a dedicated CUDA stream, following SGLang's design.
|
||||
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, Set, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.pin_memory import is_pin_memory_available
|
||||
|
||||
|
||||
class LayerwiseOffloadManager:
|
||||
"""A lightweight layerwise CPU offload manager.
|
||||
|
||||
Offloads per-layer parameters/buffers from GPU to CPU, and supports async H2D
|
||||
prefetch using a dedicated CUDA stream.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: torch.nn.Module,
|
||||
*,
|
||||
module_list_attr: str,
|
||||
num_layers: int,
|
||||
enabled: bool,
|
||||
pin_cpu_memory: bool = True,
|
||||
auto_initialize: bool = False,
|
||||
) -> None:
|
||||
self.model = model
|
||||
self.module_list_attr = module_list_attr
|
||||
self.num_layers = int(num_layers)
|
||||
self.pin_cpu_memory = bool(pin_cpu_memory)
|
||||
if self.pin_cpu_memory and not is_pin_memory_available():
|
||||
self.pin_cpu_memory = False
|
||||
|
||||
self.enabled = bool(enabled and torch.cuda.is_available())
|
||||
self.device = (
|
||||
torch.device("cuda", torch.cuda.current_device()) if self.enabled else None
|
||||
)
|
||||
self.copy_stream = torch.cuda.Stream() if self.enabled else None
|
||||
|
||||
self._layer_name_re = re.compile(
|
||||
rf"(^|\.){re.escape(module_list_attr)}\.(\d+)(\.|$)"
|
||||
)
|
||||
|
||||
self._cpu_weights: Dict[int, Dict[str, torch.Tensor]] = {}
|
||||
self._cpu_dtypes: Dict[int, Dict[str, torch.dtype]] = {}
|
||||
|
||||
self._gpu_layers: Dict[int, Set[str]] = {}
|
||||
|
||||
self._named_parameters: Dict[str, torch.nn.Parameter] = {}
|
||||
self._named_buffers: Dict[str, torch.Tensor] = {}
|
||||
|
||||
self._meta: Dict[str, Tuple[int, torch.dtype]] = {}
|
||||
|
||||
if auto_initialize:
|
||||
self.initialize()
|
||||
|
||||
def _match_layer_idx(self, name: str) -> Optional[int]:
|
||||
m = self._layer_name_re.search(name)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
return int(m.group(2))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _record_meta(self, name: str, t: torch.Tensor) -> None:
|
||||
if name not in self._meta:
|
||||
self._meta[name] = (int(t.ndim), t.dtype)
|
||||
|
||||
def _make_placeholder(self, name: str) -> torch.Tensor:
|
||||
"""Rank-preserving empty placeholder on GPU."""
|
||||
assert self.device is not None
|
||||
ndim, dtype = self._meta[name]
|
||||
shape = (0,) if ndim <= 0 else (0,) * ndim
|
||||
return torch.empty(shape, device=self.device, dtype=dtype)
|
||||
|
||||
def _get_target(self, name: str) -> torch.Tensor:
|
||||
if name in self._named_parameters:
|
||||
return self._named_parameters[name]
|
||||
return self._named_buffers[name]
|
||||
|
||||
def _offload_tensor(self, name: str, tensor: torch.Tensor, layer_idx: int) -> None:
|
||||
if layer_idx not in self._cpu_weights:
|
||||
self._cpu_weights[layer_idx] = {}
|
||||
self._cpu_dtypes[layer_idx] = {}
|
||||
|
||||
self._record_meta(name, tensor)
|
||||
|
||||
cpu_weight = tensor.detach().to("cpu")
|
||||
if self.pin_cpu_memory:
|
||||
cpu_weight = cpu_weight.pin_memory()
|
||||
|
||||
self._cpu_weights[layer_idx][name] = cpu_weight
|
||||
self._cpu_dtypes[layer_idx][name] = tensor.dtype
|
||||
|
||||
if self.device is not None:
|
||||
tensor.data = self._make_placeholder(name)
|
||||
|
||||
@torch.compiler.disable
|
||||
def initialize(self) -> None:
|
||||
"""Offload all matched layer tensors to CPU and prefetch layer 0 (sync)."""
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
self._named_parameters = dict(self.model.named_parameters())
|
||||
self._named_buffers = dict(self.model.named_buffers())
|
||||
|
||||
for name, param in self._named_parameters.items():
|
||||
layer_idx = self._match_layer_idx(name)
|
||||
if layer_idx is None or layer_idx >= self.num_layers:
|
||||
continue
|
||||
self._offload_tensor(name, param, layer_idx)
|
||||
|
||||
for name, buf in self._named_buffers.items():
|
||||
layer_idx = self._match_layer_idx(name)
|
||||
if layer_idx is None or layer_idx >= self.num_layers:
|
||||
continue
|
||||
self._offload_tensor(name, buf, layer_idx)
|
||||
|
||||
self.prefetch_layer(0, non_blocking=False)
|
||||
if self.copy_stream is not None:
|
||||
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
||||
|
||||
@torch.compiler.disable
|
||||
def prefetch_layer(self, layer_idx: int, non_blocking: bool = True) -> None:
|
||||
"""Prefetch a layer's tensors from CPU to GPU (async on copy_stream)."""
|
||||
if not self.enabled or self.device is None or self.copy_stream is None:
|
||||
return
|
||||
if layer_idx < 0 or layer_idx >= self.num_layers:
|
||||
return
|
||||
if layer_idx in self._gpu_layers:
|
||||
return
|
||||
if layer_idx not in self._cpu_weights:
|
||||
return
|
||||
|
||||
self.copy_stream.wait_stream(torch.cuda.current_stream())
|
||||
|
||||
param_names: Set[str] = set()
|
||||
with torch.cuda.stream(self.copy_stream):
|
||||
for name, cpu_weight in self._cpu_weights[layer_idx].items():
|
||||
target = self._get_target(name)
|
||||
|
||||
gpu_weight = torch.empty(
|
||||
cpu_weight.shape,
|
||||
dtype=self._cpu_dtypes[layer_idx][name],
|
||||
device=self.device,
|
||||
)
|
||||
gpu_weight.copy_(cpu_weight, non_blocking=non_blocking)
|
||||
|
||||
target.data = gpu_weight
|
||||
param_names.add(name)
|
||||
|
||||
self._gpu_layers[layer_idx] = param_names
|
||||
|
||||
@contextmanager
|
||||
def layer_scope(
|
||||
self,
|
||||
*,
|
||||
prefetch_layer_idx: Optional[int],
|
||||
release_layer_idx: Optional[int],
|
||||
non_blocking: bool = True,
|
||||
):
|
||||
if self.enabled and release_layer_idx is not None:
|
||||
cur = release_layer_idx
|
||||
if (
|
||||
cur not in self._gpu_layers
|
||||
and cur in self._cpu_weights
|
||||
and self.device is not None
|
||||
and self.copy_stream is not None
|
||||
):
|
||||
self.prefetch_layer(cur, non_blocking=False)
|
||||
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
||||
|
||||
if self.enabled and prefetch_layer_idx is not None:
|
||||
self.prefetch_layer(prefetch_layer_idx, non_blocking=non_blocking)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if self.enabled and self.copy_stream is not None:
|
||||
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
||||
if self.enabled and release_layer_idx is not None:
|
||||
self.release_layer(release_layer_idx)
|
||||
|
||||
|
||||
@torch.compiler.disable
|
||||
def release_layer(self, layer_idx: int) -> None:
|
||||
"""Release a layer's tensors back to placeholders (free VRAM)."""
|
||||
if not self.enabled or self.device is None:
|
||||
return
|
||||
|
||||
if layer_idx < 0:
|
||||
return
|
||||
|
||||
param_names = self._gpu_layers.pop(layer_idx, None)
|
||||
if not param_names:
|
||||
return
|
||||
|
||||
for name in param_names:
|
||||
target = self._get_target(name)
|
||||
# Ensure meta exists even if something unexpected happened
|
||||
self._record_meta(name, target)
|
||||
target.data = self._make_placeholder(name)
|
||||
|
||||
@torch.compiler.disable
|
||||
def release_all(self) -> None:
|
||||
"""Release all currently-resident layers back to placeholders."""
|
||||
if not self.enabled or self.device is None:
|
||||
return
|
||||
|
||||
if self.copy_stream is not None:
|
||||
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
||||
|
||||
for layer_idx in list(self._gpu_layers.keys()):
|
||||
param_names = self._gpu_layers.pop(layer_idx, None)
|
||||
if not param_names:
|
||||
continue
|
||||
for name in param_names:
|
||||
target = self._get_target(name)
|
||||
self._record_meta(name, target)
|
||||
target.data = self._make_placeholder(name)
|
||||
@@ -22,15 +22,21 @@ from transformers.utils import SAFE_WEIGHTS_INDEX_NAME
|
||||
from fastvideo.configs.models import EncoderConfig
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.layers.quantization import get_quantization_config
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.encoders.base import TextEncoder
|
||||
from fastvideo.models.hf_transformer_utils import get_diffusers_config
|
||||
from fastvideo.models.loader.fsdp_load import maybe_load_fsdp_model, shard_model
|
||||
from fastvideo.models.loader.utils import set_default_torch_dtype
|
||||
from fastvideo.models.loader.weight_utils import (
|
||||
filter_duplicate_safetensors_files, filter_files_not_needed_for_inference,
|
||||
pt_weights_iterator, safetensors_weights_iterator)
|
||||
filter_duplicate_safetensors_files,
|
||||
filter_files_not_needed_for_inference,
|
||||
pt_weights_iterator,
|
||||
safetensors_weights_iterator,
|
||||
)
|
||||
from fastvideo.models.registry import ModelRegistry
|
||||
from fastvideo.utils import PRECISION_TO_TYPE
|
||||
from fastvideo.models.layerwise_offload import LayerwiseOffloadManager
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -45,26 +51,27 @@ class ComponentLoader(ABC):
|
||||
def load(self, model_path: str, fastvideo_args: FastVideoArgs):
|
||||
"""
|
||||
Load the component based on the model path, architecture, and inference args.
|
||||
|
||||
|
||||
Args:
|
||||
model_path: Path to the component model
|
||||
fastvideo_args: FastVideoArgs
|
||||
|
||||
|
||||
Returns:
|
||||
The loaded component
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def for_module_type(cls, module_type: str,
|
||||
transformers_or_diffusers: str) -> 'ComponentLoader':
|
||||
def for_module_type(
|
||||
cls, module_type: str, transformers_or_diffusers: str
|
||||
) -> "ComponentLoader":
|
||||
"""
|
||||
Factory method to create a component loader for a specific module type.
|
||||
|
||||
|
||||
Args:
|
||||
module_type: Type of module (e.g., "vae", "text_encoder", "transformer", "scheduler")
|
||||
transformers_or_diffusers: Whether the module is from transformers or diffusers
|
||||
|
||||
|
||||
Returns:
|
||||
A component loader for the specified module type
|
||||
"""
|
||||
@@ -85,13 +92,16 @@ class ComponentLoader(ABC):
|
||||
if module_type in module_loaders:
|
||||
loader_cls, expected_library = module_loaders[module_type]
|
||||
# Assert that the library matches what's expected for this module type
|
||||
assert transformers_or_diffusers == expected_library, f"{module_type} must be loaded from {expected_library}, got {transformers_or_diffusers}"
|
||||
assert transformers_or_diffusers == expected_library, (
|
||||
f"{module_type} must be loaded from {expected_library}, got {transformers_or_diffusers}"
|
||||
)
|
||||
return loader_cls()
|
||||
|
||||
# For unknown module types, use a generic loader
|
||||
logger.warning(
|
||||
"No specific loader found for module type: %s. Using generic loader.",
|
||||
module_type)
|
||||
module_type,
|
||||
)
|
||||
return GenericComponentLoader(transformers_or_diffusers)
|
||||
|
||||
|
||||
@@ -154,36 +164,45 @@ class TextEncoderLoader(ComponentLoader):
|
||||
|
||||
if use_safetensors:
|
||||
hf_weights_files = filter_duplicate_safetensors_files(
|
||||
hf_weights_files, hf_folder, index_file)
|
||||
hf_weights_files, hf_folder, index_file
|
||||
)
|
||||
else:
|
||||
hf_weights_files = filter_files_not_needed_for_inference(
|
||||
hf_weights_files)
|
||||
hf_weights_files
|
||||
)
|
||||
|
||||
if len(hf_weights_files) == 0:
|
||||
raise RuntimeError(
|
||||
f"Cannot find any model weights with `{model_name_or_path}`")
|
||||
f"Cannot find any model weights with `{model_name_or_path}`"
|
||||
)
|
||||
|
||||
return hf_folder, hf_weights_files, use_safetensors
|
||||
|
||||
def _get_weights_iterator(
|
||||
self, source: "Source",
|
||||
to_cpu: bool) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
self, source: "Source", to_cpu: bool
|
||||
) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
"""Get an iterator for the model weights based on the load format."""
|
||||
hf_folder, hf_weights_files, use_safetensors = self._prepare_weights(
|
||||
source.model_or_path, source.fall_back_to_pt,
|
||||
source.allow_patterns_overrides)
|
||||
source.model_or_path,
|
||||
source.fall_back_to_pt,
|
||||
source.allow_patterns_overrides,
|
||||
)
|
||||
if use_safetensors:
|
||||
weights_iterator = safetensors_weights_iterator(hf_weights_files,
|
||||
to_cpu=to_cpu)
|
||||
weights_iterator = safetensors_weights_iterator(
|
||||
hf_weights_files, to_cpu=to_cpu
|
||||
)
|
||||
else:
|
||||
weights_iterator = pt_weights_iterator(hf_weights_files,
|
||||
to_cpu=to_cpu)
|
||||
weights_iterator = pt_weights_iterator(
|
||||
hf_weights_files, to_cpu=to_cpu
|
||||
)
|
||||
|
||||
if self.counter_before_loading_weights == 0.0:
|
||||
self.counter_before_loading_weights = time.perf_counter()
|
||||
# Apply the prefix.
|
||||
return ((source.prefix + name, tensor)
|
||||
for (name, tensor) in weights_iterator)
|
||||
return (
|
||||
(source.prefix + name, tensor)
|
||||
for (name, tensor) in weights_iterator
|
||||
)
|
||||
|
||||
def _get_all_weights(
|
||||
self,
|
||||
@@ -195,8 +214,9 @@ class TextEncoderLoader(ComponentLoader):
|
||||
model_path,
|
||||
prefix="",
|
||||
fall_back_to_pt=getattr(model, "fall_back_to_pt_during_load", True),
|
||||
allow_patterns_overrides=getattr(model, "allow_patterns_overrides",
|
||||
None),
|
||||
allow_patterns_overrides=getattr(
|
||||
model, "allow_patterns_overrides", None
|
||||
),
|
||||
)
|
||||
yield from self._get_weights_iterator(primary_weights, to_cpu)
|
||||
|
||||
@@ -225,53 +245,94 @@ class TextEncoderLoader(ComponentLoader):
|
||||
|
||||
# @TODO(Wei): Better way to handle this?
|
||||
try:
|
||||
encoder_config = fastvideo_args.pipeline_config.text_encoder_configs[
|
||||
0]
|
||||
encoder_config = (
|
||||
fastvideo_args.pipeline_config.text_encoder_configs[0]
|
||||
)
|
||||
encoder_config.update_model_arch(model_config)
|
||||
encoder_precision = fastvideo_args.pipeline_config.text_encoder_precisions[
|
||||
0]
|
||||
encoder_precision = (
|
||||
fastvideo_args.pipeline_config.text_encoder_precisions[0]
|
||||
)
|
||||
except Exception:
|
||||
encoder_config = fastvideo_args.pipeline_config.text_encoder_configs[
|
||||
1]
|
||||
encoder_config = (
|
||||
fastvideo_args.pipeline_config.text_encoder_configs[1]
|
||||
)
|
||||
encoder_config.update_model_arch(model_config)
|
||||
encoder_precision = fastvideo_args.pipeline_config.text_encoder_precisions[
|
||||
1]
|
||||
encoder_precision = (
|
||||
fastvideo_args.pipeline_config.text_encoder_precisions[1]
|
||||
)
|
||||
|
||||
target_device = get_local_torch_device()
|
||||
# TODO(will): add support for other dtypes
|
||||
return self.load_model(model_path, encoder_config, target_device,
|
||||
fastvideo_args, encoder_precision)
|
||||
return self.load_model(
|
||||
model_path,
|
||||
encoder_config,
|
||||
target_device,
|
||||
fastvideo_args,
|
||||
encoder_precision,
|
||||
use_text_encoder_override=True,
|
||||
)
|
||||
|
||||
def load_model(self,
|
||||
model_path: str,
|
||||
model_config: EncoderConfig,
|
||||
target_device: torch.device,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
dtype: str = "fp16"):
|
||||
use_cpu_offload = fastvideo_args.text_encoder_cpu_offload and len(
|
||||
getattr(model_config, "_fsdp_shard_conditions", [])) > 0
|
||||
def load_model(
|
||||
self,
|
||||
model_path: str,
|
||||
model_config: EncoderConfig,
|
||||
target_device: torch.device,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
dtype: str = "fp16",
|
||||
use_text_encoder_override: bool = False, # prevent subclasses from misusing
|
||||
):
|
||||
use_cpu_offload = (
|
||||
fastvideo_args.text_encoder_cpu_offload
|
||||
and len(getattr(model_config, "_fsdp_shard_conditions", [])) > 0
|
||||
)
|
||||
|
||||
from fastvideo.platforms import current_platform
|
||||
|
||||
if fastvideo_args.text_encoder_cpu_offload:
|
||||
target_device = torch.device(
|
||||
"mps") if current_platform.is_mps() else torch.device("cpu")
|
||||
target_device = (
|
||||
torch.device("mps")
|
||||
if current_platform.is_mps()
|
||||
else torch.device("cpu")
|
||||
)
|
||||
|
||||
# Set quantization config if specified
|
||||
if use_text_encoder_override and fastvideo_args.override_text_encoder_quant is not None:
|
||||
if fastvideo_args.override_text_encoder_safetensors is None:
|
||||
raise ValueError(
|
||||
"override_text_encoder_quant is set but override_text_encoder_safetensors is None"
|
||||
)
|
||||
quant_cls = get_quantization_config(
|
||||
fastvideo_args.override_text_encoder_quant
|
||||
)
|
||||
model_config.quant_config = quant_cls()
|
||||
|
||||
with set_default_torch_dtype(PRECISION_TO_TYPE[dtype]):
|
||||
with target_device:
|
||||
architectures = getattr(model_config, "architectures", [])
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(architectures)
|
||||
model = model_cls(model_config)
|
||||
model: TextEncoder = model_cls(model_config) # type: ignore
|
||||
|
||||
weights_to_load = {name for name, _ in model.named_parameters()}
|
||||
loaded_weights = model.load_weights(
|
||||
self._get_all_weights(model, model_path,
|
||||
to_cpu=use_cpu_offload))
|
||||
if use_text_encoder_override and fastvideo_args.override_text_encoder_safetensors is not None:
|
||||
loaded_weights: set[str] = model.load_weights(
|
||||
safetensors_weights_iterator(
|
||||
[fastvideo_args.override_text_encoder_safetensors],
|
||||
to_cpu=use_cpu_offload,
|
||||
)
|
||||
) # type: ignore
|
||||
else:
|
||||
loaded_weights: set[str] = model.load_weights(
|
||||
self._get_all_weights(
|
||||
model, model_path, to_cpu=use_cpu_offload
|
||||
)
|
||||
) # type: ignore
|
||||
|
||||
self.counter_after_loading_weights = time.perf_counter()
|
||||
logger.info(
|
||||
"Loading weights took %.2f seconds",
|
||||
self.counter_after_loading_weights -
|
||||
self.counter_before_loading_weights)
|
||||
self.counter_after_loading_weights
|
||||
- self.counter_before_loading_weights,
|
||||
)
|
||||
|
||||
# Explicitly move model to target device after loading weights
|
||||
model = model.to(target_device)
|
||||
@@ -296,7 +357,8 @@ class TextEncoderLoader(ComponentLoader):
|
||||
reshard_after_forward=True,
|
||||
mesh=mesh["offload"],
|
||||
fsdp_shard_conditions=model._fsdp_shard_conditions,
|
||||
pin_cpu_memory=fastvideo_args.pin_cpu_memory)
|
||||
pin_cpu_memory=fastvideo_args.pin_cpu_memory,
|
||||
)
|
||||
else:
|
||||
mesh = init_device_mesh(
|
||||
"cuda",
|
||||
@@ -309,20 +371,22 @@ class TextEncoderLoader(ComponentLoader):
|
||||
reshard_after_forward=True,
|
||||
mesh=mesh["offload"],
|
||||
fsdp_shard_conditions=model._fsdp_shard_conditions,
|
||||
pin_cpu_memory=fastvideo_args.pin_cpu_memory)
|
||||
pin_cpu_memory=fastvideo_args.pin_cpu_memory,
|
||||
)
|
||||
# We only enable strict check for non-quantized models
|
||||
# that have loaded weights tracking currently.
|
||||
# if loaded_weights is not None:
|
||||
weights_not_loaded = weights_to_load - loaded_weights
|
||||
if weights_not_loaded:
|
||||
raise ValueError("Following weights were not initialized from "
|
||||
f"checkpoint: {weights_not_loaded}")
|
||||
if weights_not_loaded and model_config.quant_config is None:
|
||||
raise ValueError(
|
||||
"Following weights were not initialized from "
|
||||
f"checkpoint: {weights_not_loaded}"
|
||||
)
|
||||
|
||||
return model.eval()
|
||||
|
||||
|
||||
class ImageEncoderLoader(TextEncoderLoader):
|
||||
|
||||
def load(self, model_path: str, fastvideo_args: FastVideoArgs):
|
||||
"""Load the text encoders based on the model path, and inference args."""
|
||||
# model_config: PretrainedConfig = get_hf_config(
|
||||
@@ -345,13 +409,21 @@ class ImageEncoderLoader(TextEncoderLoader):
|
||||
from fastvideo.platforms import current_platform
|
||||
|
||||
if fastvideo_args.image_encoder_cpu_offload:
|
||||
target_device = torch.device("mps") if current_platform.is_mps() else torch.device("cpu")
|
||||
target_device = (
|
||||
torch.device("mps")
|
||||
if current_platform.is_mps()
|
||||
else torch.device("cpu")
|
||||
)
|
||||
else:
|
||||
target_device = get_local_torch_device()
|
||||
# TODO(will): add support for other dtypes
|
||||
return self.load_model(
|
||||
model_path, encoder_config, target_device, fastvideo_args,
|
||||
fastvideo_args.pipeline_config.image_encoder_precision)
|
||||
model_path,
|
||||
encoder_config,
|
||||
target_device,
|
||||
fastvideo_args,
|
||||
fastvideo_args.pipeline_config.image_encoder_precision,
|
||||
)
|
||||
|
||||
|
||||
class ImageProcessorLoader(ComponentLoader):
|
||||
@@ -361,9 +433,12 @@ class ImageProcessorLoader(ComponentLoader):
|
||||
"""Load the image processor based on the model path, and inference args."""
|
||||
logger.info("Loading image processor from %s", model_path)
|
||||
|
||||
image_processor = AutoImageProcessor.from_pretrained(model_path, )
|
||||
logger.info("Loaded image processor: %s",
|
||||
image_processor.__class__.__name__)
|
||||
image_processor = AutoImageProcessor.from_pretrained(
|
||||
model_path,
|
||||
)
|
||||
logger.info(
|
||||
"Loaded image processor: %s", image_processor.__class__.__name__
|
||||
)
|
||||
return image_processor
|
||||
|
||||
|
||||
@@ -379,7 +454,7 @@ class TokenizerLoader(ComponentLoader):
|
||||
# in v0, this was same string as encoder_name "ClipTextModel"
|
||||
# TODO(will): pass these tokenizer kwargs from inference args? Maybe
|
||||
# other method of config?
|
||||
padding_size='right',
|
||||
padding_size="right",
|
||||
)
|
||||
logger.info("Loaded tokenizer: %s", tokenizer.__class__.__name__)
|
||||
return tokenizer
|
||||
@@ -392,7 +467,9 @@ class VAELoader(ComponentLoader):
|
||||
"""Load the VAE based on the model path, and inference args."""
|
||||
config = get_diffusers_config(model=model_path)
|
||||
class_name = config.pop("_class_name")
|
||||
assert class_name is not None, "Model config does not contain a _class_name attribute. Only diffusers format is supported."
|
||||
assert class_name is not None, (
|
||||
"Model config does not contain a _class_name attribute. Only diffusers format is supported."
|
||||
)
|
||||
fastvideo_args.model_paths["vae"] = model_path
|
||||
|
||||
vae_config = fastvideo_args.pipeline_config.vae_config
|
||||
@@ -401,23 +478,32 @@ class VAELoader(ComponentLoader):
|
||||
from fastvideo.platforms import current_platform
|
||||
|
||||
if fastvideo_args.vae_cpu_offload:
|
||||
target_device = torch.device("mps") if current_platform.is_mps() else torch.device("cpu")
|
||||
target_device = (
|
||||
torch.device("mps")
|
||||
if current_platform.is_mps()
|
||||
else torch.device("cpu")
|
||||
)
|
||||
else:
|
||||
target_device = get_local_torch_device()
|
||||
|
||||
with set_default_torch_dtype(PRECISION_TO_TYPE[
|
||||
fastvideo_args.pipeline_config.vae_precision] if fastvideo_args.pipeline_config.vae_precision else torch.bfloat16):
|
||||
with set_default_torch_dtype(
|
||||
PRECISION_TO_TYPE[fastvideo_args.pipeline_config.vae_precision]
|
||||
if fastvideo_args.pipeline_config.vae_precision
|
||||
else torch.bfloat16
|
||||
):
|
||||
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
vae = vae_cls(vae_config).to(target_device)
|
||||
|
||||
# Find all safetensors files
|
||||
safetensors_list = glob.glob(
|
||||
os.path.join(str(model_path), "*.safetensors"))
|
||||
os.path.join(str(model_path), "*.safetensors")
|
||||
)
|
||||
loaded = {}
|
||||
for sf_file in safetensors_list:
|
||||
loaded.update(safetensors_load_file(sf_file))
|
||||
vae.load_state_dict(
|
||||
loaded, strict=False) # We might only load encoder or decoder
|
||||
loaded, strict=False
|
||||
) # We might only load encoder or decoder
|
||||
|
||||
return vae.eval()
|
||||
|
||||
@@ -433,7 +519,8 @@ class TransformerLoader(ComponentLoader):
|
||||
if cls_name is None:
|
||||
raise ValueError(
|
||||
"Model config does not contain a _class_name attribute. "
|
||||
"Only diffusers format is supported.")
|
||||
"Only diffusers format is supported."
|
||||
)
|
||||
|
||||
logger.info("transformer cls_name: %s", cls_name)
|
||||
if fastvideo_args.override_transformer_cls_name is not None:
|
||||
@@ -450,40 +537,54 @@ class TransformerLoader(ComponentLoader):
|
||||
|
||||
# Find all safetensors files
|
||||
safetensors_list = glob.glob(
|
||||
os.path.join(str(model_path), "*.safetensors"))
|
||||
os.path.join(str(model_path), "*.safetensors")
|
||||
)
|
||||
if not safetensors_list:
|
||||
raise ValueError(f"No safetensors files found in {model_path}")
|
||||
|
||||
# Check if we should use custom initialization weights
|
||||
custom_weights_path = getattr(fastvideo_args, 'init_weights_from_safetensors', None)
|
||||
use_custom_weights = (custom_weights_path and os.path.exists(custom_weights_path) and
|
||||
not hasattr(fastvideo_args, '_loading_teacher_critic_model'))
|
||||
custom_weights_path = getattr(
|
||||
fastvideo_args, "init_weights_from_safetensors", None
|
||||
)
|
||||
use_custom_weights = (
|
||||
custom_weights_path
|
||||
and os.path.exists(custom_weights_path)
|
||||
and not hasattr(fastvideo_args, "_loading_teacher_critic_model")
|
||||
)
|
||||
|
||||
if use_custom_weights:
|
||||
if 'transformer_2' in model_path:
|
||||
custom_weights_path = getattr(fastvideo_args, 'init_weights_from_safetensors_2', None)
|
||||
assert custom_weights_path is not None, "Custom initialization weights must be provided"
|
||||
if "transformer_2" in model_path:
|
||||
custom_weights_path = getattr(
|
||||
fastvideo_args, "init_weights_from_safetensors_2", None
|
||||
)
|
||||
assert custom_weights_path is not None, (
|
||||
"Custom initialization weights must be provided"
|
||||
)
|
||||
if os.path.isdir(custom_weights_path):
|
||||
safetensors_list = glob.glob(
|
||||
os.path.join(str(custom_weights_path), "*.safetensors"))
|
||||
os.path.join(str(custom_weights_path), "*.safetensors")
|
||||
)
|
||||
else:
|
||||
assert custom_weights_path.endswith(".safetensors"), "Custom initialization weights must be a safetensors file"
|
||||
assert custom_weights_path.endswith(".safetensors"), (
|
||||
"Custom initialization weights must be a safetensors file"
|
||||
)
|
||||
safetensors_list = [custom_weights_path]
|
||||
|
||||
logger.info("Loading model from %s safetensors files: %s",
|
||||
len(safetensors_list), safetensors_list)
|
||||
logger.info(
|
||||
"Loading model from %s safetensors files: %s",
|
||||
len(safetensors_list),
|
||||
safetensors_list,
|
||||
)
|
||||
|
||||
default_dtype = PRECISION_TO_TYPE[
|
||||
fastvideo_args.pipeline_config.dit_precision]
|
||||
fastvideo_args.pipeline_config.dit_precision
|
||||
]
|
||||
|
||||
# Load the model using FSDP loader
|
||||
assert fastvideo_args.hsdp_shard_dim is not None
|
||||
model = maybe_load_fsdp_model(
|
||||
model_cls=model_cls,
|
||||
init_params={
|
||||
"config": dit_config,
|
||||
"hf_config": hf_config
|
||||
},
|
||||
init_params={"config": dit_config, "hf_config": hf_config},
|
||||
weight_dir_list=safetensors_list,
|
||||
device=get_local_torch_device(),
|
||||
hsdp_replicate_dim=fastvideo_args.hsdp_replicate_dim,
|
||||
@@ -498,15 +599,47 @@ class TransformerLoader(ComponentLoader):
|
||||
output_dtype=None,
|
||||
training_mode=fastvideo_args.training_mode,
|
||||
enable_torch_compile=fastvideo_args.enable_torch_compile,
|
||||
torch_compile_kwargs=fastvideo_args.torch_compile_kwargs)
|
||||
|
||||
torch_compile_kwargs=fastvideo_args.torch_compile_kwargs,
|
||||
)
|
||||
|
||||
total_params = sum(p.numel() for p in model.parameters())
|
||||
logger.info("Loaded model with %.2fB parameters", total_params / 1e9)
|
||||
|
||||
assert next(model.parameters()).dtype == default_dtype, "Model dtype does not match default dtype"
|
||||
assert next(model.parameters()).dtype == default_dtype, (
|
||||
"Model dtype does not match default dtype"
|
||||
)
|
||||
|
||||
model = model.eval()
|
||||
|
||||
if fastvideo_args.dit_layerwise_offload and hasattr(model, "blocks"):
|
||||
# Check if this is a Wan model (only Wan models support layerwise offload)
|
||||
is_wan_model = "Wan" in cls_name
|
||||
if not is_wan_model:
|
||||
logger.warning(
|
||||
"Layerwise offload is currently only supported for Wan models. "
|
||||
"Model class '%s' does not support layerwise offload. "
|
||||
"Disabling layerwise offload for this model.",
|
||||
cls_name
|
||||
)
|
||||
else:
|
||||
try:
|
||||
num_layers = len(getattr(model, "blocks"))
|
||||
except TypeError:
|
||||
num_layers = None
|
||||
if isinstance(num_layers, int) and num_layers > 0:
|
||||
# Ensure model is on the correct device (CUDA) before initializing manager
|
||||
# This ensures non-managed parameters (embeddings, final norms) are on GPU
|
||||
model = model.to(get_local_torch_device())
|
||||
mgr = LayerwiseOffloadManager(
|
||||
model,
|
||||
module_list_attr="blocks",
|
||||
num_layers=num_layers,
|
||||
enabled=True,
|
||||
pin_cpu_memory=fastvideo_args.pin_cpu_memory,
|
||||
auto_initialize=True,
|
||||
)
|
||||
setattr(model, "_layerwise_offload_manager", mgr)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
@@ -518,7 +651,9 @@ class SchedulerLoader(ComponentLoader):
|
||||
config = get_diffusers_config(model=model_path)
|
||||
|
||||
class_name = config.pop("_class_name")
|
||||
assert class_name is not None, "Model config does not contain a _class_name attribute. Only diffusers format is supported."
|
||||
assert class_name is not None, (
|
||||
"Model config does not contain a _class_name attribute. Only diffusers format is supported."
|
||||
)
|
||||
|
||||
scheduler_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
|
||||
@@ -527,7 +662,8 @@ class SchedulerLoader(ComponentLoader):
|
||||
scheduler.set_shift(fastvideo_args.pipeline_config.flow_shift)
|
||||
if fastvideo_args.pipeline_config.timesteps_scale is not None:
|
||||
scheduler.set_timesteps_scale(
|
||||
fastvideo_args.pipeline_config.timesteps_scale)
|
||||
fastvideo_args.pipeline_config.timesteps_scale
|
||||
)
|
||||
return scheduler
|
||||
|
||||
|
||||
@@ -540,8 +676,11 @@ class GenericComponentLoader(ComponentLoader):
|
||||
|
||||
def load(self, model_path: str, fastvideo_args: FastVideoArgs):
|
||||
"""Load a generic component based on the model path, and inference args."""
|
||||
logger.warning("Using generic loader for %s with library %s",
|
||||
model_path, self.library)
|
||||
logger.warning(
|
||||
"Using generic loader for %s with library %s",
|
||||
model_path,
|
||||
self.library,
|
||||
)
|
||||
|
||||
if self.library == "transformers":
|
||||
from transformers import AutoModel
|
||||
@@ -551,8 +690,10 @@ class GenericComponentLoader(ComponentLoader):
|
||||
trust_remote_code=fastvideo_args.trust_remote_code,
|
||||
revision=fastvideo_args.revision,
|
||||
)
|
||||
logger.info("Loaded generic transformers model: %s",
|
||||
model.__class__.__name__)
|
||||
logger.info(
|
||||
"Loaded generic transformers model: %s",
|
||||
model.__class__.__name__,
|
||||
)
|
||||
return model
|
||||
elif self.library == "diffusers":
|
||||
logger.warning(
|
||||
@@ -574,18 +715,21 @@ class PipelineComponentLoader:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def load_module(module_name: str, component_model_path: str,
|
||||
transformers_or_diffusers: str,
|
||||
fastvideo_args: FastVideoArgs):
|
||||
def load_module(
|
||||
module_name: str,
|
||||
component_model_path: str,
|
||||
transformers_or_diffusers: str,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
):
|
||||
"""
|
||||
Load a pipeline module.
|
||||
|
||||
|
||||
Args:
|
||||
module_name: Name of the module (e.g., "vae", "text_encoder", "transformer", "scheduler")
|
||||
component_model_path: Path to the component model
|
||||
transformers_or_diffusers: Whether the module is from transformers or diffusers
|
||||
pipeline_args: Inference arguments
|
||||
|
||||
|
||||
Returns:
|
||||
The loaded module
|
||||
"""
|
||||
@@ -597,8 +741,9 @@ class PipelineComponentLoader:
|
||||
)
|
||||
|
||||
# Get the appropriate loader for this module type
|
||||
loader = ComponentLoader.for_module_type(module_name,
|
||||
transformers_or_diffusers)
|
||||
loader = ComponentLoader.for_module_type(
|
||||
module_name, transformers_or_diffusers
|
||||
)
|
||||
|
||||
# Load the module
|
||||
return loader.load(component_model_path, fastvideo_args)
|
||||
|
||||
@@ -20,6 +20,7 @@ from torch.distributed.fsdp import (CPUOffloadPolicy, FSDPModule,
|
||||
from torch.nn.modules.module import _IncompatibleKeys
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pin_memory import is_pin_memory_available
|
||||
from fastvideo.models.loader.utils import (get_param_names_mapping,
|
||||
hf_to_custom_state_dict)
|
||||
from fastvideo.models.loader.weight_utils import safetensors_weights_iterator
|
||||
@@ -212,6 +213,11 @@ def shard_model(
|
||||
"mp_policy": mp_policy,
|
||||
}
|
||||
if cpu_offload:
|
||||
if pin_cpu_memory and not is_pin_memory_available():
|
||||
logger.warning(
|
||||
"Pinned memory is unavailable; disabling pin_cpu_memory for "
|
||||
"FSDP offload.")
|
||||
pin_cpu_memory = False
|
||||
fsdp_kwargs["offload_policy"] = CPUOffloadPolicy(
|
||||
pin_memory=pin_cpu_memory)
|
||||
|
||||
@@ -318,7 +324,7 @@ def load_model_from_full_model_state_dict(
|
||||
unused_keys)
|
||||
|
||||
# List of allowed parameter name patterns
|
||||
ALLOWED_NEW_PARAM_PATTERNS = ["gate_compress"] # Can be extended as needed
|
||||
ALLOWED_NEW_PARAM_PATTERNS = ["gate_compress", "proj_l"] # Can be extended as needed
|
||||
for new_param_name in unused_keys:
|
||||
if not any(pattern in new_param_name
|
||||
for pattern in ALLOWED_NEW_PARAM_PATTERNS):
|
||||
|
||||
@@ -30,8 +30,8 @@ _TEXT_TO_VIDEO_DIT_MODELS = {
|
||||
"CausalWanTransformer3DModel": ("dits", "causal_wanvideo", "CausalWanTransformer3DModel"),
|
||||
"StepVideoModel": ("dits", "stepvideo", "StepVideoModel"),
|
||||
"CosmosTransformer3DModel": ("dits", "cosmos", "CosmosTransformer3DModel"),
|
||||
"LongCatVideoTransformer3DModel": ("dits", "longcat_video_dit", "LongCatVideoTransformer3DModel"), # Wrapper (Phase 1)
|
||||
"LongCatTransformer3DModel": ("dits", "longcat", "LongCatTransformer3DModel"), # Native (Phase 2)
|
||||
"LongCatVideoTransformer3DModel": ("dits", "longcat_video_dit", "LongCatVideoTransformer3DModel"),
|
||||
"LongCatTransformer3DModel": ("dits", "longcat", "LongCatTransformer3DModel"),
|
||||
}
|
||||
|
||||
_IMAGE_TO_VIDEO_DIT_MODELS = {
|
||||
@@ -75,6 +75,8 @@ _SCHEDULERS = {
|
||||
"SelfForcingFlowMatchScheduler":
|
||||
("schedulers", "scheduling_self_forcing_flow_match",
|
||||
"SelfForcingFlowMatchScheduler"),
|
||||
"RCMScheduler":
|
||||
("schedulers", "scheduling_rcm", "RCMScheduler"),
|
||||
}
|
||||
|
||||
_FAST_VIDEO_MODELS = {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Scheduler adapter interfaces for unified denoising.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class SchedulerAdapter(Protocol):
|
||||
def scale_model_input(self, latents: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
...
|
||||
|
||||
def step(self, noise_pred: torch.Tensor, t: torch.Tensor,
|
||||
latents: torch.Tensor, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
def add_noise(self, latents: torch.Tensor, noise: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
...
|
||||
|
||||
def set_timesteps(self, num_steps: int, device: torch.device | None = None,
|
||||
**kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
class DefaultSchedulerAdapter:
|
||||
def __init__(self, scheduler: Any) -> None:
|
||||
self.scheduler = scheduler
|
||||
|
||||
def scale_model_input(self, latents: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
return self.scheduler.scale_model_input(latents, t)
|
||||
|
||||
def step(self, noise_pred: torch.Tensor, t: torch.Tensor,
|
||||
latents: torch.Tensor, **kwargs: Any) -> Any:
|
||||
return self.scheduler.step(noise_pred, t, latents, **kwargs)
|
||||
|
||||
def add_noise(self, latents: torch.Tensor, noise: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
return self.scheduler.add_noise(latents, noise, t)
|
||||
|
||||
def set_timesteps(self, num_steps: int, device: torch.device | None = None,
|
||||
**kwargs: Any) -> Any:
|
||||
return self.scheduler.set_timesteps(num_steps, device=device, **kwargs)
|
||||
@@ -0,0 +1,323 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# rCM (recurrent Consistency Model) Scheduler for TurboDiffusion support
|
||||
#
|
||||
# This scheduler implements the rCM sampling method from TurboDiffusion,
|
||||
# enabling 1-4 step video generation with distilled checkpoints.
|
||||
#
|
||||
# Reference:
|
||||
# TurboDiffusion: Accelerating Video Diffusion Models by 100-200 Times
|
||||
# https://arxiv.org/pdf/2512.16093
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
from diffusers.schedulers.scheduling_utils import SchedulerMixin
|
||||
from diffusers.utils import BaseOutput
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.schedulers.base import BaseScheduler
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RCMSchedulerOutput(BaseOutput):
|
||||
"""
|
||||
Output class for the RCM scheduler's `step` function output.
|
||||
|
||||
Args:
|
||||
prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, ...)`):
|
||||
Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used
|
||||
as next model input in the denoising loop.
|
||||
"""
|
||||
|
||||
prev_sample: torch.FloatTensor
|
||||
|
||||
|
||||
class RCMScheduler(SchedulerMixin, ConfigMixin, BaseScheduler):
|
||||
"""
|
||||
rCM (recurrent Consistency Model) scheduler for TurboDiffusion.
|
||||
|
||||
This scheduler implements the rCM sampling method which enables 1-4 step
|
||||
video generation using distilled checkpoints. It uses:
|
||||
1. TrigFlow → RectifiedFlow timestep conversion
|
||||
2. SDE sampling formula: x = (1 - t_next) * (x - t_cur * v_pred) + t_next * noise
|
||||
|
||||
Args:
|
||||
num_train_timesteps (`int`, defaults to 1000):
|
||||
The number of diffusion steps used to train the model.
|
||||
sigma_max (`float`, defaults to 80.0):
|
||||
The initial sigma value for rCM sampling. Controls the noise level
|
||||
at the start of sampling.
|
||||
mid_timesteps (`list[float]`, *optional*):
|
||||
Custom intermediate timesteps. If None, uses optimized defaults
|
||||
[1.5, 1.4, 1.0] for better visual quality.
|
||||
"""
|
||||
|
||||
_compatibles: list[Any] = []
|
||||
order = 1
|
||||
|
||||
@register_to_config
|
||||
def __init__(
|
||||
self,
|
||||
num_train_timesteps: int = 1000,
|
||||
sigma_max: float = 80.0,
|
||||
mid_timesteps: list[float] | None = None,
|
||||
):
|
||||
# Default mid timesteps optimized for visual quality
|
||||
if mid_timesteps is None:
|
||||
mid_timesteps = [1.5, 1.4, 1.0]
|
||||
self._mid_timesteps = mid_timesteps
|
||||
|
||||
self.num_train_timesteps = num_train_timesteps
|
||||
self.sigma_max = sigma_max
|
||||
|
||||
# Initialize with default timesteps (will be set properly via set_timesteps)
|
||||
self.timesteps = torch.tensor([1.0, 0.0], dtype=torch.float64)
|
||||
self.sigmas = self.timesteps.clone()
|
||||
|
||||
self._step_index: int | None = None
|
||||
self._begin_index: int | None = None
|
||||
|
||||
BaseScheduler.__init__(self)
|
||||
|
||||
@property
|
||||
def step_index(self) -> int | None:
|
||||
"""
|
||||
The index counter for current timestep. Increases by 1 after each scheduler step.
|
||||
"""
|
||||
return self._step_index
|
||||
|
||||
@property
|
||||
def begin_index(self) -> int | None:
|
||||
"""
|
||||
The index for the first timestep.
|
||||
"""
|
||||
return self._begin_index
|
||||
|
||||
@property
|
||||
def init_noise_sigma(self) -> float:
|
||||
"""
|
||||
Initial noise sigma for scaling latents.
|
||||
|
||||
In rCM, initial noise is scaled by the first sigma value:
|
||||
x_0 = noise * sigmas[0]
|
||||
|
||||
This property is used by LatentPreparationStage to scale initial latents.
|
||||
"""
|
||||
return float(self.sigmas[0])
|
||||
|
||||
def set_begin_index(self, begin_index: int = 0) -> None:
|
||||
"""
|
||||
Sets the begin index for the scheduler.
|
||||
|
||||
Args:
|
||||
begin_index (`int`):
|
||||
The begin index for the scheduler.
|
||||
"""
|
||||
self._begin_index = begin_index
|
||||
|
||||
def set_shift(self, shift: float) -> None:
|
||||
"""
|
||||
rCM doesn't use shift parameter, but required by BaseScheduler.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_timesteps(
|
||||
self,
|
||||
num_inference_steps: int,
|
||||
device: str | torch.device | None = None,
|
||||
sigma_max: float | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Sets the discrete timesteps used for the rCM sampling process.
|
||||
|
||||
The timesteps are computed using TrigFlow → RectifiedFlow conversion:
|
||||
1. Start with atan(sigma_max) and intermediate values
|
||||
2. Convert via: t = sin(t) / (cos(t) + sin(t))
|
||||
|
||||
Args:
|
||||
num_inference_steps (`int`):
|
||||
The number of diffusion steps (1-4 for rCM).
|
||||
device (`str` or `torch.device`, *optional*):
|
||||
The device to move timesteps to.
|
||||
sigma_max (`float`, *optional*):
|
||||
Override the initial sigma value.
|
||||
"""
|
||||
if num_inference_steps < 1 or num_inference_steps > 4:
|
||||
logger.warning(
|
||||
"rCM is optimized for 1-4 steps, got %d steps. "
|
||||
"Performance may be suboptimal.", num_inference_steps
|
||||
)
|
||||
|
||||
self.num_inference_steps = num_inference_steps
|
||||
|
||||
if sigma_max is not None:
|
||||
self.sigma_max = sigma_max
|
||||
|
||||
# Build timestep schedule
|
||||
mid_t = self._mid_timesteps[:num_inference_steps - 1]
|
||||
|
||||
# TrigFlow timesteps: [atan(sigma_max), mid_t..., 0]
|
||||
t_steps = torch.tensor(
|
||||
[math.atan(self.sigma_max), *mid_t, 0],
|
||||
dtype=torch.float64,
|
||||
device=device,
|
||||
)
|
||||
|
||||
# Convert TrigFlow → RectifiedFlow: t = sin(t) / (cos(t) + sin(t))
|
||||
t_steps = torch.sin(t_steps) / (torch.cos(t_steps) + torch.sin(t_steps))
|
||||
|
||||
# Store raw sigmas for use in step() formula
|
||||
self.sigmas = t_steps.clone()
|
||||
|
||||
# Scale timesteps by 1000 for model input (as per TurboDiffusion)
|
||||
self.timesteps = t_steps * 1000
|
||||
|
||||
self._step_index = None
|
||||
self._begin_index = None
|
||||
|
||||
logger.debug("rCM timesteps (scaled): %s", self.timesteps.tolist())
|
||||
logger.debug("rCM sigmas (raw): %s", self.sigmas.tolist())
|
||||
|
||||
def _init_step_index(self, timestep: torch.FloatTensor | None = None) -> None:
|
||||
"""Initialize step index at the beginning of sampling."""
|
||||
if self._begin_index is None:
|
||||
self._step_index = 0
|
||||
else:
|
||||
self._step_index = self._begin_index
|
||||
|
||||
def scale_model_input(
|
||||
self,
|
||||
sample: torch.Tensor,
|
||||
timestep: int | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
rCM doesn't scale model input, returns sample as-is.
|
||||
"""
|
||||
return sample
|
||||
|
||||
def scale_noise(
|
||||
self,
|
||||
sample: torch.FloatTensor,
|
||||
timestep: torch.FloatTensor | None = None,
|
||||
noise: torch.FloatTensor | None = None,
|
||||
) -> torch.FloatTensor:
|
||||
"""
|
||||
Scale initial noise for rCM sampling.
|
||||
|
||||
In rCM, initial noise is scaled by the first timestep (raw sigma):
|
||||
x_0 = noise * t_steps[0]
|
||||
|
||||
Args:
|
||||
sample: Not used (for API compatibility)
|
||||
timestep: Not used (for API compatibility)
|
||||
noise: The noise tensor to scale
|
||||
|
||||
Returns:
|
||||
Scaled noise tensor ready for sampling
|
||||
"""
|
||||
if noise is None:
|
||||
raise ValueError("noise must be provided for rCM scale_noise")
|
||||
|
||||
# Use raw sigma (not scaled timestep) for initial noise scaling
|
||||
t_initial = self.sigmas[0]
|
||||
return noise.to(torch.float64) * t_initial
|
||||
|
||||
def step(
|
||||
self,
|
||||
model_output: torch.FloatTensor,
|
||||
timestep: int | torch.Tensor,
|
||||
sample: torch.FloatTensor,
|
||||
generator: torch.Generator | None = None,
|
||||
return_dict: bool = True,
|
||||
) -> RCMSchedulerOutput | tuple[torch.FloatTensor, ...]:
|
||||
"""
|
||||
Predict the sample from the previous timestep using rCM update rule.
|
||||
|
||||
The rCM update formula is:
|
||||
x_{t+1} = (1 - t_next) * (x_t - t_cur * v_pred) + t_next * noise
|
||||
|
||||
Args:
|
||||
model_output (`torch.FloatTensor`):
|
||||
The velocity prediction from the model (v_pred).
|
||||
timestep (`int` or `torch.Tensor`):
|
||||
The current timestep index (not the actual timestep value).
|
||||
For rCM, this should be the index into self.timesteps.
|
||||
sample (`torch.FloatTensor`):
|
||||
Current sample x_t.
|
||||
generator (`torch.Generator`, *optional*):
|
||||
Random number generator for noise.
|
||||
return_dict (`bool`):
|
||||
Whether to return RCMSchedulerOutput or tuple.
|
||||
|
||||
Returns:
|
||||
`RCMSchedulerOutput` or `tuple`:
|
||||
The denoised sample for the next step.
|
||||
"""
|
||||
if self._step_index is None:
|
||||
self._init_step_index()
|
||||
|
||||
assert self._step_index is not None
|
||||
|
||||
# Get current and next sigma values (raw, unscaled) for rCM formula
|
||||
# Note: self.timesteps is scaled by 1000 for model input,
|
||||
# but we need raw values for the step formula
|
||||
t_cur = self.sigmas[self._step_index]
|
||||
# On the final step, t_next should be 0 (fully denoised)
|
||||
if self._step_index + 1 < len(self.sigmas):
|
||||
t_next = self.sigmas[self._step_index + 1]
|
||||
else:
|
||||
t_next = torch.tensor(0.0, device=sample.device, dtype=torch.float64)
|
||||
|
||||
# Ensure we're working in float64 for precision
|
||||
sample = sample.to(torch.float64)
|
||||
model_output = model_output.to(torch.float64)
|
||||
|
||||
# rCM update: x = (1 - t_next) * (x - t_cur * v_pred) + t_next * noise
|
||||
x_denoised = sample - t_cur * model_output
|
||||
|
||||
# Generate noise for SDE sampling
|
||||
if isinstance(generator, list):
|
||||
generator = generator[0]
|
||||
noise = torch.randn(
|
||||
sample.shape,
|
||||
dtype=torch.float32,
|
||||
device="cpu",
|
||||
generator=generator,
|
||||
).to(sample.device).to(torch.float64)
|
||||
|
||||
prev_sample = (1 - t_next) * x_denoised + t_next * noise
|
||||
|
||||
# Increment step counter
|
||||
self._step_index += 1
|
||||
|
||||
# Cast back to model output dtype
|
||||
prev_sample = prev_sample.to(model_output.dtype)
|
||||
|
||||
if not return_dict:
|
||||
return (prev_sample,)
|
||||
|
||||
return RCMSchedulerOutput(prev_sample=prev_sample)
|
||||
|
||||
def add_noise(
|
||||
self,
|
||||
original_samples: torch.Tensor,
|
||||
noise: torch.Tensor,
|
||||
timesteps: torch.IntTensor,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Add noise to samples (forward diffusion process).
|
||||
|
||||
Not typically used for rCM inference, but provided for API compatibility.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"add_noise is not implemented for RCMScheduler. "
|
||||
"Use scale_noise for initializing the sampling process."
|
||||
)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.config.num_train_timesteps
|
||||
@@ -1252,6 +1252,52 @@ class AutoencoderKLWan(nn.Module, ParallelTiledVAE):
|
||||
dec = dec[:, :, start_frame_idx:]
|
||||
return dec
|
||||
|
||||
def get_streaming_cache(self) -> list[torch.Tensor | None]:
|
||||
def _count_conv3d(model) -> int:
|
||||
count = 0
|
||||
for m in model.modules():
|
||||
if isinstance(m, WanCausalConv3d):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
conv_num = _count_conv3d(self.decoder)
|
||||
return [None] * conv_num
|
||||
|
||||
def streaming_decode(
|
||||
self,
|
||||
z: torch.Tensor,
|
||||
cache: list[torch.Tensor | None],
|
||||
is_first_chunk: bool = False,
|
||||
) -> tuple[torch.Tensor, list[torch.Tensor | None]]:
|
||||
"""
|
||||
Args:
|
||||
z (`torch.Tensor`): Latent tensor of shape [B, C, T, H, W].
|
||||
cache (`list[torch.Tensor | None]`): The VAE cache.
|
||||
is_first_chunk (`bool`): Whether this is the first chunk in the sequence.
|
||||
|
||||
Returns:
|
||||
A tuple of (decoded_frames, updated_cache).
|
||||
"""
|
||||
iter_ = z.shape[2]
|
||||
x = self.post_quant_conv(z)
|
||||
|
||||
with forward_context(feat_cache_arg=cache, feat_idx_arg=0):
|
||||
outputs = []
|
||||
for i in range(iter_):
|
||||
feat_idx.set(0)
|
||||
first_chunk.set(is_first_chunk and i == 0)
|
||||
decoded_chunk = self.decoder(x[:, :, i:i + 1, :, :])
|
||||
outputs.append(decoded_chunk)
|
||||
out = torch.cat(outputs, dim=2)
|
||||
|
||||
if self.config.patch_size is not None:
|
||||
out = unpatchify(out, patch_size=self.config.patch_size)
|
||||
|
||||
out = out.float()
|
||||
out = torch.clamp(out, min=-1.0, max=1.0)
|
||||
|
||||
return out, cache
|
||||
|
||||
def forward(
|
||||
self,
|
||||
sample: torch.Tensor,
|
||||
@@ -1272,3 +1318,4 @@ class AutoencoderKLWan(nn.Module, ParallelTiledVAE):
|
||||
z = posterior.mode()
|
||||
dec = self.decode(z)
|
||||
return dec
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from functools import cache
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.platforms import current_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _probe_pin_memory() -> bool:
|
||||
if os.getenv("FASTVIDEO_DISABLE_PIN_MEMORY",
|
||||
"") not in ("", "0", "false", "False"):
|
||||
return False
|
||||
if current_platform.is_cpu() or current_platform.is_mps():
|
||||
return False
|
||||
|
||||
try:
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.current_device()
|
||||
_ = torch.empty(1024, device="cpu").pin_memory()
|
||||
_ = torch.empty(1024, device="cpu", pin_memory=True)
|
||||
except Exception as exc:
|
||||
logger.warning("Pinned memory is unavailable: %s", exc)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@cache
|
||||
def _cached_pin_memory_available(pid: int) -> bool:
|
||||
return _probe_pin_memory()
|
||||
|
||||
|
||||
def is_pin_memory_available() -> bool:
|
||||
return _cached_pin_memory_available(os.getpid())
|
||||
@@ -11,11 +11,12 @@ from fastvideo.logger import init_logger
|
||||
from fastvideo.models.schedulers.scheduling_flow_match_euler_discrete import (
|
||||
FlowMatchEulerDiscreteScheduler)
|
||||
from fastvideo.pipelines.composed_pipeline_base import ComposedPipelineBase
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, CosmosDenoisingStage,
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, DenoisingStage,
|
||||
CosmosLatentPreparationStage,
|
||||
DecodingStage, InputValidationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage)
|
||||
from fastvideo.pipelines.stages.denoising_cosmos_strategy import CosmosStrategy
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -73,9 +74,10 @@ class Cosmos2VideoToWorldPipeline(ComposedPipelineBase):
|
||||
vae=self.get_module("vae")))
|
||||
|
||||
self.add_stage(stage_name="denoising_stage",
|
||||
stage=CosmosDenoisingStage(
|
||||
stage=DenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler")))
|
||||
scheduler=self.get_module("scheduler"),
|
||||
strategy_cls=CosmosStrategy))
|
||||
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae")))
|
||||
|
||||
@@ -2,5 +2,10 @@
|
||||
"""LongCat pipeline module."""
|
||||
|
||||
from fastvideo.pipelines.basic.longcat.longcat_pipeline import LongCatPipeline
|
||||
from fastvideo.pipelines.basic.longcat.longcat_i2v_pipeline import LongCatImageToVideoPipeline
|
||||
from fastvideo.pipelines.basic.longcat.longcat_vc_pipeline import LongCatVideoContinuationPipeline
|
||||
|
||||
__all__ = ["LongCatPipeline"]
|
||||
__all__ = [
|
||||
"LongCatPipeline", "LongCatImageToVideoPipeline",
|
||||
"LongCatVideoContinuationPipeline"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
LongCat Image-to-Video pipeline implementation.
|
||||
|
||||
This module implements I2V (Image-to-Video) generation for LongCat using Tier 3
|
||||
conditioning with timestep masking, num_cond_latents support, and RoPE skipping.
|
||||
|
||||
Supports:
|
||||
- Basic I2V (50 steps, guidance_scale=4.0)
|
||||
- Distilled I2V with LoRA (16 steps, guidance_scale=1.0)
|
||||
- Refinement I2V for 720p upscaling (with refinement LoRA + BSA)
|
||||
"""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
from fastvideo.pipelines.stages import (
|
||||
DecodingStage,
|
||||
DenoisingStage,
|
||||
InputValidationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage,
|
||||
)
|
||||
from fastvideo.pipelines.stages.denoising_longcat_strategy import (
|
||||
LongCatI2VStrategy)
|
||||
from fastvideo.pipelines.stages.longcat_image_vae_encoding import LongCatImageVAEEncodingStage
|
||||
from fastvideo.pipelines.stages.longcat_i2v_latent_preparation import LongCatI2VLatentPreparationStage
|
||||
from fastvideo.pipelines.stages.longcat_refine_init import LongCatRefineInitStage
|
||||
from fastvideo.pipelines.stages.longcat_refine_timestep import LongCatRefineTimestepStage
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class LongCatImageToVideoPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
"""
|
||||
LongCat Image-to-Video pipeline.
|
||||
|
||||
Generates video from a single input image using Tier 3 I2V conditioning:
|
||||
- Per-frame timestep masking (timestep[:, 0] = 0)
|
||||
- num_cond_latents parameter to transformer
|
||||
- RoPE skipping for conditioning frames
|
||||
- Selective denoising (skip first frame in scheduler)
|
||||
"""
|
||||
|
||||
_required_config_modules = [
|
||||
"text_encoder", "tokenizer", "vae", "transformer", "scheduler"
|
||||
]
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
|
||||
"""Initialize LongCat-specific components."""
|
||||
# Same BSA initialization as base LongCat pipeline
|
||||
pipeline_config = fastvideo_args.pipeline_config
|
||||
transformer = self.get_module("transformer", None)
|
||||
if transformer is None:
|
||||
return
|
||||
|
||||
# Enable BSA if configured
|
||||
if pipeline_config.enable_bsa:
|
||||
bsa_params_cfg = getattr(pipeline_config, 'bsa_params', None) or {}
|
||||
sparsity = getattr(pipeline_config, 'bsa_sparsity', None)
|
||||
cdf_threshold = getattr(pipeline_config, 'bsa_cdf_threshold', None)
|
||||
chunk_q = getattr(pipeline_config, 'bsa_chunk_q', None)
|
||||
chunk_k = getattr(pipeline_config, 'bsa_chunk_k', None)
|
||||
|
||||
effective_bsa_params = dict(bsa_params_cfg) if isinstance(
|
||||
bsa_params_cfg, dict) else {}
|
||||
if sparsity is not None:
|
||||
effective_bsa_params['sparsity'] = sparsity
|
||||
if cdf_threshold is not None:
|
||||
effective_bsa_params['cdf_threshold'] = cdf_threshold
|
||||
if chunk_q is not None:
|
||||
effective_bsa_params['chunk_3d_shape_q'] = chunk_q
|
||||
if chunk_k is not None:
|
||||
effective_bsa_params['chunk_3d_shape_k'] = chunk_k
|
||||
|
||||
# Provide defaults
|
||||
effective_bsa_params.setdefault('sparsity', 0.9375)
|
||||
effective_bsa_params.setdefault('chunk_3d_shape_q', [4, 4, 4])
|
||||
effective_bsa_params.setdefault('chunk_3d_shape_k', [4, 4, 4])
|
||||
|
||||
if hasattr(transformer, 'enable_bsa'):
|
||||
logger.info("Enabling BSA for LongCat I2V transformer")
|
||||
transformer.enable_bsa()
|
||||
if hasattr(transformer, 'blocks'):
|
||||
try:
|
||||
for blk in transformer.blocks:
|
||||
if hasattr(blk, 'self_attn'):
|
||||
blk.self_attn.bsa_params = effective_bsa_params
|
||||
except Exception as e:
|
||||
logger.warning("Failed to set BSA params: %s", e)
|
||||
logger.info("BSA parameters: %s", effective_bsa_params)
|
||||
else:
|
||||
if hasattr(transformer, 'disable_bsa'):
|
||||
transformer.disable_bsa()
|
||||
|
||||
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs):
|
||||
"""Set up I2V-specific pipeline stages."""
|
||||
|
||||
# 1. Input validation
|
||||
self.add_stage(stage_name="input_validation_stage",
|
||||
stage=InputValidationStage())
|
||||
|
||||
# 2. Text encoding (same as T2V)
|
||||
self.add_stage(stage_name="prompt_encoding_stage",
|
||||
stage=TextEncodingStage(
|
||||
text_encoders=[self.get_module("text_encoder")],
|
||||
tokenizers=[self.get_module("tokenizer")],
|
||||
))
|
||||
|
||||
# 3. Image VAE encoding (for I2V - skipped in refinement mode)
|
||||
self.add_stage(
|
||||
stage_name="image_vae_encoding_stage",
|
||||
stage=LongCatImageVAEEncodingStage(vae=self.get_module("vae")))
|
||||
|
||||
# 4. Refinement initialization (skipped if not refining)
|
||||
self.add_stage(stage_name="longcat_refine_init_stage",
|
||||
stage=LongCatRefineInitStage(vae=self.get_module("vae")))
|
||||
|
||||
# 5. Timestep preparation (generic)
|
||||
self.add_stage(stage_name="timestep_preparation_stage",
|
||||
stage=TimestepPreparationStage(
|
||||
scheduler=self.get_module("scheduler")))
|
||||
|
||||
# 6. Refinement timestep override (skipped if not refining)
|
||||
self.add_stage(stage_name="longcat_refine_timestep_stage",
|
||||
stage=LongCatRefineTimestepStage(
|
||||
scheduler=self.get_module("scheduler")))
|
||||
|
||||
# 7. Latent preparation with I2V conditioning
|
||||
self.add_stage(stage_name="latent_preparation_stage",
|
||||
stage=LongCatI2VLatentPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
transformer=self.get_module("transformer")))
|
||||
|
||||
# 8. Denoising with I2V support
|
||||
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,
|
||||
strategy_cls=LongCatI2VStrategy))
|
||||
|
||||
# 9. Decoding
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae"),
|
||||
pipeline=self))
|
||||
|
||||
|
||||
EntryClass = LongCatImageToVideoPipeline
|
||||
@@ -1,9 +1,9 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
LongCat video diffusion pipeline implementation (Phase 1: Wrapper).
|
||||
LongCat video diffusion pipeline implementation.
|
||||
|
||||
This module contains a wrapper implementation of the LongCat video diffusion pipeline
|
||||
using FastVideo's modular pipeline architecture with the original LongCat modules.
|
||||
This module implements the LongCat video diffusion pipeline using FastVideo's
|
||||
modular pipeline architecture.
|
||||
"""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
@@ -11,12 +11,14 @@ from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
from fastvideo.pipelines.stages import (
|
||||
DecodingStage,
|
||||
DenoisingStage,
|
||||
InputValidationStage,
|
||||
LatentPreparationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage,
|
||||
)
|
||||
from fastvideo.pipelines.stages.longcat_denoising import LongCatDenoisingStage
|
||||
from fastvideo.pipelines.stages.denoising_longcat_strategy import (
|
||||
LongCatStrategy)
|
||||
from fastvideo.pipelines.stages.longcat_refine_init import LongCatRefineInitStage
|
||||
from fastvideo.pipelines.stages.longcat_refine_timestep import LongCatRefineTimestepStage
|
||||
|
||||
@@ -26,9 +28,6 @@ logger = init_logger(__name__)
|
||||
class LongCatPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
"""
|
||||
LongCat video diffusion pipeline with LoRA support.
|
||||
|
||||
Phase 1 implementation using wrapper modules from third_party/longcat_video.
|
||||
This validates the pipeline infrastructure before full FastVideo integration.
|
||||
"""
|
||||
|
||||
_required_config_modules = [
|
||||
@@ -130,12 +129,13 @@ class LongCatPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
transformer=self.get_module("transformer", None)))
|
||||
|
||||
self.add_stage(stage_name="denoising_stage",
|
||||
stage=LongCatDenoisingStage(
|
||||
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))
|
||||
pipeline=self,
|
||||
strategy_cls=LongCatStrategy))
|
||||
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae"),
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
LongCat Video Continuation (VC) pipeline implementation.
|
||||
|
||||
This module implements VC (Video Continuation) generation for LongCat with
|
||||
KV cache optimization for 2-3x speedup.
|
||||
|
||||
Supports:
|
||||
- Basic VC (50 steps, guidance_scale=4.0)
|
||||
- Distilled VC with LoRA (16 steps, guidance_scale=1.0)
|
||||
- KV cache for conditioning frames
|
||||
"""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
from fastvideo.pipelines.stages import (
|
||||
DecodingStage,
|
||||
DenoisingStage,
|
||||
InputValidationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage,
|
||||
)
|
||||
from fastvideo.pipelines.stages.denoising_longcat_strategy import (
|
||||
LongCatVCStrategy)
|
||||
from fastvideo.pipelines.stages.longcat_video_vae_encoding import LongCatVideoVAEEncodingStage
|
||||
from fastvideo.pipelines.stages.longcat_i2v_latent_preparation import LongCatI2VLatentPreparationStage
|
||||
from fastvideo.pipelines.stages.longcat_kv_cache_init import LongCatKVCacheInitStage
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class LongCatVideoContinuationPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
"""
|
||||
LongCat Video Continuation pipeline.
|
||||
|
||||
Generates video continuation from multiple conditioning frames using
|
||||
optional KV cache for 2-3x speedup.
|
||||
|
||||
Key features:
|
||||
- Takes video input (13+ frames typically)
|
||||
- Encodes conditioning frames via VAE
|
||||
- Optionally pre-computes KV cache for conditioning
|
||||
- Uses cached K/V during denoising for speedup
|
||||
- Concatenates conditioning back after denoising
|
||||
"""
|
||||
|
||||
_required_config_modules = [
|
||||
"text_encoder", "tokenizer", "vae", "transformer", "scheduler"
|
||||
]
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
|
||||
"""Initialize LongCat-specific components."""
|
||||
pipeline_config = fastvideo_args.pipeline_config
|
||||
transformer = self.get_module("transformer", None)
|
||||
if transformer is None:
|
||||
return
|
||||
|
||||
# Enable BSA if configured (for VC, BSA may not be needed)
|
||||
if getattr(pipeline_config, 'enable_bsa', False):
|
||||
bsa_params_cfg = getattr(pipeline_config, 'bsa_params', None) or {}
|
||||
sparsity = getattr(pipeline_config, 'bsa_sparsity', None)
|
||||
cdf_threshold = getattr(pipeline_config, 'bsa_cdf_threshold', None)
|
||||
chunk_q = getattr(pipeline_config, 'bsa_chunk_q', None)
|
||||
chunk_k = getattr(pipeline_config, 'bsa_chunk_k', None)
|
||||
|
||||
effective_bsa_params = dict(bsa_params_cfg) if isinstance(
|
||||
bsa_params_cfg, dict) else {}
|
||||
if sparsity is not None:
|
||||
effective_bsa_params['sparsity'] = sparsity
|
||||
if cdf_threshold is not None:
|
||||
effective_bsa_params['cdf_threshold'] = cdf_threshold
|
||||
if chunk_q is not None:
|
||||
effective_bsa_params['chunk_3d_shape_q'] = chunk_q
|
||||
if chunk_k is not None:
|
||||
effective_bsa_params['chunk_3d_shape_k'] = chunk_k
|
||||
|
||||
# Provide defaults
|
||||
effective_bsa_params.setdefault('sparsity', 0.9375)
|
||||
effective_bsa_params.setdefault('chunk_3d_shape_q', [4, 4, 4])
|
||||
effective_bsa_params.setdefault('chunk_3d_shape_k', [4, 4, 4])
|
||||
|
||||
if hasattr(transformer, 'enable_bsa'):
|
||||
logger.info("Enabling BSA for LongCat VC transformer")
|
||||
transformer.enable_bsa()
|
||||
if hasattr(transformer, 'blocks'):
|
||||
try:
|
||||
for blk in transformer.blocks:
|
||||
if hasattr(blk, 'self_attn'):
|
||||
blk.self_attn.bsa_params = effective_bsa_params
|
||||
except Exception as e:
|
||||
logger.warning("Failed to set BSA params: %s", e)
|
||||
logger.info("BSA parameters: %s", effective_bsa_params)
|
||||
else:
|
||||
if hasattr(transformer, 'disable_bsa'):
|
||||
transformer.disable_bsa()
|
||||
|
||||
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs):
|
||||
"""Set up VC-specific pipeline stages."""
|
||||
|
||||
# 1. Input validation
|
||||
self.add_stage(stage_name="input_validation_stage",
|
||||
stage=InputValidationStage())
|
||||
|
||||
# 2. Text encoding
|
||||
self.add_stage(stage_name="prompt_encoding_stage",
|
||||
stage=TextEncodingStage(
|
||||
text_encoders=[self.get_module("text_encoder")],
|
||||
tokenizers=[self.get_module("tokenizer")],
|
||||
))
|
||||
|
||||
# 3. Video VAE encoding (encodes conditioning frames)
|
||||
self.add_stage(
|
||||
stage_name="video_vae_encoding_stage",
|
||||
stage=LongCatVideoVAEEncodingStage(vae=self.get_module("vae")))
|
||||
|
||||
# 4. Timestep preparation
|
||||
self.add_stage(stage_name="timestep_preparation_stage",
|
||||
stage=TimestepPreparationStage(
|
||||
scheduler=self.get_module("scheduler")))
|
||||
|
||||
# 5. Latent preparation (reuse I2V stage - it handles video_latent too)
|
||||
self.add_stage(stage_name="latent_preparation_stage",
|
||||
stage=LongCatVCLatentPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
transformer=self.get_module("transformer")))
|
||||
|
||||
# 6. KV cache initialization (optional, based on config)
|
||||
# This is always added but will skip if use_kv_cache=False
|
||||
self.add_stage(stage_name="kv_cache_init_stage",
|
||||
stage=LongCatKVCacheInitStage(
|
||||
transformer=self.get_module("transformer")))
|
||||
|
||||
# 7. Denoising with VC and KV cache support
|
||||
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,
|
||||
strategy_cls=LongCatVCStrategy))
|
||||
|
||||
# 8. Decoding
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae"),
|
||||
pipeline=self))
|
||||
|
||||
|
||||
class LongCatVCLatentPreparationStage(LongCatI2VLatentPreparationStage):
|
||||
"""
|
||||
Prepare latents with video conditioning for first N frames.
|
||||
|
||||
Extends I2V latent preparation to handle video_latent (multiple frames)
|
||||
instead of image_latent (single frame).
|
||||
"""
|
||||
|
||||
def forward(self, batch, fastvideo_args):
|
||||
"""Prepare latents with VC conditioning."""
|
||||
|
||||
# Check if we have video_latent (from VC encoding stage)
|
||||
video_latent = getattr(batch, 'video_latent', None)
|
||||
if video_latent is not None:
|
||||
# Set image_latent to video_latent for parent class compatibility
|
||||
batch.image_latent = video_latent
|
||||
|
||||
# Call parent class forward
|
||||
return super().forward(batch, fastvideo_args)
|
||||
|
||||
|
||||
EntryClass = LongCatVideoContinuationPipeline
|
||||
@@ -2,8 +2,9 @@
|
||||
"""Matrix-Game causal DMD pipeline implementation."""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
import torch
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
from fastvideo.pipelines import ComposedPipelineBase, ForwardBatch, LoRAPipeline
|
||||
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, DecodingStage,
|
||||
InputValidationStage,
|
||||
@@ -69,5 +70,61 @@ class MatrixGameCausalDMDPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
logger.info(
|
||||
"MatrixGameCausalDMDPipeline initialized with action support")
|
||||
|
||||
@torch.no_grad()
|
||||
def streaming_reset(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs):
|
||||
if not self.post_init_called:
|
||||
self.post_init()
|
||||
|
||||
# 1. Run Pre-processing stages
|
||||
stages_to_run = [
|
||||
"input_validation_stage", "prompt_encoding_stage",
|
||||
"image_encoding_stage", "conditioning_stage",
|
||||
"latent_preparation_stage", "image_latent_preparation_stage"
|
||||
]
|
||||
|
||||
for stage_name in stages_to_run:
|
||||
if stage_name in self._stage_name_mapping:
|
||||
batch = self._stage_name_mapping[stage_name].forward(
|
||||
batch, fastvideo_args)
|
||||
|
||||
# 2. Reset Denoising Stage
|
||||
denoiser = self._stage_name_mapping["denoising_stage"]
|
||||
denoiser.streaming_reset(batch, fastvideo_args)
|
||||
|
||||
# 3. Initialize VAE cache
|
||||
self._vae_cache = None
|
||||
|
||||
def streaming_step(self, keyboard_action, mouse_action) -> ForwardBatch:
|
||||
denoiser = self._stage_name_mapping["denoising_stage"]
|
||||
ctx = denoiser._streaming_ctx
|
||||
assert ctx is not None, "streaming_ctx must be set"
|
||||
|
||||
start_idx = ctx.start_index
|
||||
batch = denoiser.streaming_step(keyboard_action, mouse_action)
|
||||
end_idx = ctx.start_index
|
||||
|
||||
# Decode only the new generated block
|
||||
if end_idx > start_idx:
|
||||
current_latents = batch.latents[:, :, start_idx:end_idx, :, :]
|
||||
args = ctx.fastvideo_args
|
||||
decoder = self._stage_name_mapping["decoding_stage"]
|
||||
decoded_frames, self._vae_cache = decoder.streaming_decode(
|
||||
current_latents,
|
||||
args,
|
||||
cache=self._vae_cache,
|
||||
is_first_chunk=(start_idx == 0))
|
||||
batch.output = decoded_frames
|
||||
else:
|
||||
batch.output = None
|
||||
|
||||
return batch
|
||||
|
||||
def streaming_clear(self) -> None:
|
||||
denoiser = self._stage_name_mapping.get("denoising_stage")
|
||||
if denoiser is not None and hasattr(denoiser, "streaming_clear"):
|
||||
denoiser.streaming_clear()
|
||||
self._vae_cache = None
|
||||
|
||||
|
||||
EntryClass = [MatrixGameCausalDMDPipeline]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from fastvideo.pipelines.basic.turbodiffusion.turbodiffusion_pipeline import (
|
||||
TurboDiffusionPipeline, )
|
||||
from fastvideo.pipelines.basic.turbodiffusion.turbodiffusion_i2v_pipeline import (
|
||||
TurboDiffusionI2VPipeline, )
|
||||
|
||||
__all__ = ["TurboDiffusionPipeline", "TurboDiffusionI2VPipeline"]
|
||||
@@ -0,0 +1,89 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
TurboDiffusion I2V (Image-to-Video) Pipeline Implementation.
|
||||
|
||||
This module contains an implementation of the TurboDiffusion I2V pipeline
|
||||
for 1-4 step image-to-video generation using rCM (recurrent Consistency Model)
|
||||
sampling with SLA (Sparse-Linear Attention).
|
||||
|
||||
Key differences from T2V:
|
||||
- Uses dual models (high/low noise) with boundary switching
|
||||
- sigma_max=200 (vs 80 for T2V)
|
||||
- Mask conditioning with encoded first frame
|
||||
"""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.schedulers.scheduling_rcm import RCMScheduler
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, DecodingStage,
|
||||
DenoisingStage, ImageVAEEncodingStage,
|
||||
InputValidationStage,
|
||||
LatentPreparationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class TurboDiffusionI2VPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
"""
|
||||
TurboDiffusion I2V pipeline for 1-4 step image-to-video generation.
|
||||
|
||||
Uses RCM scheduler, SLA attention, and dual model switching for
|
||||
high-quality I2V generation.
|
||||
"""
|
||||
|
||||
_required_config_modules = [
|
||||
"text_encoder", "tokenizer", "vae", "transformer", "transformer_2",
|
||||
"scheduler"
|
||||
]
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
|
||||
# Use RCM scheduler with higher sigma_max for I2V
|
||||
logger.info(
|
||||
"Initializing RCM scheduler for TurboDiffusion I2V (sigma_max=200)")
|
||||
self.modules["scheduler"] = RCMScheduler(sigma_max=200.0)
|
||||
|
||||
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs) -> None:
|
||||
"""Set up pipeline stages with proper dependency injection."""
|
||||
|
||||
self.add_stage(stage_name="input_validation_stage",
|
||||
stage=InputValidationStage())
|
||||
|
||||
self.add_stage(stage_name="prompt_encoding_stage",
|
||||
stage=TextEncodingStage(
|
||||
text_encoders=[self.get_module("text_encoder")],
|
||||
tokenizers=[self.get_module("tokenizer")],
|
||||
))
|
||||
|
||||
self.add_stage(stage_name="conditioning_stage",
|
||||
stage=ConditioningStage())
|
||||
|
||||
self.add_stage(stage_name="timestep_preparation_stage",
|
||||
stage=TimestepPreparationStage(
|
||||
scheduler=self.get_module("scheduler")))
|
||||
|
||||
self.add_stage(stage_name="latent_preparation_stage",
|
||||
stage=LatentPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
transformer=self.get_module("transformer", None)))
|
||||
|
||||
# I2V: Encode initial image to latent space
|
||||
self.add_stage(stage_name="image_latent_preparation_stage",
|
||||
stage=ImageVAEEncodingStage(vae=self.get_module("vae")))
|
||||
|
||||
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",
|
||||
stage=DecodingStage(vae=self.get_module("vae"),
|
||||
pipeline=self))
|
||||
|
||||
|
||||
EntryClass = TurboDiffusionI2VPipeline
|
||||
@@ -0,0 +1,76 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
TurboDiffusion Video Pipeline Implementation.
|
||||
|
||||
This module contains an implementation of the TurboDiffusion video diffusion pipeline
|
||||
for 1-4 step video generation using rCM (recurrent Consistency Model) sampling
|
||||
with SLA (Sparse-Linear Attention).
|
||||
"""
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.schedulers.scheduling_rcm import RCMScheduler
|
||||
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, DecodingStage,
|
||||
DenoisingStage, InputValidationStage,
|
||||
LatentPreparationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class TurboDiffusionPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
"""
|
||||
TurboDiffusion video pipeline for 1-4 step generation.
|
||||
|
||||
Uses RCM scheduler and SLA attention for fast, high-quality video generation.
|
||||
"""
|
||||
|
||||
_required_config_modules = [
|
||||
"text_encoder", "tokenizer", "vae", "transformer", "scheduler"
|
||||
]
|
||||
|
||||
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
|
||||
# Use RCM scheduler for TurboDiffusion
|
||||
logger.info("Initializing RCM scheduler for TurboDiffusion")
|
||||
self.modules["scheduler"] = RCMScheduler(sigma_max=80.0)
|
||||
|
||||
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs) -> None:
|
||||
"""Set up pipeline stages with proper dependency injection."""
|
||||
|
||||
self.add_stage(stage_name="input_validation_stage",
|
||||
stage=InputValidationStage())
|
||||
|
||||
self.add_stage(stage_name="prompt_encoding_stage",
|
||||
stage=TextEncodingStage(
|
||||
text_encoders=[self.get_module("text_encoder")],
|
||||
tokenizers=[self.get_module("tokenizer")],
|
||||
))
|
||||
|
||||
self.add_stage(stage_name="conditioning_stage",
|
||||
stage=ConditioningStage())
|
||||
|
||||
self.add_stage(stage_name="timestep_preparation_stage",
|
||||
stage=TimestepPreparationStage(
|
||||
scheduler=self.get_module("scheduler")))
|
||||
|
||||
self.add_stage(stage_name="latent_preparation_stage",
|
||||
stage=LatentPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
transformer=self.get_module("transformer", None)))
|
||||
|
||||
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",
|
||||
stage=DecodingStage(vae=self.get_module("vae"),
|
||||
pipeline=self))
|
||||
|
||||
|
||||
EntryClass = TurboDiffusionPipeline
|
||||
@@ -11,10 +11,11 @@ from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
|
||||
# isort: off
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, DecodingStage,
|
||||
CausalDMDDenosingStage,
|
||||
InputValidationStage,
|
||||
DenoisingStage, InputValidationStage,
|
||||
LatentPreparationStage,
|
||||
TextEncodingStage)
|
||||
from fastvideo.pipelines.stages.denoising_causal_strategy import (
|
||||
CausalBlockStrategy)
|
||||
# isort: on
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -47,11 +48,12 @@ class WanCausalDMDPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
transformer=self.get_module("transformer", None)))
|
||||
|
||||
self.add_stage(stage_name="denoising_stage",
|
||||
stage=CausalDMDDenosingStage(
|
||||
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")))
|
||||
vae=self.get_module("vae"),
|
||||
strategy_cls=CausalBlockStrategy))
|
||||
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae")))
|
||||
|
||||
@@ -14,10 +14,11 @@ from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
|
||||
|
||||
# isort: off
|
||||
from fastvideo.pipelines.stages import (ConditioningStage, DecodingStage,
|
||||
DmdDenoisingStage, InputValidationStage,
|
||||
DenoisingStage, InputValidationStage,
|
||||
LatentPreparationStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage)
|
||||
from fastvideo.pipelines.stages.denoising_dmd_strategy import DmdStrategy
|
||||
# isort: on
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -63,9 +64,10 @@ class WanDMDPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
use_btchw_layout=True))
|
||||
|
||||
self.add_stage(stage_name="denoising_stage",
|
||||
stage=DmdDenoisingStage(
|
||||
stage=DenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler")))
|
||||
scheduler=FlowMatchEulerDiscreteScheduler(shift=8.0),
|
||||
strategy_cls=DmdStrategy))
|
||||
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae")))
|
||||
|
||||
@@ -13,12 +13,13 @@ from fastvideo.pipelines.lora_pipeline import LoRAPipeline
|
||||
|
||||
# isort: off
|
||||
from fastvideo.pipelines.stages import (
|
||||
ImageEncodingStage, ConditioningStage, DecodingStage, DmdDenoisingStage,
|
||||
ImageEncodingStage, ConditioningStage, DecodingStage, DenoisingStage,
|
||||
ImageVAEEncodingStage, InputValidationStage, LatentPreparationStage,
|
||||
TextEncodingStage, TimestepPreparationStage)
|
||||
# isort: on
|
||||
from fastvideo.models.schedulers.scheduling_flow_match_euler_discrete import (
|
||||
FlowMatchEulerDiscreteScheduler)
|
||||
from fastvideo.pipelines.stages.denoising_dmd_strategy import DmdStrategy
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -69,9 +70,10 @@ class WanImageToVideoDmdPipeline(LoRAPipeline, ComposedPipelineBase):
|
||||
stage=ImageVAEEncodingStage(vae=self.get_module("vae")))
|
||||
|
||||
self.add_stage(stage_name="denoising_stage",
|
||||
stage=DmdDenoisingStage(
|
||||
stage=DenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler")))
|
||||
scheduler=FlowMatchEulerDiscreteScheduler(shift=8.0),
|
||||
strategy_cls=DmdStrategy))
|
||||
|
||||
self.add_stage(stage_name="decoding_stage",
|
||||
stage=DecodingStage(vae=self.get_module("vae")))
|
||||
|
||||
@@ -23,6 +23,8 @@ _PIPELINE_NAME_TO_ARCHITECTURE_NAME: dict[str, str] = {
|
||||
"WanImageToVideoPipeline": "wan",
|
||||
"WanVideoToVideoPipeline": "wan",
|
||||
"WanCausalDMDPipeline": "wan",
|
||||
"TurboDiffusionPipeline": "turbodiffusion",
|
||||
"TurboDiffusionI2VPipeline": "turbodiffusion",
|
||||
"StepVideoPipeline": "stepvideo",
|
||||
"HunyuanVideoPipeline": "hunyuan",
|
||||
"HunyuanVideo15Pipeline": "hunyuan15",
|
||||
@@ -30,6 +32,8 @@ _PIPELINE_NAME_TO_ARCHITECTURE_NAME: dict[str, str] = {
|
||||
"MatrixGamePipeline": "matrixgame",
|
||||
"MatrixGameCausalDMDPipeline": "matrixgame",
|
||||
"LongCatPipeline": "longcat",
|
||||
"LongCatImageToVideoPipeline": "longcat",
|
||||
"LongCatVideoContinuationPipeline": "longcat",
|
||||
}
|
||||
|
||||
_PREPROCESS_WORKLOAD_TYPE_TO_PIPELINE_NAME: dict[WorkloadType, str] = {
|
||||
|
||||
@@ -7,12 +7,9 @@ complete diffusion pipelines.
|
||||
"""
|
||||
|
||||
from fastvideo.pipelines.stages.base import PipelineStage
|
||||
from fastvideo.pipelines.stages.causal_denoising import CausalDMDDenosingStage
|
||||
from fastvideo.pipelines.stages.conditioning import ConditioningStage
|
||||
from fastvideo.pipelines.stages.decoding import DecodingStage
|
||||
from fastvideo.pipelines.stages.denoising import (CosmosDenoisingStage,
|
||||
DenoisingStage,
|
||||
DmdDenoisingStage)
|
||||
from fastvideo.pipelines.stages.denoising import DenoisingStage
|
||||
from fastvideo.pipelines.stages.encoding import EncodingStage
|
||||
from fastvideo.pipelines.stages.image_encoding import (
|
||||
ImageEncodingStage, MatrixGameImageEncodingStage, RefImageEncodingStage,
|
||||
@@ -28,6 +25,10 @@ from fastvideo.pipelines.stages.text_encoding import TextEncodingStage
|
||||
from fastvideo.pipelines.stages.timestep_preparation import (
|
||||
TimestepPreparationStage)
|
||||
|
||||
# LongCat stages
|
||||
from fastvideo.pipelines.stages.longcat_video_vae_encoding import LongCatVideoVAEEncodingStage
|
||||
from fastvideo.pipelines.stages.longcat_kv_cache_init import LongCatKVCacheInitStage
|
||||
|
||||
__all__ = [
|
||||
"PipelineStage",
|
||||
"InputValidationStage",
|
||||
@@ -36,10 +37,7 @@ __all__ = [
|
||||
"CosmosLatentPreparationStage",
|
||||
"ConditioningStage",
|
||||
"DenoisingStage",
|
||||
"DmdDenoisingStage",
|
||||
"CausalDMDDenosingStage",
|
||||
"MatrixGameCausalDenoisingStage",
|
||||
"CosmosDenoisingStage",
|
||||
"EncodingStage",
|
||||
"DecodingStage",
|
||||
"ImageEncodingStage",
|
||||
@@ -50,4 +48,7 @@ __all__ = [
|
||||
"VideoVAEEncodingStage",
|
||||
"TextEncodingStage",
|
||||
"StepvideoPromptEncodingStage",
|
||||
# LongCat stages
|
||||
"LongCatVideoVAEEncodingStage",
|
||||
"LongCatKVCacheInitStage",
|
||||
]
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
import torch # type: ignore
|
||||
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.utils import pred_noise_to_pred_video, pred_noise_to_x_bound
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising import DenoisingStage
|
||||
from fastvideo.pipelines.stages.validators import StageValidators as V
|
||||
from fastvideo.pipelines.stages.validators import VerificationResult
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.sliding_tile_attn import (
|
||||
SlidingTileAttentionBackend)
|
||||
st_attn_available = True
|
||||
except ImportError:
|
||||
st_attn_available = False
|
||||
SlidingTileAttentionBackend = None # type: ignore
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.video_sparse_attn import (
|
||||
VideoSparseAttentionBackend)
|
||||
vsa_available = True
|
||||
except ImportError:
|
||||
vsa_available = False
|
||||
VideoSparseAttentionBackend = None # type: ignore
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class CausalDMDDenosingStage(DenoisingStage):
|
||||
"""
|
||||
Denoising stage for causal diffusion.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
transformer,
|
||||
scheduler,
|
||||
transformer_2=None,
|
||||
vae=None) -> None:
|
||||
super().__init__(transformer, scheduler, transformer_2)
|
||||
# KV and cross-attention cache state (initialized on first forward)
|
||||
self.transformer = transformer
|
||||
self.transformer_2 = transformer_2
|
||||
self.vae = vae
|
||||
# Model-dependent constants (aligned with causal_inference.py assumptions)
|
||||
self.num_transformer_blocks = len(self.transformer.blocks)
|
||||
self.num_frames_per_block = self.transformer.config.arch_config.num_frames_per_block
|
||||
self.sliding_window_num_frames = self.transformer.config.arch_config.sliding_window_num_frames
|
||||
|
||||
try:
|
||||
self.local_attn_size = getattr(self.transformer.model,
|
||||
"local_attn_size",
|
||||
-1) # type: ignore
|
||||
except Exception:
|
||||
self.local_attn_size = -1
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
) -> ForwardBatch:
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
latent_seq_length = batch.latents.shape[-1] * batch.latents.shape[-2]
|
||||
patch_ratio = self.transformer.config.arch_config.patch_size[
|
||||
-1] * self.transformer.config.arch_config.patch_size[-2]
|
||||
self.frame_seq_length = latent_seq_length // patch_ratio
|
||||
# TODO(will): make this a parameter once we add i2v support
|
||||
independent_first_frame = self.transformer.independent_first_frame if hasattr(
|
||||
self.transformer, 'independent_first_frame') else False
|
||||
# Timesteps for DMD
|
||||
timesteps = torch.tensor(
|
||||
fastvideo_args.pipeline_config.dmd_denoising_steps,
|
||||
dtype=torch.long).cpu()
|
||||
if fastvideo_args.pipeline_config.warp_denoising_step:
|
||||
scheduler_timesteps = torch.cat((self.scheduler.timesteps.cpu(),
|
||||
torch.tensor([0],
|
||||
dtype=torch.float32)))
|
||||
timesteps = scheduler_timesteps[1000 - timesteps]
|
||||
timesteps = timesteps.to(get_local_torch_device())
|
||||
|
||||
if fastvideo_args.pipeline_config.dit_config.boundary_ratio is not None:
|
||||
boundary_timestep = fastvideo_args.pipeline_config.dit_config.boundary_ratio * self.scheduler.num_train_timesteps
|
||||
high_noise_timesteps = timesteps[timesteps >= boundary_timestep]
|
||||
else:
|
||||
boundary_timestep = None
|
||||
high_noise_timesteps = None
|
||||
|
||||
# Image kwargs (kept empty unless caller provides compatible args)
|
||||
image_kwargs: dict = {}
|
||||
|
||||
pos_cond_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
# "encoder_hidden_states_2": batch.clip_embedding_pos,
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
# STA
|
||||
if st_attn_available and self.attn_backend == SlidingTileAttentionBackend:
|
||||
self.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
# Latents and prompts
|
||||
assert batch.latents is not None, "latents must be provided"
|
||||
latents = batch.latents # [B, C, T, H, W]
|
||||
b, c, t, h, w = latents.shape
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert torch.isnan(prompt_embeds[0]).sum() == 0
|
||||
|
||||
# Initialize or reset caches
|
||||
kv_cache1 = self._initialize_kv_cache(batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device)
|
||||
kv_cache2 = None
|
||||
if boundary_timestep is not None:
|
||||
# Initialize the low noise kv cache
|
||||
kv_cache2 = self._initialize_kv_cache(batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device)
|
||||
|
||||
def _get_kv_cache(timestep: float) -> list[dict]:
|
||||
if boundary_timestep is not None:
|
||||
if timestep >= boundary_timestep:
|
||||
return kv_cache1
|
||||
else:
|
||||
assert kv_cache2 is not None, "kv_cache2 is not initialized"
|
||||
return kv_cache2
|
||||
return kv_cache1
|
||||
|
||||
crossattn_cache = self._initialize_crossattn_cache(
|
||||
batch_size=latents.shape[0],
|
||||
max_text_len=fastvideo_args.pipeline_config.text_encoder_configs[0].
|
||||
arch_config.text_len,
|
||||
dtype=target_dtype,
|
||||
device=latents.device)
|
||||
|
||||
pos_start_base = 0
|
||||
|
||||
# Determine block sizes
|
||||
if t % self.num_frames_per_block != 0:
|
||||
raise ValueError(
|
||||
"num_frames must be divisible by num_frames_per_block for causal DMD denoising"
|
||||
)
|
||||
num_blocks = t // self.num_frames_per_block
|
||||
block_sizes = [self.num_frames_per_block] * num_blocks
|
||||
start_index = 0
|
||||
|
||||
# For now hardcode the first block to be 1 frame assuming the model is Wan2.2-MoE
|
||||
if boundary_timestep is not None:
|
||||
block_sizes[0] = 1
|
||||
|
||||
first_frame_latent = None
|
||||
if batch.pil_image is not None:
|
||||
# Causal video gen directly replaces the first frame of the latent with
|
||||
# the image latent instead of appending along the channel dim
|
||||
assert self.vae is not None, "VAE is not provided for causal video gen task"
|
||||
self.vae = self.vae.to(get_local_torch_device())
|
||||
first_frame_latent = self.vae.encode(batch.pil_image).mean.float()
|
||||
if (hasattr(self.vae, "shift_factor")
|
||||
and self.vae.shift_factor is not None):
|
||||
if isinstance(self.vae.shift_factor, torch.Tensor):
|
||||
first_frame_latent -= self.vae.shift_factor.to(
|
||||
first_frame_latent.device, first_frame_latent.dtype)
|
||||
else:
|
||||
first_frame_latent -= self.vae.shift_factor
|
||||
|
||||
if isinstance(self.vae.scaling_factor, torch.Tensor):
|
||||
first_frame_latent = first_frame_latent * self.vae.scaling_factor.to(
|
||||
first_frame_latent.device, first_frame_latent.dtype)
|
||||
else:
|
||||
first_frame_latent = first_frame_latent * self.vae.scaling_factor
|
||||
|
||||
if fastvideo_args.vae_cpu_offload:
|
||||
self.vae = self.vae.to("cpu")
|
||||
|
||||
# Fill the low noise and high noise kv cache with first_frame_latent and timestep 0
|
||||
t_zero = torch.zeros([latents.shape[0], 1],
|
||||
device=latents.device,
|
||||
dtype=torch.long)
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled), \
|
||||
set_forward_context(current_timestep=0,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch):
|
||||
self.transformer(
|
||||
first_frame_latent.to(target_dtype),
|
||||
prompt_embeds,
|
||||
t_zero,
|
||||
kv_cache=kv_cache1,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
if boundary_timestep is not None:
|
||||
self.transformer_2(
|
||||
first_frame_latent.to(target_dtype),
|
||||
prompt_embeds,
|
||||
t_zero,
|
||||
kv_cache=kv_cache2,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
start_index += 1
|
||||
block_sizes.pop(0)
|
||||
latents[:, :, :1, :, :] = first_frame_latent
|
||||
|
||||
# DMD loop in causal blocks
|
||||
with self.progress_bar(total=len(block_sizes) *
|
||||
len(timesteps)) as progress_bar:
|
||||
for current_num_frames in block_sizes:
|
||||
current_latents = latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :]
|
||||
# use BTCHW for DMD conversion routines
|
||||
noise_latents_btchw = current_latents.permute(0, 2, 1, 3, 4)
|
||||
video_raw_latent_shape = noise_latents_btchw.shape
|
||||
|
||||
for i, t_cur in enumerate(timesteps):
|
||||
if boundary_timestep is not None and t_cur < boundary_timestep:
|
||||
current_model = self.transformer_2
|
||||
else:
|
||||
current_model = self.transformer
|
||||
# Copy for pred conversion
|
||||
noise_latents = noise_latents_btchw.clone()
|
||||
latent_model_input = current_latents.to(target_dtype)
|
||||
|
||||
if batch.image_latent is not None and independent_first_frame and start_index == 0:
|
||||
latent_model_input = torch.cat([
|
||||
latent_model_input,
|
||||
batch.image_latent.to(target_dtype)
|
||||
],
|
||||
dim=2)
|
||||
|
||||
# Prepare inputs
|
||||
t_expand = t_cur.repeat(latent_model_input.shape[0])
|
||||
|
||||
# Attention metadata if needed
|
||||
if (vsa_available and self.attn_backend
|
||||
== VideoSparseAttentionBackend):
|
||||
self.attn_metadata_builder_cls = self.attn_backend.get_builder_cls(
|
||||
)
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = self.attn_metadata_builder_cls(
|
||||
)
|
||||
attn_metadata = self.attn_metadata_builder.build( # type: ignore
|
||||
current_timestep=i, # type: ignore
|
||||
raw_latent_shape=(current_num_frames, h,
|
||||
w), # type: ignore
|
||||
patch_size=fastvideo_args.pipeline_config.
|
||||
dit_config.patch_size, # type: ignore
|
||||
STA_param=batch.STA_param, # type: ignore
|
||||
VSA_sparsity=fastvideo_args.
|
||||
VSA_sparsity, # type: ignore
|
||||
device=get_local_torch_device(), # type: ignore
|
||||
) # type: ignore
|
||||
assert attn_metadata is not None, "attn_metadata cannot be None"
|
||||
else:
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_metadata = None
|
||||
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled), \
|
||||
set_forward_context(current_timestep=i,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch):
|
||||
# Run transformer; follow DMD stage pattern
|
||||
t_expanded_noise = t_cur * torch.ones(
|
||||
(latent_model_input.shape[0], 1),
|
||||
device=latent_model_input.device,
|
||||
dtype=torch.long)
|
||||
pred_noise_btchw = current_model(
|
||||
latent_model_input,
|
||||
prompt_embeds,
|
||||
t_expanded_noise,
|
||||
kv_cache=_get_kv_cache(t_cur),
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
).permute(0, 2, 1, 3, 4)
|
||||
|
||||
# Convert pred noise to pred video with FM Euler scheduler utilities
|
||||
if boundary_timestep is not None and t_cur >= boundary_timestep:
|
||||
pred_video_btchw = pred_noise_to_x_bound(
|
||||
pred_noise=pred_noise_btchw.flatten(0, 1),
|
||||
noise_input_latent=noise_latents.flatten(0, 1),
|
||||
timestep=t_expand,
|
||||
boundary_timestep=torch.ones_like(t_expand) *
|
||||
boundary_timestep,
|
||||
scheduler=self.scheduler).unflatten(
|
||||
0, pred_noise_btchw.shape[:2])
|
||||
else:
|
||||
pred_video_btchw = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise_btchw.flatten(0, 1),
|
||||
noise_input_latent=noise_latents.flatten(0, 1),
|
||||
timestep=t_expand,
|
||||
scheduler=self.scheduler).unflatten(
|
||||
0, pred_noise_btchw.shape[:2])
|
||||
|
||||
if i < len(timesteps) - 1:
|
||||
next_timestep = timesteps[i + 1] * torch.ones(
|
||||
[1],
|
||||
dtype=torch.long,
|
||||
device=pred_video_btchw.device)
|
||||
noise = torch.randn(
|
||||
video_raw_latent_shape,
|
||||
dtype=pred_video_btchw.dtype,
|
||||
generator=(batch.generator[0] if isinstance(
|
||||
batch.generator, list) else
|
||||
batch.generator)).to(self.device)
|
||||
noise_btchw = noise
|
||||
if boundary_timestep is not None and i < len(
|
||||
high_noise_timesteps) - 1:
|
||||
noise_latents_btchw = self.scheduler.add_noise_high(
|
||||
pred_video_btchw.flatten(0, 1),
|
||||
noise_btchw.flatten(0, 1), next_timestep,
|
||||
torch.ones_like(next_timestep) *
|
||||
boundary_timestep).unflatten(
|
||||
0, pred_video_btchw.shape[:2])
|
||||
elif boundary_timestep is not None and i == len(
|
||||
high_noise_timesteps) - 1:
|
||||
noise_latents_btchw = pred_video_btchw
|
||||
else:
|
||||
noise_latents_btchw = self.scheduler.add_noise(
|
||||
pred_video_btchw.flatten(0, 1),
|
||||
noise_btchw.flatten(0, 1),
|
||||
next_timestep).unflatten(
|
||||
0, pred_video_btchw.shape[:2])
|
||||
current_latents = noise_latents_btchw.permute(
|
||||
0, 2, 1, 3, 4)
|
||||
else:
|
||||
current_latents = pred_video_btchw.permute(
|
||||
0, 2, 1, 3, 4)
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
|
||||
# Write back and advance
|
||||
latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :] = current_latents
|
||||
|
||||
# Re-run with context timestep to update KV cache using clean context
|
||||
context_noise = getattr(fastvideo_args.pipeline_config,
|
||||
"context_noise", 0)
|
||||
t_context = torch.ones([latents.shape[0]],
|
||||
device=latents.device,
|
||||
dtype=torch.long) * int(context_noise)
|
||||
context_bcthw = current_latents.to(target_dtype)
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled), \
|
||||
set_forward_context(current_timestep=0,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch):
|
||||
t_expanded_context = t_context.unsqueeze(1)
|
||||
|
||||
if boundary_timestep is not None:
|
||||
self.transformer_2(
|
||||
context_bcthw,
|
||||
prompt_embeds,
|
||||
t_expanded_context,
|
||||
kv_cache=kv_cache2,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
self.transformer(
|
||||
context_bcthw,
|
||||
prompt_embeds,
|
||||
t_expanded_context,
|
||||
kv_cache=kv_cache1,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
start_index += current_num_frames
|
||||
|
||||
if boundary_timestep is not None:
|
||||
num_frames_to_remove = self.num_frames_per_block - 1
|
||||
latents = latents[:, :, :-num_frames_to_remove, :, :]
|
||||
|
||||
batch.latents = latents
|
||||
return batch
|
||||
|
||||
def _initialize_kv_cache(self, batch_size, dtype, device) -> list[dict]:
|
||||
"""
|
||||
Initialize a Per-GPU KV cache aligned with the Wan model assumptions.
|
||||
"""
|
||||
kv_cache1 = []
|
||||
num_attention_heads = self.transformer.num_attention_heads
|
||||
attention_head_dim = self.transformer.attention_head_dim
|
||||
if self.local_attn_size != -1:
|
||||
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
||||
else:
|
||||
kv_cache_size = self.frame_seq_length * self.sliding_window_num_frames
|
||||
|
||||
for _ in range(self.num_transformer_blocks):
|
||||
kv_cache1.append({
|
||||
"k":
|
||||
torch.zeros([
|
||||
batch_size, kv_cache_size, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"v":
|
||||
torch.zeros([
|
||||
batch_size, kv_cache_size, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"global_end_index":
|
||||
torch.tensor([0], dtype=torch.long, device=device),
|
||||
"local_end_index":
|
||||
torch.tensor([0], dtype=torch.long, device=device),
|
||||
})
|
||||
|
||||
return kv_cache1
|
||||
|
||||
def _initialize_crossattn_cache(self, batch_size, max_text_len, dtype,
|
||||
device) -> list[dict]:
|
||||
"""
|
||||
Initialize a Per-GPU cross-attention cache aligned with the Wan model assumptions.
|
||||
"""
|
||||
crossattn_cache = []
|
||||
num_attention_heads = self.transformer.num_attention_heads
|
||||
attention_head_dim = self.transformer.attention_head_dim
|
||||
for _ in range(self.num_transformer_blocks):
|
||||
crossattn_cache.append({
|
||||
"k":
|
||||
torch.zeros([
|
||||
batch_size, max_text_len, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"v":
|
||||
torch.zeros([
|
||||
batch_size, max_text_len, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"is_init":
|
||||
False,
|
||||
})
|
||||
return crossattn_cache
|
||||
|
||||
def verify_input(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> VerificationResult:
|
||||
"""Verify denoising stage inputs."""
|
||||
result = VerificationResult()
|
||||
result.add_check("latents", batch.latents,
|
||||
[V.is_tensor, V.with_dims(5)])
|
||||
result.add_check("prompt_embeds", batch.prompt_embeds, V.list_not_empty)
|
||||
result.add_check("image_embeds", batch.image_embeds, V.is_list)
|
||||
result.add_check("image_latent", batch.image_latent,
|
||||
V.none_or_tensor_with_dims(5))
|
||||
result.add_check("num_inference_steps", batch.num_inference_steps,
|
||||
V.positive_int)
|
||||
result.add_check("guidance_scale", batch.guidance_scale,
|
||||
V.positive_float)
|
||||
result.add_check("eta", batch.eta, V.non_negative_float)
|
||||
result.add_check("generator", batch.generator,
|
||||
V.generator_or_list_generators)
|
||||
result.add_check("do_classifier_free_guidance",
|
||||
batch.do_classifier_free_guidance, V.bool_value)
|
||||
result.add_check(
|
||||
"negative_prompt_embeds", batch.negative_prompt_embeds, lambda x:
|
||||
not batch.do_classifier_free_guidance or V.list_not_empty(x))
|
||||
return result
|
||||
@@ -50,32 +50,7 @@ class DecodingStage(PipelineStage):
|
||||
result.add_check("output", batch.output, [V.is_tensor, V.with_dims(5)])
|
||||
return result
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor,
|
||||
fastvideo_args: FastVideoArgs) -> torch.Tensor:
|
||||
"""
|
||||
Decode latent representations into pixel space using VAE.
|
||||
|
||||
Args:
|
||||
latents: Input latent tensor with shape (batch, channels, frames, height_latents, width_latents)
|
||||
fastvideo_args: Configuration containing:
|
||||
- disable_autocast: Whether to disable automatic mixed precision (default: False)
|
||||
- pipeline_config.vae_precision: VAE computation precision ("fp32", "fp16", "bf16")
|
||||
- pipeline_config.vae_tiling: Whether to enable VAE tiling for memory efficiency
|
||||
|
||||
Returns:
|
||||
Decoded video tensor with shape (batch, channels, frames, height, width),
|
||||
normalized to [0, 1] range and moved to CPU as float32
|
||||
"""
|
||||
self.vae = self.vae.to(get_local_torch_device())
|
||||
latents = latents.to(get_local_torch_device())
|
||||
|
||||
# Setup VAE precision
|
||||
vae_dtype = PRECISION_TO_TYPE[
|
||||
fastvideo_args.pipeline_config.vae_precision]
|
||||
vae_autocast_enabled = (
|
||||
vae_dtype != torch.float32) and not fastvideo_args.disable_autocast
|
||||
|
||||
def _denormalize_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
||||
# denormalization for MatrixGame VAE
|
||||
# z = z * std + mean during decode
|
||||
if (hasattr(self.vae.config, 'latents_mean')
|
||||
@@ -109,6 +84,35 @@ class DecodingStage(PipelineStage):
|
||||
latents.dtype)
|
||||
else:
|
||||
latents += self.vae.shift_factor
|
||||
return latents
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(self, latents: torch.Tensor,
|
||||
fastvideo_args: FastVideoArgs) -> torch.Tensor:
|
||||
"""
|
||||
Decode latent representations into pixel space using VAE.
|
||||
|
||||
Args:
|
||||
latents: Input latent tensor with shape (batch, channels, frames, height_latents, width_latents)
|
||||
fastvideo_args: Configuration containing:
|
||||
- disable_autocast: Whether to disable automatic mixed precision (default: False)
|
||||
- pipeline_config.vae_precision: VAE computation precision ("fp32", "fp16", "bf16")
|
||||
- pipeline_config.vae_tiling: Whether to enable VAE tiling for memory efficiency
|
||||
|
||||
Returns:
|
||||
Decoded video tensor with shape (batch, channels, frames, height, width),
|
||||
normalized to [0, 1] range and moved to CPU as float32
|
||||
"""
|
||||
self.vae = self.vae.to(get_local_torch_device())
|
||||
latents = latents.to(get_local_torch_device())
|
||||
|
||||
# Setup VAE precision
|
||||
vae_dtype = PRECISION_TO_TYPE[
|
||||
fastvideo_args.pipeline_config.vae_precision]
|
||||
vae_autocast_enabled = (
|
||||
vae_dtype != torch.float32) and not fastvideo_args.disable_autocast
|
||||
|
||||
latents = self._denormalize_latents(latents)
|
||||
|
||||
# Decode latents
|
||||
with torch.autocast(device_type="cuda",
|
||||
@@ -126,6 +130,57 @@ class DecodingStage(PipelineStage):
|
||||
image = (image / 2 + 0.5).clamp(0, 1)
|
||||
return image
|
||||
|
||||
@torch.no_grad()
|
||||
def streaming_decode(
|
||||
self,
|
||||
latents: torch.Tensor,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
cache: list[torch.Tensor | None] | None = None,
|
||||
is_first_chunk: bool = False,
|
||||
) -> tuple[torch.Tensor, list[torch.Tensor | None]]:
|
||||
"""
|
||||
Decode latent representations into pixel space using VAE with streaming cache.
|
||||
|
||||
Args:
|
||||
latents: Input latent tensor with shape (batch, channels, frames, height_latents, width_latents)
|
||||
fastvideo_args: Configuration object.
|
||||
cache: VAE cache from previous call, or None to initialize a new cache.
|
||||
is_first_chunk: Whether this is the first chunk.
|
||||
|
||||
Returns:
|
||||
A tuple of (decoded_frames, updated_cache).
|
||||
"""
|
||||
self.vae = self.vae.to(get_local_torch_device())
|
||||
latents = latents.to(get_local_torch_device())
|
||||
|
||||
# Setup VAE precision
|
||||
vae_dtype = PRECISION_TO_TYPE[
|
||||
fastvideo_args.pipeline_config.vae_precision]
|
||||
vae_autocast_enabled = (
|
||||
vae_dtype != torch.float32) and not fastvideo_args.disable_autocast
|
||||
|
||||
latents = self._denormalize_latents(latents)
|
||||
|
||||
# Initialize cache if needed
|
||||
if cache is None:
|
||||
cache = self.vae.get_streaming_cache()
|
||||
|
||||
# Decode latents with streaming
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=vae_dtype,
|
||||
enabled=vae_autocast_enabled):
|
||||
if fastvideo_args.pipeline_config.vae_tiling:
|
||||
self.vae.enable_tiling()
|
||||
if not vae_autocast_enabled:
|
||||
latents = latents.to(vae_dtype)
|
||||
image, cache = self.vae.streaming_decode(latents, cache,
|
||||
is_first_chunk)
|
||||
|
||||
# Normalize image to [0, 1] range
|
||||
image = (image / 2 + 0.5).clamp(0, 1)
|
||||
assert cache is not None, "cache should not be None after streaming_decode"
|
||||
return image, cache
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
|
||||
@@ -13,41 +13,14 @@ from tqdm.auto import tqdm
|
||||
|
||||
from fastvideo.attention import get_attn_backend
|
||||
from fastvideo.configs.pipelines.base import STA_Mode
|
||||
from fastvideo.distributed import (get_local_torch_device, get_world_group)
|
||||
from fastvideo.distributed import get_world_group
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.loader.component_loader import TransformerLoader
|
||||
from fastvideo.models.schedulers.scheduling_flow_match_euler_discrete import (
|
||||
FlowMatchEulerDiscreteScheduler)
|
||||
from fastvideo.models.utils import pred_noise_to_pred_video
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.base import PipelineStage
|
||||
from fastvideo.pipelines.stages.validators import StageValidators as V
|
||||
from fastvideo.pipelines.stages.validators import VerificationResult
|
||||
from fastvideo.platforms import AttentionBackendEnum
|
||||
from fastvideo.utils import dict_to_3d_list, masks_like
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.sliding_tile_attn import (
|
||||
SlidingTileAttentionBackend)
|
||||
st_attn_available = True
|
||||
except ImportError:
|
||||
st_attn_available = False
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.vmoba import VMOBAAttentionBackend
|
||||
from fastvideo.utils import is_vmoba_available
|
||||
vmoba_attn_available = is_vmoba_available()
|
||||
except ImportError:
|
||||
vmoba_attn_available = False
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.video_sparse_attn import (
|
||||
VideoSparseAttentionBackend)
|
||||
vsa_available = True
|
||||
except ImportError:
|
||||
vsa_available = False
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -65,13 +38,15 @@ class DenoisingStage(PipelineStage):
|
||||
scheduler,
|
||||
pipeline=None,
|
||||
transformer_2=None,
|
||||
vae=None) -> None:
|
||||
vae=None,
|
||||
strategy_cls=None) -> None:
|
||||
super().__init__()
|
||||
self.transformer = transformer
|
||||
self.transformer_2 = transformer_2
|
||||
self.scheduler = scheduler
|
||||
self.vae = vae
|
||||
self.pipeline = weakref.ref(pipeline) if pipeline else None
|
||||
self.strategy_cls = strategy_cls
|
||||
attn_head_size = self.transformer.hidden_size // self.transformer.num_attention_heads
|
||||
self.attn_backend = get_attn_backend(
|
||||
head_size=attn_head_size,
|
||||
@@ -92,389 +67,23 @@ class DenoisingStage(PipelineStage):
|
||||
) -> ForwardBatch:
|
||||
"""
|
||||
Run the denoising loop.
|
||||
|
||||
Args:
|
||||
batch: The current batch information.
|
||||
fastvideo_args: The inference arguments.
|
||||
|
||||
Returns:
|
||||
The batch with denoised latents.
|
||||
"""
|
||||
pipeline = self.pipeline() if self.pipeline else None
|
||||
if not fastvideo_args.model_loaded["transformer"]:
|
||||
loader = TransformerLoader()
|
||||
self.transformer = loader.load(
|
||||
fastvideo_args.model_paths["transformer"], fastvideo_args)
|
||||
if pipeline:
|
||||
pipeline.add_module("transformer", self.transformer)
|
||||
fastvideo_args.model_loaded["transformer"] = True
|
||||
from fastvideo.pipelines.stages.denoising_engine import DenoisingEngine
|
||||
from fastvideo.pipelines.stages.denoising_standard_strategy import (
|
||||
StandardStrategy)
|
||||
|
||||
# Prepare extra step kwargs for scheduler
|
||||
extra_step_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.scheduler.step,
|
||||
{
|
||||
"generator": batch.generator,
|
||||
"eta": batch.eta
|
||||
},
|
||||
)
|
||||
strategy_cls = self.strategy_cls or StandardStrategy
|
||||
engine = DenoisingEngine(strategy_cls(self),
|
||||
hooks=self._build_engine_hooks())
|
||||
return engine.run(batch, fastvideo_args)
|
||||
|
||||
# Setup precision and autocast settings
|
||||
# TODO(will): make the precision configurable for inference
|
||||
# target_dtype = PRECISION_TO_TYPE[fastvideo_args.precision]
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
# Get timesteps and calculate warmup steps
|
||||
timesteps = batch.timesteps
|
||||
# TODO(will): remove this once we add input/output validation for stages
|
||||
if timesteps is None:
|
||||
raise ValueError("Timesteps must be provided")
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
num_warmup_steps = len(
|
||||
timesteps) - num_inference_steps * self.scheduler.order
|
||||
|
||||
# Prepare image latents and embeddings for I2V generation
|
||||
image_embeds = batch.image_embeds
|
||||
if len(image_embeds) > 0:
|
||||
assert not torch.isnan(
|
||||
image_embeds[0]).any(), "image_embeds contains nan"
|
||||
image_embeds = [
|
||||
image_embed.to(target_dtype) for image_embed in image_embeds
|
||||
]
|
||||
|
||||
image_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_image": image_embeds,
|
||||
"mask_strategy": dict_to_3d_list(
|
||||
None, t_max=50, l_max=60, h_max=24)
|
||||
},
|
||||
)
|
||||
|
||||
pos_cond_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_2": batch.clip_embedding_pos,
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
neg_cond_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_2": batch.clip_embedding_neg,
|
||||
"encoder_attention_mask": batch.negative_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
action_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"mouse_cond": batch.mouse_cond,
|
||||
"keyboard_cond": batch.keyboard_cond,
|
||||
},
|
||||
)
|
||||
|
||||
# Prepare STA parameters
|
||||
if st_attn_available and self.attn_backend == SlidingTileAttentionBackend:
|
||||
self.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
# Get latents and embeddings
|
||||
latents = batch.latents
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert not torch.isnan(
|
||||
prompt_embeds[0]).any(), "prompt_embeds contains nan"
|
||||
if batch.do_classifier_free_guidance:
|
||||
neg_prompt_embeds = batch.negative_prompt_embeds
|
||||
assert neg_prompt_embeds is not None
|
||||
assert not torch.isnan(
|
||||
neg_prompt_embeds[0]).any(), "neg_prompt_embeds contains nan"
|
||||
|
||||
# (Wan2.2) Calculate timestep to switch from high noise expert to low noise expert
|
||||
boundary_ratio = fastvideo_args.pipeline_config.dit_config.boundary_ratio
|
||||
if batch.boundary_ratio is not None:
|
||||
logger.info("Overriding boundary ratio from %s to %s",
|
||||
boundary_ratio, batch.boundary_ratio)
|
||||
boundary_ratio = batch.boundary_ratio
|
||||
|
||||
if boundary_ratio is not None:
|
||||
boundary_timestep = boundary_ratio * self.scheduler.num_train_timesteps
|
||||
else:
|
||||
boundary_timestep = None
|
||||
latent_model_input = latents.to(target_dtype)
|
||||
assert latent_model_input.shape[0] == 1, "only support batch size 1"
|
||||
|
||||
if fastvideo_args.pipeline_config.ti2v_task and batch.pil_image is not None:
|
||||
# TI2V directly replaces the first frame of the latent with
|
||||
# the image latent instead of appending along the channel dim
|
||||
assert batch.image_latent is None, "TI2V task should not have image latents"
|
||||
assert self.vae is not None, "VAE is not provided for TI2V task"
|
||||
z = self.vae.encode(batch.pil_image).mean.float()
|
||||
if (hasattr(self.vae, "shift_factor")
|
||||
and self.vae.shift_factor is not None):
|
||||
if isinstance(self.vae.shift_factor, torch.Tensor):
|
||||
z -= self.vae.shift_factor.to(z.device, z.dtype)
|
||||
else:
|
||||
z -= self.vae.shift_factor
|
||||
|
||||
if isinstance(self.vae.scaling_factor, torch.Tensor):
|
||||
z = z * self.vae.scaling_factor.to(z.device, z.dtype)
|
||||
else:
|
||||
z = z * self.vae.scaling_factor
|
||||
|
||||
latent_model_input = latent_model_input.squeeze(0)
|
||||
_, mask2 = masks_like([latent_model_input], zero=True)
|
||||
|
||||
latent_model_input = (1. -
|
||||
mask2[0]) * z + mask2[0] * latent_model_input
|
||||
# latent_model_input = latent_model_input.unsqueeze(0)
|
||||
latent_model_input = latent_model_input.to(get_local_torch_device())
|
||||
latents = latent_model_input
|
||||
F = batch.num_frames
|
||||
temporal_scale = fastvideo_args.pipeline_config.vae_config.arch_config.scale_factor_temporal
|
||||
spatial_scale = fastvideo_args.pipeline_config.vae_config.arch_config.scale_factor_spatial
|
||||
patch_size = fastvideo_args.pipeline_config.dit_config.arch_config.patch_size
|
||||
seq_len = ((F - 1) // temporal_scale +
|
||||
1) * (batch.height // spatial_scale) * (
|
||||
batch.width // spatial_scale) // (patch_size[1] *
|
||||
patch_size[2])
|
||||
|
||||
# Initialize lists for ODE trajectory
|
||||
trajectory_timesteps: list[torch.Tensor] = []
|
||||
trajectory_latents: list[torch.Tensor] = []
|
||||
|
||||
# Run denoising loop
|
||||
with self.progress_bar(total=num_inference_steps) as progress_bar:
|
||||
for i, t in enumerate(timesteps):
|
||||
# Skip if interrupted
|
||||
if hasattr(self, 'interrupt') and self.interrupt:
|
||||
continue
|
||||
|
||||
if boundary_timestep is None or t >= boundary_timestep:
|
||||
if (fastvideo_args.dit_cpu_offload
|
||||
and self.transformer_2 is not None and next(
|
||||
self.transformer_2.parameters()).device.type
|
||||
== 'cuda'):
|
||||
self.transformer_2.to('cpu')
|
||||
current_model = self.transformer
|
||||
current_guidance_scale = batch.guidance_scale
|
||||
else:
|
||||
# low-noise stage in wan2.2
|
||||
if fastvideo_args.dit_cpu_offload and next(
|
||||
self.transformer.parameters(
|
||||
)).device.type == 'cuda':
|
||||
self.transformer.to('cpu')
|
||||
current_model = self.transformer_2
|
||||
current_guidance_scale = batch.guidance_scale_2
|
||||
assert current_model is not None, "current_model is None"
|
||||
|
||||
# Expand latents for V2V/I2V
|
||||
latent_model_input = latents.to(target_dtype)
|
||||
if batch.video_latent is not None:
|
||||
latent_model_input = torch.cat([
|
||||
latent_model_input, batch.video_latent,
|
||||
torch.zeros_like(latents)
|
||||
],
|
||||
dim=1).to(target_dtype)
|
||||
elif batch.image_latent is not None:
|
||||
assert not fastvideo_args.pipeline_config.ti2v_task, "image latents should not be provided for TI2V task"
|
||||
latent_model_input = torch.cat(
|
||||
[latent_model_input, batch.image_latent],
|
||||
dim=1).to(target_dtype)
|
||||
|
||||
assert not torch.isnan(
|
||||
latent_model_input).any(), "latent_model_input contains nan"
|
||||
if fastvideo_args.pipeline_config.ti2v_task and batch.pil_image is not None:
|
||||
timestep = torch.stack([t]).to(get_local_torch_device())
|
||||
temp_ts = (mask2[0][0][:, ::2, ::2] * timestep).flatten()
|
||||
temp_ts = torch.cat([
|
||||
temp_ts,
|
||||
temp_ts.new_ones(seq_len - temp_ts.size(0)) * timestep
|
||||
])
|
||||
timestep = temp_ts.unsqueeze(0)
|
||||
t_expand = timestep.repeat(latent_model_input.shape[0], 1)
|
||||
else:
|
||||
t_expand = t.repeat(latent_model_input.shape[0])
|
||||
|
||||
latent_model_input = self.scheduler.scale_model_input(
|
||||
latent_model_input, t)
|
||||
|
||||
# Prepare inputs for transformer
|
||||
guidance_expand = (
|
||||
torch.tensor(
|
||||
[fastvideo_args.pipeline_config.embedded_cfg_scale] *
|
||||
latent_model_input.shape[0],
|
||||
dtype=torch.float32,
|
||||
device=get_local_torch_device(),
|
||||
).to(target_dtype) *
|
||||
1000.0 if fastvideo_args.pipeline_config.embedded_cfg_scale
|
||||
is not None else None)
|
||||
|
||||
# Predict noise residual
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
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(
|
||||
)
|
||||
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = self.attn_metadata_builder_cls(
|
||||
)
|
||||
# TODO(will): clean this up
|
||||
attn_metadata = self.attn_metadata_builder.build( # type: ignore
|
||||
current_timestep=i, # type: ignore
|
||||
raw_latent_shape=batch.
|
||||
raw_latent_shape[2:5], # type: ignore
|
||||
patch_size=fastvideo_args.
|
||||
pipeline_config. # type: ignore
|
||||
dit_config.patch_size, # type: ignore
|
||||
STA_param=batch.STA_param, # type: ignore
|
||||
VSA_sparsity=fastvideo_args.
|
||||
VSA_sparsity, # type: ignore
|
||||
device=get_local_torch_device(),
|
||||
)
|
||||
assert attn_metadata is not None, "attn_metadata cannot be None"
|
||||
else:
|
||||
attn_metadata = None
|
||||
elif (vmoba_attn_available
|
||||
and self.attn_backend == VMOBAAttentionBackend):
|
||||
self.attn_metadata_builder_cls = self.attn_backend.get_builder_cls(
|
||||
)
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = self.attn_metadata_builder_cls(
|
||||
)
|
||||
# Prepare V-MoBA parameters from config
|
||||
moba_params = fastvideo_args.moba_config.copy()
|
||||
moba_params.update({
|
||||
"current_timestep":
|
||||
i,
|
||||
"raw_latent_shape":
|
||||
batch.raw_latent_shape[2:5],
|
||||
"patch_size":
|
||||
fastvideo_args.pipeline_config.dit_config.
|
||||
patch_size,
|
||||
"device":
|
||||
get_local_torch_device(),
|
||||
})
|
||||
attn_metadata = self.attn_metadata_builder.build(
|
||||
**moba_params)
|
||||
assert attn_metadata is not None, "attn_metadata cannot be None"
|
||||
else:
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_metadata = None
|
||||
# TODO(will): finalize the interface. vLLM uses this to
|
||||
# support torch dynamo compilation. They pass in
|
||||
# attn_metadata, vllm_config, and num_tokens. We can pass in
|
||||
# fastvideo_args or training_args, and attn_metadata.
|
||||
batch.is_cfg_negative = False
|
||||
with set_forward_context(
|
||||
current_timestep=i,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch,
|
||||
# fastvideo_args=fastvideo_args
|
||||
):
|
||||
# Run transformer
|
||||
noise_pred = current_model(
|
||||
latent_model_input,
|
||||
prompt_embeds,
|
||||
t_expand,
|
||||
guidance=guidance_expand,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
**action_kwargs,
|
||||
)
|
||||
|
||||
if batch.do_classifier_free_guidance:
|
||||
batch.is_cfg_negative = True
|
||||
with set_forward_context(
|
||||
current_timestep=i,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch,
|
||||
):
|
||||
noise_pred_uncond = current_model(
|
||||
latent_model_input,
|
||||
neg_prompt_embeds,
|
||||
t_expand,
|
||||
guidance=guidance_expand,
|
||||
**image_kwargs,
|
||||
**neg_cond_kwargs,
|
||||
**action_kwargs,
|
||||
)
|
||||
|
||||
noise_pred_text = noise_pred
|
||||
noise_pred = noise_pred_uncond + current_guidance_scale * (
|
||||
noise_pred_text - noise_pred_uncond)
|
||||
|
||||
# Apply guidance rescale if needed
|
||||
if batch.guidance_rescale > 0.0:
|
||||
# Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
|
||||
noise_pred = self.rescale_noise_cfg(
|
||||
noise_pred,
|
||||
noise_pred_text,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
)
|
||||
# Compute the previous noisy sample
|
||||
latents = self.scheduler.step(noise_pred,
|
||||
t,
|
||||
latents,
|
||||
**extra_step_kwargs,
|
||||
return_dict=False)[0]
|
||||
if fastvideo_args.pipeline_config.ti2v_task and batch.pil_image is not None:
|
||||
latents = latents.squeeze(0)
|
||||
latents = (1. - mask2[0]) * z + mask2[0] * latents
|
||||
# latents = latents.unsqueeze(0)
|
||||
|
||||
# save trajectory latents if needed
|
||||
if batch.return_trajectory_latents:
|
||||
trajectory_timesteps.append(t)
|
||||
trajectory_latents.append(latents)
|
||||
|
||||
# Update progress bar
|
||||
if i == len(timesteps) - 1 or (
|
||||
(i + 1) > num_warmup_steps and
|
||||
(i + 1) % self.scheduler.order == 0
|
||||
and progress_bar is not None):
|
||||
progress_bar.update()
|
||||
|
||||
# Gather results if using sequence parallelism
|
||||
trajectory_tensor: torch.Tensor | None = None
|
||||
if trajectory_latents:
|
||||
trajectory_tensor = torch.stack(trajectory_latents, dim=1)
|
||||
trajectory_timesteps_tensor = torch.stack(trajectory_timesteps,
|
||||
dim=0)
|
||||
else:
|
||||
trajectory_tensor = None
|
||||
trajectory_timesteps_tensor = None
|
||||
|
||||
if trajectory_tensor is not None and trajectory_timesteps_tensor is not None:
|
||||
batch.trajectory_timesteps = trajectory_timesteps_tensor.cpu()
|
||||
batch.trajectory_latents = trajectory_tensor.cpu()
|
||||
|
||||
# Update batch with final latents
|
||||
batch.latents = latents
|
||||
|
||||
# Save STA mask search results if needed
|
||||
if st_attn_available and self.attn_backend == SlidingTileAttentionBackend and fastvideo_args.STA_mode == STA_Mode.STA_SEARCHING:
|
||||
self.save_sta_search_results(batch)
|
||||
|
||||
# deallocate transformer if on mps
|
||||
if torch.backends.mps.is_available():
|
||||
logger.info("Memory before deallocating transformer: %s",
|
||||
torch.mps.current_allocated_memory())
|
||||
del self.transformer
|
||||
if pipeline is not None and "transformer" in pipeline.modules:
|
||||
del pipeline.modules["transformer"]
|
||||
fastvideo_args.model_loaded["transformer"] = False
|
||||
logger.info("Memory after deallocating transformer: %s",
|
||||
torch.mps.current_allocated_memory())
|
||||
|
||||
return batch
|
||||
def _build_engine_hooks(self):
|
||||
import fastvideo.envs as envs
|
||||
if not envs.FASTVIDEO_DENOISING_PERF_LOGGING:
|
||||
return []
|
||||
from fastvideo.pipelines.stages.denoising_engine_hooks import (
|
||||
PerfLoggingHook)
|
||||
return [PerfLoggingHook()]
|
||||
|
||||
def prepare_extra_func_kwargs(self, func, kwargs) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -685,8 +294,9 @@ class DenoisingStage(PipelineStage):
|
||||
fastvideo_args: FastVideoArgs) -> VerificationResult:
|
||||
"""Verify denoising stage inputs."""
|
||||
result = VerificationResult()
|
||||
result.add_check("timesteps", batch.timesteps,
|
||||
[V.is_tensor, V.min_dims(1)])
|
||||
if batch.timesteps is not None:
|
||||
result.add_check("timesteps", batch.timesteps,
|
||||
[V.is_tensor, V.min_dims(1)])
|
||||
result.add_check("latents", batch.latents,
|
||||
[V.is_tensor, V.with_dims(5)])
|
||||
result.add_check("prompt_embeds", batch.prompt_embeds, V.list_not_empty)
|
||||
@@ -714,475 +324,3 @@ class DenoisingStage(PipelineStage):
|
||||
result.add_check("latents", batch.latents,
|
||||
[V.is_tensor, V.with_dims(5)])
|
||||
return result
|
||||
|
||||
|
||||
class CosmosDenoisingStage(DenoisingStage):
|
||||
"""
|
||||
Denoising stage for Cosmos models using FlowMatchEulerDiscreteScheduler.
|
||||
"""
|
||||
|
||||
def __init__(self, transformer, scheduler, pipeline=None) -> None:
|
||||
super().__init__(transformer, scheduler, pipeline)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
) -> ForwardBatch:
|
||||
pipeline = self.pipeline() if self.pipeline else None
|
||||
if not fastvideo_args.model_loaded["transformer"]:
|
||||
loader = TransformerLoader()
|
||||
self.transformer = loader.load(
|
||||
fastvideo_args.model_paths["transformer"], fastvideo_args)
|
||||
if pipeline:
|
||||
pipeline.add_module("transformer", self.transformer)
|
||||
fastvideo_args.model_loaded["transformer"] = True
|
||||
|
||||
extra_step_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.scheduler.step,
|
||||
{
|
||||
"generator": batch.generator,
|
||||
"eta": batch.eta
|
||||
},
|
||||
)
|
||||
|
||||
if hasattr(self.transformer, 'module'):
|
||||
transformer_dtype = next(self.transformer.module.parameters()).dtype
|
||||
else:
|
||||
transformer_dtype = next(self.transformer.parameters()).dtype
|
||||
target_dtype = transformer_dtype
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
latents = batch.latents
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
guidance_scale = batch.guidance_scale
|
||||
|
||||
sigma_max = 80.0
|
||||
sigma_min = 0.002
|
||||
sigma_data = 1.0
|
||||
final_sigmas_type = "sigma_min"
|
||||
|
||||
if self.scheduler is not None:
|
||||
self.scheduler.register_to_config(
|
||||
sigma_max=sigma_max,
|
||||
sigma_min=sigma_min,
|
||||
sigma_data=sigma_data,
|
||||
final_sigmas_type=final_sigmas_type,
|
||||
)
|
||||
|
||||
self.scheduler.set_timesteps(num_inference_steps, device=latents.device)
|
||||
timesteps = self.scheduler.timesteps
|
||||
|
||||
if (hasattr(self.scheduler.config, 'final_sigmas_type')
|
||||
and self.scheduler.config.final_sigmas_type == "sigma_min"
|
||||
and len(self.scheduler.sigmas) > 1):
|
||||
self.scheduler.sigmas[-1] = self.scheduler.sigmas[-2]
|
||||
|
||||
conditioning_latents = getattr(batch, 'conditioning_latents', None)
|
||||
unconditioning_latents = conditioning_latents
|
||||
|
||||
with self.progress_bar(total=num_inference_steps) as progress_bar:
|
||||
for i, t in enumerate(timesteps):
|
||||
if hasattr(self, 'interrupt') and self.interrupt:
|
||||
continue
|
||||
|
||||
current_sigma = self.scheduler.sigmas[i]
|
||||
current_t = current_sigma / (current_sigma + 1)
|
||||
c_in = 1 - current_t
|
||||
c_skip = 1 - current_t
|
||||
c_out = -current_t
|
||||
|
||||
timestep = current_t.view(1, 1, 1, 1,
|
||||
1).expand(latents.size(0), -1,
|
||||
latents.size(2), -1,
|
||||
-1) # [B, 1, T, 1, 1]
|
||||
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
|
||||
cond_latent = latents * c_in
|
||||
|
||||
if hasattr(
|
||||
batch, 'cond_indicator'
|
||||
) and batch.cond_indicator is not None and conditioning_latents is not None:
|
||||
cond_latent = batch.cond_indicator * conditioning_latents + (
|
||||
1 - batch.cond_indicator) * cond_latent
|
||||
else:
|
||||
logger.warning(
|
||||
"Step %s: Missing conditioning data - cond_indicator: %s, conditioning_latents: %s",
|
||||
i, hasattr(batch, 'cond_indicator'),
|
||||
conditioning_latents is not None)
|
||||
|
||||
cond_latent = cond_latent.to(target_dtype)
|
||||
|
||||
cond_timestep = timestep
|
||||
if hasattr(batch, 'cond_indicator'
|
||||
) and batch.cond_indicator is not None:
|
||||
sigma_conditioning = 0.0001
|
||||
t_conditioning = sigma_conditioning / (
|
||||
sigma_conditioning + 1)
|
||||
cond_timestep = batch.cond_indicator * t_conditioning + (
|
||||
1 - batch.cond_indicator) * timestep
|
||||
cond_timestep = cond_timestep.to(target_dtype)
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=i,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
):
|
||||
# Use conditioning masks from CosmosLatentPreparationStage
|
||||
condition_mask = batch.cond_mask.to(
|
||||
target_dtype) if hasattr(batch,
|
||||
'cond_mask') else None
|
||||
padding_mask = torch.zeros(1,
|
||||
1,
|
||||
batch.height,
|
||||
batch.width,
|
||||
device=cond_latent.device,
|
||||
dtype=target_dtype)
|
||||
|
||||
# Fallback if masks not available
|
||||
if condition_mask is None:
|
||||
batch_size, num_channels, num_frames, height, width = cond_latent.shape
|
||||
condition_mask = torch.zeros(
|
||||
batch_size,
|
||||
1,
|
||||
num_frames,
|
||||
height,
|
||||
width,
|
||||
device=cond_latent.device,
|
||||
dtype=target_dtype)
|
||||
|
||||
noise_pred = self.transformer(
|
||||
hidden_states=cond_latent,
|
||||
timestep=cond_timestep.to(target_dtype),
|
||||
encoder_hidden_states=batch.prompt_embeds[0].to(
|
||||
target_dtype),
|
||||
fps=24, # TODO: get fps from batch or config
|
||||
condition_mask=condition_mask,
|
||||
padding_mask=padding_mask,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
cond_pred = (c_skip * latents +
|
||||
c_out * noise_pred.float()).to(target_dtype)
|
||||
|
||||
if hasattr(
|
||||
batch, 'cond_indicator'
|
||||
) and batch.cond_indicator is not None and conditioning_latents is not None:
|
||||
cond_pred = batch.cond_indicator * conditioning_latents + (
|
||||
1 - batch.cond_indicator) * cond_pred
|
||||
|
||||
if batch.do_classifier_free_guidance and batch.negative_prompt_embeds is not None:
|
||||
uncond_latent = latents * c_in
|
||||
|
||||
if hasattr(
|
||||
batch, 'uncond_indicator'
|
||||
) and batch.uncond_indicator is not None and unconditioning_latents is not None:
|
||||
uncond_latent = batch.uncond_indicator * unconditioning_latents + (
|
||||
1 - batch.uncond_indicator) * uncond_latent
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=i,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
):
|
||||
uncond_condition_mask = batch.uncond_mask.to(
|
||||
target_dtype
|
||||
) if hasattr(
|
||||
batch, 'uncond_mask'
|
||||
) and batch.uncond_mask is not None else condition_mask
|
||||
|
||||
uncond_timestep = timestep
|
||||
if hasattr(batch, 'uncond_indicator'
|
||||
) and batch.uncond_indicator is not None:
|
||||
sigma_conditioning = 0.0001
|
||||
t_conditioning = sigma_conditioning / (
|
||||
sigma_conditioning + 1)
|
||||
uncond_timestep = batch.uncond_indicator * t_conditioning + (
|
||||
1 - batch.uncond_indicator) * timestep
|
||||
uncond_timestep = uncond_timestep.to(
|
||||
target_dtype)
|
||||
|
||||
noise_pred_uncond = self.transformer(
|
||||
hidden_states=uncond_latent.to(target_dtype),
|
||||
timestep=uncond_timestep.to(target_dtype),
|
||||
encoder_hidden_states=batch.
|
||||
negative_prompt_embeds[0].to(target_dtype),
|
||||
fps=24, # TODO: get fps from batch or config
|
||||
condition_mask=uncond_condition_mask,
|
||||
padding_mask=padding_mask,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
uncond_pred = (
|
||||
c_skip * latents +
|
||||
c_out * noise_pred_uncond.float()).to(target_dtype)
|
||||
|
||||
if hasattr(
|
||||
batch, 'uncond_indicator'
|
||||
) and batch.uncond_indicator is not None and unconditioning_latents is not None:
|
||||
uncond_pred = batch.uncond_indicator * unconditioning_latents + (
|
||||
1 - batch.uncond_indicator) * uncond_pred
|
||||
|
||||
guidance_diff = cond_pred - uncond_pred
|
||||
final_pred = cond_pred + guidance_scale * guidance_diff
|
||||
else:
|
||||
final_pred = cond_pred
|
||||
|
||||
# Convert to noise for scheduler step
|
||||
if current_sigma > 1e-8:
|
||||
noise_for_scheduler = (latents - final_pred) / current_sigma
|
||||
else:
|
||||
logger.warning(
|
||||
"Step %s: current_sigma too small (%s), using final_pred directly",
|
||||
i, current_sigma)
|
||||
noise_for_scheduler = final_pred
|
||||
|
||||
if torch.isnan(noise_for_scheduler).sum() > 0:
|
||||
logger.error(
|
||||
"Step %s: NaN detected in noise_for_scheduler, sum: %s",
|
||||
i,
|
||||
noise_for_scheduler.float().sum().item())
|
||||
logger.error(
|
||||
"Step %s: latents sum: %s, final_pred sum: %s, current_sigma: %s",
|
||||
i,
|
||||
latents.float().sum().item(),
|
||||
final_pred.float().sum().item(), current_sigma)
|
||||
|
||||
latents = self.scheduler.step(noise_for_scheduler,
|
||||
t,
|
||||
latents,
|
||||
**extra_step_kwargs,
|
||||
return_dict=False)[0]
|
||||
|
||||
progress_bar.update()
|
||||
|
||||
batch.latents = latents
|
||||
|
||||
return batch
|
||||
|
||||
def verify_input(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> VerificationResult:
|
||||
"""Verify Cosmos denoising stage inputs."""
|
||||
result = VerificationResult()
|
||||
result.add_check("latents", batch.latents,
|
||||
[V.is_tensor, V.with_dims(5)])
|
||||
result.add_check("prompt_embeds", batch.prompt_embeds, V.list_not_empty)
|
||||
result.add_check("num_inference_steps", batch.num_inference_steps,
|
||||
V.positive_int)
|
||||
result.add_check("guidance_scale", batch.guidance_scale,
|
||||
V.positive_float)
|
||||
result.add_check("do_classifier_free_guidance",
|
||||
batch.do_classifier_free_guidance, V.bool_value)
|
||||
result.add_check(
|
||||
"negative_prompt_embeds", batch.negative_prompt_embeds, lambda x:
|
||||
not batch.do_classifier_free_guidance or V.list_not_empty(x))
|
||||
return result
|
||||
|
||||
def verify_output(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> VerificationResult:
|
||||
"""Verify Cosmos denoising stage outputs."""
|
||||
result = VerificationResult()
|
||||
result.add_check("latents", batch.latents,
|
||||
[V.is_tensor, V.with_dims(5)])
|
||||
return result
|
||||
|
||||
|
||||
class DmdDenoisingStage(DenoisingStage):
|
||||
"""
|
||||
Denoising stage for DMD.
|
||||
"""
|
||||
|
||||
def __init__(self, transformer, scheduler) -> None:
|
||||
super().__init__(transformer, scheduler)
|
||||
self.scheduler = FlowMatchEulerDiscreteScheduler(shift=8.0)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
) -> ForwardBatch:
|
||||
"""
|
||||
Run the denoising loop.
|
||||
|
||||
Args:
|
||||
batch: The current batch information.
|
||||
fastvideo_args: The inference arguments.
|
||||
|
||||
Returns:
|
||||
The batch with denoised latents.
|
||||
"""
|
||||
# Setup precision and autocast settings
|
||||
# TODO(will): make the precision configurable for inference
|
||||
# target_dtype = PRECISION_TO_TYPE[fastvideo_args.precision]
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
# Get timesteps and calculate warmup steps
|
||||
timesteps = batch.timesteps
|
||||
|
||||
# TODO(will): remove this once we add input/output validation for stages
|
||||
if timesteps is None:
|
||||
raise ValueError("Timesteps must be provided")
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
num_warmup_steps = len(
|
||||
timesteps) - num_inference_steps * self.scheduler.order
|
||||
|
||||
# Prepare image latents and embeddings for I2V generation
|
||||
image_embeds = batch.image_embeds
|
||||
if len(image_embeds) > 0:
|
||||
assert torch.isnan(image_embeds[0]).sum() == 0
|
||||
image_embeds = [
|
||||
image_embed.to(target_dtype) for image_embed in image_embeds
|
||||
]
|
||||
|
||||
image_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_image": image_embeds,
|
||||
"mask_strategy": dict_to_3d_list(
|
||||
None, t_max=50, l_max=60, h_max=24)
|
||||
},
|
||||
)
|
||||
|
||||
pos_cond_kwargs = self.prepare_extra_func_kwargs(
|
||||
self.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_2": batch.clip_embedding_pos,
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
# Prepare STA parameters
|
||||
if st_attn_available and self.attn_backend == SlidingTileAttentionBackend:
|
||||
self.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
# Get latents and embeddings
|
||||
assert batch.latents is not None, "latents must be provided"
|
||||
latents = batch.latents
|
||||
|
||||
video_raw_latent_shape = latents.shape
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert not torch.isnan(
|
||||
prompt_embeds[0]).any(), "prompt_embeds contains nan"
|
||||
timesteps = torch.tensor(
|
||||
fastvideo_args.pipeline_config.dmd_denoising_steps,
|
||||
dtype=torch.long,
|
||||
device=get_local_torch_device())
|
||||
|
||||
# Run denoising loop
|
||||
with self.progress_bar(total=len(timesteps)) as progress_bar:
|
||||
for i, t in enumerate(timesteps):
|
||||
# Skip if interrupted
|
||||
if hasattr(self, 'interrupt') and self.interrupt:
|
||||
continue
|
||||
# Expand latents for I2V
|
||||
noise_latents = latents.clone()
|
||||
latent_model_input = latents.to(target_dtype)
|
||||
|
||||
if batch.image_latent is not None:
|
||||
latent_model_input = torch.cat([
|
||||
latent_model_input,
|
||||
batch.image_latent.permute(0, 2, 1, 3, 4)
|
||||
],
|
||||
dim=2).to(target_dtype)
|
||||
assert not torch.isnan(
|
||||
latent_model_input).any(), "latent_model_input contains nan"
|
||||
|
||||
# Prepare inputs for transformer
|
||||
t_expand = t.repeat(latent_model_input.shape[0])
|
||||
guidance_expand = (
|
||||
torch.tensor(
|
||||
[fastvideo_args.pipeline_config.embedded_cfg_scale] *
|
||||
latent_model_input.shape[0],
|
||||
dtype=torch.float32,
|
||||
device=get_local_torch_device(),
|
||||
).to(target_dtype) *
|
||||
1000.0 if fastvideo_args.pipeline_config.embedded_cfg_scale
|
||||
is not None else None)
|
||||
|
||||
# Predict noise residual
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
if (vsa_available and self.attn_backend
|
||||
== VideoSparseAttentionBackend):
|
||||
self.attn_metadata_builder_cls = self.attn_backend.get_builder_cls(
|
||||
)
|
||||
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = self.attn_metadata_builder_cls(
|
||||
)
|
||||
# TODO(will): clean this up
|
||||
attn_metadata = self.attn_metadata_builder.build( # type: ignore
|
||||
current_timestep=i, # type: ignore
|
||||
raw_latent_shape=batch.
|
||||
raw_latent_shape[2:5], # type: ignore
|
||||
patch_size=fastvideo_args.
|
||||
pipeline_config. # type: ignore
|
||||
dit_config.patch_size, # type: ignore
|
||||
STA_param=batch.STA_param, # type: ignore
|
||||
VSA_sparsity=fastvideo_args.
|
||||
VSA_sparsity, # type: ignore
|
||||
device=get_local_torch_device(), # type: ignore
|
||||
) # type: ignore
|
||||
assert attn_metadata is not None, "attn_metadata cannot be None"
|
||||
else:
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_metadata = None
|
||||
|
||||
batch.is_cfg_negative = False
|
||||
with set_forward_context(
|
||||
current_timestep=i,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch,
|
||||
# fastvideo_args=fastvideo_args
|
||||
):
|
||||
# Run transformer
|
||||
pred_noise = self.transformer(
|
||||
latent_model_input.permute(0, 2, 1, 3, 4),
|
||||
prompt_embeds,
|
||||
t_expand,
|
||||
guidance=guidance_expand,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
).permute(0, 2, 1, 3, 4)
|
||||
|
||||
pred_video = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noise_latents.flatten(0, 1),
|
||||
timestep=t_expand,
|
||||
scheduler=self.scheduler).unflatten(
|
||||
0, pred_noise.shape[:2])
|
||||
|
||||
if i < len(timesteps) - 1:
|
||||
next_timestep = timesteps[i + 1] * torch.ones(
|
||||
[1], dtype=torch.long, device=pred_video.device)
|
||||
noise = torch.randn(video_raw_latent_shape,
|
||||
dtype=pred_video.dtype,
|
||||
generator=batch.generator[0]).to(
|
||||
self.device)
|
||||
latents = self.scheduler.add_noise(
|
||||
pred_video.flatten(0, 1), noise.flatten(0, 1),
|
||||
next_timestep).unflatten(0, pred_video.shape[:2])
|
||||
else:
|
||||
latents = pred_video
|
||||
|
||||
# Update progress bar
|
||||
if i == len(timesteps) - 1 or (
|
||||
(i + 1) > num_warmup_steps and
|
||||
(i + 1) % self.scheduler.order == 0
|
||||
and progress_bar is not None):
|
||||
progress_bar.update()
|
||||
|
||||
# Gather results if using sequence parallelism
|
||||
latents = latents.permute(0, 2, 1, 3, 4)
|
||||
# Update batch with final latents
|
||||
batch.latents = latents
|
||||
|
||||
return batch
|
||||
@@ -0,0 +1,623 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Causal block denoising strategy (DMD).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.models.utils import pred_noise_to_pred_video, pred_noise_to_x_bound
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
BlockContext,
|
||||
BlockDenoisingStrategy,
|
||||
BlockPlan,
|
||||
BlockPlanItem,
|
||||
ModelInputs,
|
||||
StrategyState,
|
||||
)
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.sliding_tile_attn import (
|
||||
SlidingTileAttentionBackend)
|
||||
st_attn_available = True
|
||||
except ImportError:
|
||||
st_attn_available = False
|
||||
SlidingTileAttentionBackend = None # type: ignore
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.video_sparse_attn import (
|
||||
VideoSparseAttentionBackend)
|
||||
vsa_available = True
|
||||
except ImportError:
|
||||
vsa_available = False
|
||||
VideoSparseAttentionBackend = None # type: ignore
|
||||
|
||||
|
||||
class CausalBlockStrategy(BlockDenoisingStrategy):
|
||||
|
||||
def __init__(self, stage: Any) -> None:
|
||||
self.stage = stage
|
||||
self.num_transformer_blocks = 0
|
||||
self.num_frames_per_block = 0
|
||||
self.sliding_window_num_frames = 0
|
||||
self.local_attn_size = -1
|
||||
self.frame_seq_length = 0
|
||||
|
||||
def _ensure_model_constants(self) -> None:
|
||||
transformer = self.stage.transformer
|
||||
self.num_transformer_blocks = len(transformer.blocks)
|
||||
arch_config = transformer.config.arch_config
|
||||
self.num_frames_per_block = arch_config.num_frames_per_block
|
||||
self.sliding_window_num_frames = arch_config.sliding_window_num_frames
|
||||
try:
|
||||
self.local_attn_size = getattr(transformer.model, "local_attn_size",
|
||||
-1)
|
||||
except Exception:
|
||||
self.local_attn_size = -1
|
||||
|
||||
def _initialize_kv_cache(self, batch_size, dtype, device) -> list[dict]:
|
||||
kv_cache1 = []
|
||||
num_attention_heads = self.stage.transformer.num_attention_heads
|
||||
attention_head_dim = self.stage.transformer.attention_head_dim
|
||||
if self.local_attn_size != -1:
|
||||
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
||||
else:
|
||||
kv_cache_size = self.frame_seq_length * self.sliding_window_num_frames
|
||||
|
||||
for _ in range(self.num_transformer_blocks):
|
||||
kv_cache1.append({
|
||||
"k":
|
||||
torch.zeros([
|
||||
batch_size, kv_cache_size, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"v":
|
||||
torch.zeros([
|
||||
batch_size, kv_cache_size, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"global_end_index":
|
||||
torch.tensor([0], dtype=torch.long, device=device),
|
||||
"local_end_index":
|
||||
torch.tensor([0], dtype=torch.long, device=device),
|
||||
})
|
||||
|
||||
return kv_cache1
|
||||
|
||||
def _initialize_crossattn_cache(self, batch_size, max_text_len, dtype,
|
||||
device) -> list[dict]:
|
||||
crossattn_cache = []
|
||||
num_attention_heads = self.stage.transformer.num_attention_heads
|
||||
attention_head_dim = self.stage.transformer.attention_head_dim
|
||||
for _ in range(self.num_transformer_blocks):
|
||||
crossattn_cache.append({
|
||||
"k":
|
||||
torch.zeros([
|
||||
batch_size, max_text_len, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"v":
|
||||
torch.zeros([
|
||||
batch_size, max_text_len, num_attention_heads,
|
||||
attention_head_dim
|
||||
],
|
||||
dtype=dtype,
|
||||
device=device),
|
||||
"is_init":
|
||||
False,
|
||||
})
|
||||
return crossattn_cache
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
self._ensure_model_constants()
|
||||
latents = batch.latents
|
||||
if latents is None:
|
||||
raise ValueError("latents must be provided")
|
||||
|
||||
latent_seq_length = latents.shape[-1] * latents.shape[-2]
|
||||
patch_ratio = (
|
||||
self.stage.transformer.config.arch_config.patch_size[-1] *
|
||||
self.stage.transformer.config.arch_config.patch_size[-2])
|
||||
self.frame_seq_length = latent_seq_length // patch_ratio
|
||||
|
||||
independent_first_frame = getattr(self.stage.transformer,
|
||||
"independent_first_frame", False)
|
||||
|
||||
timesteps = torch.tensor(
|
||||
fastvideo_args.pipeline_config.dmd_denoising_steps,
|
||||
dtype=torch.long).cpu()
|
||||
if getattr(fastvideo_args.pipeline_config, "warp_denoising_step",
|
||||
False):
|
||||
scheduler_timesteps = torch.cat((
|
||||
self.stage.scheduler.timesteps.cpu(),
|
||||
torch.tensor([0], dtype=torch.float32),
|
||||
))
|
||||
timesteps = scheduler_timesteps[1000 - timesteps]
|
||||
timesteps = timesteps.to(get_local_torch_device())
|
||||
|
||||
boundary_ratio = fastvideo_args.pipeline_config.dit_config.boundary_ratio
|
||||
if boundary_ratio is not None:
|
||||
boundary_timestep = (boundary_ratio *
|
||||
self.stage.scheduler.num_train_timesteps)
|
||||
high_noise_timesteps = timesteps[timesteps >= boundary_timestep]
|
||||
else:
|
||||
boundary_timestep = None
|
||||
high_noise_timesteps = None
|
||||
|
||||
image_kwargs: dict[str, Any] = {}
|
||||
pos_cond_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
if (st_attn_available
|
||||
and self.stage.attn_backend == SlidingTileAttentionBackend):
|
||||
self.stage.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert torch.isnan(prompt_embeds[0]).sum() == 0
|
||||
|
||||
kv_cache1 = self._initialize_kv_cache(batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device)
|
||||
kv_cache2 = None
|
||||
if boundary_timestep is not None:
|
||||
kv_cache2 = self._initialize_kv_cache(
|
||||
batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
|
||||
text_len = None
|
||||
if fastvideo_args.pipeline_config.text_encoder_configs:
|
||||
text_len = getattr(
|
||||
fastvideo_args.pipeline_config.text_encoder_configs[0].
|
||||
arch_config, "text_len", None)
|
||||
if not text_len:
|
||||
if batch.prompt_attention_mask:
|
||||
text_len = batch.prompt_attention_mask[0].shape[-1]
|
||||
elif batch.prompt_embeds:
|
||||
text_len = batch.prompt_embeds[0].shape[1]
|
||||
else:
|
||||
text_len = 0
|
||||
|
||||
crossattn_cache = self._initialize_crossattn_cache(
|
||||
batch_size=latents.shape[0],
|
||||
max_text_len=text_len,
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
|
||||
num_frames = latents.shape[2]
|
||||
if num_frames % self.num_frames_per_block != 0:
|
||||
raise ValueError(
|
||||
"num_frames must be divisible by num_frames_per_block for "
|
||||
"causal DMD denoising")
|
||||
num_blocks = num_frames // self.num_frames_per_block
|
||||
block_sizes = [self.num_frames_per_block] * num_blocks
|
||||
start_index = 0
|
||||
|
||||
if boundary_timestep is not None:
|
||||
block_sizes[0] = 1
|
||||
|
||||
pos_start_base = 0
|
||||
if batch.pil_image is not None:
|
||||
assert self.stage.vae is not None, (
|
||||
"VAE is not provided for causal video gen task")
|
||||
self.stage.vae = self.stage.vae.to(get_local_torch_device())
|
||||
first_frame_latent = self.stage.vae.encode(
|
||||
batch.pil_image).mean.float()
|
||||
if (hasattr(self.stage.vae, "shift_factor")
|
||||
and self.stage.vae.shift_factor is not None):
|
||||
if isinstance(self.stage.vae.shift_factor, torch.Tensor):
|
||||
first_frame_latent -= self.stage.vae.shift_factor.to(
|
||||
first_frame_latent.device, first_frame_latent.dtype)
|
||||
else:
|
||||
first_frame_latent -= self.stage.vae.shift_factor
|
||||
|
||||
if isinstance(self.stage.vae.scaling_factor, torch.Tensor):
|
||||
first_frame_latent = (
|
||||
first_frame_latent * self.stage.vae.scaling_factor.to(
|
||||
first_frame_latent.device, first_frame_latent.dtype))
|
||||
else:
|
||||
first_frame_latent = (first_frame_latent *
|
||||
self.stage.vae.scaling_factor)
|
||||
|
||||
if fastvideo_args.vae_cpu_offload:
|
||||
self.stage.vae = self.stage.vae.to("cpu")
|
||||
|
||||
t_zero = torch.zeros([latents.shape[0], 1],
|
||||
device=latents.device,
|
||||
dtype=torch.long)
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled), \
|
||||
set_forward_context(current_timestep=0,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch):
|
||||
self.stage.transformer(
|
||||
first_frame_latent.to(target_dtype),
|
||||
prompt_embeds,
|
||||
t_zero,
|
||||
kv_cache=kv_cache1,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
if boundary_timestep is not None:
|
||||
self.stage.transformer_2(
|
||||
first_frame_latent.to(target_dtype),
|
||||
prompt_embeds,
|
||||
t_zero,
|
||||
kv_cache=kv_cache2,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=(pos_start_base + start_index) *
|
||||
self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
start_index += 1
|
||||
block_sizes.pop(0)
|
||||
latents[:, :, :1, :, :] = first_frame_latent
|
||||
|
||||
progress_bar = self.stage.progress_bar(total=len(block_sizes) *
|
||||
len(timesteps))
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch":
|
||||
batch,
|
||||
"fastvideo_args":
|
||||
fastvideo_args,
|
||||
"target_dtype":
|
||||
target_dtype,
|
||||
"autocast_enabled":
|
||||
autocast_enabled,
|
||||
"boundary_timestep":
|
||||
boundary_timestep,
|
||||
"high_noise_timesteps":
|
||||
high_noise_timesteps,
|
||||
"image_kwargs":
|
||||
image_kwargs,
|
||||
"pos_cond_kwargs":
|
||||
pos_cond_kwargs,
|
||||
"kv_cache1":
|
||||
kv_cache1,
|
||||
"kv_cache2":
|
||||
kv_cache2,
|
||||
"crossattn_cache":
|
||||
crossattn_cache,
|
||||
"block_sizes":
|
||||
block_sizes,
|
||||
"start_index":
|
||||
start_index,
|
||||
"progress_bar":
|
||||
progress_bar,
|
||||
"independent_first_frame":
|
||||
independent_first_frame,
|
||||
"context_noise":
|
||||
getattr(fastvideo_args.pipeline_config, "context_noise", 0),
|
||||
"pos_start_base":
|
||||
pos_start_base,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=len(timesteps),
|
||||
prompt_embeds=batch.prompt_embeds,
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=batch.prompt_attention_mask,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=batch.image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def block_plan(self, state: StrategyState) -> BlockPlan:
|
||||
block_sizes = state.extra["block_sizes"]
|
||||
start_index = state.extra["start_index"]
|
||||
items: list[BlockPlanItem] = []
|
||||
for block_size in block_sizes:
|
||||
items.append(
|
||||
BlockPlanItem(
|
||||
start_index=start_index,
|
||||
num_frames=block_size,
|
||||
use_kv_cache=True,
|
||||
model_selector="default",
|
||||
))
|
||||
start_index += block_size
|
||||
return BlockPlan(items=items)
|
||||
|
||||
def init_block_context(self, state: StrategyState,
|
||||
block_item: BlockPlanItem,
|
||||
block_idx: int) -> BlockContext:
|
||||
return BlockContext(
|
||||
kv_cache=state.extra["kv_cache1"],
|
||||
kv_cache_2=state.extra["kv_cache2"],
|
||||
crossattn_cache=state.extra["crossattn_cache"],
|
||||
action_cache=None,
|
||||
extra={
|
||||
"block_idx": block_idx,
|
||||
"start_index": block_item.start_index,
|
||||
"num_frames": block_item.num_frames,
|
||||
},
|
||||
)
|
||||
|
||||
def process_block(self, state: StrategyState, block_ctx: BlockContext,
|
||||
block_item: BlockPlanItem) -> None:
|
||||
batch = state.extra["batch"]
|
||||
fastvideo_args = state.extra["fastvideo_args"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
boundary_timestep = state.extra["boundary_timestep"]
|
||||
high_noise_timesteps = state.extra["high_noise_timesteps"]
|
||||
image_kwargs = state.extra["image_kwargs"]
|
||||
pos_cond_kwargs = state.extra["pos_cond_kwargs"]
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
independent_first_frame = state.extra["independent_first_frame"]
|
||||
|
||||
start_index = block_item.start_index
|
||||
current_num_frames = block_item.num_frames
|
||||
|
||||
kv_cache1 = block_ctx.kv_cache
|
||||
kv_cache2 = block_ctx.kv_cache_2
|
||||
crossattn_cache = block_ctx.crossattn_cache
|
||||
|
||||
def _get_kv_cache(timestep_val: float) -> list[dict]:
|
||||
if boundary_timestep is not None:
|
||||
if timestep_val >= boundary_timestep:
|
||||
return kv_cache1
|
||||
if kv_cache2 is None:
|
||||
raise ValueError("kv_cache2 is not initialized")
|
||||
return kv_cache2
|
||||
return kv_cache1
|
||||
|
||||
current_latents = state.latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :]
|
||||
noise_latents_btchw = current_latents.permute(0, 2, 1, 3, 4)
|
||||
video_raw_latent_shape = noise_latents_btchw.shape
|
||||
h, w = current_latents.shape[-2:]
|
||||
|
||||
attn_metadata = None
|
||||
for i, t_cur in enumerate(state.timesteps):
|
||||
if boundary_timestep is not None and t_cur < boundary_timestep:
|
||||
current_model = self.stage.transformer_2
|
||||
else:
|
||||
current_model = self.stage.transformer
|
||||
noise_latents = noise_latents_btchw.clone()
|
||||
latent_model_input = current_latents.to(target_dtype)
|
||||
|
||||
if (batch.image_latent is not None and independent_first_frame
|
||||
and start_index == 0):
|
||||
latent_model_input = torch.cat(
|
||||
[latent_model_input,
|
||||
batch.image_latent.to(target_dtype)],
|
||||
dim=2)
|
||||
|
||||
t_expand = t_cur.repeat(latent_model_input.shape[0])
|
||||
|
||||
if (vsa_available
|
||||
and self.stage.attn_backend == VideoSparseAttentionBackend):
|
||||
self.attn_metadata_builder_cls = (
|
||||
self.stage.attn_backend.get_builder_cls())
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = (
|
||||
self.attn_metadata_builder_cls())
|
||||
attn_metadata = self.attn_metadata_builder.build( # type: ignore
|
||||
current_timestep=i, # type: ignore
|
||||
raw_latent_shape=(current_num_frames, h,
|
||||
w), # type: ignore
|
||||
patch_size=fastvideo_args.pipeline_config.dit_config.
|
||||
patch_size, # type: ignore
|
||||
STA_param=batch.STA_param, # type: ignore
|
||||
VSA_sparsity=fastvideo_args.
|
||||
VSA_sparsity, # type: ignore
|
||||
device=get_local_torch_device(), # type: ignore
|
||||
)
|
||||
assert attn_metadata is not None, (
|
||||
"attn_metadata cannot be None")
|
||||
else:
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_metadata = None
|
||||
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled), \
|
||||
set_forward_context(current_timestep=i,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch):
|
||||
t_expanded_noise = t_cur * torch.ones(
|
||||
(latent_model_input.shape[0], 1),
|
||||
device=latent_model_input.device,
|
||||
dtype=torch.long)
|
||||
pred_noise_btchw = current_model(
|
||||
latent_model_input,
|
||||
batch.prompt_embeds,
|
||||
t_expanded_noise,
|
||||
kv_cache=_get_kv_cache(t_cur),
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=start_index * self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
).permute(0, 2, 1, 3, 4)
|
||||
|
||||
if boundary_timestep is not None and t_cur >= boundary_timestep:
|
||||
pred_video_btchw = pred_noise_to_x_bound(
|
||||
pred_noise=pred_noise_btchw.flatten(0, 1),
|
||||
noise_input_latent=noise_latents.flatten(0, 1),
|
||||
timestep=t_expand,
|
||||
boundary_timestep=torch.ones_like(t_expand) *
|
||||
boundary_timestep,
|
||||
scheduler=self.stage.scheduler,
|
||||
).unflatten(0, pred_noise_btchw.shape[:2])
|
||||
else:
|
||||
pred_video_btchw = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise_btchw.flatten(0, 1),
|
||||
noise_input_latent=noise_latents.flatten(0, 1),
|
||||
timestep=t_expand,
|
||||
scheduler=self.stage.scheduler,
|
||||
).unflatten(0, pred_noise_btchw.shape[:2])
|
||||
|
||||
if i < len(state.timesteps) - 1:
|
||||
next_timestep = state.timesteps[i + 1] * torch.ones(
|
||||
[1],
|
||||
dtype=torch.long,
|
||||
device=pred_video_btchw.device,
|
||||
)
|
||||
noise = torch.randn(
|
||||
video_raw_latent_shape,
|
||||
dtype=pred_video_btchw.dtype,
|
||||
generator=(batch.generator[0] if isinstance(
|
||||
batch.generator, list) else batch.generator),
|
||||
).to(self.stage.device)
|
||||
noise_btchw = noise
|
||||
if (boundary_timestep is not None
|
||||
and high_noise_timesteps is not None
|
||||
and i < len(high_noise_timesteps) - 1):
|
||||
noise_latents_btchw = self.stage.scheduler.add_noise_high(
|
||||
pred_video_btchw.flatten(0, 1),
|
||||
noise_btchw.flatten(0, 1),
|
||||
next_timestep,
|
||||
torch.ones_like(next_timestep) * boundary_timestep,
|
||||
).unflatten(0, pred_video_btchw.shape[:2])
|
||||
elif (boundary_timestep is not None
|
||||
and high_noise_timesteps is not None
|
||||
and i == len(high_noise_timesteps) - 1):
|
||||
noise_latents_btchw = pred_video_btchw
|
||||
else:
|
||||
noise_latents_btchw = self.stage.scheduler.add_noise(
|
||||
pred_video_btchw.flatten(0, 1),
|
||||
noise_btchw.flatten(0, 1),
|
||||
next_timestep,
|
||||
).unflatten(0, pred_video_btchw.shape[:2])
|
||||
current_latents = noise_latents_btchw.permute(0, 2, 1, 3, 4)
|
||||
else:
|
||||
current_latents = pred_video_btchw.permute(0, 2, 1, 3, 4)
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
|
||||
block_ctx.extra["attn_metadata"] = attn_metadata
|
||||
state.latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :] = (current_latents)
|
||||
|
||||
def update_context(self, state: StrategyState, block_ctx: BlockContext,
|
||||
block_item: BlockPlanItem) -> None:
|
||||
batch = state.extra["batch"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
boundary_timestep = state.extra["boundary_timestep"]
|
||||
kv_cache1 = state.extra["kv_cache1"]
|
||||
kv_cache2 = state.extra["kv_cache2"]
|
||||
crossattn_cache = state.extra["crossattn_cache"]
|
||||
image_kwargs = state.extra["image_kwargs"]
|
||||
pos_cond_kwargs = state.extra["pos_cond_kwargs"]
|
||||
context_noise = state.extra["context_noise"]
|
||||
|
||||
start_index = block_item.start_index
|
||||
current_num_frames = block_item.num_frames
|
||||
|
||||
current_latents = state.latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :]
|
||||
latents_device = current_latents.device
|
||||
|
||||
t_context = torch.ones([current_latents.shape[0]],
|
||||
device=latents_device,
|
||||
dtype=torch.long) * int(context_noise)
|
||||
context_bcthw = current_latents.to(target_dtype)
|
||||
|
||||
attn_metadata = block_ctx.extra.get("attn_metadata")
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled), \
|
||||
set_forward_context(current_timestep=0,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch):
|
||||
t_expanded_context = t_context.unsqueeze(1)
|
||||
|
||||
if boundary_timestep is not None:
|
||||
self.stage.transformer_2(
|
||||
context_bcthw,
|
||||
batch.prompt_embeds,
|
||||
t_expanded_context,
|
||||
kv_cache=kv_cache2,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=start_index * self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
self.stage.transformer(
|
||||
context_bcthw,
|
||||
batch.prompt_embeds,
|
||||
t_expanded_context,
|
||||
kv_cache=kv_cache1,
|
||||
crossattn_cache=crossattn_cache,
|
||||
current_start=start_index * self.frame_seq_length,
|
||||
start_frame=start_index,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
)
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
|
||||
batch = state.extra["batch"]
|
||||
boundary_timestep = state.extra["boundary_timestep"]
|
||||
|
||||
latents = state.latents
|
||||
if boundary_timestep is not None:
|
||||
num_frames_to_remove = self.num_frames_per_block - 1
|
||||
latents = latents[:, :, :-num_frames_to_remove, :, :]
|
||||
|
||||
batch.latents = latents
|
||||
return batch
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
raise NotImplementedError
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
raise NotImplementedError
|
||||
|
||||
def cfg_combine(self, state: StrategyState,
|
||||
noise_pred: torch.Tensor) -> torch.Tensor:
|
||||
raise NotImplementedError
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,325 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Cosmos denoising strategy using FlowMatchEulerDiscreteScheduler.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.loader.component_loader import TransformerLoader
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
DenoisingStrategy,
|
||||
ModelInputs,
|
||||
StrategyState,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class CosmosStrategy(DenoisingStrategy):
|
||||
|
||||
def __init__(self, stage: Any) -> None:
|
||||
self.stage = stage
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
pipeline = self.stage.pipeline() if self.stage.pipeline else None
|
||||
if not fastvideo_args.model_loaded["transformer"]:
|
||||
loader = TransformerLoader()
|
||||
self.stage.transformer = loader.load(
|
||||
fastvideo_args.model_paths["transformer"], fastvideo_args)
|
||||
if pipeline:
|
||||
pipeline.add_module("transformer", self.stage.transformer)
|
||||
fastvideo_args.model_loaded["transformer"] = True
|
||||
|
||||
extra_step_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.scheduler.step,
|
||||
{
|
||||
"generator": batch.generator,
|
||||
"eta": batch.eta
|
||||
},
|
||||
)
|
||||
|
||||
if hasattr(self.stage.transformer, "module"):
|
||||
transformer_dtype = next(
|
||||
self.stage.transformer.module.parameters()).dtype
|
||||
else:
|
||||
transformer_dtype = next(self.stage.transformer.parameters()).dtype
|
||||
target_dtype = transformer_dtype
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
latents = batch.latents
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
|
||||
sigma_max = 80.0
|
||||
sigma_min = 0.002
|
||||
sigma_data = 1.0
|
||||
final_sigmas_type = "sigma_min"
|
||||
|
||||
if self.stage.scheduler is not None:
|
||||
self.stage.scheduler.register_to_config(
|
||||
sigma_max=sigma_max,
|
||||
sigma_min=sigma_min,
|
||||
sigma_data=sigma_data,
|
||||
final_sigmas_type=final_sigmas_type,
|
||||
)
|
||||
|
||||
self.stage.scheduler.set_timesteps(num_inference_steps,
|
||||
device=latents.device)
|
||||
timesteps = self.stage.scheduler.timesteps
|
||||
|
||||
if (hasattr(self.stage.scheduler.config, "final_sigmas_type")
|
||||
and self.stage.scheduler.config.final_sigmas_type == "sigma_min"
|
||||
and len(self.stage.scheduler.sigmas) > 1):
|
||||
self.stage.scheduler.sigmas[-1] = self.stage.scheduler.sigmas[-2]
|
||||
|
||||
conditioning_latents = getattr(batch, "conditioning_latents", None)
|
||||
unconditioning_latents = conditioning_latents
|
||||
|
||||
progress_bar = self.stage.progress_bar(total=num_inference_steps)
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"fastvideo_args": fastvideo_args,
|
||||
"extra_step_kwargs": extra_step_kwargs,
|
||||
"target_dtype": target_dtype,
|
||||
"autocast_enabled": autocast_enabled,
|
||||
"progress_bar": progress_bar,
|
||||
"conditioning_latents": conditioning_latents,
|
||||
"unconditioning_latents": unconditioning_latents,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=num_inference_steps,
|
||||
prompt_embeds=batch.prompt_embeds,
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=batch.prompt_attention_mask,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=batch.image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
state.extra["step_idx"] = step_idx
|
||||
return ModelInputs(
|
||||
latent_model_input=state.latents,
|
||||
timestep=t,
|
||||
prompt_embeds=state.prompt_embeds,
|
||||
prompt_attention_mask=state.prompt_attention_mask,
|
||||
)
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
batch = state.extra["batch"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
conditioning_latents = state.extra["conditioning_latents"]
|
||||
unconditioning_latents = state.extra["unconditioning_latents"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
|
||||
if getattr(self.stage, "interrupt", False):
|
||||
return state.latents
|
||||
|
||||
current_sigma = self.stage.scheduler.sigmas[step_idx]
|
||||
current_t = current_sigma / (current_sigma + 1)
|
||||
c_in = 1 - current_t
|
||||
c_skip = 1 - current_t
|
||||
c_out = -current_t
|
||||
|
||||
timestep = current_t.view(1, 1, 1, 1,
|
||||
1).expand(state.latents.size(0), -1,
|
||||
state.latents.size(2), -1, -1)
|
||||
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
cond_latent = state.latents * c_in
|
||||
|
||||
if (hasattr(batch, "cond_indicator")
|
||||
and batch.cond_indicator is not None
|
||||
and conditioning_latents is not None):
|
||||
cond_latent = (batch.cond_indicator * conditioning_latents +
|
||||
(1 - batch.cond_indicator) * cond_latent)
|
||||
else:
|
||||
logger.warning(
|
||||
"Step %s: Missing conditioning data - "
|
||||
"cond_indicator: %s, conditioning_latents: %s", step_idx,
|
||||
hasattr(batch, "cond_indicator"), conditioning_latents
|
||||
is not None)
|
||||
|
||||
cond_latent = cond_latent.to(target_dtype)
|
||||
|
||||
cond_timestep = timestep
|
||||
if hasattr(batch,
|
||||
"cond_indicator") and batch.cond_indicator is not None:
|
||||
sigma_conditioning = 0.0001
|
||||
t_conditioning = sigma_conditioning / (sigma_conditioning + 1)
|
||||
cond_timestep = (batch.cond_indicator * t_conditioning +
|
||||
(1 - batch.cond_indicator) * timestep)
|
||||
cond_timestep = cond_timestep.to(target_dtype)
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
):
|
||||
condition_mask = (batch.cond_mask.to(target_dtype) if hasattr(
|
||||
batch, "cond_mask") else None)
|
||||
padding_mask = torch.zeros(1,
|
||||
1,
|
||||
batch.height,
|
||||
batch.width,
|
||||
device=cond_latent.device,
|
||||
dtype=target_dtype)
|
||||
|
||||
if condition_mask is None:
|
||||
batch_size, _, num_frames, height, width = cond_latent.shape
|
||||
condition_mask = torch.zeros(batch_size,
|
||||
1,
|
||||
num_frames,
|
||||
height,
|
||||
width,
|
||||
device=cond_latent.device,
|
||||
dtype=target_dtype)
|
||||
|
||||
noise_pred = self.stage.transformer(
|
||||
hidden_states=cond_latent,
|
||||
timestep=cond_timestep.to(target_dtype),
|
||||
encoder_hidden_states=batch.prompt_embeds[0].to(
|
||||
target_dtype),
|
||||
fps=24,
|
||||
condition_mask=condition_mask,
|
||||
padding_mask=padding_mask,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
cond_pred = (c_skip * state.latents +
|
||||
c_out * noise_pred.float()).to(target_dtype)
|
||||
|
||||
if (hasattr(batch, "cond_indicator")
|
||||
and batch.cond_indicator is not None
|
||||
and conditioning_latents is not None):
|
||||
cond_pred = (batch.cond_indicator * conditioning_latents +
|
||||
(1 - batch.cond_indicator) * cond_pred)
|
||||
|
||||
if (state.do_cfg and batch.negative_prompt_embeds is not None):
|
||||
uncond_latent = state.latents * c_in
|
||||
|
||||
if (hasattr(batch, "uncond_indicator")
|
||||
and batch.uncond_indicator is not None
|
||||
and unconditioning_latents is not None):
|
||||
uncond_latent = (
|
||||
batch.uncond_indicator * unconditioning_latents +
|
||||
(1 - batch.uncond_indicator) * uncond_latent)
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
):
|
||||
uncond_condition_mask = (
|
||||
batch.uncond_mask.to(target_dtype) if
|
||||
(hasattr(batch, "uncond_mask")
|
||||
and batch.uncond_mask is not None) else condition_mask)
|
||||
|
||||
uncond_timestep = timestep
|
||||
if (hasattr(batch, "uncond_indicator")
|
||||
and batch.uncond_indicator is not None):
|
||||
sigma_conditioning = 0.0001
|
||||
t_conditioning = sigma_conditioning / (
|
||||
sigma_conditioning + 1)
|
||||
uncond_timestep = (
|
||||
batch.uncond_indicator * t_conditioning +
|
||||
(1 - batch.uncond_indicator) * timestep)
|
||||
uncond_timestep = uncond_timestep.to(target_dtype)
|
||||
|
||||
noise_pred_uncond = self.stage.transformer(
|
||||
hidden_states=uncond_latent.to(target_dtype),
|
||||
timestep=uncond_timestep.to(target_dtype),
|
||||
encoder_hidden_states=batch.negative_prompt_embeds[0].
|
||||
to(target_dtype),
|
||||
fps=24,
|
||||
condition_mask=uncond_condition_mask,
|
||||
padding_mask=padding_mask,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
uncond_pred = (
|
||||
c_skip * state.latents +
|
||||
c_out * noise_pred_uncond.float()).to(target_dtype)
|
||||
|
||||
if (hasattr(batch, "uncond_indicator")
|
||||
and batch.uncond_indicator is not None
|
||||
and unconditioning_latents is not None):
|
||||
uncond_pred = (
|
||||
batch.uncond_indicator * unconditioning_latents +
|
||||
(1 - batch.uncond_indicator) * uncond_pred)
|
||||
|
||||
guidance_diff = cond_pred - uncond_pred
|
||||
final_pred = cond_pred + state.guidance_scale * guidance_diff
|
||||
else:
|
||||
final_pred = cond_pred
|
||||
|
||||
if current_sigma > 1e-8:
|
||||
noise_for_scheduler = (state.latents - final_pred) / current_sigma
|
||||
else:
|
||||
logger.warning(
|
||||
"Step %s: current_sigma too small (%s), using final_pred directly",
|
||||
step_idx, current_sigma)
|
||||
noise_for_scheduler = final_pred
|
||||
|
||||
if torch.isnan(noise_for_scheduler).sum() > 0:
|
||||
logger.error(
|
||||
"Step %s: NaN detected in noise_for_scheduler, sum: %s",
|
||||
step_idx,
|
||||
noise_for_scheduler.float().sum().item())
|
||||
logger.error(
|
||||
"Step %s: latents sum: %s, final_pred sum: %s, current_sigma: %s",
|
||||
step_idx,
|
||||
state.latents.float().sum().item(),
|
||||
final_pred.float().sum().item(), current_sigma)
|
||||
|
||||
return noise_for_scheduler
|
||||
|
||||
def cfg_combine(self, state: StrategyState,
|
||||
noise_pred: torch.Tensor) -> torch.Tensor:
|
||||
return noise_pred
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
latents = self.stage.scheduler.step(
|
||||
noise_pred,
|
||||
t,
|
||||
state.latents,
|
||||
**state.extra["extra_step_kwargs"],
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
|
||||
return latents
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
batch = state.extra["batch"]
|
||||
batch.latents = state.latents
|
||||
return batch
|
||||
@@ -0,0 +1,269 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
DMD denoising strategy (FlowMatch-based).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.utils import pred_noise_to_pred_video
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
DenoisingStrategy,
|
||||
ModelInputs,
|
||||
StrategyState,
|
||||
)
|
||||
from fastvideo.utils import dict_to_3d_list
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.sliding_tile_attn import (
|
||||
SlidingTileAttentionBackend)
|
||||
st_attn_available = True
|
||||
except ImportError:
|
||||
st_attn_available = False
|
||||
SlidingTileAttentionBackend = None # type: ignore
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.video_sparse_attn import (
|
||||
VideoSparseAttentionBackend)
|
||||
vsa_available = True
|
||||
except ImportError:
|
||||
vsa_available = False
|
||||
VideoSparseAttentionBackend = None # type: ignore
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class DmdStrategy(DenoisingStrategy):
|
||||
|
||||
def __init__(self, stage: Any) -> None:
|
||||
self.stage = stage
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
timesteps = batch.timesteps
|
||||
if timesteps is None:
|
||||
raise ValueError("Timesteps must be provided")
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
num_warmup_steps = len(
|
||||
timesteps) - num_inference_steps * self.stage.scheduler.order
|
||||
|
||||
image_embeds = batch.image_embeds
|
||||
if len(image_embeds) > 0:
|
||||
assert torch.isnan(image_embeds[0]).sum() == 0
|
||||
image_embeds = [
|
||||
image_embed.to(target_dtype) for image_embed in image_embeds
|
||||
]
|
||||
|
||||
image_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_image": image_embeds,
|
||||
"mask_strategy": dict_to_3d_list(
|
||||
None, t_max=50, l_max=60, h_max=24)
|
||||
},
|
||||
)
|
||||
|
||||
pos_cond_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_2": batch.clip_embedding_pos,
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
if (st_attn_available
|
||||
and self.stage.attn_backend == SlidingTileAttentionBackend):
|
||||
self.stage.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
assert batch.latents is not None, "latents must be provided"
|
||||
latents = batch.latents
|
||||
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert not torch.isnan(
|
||||
prompt_embeds[0]).any(), "prompt_embeds contains nan"
|
||||
|
||||
timesteps = torch.tensor(
|
||||
fastvideo_args.pipeline_config.dmd_denoising_steps,
|
||||
dtype=torch.long,
|
||||
device=get_local_torch_device())
|
||||
|
||||
progress_bar = self.stage.progress_bar(total=len(timesteps))
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"fastvideo_args": fastvideo_args,
|
||||
"target_dtype": target_dtype,
|
||||
"autocast_enabled": autocast_enabled,
|
||||
"num_warmup_steps": num_warmup_steps,
|
||||
"image_kwargs": image_kwargs,
|
||||
"pos_cond_kwargs": pos_cond_kwargs,
|
||||
"progress_bar": progress_bar,
|
||||
"video_raw_latent_shape": latents.shape,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=num_inference_steps,
|
||||
prompt_embeds=prompt_embeds,
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=batch.prompt_attention_mask,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
state.extra["step_idx"] = step_idx
|
||||
return ModelInputs(
|
||||
latent_model_input=state.latents,
|
||||
timestep=t,
|
||||
prompt_embeds=state.prompt_embeds,
|
||||
prompt_attention_mask=state.prompt_attention_mask,
|
||||
)
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
batch = state.extra["batch"]
|
||||
fastvideo_args = state.extra["fastvideo_args"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
|
||||
if getattr(self.stage, "interrupt", False):
|
||||
state.extra["pred_latents"] = state.latents
|
||||
return state.latents
|
||||
|
||||
latents = state.latents
|
||||
noise_latents = latents.clone()
|
||||
latent_model_input = latents.to(target_dtype)
|
||||
|
||||
if batch.image_latent is not None:
|
||||
latent_model_input = torch.cat(
|
||||
[latent_model_input,
|
||||
batch.image_latent.permute(0, 2, 1, 3, 4)],
|
||||
dim=2).to(target_dtype)
|
||||
|
||||
t_expand = model_inputs.timestep.repeat(latent_model_input.shape[0])
|
||||
guidance_expand = None
|
||||
if fastvideo_args.pipeline_config.embedded_cfg_scale is not None:
|
||||
guidance_expand = (torch.tensor(
|
||||
[fastvideo_args.pipeline_config.embedded_cfg_scale] *
|
||||
latent_model_input.shape[0],
|
||||
dtype=torch.float32,
|
||||
device=get_local_torch_device(),
|
||||
).to(target_dtype) * 1000.0)
|
||||
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
if (vsa_available
|
||||
and self.stage.attn_backend == VideoSparseAttentionBackend):
|
||||
self.attn_metadata_builder_cls = (
|
||||
self.stage.attn_backend.get_builder_cls())
|
||||
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = (
|
||||
self.attn_metadata_builder_cls())
|
||||
attn_metadata = self.attn_metadata_builder.build( # type: ignore
|
||||
current_timestep=step_idx, # type: ignore
|
||||
raw_latent_shape=batch.
|
||||
raw_latent_shape[2:5], # type: ignore
|
||||
patch_size=fastvideo_args.pipeline_config.dit_config.
|
||||
patch_size, # type: ignore
|
||||
STA_param=batch.STA_param, # type: ignore
|
||||
VSA_sparsity=fastvideo_args.
|
||||
VSA_sparsity, # type: ignore
|
||||
device=get_local_torch_device(), # type: ignore
|
||||
)
|
||||
assert attn_metadata is not None, (
|
||||
"attn_metadata cannot be None")
|
||||
else:
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_metadata = None
|
||||
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch,
|
||||
):
|
||||
pred_noise = self.stage.transformer(
|
||||
latent_model_input.permute(0, 2, 1, 3, 4),
|
||||
state.prompt_embeds,
|
||||
t_expand,
|
||||
guidance=guidance_expand,
|
||||
**state.extra["image_kwargs"],
|
||||
**state.extra["pos_cond_kwargs"],
|
||||
).permute(0, 2, 1, 3, 4)
|
||||
|
||||
pred_video = pred_noise_to_pred_video(
|
||||
pred_noise=pred_noise.flatten(0, 1),
|
||||
noise_input_latent=noise_latents.flatten(0, 1),
|
||||
timestep=t_expand,
|
||||
scheduler=self.stage.scheduler).unflatten(0, pred_noise.shape[:2])
|
||||
|
||||
if step_idx < len(state.timesteps) - 1:
|
||||
next_timestep = state.timesteps[step_idx + 1] * torch.ones(
|
||||
[1], dtype=torch.long, device=pred_video.device)
|
||||
generator = batch.generator
|
||||
if isinstance(generator, list):
|
||||
generator = generator[0] if generator else None
|
||||
noise = torch.randn(state.extra["video_raw_latent_shape"],
|
||||
dtype=pred_video.dtype,
|
||||
generator=generator).to(self.stage.device)
|
||||
latents = self.stage.scheduler.add_noise(pred_video.flatten(0, 1),
|
||||
noise.flatten(0, 1),
|
||||
next_timestep).unflatten(
|
||||
0,
|
||||
pred_video.shape[:2])
|
||||
else:
|
||||
latents = pred_video
|
||||
|
||||
state.extra["pred_latents"] = latents
|
||||
return latents
|
||||
|
||||
def cfg_combine(self, state: StrategyState,
|
||||
noise_pred: torch.Tensor) -> torch.Tensor:
|
||||
return noise_pred
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
num_warmup_steps = state.extra["num_warmup_steps"]
|
||||
|
||||
if step_idx == len(state.timesteps) - 1 or (
|
||||
(step_idx + 1) > num_warmup_steps and
|
||||
(step_idx + 1) % self.stage.scheduler.order == 0
|
||||
and progress_bar is not None):
|
||||
progress_bar.update()
|
||||
|
||||
return state.extra.get("pred_latents", state.latents)
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
|
||||
batch = state.extra["batch"]
|
||||
latents = state.extra["pred_latents"].permute(0, 2, 1, 3, 4)
|
||||
batch.latents = latents
|
||||
return batch
|
||||
@@ -0,0 +1,177 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Scaffolding for a unified denoising engine with hook support.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Protocol, TYPE_CHECKING
|
||||
from collections.abc import Sequence
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.models.schedulers.adapter import SchedulerAdapter
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
BlockDenoisingStrategy,
|
||||
DenoisingStrategy,
|
||||
StrategyState,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
|
||||
|
||||
class EngineHook(Protocol):
|
||||
|
||||
def on_init(self, engine: DenoisingEngine, batch: ForwardBatch,
|
||||
args: FastVideoArgs) -> None:
|
||||
...
|
||||
|
||||
def pre_run(self, state: StrategyState) -> None:
|
||||
...
|
||||
|
||||
def pre_step(self, state: StrategyState, step_idx: int,
|
||||
t: torch.Tensor) -> None:
|
||||
...
|
||||
|
||||
def post_step(self, state: StrategyState, step_idx: int,
|
||||
t: torch.Tensor) -> None:
|
||||
...
|
||||
|
||||
def post_run(self, state: StrategyState, batch: ForwardBatch) -> None:
|
||||
...
|
||||
|
||||
|
||||
class BaseEngineHook:
|
||||
|
||||
def on_init(self, engine: DenoisingEngine, batch: ForwardBatch,
|
||||
args: FastVideoArgs) -> None:
|
||||
return None
|
||||
|
||||
def pre_run(self, state: StrategyState) -> None:
|
||||
return None
|
||||
|
||||
def pre_step(self, state: StrategyState, step_idx: int,
|
||||
t: torch.Tensor) -> None:
|
||||
return None
|
||||
|
||||
def post_step(self, state: StrategyState, step_idx: int,
|
||||
t: torch.Tensor) -> None:
|
||||
return None
|
||||
|
||||
def post_run(self, state: StrategyState, batch: ForwardBatch) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class GuidanceCache:
|
||||
|
||||
def __init__(self, maxsize: int = 8) -> None:
|
||||
self._build = lru_cache(maxsize=maxsize)(self._build_impl)
|
||||
|
||||
def _build_impl(self, batch_size: int, dtype: torch.dtype,
|
||||
device: torch.device, guidance_val: float) -> torch.Tensor:
|
||||
return (torch.full(
|
||||
(batch_size, ),
|
||||
guidance_val,
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
).to(dtype) * 1000.0)
|
||||
|
||||
def get(self, batch_size: int, dtype: torch.dtype, device: torch.device,
|
||||
guidance_val: float | None) -> torch.Tensor | None:
|
||||
if guidance_val is None:
|
||||
return None
|
||||
return self._build(batch_size, dtype, device, guidance_val)
|
||||
|
||||
|
||||
class DenoisingEngine:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
strategy: DenoisingStrategy,
|
||||
*,
|
||||
scheduler_adapter: SchedulerAdapter | None = None,
|
||||
hooks: Sequence[EngineHook] | None = None,
|
||||
) -> None:
|
||||
self.strategy = strategy
|
||||
self.scheduler_adapter = scheduler_adapter
|
||||
self.hooks = list(hooks) if hooks is not None else []
|
||||
|
||||
def run(self, batch: ForwardBatch, args: FastVideoArgs) -> ForwardBatch:
|
||||
for hook in self.hooks:
|
||||
hook.on_init(self, batch, args)
|
||||
|
||||
state = self.strategy.prepare(batch, args)
|
||||
if self.scheduler_adapter is not None:
|
||||
state.extra.setdefault("scheduler_adapter", self.scheduler_adapter)
|
||||
for hook in self.hooks:
|
||||
hook.pre_run(state)
|
||||
|
||||
if isinstance(self.strategy, BlockDenoisingStrategy):
|
||||
self.run_blocks(state)
|
||||
else:
|
||||
timesteps = state.timesteps
|
||||
for i, t in enumerate(timesteps):
|
||||
for hook in self.hooks:
|
||||
hook.pre_step(state, i, t)
|
||||
|
||||
model_inputs = self.strategy.make_model_inputs(state, t, i)
|
||||
noise_pred = self.strategy.forward(state, model_inputs)
|
||||
noise_pred = self.strategy.cfg_combine(state, noise_pred)
|
||||
state.latents = self.strategy.scheduler_step(
|
||||
state,
|
||||
noise_pred,
|
||||
t,
|
||||
)
|
||||
|
||||
for hook in self.hooks:
|
||||
hook.post_step(state, i, t)
|
||||
|
||||
for hook in self.hooks:
|
||||
hook.post_run(state, batch)
|
||||
|
||||
return self.strategy.postprocess(state)
|
||||
|
||||
def run_blocks(
|
||||
self,
|
||||
state: StrategyState,
|
||||
*,
|
||||
block_plan=None,
|
||||
start_block: int = 0,
|
||||
num_blocks: int | None = None,
|
||||
) -> None:
|
||||
if not isinstance(self.strategy, BlockDenoisingStrategy):
|
||||
raise TypeError("run_blocks requires a BlockDenoisingStrategy")
|
||||
|
||||
strategy = self.strategy
|
||||
if block_plan is None:
|
||||
block_plan = strategy.block_plan(state)
|
||||
|
||||
items = block_plan.items
|
||||
end_block = len(items)
|
||||
if num_blocks is not None:
|
||||
end_block = min(end_block, start_block + num_blocks)
|
||||
|
||||
for block_idx in range(start_block, end_block):
|
||||
block_item = items[block_idx]
|
||||
t_hook = self._block_hook_t(state, block_idx)
|
||||
for hook in self.hooks:
|
||||
hook.pre_step(state, block_idx, t_hook)
|
||||
block_ctx = strategy.init_block_context(
|
||||
state,
|
||||
block_item,
|
||||
block_idx,
|
||||
)
|
||||
strategy.process_block(state, block_ctx, block_item)
|
||||
strategy.update_context(state, block_ctx, block_item)
|
||||
for hook in self.hooks:
|
||||
hook.post_step(state, block_idx, t_hook)
|
||||
|
||||
def _block_hook_t(self, state: StrategyState,
|
||||
block_idx: int) -> torch.Tensor:
|
||||
timesteps = state.timesteps
|
||||
if timesteps.numel() > 0:
|
||||
return timesteps[0]
|
||||
return torch.tensor(block_idx, device=state.latents.device)
|
||||
@@ -0,0 +1,77 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Engine hook implementations for denoising runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_engine import DenoisingEngine
|
||||
from fastvideo.pipelines.stages.denoising_strategies import StrategyState
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class PerfLoggingHook:
|
||||
"""
|
||||
Record per-step and total denoising times into batch logging info.
|
||||
"""
|
||||
|
||||
def __init__(self, stage_name: str = "DenoisingEngine") -> None:
|
||||
self.stage_name = stage_name
|
||||
self._batch: ForwardBatch | None = None
|
||||
self._run_start = 0.0
|
||||
self._step_starts: dict[int, float] = {}
|
||||
self._step_times_ms: list[float] = []
|
||||
|
||||
def on_init(self, engine: DenoisingEngine, batch: ForwardBatch, args):
|
||||
self._batch = batch
|
||||
|
||||
def pre_run(self, state: StrategyState) -> None:
|
||||
self._run_start = time.perf_counter()
|
||||
self._step_starts.clear()
|
||||
self._step_times_ms.clear()
|
||||
|
||||
def pre_step(self, state: StrategyState, step_idx: int, t) -> None:
|
||||
self._step_starts[step_idx] = time.perf_counter()
|
||||
|
||||
def post_step(self, state: StrategyState, step_idx: int, t) -> None:
|
||||
start = self._step_starts.pop(step_idx, None)
|
||||
if start is None:
|
||||
return
|
||||
self._step_times_ms.append((time.perf_counter() - start) * 1000.0)
|
||||
|
||||
def post_run(self, state: StrategyState, batch: ForwardBatch) -> None:
|
||||
total_ms = (time.perf_counter() - self._run_start) * 1000.0
|
||||
target_batch = self._batch or batch
|
||||
if target_batch is None:
|
||||
return
|
||||
target_batch.logging_info.add_stage_metric(self.stage_name,
|
||||
"denoise_step_times_ms",
|
||||
list(self._step_times_ms))
|
||||
target_batch.logging_info.add_stage_metric(self.stage_name,
|
||||
"denoise_total_ms", total_ms)
|
||||
if not self._step_times_ms:
|
||||
return
|
||||
if not self._is_primary_rank():
|
||||
return
|
||||
mean_ms = sum(self._step_times_ms) / len(self._step_times_ms)
|
||||
logger.info(
|
||||
"[%s] denoise steps=%d total_ms=%.2f mean_step_ms=%.2f min=%.2f max=%.2f",
|
||||
self.stage_name,
|
||||
len(self._step_times_ms),
|
||||
total_ms,
|
||||
mean_ms,
|
||||
min(self._step_times_ms),
|
||||
max(self._step_times_ms),
|
||||
)
|
||||
|
||||
def _is_primary_rank(self) -> bool:
|
||||
try:
|
||||
from fastvideo.distributed import get_world_group
|
||||
return get_world_group().local_rank == 0
|
||||
except Exception:
|
||||
return True
|
||||
@@ -0,0 +1,551 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
LongCat denoising strategies (base, I2V, VC).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from tqdm import tqdm
|
||||
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.loader.component_loader import TransformerLoader
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
DenoisingStrategy,
|
||||
ModelInputs,
|
||||
StrategyState,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class _BaseLongCatStrategy(DenoisingStrategy):
|
||||
|
||||
def __init__(self, stage: Any) -> None:
|
||||
self.stage = stage
|
||||
|
||||
def _load_transformer(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
attach_pipeline: bool) -> None:
|
||||
if fastvideo_args.model_loaded["transformer"]:
|
||||
return
|
||||
loader = TransformerLoader()
|
||||
self.stage.transformer = loader.load(
|
||||
fastvideo_args.model_paths["transformer"], fastvideo_args)
|
||||
if attach_pipeline:
|
||||
pipeline = self.stage.pipeline() if self.stage.pipeline else None
|
||||
if pipeline:
|
||||
pipeline.add_module("transformer", self.stage.transformer)
|
||||
fastvideo_args.model_loaded["transformer"] = True
|
||||
|
||||
def _build_prompt_inputs(self, batch: ForwardBatch):
|
||||
prompt_embeds = batch.prompt_embeds[0]
|
||||
prompt_attention_mask = (batch.prompt_attention_mask[0]
|
||||
if batch.prompt_attention_mask else None)
|
||||
do_cfg = batch.do_classifier_free_guidance
|
||||
|
||||
if do_cfg:
|
||||
negative_prompt_embeds = batch.negative_prompt_embeds[0]
|
||||
negative_prompt_attention_mask = (batch.negative_attention_mask[0]
|
||||
if batch.negative_attention_mask
|
||||
else None)
|
||||
prompt_embeds_combined = torch.cat(
|
||||
[negative_prompt_embeds, prompt_embeds], dim=0)
|
||||
if prompt_attention_mask is not None:
|
||||
prompt_attention_mask_combined = torch.cat(
|
||||
[negative_prompt_attention_mask, prompt_attention_mask],
|
||||
dim=0)
|
||||
else:
|
||||
prompt_attention_mask_combined = None
|
||||
else:
|
||||
prompt_embeds_combined = prompt_embeds
|
||||
prompt_attention_mask_combined = prompt_attention_mask
|
||||
|
||||
return prompt_embeds, prompt_attention_mask, prompt_embeds_combined, \
|
||||
prompt_attention_mask_combined
|
||||
|
||||
def optimized_scale(self, positive_flat: torch.Tensor,
|
||||
negative_flat: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Calculate optimized scale from CFG-zero paper.
|
||||
|
||||
st_star = (v_cond^T * v_uncond) / ||v_uncond||^2
|
||||
"""
|
||||
dot_product = torch.sum(positive_flat * negative_flat,
|
||||
dim=1,
|
||||
keepdim=True)
|
||||
squared_norm = torch.sum(negative_flat**2, dim=1, keepdim=True) + 1e-8
|
||||
return dot_product / squared_norm
|
||||
|
||||
def cfg_combine(self, state: StrategyState,
|
||||
noise_pred: torch.Tensor) -> torch.Tensor:
|
||||
return noise_pred
|
||||
|
||||
|
||||
class LongCatStrategy(_BaseLongCatStrategy):
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
self._load_transformer(batch, fastvideo_args, attach_pipeline=True)
|
||||
|
||||
if hasattr(self.stage.transformer, "module"):
|
||||
transformer_dtype = next(
|
||||
self.stage.transformer.module.parameters()).dtype
|
||||
else:
|
||||
transformer_dtype = next(self.stage.transformer.parameters()).dtype
|
||||
|
||||
target_dtype = transformer_dtype
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
prompt_embeds, prompt_attention_mask, prompt_embeds_combined, \
|
||||
prompt_attention_mask_combined = self._build_prompt_inputs(batch)
|
||||
|
||||
timesteps = batch.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
progress_bar = tqdm(total=num_inference_steps, desc="LongCat Denoising")
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"target_dtype": target_dtype,
|
||||
"autocast_enabled": autocast_enabled,
|
||||
"prompt_embeds_combined": prompt_embeds_combined,
|
||||
"prompt_attention_mask_combined": prompt_attention_mask_combined,
|
||||
"progress_bar": progress_bar,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=batch.latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=num_inference_steps,
|
||||
prompt_embeds=[prompt_embeds],
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=[prompt_attention_mask]
|
||||
if prompt_attention_mask is not None else None,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=batch.image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
latents = state.latents
|
||||
|
||||
if state.do_cfg:
|
||||
latent_model_input = torch.cat([latents] * 2)
|
||||
else:
|
||||
latent_model_input = latents
|
||||
|
||||
latent_model_input = latent_model_input.to(target_dtype)
|
||||
timestep = t.expand(latent_model_input.shape[0]).to(target_dtype)
|
||||
|
||||
state.extra["step_idx"] = step_idx
|
||||
return ModelInputs(
|
||||
latent_model_input=latent_model_input,
|
||||
timestep=timestep,
|
||||
prompt_embeds=state.prompt_embeds,
|
||||
prompt_attention_mask=state.prompt_attention_mask,
|
||||
)
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
batch = state.extra["batch"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
prompt_embeds_combined = state.extra["prompt_embeds_combined"]
|
||||
prompt_attention_mask_combined = state.extra[
|
||||
"prompt_attention_mask_combined"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
|
||||
batch.is_cfg_negative = False
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
), torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
noise_pred = self.stage.transformer(
|
||||
hidden_states=model_inputs.latent_model_input,
|
||||
encoder_hidden_states=prompt_embeds_combined,
|
||||
timestep=model_inputs.timestep,
|
||||
encoder_attention_mask=prompt_attention_mask_combined,
|
||||
)
|
||||
|
||||
if state.do_cfg:
|
||||
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
|
||||
B = noise_pred_cond.shape[0]
|
||||
positive = noise_pred_cond.reshape(B, -1)
|
||||
negative = noise_pred_uncond.reshape(B, -1)
|
||||
st_star = self.optimized_scale(positive, negative)
|
||||
st_star = st_star.view(B, 1, 1, 1, 1)
|
||||
noise_pred = (noise_pred_uncond * st_star + state.guidance_scale *
|
||||
(noise_pred_cond - noise_pred_uncond * st_star))
|
||||
|
||||
noise_pred = -noise_pred
|
||||
return noise_pred
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
latents = self.stage.scheduler.step(noise_pred,
|
||||
t,
|
||||
state.latents,
|
||||
return_dict=False)[0]
|
||||
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
return latents
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
batch = state.extra["batch"]
|
||||
batch.latents = state.latents
|
||||
return batch
|
||||
|
||||
|
||||
class LongCatI2VStrategy(_BaseLongCatStrategy):
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
self._load_transformer(batch, fastvideo_args, attach_pipeline=False)
|
||||
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
prompt_embeds, prompt_attention_mask, prompt_embeds_combined, \
|
||||
prompt_attention_mask_combined = self._build_prompt_inputs(batch)
|
||||
|
||||
timesteps = batch.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
progress_bar = tqdm(total=num_inference_steps, desc="I2V Denoising")
|
||||
|
||||
num_cond_latents = getattr(batch, "num_cond_latents", 0)
|
||||
if num_cond_latents > 0:
|
||||
logger.info("I2V Denoising: num_cond_latents=%s, latent_shape=%s",
|
||||
num_cond_latents, batch.latents.shape)
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"target_dtype": target_dtype,
|
||||
"autocast_enabled": autocast_enabled,
|
||||
"prompt_embeds_combined": prompt_embeds_combined,
|
||||
"prompt_attention_mask_combined": prompt_attention_mask_combined,
|
||||
"num_cond_latents": num_cond_latents,
|
||||
"progress_bar": progress_bar,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=batch.latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=num_inference_steps,
|
||||
prompt_embeds=[prompt_embeds],
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=[prompt_attention_mask]
|
||||
if prompt_attention_mask is not None else None,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=batch.image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
num_cond_latents = state.extra["num_cond_latents"]
|
||||
latents = state.latents
|
||||
|
||||
if state.do_cfg:
|
||||
latent_model_input = torch.cat([latents] * 2)
|
||||
else:
|
||||
latent_model_input = latents
|
||||
|
||||
latent_model_input = latent_model_input.to(target_dtype)
|
||||
timestep = t.expand(latent_model_input.shape[0]).to(target_dtype)
|
||||
timestep = timestep.unsqueeze(-1).repeat(1, latent_model_input.shape[2])
|
||||
if num_cond_latents > 0:
|
||||
timestep[:, :num_cond_latents] = 0
|
||||
|
||||
state.extra["step_idx"] = step_idx
|
||||
return ModelInputs(
|
||||
latent_model_input=latent_model_input,
|
||||
timestep=timestep,
|
||||
prompt_embeds=state.prompt_embeds,
|
||||
prompt_attention_mask=state.prompt_attention_mask,
|
||||
extra_kwargs={"num_cond_latents": num_cond_latents},
|
||||
)
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
batch = state.extra["batch"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
prompt_embeds_combined = state.extra["prompt_embeds_combined"]
|
||||
prompt_attention_mask_combined = state.extra[
|
||||
"prompt_attention_mask_combined"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
num_cond_latents = state.extra["num_cond_latents"]
|
||||
|
||||
batch.is_cfg_negative = False
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
), torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
noise_pred = self.stage.transformer(
|
||||
hidden_states=model_inputs.latent_model_input,
|
||||
encoder_hidden_states=prompt_embeds_combined,
|
||||
timestep=model_inputs.timestep,
|
||||
encoder_attention_mask=prompt_attention_mask_combined,
|
||||
num_cond_latents=num_cond_latents,
|
||||
)
|
||||
|
||||
if state.do_cfg:
|
||||
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
|
||||
B = noise_pred_cond.shape[0]
|
||||
positive = noise_pred_cond.reshape(B, -1)
|
||||
negative = noise_pred_uncond.reshape(B, -1)
|
||||
st_star = self.optimized_scale(positive, negative)
|
||||
st_star = st_star.view(B, 1, 1, 1, 1)
|
||||
noise_pred = (noise_pred_uncond * st_star + state.guidance_scale *
|
||||
(noise_pred_cond - noise_pred_uncond * st_star))
|
||||
|
||||
noise_pred = -noise_pred
|
||||
return noise_pred
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
num_cond_latents = state.extra["num_cond_latents"]
|
||||
latents = state.latents
|
||||
if num_cond_latents > 0:
|
||||
latents[:, :, num_cond_latents:] = self.stage.scheduler.step(
|
||||
noise_pred[:, :, num_cond_latents:],
|
||||
t,
|
||||
latents[:, :, num_cond_latents:],
|
||||
return_dict=False)[0]
|
||||
else:
|
||||
latents = self.stage.scheduler.step(noise_pred,
|
||||
t,
|
||||
latents,
|
||||
return_dict=False)[0]
|
||||
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
return latents
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
batch = state.extra["batch"]
|
||||
batch.latents = state.latents
|
||||
return batch
|
||||
|
||||
|
||||
class LongCatVCStrategy(_BaseLongCatStrategy):
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
self._load_transformer(batch, fastvideo_args, attach_pipeline=False)
|
||||
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
prompt_embeds, prompt_attention_mask, prompt_embeds_combined, \
|
||||
prompt_attention_mask_combined = self._build_prompt_inputs(batch)
|
||||
|
||||
timesteps = batch.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
progress_bar = tqdm(total=num_inference_steps, desc="VC Denoising")
|
||||
|
||||
num_cond_latents = getattr(batch, "num_cond_latents", 0)
|
||||
use_kv_cache = getattr(batch, "use_kv_cache", False)
|
||||
kv_cache_dict = getattr(batch, "kv_cache_dict", {})
|
||||
|
||||
logger.info(
|
||||
"VC Denoising: num_cond_latents=%d, use_kv_cache=%s, latent_shape=%s",
|
||||
num_cond_latents, use_kv_cache, batch.latents.shape)
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"target_dtype": target_dtype,
|
||||
"autocast_enabled": autocast_enabled,
|
||||
"prompt_embeds_combined": prompt_embeds_combined,
|
||||
"prompt_attention_mask_combined": prompt_attention_mask_combined,
|
||||
"num_cond_latents": num_cond_latents,
|
||||
"use_kv_cache": use_kv_cache,
|
||||
"kv_cache_dict": kv_cache_dict,
|
||||
"progress_bar": progress_bar,
|
||||
"step_times": [],
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=batch.latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=num_inference_steps,
|
||||
prompt_embeds=[prompt_embeds],
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=[prompt_attention_mask]
|
||||
if prompt_attention_mask is not None else None,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=batch.image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
num_cond_latents = state.extra["num_cond_latents"]
|
||||
use_kv_cache = state.extra["use_kv_cache"]
|
||||
latents = state.latents
|
||||
|
||||
if state.do_cfg:
|
||||
latent_model_input = torch.cat([latents] * 2)
|
||||
else:
|
||||
latent_model_input = latents
|
||||
|
||||
latent_model_input = latent_model_input.to(target_dtype)
|
||||
timestep = t.expand(latent_model_input.shape[0]).to(target_dtype)
|
||||
timestep = timestep.unsqueeze(-1).repeat(1, latent_model_input.shape[2])
|
||||
if not use_kv_cache and num_cond_latents > 0:
|
||||
timestep[:, :num_cond_latents] = 0
|
||||
|
||||
state.extra["step_idx"] = step_idx
|
||||
state.extra["step_start"] = time.time()
|
||||
|
||||
extra_kwargs = {"num_cond_latents": num_cond_latents}
|
||||
if use_kv_cache:
|
||||
extra_kwargs["kv_cache_dict"] = state.extra["kv_cache_dict"]
|
||||
|
||||
return ModelInputs(
|
||||
latent_model_input=latent_model_input,
|
||||
timestep=timestep,
|
||||
prompt_embeds=state.prompt_embeds,
|
||||
prompt_attention_mask=state.prompt_attention_mask,
|
||||
extra_kwargs=extra_kwargs,
|
||||
)
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
batch = state.extra["batch"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
prompt_embeds_combined = state.extra["prompt_embeds_combined"]
|
||||
prompt_attention_mask_combined = state.extra[
|
||||
"prompt_attention_mask_combined"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
|
||||
batch.is_cfg_negative = False
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=None,
|
||||
forward_batch=batch,
|
||||
), torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
noise_pred = self.stage.transformer(
|
||||
hidden_states=model_inputs.latent_model_input,
|
||||
encoder_hidden_states=prompt_embeds_combined,
|
||||
timestep=model_inputs.timestep,
|
||||
encoder_attention_mask=prompt_attention_mask_combined,
|
||||
**model_inputs.extra_kwargs,
|
||||
)
|
||||
|
||||
if state.do_cfg:
|
||||
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
|
||||
B = noise_pred_cond.shape[0]
|
||||
positive = noise_pred_cond.reshape(B, -1)
|
||||
negative = noise_pred_uncond.reshape(B, -1)
|
||||
st_star = self.optimized_scale(positive, negative)
|
||||
st_star = st_star.view(B, 1, 1, 1, 1)
|
||||
noise_pred = (noise_pred_uncond * st_star + state.guidance_scale *
|
||||
(noise_pred_cond - noise_pred_uncond * st_star))
|
||||
|
||||
noise_pred = -noise_pred
|
||||
return noise_pred
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
num_cond_latents = state.extra["num_cond_latents"]
|
||||
use_kv_cache = state.extra["use_kv_cache"]
|
||||
latents = state.latents
|
||||
|
||||
if use_kv_cache:
|
||||
latents = self.stage.scheduler.step(noise_pred,
|
||||
t,
|
||||
latents,
|
||||
return_dict=False)[0]
|
||||
else:
|
||||
if num_cond_latents > 0:
|
||||
latents[:, :, num_cond_latents:] = self.stage.scheduler.step(
|
||||
noise_pred[:, :, num_cond_latents:],
|
||||
t,
|
||||
latents[:, :, num_cond_latents:],
|
||||
return_dict=False,
|
||||
)[0]
|
||||
else:
|
||||
latents = self.stage.scheduler.step(noise_pred,
|
||||
t,
|
||||
latents,
|
||||
return_dict=False)[0]
|
||||
|
||||
step_time = time.time() - state.extra["step_start"]
|
||||
state.extra["step_times"].append(step_time)
|
||||
if state.extra["step_idx"] < 3:
|
||||
logger.info("Step %d: %.2fs", state.extra["step_idx"], step_time)
|
||||
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
if progress_bar is not None:
|
||||
progress_bar.update()
|
||||
|
||||
return latents
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
|
||||
batch = state.extra["batch"]
|
||||
use_kv_cache = state.extra["use_kv_cache"]
|
||||
step_times = state.extra["step_times"]
|
||||
|
||||
latents = state.latents
|
||||
if use_kv_cache and hasattr(
|
||||
batch, "cond_latents") and batch.cond_latents is not None:
|
||||
latents = torch.cat([batch.cond_latents, latents], dim=2)
|
||||
logger.info(
|
||||
"Concatenated conditioning latents back, final shape: %s",
|
||||
latents.shape)
|
||||
|
||||
if step_times:
|
||||
avg_time = sum(step_times) / len(step_times)
|
||||
logger.info("Average step time: %.2fs (total: %.1fs)", avg_time,
|
||||
sum(step_times))
|
||||
|
||||
batch.latents = latents
|
||||
return batch
|
||||
@@ -0,0 +1,324 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
MatrixGame causal block denoising strategy.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
BlockContext,
|
||||
BlockDenoisingStrategy,
|
||||
BlockPlan,
|
||||
BlockPlanItem,
|
||||
ModelInputs,
|
||||
StrategyState,
|
||||
)
|
||||
from fastvideo.pipelines.stages.matrixgame_denoising import BlockProcessingContext
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.sliding_tile_attn import (
|
||||
SlidingTileAttentionBackend)
|
||||
st_attn_available = True
|
||||
except ImportError:
|
||||
st_attn_available = False
|
||||
SlidingTileAttentionBackend = None # type: ignore
|
||||
|
||||
|
||||
class MatrixGameBlockStrategy(BlockDenoisingStrategy):
|
||||
|
||||
def __init__(self, stage: Any) -> None:
|
||||
self.stage = stage
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
latents = batch.latents
|
||||
if latents is None:
|
||||
raise ValueError("latents must be provided")
|
||||
|
||||
latent_seq_length = latents.shape[-1] * latents.shape[-2]
|
||||
patch_size = self.stage.transformer.patch_size
|
||||
patch_ratio = patch_size[-1] * patch_size[-2]
|
||||
self.stage.frame_seq_length = latent_seq_length // patch_ratio
|
||||
|
||||
timesteps = torch.tensor(
|
||||
fastvideo_args.pipeline_config.dmd_denoising_steps,
|
||||
dtype=torch.long).cpu()
|
||||
if getattr(fastvideo_args.pipeline_config, "warp_denoising_step",
|
||||
False):
|
||||
scheduler_timesteps = torch.cat((
|
||||
self.stage.scheduler.timesteps.cpu(),
|
||||
torch.tensor([0], dtype=torch.float32),
|
||||
))
|
||||
timesteps = scheduler_timesteps[1000 - timesteps]
|
||||
timesteps = timesteps.to(get_local_torch_device())
|
||||
|
||||
boundary_ratio = getattr(fastvideo_args.pipeline_config.dit_config,
|
||||
"boundary_ratio", None)
|
||||
if boundary_ratio is not None:
|
||||
boundary_timestep = (boundary_ratio *
|
||||
self.stage.scheduler.num_train_timesteps)
|
||||
high_noise_timesteps = timesteps[timesteps >= boundary_timestep]
|
||||
else:
|
||||
boundary_timestep = None
|
||||
high_noise_timesteps = None
|
||||
|
||||
image_embeds = batch.image_embeds
|
||||
if len(image_embeds) > 0:
|
||||
assert torch.isnan(image_embeds[0]).sum() == 0
|
||||
image_embeds = [
|
||||
image_embed.to(target_dtype) for image_embed in image_embeds
|
||||
]
|
||||
|
||||
image_kwargs = {"encoder_hidden_states_image": image_embeds}
|
||||
pos_cond_kwargs: dict[str, Any] = {}
|
||||
|
||||
if (st_attn_available
|
||||
and self.stage.attn_backend == SlidingTileAttentionBackend):
|
||||
self.stage.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert torch.isnan(prompt_embeds[0]).sum() == 0
|
||||
|
||||
kv_cache1 = self.stage._initialize_kv_cache(batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device)
|
||||
kv_cache2 = None
|
||||
if boundary_timestep is not None:
|
||||
kv_cache2 = self.stage._initialize_kv_cache(
|
||||
batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
|
||||
kv_cache_mouse = None
|
||||
kv_cache_keyboard = None
|
||||
if self.stage.use_action_module:
|
||||
kv_cache_mouse, kv_cache_keyboard = (
|
||||
self.stage._initialize_action_kv_cache(
|
||||
batch_size=latents.shape[0],
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
))
|
||||
|
||||
crossattn_cache = self.stage._initialize_crossattn_cache(
|
||||
batch_size=latents.shape[0],
|
||||
max_text_len=257,
|
||||
dtype=target_dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
|
||||
num_frames = latents.shape[2]
|
||||
if num_frames % self.stage.num_frame_per_block != 0:
|
||||
raise ValueError(
|
||||
"num_frames must be divisible by num_frame_per_block for "
|
||||
"causal denoising")
|
||||
num_blocks = num_frames // self.stage.num_frame_per_block
|
||||
block_sizes = [self.stage.num_frame_per_block] * num_blocks
|
||||
start_index = 0
|
||||
|
||||
if boundary_timestep is not None:
|
||||
block_sizes[0] = 1
|
||||
|
||||
ctx = BlockProcessingContext(
|
||||
batch=batch,
|
||||
block_idx=0,
|
||||
start_index=0,
|
||||
kv_cache1=kv_cache1,
|
||||
kv_cache2=kv_cache2,
|
||||
kv_cache_mouse=kv_cache_mouse,
|
||||
kv_cache_keyboard=kv_cache_keyboard,
|
||||
crossattn_cache=crossattn_cache,
|
||||
timesteps=timesteps,
|
||||
block_sizes=block_sizes,
|
||||
noise_pool=None,
|
||||
fastvideo_args=fastvideo_args,
|
||||
target_dtype=target_dtype,
|
||||
autocast_enabled=autocast_enabled,
|
||||
boundary_timestep=boundary_timestep,
|
||||
high_noise_timesteps=high_noise_timesteps,
|
||||
context_noise=getattr(fastvideo_args.pipeline_config,
|
||||
"context_noise", 0),
|
||||
image_kwargs=image_kwargs,
|
||||
pos_cond_kwargs=pos_cond_kwargs,
|
||||
)
|
||||
|
||||
progress_bar = self.stage.progress_bar(total=len(block_sizes) *
|
||||
len(timesteps))
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"fastvideo_args": fastvideo_args,
|
||||
"ctx": ctx,
|
||||
"block_sizes": block_sizes,
|
||||
"start_index": start_index,
|
||||
"progress_bar": progress_bar,
|
||||
"boundary_timestep": boundary_timestep,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=len(timesteps),
|
||||
prompt_embeds=batch.prompt_embeds,
|
||||
negative_prompt_embeds=batch.negative_prompt_embeds,
|
||||
prompt_attention_mask=batch.prompt_attention_mask,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def block_plan(self, state: StrategyState) -> BlockPlan:
|
||||
block_sizes = state.extra["block_sizes"]
|
||||
start_index = state.extra["start_index"]
|
||||
items: list[BlockPlanItem] = []
|
||||
for block_size in block_sizes:
|
||||
items.append(
|
||||
BlockPlanItem(
|
||||
start_index=start_index,
|
||||
num_frames=block_size,
|
||||
use_kv_cache=True,
|
||||
model_selector="default",
|
||||
))
|
||||
start_index += block_size
|
||||
return BlockPlan(items=items)
|
||||
|
||||
def init_block_context(self, state: StrategyState,
|
||||
block_item: BlockPlanItem,
|
||||
block_idx: int) -> BlockContext:
|
||||
ctx = state.extra["ctx"]
|
||||
ctx.block_idx = block_idx
|
||||
ctx.start_index = block_item.start_index
|
||||
|
||||
action_kwargs = self.stage._prepare_action_kwargs(
|
||||
state.extra["batch"],
|
||||
block_item.start_index,
|
||||
block_item.num_frames,
|
||||
)
|
||||
|
||||
return BlockContext(
|
||||
kv_cache=ctx.kv_cache1,
|
||||
kv_cache_2=ctx.kv_cache2,
|
||||
crossattn_cache=ctx.crossattn_cache,
|
||||
action_cache=None,
|
||||
extra={
|
||||
"ctx": ctx,
|
||||
"action_kwargs": action_kwargs,
|
||||
"start_index": block_item.start_index,
|
||||
"num_frames": block_item.num_frames,
|
||||
},
|
||||
)
|
||||
|
||||
def process_block(self, state: StrategyState, block_ctx: BlockContext,
|
||||
block_item: BlockPlanItem) -> None:
|
||||
ctx = block_ctx.extra["ctx"]
|
||||
batch = state.extra["batch"]
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
|
||||
start_index = block_item.start_index
|
||||
current_num_frames = block_item.num_frames
|
||||
action_kwargs = block_ctx.extra["action_kwargs"]
|
||||
|
||||
current_latents = state.latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :]
|
||||
|
||||
noise_generator = None
|
||||
if ctx.noise_pool is not None:
|
||||
latents_device = state.latents.device
|
||||
|
||||
def noise_generator(shape: tuple, dtype: torch.dtype,
|
||||
step_idx: int) -> torch.Tensor:
|
||||
if step_idx < len(ctx.noise_pool):
|
||||
noise = ctx.noise_pool[step_idx]
|
||||
if noise.shape != shape:
|
||||
noise = noise[:, :shape[1], :, :, :]
|
||||
return noise.to(device=latents_device, dtype=dtype)
|
||||
|
||||
generator = batch.generator
|
||||
if isinstance(generator, list):
|
||||
generator = generator[0] if generator else None
|
||||
return torch.randn(shape, dtype=dtype,
|
||||
generator=generator).to(latents_device)
|
||||
|
||||
current_latents = self.stage._process_single_block(
|
||||
current_latents=current_latents,
|
||||
batch=batch,
|
||||
start_index=start_index,
|
||||
current_num_frames=current_num_frames,
|
||||
timesteps=state.timesteps,
|
||||
ctx=ctx,
|
||||
action_kwargs=action_kwargs,
|
||||
progress_bar=progress_bar,
|
||||
noise_generator=noise_generator,
|
||||
)
|
||||
|
||||
state.latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :] = (current_latents)
|
||||
|
||||
def update_context(self, state: StrategyState, block_ctx: BlockContext,
|
||||
block_item: BlockPlanItem) -> None:
|
||||
ctx = block_ctx.extra["ctx"]
|
||||
batch = state.extra["batch"]
|
||||
action_kwargs = block_ctx.extra["action_kwargs"]
|
||||
start_index = block_item.start_index
|
||||
current_num_frames = block_item.num_frames
|
||||
|
||||
current_latents = state.latents[:, :, start_index:start_index +
|
||||
current_num_frames, :, :]
|
||||
|
||||
self.stage._update_context_cache(
|
||||
current_latents=current_latents,
|
||||
batch=batch,
|
||||
start_index=start_index,
|
||||
current_num_frames=current_num_frames,
|
||||
ctx=ctx,
|
||||
action_kwargs=action_kwargs,
|
||||
context_noise=ctx.context_noise,
|
||||
)
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
|
||||
batch = state.extra["batch"]
|
||||
boundary_timestep = state.extra["boundary_timestep"]
|
||||
|
||||
latents = state.latents
|
||||
if boundary_timestep is not None:
|
||||
num_frames_to_remove = self.stage.num_frame_per_block - 1
|
||||
if num_frames_to_remove > 0:
|
||||
latents = latents[:, :, :-num_frames_to_remove, :, :]
|
||||
|
||||
batch.latents = latents
|
||||
return batch
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
raise NotImplementedError
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
raise NotImplementedError
|
||||
|
||||
def cfg_combine(self, state: StrategyState,
|
||||
noise_pred: torch.Tensor) -> torch.Tensor:
|
||||
raise NotImplementedError
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,552 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Standard denoising strategy backed by the legacy DenoisingStage utilities.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.configs.pipelines.base import STA_Mode
|
||||
from fastvideo.distributed import get_local_torch_device
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.models.loader.component_loader import TransformerLoader
|
||||
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
||||
from fastvideo.pipelines.stages.denoising_strategies import (
|
||||
DenoisingStrategy,
|
||||
ModelInputs,
|
||||
StrategyState,
|
||||
)
|
||||
from fastvideo.utils import dict_to_3d_list, masks_like
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.sliding_tile_attn import (
|
||||
SlidingTileAttentionBackend)
|
||||
st_attn_available = True
|
||||
except ImportError:
|
||||
st_attn_available = False
|
||||
SlidingTileAttentionBackend = None # type: ignore
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.vmoba import VMOBAAttentionBackend
|
||||
from fastvideo.utils import is_vmoba_available
|
||||
vmoba_attn_available = is_vmoba_available()
|
||||
except ImportError:
|
||||
vmoba_attn_available = False
|
||||
VMOBAAttentionBackend = None # type: ignore
|
||||
|
||||
try:
|
||||
from fastvideo.attention.backends.video_sparse_attn import (
|
||||
VideoSparseAttentionBackend)
|
||||
vsa_available = True
|
||||
except ImportError:
|
||||
vsa_available = False
|
||||
VideoSparseAttentionBackend = None # type: ignore
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class StandardStrategy(DenoisingStrategy):
|
||||
|
||||
def __init__(self, stage: Any) -> None:
|
||||
self.stage = stage
|
||||
|
||||
def prepare(self, batch: ForwardBatch,
|
||||
fastvideo_args: FastVideoArgs) -> StrategyState:
|
||||
pipeline = self.stage.pipeline() if self.stage.pipeline else None
|
||||
if not fastvideo_args.model_loaded["transformer"]:
|
||||
loader = TransformerLoader()
|
||||
self.stage.transformer = loader.load(
|
||||
fastvideo_args.model_paths["transformer"], fastvideo_args)
|
||||
if pipeline:
|
||||
pipeline.add_module("transformer", self.stage.transformer)
|
||||
fastvideo_args.model_loaded["transformer"] = True
|
||||
|
||||
extra_step_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.scheduler.step,
|
||||
{
|
||||
"generator": batch.generator,
|
||||
"eta": batch.eta
|
||||
},
|
||||
)
|
||||
|
||||
target_dtype = torch.bfloat16
|
||||
autocast_enabled = (target_dtype != torch.float32
|
||||
) and not fastvideo_args.disable_autocast
|
||||
|
||||
timesteps = batch.timesteps
|
||||
if timesteps is None:
|
||||
raise ValueError("Timesteps must be provided")
|
||||
num_inference_steps = batch.num_inference_steps
|
||||
num_warmup_steps = len(
|
||||
timesteps) - num_inference_steps * self.stage.scheduler.order
|
||||
|
||||
image_embeds = batch.image_embeds
|
||||
if len(image_embeds) > 0:
|
||||
assert not torch.isnan(
|
||||
image_embeds[0]).any(), "image_embeds contains nan"
|
||||
image_embeds = [
|
||||
image_embed.to(target_dtype) for image_embed in image_embeds
|
||||
]
|
||||
|
||||
image_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_image": image_embeds,
|
||||
"mask_strategy": dict_to_3d_list(
|
||||
None, t_max=50, l_max=60, h_max=24)
|
||||
},
|
||||
)
|
||||
|
||||
pos_cond_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_2": batch.clip_embedding_pos,
|
||||
"encoder_attention_mask": batch.prompt_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
neg_cond_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"encoder_hidden_states_2": batch.clip_embedding_neg,
|
||||
"encoder_attention_mask": batch.negative_attention_mask,
|
||||
},
|
||||
)
|
||||
|
||||
action_kwargs = self.stage.prepare_extra_func_kwargs(
|
||||
self.stage.transformer.forward,
|
||||
{
|
||||
"mouse_cond": batch.mouse_cond,
|
||||
"keyboard_cond": batch.keyboard_cond,
|
||||
},
|
||||
)
|
||||
|
||||
if (st_attn_available
|
||||
and self.stage.attn_backend == SlidingTileAttentionBackend):
|
||||
self.stage.prepare_sta_param(batch, fastvideo_args)
|
||||
|
||||
latents = batch.latents
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
assert not torch.isnan(
|
||||
prompt_embeds[0]).any(), "prompt_embeds contains nan"
|
||||
|
||||
neg_prompt_embeds = None
|
||||
if batch.do_classifier_free_guidance:
|
||||
neg_prompt_embeds = batch.negative_prompt_embeds
|
||||
assert neg_prompt_embeds is not None
|
||||
assert not torch.isnan(
|
||||
neg_prompt_embeds[0]).any(), "neg_prompt_embeds contains nan"
|
||||
|
||||
boundary_ratio = (
|
||||
fastvideo_args.pipeline_config.dit_config.boundary_ratio)
|
||||
if batch.boundary_ratio is not None:
|
||||
logger.info("Overriding boundary ratio from %s to %s",
|
||||
boundary_ratio, batch.boundary_ratio)
|
||||
boundary_ratio = batch.boundary_ratio
|
||||
if boundary_ratio is not None:
|
||||
boundary_timestep = (boundary_ratio *
|
||||
self.stage.scheduler.num_train_timesteps)
|
||||
else:
|
||||
boundary_timestep = None
|
||||
|
||||
latent_model_input = latents.to(target_dtype)
|
||||
if latent_model_input.ndim == 5:
|
||||
assert latent_model_input.shape[0] == 1, (
|
||||
"only support batch size 1")
|
||||
|
||||
ti2v_mask = None
|
||||
ti2v_z = None
|
||||
ti2v_seq_len = None
|
||||
if (fastvideo_args.pipeline_config.ti2v_task
|
||||
and batch.pil_image is not None):
|
||||
assert batch.image_latent is None, (
|
||||
"TI2V task should not have image latents")
|
||||
assert self.stage.vae is not None, (
|
||||
"VAE is not provided for TI2V task")
|
||||
z = self.stage.vae.encode(batch.pil_image).mean.float()
|
||||
if (hasattr(self.stage.vae, "shift_factor")
|
||||
and self.stage.vae.shift_factor is not None):
|
||||
if isinstance(self.stage.vae.shift_factor, torch.Tensor):
|
||||
z -= self.stage.vae.shift_factor.to(z.device, z.dtype)
|
||||
else:
|
||||
z -= self.stage.vae.shift_factor
|
||||
|
||||
if isinstance(self.stage.vae.scaling_factor, torch.Tensor):
|
||||
z = z * self.stage.vae.scaling_factor.to(z.device, z.dtype)
|
||||
else:
|
||||
z = z * self.stage.vae.scaling_factor
|
||||
|
||||
latent_model_input = latents.to(target_dtype).squeeze(0)
|
||||
_, mask2 = masks_like([latent_model_input], zero=True)
|
||||
latent_model_input = ((1. - mask2[0]) * z +
|
||||
mask2[0] * latent_model_input)
|
||||
latent_model_input = latent_model_input.to(get_local_torch_device())
|
||||
latents = latent_model_input
|
||||
F = batch.num_frames
|
||||
temporal_scale = (fastvideo_args.pipeline_config.vae_config.
|
||||
arch_config.scale_factor_temporal)
|
||||
spatial_scale = (fastvideo_args.pipeline_config.vae_config.
|
||||
arch_config.scale_factor_spatial)
|
||||
patch_size = (fastvideo_args.pipeline_config.dit_config.arch_config.
|
||||
patch_size)
|
||||
seq_len = ((F - 1) // temporal_scale +
|
||||
1) * (batch.height // spatial_scale) * (
|
||||
batch.width // spatial_scale) // (patch_size[1] *
|
||||
patch_size[2])
|
||||
ti2v_mask = mask2[0]
|
||||
ti2v_z = z
|
||||
ti2v_seq_len = seq_len
|
||||
|
||||
trajectory_timesteps: list[torch.Tensor] | None = None
|
||||
trajectory_latents: list[torch.Tensor] | None = None
|
||||
if batch.return_trajectory_latents:
|
||||
trajectory_timesteps = []
|
||||
trajectory_latents = []
|
||||
|
||||
progress_bar = self.stage.progress_bar(total=num_inference_steps)
|
||||
|
||||
extra: dict[str, Any] = {
|
||||
"batch": batch,
|
||||
"fastvideo_args": fastvideo_args,
|
||||
"extra_step_kwargs": extra_step_kwargs,
|
||||
"target_dtype": target_dtype,
|
||||
"autocast_enabled": autocast_enabled,
|
||||
"num_warmup_steps": num_warmup_steps,
|
||||
"image_kwargs": image_kwargs,
|
||||
"pos_cond_kwargs": pos_cond_kwargs,
|
||||
"neg_cond_kwargs": neg_cond_kwargs,
|
||||
"action_kwargs": action_kwargs,
|
||||
"boundary_timestep": boundary_timestep,
|
||||
"progress_bar": progress_bar,
|
||||
"trajectory_timesteps": trajectory_timesteps,
|
||||
"trajectory_latents": trajectory_latents,
|
||||
"ti2v_mask": ti2v_mask,
|
||||
"ti2v_z": ti2v_z,
|
||||
"ti2v_seq_len": ti2v_seq_len,
|
||||
}
|
||||
|
||||
return StrategyState(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
num_inference_steps=num_inference_steps,
|
||||
prompt_embeds=prompt_embeds,
|
||||
negative_prompt_embeds=neg_prompt_embeds,
|
||||
prompt_attention_mask=batch.prompt_attention_mask,
|
||||
negative_attention_mask=batch.negative_attention_mask,
|
||||
image_embeds=image_embeds,
|
||||
guidance_scale=batch.guidance_scale,
|
||||
guidance_scale_2=batch.guidance_scale_2,
|
||||
guidance_rescale=batch.guidance_rescale,
|
||||
do_cfg=batch.do_classifier_free_guidance,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def make_model_inputs(self, state: StrategyState, t: torch.Tensor,
|
||||
step_idx: int) -> ModelInputs:
|
||||
batch = state.extra["batch"]
|
||||
fastvideo_args = state.extra["fastvideo_args"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
boundary_timestep = state.extra["boundary_timestep"]
|
||||
|
||||
if getattr(self.stage, "interrupt", False):
|
||||
state.extra["skip_step"] = True
|
||||
else:
|
||||
state.extra["skip_step"] = False
|
||||
|
||||
if boundary_timestep is None or t >= boundary_timestep:
|
||||
if (fastvideo_args.dit_cpu_offload
|
||||
and not fastvideo_args.dit_layerwise_offload
|
||||
and self.stage.transformer_2 is not None
|
||||
and next(self.stage.transformer_2.parameters()).device.type
|
||||
== 'cuda'):
|
||||
self.stage.transformer_2.to('cpu')
|
||||
current_model = self.stage.transformer
|
||||
if (fastvideo_args.dit_cpu_offload
|
||||
and not fastvideo_args.dit_layerwise_offload
|
||||
and not fastvideo_args.use_fsdp_inference
|
||||
and current_model is not None):
|
||||
transformer_device = next(
|
||||
current_model.parameters()).device.type
|
||||
if transformer_device == 'cpu':
|
||||
current_model.to(get_local_torch_device())
|
||||
current_guidance_scale = batch.guidance_scale
|
||||
else:
|
||||
if (fastvideo_args.dit_cpu_offload
|
||||
and not fastvideo_args.dit_layerwise_offload
|
||||
and next(self.stage.transformer.parameters()).device.type
|
||||
== 'cuda'):
|
||||
self.stage.transformer.to('cpu')
|
||||
current_model = self.stage.transformer_2
|
||||
if (fastvideo_args.dit_cpu_offload
|
||||
and not fastvideo_args.dit_layerwise_offload
|
||||
and not fastvideo_args.use_fsdp_inference
|
||||
and current_model is not None):
|
||||
transformer_2_device = next(
|
||||
current_model.parameters()).device.type
|
||||
if transformer_2_device == 'cpu':
|
||||
current_model.to(get_local_torch_device())
|
||||
current_guidance_scale = batch.guidance_scale_2
|
||||
|
||||
assert current_model is not None, "current_model is None"
|
||||
state.extra["current_model"] = current_model
|
||||
state.extra["current_guidance_scale"] = current_guidance_scale
|
||||
state.extra["step_idx"] = step_idx
|
||||
|
||||
latent_model_input = state.latents.to(target_dtype)
|
||||
if batch.video_latent is not None:
|
||||
latent_model_input = torch.cat([
|
||||
latent_model_input, batch.video_latent,
|
||||
torch.zeros_like(state.latents)
|
||||
],
|
||||
dim=1).to(target_dtype)
|
||||
elif batch.image_latent is not None:
|
||||
assert not fastvideo_args.pipeline_config.ti2v_task, (
|
||||
"image latents should not be provided for TI2V task")
|
||||
latent_model_input = torch.cat(
|
||||
[latent_model_input, batch.image_latent],
|
||||
dim=1).to(target_dtype)
|
||||
|
||||
if (fastvideo_args.pipeline_config.ti2v_task
|
||||
and batch.pil_image is not None):
|
||||
timestep = torch.stack([t]).to(get_local_torch_device())
|
||||
mask2 = state.extra["ti2v_mask"]
|
||||
seq_len = state.extra["ti2v_seq_len"]
|
||||
temp_ts = (mask2[0][:, ::2, ::2] * timestep).flatten()
|
||||
temp_ts = torch.cat([
|
||||
temp_ts,
|
||||
temp_ts.new_ones(seq_len - temp_ts.size(0)) * timestep
|
||||
])
|
||||
timestep = temp_ts.unsqueeze(0)
|
||||
t_expand = timestep.repeat(latent_model_input.shape[0], 1)
|
||||
else:
|
||||
t_expand = t.repeat(latent_model_input.shape[0])
|
||||
|
||||
latent_model_input = self.stage.scheduler.scale_model_input(
|
||||
latent_model_input, t)
|
||||
|
||||
guidance_expand = None
|
||||
if fastvideo_args.pipeline_config.embedded_cfg_scale is not None:
|
||||
guidance_expand = (torch.tensor(
|
||||
[fastvideo_args.pipeline_config.embedded_cfg_scale] *
|
||||
latent_model_input.shape[0],
|
||||
dtype=torch.float32,
|
||||
device=get_local_torch_device(),
|
||||
).to(target_dtype) * 1000.0)
|
||||
|
||||
state.extra["guidance_expand"] = guidance_expand
|
||||
return ModelInputs(
|
||||
latent_model_input=latent_model_input,
|
||||
timestep=t_expand,
|
||||
prompt_embeds=state.prompt_embeds,
|
||||
prompt_attention_mask=state.prompt_attention_mask,
|
||||
)
|
||||
|
||||
def forward(self, state: StrategyState,
|
||||
model_inputs: ModelInputs) -> torch.Tensor:
|
||||
if state.extra.get("skip_step", False):
|
||||
return state.latents
|
||||
|
||||
batch = state.extra["batch"]
|
||||
fastvideo_args = state.extra["fastvideo_args"]
|
||||
target_dtype = state.extra["target_dtype"]
|
||||
autocast_enabled = state.extra["autocast_enabled"]
|
||||
current_model = state.extra["current_model"]
|
||||
current_guidance_scale = state.extra["current_guidance_scale"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
guidance_expand = state.extra["guidance_expand"]
|
||||
|
||||
image_kwargs = state.extra["image_kwargs"]
|
||||
pos_cond_kwargs = state.extra["pos_cond_kwargs"]
|
||||
neg_cond_kwargs = state.extra["neg_cond_kwargs"]
|
||||
action_kwargs = state.extra["action_kwargs"]
|
||||
|
||||
if ((st_attn_available
|
||||
and self.stage.attn_backend == SlidingTileAttentionBackend) or
|
||||
(vsa_available
|
||||
and self.stage.attn_backend == VideoSparseAttentionBackend)):
|
||||
self.attn_metadata_builder_cls = (
|
||||
self.stage.attn_backend.get_builder_cls())
|
||||
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = self.attn_metadata_builder_cls()
|
||||
attn_metadata = self.attn_metadata_builder.build( # type: ignore
|
||||
current_timestep=step_idx, # type: ignore
|
||||
raw_latent_shape=batch.
|
||||
raw_latent_shape[2:5], # type: ignore
|
||||
patch_size=fastvideo_args.pipeline_config.dit_config.
|
||||
patch_size, # type: ignore
|
||||
STA_param=batch.STA_param, # type: ignore
|
||||
VSA_sparsity=fastvideo_args.VSA_sparsity, # type: ignore
|
||||
device=get_local_torch_device(),
|
||||
)
|
||||
assert attn_metadata is not None, (
|
||||
"attn_metadata cannot be None")
|
||||
else:
|
||||
attn_metadata = None
|
||||
elif (vmoba_attn_available
|
||||
and self.stage.attn_backend == VMOBAAttentionBackend):
|
||||
self.attn_metadata_builder_cls = (
|
||||
self.stage.attn_backend.get_builder_cls())
|
||||
if self.attn_metadata_builder_cls is not None:
|
||||
self.attn_metadata_builder = self.attn_metadata_builder_cls()
|
||||
moba_params = fastvideo_args.moba_config.copy()
|
||||
moba_params.update({
|
||||
"current_timestep":
|
||||
step_idx,
|
||||
"raw_latent_shape":
|
||||
batch.raw_latent_shape[2:5],
|
||||
"patch_size":
|
||||
fastvideo_args.pipeline_config.dit_config.patch_size,
|
||||
"device":
|
||||
get_local_torch_device(),
|
||||
})
|
||||
attn_metadata = self.attn_metadata_builder.build(**moba_params)
|
||||
assert attn_metadata is not None, (
|
||||
"attn_metadata cannot be None")
|
||||
else:
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_metadata = None
|
||||
|
||||
with torch.autocast(device_type="cuda",
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled):
|
||||
batch.is_cfg_negative = False
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch,
|
||||
):
|
||||
noise_pred = current_model(
|
||||
model_inputs.latent_model_input,
|
||||
state.prompt_embeds,
|
||||
model_inputs.timestep,
|
||||
guidance=guidance_expand,
|
||||
**image_kwargs,
|
||||
**pos_cond_kwargs,
|
||||
**action_kwargs,
|
||||
)
|
||||
|
||||
if state.do_cfg:
|
||||
batch.is_cfg_negative = True
|
||||
with set_forward_context(
|
||||
current_timestep=step_idx,
|
||||
attn_metadata=attn_metadata,
|
||||
forward_batch=batch,
|
||||
):
|
||||
noise_pred_uncond = current_model(
|
||||
model_inputs.latent_model_input,
|
||||
state.negative_prompt_embeds,
|
||||
model_inputs.timestep,
|
||||
guidance=guidance_expand,
|
||||
**image_kwargs,
|
||||
**neg_cond_kwargs,
|
||||
**action_kwargs,
|
||||
)
|
||||
|
||||
noise_pred_text = noise_pred
|
||||
noise_pred = noise_pred_uncond + current_guidance_scale * (
|
||||
noise_pred_text - noise_pred_uncond)
|
||||
|
||||
if state.guidance_rescale > 0.0:
|
||||
noise_pred = self.stage.rescale_noise_cfg(
|
||||
noise_pred,
|
||||
noise_pred_text,
|
||||
guidance_rescale=state.guidance_rescale,
|
||||
)
|
||||
|
||||
return noise_pred
|
||||
|
||||
def cfg_combine(self, state: StrategyState,
|
||||
noise_pred: torch.Tensor) -> torch.Tensor:
|
||||
return noise_pred
|
||||
|
||||
def scheduler_step(self, state: StrategyState, noise_pred: torch.Tensor,
|
||||
t: torch.Tensor) -> torch.Tensor:
|
||||
if state.extra.get("skip_step", False):
|
||||
return state.latents
|
||||
|
||||
batch = state.extra["batch"]
|
||||
extra_step_kwargs = state.extra["extra_step_kwargs"]
|
||||
latents = self.stage.scheduler.step(
|
||||
noise_pred,
|
||||
t,
|
||||
state.latents,
|
||||
**extra_step_kwargs,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
if (state.extra["ti2v_mask"] is not None
|
||||
and batch.pil_image is not None):
|
||||
mask2 = state.extra["ti2v_mask"]
|
||||
z = state.extra["ti2v_z"]
|
||||
latents = latents.squeeze(0)
|
||||
latents = (1. - mask2) * z + mask2 * latents
|
||||
|
||||
if state.extra["trajectory_latents"] is not None:
|
||||
state.extra["trajectory_timesteps"].append(t)
|
||||
state.extra["trajectory_latents"].append(latents)
|
||||
|
||||
progress_bar = state.extra["progress_bar"]
|
||||
step_idx = state.extra["step_idx"]
|
||||
num_warmup_steps = state.extra["num_warmup_steps"]
|
||||
timesteps = state.timesteps
|
||||
if step_idx == len(timesteps) - 1 or (
|
||||
(step_idx + 1) > num_warmup_steps and
|
||||
(step_idx + 1) % self.stage.scheduler.order == 0
|
||||
and progress_bar is not None):
|
||||
progress_bar.update()
|
||||
|
||||
return latents
|
||||
|
||||
def postprocess(self, state: StrategyState) -> ForwardBatch:
|
||||
batch = state.extra["batch"]
|
||||
fastvideo_args = state.extra["fastvideo_args"]
|
||||
progress_bar = state.extra.get("progress_bar")
|
||||
|
||||
if state.extra["trajectory_latents"]:
|
||||
trajectory_tensor = torch.stack(state.extra["trajectory_latents"],
|
||||
dim=1)
|
||||
trajectory_timesteps_tensor = torch.stack(
|
||||
state.extra["trajectory_timesteps"], dim=0)
|
||||
batch.trajectory_timesteps = trajectory_timesteps_tensor.cpu()
|
||||
batch.trajectory_latents = trajectory_tensor.cpu()
|
||||
|
||||
batch.latents = state.latents
|
||||
|
||||
if fastvideo_args.dit_layerwise_offload:
|
||||
mgr = getattr(self.stage.transformer, "_layerwise_offload_manager",
|
||||
None)
|
||||
if mgr is not None and getattr(mgr, "enabled", False):
|
||||
mgr.release_all()
|
||||
if self.stage.transformer_2 is not None:
|
||||
mgr2 = getattr(self.stage.transformer_2,
|
||||
"_layerwise_offload_manager", None)
|
||||
if mgr2 is not None and getattr(mgr2, "enabled", False):
|
||||
mgr2.release_all()
|
||||
|
||||
if (st_attn_available
|
||||
and self.stage.attn_backend == SlidingTileAttentionBackend
|
||||
and fastvideo_args.STA_mode == STA_Mode.STA_SEARCHING):
|
||||
self.stage.save_sta_search_results(batch)
|
||||
|
||||
pipeline = self.stage.pipeline() if self.stage.pipeline else None
|
||||
if torch.backends.mps.is_available():
|
||||
logger.info("Memory before deallocating transformer: %s",
|
||||
torch.mps.current_allocated_memory())
|
||||
del self.stage.transformer
|
||||
if pipeline is not None and "transformer" in pipeline.modules:
|
||||
del pipeline.modules["transformer"]
|
||||
fastvideo_args.model_loaded["transformer"] = False
|
||||
logger.info("Memory after deallocating transformer: %s",
|
||||
torch.mps.current_allocated_memory())
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.close()
|
||||
|
||||
return batch
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user