[Diffusion] [Model] Dreamid omni (#1855)
Signed-off-by: dengyunyang <584797741@qq.com>
This commit is contained in:
1 parent
92f771d455
commit
3e8cb6c631
17 files changed
+1773
-9
No files matched your search
@@ -28,6 +28,7 @@ nav:
|
||||
- Text-To-Audio: user_guide/examples/offline_inference/text_to_audio.md
|
||||
- Text-To-Image: user_guide/examples/offline_inference/text_to_image.md
|
||||
- Text-To-Video: user_guide/examples/offline_inference/text_to_video.md
|
||||
- X-To-Video-Audio: user_guide/examples/offline_inference/x_to_video_audio.md
|
||||
- Online Serving:
|
||||
- BAGEL-7B-MoT: user_guide/examples/online_serving/bagel.md
|
||||
- vLLM-Omni Helm Chart: user_guide/examples/online_serving/chart-helm.md
|
||||
|
||||
@@ -51,6 +51,7 @@ th {
|
||||
|`MiMoAudioForConditionalGeneration` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` |
|
||||
|`Flux2Pipeline` | FLUX.2-dev | `black-forest-labs/FLUX.2-dev` |
|
||||
|`FishSpeechSlowARForConditionalGeneration` | Fish Speech S2 Pro | `fishaudio/s2-pro` |
|
||||
|`DreamIDOmniPipeline`| DreamID-Omni | `XuGuo699/DreamID-Omni` |
|
||||
|
||||
|
||||
## List of Supported Models for NPU
|
||||
|
||||
@@ -39,6 +39,7 @@ The following table shows which models are currently supported by parallelism me
|
||||
| **FLUX.1-dev** | `black-forest-labs/FLUX.1-dev` | ❌ | ❌ | ✅ | ✅ | ❌ | N/A |
|
||||
| **FLUX.2-dev** | `black-forest-labs/FLUX.2-dev` | ❌ | ❌ | ❌ | ✅ | ❌ | N/A |
|
||||
| **HunyuanImage3.0** | `tencent/HunyuanImage-3.0`, `tencent/HunyuanImage-3.0-Instruct` | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| **DreamID-Omni** | `XuGuo699/DreamID-Omni` | ❌ | ❌ | ✅ | ❌ | ❌ | N/A |
|
||||
|
||||
!!! note "TP Limitations for Diffusion Models"
|
||||
We currently implement Tensor Parallelism (TP) only for the DiT (Diffusion Transformer) blocks. This is because the `text_encoder` component in vLLM-Omni uses the original Transformers implementation, which does not yet support TP.
|
||||
|
||||
@@ -77,6 +77,7 @@ The following table shows which models are currently supported by each accelerat
|
||||
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|:------------:|:----:|:----:|
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **LTX-2** | `Lightricks/LTX-2` | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **DreamID-Omni** | `XuGuo699/DreamID-Omni` | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||
|
||||
### Quantization
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# X-To-Video-Audio
|
||||
|
||||
The `DreamID-Omni` pipeline generates short videos from text, image and video.
|
||||
|
||||
## Local CLI Usage
|
||||
### Download the Model locally
|
||||
Since DreamID-Omni combine multiple models, and without any config, so we need to download them locally.
|
||||
|
||||
```bash
|
||||
python download_dreamid_omni.py --output-dir ./dreamid_omni
|
||||
```
|
||||
After download, the model directory will look like this:
|
||||
|
||||
```
|
||||
dreamid_omni/
|
||||
├── DreamID-Omni/
|
||||
│ ├── dreamid_omni.safetensors
|
||||
├── MMAudio/
|
||||
│ ├── ext_weights/
|
||||
│ │ ├── best_netG.pt
|
||||
│ │ ├── v1-16.pth
|
||||
├── Wan2.2-TI2V-5B/
|
||||
│ ├── google/*
|
||||
│ ├── models_t5_umt5-xxl-enc-bf16.pth
|
||||
│ ├── Wan2.2_VAE.pth
|
||||
│
|
||||
├── model_index.json # create by download_dreamid_omni.py
|
||||
```
|
||||
|
||||
### Run the Inference
|
||||
```
|
||||
python x_to_video_audio.py \
|
||||
--model /xx/dreamid_omni \
|
||||
--prompt "Two people walking together and singing happily" \
|
||||
--image-path ./example0.png ./example1.png \
|
||||
--audio-path ./example0.wav ./example1.wav \
|
||||
--video-negative-prompt "jitter, bad hands, blur, distortion" \
|
||||
--audio-negative-prompt "robotic, muffled, echo, distorted" \
|
||||
--cfg-parallel-size 2 \
|
||||
--num-inference-steps 45 \
|
||||
--height 704 \
|
||||
--width 1280 \
|
||||
--output dreamid_omni.mp4
|
||||
```
|
||||
In the current test scenario (2 images + 2 audio inputs), the VRAM requirement is 72GB, regardless of whether cfg-parallel is enabled or disabled.
|
||||
The VRAM usage can be reduced by enabling CPU offload via --enable-cpu-offload.
|
||||
|
||||
Key arguments:
|
||||
- `--prompt`: text description (string).
|
||||
- `--model`: path to the model local directory.
|
||||
- `--height/--width`: output resolution (defaults 704 * 1024).
|
||||
- `--image-path`: path to the input image list.
|
||||
- `--audio-path`: path to the input audio list, indicate the timbre of the output video.
|
||||
- `--cfg-parallel-size`: number of parallel cfg parallel (defaults 1).
|
||||
- `--num-inference-steps`: number of denoising steps (defaults 45).
|
||||
- `--video-negative-prompt`: negative prompt for video generation.
|
||||
- `--audio-negative-prompt`: negative prompt for audio generation.
|
||||
- `--enable-cpu-offload`: enable CPU offload (defaults False).
|
||||
@@ -0,0 +1,103 @@
|
||||
import argparse
|
||||
import fcntl
|
||||
import os
|
||||
import site
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
DEPENDENCY_REPO = "https://github.com/bytedance/DreamID-V.git"
|
||||
DEPENDENCY_BRANCH = "omni"
|
||||
CACHE_DIR = Path(tempfile.gettempdir()) / "vllm-omni-dependency"
|
||||
LOCK_FILE = CACHE_DIR / ".install.lock"
|
||||
DEPENDENCY_DIR = CACHE_DIR / "DreamID-Omni"
|
||||
|
||||
|
||||
def download_dependency():
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(LOCK_FILE, "w") as f:
|
||||
fcntl.flock(f, fcntl.LOCK_EX)
|
||||
if not DEPENDENCY_DIR.exists():
|
||||
print(f"Downloading DreamID-Omni to {DEPENDENCY_DIR} ...")
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth", "1", DEPENDENCY_REPO, "--branch", DEPENDENCY_BRANCH, str(DEPENDENCY_DIR)],
|
||||
check=True,
|
||||
)
|
||||
print("Download finished.")
|
||||
fcntl.flock(f, fcntl.LOCK_UN)
|
||||
|
||||
# write .pth to site-packages
|
||||
site_packages = Path(site.getsitepackages()[0])
|
||||
pth_file = site_packages / "vllm_omni_dependency.pth"
|
||||
pth_file.write_text(str(DEPENDENCY_DIR))
|
||||
print(f"Added {DEPENDENCY_DIR} to site-packages via {pth_file}")
|
||||
|
||||
|
||||
def timed_download(repo_id: str, local_dir: str, allow_patterns: list | None = None):
|
||||
"""Download files from HF repo and log time + destination."""
|
||||
if os.path.exists(local_dir):
|
||||
print(f"Directory {local_dir} already exists. Skipping download.")
|
||||
return
|
||||
print(f"Starting download from {repo_id} into {local_dir}")
|
||||
start_time = time.time()
|
||||
|
||||
snapshot_download(
|
||||
repo_id=repo_id,
|
||||
local_dir=local_dir,
|
||||
local_dir_use_symlinks=False,
|
||||
allow_patterns=allow_patterns,
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"✅ Finished downloading {repo_id} in {elapsed:.2f} seconds. Files saved at: {local_dir}")
|
||||
|
||||
|
||||
def main(output_dir: str):
|
||||
# Wan2.2
|
||||
wan_dir = os.path.join(output_dir, "Wan2.2-TI2V-5B")
|
||||
timed_download(
|
||||
repo_id="Wan-AI/Wan2.2-TI2V-5B",
|
||||
local_dir=wan_dir,
|
||||
allow_patterns=["google/*", "models_t5_umt5-xxl-enc-bf16.pth", "Wan2.2_VAE.pth"],
|
||||
)
|
||||
|
||||
# MMAudio
|
||||
mm_audio_dir = os.path.join(output_dir, "MMAudio")
|
||||
timed_download(
|
||||
repo_id="hkchengrex/MMAudio",
|
||||
local_dir=mm_audio_dir,
|
||||
allow_patterns=["ext_weights/best_netG.pt", "ext_weights/v1-16.pth"],
|
||||
)
|
||||
|
||||
dreamid_dir = os.path.join(output_dir, "DreamID-Omni")
|
||||
|
||||
timed_download(repo_id="XuGuo699/DreamID-Omni", local_dir=dreamid_dir)
|
||||
|
||||
# Now we construct the config file
|
||||
import json
|
||||
|
||||
data = {
|
||||
"_class_name": "DreamIDOmniPipeline",
|
||||
"fusion": "DreamID-Omni/dreamid_omni.safetensors",
|
||||
}
|
||||
|
||||
with open(os.path.join(output_dir, "model_index.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
print(f"model_index.json created at {os.path.join(output_dir, 'model_index.json')}")
|
||||
|
||||
# now we download the dependency code
|
||||
download_dependency()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Download models from Hugging Face")
|
||||
parser.add_argument(
|
||||
"--output-dir", type=str, default="./dreamid_omni", help="Base directory to save downloaded models"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
main(args.output_dir)
|
||||
@@ -0,0 +1,58 @@
|
||||
# X-To-Video-Audio
|
||||
|
||||
The `DreamID-Omni` pipeline generates short videos from text, image and video.
|
||||
|
||||
## Local CLI Usage
|
||||
### Download the Model locally
|
||||
Since DreamID-Omni combine multiple models, and without any config, so we need to download them locally.
|
||||
|
||||
```bash
|
||||
python download_dreamid_omni.py --output-dir ./dreamid_omni
|
||||
```
|
||||
After download, the model directory will look like this:
|
||||
|
||||
```
|
||||
dreamid_omni/
|
||||
├── DreamID-Omni/
|
||||
│ ├── dreamid_omni.safetensors
|
||||
├── MMAudio/
|
||||
│ ├── ext_weights/
|
||||
│ │ ├── best_netG.pt
|
||||
│ │ ├── v1-16.pth
|
||||
├── Wan2.2-TI2V-5B/
|
||||
│ ├── google/*
|
||||
│ ├── models_t5_umt5-xxl-enc-bf16.pth
|
||||
│ ├── Wan2.2_VAE.pth
|
||||
│
|
||||
├── model_index.json # create by download_dreamid_omni.py
|
||||
```
|
||||
|
||||
### Run the Inference
|
||||
```
|
||||
python x_to_video_audio.py \
|
||||
--model /xx/dreamid_omni \
|
||||
--prompt "Two people walking together and singing happily" \
|
||||
--image-path ./example0.png ./example1.png \
|
||||
--audio-path ./example0.wav ./example1.wav \
|
||||
--video-negative-prompt "jitter, bad hands, blur, distortion" \
|
||||
--audio-negative-prompt "robotic, muffled, echo, distorted" \
|
||||
--cfg-parallel-size 2 \
|
||||
--num-inference-steps 45 \
|
||||
--height 704 \
|
||||
--width 1280 \
|
||||
--output dreamid_omni.mp4
|
||||
```
|
||||
In the current test scenario (2 images + 2 audio inputs), the VRAM requirement is 72GB, regardless of whether cfg-parallel is enabled or disabled.
|
||||
The VRAM usage can be reduced by enabling CPU offload via --enable-cpu-offload.
|
||||
|
||||
Key arguments:
|
||||
- `--prompt`: text description (string).
|
||||
- `--model`: path to the model local directory.
|
||||
- `--height/--width`: output resolution (defaults 704 * 1024).
|
||||
- `--image-path`: path to the input image list.
|
||||
- `--audio-path`: path to the input audio list, indicate the timbre of the output video.
|
||||
- `--cfg-parallel-size`: number of parallel cfg parallel (defaults 1).
|
||||
- `--num-inference-steps`: number of denoising steps (defaults 45).
|
||||
- `--video-negative-prompt`: negative prompt for video generation.
|
||||
- `--audio-negative-prompt`: negative prompt for audio generation.
|
||||
- `--enable-cpu-offload`: enable CPU offload (defaults False).
|
||||
@@ -0,0 +1,148 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import time
|
||||
|
||||
import librosa
|
||||
from PIL import Image
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif"}
|
||||
AUDIO_EXTENSIONS = {".wav", ".mp3", ".flac", ".m4a", ".aac", ".ogg"}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Offline inference for DreamID-Omni (video + audio).")
|
||||
parser.add_argument("--model", required=True, help="DreamID ckpt root directory.")
|
||||
parser.add_argument("--model-type", default="dreamid-omni", help="Model type.")
|
||||
parser.add_argument("--prompt", default=None, help="Text prompt.")
|
||||
|
||||
parser.add_argument("--image-path", type=str, nargs="+", help="list of image-path")
|
||||
parser.add_argument("--audio-path", type=str, nargs="+", help="list of audio-path")
|
||||
parser.add_argument("--prompt-file", type=str, default=None, help="Text prompt in json format.")
|
||||
|
||||
parser.add_argument("--height", type=int, default=704, help="Video height.")
|
||||
parser.add_argument("--width", type=int, default=1280, help="Video width.")
|
||||
parser.add_argument("--num-inference-steps", type=int, default=45, help="Sampling steps.")
|
||||
parser.add_argument("--solver-name", default="unipc", help="Solver name: unipc|dpm++|euler.")
|
||||
parser.add_argument("--shift", type=float, default=5.0, help="Scheduler shift.")
|
||||
parser.add_argument("--seed", type=int, default=103, help="Random seed for reproducible generation.")
|
||||
parser.add_argument(
|
||||
"--cfg-parallel-size",
|
||||
type=int,
|
||||
default=1,
|
||||
choices=[1, 2],
|
||||
help="Number of GPUs used for classifier free guidance parallel size.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--video-negative-prompt",
|
||||
default="jitter, bad hands, blur, distortion",
|
||||
help="Negative prompt for video.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-negative-prompt",
|
||||
default="robotic, muffled, echo, distorted",
|
||||
help="Negative prompt for audio.",
|
||||
)
|
||||
parser.add_argument("--output", default="dreamid_output.mp4", help="Output video path.")
|
||||
parser.add_argument(
|
||||
"--enable-cpu-offload",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable CPU offloading for diffusion models.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_image_and_audio(image_paths, audio_paths):
|
||||
image = []
|
||||
audio = []
|
||||
|
||||
for path in image_paths:
|
||||
with Image.open(path) as img:
|
||||
img = img.convert("RGB")
|
||||
image.append(img)
|
||||
|
||||
for path in audio_paths:
|
||||
audio_array, sr = librosa.load(path, sr=16000)
|
||||
audio_array = audio_array[int(sr * 1) : int(sr * 3)]
|
||||
audio.append(audio_array)
|
||||
return image, audio
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.prompt is None and args.prompt_file is None:
|
||||
raise ValueError("Either --prompt or --prompt-file must be provided.")
|
||||
|
||||
text_prompt = args.prompt
|
||||
if args.prompt_file:
|
||||
import json
|
||||
|
||||
with open(args.prompt_file) as f:
|
||||
text_prompt = json.load(f)
|
||||
text_prompt = re.sub(
|
||||
r"\[SPEAKER_TIMESTAMPS_START\].*?\[SPEAKER_TIMESTAMPS_END\]", "", text_prompt, flags=re.DOTALL
|
||||
).strip()
|
||||
text_prompt = re.sub(
|
||||
r"\[AUDIO_DESCRIPTION_START].*?\[AUDIO_DESCRIPTION_END]", "", text_prompt, flags=re.DOTALL
|
||||
).strip()
|
||||
text_prompt = re.sub(r"\[[A-Z_]+\]", "", text_prompt)
|
||||
text_prompt = re.sub(r"\n\s*\n", "\n", text_prompt).strip()
|
||||
|
||||
image, audio = load_image_and_audio(args.image_path, args.audio_path)
|
||||
|
||||
prompt = {
|
||||
"prompt": text_prompt,
|
||||
"video_negative_prompt": args.video_negative_prompt,
|
||||
"audio_negative_prompt": args.audio_negative_prompt,
|
||||
"multi_modal_data": {"image": image, "audio": audio},
|
||||
}
|
||||
|
||||
sampling_params = OmniDiffusionSamplingParams(
|
||||
height=args.height,
|
||||
width=args.width,
|
||||
num_inference_steps=args.num_inference_steps,
|
||||
seed=args.seed,
|
||||
extra_args={
|
||||
"solver_name": args.solver_name,
|
||||
"shift": args.shift,
|
||||
},
|
||||
)
|
||||
|
||||
parallel_config = DiffusionParallelConfig(
|
||||
cfg_parallel_size=args.cfg_parallel_size,
|
||||
)
|
||||
|
||||
omni = Omni(
|
||||
model=args.model,
|
||||
parallel_config=parallel_config,
|
||||
model_type=args.model_type,
|
||||
enable_cpu_offload=args.enable_cpu_offload,
|
||||
)
|
||||
start = time.perf_counter()
|
||||
outputs = omni.generate(prompt, sampling_params)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
if not outputs:
|
||||
raise RuntimeError("No output returned from DreamID-Omni.")
|
||||
output = outputs[0].request_output
|
||||
generated_video = output[0].images[0][0]
|
||||
generated_audio = output[0].images[0][1]
|
||||
try:
|
||||
from dreamid_omni.utils.io_utils import save_video
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to extract video and audio from DreamID-Omni output. Error: {e}")
|
||||
output_path = args.output
|
||||
save_video(output_path, generated_video, generated_audio, fps=24, sample_rate=16000)
|
||||
print(f"Saved generated video to {output_path}")
|
||||
print(f"Total time: {elapsed:.2f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -322,6 +322,7 @@ class OmniDiffusionConfig:
|
||||
|
||||
dtype: torch.dtype = torch.bfloat16
|
||||
|
||||
model_config: dict[str, Any] = field(default_factory=dict)
|
||||
tf_model_config: TransformerConfig = field(default_factory=TransformerConfig)
|
||||
|
||||
# Attention
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import PIL.Image
|
||||
from vllm.logger import init_logger
|
||||
|
||||
@@ -30,6 +31,13 @@ def supports_image_input(model_class_name: str) -> bool:
|
||||
return bool(getattr(model_cls, "support_image_input", False))
|
||||
|
||||
|
||||
def supports_audio_input(model_class_name: str) -> bool:
|
||||
model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name)
|
||||
if model_cls is None:
|
||||
return False
|
||||
return bool(getattr(model_cls, "support_audio_input", False))
|
||||
|
||||
|
||||
def image_color_format(model_class_name: str) -> str:
|
||||
model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name)
|
||||
return getattr(model_cls, "color_format", "RGB")
|
||||
@@ -369,7 +377,19 @@ class DiffusionEngine:
|
||||
dummy_image = PIL.Image.new(color_format, (width, height))
|
||||
else:
|
||||
dummy_image = None
|
||||
prompt: OmniTextPrompt = {"prompt": "dummy run", "multi_modal_data": {"image": dummy_image}}
|
||||
|
||||
if supports_audio_input(self.od_config.model_class_name):
|
||||
audio_sr = 16000
|
||||
audio_duration_sec = 4
|
||||
audio_array = np.random.randn(audio_sr * audio_duration_sec).astype(np.float32)
|
||||
dummy_audio = audio_array[audio_sr * 1 : audio_sr * 3]
|
||||
else:
|
||||
dummy_audio = None
|
||||
|
||||
prompt: OmniTextPrompt = {
|
||||
"prompt": "dummy run",
|
||||
"multi_modal_data": {"image": dummy_image, "audio": dummy_audio},
|
||||
}
|
||||
req = OmniDiffusionRequest(
|
||||
prompts=[prompt],
|
||||
sampling_params=OmniDiffusionSamplingParams(
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from .pipeline_dreamid_omni import DreamIDOmniPipeline
|
||||
|
||||
__all__ = [
|
||||
"DreamIDOmniPipeline",
|
||||
]
|
||||
@@ -0,0 +1,335 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
|
||||
try:
|
||||
from dreamid_omni.modules.model import WanLayerNorm, WanRMSNorm
|
||||
except ImportError:
|
||||
raise ImportError("Failed to import from dependency 'dreamid_omni'.")
|
||||
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.models.dreamid_omni.wan2_2 import WanModel, rope_apply
|
||||
|
||||
|
||||
class FusionModel(nn.Module):
|
||||
def __init__(self, video_config=None, audio_config=None):
|
||||
super().__init__()
|
||||
has_video = True
|
||||
has_audio = True
|
||||
if video_config is not None:
|
||||
self.video_model = WanModel(**video_config)
|
||||
else:
|
||||
has_video = False
|
||||
self.video_model = None
|
||||
print("Warning: No video model is provided!")
|
||||
|
||||
if audio_config is not None:
|
||||
self.audio_model = WanModel(**audio_config)
|
||||
else:
|
||||
has_audio = False
|
||||
self.audio_model = None
|
||||
print("Warning: No audio model is provided!")
|
||||
|
||||
if has_video and has_audio:
|
||||
assert len(self.video_model.blocks) == len(self.audio_model.blocks)
|
||||
self.num_blocks = len(self.video_model.blocks)
|
||||
|
||||
self.inject_cross_attention_kv_projections()
|
||||
self.device = get_local_device()
|
||||
|
||||
self.num_heads = self.video_model.num_heads
|
||||
self.head_dim = self.video_model.dim // self.video_model.num_heads
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_heads,
|
||||
head_size=self.head_dim,
|
||||
num_kv_heads=self.num_heads,
|
||||
softmax_scale=1.0 / (self.head_dim**0.5),
|
||||
causal=False,
|
||||
)
|
||||
|
||||
def inject_cross_attention_kv_projections(self):
|
||||
for vid_block in self.video_model.blocks:
|
||||
vid_block.cross_attn.k_fusion = nn.Linear(vid_block.dim, vid_block.dim)
|
||||
vid_block.cross_attn.v_fusion = nn.Linear(vid_block.dim, vid_block.dim)
|
||||
vid_block.cross_attn.pre_attn_norm_fusion = WanLayerNorm(vid_block.dim, elementwise_affine=True)
|
||||
vid_block.cross_attn.norm_k_fusion = (
|
||||
WanRMSNorm(vid_block.dim, eps=1e-6) if vid_block.qk_norm else nn.Identity()
|
||||
)
|
||||
|
||||
for audio_block in self.audio_model.blocks:
|
||||
audio_block.cross_attn.k_fusion = nn.Linear(audio_block.dim, audio_block.dim)
|
||||
audio_block.cross_attn.v_fusion = nn.Linear(audio_block.dim, audio_block.dim)
|
||||
audio_block.cross_attn.pre_attn_norm_fusion = WanLayerNorm(audio_block.dim, elementwise_affine=True)
|
||||
audio_block.cross_attn.norm_k_fusion = (
|
||||
WanRMSNorm(audio_block.dim, eps=1e-6) if audio_block.qk_norm else nn.Identity()
|
||||
)
|
||||
|
||||
def merge_kwargs(self, vid_kwargs, audio_kwargs):
|
||||
"""
|
||||
keys in each kwarg:
|
||||
e
|
||||
seq_lens
|
||||
grid_sizes
|
||||
freqs
|
||||
context
|
||||
context_lens
|
||||
"""
|
||||
merged_kwargs = {}
|
||||
for key in vid_kwargs:
|
||||
merged_kwargs[f"vid_{key}"] = vid_kwargs[key]
|
||||
for key in audio_kwargs:
|
||||
merged_kwargs[f"audio_{key}"] = audio_kwargs[key]
|
||||
return merged_kwargs
|
||||
|
||||
def single_fusion_cross_attention_forward(
|
||||
self,
|
||||
cross_attn_block,
|
||||
src_seq,
|
||||
src_grid_sizes,
|
||||
src_freqs,
|
||||
target_seq,
|
||||
target_seq_lens,
|
||||
target_grid_sizes,
|
||||
target_freqs,
|
||||
context,
|
||||
context_lens,
|
||||
src_ref_lengths=None,
|
||||
src_freqs_scaling=None,
|
||||
target_ref_lengths=None,
|
||||
target_freqs_scaling=None,
|
||||
):
|
||||
b, n, d = src_seq.size(0), cross_attn_block.num_heads, cross_attn_block.head_dim
|
||||
if hasattr(cross_attn_block, "k_img"):
|
||||
## means is i2v block
|
||||
q, k, v, k_img, v_img = cross_attn_block.qkv_fn(src_seq, context)
|
||||
else:
|
||||
## means is t2v block
|
||||
q, k, v = cross_attn_block.qkv_fn(src_seq, context)
|
||||
k_img = v_img = None
|
||||
|
||||
x = self.attn(q, k, v)
|
||||
|
||||
if k_img is not None:
|
||||
img_x = self.attn(q, k_img, v_img)
|
||||
x = x + img_x
|
||||
|
||||
# is_vid = src_grid_sizes.shape[1] > 1
|
||||
# compute target attention
|
||||
target_seq = cross_attn_block.pre_attn_norm_fusion(target_seq)
|
||||
k_target = cross_attn_block.norm_k_fusion(cross_attn_block.k_fusion(target_seq)).view(b, -1, n, d)
|
||||
v_target = cross_attn_block.v_fusion(target_seq).view(b, -1, n, d)
|
||||
|
||||
q = rope_apply(q, src_grid_sizes, src_freqs, ref_lengths=src_ref_lengths, freqs_scaling=src_freqs_scaling)
|
||||
k_target = rope_apply(
|
||||
k_target,
|
||||
target_grid_sizes,
|
||||
target_freqs,
|
||||
ref_lengths=target_ref_lengths,
|
||||
freqs_scaling=target_freqs_scaling,
|
||||
)
|
||||
|
||||
target_x = self.attn(q, k_target, v_target)
|
||||
|
||||
x = x + target_x
|
||||
|
||||
x = x.flatten(2) # [B, L/P, C]
|
||||
|
||||
x = cross_attn_block.o(x)
|
||||
return x
|
||||
|
||||
def single_fusion_cross_attention_ffn_forward(
|
||||
self,
|
||||
attn_block,
|
||||
src_seq,
|
||||
src_grid_sizes,
|
||||
src_freqs,
|
||||
target_seq,
|
||||
target_seq_lens,
|
||||
target_grid_sizes,
|
||||
target_freqs,
|
||||
context,
|
||||
context_lens,
|
||||
src_e,
|
||||
src_ref_lengths=None,
|
||||
src_freqs_scaling=None,
|
||||
target_ref_lengths=None,
|
||||
target_freqs_scaling=None,
|
||||
):
|
||||
src_seq = src_seq + self.single_fusion_cross_attention_forward(
|
||||
attn_block.cross_attn,
|
||||
attn_block.norm3(src_seq),
|
||||
src_grid_sizes=src_grid_sizes,
|
||||
src_freqs=src_freqs,
|
||||
target_seq=target_seq,
|
||||
target_seq_lens=target_seq_lens,
|
||||
target_grid_sizes=target_grid_sizes,
|
||||
target_freqs=target_freqs,
|
||||
context=context,
|
||||
context_lens=context_lens,
|
||||
src_ref_lengths=src_ref_lengths,
|
||||
src_freqs_scaling=src_freqs_scaling,
|
||||
target_ref_lengths=target_ref_lengths,
|
||||
target_freqs_scaling=target_freqs_scaling,
|
||||
)
|
||||
y = attn_block.ffn(attn_block.norm2(src_seq).bfloat16() * (1 + src_e[4].squeeze(2)) + src_e[3].squeeze(2))
|
||||
with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=True):
|
||||
src_seq = src_seq + y * src_e[5].squeeze(2)
|
||||
return src_seq
|
||||
|
||||
def single_fusion_block_forward(
|
||||
self,
|
||||
vid_block,
|
||||
audio_block,
|
||||
vid,
|
||||
audio,
|
||||
vid_e,
|
||||
vid_seq_lens,
|
||||
vid_grid_sizes,
|
||||
vid_freqs,
|
||||
vid_context,
|
||||
vid_context_lens,
|
||||
vid_ref_lengths,
|
||||
vid_freqs_scaling,
|
||||
audio_e,
|
||||
audio_seq_lens,
|
||||
audio_grid_sizes,
|
||||
audio_freqs,
|
||||
audio_context,
|
||||
audio_context_lens,
|
||||
audio_ref_lengths,
|
||||
audio_freqs_scaling,
|
||||
):
|
||||
## audio modulation
|
||||
assert audio_e.dtype == torch.bfloat16
|
||||
assert len(audio_e.shape) == 4 and audio_e.size(2) == 6 and audio_e.shape[1] == audio.shape[1], (
|
||||
f"{audio_e.shape}, {audio.shape}"
|
||||
)
|
||||
with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=True):
|
||||
audio_e = audio_block.modulation(audio_e).chunk(6, dim=2)
|
||||
assert audio_e[0].dtype == torch.bfloat16
|
||||
|
||||
# audio self-attention
|
||||
audio_y = audio_block.self_attn(
|
||||
audio_block.norm1(audio).bfloat16() * (1 + audio_e[1].squeeze(2)) + audio_e[0].squeeze(2),
|
||||
audio_seq_lens,
|
||||
audio_grid_sizes,
|
||||
audio_freqs,
|
||||
ref_lengths=audio_ref_lengths,
|
||||
freqs_scaling=audio_freqs_scaling,
|
||||
)
|
||||
with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=True):
|
||||
audio = audio + audio_y * audio_e[2].squeeze(2)
|
||||
|
||||
## video modulation
|
||||
assert len(vid_e.shape) == 4 and vid_e.size(2) == 6 and vid_e.shape[1] == vid.shape[1], (
|
||||
f"{vid_e.shape}, {vid.shape}"
|
||||
)
|
||||
with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=True):
|
||||
vid_e = vid_block.modulation(vid_e).chunk(6, dim=2)
|
||||
|
||||
# video self-attention
|
||||
vid_y = vid_block.self_attn(
|
||||
vid_block.norm1(vid).bfloat16() * (1 + vid_e[1].squeeze(2)) + vid_e[0].squeeze(2),
|
||||
vid_seq_lens,
|
||||
vid_grid_sizes,
|
||||
vid_freqs,
|
||||
ref_lengths=vid_ref_lengths,
|
||||
)
|
||||
|
||||
with torch.autocast(device_type=self.device.type, dtype=torch.bfloat16, enabled=True):
|
||||
vid = vid + vid_y * vid_e[2].squeeze(2)
|
||||
|
||||
og_audio = audio
|
||||
|
||||
# audio cross-attention
|
||||
audio = self.single_fusion_cross_attention_ffn_forward(
|
||||
audio_block,
|
||||
audio,
|
||||
audio_grid_sizes,
|
||||
audio_freqs,
|
||||
vid,
|
||||
vid_seq_lens,
|
||||
vid_grid_sizes,
|
||||
vid_freqs,
|
||||
audio_context,
|
||||
audio_context_lens,
|
||||
audio_e,
|
||||
src_ref_lengths=audio_ref_lengths,
|
||||
src_freqs_scaling=audio_freqs_scaling,
|
||||
target_ref_lengths=vid_ref_lengths,
|
||||
target_freqs_scaling=None,
|
||||
)
|
||||
|
||||
assert not torch.equal(og_audio, audio), "Audio should be changed after cross-attention!"
|
||||
|
||||
# video cross-attention
|
||||
vid = self.single_fusion_cross_attention_ffn_forward(
|
||||
vid_block,
|
||||
vid,
|
||||
vid_grid_sizes,
|
||||
vid_freqs,
|
||||
og_audio,
|
||||
audio_seq_lens,
|
||||
audio_grid_sizes,
|
||||
audio_freqs,
|
||||
vid_context,
|
||||
vid_context_lens,
|
||||
vid_e,
|
||||
src_ref_lengths=vid_ref_lengths,
|
||||
src_freqs_scaling=None,
|
||||
target_ref_lengths=audio_ref_lengths,
|
||||
target_freqs_scaling=audio_freqs_scaling,
|
||||
)
|
||||
|
||||
return vid, audio
|
||||
|
||||
def forward(
|
||||
self,
|
||||
vid,
|
||||
audio,
|
||||
t,
|
||||
vid_context,
|
||||
audio_context,
|
||||
vid_seq_len,
|
||||
audio_seq_len,
|
||||
ref_ip_lengths=None,
|
||||
ref_audio_lengths=None,
|
||||
slg_layer=False,
|
||||
freqs_scaling=None,
|
||||
):
|
||||
vid, vid_e, vid_kwargs = self.video_model.prepare_transformer_block_kwargs(
|
||||
x=vid, t=t, context=vid_context, seq_len=vid_seq_len, ref_lengths=ref_ip_lengths
|
||||
)
|
||||
|
||||
audio, audio_e, audio_kwargs = self.audio_model.prepare_transformer_block_kwargs(
|
||||
x=audio,
|
||||
t=t,
|
||||
context=audio_context,
|
||||
seq_len=audio_seq_len,
|
||||
ref_lengths=ref_audio_lengths,
|
||||
freqs_scaling=freqs_scaling,
|
||||
)
|
||||
|
||||
kwargs = self.merge_kwargs(vid_kwargs, audio_kwargs)
|
||||
|
||||
for i in range(self.num_blocks):
|
||||
"""
|
||||
1 fusion block refers to 1 audio block with 1 video block.
|
||||
"""
|
||||
|
||||
vid_block = self.video_model.blocks[i]
|
||||
audio_block = self.audio_model.blocks[i]
|
||||
|
||||
vid, audio = self.single_fusion_block_forward(
|
||||
vid_block=vid_block, audio_block=audio_block, vid=vid, audio=audio, **kwargs
|
||||
)
|
||||
|
||||
vid = self.video_model.post_transformer_block_out(vid, vid_kwargs["grid_sizes"], vid_e)
|
||||
audio = self.audio_model.post_transformer_block_out(audio, audio_kwargs["grid_sizes"], audio_e)
|
||||
|
||||
return vid, audio
|
||||
|
||||
def set_rope_params(self):
|
||||
self.video_model.set_rope_params()
|
||||
self.audio_model.set_rope_params()
|
||||
@@ -0,0 +1,508 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
|
||||
import torch
|
||||
import torch.distributed
|
||||
from diffusers import FlowMatchEulerDiscreteScheduler
|
||||
from PIL import Image, ImageOps
|
||||
from torch import nn
|
||||
from torchvision.transforms import Compose, Normalize
|
||||
from tqdm import tqdm
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
||||
from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
get_cfg_group,
|
||||
get_classifier_free_guidance_rank,
|
||||
get_classifier_free_guidance_world_size,
|
||||
)
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.models.interface import SupportAudioInput, SupportImageInput
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
|
||||
try:
|
||||
from dreamid_omni.utils.divisible_crop import DivisibleCrop
|
||||
from dreamid_omni.utils.fm_solvers import FlowDPMSolverMultistepScheduler, get_sampling_sigmas, retrieve_timesteps
|
||||
from dreamid_omni.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
|
||||
from dreamid_omni.utils.model_loading_utils import (
|
||||
init_mmaudio_vae,
|
||||
init_text_model,
|
||||
init_wan_vae_2_2,
|
||||
load_fusion_checkpoint,
|
||||
)
|
||||
from dreamid_omni.utils.rearrange import Rearrange
|
||||
from dreamid_omni.utils.resize import NaResize
|
||||
except ImportError:
|
||||
raise ImportError("Failed to import from dependency 'dreamid_omni'.")
|
||||
from vllm_omni.diffusion.models.dreamid_omni.fusion import FusionModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUDIO_CONFIG = {
|
||||
"patch_size": [1],
|
||||
"model_type": "t2a",
|
||||
"dim": 3072,
|
||||
"ffn_dim": 14336,
|
||||
"freq_dim": 256,
|
||||
"num_heads": 24,
|
||||
"num_layers": 30,
|
||||
"in_dim": 20,
|
||||
"out_dim": 20,
|
||||
"text_len": 512,
|
||||
"window_size": [-1, -1],
|
||||
"qk_norm": True,
|
||||
"cross_attn_norm": True,
|
||||
"eps": 1e-6,
|
||||
"temporal_rope_scaling_factor": 0.19676,
|
||||
}
|
||||
|
||||
VIDEO_CONFIG = {
|
||||
"patch_size": [1, 2, 2],
|
||||
"model_type": "ti2v",
|
||||
"dim": 3072,
|
||||
"ffn_dim": 14336,
|
||||
"freq_dim": 256,
|
||||
"num_heads": 24,
|
||||
"num_layers": 30,
|
||||
"in_dim": 48,
|
||||
"out_dim": 48,
|
||||
"text_len": 512,
|
||||
"window_size": [-1, -1],
|
||||
"qk_norm": True,
|
||||
"cross_attn_norm": True,
|
||||
"eps": 1e-6,
|
||||
}
|
||||
|
||||
|
||||
class DreamIDOmniPipeline(nn.Module, CFGParallelMixin, SupportImageInput, SupportAudioInput):
|
||||
"""DreamID-Omni pipeline for vLLM-Omni."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
od_config: OmniDiffusionConfig,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
self.od_config = od_config
|
||||
self.parallel_config = od_config.parallel_config
|
||||
|
||||
self.device = get_local_device()
|
||||
model = od_config.model
|
||||
self.target_dtype = od_config.dtype
|
||||
|
||||
# Init Models
|
||||
## Load VAEs
|
||||
vae_model_video = init_wan_vae_2_2(model, rank=self.device)
|
||||
vae_model_video.model.requires_grad_(False).eval()
|
||||
vae_model_video.model = vae_model_video.model.bfloat16()
|
||||
self.vae_model_video = vae_model_video
|
||||
self.vae = self.vae_model_video
|
||||
|
||||
vae_model_audio = init_mmaudio_vae(model, rank=self.device)
|
||||
vae_model_audio.requires_grad_(False).eval()
|
||||
self.vae_model_audio = vae_model_audio.bfloat16()
|
||||
|
||||
# Load T5 text model
|
||||
self.text_model = init_text_model(model, rank=self.device)
|
||||
self.text_encoder = self.text_model.model
|
||||
|
||||
# Fusion model
|
||||
## load audio/video model config
|
||||
Fusion_model = FusionModel(VIDEO_CONFIG, AUDIO_CONFIG)
|
||||
|
||||
checkpoint_path = self.od_config.model_config.get("fusion", None)
|
||||
assert checkpoint_path is not None, "fusion checkpoint path is None"
|
||||
load_fusion_checkpoint(Fusion_model, checkpoint_path=os.path.join(model, checkpoint_path))
|
||||
self.model = Fusion_model
|
||||
self.transformer = self.model
|
||||
|
||||
# Fixed attributes, non-configurable
|
||||
self.audio_latent_channel = AUDIO_CONFIG.get("in_dim")
|
||||
self.video_latent_channel = VIDEO_CONFIG.get("in_dim")
|
||||
self.video_latent_length = 31
|
||||
self.audio_latent_length = 157
|
||||
self.target_area = 960 * 960
|
||||
|
||||
# Fixed attributes, CFG scales
|
||||
self.video_cfg_scale = 3.0
|
||||
self.video_ref_cfg_scale = 1.5
|
||||
self.audio_cfg_scale = 4.0
|
||||
self.audio_ref_cfg_scale = 2.0
|
||||
|
||||
# Schedulers will be set in forward
|
||||
self.scheduler_video = None
|
||||
self.scheduler_audio = None
|
||||
|
||||
def load_image_latent_ref_ip_video(
|
||||
self,
|
||||
images,
|
||||
audios,
|
||||
video_frame_height_width,
|
||||
):
|
||||
# Load size.
|
||||
patch_size = self.model.video_model.patch_size
|
||||
vae_stride = [4, 16, 16]
|
||||
|
||||
# Load image and video.
|
||||
ref_vae_latents = {
|
||||
"image": [],
|
||||
"audio": [],
|
||||
}
|
||||
video_h = video_frame_height_width[0]
|
||||
video_w = video_frame_height_width[1]
|
||||
ref_audio_lengths = []
|
||||
for img in images:
|
||||
# Calculate the required size to keep aspect ratio and fill the rest with padding.
|
||||
img_ratio = img.width / img.height
|
||||
target_ratio = video_w / video_h
|
||||
|
||||
if img_ratio > target_ratio: # Image is wider than target
|
||||
new_width = video_w
|
||||
new_height = int(new_width / img_ratio)
|
||||
else: # Image is taller than target
|
||||
new_height = video_h
|
||||
new_width = int(new_height * img_ratio)
|
||||
|
||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Create a new image with the target size and place the resized image in the center
|
||||
delta_w = video_w - img.size[0]
|
||||
delta_h = video_h - img.size[1]
|
||||
padding = (delta_w // 2, delta_h // 2, delta_w - (delta_w // 2), delta_h - (delta_h // 2))
|
||||
new_img = ImageOps.expand(img, padding, fill=(255, 255, 255))
|
||||
|
||||
# Transform to tensor and normalize.
|
||||
image_transform = Compose(
|
||||
[
|
||||
NaResize(
|
||||
resolution=math.sqrt(
|
||||
video_frame_height_width[0] * video_frame_height_width[1]
|
||||
), # 256*448, 480*832
|
||||
mode="area",
|
||||
downsample_only=True,
|
||||
),
|
||||
DivisibleCrop((vae_stride[1] * patch_size[1], vae_stride[2] * patch_size[2])),
|
||||
Normalize(0.5, 0.5),
|
||||
Rearrange("t c h w -> c t h w"),
|
||||
]
|
||||
)
|
||||
new_img = image_transform([new_img])
|
||||
new_img = new_img.transpose(0, 1)
|
||||
new_img = new_img.to(self.device)
|
||||
new_img = new_img.to(self.target_dtype)
|
||||
|
||||
img_vae_latent = self.vae_model_video.wrapped_encode(new_img[:, :, None]).to(self.target_dtype).squeeze(0)
|
||||
ref_vae_latents["image"].append(img_vae_latent)
|
||||
|
||||
for audio_array in audios:
|
||||
audio_tensor = torch.from_numpy(audio_array).float().unsqueeze(0)
|
||||
# print(audio_tensor.shape)#torch.Size([1, 81280])
|
||||
audio_vae_latent = self.vae_model_audio.wrapped_encode(audio_tensor)
|
||||
# print(audio_vae_latent.shape)#torch.Size([1, 20, length])
|
||||
audio_length = audio_vae_latent.shape[2]
|
||||
ref_audio_lengths.append(audio_length)
|
||||
audio_vae_latent = audio_vae_latent.squeeze(0).transpose(0, 1)
|
||||
ref_vae_latents["audio"].append(audio_vae_latent)
|
||||
|
||||
ref_vae_latents["image"] = torch.cat(ref_vae_latents["image"], dim=1)
|
||||
|
||||
ref_vae_latents["audio"] = torch.cat(ref_vae_latents["audio"], dim=0)
|
||||
|
||||
return ref_vae_latents, ref_audio_lengths
|
||||
|
||||
def load_weights(self, weights):
|
||||
pass
|
||||
|
||||
def get_scheduler_time_steps(self, sampling_steps, solver_name="unipc", device=0, shift=5.0):
|
||||
torch.manual_seed(4)
|
||||
|
||||
if solver_name == "unipc":
|
||||
sample_scheduler = FlowUniPCMultistepScheduler(
|
||||
num_train_timesteps=1000, shift=1, use_dynamic_shifting=False
|
||||
)
|
||||
sample_scheduler.set_timesteps(sampling_steps, device=device, shift=shift)
|
||||
timesteps = sample_scheduler.timesteps
|
||||
|
||||
elif solver_name == "dpm++":
|
||||
sample_scheduler = FlowDPMSolverMultistepScheduler(
|
||||
num_train_timesteps=1000, shift=1, use_dynamic_shifting=False
|
||||
)
|
||||
sampling_sigmas = get_sampling_sigmas(sampling_steps, shift=shift)
|
||||
timesteps, _ = retrieve_timesteps(sample_scheduler, device=device, sigmas=sampling_sigmas)
|
||||
|
||||
elif solver_name == "euler":
|
||||
sample_scheduler = FlowMatchEulerDiscreteScheduler(shift=shift)
|
||||
timesteps, sampling_steps = retrieve_timesteps(
|
||||
sample_scheduler,
|
||||
sampling_steps,
|
||||
device=device,
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError("Unsupported solver.")
|
||||
|
||||
return sample_scheduler, timesteps
|
||||
|
||||
def diffuse(
|
||||
self,
|
||||
video_noise: torch.Tensor,
|
||||
audio_noise: torch.Tensor,
|
||||
latents_ref_image: torch.Tensor,
|
||||
latents_ref_audio: torch.Tensor,
|
||||
timesteps_video: torch.Tensor,
|
||||
timesteps_audio: torch.Tensor,
|
||||
text_embeddings_video_pos: torch.Tensor,
|
||||
text_embeddings_video_neg: torch.Tensor,
|
||||
text_embeddings_audio_pos: torch.Tensor,
|
||||
text_embeddings_audio_neg: torch.Tensor,
|
||||
max_seq_len_video: int,
|
||||
max_seq_len_audio: int,
|
||||
freqs_scaling_tensor: torch.Tensor,
|
||||
ref_ip_num: int,
|
||||
ref_audio_length: int,
|
||||
ref_audio_lengths: list,
|
||||
scheduler_video,
|
||||
scheduler_audio,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Diffusion loop with CFG parallel support for DreamID-Omni.
|
||||
"""
|
||||
for i, (t_v, t_a) in tqdm(enumerate(zip(timesteps_video, timesteps_audio))):
|
||||
timestep_input = torch.full((1,), t_v, device=self.device)
|
||||
|
||||
model_input_video = torch.cat([video_noise[:, :-ref_ip_num], latents_ref_image], dim=1)
|
||||
model_input_video_neg = torch.cat(
|
||||
[video_noise[:, :-ref_ip_num], torch.zeros_like(latents_ref_image)], dim=1
|
||||
)
|
||||
|
||||
model_input_audio = torch.cat([audio_noise[:-ref_audio_length, :], latents_ref_audio], dim=0)
|
||||
|
||||
model_input_audio_neg = torch.cat(
|
||||
[audio_noise[:-ref_audio_length, :], torch.zeros_like(latents_ref_audio)], dim=0
|
||||
)
|
||||
ref_ip_lengths = [ref_ip_num]
|
||||
|
||||
common_args = {
|
||||
"vid_seq_len": max_seq_len_video,
|
||||
"audio_seq_len": max_seq_len_audio,
|
||||
"freqs_scaling": freqs_scaling_tensor,
|
||||
"ref_ip_lengths": [ref_ip_lengths],
|
||||
"ref_audio_lengths": [ref_audio_lengths],
|
||||
}
|
||||
pos_args = {
|
||||
**common_args,
|
||||
"audio_context": [text_embeddings_audio_pos],
|
||||
"vid_context": [text_embeddings_video_pos],
|
||||
}
|
||||
neg_args = {
|
||||
**common_args,
|
||||
"audio_context": [text_embeddings_audio_neg],
|
||||
"vid_context": [text_embeddings_video_neg],
|
||||
}
|
||||
|
||||
if get_classifier_free_guidance_world_size() > 1:
|
||||
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
|
||||
cfg_group = get_cfg_group()
|
||||
cfg_rank = get_classifier_free_guidance_rank()
|
||||
|
||||
if cfg_rank == 0:
|
||||
pred_vid, pred_audio = self.model(
|
||||
vid=[model_input_video], audio=[model_input_audio], t=timestep_input, **pos_args
|
||||
)
|
||||
pre_vid_ip_neg, _ = self.model(
|
||||
vid=[model_input_video_neg], audio=[model_input_audio], t=timestep_input, **pos_args
|
||||
)
|
||||
pred_vid_0 = pred_vid[0]
|
||||
pred_audio_0 = pred_audio[0]
|
||||
pre_vid_ip_0 = pre_vid_ip_neg[0]
|
||||
pred_refaudio_0 = torch.zeros_like(pred_audio_0) # dummy tensor
|
||||
else:
|
||||
pred_vid, pred_audio = self.model(
|
||||
vid=[model_input_video], audio=[model_input_audio], t=timestep_input, **neg_args
|
||||
)
|
||||
_, pred_refaudio_neg = self.model(
|
||||
vid=[model_input_video], audio=[model_input_audio_neg], t=timestep_input, **pos_args
|
||||
)
|
||||
pred_vid_0 = pred_vid[0]
|
||||
pred_audio_0 = pred_audio[0]
|
||||
pre_vid_ip_0 = torch.zeros_like(pred_vid_0) # dummy tensor
|
||||
pred_refaudio_0 = pred_refaudio_neg[0]
|
||||
|
||||
pred_vid_gathered = cfg_group.all_gather(pred_vid_0, separate_tensors=True)
|
||||
pred_audio_gathered = cfg_group.all_gather(pred_audio_0, separate_tensors=True)
|
||||
pre_vid_ip_gathered = cfg_group.all_gather(pre_vid_ip_0, separate_tensors=True)
|
||||
pred_refaudio_gathered = cfg_group.all_gather(pred_refaudio_0, separate_tensors=True)
|
||||
|
||||
pred_vid_pos = [pred_vid_gathered[0]]
|
||||
pred_vid_neg = [pred_vid_gathered[1]]
|
||||
pred_audio_pos = [pred_audio_gathered[0]]
|
||||
pred_audio_neg = [pred_audio_gathered[1]]
|
||||
pre_vid_ip_neg = [pre_vid_ip_gathered[0]]
|
||||
pred_refaudio_neg = [pred_refaudio_gathered[1]]
|
||||
else:
|
||||
pred_vid_pos, pred_audio_pos = self.model(
|
||||
vid=[model_input_video], audio=[model_input_audio], t=timestep_input, **pos_args
|
||||
)
|
||||
|
||||
pred_vid_neg, pred_audio_neg = self.model(
|
||||
vid=[model_input_video], audio=[model_input_audio], t=timestep_input, **neg_args
|
||||
)
|
||||
|
||||
pre_vid_ip_neg, _ = self.model(
|
||||
vid=[model_input_video_neg], audio=[model_input_audio], t=timestep_input, **pos_args
|
||||
)
|
||||
|
||||
_, pred_refaudio_neg = self.model(
|
||||
vid=[model_input_video], audio=[model_input_audio_neg], t=timestep_input, **pos_args
|
||||
)
|
||||
|
||||
pred_video_guided = (
|
||||
pred_vid_neg[0]
|
||||
+ self.video_cfg_scale * (pred_vid_pos[0] - pred_vid_neg[0])
|
||||
+ self.video_ref_cfg_scale * (pred_vid_pos[0] - pre_vid_ip_neg[0])
|
||||
)
|
||||
|
||||
pred_audio_guided = (
|
||||
pred_audio_neg[0]
|
||||
+ self.audio_cfg_scale * (pred_audio_pos[0] - pred_audio_neg[0])
|
||||
+ self.audio_ref_cfg_scale * (pred_audio_pos[0] - pred_refaudio_neg[0])
|
||||
)
|
||||
video_noise = scheduler_video.step(
|
||||
pred_video_guided.unsqueeze(0), t_v, video_noise.unsqueeze(0), return_dict=False
|
||||
)[0].squeeze(0)
|
||||
audio_noise = scheduler_audio.step(
|
||||
pred_audio_guided.unsqueeze(0), t_a, audio_noise.unsqueeze(0), return_dict=False
|
||||
)[0].squeeze(0)
|
||||
return video_noise, audio_noise
|
||||
|
||||
def forward(
|
||||
self,
|
||||
request: OmniDiffusionRequest,
|
||||
**kwargs,
|
||||
) -> DiffusionOutput:
|
||||
"""Main forward pass for DreamID-Omni pipeline for R2AV task."""
|
||||
# Extract parameters from request
|
||||
r_prompts = request.prompts[0]
|
||||
if isinstance(r_prompts, str):
|
||||
prompt = r_prompts
|
||||
video_negative_prompt = ""
|
||||
audio_negative_prompt = ""
|
||||
else:
|
||||
prompt = r_prompts.get("prompt", "")
|
||||
video_negative_prompt = r_prompts.get("video_negative_prompt", "")
|
||||
audio_negative_prompt = r_prompts.get("audio_negative_prompt", "")
|
||||
|
||||
multi_modal_data = r_prompts.get("multi_modal_data", {}) if not isinstance(r_prompts, str) else None
|
||||
raw_images = multi_modal_data.get("image", None) if multi_modal_data is not None else None
|
||||
raw_audios = multi_modal_data.get("audio", None) if multi_modal_data is not None else None
|
||||
if raw_images is None or raw_audios is None:
|
||||
raise ValueError("This model requires image and audio to run.")
|
||||
if not isinstance(raw_images, list):
|
||||
raw_images = [raw_images]
|
||||
if not isinstance(raw_audios, list):
|
||||
raw_audios = [raw_audios]
|
||||
if len(raw_images) != len(raw_audios):
|
||||
raise ValueError("'image' and 'audio' lists must have the same length.")
|
||||
|
||||
height = request.sampling_params.height
|
||||
width = request.sampling_params.width
|
||||
num_inference_steps = request.sampling_params.num_inference_steps
|
||||
shift = request.sampling_params.extra_args.get("shift", 5.0)
|
||||
solver_name = request.sampling_params.extra_args.get("solver_name", "unipc")
|
||||
seed = request.sampling_params.seed if request.sampling_params.seed is not None else 42
|
||||
|
||||
# 1. Prepare reference latents
|
||||
ref_vae_latents, ref_audio_lengths = self.load_image_latent_ref_ip_video(
|
||||
images=raw_images,
|
||||
audios=raw_audios,
|
||||
video_frame_height_width=(height, width),
|
||||
)
|
||||
|
||||
latents_ref_image = ref_vae_latents["image"]
|
||||
latents_ref_audio = ref_vae_latents["audio"]
|
||||
ref_ip_num = latents_ref_image.shape[1]
|
||||
ref_audio_length = latents_ref_audio.shape[0]
|
||||
|
||||
# 2. scheduler
|
||||
scheduler_video, timesteps_video = self.get_scheduler_time_steps(
|
||||
sampling_steps=num_inference_steps, device=self.device, solver_name=solver_name, shift=shift
|
||||
)
|
||||
scheduler_audio, timesteps_audio = self.get_scheduler_time_steps(
|
||||
sampling_steps=num_inference_steps, device=self.device, solver_name=solver_name, shift=shift
|
||||
)
|
||||
|
||||
# 3. text embedding
|
||||
text_embeddings = self.text_model([prompt, video_negative_prompt, audio_negative_prompt], device=self.device)
|
||||
text_embeddings = [emb.to(self.target_dtype) for emb in text_embeddings]
|
||||
text_embeddings_audio_pos = text_embeddings[0]
|
||||
text_embeddings_video_pos = text_embeddings[0]
|
||||
text_embeddings_video_neg = text_embeddings[1]
|
||||
text_embeddings_audio_neg = text_embeddings[2]
|
||||
|
||||
video_latent_h, video_latent_w = height // 16, width // 16
|
||||
|
||||
video_noise_len = self.video_latent_length + ref_ip_num
|
||||
audio_noise_len = self.audio_latent_length + ref_audio_length
|
||||
freqs_scaling_tensor = torch.tensor(
|
||||
self.video_latent_length / self.audio_latent_length, device=self.device, dtype=self.target_dtype
|
||||
)
|
||||
|
||||
video_noise = torch.randn(
|
||||
(self.video_latent_channel, video_noise_len, video_latent_h, video_latent_w),
|
||||
device=self.device,
|
||||
dtype=self.target_dtype,
|
||||
generator=torch.Generator(device=self.device).manual_seed(seed),
|
||||
)
|
||||
audio_noise = torch.randn(
|
||||
(audio_noise_len, self.audio_latent_channel),
|
||||
device=self.device,
|
||||
dtype=self.target_dtype,
|
||||
generator=torch.Generator(device=self.device).manual_seed(seed),
|
||||
)
|
||||
|
||||
_patch_size_h, _patch_size_w = self.model.video_model.patch_size[1], self.model.video_model.patch_size[2]
|
||||
|
||||
max_seq_len_video = (
|
||||
video_noise.shape[1] * video_noise.shape[2] * video_noise.shape[3] // (_patch_size_h * _patch_size_w)
|
||||
)
|
||||
|
||||
max_seq_len_audio = audio_noise_len
|
||||
|
||||
with torch.amp.autocast("cuda", enabled=self.target_dtype != torch.float32, dtype=self.target_dtype):
|
||||
video_noise, audio_noise = self.diffuse(
|
||||
video_noise,
|
||||
audio_noise,
|
||||
latents_ref_image,
|
||||
latents_ref_audio,
|
||||
timesteps_video,
|
||||
timesteps_audio,
|
||||
text_embeddings_video_pos,
|
||||
text_embeddings_video_neg,
|
||||
text_embeddings_audio_pos,
|
||||
text_embeddings_audio_neg,
|
||||
max_seq_len_video,
|
||||
max_seq_len_audio,
|
||||
freqs_scaling_tensor,
|
||||
ref_ip_num,
|
||||
ref_audio_length,
|
||||
ref_audio_lengths,
|
||||
scheduler_video,
|
||||
scheduler_audio,
|
||||
)
|
||||
|
||||
video_noise_for_decode = video_noise[:, :-ref_ip_num]
|
||||
audio_noise_for_decode = audio_noise[:-ref_audio_length, :]
|
||||
|
||||
audio_latents_for_vae = audio_noise_for_decode.unsqueeze(0).transpose(1, 2)
|
||||
generated_audio = self.vae_model_audio.wrapped_decode(audio_latents_for_vae).squeeze().cpu().float().numpy()
|
||||
|
||||
video_latents_for_vae = video_noise_for_decode.unsqueeze(0)
|
||||
generated_video = self.vae_model_video.wrapped_decode(video_latents_for_vae).squeeze(0).cpu().float().numpy()
|
||||
|
||||
return DiffusionOutput(output=(generated_video, generated_audio))
|
||||
@@ -0,0 +1,506 @@
|
||||
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.amp as amp
|
||||
import torch.nn as nn
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
|
||||
try:
|
||||
from dreamid_omni.modules.model import (
|
||||
ChannelLastConv1d,
|
||||
ConvMLP,
|
||||
Head,
|
||||
MLPProj,
|
||||
ModulationAdd,
|
||||
WanLayerNorm,
|
||||
WanRMSNorm,
|
||||
rope_apply, # diff from wan2.2
|
||||
rope_params,
|
||||
sinusoidal_embedding_1d,
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError("Failed to import from dependency 'dreamid_omni'.")
|
||||
|
||||
|
||||
class WanSelfAttention(nn.Module):
|
||||
"""Optimized self-attention module using vLLM layers."""
|
||||
|
||||
def __init__(self, dim, num_heads, window_size=(-1, -1), qk_norm=True, eps=1e-6):
|
||||
assert dim % num_heads == 0
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = dim // num_heads
|
||||
self.window_size = window_size
|
||||
self.qk_norm = qk_norm
|
||||
self.eps = eps
|
||||
|
||||
# layers
|
||||
self.q = nn.Linear(dim, dim)
|
||||
self.k = nn.Linear(dim, dim)
|
||||
self.v = nn.Linear(dim, dim)
|
||||
self.o = nn.Linear(dim, dim)
|
||||
self.norm_q = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
|
||||
self.norm_k = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
|
||||
# Unified attention layer
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_heads,
|
||||
head_size=self.head_dim,
|
||||
num_kv_heads=self.num_heads,
|
||||
softmax_scale=1.0 / (self.head_dim**0.5),
|
||||
causal=False,
|
||||
)
|
||||
|
||||
# query, key, value function
|
||||
def qkv_fn(self, x):
|
||||
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
|
||||
|
||||
q = self.norm_q(self.q(x)).view(b, s, n, d)
|
||||
k = self.norm_k(self.k(x)).view(b, s, n, d)
|
||||
v = self.v(x).view(b, s, n, d)
|
||||
return q, k, v
|
||||
|
||||
def forward(self, x, seq_lens, grid_sizes, freqs, ref_lengths=None, freqs_scaling=None):
|
||||
r"""
|
||||
Args:
|
||||
x(Tensor): Shape [B, L, C]
|
||||
seq_lens(Tensor): Shape [B]
|
||||
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
|
||||
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
|
||||
ref_lengths(Tensor): Shape [B]
|
||||
"""
|
||||
q, k, v = self.qkv_fn(x)
|
||||
|
||||
x = self.attn(
|
||||
rope_apply(q, grid_sizes, freqs, ref_lengths=ref_lengths, freqs_scaling=freqs_scaling),
|
||||
rope_apply(k, grid_sizes, freqs, ref_lengths=ref_lengths, freqs_scaling=freqs_scaling),
|
||||
v,
|
||||
)
|
||||
|
||||
# output
|
||||
x = x.flatten(2)
|
||||
x = self.o(x)
|
||||
return x
|
||||
|
||||
|
||||
class WanT2VCrossAttention(WanSelfAttention):
|
||||
def qkv_fn(self, x, context):
|
||||
b, n, d = x.size(0), self.num_heads, self.head_dim
|
||||
|
||||
# compute query, key, value
|
||||
q = self.norm_q(self.q(x)).view(b, -1, n, d)
|
||||
k = self.norm_k(self.k(context)).view(b, -1, n, d)
|
||||
v = self.v(context).view(b, -1, n, d)
|
||||
|
||||
return q, k, v
|
||||
|
||||
def forward(self, x, context, context_lens):
|
||||
r"""
|
||||
Args:
|
||||
x(Tensor): Shape [B, L1, C]
|
||||
context(Tensor): Shape [B, L2, C]
|
||||
context_lens(Tensor): Shape [B]
|
||||
"""
|
||||
q, k, v = self.qkv_fn(x, context)
|
||||
|
||||
# compute attention
|
||||
x = self.attn(q, k, v)
|
||||
|
||||
# output
|
||||
x = x.flatten(2)
|
||||
x = self.o(x)
|
||||
return x
|
||||
|
||||
|
||||
class WanI2VCrossAttention(nn.Module):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
raise RuntimeError("WanT2VCrossAttention is currently disabled and should not be used.")
|
||||
|
||||
|
||||
WAN_CROSSATTENTION_CLASSES = {
|
||||
"t2v_cross_attn": WanT2VCrossAttention,
|
||||
"i2v_cross_attn": WanI2VCrossAttention,
|
||||
}
|
||||
|
||||
|
||||
class WanAttentionBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
cross_attn_type,
|
||||
dim,
|
||||
ffn_dim,
|
||||
num_heads,
|
||||
window_size=(-1, -1),
|
||||
qk_norm=True,
|
||||
cross_attn_norm=False,
|
||||
eps=1e-6,
|
||||
additional_emb_length=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.ffn_dim = ffn_dim
|
||||
self.num_heads = num_heads
|
||||
self.window_size = window_size
|
||||
self.qk_norm = qk_norm
|
||||
self.cross_attn_norm = cross_attn_norm
|
||||
self.eps = eps
|
||||
|
||||
# layers
|
||||
self.norm1 = WanLayerNorm(dim, eps)
|
||||
self.self_attn = WanSelfAttention(dim, num_heads, window_size, qk_norm, eps)
|
||||
self.norm3 = WanLayerNorm(dim, eps, elementwise_affine=True) if cross_attn_norm else nn.Identity()
|
||||
if cross_attn_type == "i2v_cross_attn":
|
||||
assert additional_emb_length is not None, "additional_emb_length should be specified for i2v_cross_attn"
|
||||
self.cross_attn = WanI2VCrossAttention(dim, num_heads, (-1, -1), qk_norm, eps, additional_emb_length)
|
||||
else:
|
||||
assert additional_emb_length is None, "additional_emb_length should be None for t2v_cross_attn"
|
||||
self.cross_attn = WanT2VCrossAttention(
|
||||
dim,
|
||||
num_heads,
|
||||
(-1, -1),
|
||||
qk_norm,
|
||||
eps,
|
||||
)
|
||||
self.norm2 = WanLayerNorm(dim, eps)
|
||||
self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(approximate="tanh"), nn.Linear(ffn_dim, dim))
|
||||
|
||||
# modulation
|
||||
self.modulation = ModulationAdd(dim, 6)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x,
|
||||
e,
|
||||
seq_lens,
|
||||
grid_sizes,
|
||||
freqs,
|
||||
context,
|
||||
context_lens,
|
||||
):
|
||||
r"""
|
||||
Args:
|
||||
x(Tensor): Shape [B, L, C]
|
||||
e(Tensor): Shape [B, L1, 6, C]
|
||||
seq_lens(Tensor): Shape [B], length of each sequence in batch
|
||||
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
|
||||
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
|
||||
"""
|
||||
assert e.dtype == torch.bfloat16
|
||||
assert len(e.shape) == 4 and e.size(2) == 6 and e.shape[1] == x.shape[1], f"{e.shape}, {x.shape}"
|
||||
with amp.autocast("cuda", dtype=torch.bfloat16):
|
||||
e = self.modulation(e).chunk(6, dim=2)
|
||||
assert e[0].dtype == torch.bfloat16
|
||||
|
||||
# self-attention
|
||||
y = self.self_attn(
|
||||
self.norm1(x).bfloat16() * (1 + e[1].squeeze(2)) + e[0].squeeze(2), seq_lens, grid_sizes, freqs
|
||||
)
|
||||
with amp.autocast("cuda", dtype=torch.bfloat16):
|
||||
x = x + y * e[2].squeeze(2)
|
||||
|
||||
# cross-attention & ffn function
|
||||
def cross_attn_ffn(x, context, context_lens, e):
|
||||
x = x + self.cross_attn(self.norm3(x), context, context_lens)
|
||||
y = self.ffn(self.norm2(x).bfloat16() * (1 + e[4].squeeze(2)) + e[3].squeeze(2))
|
||||
with amp.autocast("cuda", dtype=torch.bfloat16):
|
||||
x = x + y * e[5].squeeze(2)
|
||||
return x
|
||||
|
||||
x = cross_attn_ffn(x, context, context_lens, e)
|
||||
return x
|
||||
|
||||
|
||||
class WanModel(ModelMixin, ConfigMixin):
|
||||
r"""
|
||||
Wan diffusion backbone supporting both text-to-video and image-to-video, text-to-audio.
|
||||
"""
|
||||
|
||||
@register_to_config
|
||||
def __init__(
|
||||
self,
|
||||
model_type="t2v",
|
||||
patch_size=(1, 2, 2),
|
||||
text_len=512,
|
||||
in_dim=16,
|
||||
dim=2048,
|
||||
ffn_dim=8192,
|
||||
freq_dim=256,
|
||||
text_dim=4096,
|
||||
additional_emb_dim=None,
|
||||
additional_emb_length=None,
|
||||
out_dim=16,
|
||||
num_heads=16,
|
||||
num_layers=32,
|
||||
window_size=(-1, -1),
|
||||
qk_norm=True,
|
||||
cross_attn_norm=True,
|
||||
temporal_rope_scaling_factor=1.0,
|
||||
eps=1e-6,
|
||||
):
|
||||
r"""
|
||||
Initialize the diffusion model backbone.
|
||||
|
||||
Args:
|
||||
model_type (`str`, *optional*, defaults to 't2v'):
|
||||
Model variant - 't2v' (text-to-video) or 'i2v' (image-to-video)
|
||||
patch_size (`tuple`, *optional*, defaults to (1, 2, 2)):
|
||||
3D patch dimensions for video embedding (t_patch, h_patch, w_patch)
|
||||
text_len (`int`, *optional*, defaults to 512):
|
||||
Fixed length for text embeddings
|
||||
in_dim (`int`, *optional*, defaults to 16):
|
||||
Input video channels (C_in)
|
||||
dim (`int`, *optional*, defaults to 2048):
|
||||
Hidden dimension of the transformer
|
||||
ffn_dim (`int`, *optional*, defaults to 8192):
|
||||
Intermediate dimension in feed-forward network
|
||||
freq_dim (`int`, *optional*, defaults to 256):
|
||||
Dimension for sinusoidal time embeddings
|
||||
text_dim (`int`, *optional*, defaults to 4096):
|
||||
Input dimension for text embeddings
|
||||
out_dim (`int`, *optional*, defaults to 16):
|
||||
Output video channels (C_out)
|
||||
num_heads (`int`, *optional*, defaults to 16):
|
||||
Number of attention heads
|
||||
num_layers (`int`, *optional*, defaults to 32):
|
||||
Number of transformer blocks
|
||||
window_size (`tuple`, *optional*, defaults to (-1, -1)):
|
||||
Window size for local attention (-1 indicates global attention)
|
||||
qk_norm (`bool`, *optional*, defaults to True):
|
||||
Enable query/key normalization
|
||||
cross_attn_norm (`bool`, *optional*, defaults to False):
|
||||
Enable cross-attention normalization
|
||||
eps (`float`, *optional*, defaults to 1e-6):
|
||||
Epsilon value for normalization layers
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
assert model_type in [
|
||||
"t2v",
|
||||
"i2v",
|
||||
"t2a",
|
||||
"tt2a",
|
||||
"ti2v",
|
||||
] ## tt2a means text transcript + text description to audio (to support both TTS and T2A
|
||||
self.model_type = model_type
|
||||
is_audio_type = "a" in self.model_type
|
||||
is_video_type = "v" in self.model_type
|
||||
assert is_audio_type ^ is_video_type, "Either audio or video model should be specified"
|
||||
if is_audio_type:
|
||||
## audio model
|
||||
assert len(patch_size) == 1 and patch_size[0] == 1, (
|
||||
"Audio model should only accept 1 dimensional input, and we dont do patchify"
|
||||
)
|
||||
|
||||
self.patch_size = patch_size
|
||||
self.text_len = text_len
|
||||
self.in_dim = in_dim
|
||||
self.dim = dim
|
||||
self.ffn_dim = ffn_dim
|
||||
self.freq_dim = freq_dim
|
||||
self.text_dim = text_dim
|
||||
self.out_dim = out_dim
|
||||
self.num_heads = num_heads
|
||||
self.num_layers = num_layers
|
||||
self.window_size = window_size
|
||||
self.qk_norm = qk_norm
|
||||
self.cross_attn_norm = cross_attn_norm
|
||||
self.eps = eps
|
||||
self.temporal_rope_scaling_factor = temporal_rope_scaling_factor
|
||||
self.is_audio_type = is_audio_type
|
||||
self.is_video_type = is_video_type
|
||||
# embeddings
|
||||
if is_audio_type:
|
||||
## hardcoded to MMAudio
|
||||
self.patch_embedding = nn.Sequential(
|
||||
ChannelLastConv1d(in_dim, dim, kernel_size=7, padding=3),
|
||||
nn.SiLU(),
|
||||
ConvMLP(dim, dim * 4, kernel_size=7, padding=3),
|
||||
)
|
||||
else:
|
||||
self.patch_embedding = nn.Conv3d(in_dim, dim, kernel_size=patch_size, stride=patch_size)
|
||||
|
||||
self.text_embedding = nn.Sequential(nn.Linear(text_dim, dim), nn.GELU(approximate="tanh"), nn.Linear(dim, dim))
|
||||
|
||||
self.time_embedding = nn.Sequential(nn.Linear(freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
|
||||
self.time_projection = nn.Sequential(nn.SiLU(), nn.Linear(dim, dim * 6))
|
||||
|
||||
# blocks
|
||||
## so i2v and tt2a share the same cross attention while t2v and t2a share the same cross attention
|
||||
cross_attn_type = "t2v_cross_attn" if model_type in ["t2v", "t2a", "ti2v"] else "i2v_cross_attn"
|
||||
|
||||
if cross_attn_type == "t2v_cross_attn":
|
||||
assert additional_emb_dim is None and additional_emb_length is None, (
|
||||
"additional_emb_length should be None for t2v and t2a model"
|
||||
)
|
||||
else:
|
||||
assert additional_emb_dim is not None and additional_emb_length is not None, (
|
||||
"additional_emb_length should be specified for i2v and tt2a model"
|
||||
)
|
||||
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
WanAttentionBlock(
|
||||
cross_attn_type,
|
||||
dim,
|
||||
ffn_dim,
|
||||
num_heads,
|
||||
window_size,
|
||||
qk_norm,
|
||||
cross_attn_norm,
|
||||
eps,
|
||||
additional_emb_length,
|
||||
)
|
||||
for _ in range(num_layers)
|
||||
]
|
||||
)
|
||||
|
||||
# head
|
||||
self.head = Head(dim, out_dim, patch_size, eps)
|
||||
|
||||
self.set_rope_params()
|
||||
|
||||
if model_type in ["i2v", "tt2a"]:
|
||||
self.img_emb = MLPProj(additional_emb_dim, dim)
|
||||
|
||||
def set_rope_params(self):
|
||||
# buffers (don't use register_buffer otherwise dtype will be changed in to())
|
||||
dim = self.dim
|
||||
num_heads = self.num_heads
|
||||
assert (dim % num_heads) == 0 and (dim // num_heads) % 2 == 0
|
||||
d = dim // num_heads
|
||||
|
||||
if self.is_audio_type:
|
||||
## to be determined
|
||||
# self.freqs = rope_params(1024, d, freqs_scaling=temporal_rope_scaling_factor)
|
||||
self.freqs = rope_params(1024, d - 4 * (d // 6), freqs_scaling=self.temporal_rope_scaling_factor)
|
||||
else:
|
||||
self.freqs = torch.cat(
|
||||
[rope_params(1024, d - 4 * (d // 6)), rope_params(1024, 2 * (d // 6)), rope_params(1024, 2 * (d // 6))],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
def prepare_transformer_block_kwargs(
|
||||
self,
|
||||
x,
|
||||
t,
|
||||
context,
|
||||
seq_len,
|
||||
ref_lengths=None,
|
||||
freqs_scaling=None,
|
||||
):
|
||||
# params
|
||||
## need to change!
|
||||
device = next(self.patch_embedding.parameters()).device
|
||||
|
||||
if self.is_audio_type and freqs_scaling is not None:
|
||||
if isinstance(freqs_scaling, torch.Tensor):
|
||||
scale_val = freqs_scaling.item()
|
||||
else:
|
||||
scale_val = freqs_scaling
|
||||
d = self.dim // self.num_heads
|
||||
|
||||
current_freqs = rope_params(1024, d - 4 * (d // 6), freqs_scaling=scale_val).to(device)
|
||||
else:
|
||||
current_freqs = self.freqs
|
||||
if current_freqs.device != device:
|
||||
current_freqs = current_freqs.to(device)
|
||||
|
||||
# embeddings
|
||||
x = [self.patch_embedding(u.unsqueeze(0)) for u in x] ## x is list of [B L D] or [B C F H W]
|
||||
if self.is_audio_type:
|
||||
# [B, 1]
|
||||
grid_sizes = torch.stack([torch.tensor(u.shape[1:2], dtype=torch.long) for u in x])
|
||||
else:
|
||||
# [B, 3]
|
||||
grid_sizes = torch.stack([torch.tensor(u.shape[2:], dtype=torch.long) for u in x])
|
||||
x = [u.flatten(2).transpose(1, 2) for u in x] # [B C F H W] -> [B (F H W) C] -> [B L C]
|
||||
|
||||
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long)
|
||||
assert seq_lens.max() <= seq_len, f"Sequence length {seq_lens.max()} exceeds maximum {seq_len}."
|
||||
x = torch.cat(
|
||||
[torch.cat([u, u.new_zeros(1, seq_len - u.size(1), u.size(2))], dim=1) for u in x]
|
||||
) # single [B, L, C]
|
||||
|
||||
# time embeddings
|
||||
if t.dim() == 1:
|
||||
t = t.unsqueeze(1).expand(t.size(0), seq_len)
|
||||
with amp.autocast("cuda", dtype=torch.bfloat16):
|
||||
bt = t.size(0)
|
||||
t = t.flatten()
|
||||
e = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, t).unflatten(0, (bt, seq_len)).bfloat16())
|
||||
e0 = self.time_projection(e).unflatten(2, (6, self.dim)) # [1, 26784, 6, 3072] - B, seq_len, 6, dim
|
||||
assert e.dtype == torch.bfloat16 and e0.dtype == torch.bfloat16
|
||||
|
||||
# context
|
||||
context_lens = None
|
||||
context = self.text_embedding(
|
||||
torch.stack([torch.cat([u, u.new_zeros(self.text_len - u.size(0), u.size(1))]) for u in context])
|
||||
)
|
||||
|
||||
# arguments
|
||||
kwargs = dict(
|
||||
e=e0,
|
||||
seq_lens=seq_lens,
|
||||
grid_sizes=grid_sizes,
|
||||
freqs=current_freqs,
|
||||
context=context,
|
||||
context_lens=context_lens,
|
||||
ref_lengths=ref_lengths,
|
||||
freqs_scaling=freqs_scaling,
|
||||
)
|
||||
|
||||
return x, e, kwargs
|
||||
|
||||
def post_transformer_block_out(self, x, grid_sizes, e):
|
||||
# head
|
||||
x = self.head(x, e)
|
||||
# unpatchify
|
||||
if self.is_audio_type:
|
||||
## grid_sizes is [B 1] where 1 is L,
|
||||
# converting grid_sizes from [B 1] -> [B]
|
||||
grid_sizes = [gs[0] for gs in grid_sizes]
|
||||
assert len(x) == len(grid_sizes)
|
||||
x = [u[:gs] for u, gs in zip(x, grid_sizes)]
|
||||
else:
|
||||
## grid_sizes is [B 3] where 3 is F H w
|
||||
x = self.unpatchify(x, grid_sizes)
|
||||
|
||||
return [u.bfloat16() for u in x]
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
raise NotImplementedError("WanModel model does not support forward pass.")
|
||||
|
||||
def unpatchify(self, x, grid_sizes) -> list[torch.Tensor]:
|
||||
r"""
|
||||
Reconstruct video tensors from patch embeddings.
|
||||
|
||||
Args:
|
||||
x (List[Tensor]):
|
||||
List of patchified features, each with shape [L, C_out * prod(patch_size)]
|
||||
grid_sizes (Tensor):
|
||||
Original spatial-temporal grid dimensions before patching,
|
||||
shape [B, 3] (3 dimensions correspond to F_patches, H_patches, W_patches)
|
||||
|
||||
Returns:
|
||||
List[Tensor]:
|
||||
Reconstructed video tensors with shape [C_out, F, H / 8, W / 8]
|
||||
"""
|
||||
|
||||
c = self.out_dim
|
||||
out = []
|
||||
for u, v in zip(x, grid_sizes.tolist()):
|
||||
# v is [F H w] F * H * 80, 100, it was right padded by 20.
|
||||
u = u[: math.prod(v)].view(*v, *self.patch_size, c)
|
||||
u = torch.einsum("fhwpqrc->cfphqwr", u)
|
||||
u = u.reshape(c, *[i * j for i, j in zip(v, self.patch_size)])
|
||||
out.append(u)
|
||||
# out is list of [C F H W]
|
||||
return out
|
||||
@@ -13,6 +13,11 @@ class SupportImageInput(Protocol):
|
||||
color_format: ClassVar[str] = "RGB" # Default color format
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportAudioInput(Protocol):
|
||||
support_audio_input: ClassVar[bool] = True
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportAudioOutput(Protocol):
|
||||
support_audio_output: ClassVar[bool] = True
|
||||
@@ -137,6 +137,11 @@ _DIFFUSION_MODELS = {
|
||||
"pipeline_flux2",
|
||||
"Flux2Pipeline",
|
||||
),
|
||||
"DreamIDOmniPipeline": (
|
||||
"dreamid_omni",
|
||||
"pipeline_dreamid_omni",
|
||||
"DreamIDOmniPipeline",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -196,9 +201,9 @@ def initialize_model(
|
||||
od_config.vae_use_tiling = True
|
||||
|
||||
# Configure VAE memory optimization settings from config
|
||||
if hasattr(model.vae, "use_slicing"):
|
||||
if hasattr(model, "vae") and hasattr(model.vae, "use_slicing"):
|
||||
model.vae.use_slicing = od_config.vae_use_slicing
|
||||
if hasattr(model.vae, "use_tiling"):
|
||||
if hasattr(model, "vae") and hasattr(model.vae, "use_tiling"):
|
||||
model.vae.use_tiling = od_config.vae_use_tiling
|
||||
|
||||
if is_distributed_vae:
|
||||
|
||||
@@ -61,18 +61,23 @@ class OmniDiffusion:
|
||||
"model_index.json",
|
||||
od_config.model,
|
||||
)
|
||||
if config_dict is not None:
|
||||
if od_config.model_class_name is None:
|
||||
od_config.model_class_name = config_dict.get("_class_name", None)
|
||||
od_config.update_multimodal_support()
|
||||
|
||||
if config_dict is None:
|
||||
raise FileNotFoundError("model_index.json not found")
|
||||
|
||||
if od_config.model_class_name is None:
|
||||
od_config.model_class_name = config_dict.get("_class_name", None)
|
||||
od_config.update_multimodal_support()
|
||||
|
||||
if od_config.model_class_name == "DreamIDOmniPipeline":
|
||||
od_config.model_config = config_dict
|
||||
else:
|
||||
tf_config_dict = get_hf_file_to_dict(
|
||||
"transformer/config.json",
|
||||
od_config.model,
|
||||
)
|
||||
od_config.tf_model_config = TransformerConfig.from_dict(tf_config_dict)
|
||||
else:
|
||||
raise FileNotFoundError("model_index.json not found")
|
||||
|
||||
except (AttributeError, OSError, ValueError, FileNotFoundError):
|
||||
cfg = get_hf_file_to_dict("config.json", od_config.model)
|
||||
if cfg is None:
|
||||
|
||||
Reference in new issue
Block a user