Upload folder using huggingface_hub
Browse files- models/__init__.py +38 -0
- models/data.py +17 -0
- models/model_interface.py +114 -0
- models/scheduler.py +103 -0
- models/util.py +17 -0
- models/wan/__init__.py +0 -0
- models/wan/causal_model.py +1013 -0
- models/wan/causal_stream_inference.py +420 -0
- models/wan/flow_match.py +83 -0
- models/wan/taehv_wrapper.py +505 -0
- models/wan/wan_base/README.md +2 -0
- models/wan/wan_base/__init__.py +1 -0
- models/wan/wan_base/modules/__init__.py +16 -0
- models/wan/wan_base/modules/attention.py +310 -0
- models/wan/wan_base/modules/model.py +653 -0
- models/wan/wan_base/modules/t5.py +515 -0
- models/wan/wan_base/modules/tokenizers.py +82 -0
- models/wan/wan_base/modules/vae.py +754 -0
- models/wan/wan_wrapper.py +351 -0
- requirements.txt +12 -1
- streamdiffusionv2/__init__.py +22 -0
- streamdiffusionv2/pipeline.py +437 -0
- streamv2v/__init__.py +14 -0
- streamv2v/api.py +309 -0
- streamv2v/communication/__init__.py +27 -0
- streamv2v/communication/buffer_manager.py +252 -0
- streamv2v/communication/data_containers.py +148 -0
- streamv2v/communication/distributed_communicator.py +392 -0
- streamv2v/communication/kv_cache_manager.py +263 -0
- streamv2v/communication/model_data_transfer.py +273 -0
- streamv2v/communication/test_communication.py +393 -0
- streamv2v/communication/utils.py +288 -0
- streamv2v/configs/__init__.py +2 -0
- streamv2v/configs/wan_causal_dmd_v2v.yaml +57 -0
- streamv2v/configs/wan_causal_dmd_v2v_fast.yaml +57 -0
- streamv2v/inference.py +525 -0
- streamv2v/inference_common.py +142 -0
- streamv2v/inference_pipe.py +1022 -0
- streamv2v/inference_wo_batch.py +451 -0
models/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference-facing model registry for StreamDiffusionV2."""
|
| 2 |
+
|
| 3 |
+
from .wan.wan_wrapper import (
|
| 4 |
+
CausalWanDiffusionWrapper,
|
| 5 |
+
WanDiffusionWrapper,
|
| 6 |
+
WanTextEncoder,
|
| 7 |
+
WanVAEWrapper,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
DIFFUSION_NAME_TO_CLASS = {
|
| 12 |
+
"wan": WanDiffusionWrapper,
|
| 13 |
+
"causal_wan": CausalWanDiffusionWrapper,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
TEXT_ENCODER_NAME_TO_CLASS = {
|
| 18 |
+
"wan": WanTextEncoder,
|
| 19 |
+
"causal_wan": WanTextEncoder,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
VAE_NAME_TO_CLASS = {
|
| 24 |
+
"wan": WanVAEWrapper,
|
| 25 |
+
"causal_wan": WanVAEWrapper,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_diffusion_wrapper(model_name):
|
| 30 |
+
return DIFFUSION_NAME_TO_CLASS[model_name]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def get_text_encoder_wrapper(model_name):
|
| 34 |
+
return TEXT_ENCODER_NAME_TO_CLASS[model_name]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_vae_wrapper(model_name):
|
| 38 |
+
return VAE_NAME_TO_CLASS[model_name]
|
models/data.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal dataset helpers used by inference entrypoints."""
|
| 2 |
+
|
| 3 |
+
from torch.utils.data import Dataset
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TextDataset(Dataset):
|
| 7 |
+
"""Load one prompt per line from a UTF-8 text file."""
|
| 8 |
+
|
| 9 |
+
def __init__(self, data_path: str):
|
| 10 |
+
with open(data_path, "r", encoding="utf-8") as handle:
|
| 11 |
+
self.texts = [line.strip() for line in handle]
|
| 12 |
+
|
| 13 |
+
def __len__(self) -> int:
|
| 14 |
+
return len(self.texts)
|
| 15 |
+
|
| 16 |
+
def __getitem__(self, idx: int) -> str:
|
| 17 |
+
return self.texts[idx]
|
models/model_interface.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models.scheduler import SchedulerInterface
|
| 2 |
+
from abc import abstractmethod, ABC
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
import torch
|
| 5 |
+
import types
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class DiffusionModelInterface(ABC, torch.nn.Module):
|
| 9 |
+
scheduler: SchedulerInterface
|
| 10 |
+
|
| 11 |
+
@abstractmethod
|
| 12 |
+
def forward(
|
| 13 |
+
self, noisy_image_or_video: torch.Tensor, conditional_dict: dict,
|
| 14 |
+
timestep: torch.Tensor, kv_cache: Optional[List[dict]] = None,
|
| 15 |
+
crossattn_cache: Optional[List[dict]] = None,
|
| 16 |
+
current_start: Optional[int] = None,
|
| 17 |
+
current_end: Optional[int] = None
|
| 18 |
+
) -> torch.Tensor:
|
| 19 |
+
"""
|
| 20 |
+
A method to run diffusion model.
|
| 21 |
+
Input:
|
| 22 |
+
- noisy_image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
|
| 23 |
+
- conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
|
| 24 |
+
- timestep: a tensor with shape [B, F] where the number of frame is 1 for images.
|
| 25 |
+
all data should be on the same device as the model.
|
| 26 |
+
- kv_cache: a list of dictionaries containing the key and value tensors for each attention layer.
|
| 27 |
+
- current_start: the start index of the current frame in the sequence.
|
| 28 |
+
- current_end: the end index of the current frame in the sequence.
|
| 29 |
+
Output: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
|
| 30 |
+
We always expect a X0 prediction form for the output.
|
| 31 |
+
"""
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
def get_scheduler(self) -> SchedulerInterface:
|
| 35 |
+
"""
|
| 36 |
+
Update the current scheduler with the interface's static method
|
| 37 |
+
"""
|
| 38 |
+
scheduler = self.scheduler
|
| 39 |
+
scheduler.convert_x0_to_noise = types.MethodType(
|
| 40 |
+
SchedulerInterface.convert_x0_to_noise, scheduler)
|
| 41 |
+
scheduler.convert_noise_to_x0 = types.MethodType(
|
| 42 |
+
SchedulerInterface.convert_noise_to_x0, scheduler)
|
| 43 |
+
scheduler.convert_velocity_to_x0 = types.MethodType(
|
| 44 |
+
SchedulerInterface.convert_velocity_to_x0, scheduler)
|
| 45 |
+
self.scheduler = scheduler
|
| 46 |
+
return scheduler
|
| 47 |
+
|
| 48 |
+
def post_init(self):
|
| 49 |
+
"""
|
| 50 |
+
A few custom initialization steps that should be called after the object is created.
|
| 51 |
+
Currently, the only one we have is to bind a few methods to scheduler.
|
| 52 |
+
We can gradually add more methods here if needed.
|
| 53 |
+
"""
|
| 54 |
+
self.get_scheduler()
|
| 55 |
+
|
| 56 |
+
def set_module_grad(self, module_grad: dict) -> None:
|
| 57 |
+
"""
|
| 58 |
+
Adjusts the state of each module in the object.
|
| 59 |
+
|
| 60 |
+
Parameters:
|
| 61 |
+
- module_grad (dict): A dictionary where each key is the name of a module (as an attribute of the object),
|
| 62 |
+
and each value is a bool indicating whether the module's parameters require gradients.
|
| 63 |
+
|
| 64 |
+
Functionality:
|
| 65 |
+
For each module name in the dictionary:
|
| 66 |
+
- Updates whether its parameters require gradients based on 'is_trainable'.
|
| 67 |
+
"""
|
| 68 |
+
for k, is_trainable in module_grad.items():
|
| 69 |
+
getattr(self, k).requires_grad_(is_trainable)
|
| 70 |
+
|
| 71 |
+
@abstractmethod
|
| 72 |
+
def enable_gradient_checkpointing(self) -> None:
|
| 73 |
+
"""
|
| 74 |
+
Activates gradient checkpointing for the current model (may be referred to as *activation checkpointing* or
|
| 75 |
+
*checkpoint activations* in other frameworks).
|
| 76 |
+
"""
|
| 77 |
+
pass
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class VAEInterface(ABC, torch.nn.Module):
|
| 81 |
+
@abstractmethod
|
| 82 |
+
def decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor:
|
| 83 |
+
"""
|
| 84 |
+
A method to decode a latent representation to an image or video.
|
| 85 |
+
Input: a tensor with shape [B, F // T, C, H // S, W // S] where T and S are temporal and spatial compression factors.
|
| 86 |
+
Output: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
|
| 87 |
+
"""
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class TextEncoderInterface(ABC, torch.nn.Module):
|
| 92 |
+
@abstractmethod
|
| 93 |
+
def forward(self, text_prompts: List[str]) -> dict:
|
| 94 |
+
"""
|
| 95 |
+
A method to tokenize text prompts with a tokenizer and encode them into a latent representation.
|
| 96 |
+
Input: a list of strings.
|
| 97 |
+
Output: a dictionary containing the encoded representation of the text prompts.
|
| 98 |
+
"""
|
| 99 |
+
pass
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class InferencePipelineInterface(ABC):
|
| 103 |
+
@abstractmethod
|
| 104 |
+
def inference_with_trajectory(self, noise: torch.Tensor, conditional_dict: dict) -> torch.Tensor:
|
| 105 |
+
"""
|
| 106 |
+
Run inference with the given diffusion / distilled generators.
|
| 107 |
+
Input:
|
| 108 |
+
- noise: a tensor sampled from N(0, 1) with shape [B, F, C, H, W] where the number of frame is 1 for images.
|
| 109 |
+
- conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
|
| 110 |
+
Output:
|
| 111 |
+
- output: a tensor with shape [B, T, F, C, H, W].
|
| 112 |
+
T is the total number of timesteps. output[0] is a pure noise and output[i] and i>0
|
| 113 |
+
represents the x0 prediction at each timestep.
|
| 114 |
+
"""
|
models/scheduler.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import abstractmethod, ABC
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class SchedulerInterface(ABC):
|
| 6 |
+
"""
|
| 7 |
+
Base class for diffusion noise schedule.
|
| 8 |
+
"""
|
| 9 |
+
alphas_cumprod: torch.Tensor # [T], alphas for defining the noise schedule
|
| 10 |
+
|
| 11 |
+
@abstractmethod
|
| 12 |
+
def add_noise(
|
| 13 |
+
self, clean_latent: torch.Tensor,
|
| 14 |
+
noise: torch.Tensor, timestep: torch.Tensor
|
| 15 |
+
):
|
| 16 |
+
"""
|
| 17 |
+
Diffusion forward corruption process.
|
| 18 |
+
Input:
|
| 19 |
+
- clean_latent: the clean latent with shape [B, C, H, W]
|
| 20 |
+
- noise: the noise with shape [B, C, H, W]
|
| 21 |
+
- timestep: the timestep with shape [B]
|
| 22 |
+
Output: the corrupted latent with shape [B, C, H, W]
|
| 23 |
+
"""
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
def convert_x0_to_noise(
|
| 27 |
+
self, x0: torch.Tensor, xt: torch.Tensor,
|
| 28 |
+
timestep: torch.Tensor
|
| 29 |
+
) -> torch.Tensor:
|
| 30 |
+
"""
|
| 31 |
+
Convert the diffusion network's x0 prediction to noise predidction.
|
| 32 |
+
x0: the predicted clean data with shape [B, C, H, W]
|
| 33 |
+
xt: the input noisy data with shape [B, C, H, W]
|
| 34 |
+
timestep: the timestep with shape [B]
|
| 35 |
+
|
| 36 |
+
noise = (xt-sqrt(alpha_t)*x0) / sqrt(beta_t) (eq 11 in https://arxiv.org/abs/2311.18828)
|
| 37 |
+
"""
|
| 38 |
+
# use higher precision for calculations
|
| 39 |
+
original_dtype = x0.dtype
|
| 40 |
+
x0, xt, alphas_cumprod = map(
|
| 41 |
+
lambda x: x.double().to(x0.device), [x0, xt,
|
| 42 |
+
self.alphas_cumprod]
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
alpha_prod_t = alphas_cumprod[timestep].reshape(-1, 1, 1, 1)
|
| 46 |
+
beta_prod_t = 1 - alpha_prod_t
|
| 47 |
+
|
| 48 |
+
noise_pred = (xt - alpha_prod_t **
|
| 49 |
+
(0.5) * x0) / beta_prod_t ** (0.5)
|
| 50 |
+
return noise_pred.to(original_dtype)
|
| 51 |
+
|
| 52 |
+
def convert_noise_to_x0(
|
| 53 |
+
self, noise: torch.Tensor, xt: torch.Tensor,
|
| 54 |
+
timestep: torch.Tensor
|
| 55 |
+
) -> torch.Tensor:
|
| 56 |
+
"""
|
| 57 |
+
Convert the diffusion network's noise prediction to x0 predidction.
|
| 58 |
+
noise: the predicted noise with shape [B, C, H, W]
|
| 59 |
+
xt: the input noisy data with shape [B, C, H, W]
|
| 60 |
+
timestep: the timestep with shape [B]
|
| 61 |
+
|
| 62 |
+
x0 = (x_t - sqrt(beta_t) * noise) / sqrt(alpha_t) (eq 11 in https://arxiv.org/abs/2311.18828)
|
| 63 |
+
"""
|
| 64 |
+
# use higher precision for calculations
|
| 65 |
+
original_dtype = noise.dtype
|
| 66 |
+
noise, xt, alphas_cumprod = map(
|
| 67 |
+
lambda x: x.double().to(noise.device), [noise, xt,
|
| 68 |
+
self.alphas_cumprod]
|
| 69 |
+
)
|
| 70 |
+
alpha_prod_t = alphas_cumprod[timestep].reshape(-1, 1, 1, 1)
|
| 71 |
+
beta_prod_t = 1 - alpha_prod_t
|
| 72 |
+
|
| 73 |
+
x0_pred = (xt - beta_prod_t **
|
| 74 |
+
(0.5) * noise) / alpha_prod_t ** (0.5)
|
| 75 |
+
return x0_pred.to(original_dtype)
|
| 76 |
+
|
| 77 |
+
def convert_velocity_to_x0(
|
| 78 |
+
self, velocity: torch.Tensor, xt: torch.Tensor,
|
| 79 |
+
timestep: torch.Tensor
|
| 80 |
+
) -> torch.Tensor:
|
| 81 |
+
"""
|
| 82 |
+
Convert the diffusion network's velocity prediction to x0 predidction.
|
| 83 |
+
velocity: the predicted noise with shape [B, C, H, W]
|
| 84 |
+
xt: the input noisy data with shape [B, C, H, W]
|
| 85 |
+
timestep: the timestep with shape [B]
|
| 86 |
+
|
| 87 |
+
v = sqrt(alpha_t) * noise - sqrt(beta_t) x0
|
| 88 |
+
noise = (xt-sqrt(alpha_t)*x0) / sqrt(beta_t)
|
| 89 |
+
given v, x_t, we have
|
| 90 |
+
x0 = sqrt(alpha_t) * x_t - sqrt(beta_t) * v
|
| 91 |
+
see derivations https://chatgpt.com/share/679fb6c8-3a30-8008-9b0e-d1ae892dac56
|
| 92 |
+
"""
|
| 93 |
+
# use higher precision for calculations
|
| 94 |
+
original_dtype = velocity.dtype
|
| 95 |
+
velocity, xt, alphas_cumprod = map(
|
| 96 |
+
lambda x: x.double().to(velocity.device), [velocity, xt,
|
| 97 |
+
self.alphas_cumprod]
|
| 98 |
+
)
|
| 99 |
+
alpha_prod_t = alphas_cumprod[timestep].reshape(-1, 1, 1, 1)
|
| 100 |
+
beta_prod_t = 1 - alpha_prod_t
|
| 101 |
+
|
| 102 |
+
x0_pred = (alpha_prod_t ** 0.5) * xt - (beta_prod_t ** 0.5) * velocity
|
| 103 |
+
return x0_pred.to(original_dtype)
|
models/util.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small runtime helpers used by the offline and online inference entrypoints."""
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def set_seed(seed: int, deterministic: bool = False) -> None:
|
| 10 |
+
"""Seed Python, NumPy, and PyTorch for reproducible inference."""
|
| 11 |
+
random.seed(seed)
|
| 12 |
+
np.random.seed(seed)
|
| 13 |
+
torch.manual_seed(seed)
|
| 14 |
+
torch.cuda.manual_seed_all(seed)
|
| 15 |
+
|
| 16 |
+
if deterministic:
|
| 17 |
+
torch.use_deterministic_algorithms(True)
|
models/wan/__init__.py
ADDED
|
File without changes
|
models/wan/causal_model.py
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models.wan.wan_base.modules.attention import attention
|
| 2 |
+
from models.wan.wan_base.modules.model import (
|
| 3 |
+
WanRMSNorm,
|
| 4 |
+
rope_apply,
|
| 5 |
+
WanLayerNorm,
|
| 6 |
+
WAN_CROSSATTENTION_CLASSES,
|
| 7 |
+
Head,
|
| 8 |
+
rope_params,
|
| 9 |
+
MLPProj,
|
| 10 |
+
sinusoidal_embedding_1d
|
| 11 |
+
)
|
| 12 |
+
from torch.nn.attention.flex_attention import create_block_mask, flex_attention
|
| 13 |
+
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
| 14 |
+
from torch.nn.attention.flex_attention import BlockMask
|
| 15 |
+
from diffusers.models.modeling_utils import ModelMixin
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
import torch.nn as nn
|
| 18 |
+
import torch
|
| 19 |
+
import math
|
| 20 |
+
from collections import OrderedDict
|
| 21 |
+
import torch.distributed as dist
|
| 22 |
+
import warnings
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
import flash_attn_interface
|
| 26 |
+
FLASH_ATTN_AVAILABLE = True
|
| 27 |
+
except (ImportError, ModuleNotFoundError):
|
| 28 |
+
try:
|
| 29 |
+
from flash_attn import flash_attn_interface
|
| 30 |
+
FLASH_ATTN_AVAILABLE = True
|
| 31 |
+
except (ImportError, ModuleNotFoundError):
|
| 32 |
+
flash_attn_interface = None
|
| 33 |
+
FLASH_ATTN_AVAILABLE = False
|
| 34 |
+
|
| 35 |
+
# wan 1.3B model has a weird channel / head configurations and require max-autotune to work with flexattention
|
| 36 |
+
# see https://github.com/pytorch/pytorch/issues/133254
|
| 37 |
+
# change to default for other models
|
| 38 |
+
flex_attention = torch.compile(
|
| 39 |
+
flex_attention, dynamic=False, mode="max-autotune")
|
| 40 |
+
|
| 41 |
+
_CAUSAL_ROPE_FREQ_CACHE = OrderedDict()
|
| 42 |
+
_CAUSAL_ROPE_FREQ_CACHE_SIZE = 16
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _causal_rope_cache_key(freqs, f, h, w, start_frame, device):
|
| 46 |
+
return (
|
| 47 |
+
freqs,
|
| 48 |
+
device.type,
|
| 49 |
+
device.index,
|
| 50 |
+
f,
|
| 51 |
+
h,
|
| 52 |
+
w,
|
| 53 |
+
start_frame,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _get_causal_rope_freqs(freqs_source, freqs_parts, f, h, w, start_frame, device):
|
| 58 |
+
key = _causal_rope_cache_key(freqs_source, f, h, w, start_frame, device)
|
| 59 |
+
cached = _CAUSAL_ROPE_FREQ_CACHE.get(key)
|
| 60 |
+
if cached is not None:
|
| 61 |
+
_CAUSAL_ROPE_FREQ_CACHE.move_to_end(key)
|
| 62 |
+
return cached
|
| 63 |
+
|
| 64 |
+
temporal, height, width = freqs_parts
|
| 65 |
+
temporal_freqs = temporal[start_frame:start_frame + f].repeat_interleave(h * w, dim=0)
|
| 66 |
+
height_freqs = height[:h].repeat_interleave(w, dim=0).repeat(f, 1)
|
| 67 |
+
width_freqs = width[:w].repeat(h, 1).repeat(f, 1)
|
| 68 |
+
rope_freqs = torch.cat([temporal_freqs, height_freqs, width_freqs], dim=-1).unsqueeze(1)
|
| 69 |
+
|
| 70 |
+
_CAUSAL_ROPE_FREQ_CACHE[key] = rope_freqs
|
| 71 |
+
if len(_CAUSAL_ROPE_FREQ_CACHE) > _CAUSAL_ROPE_FREQ_CACHE_SIZE:
|
| 72 |
+
_CAUSAL_ROPE_FREQ_CACHE.popitem(last=False)
|
| 73 |
+
return rope_freqs
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _prepare_causal_rope_cache(grid_sizes, freqs, start_frame=0):
|
| 77 |
+
c = freqs.shape[1]
|
| 78 |
+
freqs_parts = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
|
| 79 |
+
|
| 80 |
+
if isinstance(start_frame, torch.Tensor):
|
| 81 |
+
start_frames = start_frame.tolist()
|
| 82 |
+
else:
|
| 83 |
+
start_frames = [int(start_frame)] * grid_sizes.shape[0]
|
| 84 |
+
|
| 85 |
+
rope_cache = []
|
| 86 |
+
for grid_size, sf in zip(grid_sizes.tolist(), start_frames):
|
| 87 |
+
f, h, w = grid_size
|
| 88 |
+
seq_len = f * h * w
|
| 89 |
+
rope_freqs = _get_causal_rope_freqs(freqs, freqs_parts, f, h, w, sf, freqs.device)
|
| 90 |
+
rope_cache.append((seq_len, rope_freqs))
|
| 91 |
+
return rope_cache
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def causal_rope_apply(x, grid_sizes, freqs, start_frame=0, rope_cache=None):
|
| 95 |
+
n = x.size(2)
|
| 96 |
+
|
| 97 |
+
if rope_cache is None:
|
| 98 |
+
rope_cache = _prepare_causal_rope_cache(grid_sizes, freqs, start_frame=start_frame)
|
| 99 |
+
|
| 100 |
+
output = x.clone()
|
| 101 |
+
|
| 102 |
+
for i, (seq_len, freqs_i) in enumerate(rope_cache):
|
| 103 |
+
x_i = torch.view_as_complex(
|
| 104 |
+
x[i, :seq_len].to(torch.float64).reshape(seq_len, n, -1, 2)
|
| 105 |
+
)
|
| 106 |
+
output[i, :seq_len] = torch.view_as_real(x_i * freqs_i).flatten(2).type_as(x)
|
| 107 |
+
|
| 108 |
+
return output
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def attention_with_kvcache_fallback(q, k_cache, v_cache, cache_seqlens):
|
| 112 |
+
out_dtype = q.dtype
|
| 113 |
+
max_seq_len = k_cache.shape[1]
|
| 114 |
+
|
| 115 |
+
def prepare_inputs(q_tensor, k_tensor, v_tensor):
|
| 116 |
+
if q_tensor.device.type == "cpu" and q_tensor.dtype in (torch.float16, torch.bfloat16):
|
| 117 |
+
q_tensor = q_tensor.float()
|
| 118 |
+
k_tensor = k_tensor.float()
|
| 119 |
+
v_tensor = v_tensor.float()
|
| 120 |
+
return q_tensor, k_tensor, v_tensor
|
| 121 |
+
|
| 122 |
+
# Fast path: every sample uses the same fully valid cache span.
|
| 123 |
+
if torch.all(cache_seqlens == max_seq_len):
|
| 124 |
+
q_all = q.transpose(1, 2)
|
| 125 |
+
k_all = k_cache.transpose(1, 2)
|
| 126 |
+
v_all = v_cache.transpose(1, 2)
|
| 127 |
+
q_all, k_all, v_all = prepare_inputs(q_all, k_all, v_all)
|
| 128 |
+
x = F.scaled_dot_product_attention(
|
| 129 |
+
q_all,
|
| 130 |
+
k_all,
|
| 131 |
+
v_all,
|
| 132 |
+
attn_mask=None,
|
| 133 |
+
dropout_p=0.0,
|
| 134 |
+
# Keep parity with flash_attn_with_kvcache(..., causal=False).
|
| 135 |
+
is_causal=False,
|
| 136 |
+
)
|
| 137 |
+
return x.transpose(1, 2).to(out_dtype).contiguous()
|
| 138 |
+
|
| 139 |
+
outputs = []
|
| 140 |
+
for batch_idx, seq_len in enumerate(cache_seqlens.tolist()):
|
| 141 |
+
q_i = q[batch_idx:batch_idx + 1].transpose(1, 2)
|
| 142 |
+
k_i = k_cache[batch_idx:batch_idx + 1, :seq_len].transpose(1, 2)
|
| 143 |
+
v_i = v_cache[batch_idx:batch_idx + 1, :seq_len].transpose(1, 2)
|
| 144 |
+
q_i, k_i, v_i = prepare_inputs(q_i, k_i, v_i)
|
| 145 |
+
|
| 146 |
+
x_i = F.scaled_dot_product_attention(
|
| 147 |
+
q_i,
|
| 148 |
+
k_i,
|
| 149 |
+
v_i,
|
| 150 |
+
attn_mask=None,
|
| 151 |
+
dropout_p=0.0,
|
| 152 |
+
# Keep parity with flash_attn_with_kvcache(..., causal=False).
|
| 153 |
+
is_causal=False,
|
| 154 |
+
)
|
| 155 |
+
outputs.append(x_i.transpose(1, 2).to(out_dtype))
|
| 156 |
+
|
| 157 |
+
return torch.cat(outputs, dim=0).contiguous()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class CausalWanSelfAttention(nn.Module):
|
| 161 |
+
|
| 162 |
+
def __init__(self,
|
| 163 |
+
dim,
|
| 164 |
+
num_heads,
|
| 165 |
+
window_size=(-1, -1),
|
| 166 |
+
qk_norm=True,
|
| 167 |
+
eps=1e-6):
|
| 168 |
+
assert dim % num_heads == 0
|
| 169 |
+
super().__init__()
|
| 170 |
+
self.dim = dim
|
| 171 |
+
self.num_heads = num_heads
|
| 172 |
+
self.head_dim = dim // num_heads
|
| 173 |
+
self.window_size = window_size
|
| 174 |
+
self.qk_norm = qk_norm
|
| 175 |
+
self.eps = eps
|
| 176 |
+
|
| 177 |
+
self.sink_size = 3
|
| 178 |
+
self.adapt_sink_thr = -1
|
| 179 |
+
self.evict_idx = None
|
| 180 |
+
|
| 181 |
+
# layers
|
| 182 |
+
self.q = nn.Linear(dim, dim)
|
| 183 |
+
self.k = nn.Linear(dim, dim)
|
| 184 |
+
self.v = nn.Linear(dim, dim)
|
| 185 |
+
self.o = nn.Linear(dim, dim)
|
| 186 |
+
self.norm_q = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
|
| 187 |
+
self.norm_k = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
|
| 188 |
+
|
| 189 |
+
def forward(
|
| 190 |
+
self,
|
| 191 |
+
x,
|
| 192 |
+
seq_lens,
|
| 193 |
+
grid_sizes,
|
| 194 |
+
freqs,
|
| 195 |
+
block_mask,
|
| 196 |
+
kv_cache=None,
|
| 197 |
+
current_start=0,
|
| 198 |
+
current_end=0,
|
| 199 |
+
causal_rope_cache=None,
|
| 200 |
+
):
|
| 201 |
+
r"""
|
| 202 |
+
Args:
|
| 203 |
+
x(Tensor): Shape [B, L, num_heads, C / num_heads]
|
| 204 |
+
seq_lens(Tensor): Shape [B]
|
| 205 |
+
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
|
| 206 |
+
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
|
| 207 |
+
block_mask (BlockMask)
|
| 208 |
+
"""
|
| 209 |
+
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
|
| 210 |
+
|
| 211 |
+
# query, key, value function
|
| 212 |
+
def qkv_fn(x):
|
| 213 |
+
q = self.norm_q(self.q(x)).view(b, s, n, d)
|
| 214 |
+
k = self.norm_k(self.k(x)).view(b, s, n, d)
|
| 215 |
+
v = self.v(x).view(b, s, n, d)
|
| 216 |
+
return q, k, v
|
| 217 |
+
|
| 218 |
+
q, k, v = qkv_fn(x)
|
| 219 |
+
|
| 220 |
+
if kv_cache is None:
|
| 221 |
+
roped_query = rope_apply(q, grid_sizes, freqs).type_as(v)
|
| 222 |
+
roped_key = rope_apply(k, grid_sizes, freqs).type_as(v)
|
| 223 |
+
|
| 224 |
+
padded_length = math.ceil(q.shape[1] / 128) * 128 - q.shape[1]
|
| 225 |
+
padded_roped_query = torch.cat(
|
| 226 |
+
[roped_query,
|
| 227 |
+
torch.zeros([q.shape[0], padded_length, q.shape[2], q.shape[3]],
|
| 228 |
+
device=q.device, dtype=v.dtype)],
|
| 229 |
+
dim=1
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
padded_roped_key = torch.cat(
|
| 233 |
+
[roped_key, torch.zeros([k.shape[0], padded_length, k.shape[2], k.shape[3]],
|
| 234 |
+
device=k.device, dtype=v.dtype)],
|
| 235 |
+
dim=1
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
padded_v = torch.cat(
|
| 239 |
+
[v, torch.zeros([v.shape[0], padded_length, v.shape[2], v.shape[3]],
|
| 240 |
+
device=v.device, dtype=v.dtype)],
|
| 241 |
+
dim=1
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
x = flex_attention(
|
| 245 |
+
query=padded_roped_query.transpose(2, 1),
|
| 246 |
+
key=padded_roped_key.transpose(2, 1),
|
| 247 |
+
value=padded_v.transpose(2, 1),
|
| 248 |
+
block_mask=block_mask
|
| 249 |
+
)[:, :, :-padded_length].transpose(2, 1)
|
| 250 |
+
else:
|
| 251 |
+
frame_seqlen = math.prod(grid_sizes[0][1:]).item()
|
| 252 |
+
current_start_frame = current_start // frame_seqlen
|
| 253 |
+
roped_query = causal_rope_apply(
|
| 254 |
+
q,
|
| 255 |
+
grid_sizes,
|
| 256 |
+
freqs,
|
| 257 |
+
start_frame=current_start_frame,
|
| 258 |
+
rope_cache=causal_rope_cache,
|
| 259 |
+
).type_as(v)
|
| 260 |
+
roped_key = causal_rope_apply(
|
| 261 |
+
k,
|
| 262 |
+
grid_sizes,
|
| 263 |
+
freqs,
|
| 264 |
+
start_frame=current_start_frame,
|
| 265 |
+
rope_cache=causal_rope_cache,
|
| 266 |
+
).type_as(v)
|
| 267 |
+
|
| 268 |
+
seq_lens = []
|
| 269 |
+
kv_cache_size = kv_cache["k"].shape[1]
|
| 270 |
+
cache_bs = kv_cache['k'].shape[0]
|
| 271 |
+
|
| 272 |
+
# Ring-buffer queue init
|
| 273 |
+
if self.evict_idx is None:
|
| 274 |
+
self.evict_idx = [[]]
|
| 275 |
+
|
| 276 |
+
if len(self.evict_idx) < cache_bs:
|
| 277 |
+
self.evict_idx = [self.evict_idx[0].copy() for _ in range(cache_bs)]
|
| 278 |
+
|
| 279 |
+
for i, c_start in enumerate(current_start):
|
| 280 |
+
num_new_tokens = roped_query.shape[1]
|
| 281 |
+
current_end = c_start + roped_query.shape[1]
|
| 282 |
+
sink_tokens = self.sink_size * frame_seqlen
|
| 283 |
+
|
| 284 |
+
if sink_tokens > 0 and self.adapt_sink_thr > -1 and v.shape[1] <= frame_seqlen:
|
| 285 |
+
# Caculate similarity between new keys/values and the oldest ones in the cache
|
| 286 |
+
k_sink_mean = kv_cache["k"][i:i+1, :sink_tokens].reshape(self.sink_size, frame_seqlen, -1).mean(1)
|
| 287 |
+
k_new_mean = roped_key[i:i+1].reshape(1, frame_seqlen, -1).mean(1)
|
| 288 |
+
k_cos_sim = torch.cosine_similarity(k_sink_mean, k_new_mean, dim=-1)
|
| 289 |
+
|
| 290 |
+
v_sink_mean = kv_cache["v"][i:i+1, :sink_tokens].reshape(self.sink_size, frame_seqlen, -1).mean(1)
|
| 291 |
+
v_new_mean = v[i:i+1].reshape(1, frame_seqlen, -1).mean(1)
|
| 292 |
+
v_cos_sim = torch.cosine_similarity(v_sink_mean, v_new_mean, dim=-1)
|
| 293 |
+
|
| 294 |
+
avg_cos_sim = (k_cos_sim + v_cos_sim)/2
|
| 295 |
+
# When the similarity is low, refresh the sink
|
| 296 |
+
if avg_cos_sim.min() < self.adapt_sink_thr:
|
| 297 |
+
idx = torch.argmin(avg_cos_sim).item()
|
| 298 |
+
temp_evict_idx = (idx+1) * frame_seqlen
|
| 299 |
+
self.evict_idx[i].insert(0, temp_evict_idx)
|
| 300 |
+
|
| 301 |
+
# If we are using local attention and the current KV cache size is larger than the local attention size, we need to truncate the KV cache
|
| 302 |
+
if current_end > kv_cache_size or kv_cache["local_end_index"][i]>=kv_cache_size:
|
| 303 |
+
kv_cache["global_end_index"][i].fill_(c_start)
|
| 304 |
+
kv_cache["local_end_index"][i].fill_(kv_cache_size)
|
| 305 |
+
|
| 306 |
+
target_end = self.evict_idx[i][0]
|
| 307 |
+
|
| 308 |
+
# current_step = kv_cache['current_step']
|
| 309 |
+
# Update the buffer
|
| 310 |
+
if cache_bs==1 and kv_cache['current_step'] > 1:
|
| 311 |
+
kv_cache['current_step']-=1
|
| 312 |
+
else:
|
| 313 |
+
evict_idx = self.evict_idx[i].pop(0)
|
| 314 |
+
if evict_idx > sink_tokens:
|
| 315 |
+
self.evict_idx[i].append(evict_idx)
|
| 316 |
+
kv_cache['current_step']=kv_cache['total_steps']
|
| 317 |
+
|
| 318 |
+
# print(f"self.evict_idx: {self.evict_idx[i]}, total steps: {kv_cache['total_steps']}, current step: {current_step}, target: {target_end-num_new_tokens}:{target_end}, kv size:{kv_cache_size}")
|
| 319 |
+
|
| 320 |
+
# Newly added cache covers the oldest one
|
| 321 |
+
kv_cache["k"][i:i+1, target_end-num_new_tokens:target_end] = roped_key[i:i+1]
|
| 322 |
+
kv_cache["v"][i:i+1, target_end-num_new_tokens:target_end] = v[i:i+1]
|
| 323 |
+
|
| 324 |
+
local_end_index = kv_cache["local_end_index"][i].item()
|
| 325 |
+
|
| 326 |
+
else:
|
| 327 |
+
local_end_index = kv_cache["local_end_index"][i].item() + current_end - kv_cache["global_end_index"][i].item()
|
| 328 |
+
|
| 329 |
+
rolling_end = (current_end + num_new_tokens).item()
|
| 330 |
+
if rolling_end > self.sink_size * frame_seqlen and rolling_end <= kv_cache_size \
|
| 331 |
+
and (not self.evict_idx[i] or self.evict_idx[i][-1] != rolling_end):
|
| 332 |
+
self.evict_idx[i].append(rolling_end)
|
| 333 |
+
|
| 334 |
+
local_start_index = local_end_index - num_new_tokens
|
| 335 |
+
# print(f"target: {local_start_index}:{local_end_index}")
|
| 336 |
+
kv_cache["k"][i:i+1, local_start_index:local_end_index] = roped_key[i:i+1]
|
| 337 |
+
kv_cache["v"][i:i+1, local_start_index:local_end_index] = v[i:i+1]
|
| 338 |
+
|
| 339 |
+
seq_lens.append(local_end_index)
|
| 340 |
+
|
| 341 |
+
kv_cache["global_end_index"][i].fill_(current_end)
|
| 342 |
+
kv_cache["local_end_index"][i].fill_(local_end_index)
|
| 343 |
+
|
| 344 |
+
seq_lens = torch.tensor(seq_lens, dtype=torch.int32, device=roped_query.device)
|
| 345 |
+
|
| 346 |
+
max_seq_len = int(seq_lens.max().item())
|
| 347 |
+
k_cache = kv_cache["k"][:, :max_seq_len]
|
| 348 |
+
v_cache = kv_cache["v"][:, :max_seq_len]
|
| 349 |
+
|
| 350 |
+
if FLASH_ATTN_AVAILABLE:
|
| 351 |
+
try:
|
| 352 |
+
with torch.cuda.device(roped_query.device):
|
| 353 |
+
x = flash_attn_interface.flash_attn_with_kvcache(
|
| 354 |
+
q=roped_query,
|
| 355 |
+
k_cache=k_cache,
|
| 356 |
+
v_cache=v_cache,
|
| 357 |
+
cache_seqlens=seq_lens,
|
| 358 |
+
)
|
| 359 |
+
except RuntimeError as exc:
|
| 360 |
+
if "DeviceType::CUDA" not in str(exc):
|
| 361 |
+
raise
|
| 362 |
+
warnings.warn(
|
| 363 |
+
"flash_attn_with_kvcache failed on the current GPU; "
|
| 364 |
+
"falling back to scaled_dot_product_attention.",
|
| 365 |
+
stacklevel=2,
|
| 366 |
+
)
|
| 367 |
+
x = attention_with_kvcache_fallback(
|
| 368 |
+
q=roped_query,
|
| 369 |
+
k_cache=k_cache,
|
| 370 |
+
v_cache=v_cache,
|
| 371 |
+
cache_seqlens=seq_lens,
|
| 372 |
+
)
|
| 373 |
+
else:
|
| 374 |
+
warnings.warn(
|
| 375 |
+
"flash_attn is not installed; falling back to "
|
| 376 |
+
"scaled_dot_product_attention for KV-cache attention.",
|
| 377 |
+
stacklevel=2,
|
| 378 |
+
)
|
| 379 |
+
x = attention_with_kvcache_fallback(
|
| 380 |
+
q=roped_query,
|
| 381 |
+
k_cache=k_cache,
|
| 382 |
+
v_cache=v_cache,
|
| 383 |
+
cache_seqlens=seq_lens,
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
# output
|
| 387 |
+
x = x.flatten(2)
|
| 388 |
+
x = self.o(x)
|
| 389 |
+
return x
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
class CausalWanAttentionBlock(nn.Module):
|
| 393 |
+
|
| 394 |
+
def __init__(self,
|
| 395 |
+
cross_attn_type,
|
| 396 |
+
dim,
|
| 397 |
+
ffn_dim,
|
| 398 |
+
num_heads,
|
| 399 |
+
window_size=(-1, -1),
|
| 400 |
+
qk_norm=True,
|
| 401 |
+
cross_attn_norm=False,
|
| 402 |
+
eps=1e-6):
|
| 403 |
+
super().__init__()
|
| 404 |
+
self.dim = dim
|
| 405 |
+
self.ffn_dim = ffn_dim
|
| 406 |
+
self.num_heads = num_heads
|
| 407 |
+
self.window_size = window_size
|
| 408 |
+
self.qk_norm = qk_norm
|
| 409 |
+
self.cross_attn_norm = cross_attn_norm
|
| 410 |
+
self.eps = eps
|
| 411 |
+
|
| 412 |
+
# layers
|
| 413 |
+
self.norm1 = WanLayerNorm(dim, eps)
|
| 414 |
+
self.self_attn = CausalWanSelfAttention(dim, num_heads, window_size, qk_norm,
|
| 415 |
+
eps)
|
| 416 |
+
self.norm3 = WanLayerNorm(
|
| 417 |
+
dim, eps,
|
| 418 |
+
elementwise_affine=True) if cross_attn_norm else nn.Identity()
|
| 419 |
+
self.cross_attn = WAN_CROSSATTENTION_CLASSES[cross_attn_type](dim,
|
| 420 |
+
num_heads,
|
| 421 |
+
(-1, -1),
|
| 422 |
+
qk_norm,
|
| 423 |
+
eps)
|
| 424 |
+
self.norm2 = WanLayerNorm(dim, eps)
|
| 425 |
+
self.ffn = nn.Sequential(
|
| 426 |
+
nn.Linear(dim, ffn_dim), nn.GELU(approximate='tanh'),
|
| 427 |
+
nn.Linear(ffn_dim, dim))
|
| 428 |
+
|
| 429 |
+
# modulation
|
| 430 |
+
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
|
| 431 |
+
|
| 432 |
+
def forward(
|
| 433 |
+
self,
|
| 434 |
+
x,
|
| 435 |
+
e,
|
| 436 |
+
seq_lens,
|
| 437 |
+
grid_sizes,
|
| 438 |
+
freqs,
|
| 439 |
+
context,
|
| 440 |
+
context_lens,
|
| 441 |
+
block_mask,
|
| 442 |
+
kv_cache=None,
|
| 443 |
+
crossattn_cache=None,
|
| 444 |
+
current_start=0,
|
| 445 |
+
current_end=0,
|
| 446 |
+
causal_rope_cache=None,
|
| 447 |
+
):
|
| 448 |
+
r"""
|
| 449 |
+
Args:
|
| 450 |
+
x(Tensor): Shape [B, L, C]
|
| 451 |
+
e(Tensor): Shape [B, F, 6, C]
|
| 452 |
+
seq_lens(Tensor): Shape [B], length of each sequence in batch
|
| 453 |
+
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
|
| 454 |
+
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
|
| 455 |
+
"""
|
| 456 |
+
num_frames, frame_seqlen = e.shape[1], x.shape[1] // e.shape[1]
|
| 457 |
+
# assert e.dtype == torch.float32
|
| 458 |
+
# with amp.autocast(dtype=torch.float32):
|
| 459 |
+
e = (self.modulation.unsqueeze(1) + e).chunk(6, dim=2)
|
| 460 |
+
# assert e[0].dtype == torch.float32
|
| 461 |
+
|
| 462 |
+
# self-attention
|
| 463 |
+
y = self.self_attn(
|
| 464 |
+
(self.norm1(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen))
|
| 465 |
+
* (1 + e[1]) + e[0]).flatten(1, 2),
|
| 466 |
+
seq_lens, grid_sizes,
|
| 467 |
+
freqs, block_mask, kv_cache, current_start, current_end, causal_rope_cache)
|
| 468 |
+
|
| 469 |
+
# with amp.autocast(dtype=torch.float32):
|
| 470 |
+
x = x + (y.unflatten(dim=1, sizes=(num_frames, frame_seqlen))
|
| 471 |
+
* e[2]).flatten(1, 2)
|
| 472 |
+
|
| 473 |
+
# cross-attention & ffn function
|
| 474 |
+
def cross_attn_ffn(x, context, context_lens, e, crossattn_cache=None):
|
| 475 |
+
x = x + self.cross_attn(self.norm3(x), context,
|
| 476 |
+
context_lens, crossattn_cache=crossattn_cache)
|
| 477 |
+
y = self.ffn(
|
| 478 |
+
(self.norm2(x).unflatten(dim=1, sizes=(num_frames,
|
| 479 |
+
frame_seqlen)) * (1 + e[4]) + e[3]).flatten(1, 2)
|
| 480 |
+
)
|
| 481 |
+
# with amp.autocast(dtype=torch.float32):
|
| 482 |
+
x = x + (y.unflatten(dim=1, sizes=(num_frames,
|
| 483 |
+
frame_seqlen)) * e[5]).flatten(1, 2)
|
| 484 |
+
return x
|
| 485 |
+
|
| 486 |
+
x = cross_attn_ffn(x, context, context_lens, e, crossattn_cache)
|
| 487 |
+
return x
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
class CausalHead(nn.Module):
|
| 491 |
+
|
| 492 |
+
def __init__(self, dim, out_dim, patch_size, eps=1e-6):
|
| 493 |
+
super().__init__()
|
| 494 |
+
self.dim = dim
|
| 495 |
+
self.out_dim = out_dim
|
| 496 |
+
self.patch_size = patch_size
|
| 497 |
+
self.eps = eps
|
| 498 |
+
|
| 499 |
+
# layers
|
| 500 |
+
out_dim = math.prod(patch_size) * out_dim
|
| 501 |
+
self.norm = WanLayerNorm(dim, eps)
|
| 502 |
+
self.head = nn.Linear(dim, out_dim)
|
| 503 |
+
|
| 504 |
+
# modulation
|
| 505 |
+
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
|
| 506 |
+
|
| 507 |
+
def forward(self, x, e):
|
| 508 |
+
r"""
|
| 509 |
+
Args:
|
| 510 |
+
x(Tensor): Shape [B, L1, C]
|
| 511 |
+
e(Tensor): Shape [B, F, 1, C]
|
| 512 |
+
"""
|
| 513 |
+
# assert e.dtype == torch.float32
|
| 514 |
+
# with amp.autocast(dtype=torch.float32):
|
| 515 |
+
num_frames, frame_seqlen = e.shape[1], x.shape[1] // e.shape[1]
|
| 516 |
+
e = (self.modulation.unsqueeze(1) + e).chunk(2, dim=2)
|
| 517 |
+
x = (self.head(
|
| 518 |
+
self.norm(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen)) *
|
| 519 |
+
(1 + e[1]) + e[0]))
|
| 520 |
+
return x
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
class CausalWanModel(ModelMixin, ConfigMixin):
|
| 524 |
+
r"""
|
| 525 |
+
Wan diffusion backbone supporting both text-to-video and image-to-video.
|
| 526 |
+
"""
|
| 527 |
+
|
| 528 |
+
ignore_for_config = [
|
| 529 |
+
'patch_size', 'cross_attn_norm', 'qk_norm', 'text_dim', 'window_size'
|
| 530 |
+
]
|
| 531 |
+
_no_split_modules = ['WanAttentionBlock']
|
| 532 |
+
_supports_gradient_checkpointing = True
|
| 533 |
+
|
| 534 |
+
@register_to_config
|
| 535 |
+
def __init__(self,
|
| 536 |
+
model_type='t2v',
|
| 537 |
+
patch_size=(1, 2, 2),
|
| 538 |
+
text_len=512,
|
| 539 |
+
in_dim=16,
|
| 540 |
+
dim=2048,
|
| 541 |
+
ffn_dim=8192,
|
| 542 |
+
freq_dim=256,
|
| 543 |
+
text_dim=4096,
|
| 544 |
+
out_dim=16,
|
| 545 |
+
num_heads=16,
|
| 546 |
+
num_layers=32,
|
| 547 |
+
window_size=(-1, -1),
|
| 548 |
+
qk_norm=True,
|
| 549 |
+
cross_attn_norm=True,
|
| 550 |
+
eps=1e-6):
|
| 551 |
+
r"""
|
| 552 |
+
Initialize the diffusion model backbone.
|
| 553 |
+
|
| 554 |
+
Args:
|
| 555 |
+
model_type (`str`, *optional*, defaults to 't2v'):
|
| 556 |
+
Model variant - 't2v' (text-to-video) or 'i2v' (image-to-video)
|
| 557 |
+
patch_size (`tuple`, *optional*, defaults to (1, 2, 2)):
|
| 558 |
+
3D patch dimensions for video embedding (t_patch, h_patch, w_patch)
|
| 559 |
+
text_len (`int`, *optional*, defaults to 512):
|
| 560 |
+
Fixed length for text embeddings
|
| 561 |
+
in_dim (`int`, *optional*, defaults to 16):
|
| 562 |
+
Input video channels (C_in)
|
| 563 |
+
dim (`int`, *optional*, defaults to 2048):
|
| 564 |
+
Hidden dimension of the transformer
|
| 565 |
+
ffn_dim (`int`, *optional*, defaults to 8192):
|
| 566 |
+
Intermediate dimension in feed-forward network
|
| 567 |
+
freq_dim (`int`, *optional*, defaults to 256):
|
| 568 |
+
Dimension for sinusoidal time embeddings
|
| 569 |
+
text_dim (`int`, *optional*, defaults to 4096):
|
| 570 |
+
Input dimension for text embeddings
|
| 571 |
+
out_dim (`int`, *optional*, defaults to 16):
|
| 572 |
+
Output video channels (C_out)
|
| 573 |
+
num_heads (`int`, *optional*, defaults to 16):
|
| 574 |
+
Number of attention heads
|
| 575 |
+
num_layers (`int`, *optional*, defaults to 32):
|
| 576 |
+
Number of transformer blocks
|
| 577 |
+
window_size (`tuple`, *optional*, defaults to (-1, -1)):
|
| 578 |
+
Window size for local attention (-1 indicates global attention)
|
| 579 |
+
qk_norm (`bool`, *optional*, defaults to True):
|
| 580 |
+
Enable query/key normalization
|
| 581 |
+
cross_attn_norm (`bool`, *optional*, defaults to False):
|
| 582 |
+
Enable cross-attention normalization
|
| 583 |
+
eps (`float`, *optional*, defaults to 1e-6):
|
| 584 |
+
Epsilon value for normalization layers
|
| 585 |
+
"""
|
| 586 |
+
|
| 587 |
+
super().__init__()
|
| 588 |
+
|
| 589 |
+
assert model_type in ['t2v', 'i2v']
|
| 590 |
+
self.model_type = model_type
|
| 591 |
+
|
| 592 |
+
self.patch_size = patch_size
|
| 593 |
+
self.text_len = text_len
|
| 594 |
+
self.in_dim = in_dim
|
| 595 |
+
self.dim = dim
|
| 596 |
+
self.ffn_dim = ffn_dim
|
| 597 |
+
self.freq_dim = freq_dim
|
| 598 |
+
self.text_dim = text_dim
|
| 599 |
+
self.out_dim = out_dim
|
| 600 |
+
self.num_heads = num_heads
|
| 601 |
+
self.num_layers = num_layers
|
| 602 |
+
self.window_size = window_size
|
| 603 |
+
self.qk_norm = qk_norm
|
| 604 |
+
self.cross_attn_norm = cross_attn_norm
|
| 605 |
+
self.eps = eps
|
| 606 |
+
|
| 607 |
+
# embeddings
|
| 608 |
+
self.patch_embedding = nn.Conv3d(
|
| 609 |
+
in_dim, dim, kernel_size=patch_size, stride=patch_size)
|
| 610 |
+
self.text_embedding = nn.Sequential(
|
| 611 |
+
nn.Linear(text_dim, dim), nn.GELU(approximate='tanh'),
|
| 612 |
+
nn.Linear(dim, dim))
|
| 613 |
+
|
| 614 |
+
self.time_embedding = nn.Sequential(
|
| 615 |
+
nn.Linear(freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
|
| 616 |
+
self.time_projection = nn.Sequential(
|
| 617 |
+
nn.SiLU(), nn.Linear(dim, dim * 6))
|
| 618 |
+
|
| 619 |
+
# blocks
|
| 620 |
+
cross_attn_type = 't2v_cross_attn' if model_type == 't2v' else 'i2v_cross_attn'
|
| 621 |
+
self.blocks = nn.ModuleList([
|
| 622 |
+
CausalWanAttentionBlock(cross_attn_type, dim, ffn_dim, num_heads,
|
| 623 |
+
window_size, qk_norm, cross_attn_norm, eps)
|
| 624 |
+
for _ in range(num_layers)
|
| 625 |
+
])
|
| 626 |
+
|
| 627 |
+
# head
|
| 628 |
+
self.head = CausalHead(dim, out_dim, patch_size, eps)
|
| 629 |
+
|
| 630 |
+
# buffers (don't use register_buffer otherwise dtype will be changed in to())
|
| 631 |
+
assert (dim % num_heads) == 0 and (dim // num_heads) % 2 == 0
|
| 632 |
+
d = dim // num_heads
|
| 633 |
+
self.freqs = torch.cat([
|
| 634 |
+
rope_params(1024, d - 4 * (d // 6)),
|
| 635 |
+
rope_params(1024, 2 * (d // 6)),
|
| 636 |
+
rope_params(1024, 2 * (d // 6))
|
| 637 |
+
],
|
| 638 |
+
dim=1)
|
| 639 |
+
|
| 640 |
+
if model_type == 'i2v':
|
| 641 |
+
self.img_emb = MLPProj(1280, dim)
|
| 642 |
+
|
| 643 |
+
# initialize weights
|
| 644 |
+
self.init_weights()
|
| 645 |
+
|
| 646 |
+
self.gradient_checkpointing = False
|
| 647 |
+
|
| 648 |
+
self.block_mask = None
|
| 649 |
+
|
| 650 |
+
self.num_frame_per_block = 1
|
| 651 |
+
|
| 652 |
+
def _set_gradient_checkpointing(self, module, value=False):
|
| 653 |
+
self.gradient_checkpointing = value
|
| 654 |
+
|
| 655 |
+
@staticmethod
|
| 656 |
+
def _prepare_blockwise_causal_attn_mask(
|
| 657 |
+
device: torch.device | str, num_frames: int = 21,
|
| 658 |
+
frame_seqlen: int = 1560, num_frame_per_block=1
|
| 659 |
+
) -> BlockMask:
|
| 660 |
+
"""
|
| 661 |
+
we will divide the token sequence into the following format
|
| 662 |
+
[1 latent frame] [1 latent frame] ... [1 latent frame]
|
| 663 |
+
We use flexattention to construct the attention mask
|
| 664 |
+
"""
|
| 665 |
+
total_length = num_frames * frame_seqlen
|
| 666 |
+
|
| 667 |
+
# we do right padding to get to a multiple of 128
|
| 668 |
+
padded_length = math.ceil(total_length / 128) * 128 - total_length
|
| 669 |
+
|
| 670 |
+
ends = torch.zeros(total_length + padded_length,
|
| 671 |
+
device=device, dtype=torch.long)
|
| 672 |
+
|
| 673 |
+
# Block-wise causal mask will attend to all elements that are before the end of the current chunk
|
| 674 |
+
frame_indices = torch.arange(
|
| 675 |
+
start=0,
|
| 676 |
+
end=total_length,
|
| 677 |
+
step=frame_seqlen * num_frame_per_block,
|
| 678 |
+
device=device
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
for tmp in frame_indices:
|
| 682 |
+
ends[tmp:tmp + frame_seqlen * num_frame_per_block] = tmp + \
|
| 683 |
+
frame_seqlen * num_frame_per_block
|
| 684 |
+
|
| 685 |
+
def attention_mask(b, h, q_idx, kv_idx):
|
| 686 |
+
return (kv_idx < ends[q_idx]) | (q_idx == kv_idx)
|
| 687 |
+
# return ((kv_idx < total_length) & (q_idx < total_length)) | (q_idx == kv_idx) # bidirectional mask
|
| 688 |
+
|
| 689 |
+
block_mask = create_block_mask(attention_mask, B=None, H=None, Q_LEN=total_length + padded_length,
|
| 690 |
+
KV_LEN=total_length + padded_length, _compile=False, device=device)
|
| 691 |
+
|
| 692 |
+
import torch.distributed as dist
|
| 693 |
+
if not dist.is_initialized() or dist.get_rank() == 0:
|
| 694 |
+
print(
|
| 695 |
+
f" cache a block wise causal mask with block size of {num_frame_per_block} frames")
|
| 696 |
+
print(block_mask)
|
| 697 |
+
|
| 698 |
+
return block_mask
|
| 699 |
+
|
| 700 |
+
def _forward_inference(
|
| 701 |
+
self,
|
| 702 |
+
x,
|
| 703 |
+
t,
|
| 704 |
+
context,
|
| 705 |
+
seq_len,
|
| 706 |
+
clip_fea=None,
|
| 707 |
+
y=None,
|
| 708 |
+
kv_cache: dict = None,
|
| 709 |
+
crossattn_cache: dict = None,
|
| 710 |
+
current_start: int = 0,
|
| 711 |
+
current_end: int = 0,
|
| 712 |
+
block_mode: str = 'input',
|
| 713 |
+
block_num: int = [-1],
|
| 714 |
+
patched_x_shape: torch.Tensor = None,
|
| 715 |
+
):
|
| 716 |
+
r"""
|
| 717 |
+
Run the diffusion model with kv caching.
|
| 718 |
+
See Algorithm 2 of CausVid paper https://arxiv.org/abs/2412.07772 for details.
|
| 719 |
+
This function will be run for num_frame times.
|
| 720 |
+
Process the latent frames one by one (1560 tokens each)
|
| 721 |
+
|
| 722 |
+
Args:
|
| 723 |
+
x (List[Tensor]):
|
| 724 |
+
List of input video tensors, each with shape [C_in, F, H, W]
|
| 725 |
+
t (Tensor):
|
| 726 |
+
Diffusion timesteps tensor of shape [B]
|
| 727 |
+
context (List[Tensor]):
|
| 728 |
+
List of text embeddings each with shape [L, C]
|
| 729 |
+
seq_len (`int`):
|
| 730 |
+
Maximum sequence length for positional encoding
|
| 731 |
+
clip_fea (Tensor, *optional*):
|
| 732 |
+
CLIP image features for image-to-video mode
|
| 733 |
+
y (List[Tensor], *optional*):
|
| 734 |
+
Conditional video inputs for image-to-video mode, same shape as x
|
| 735 |
+
|
| 736 |
+
Returns:
|
| 737 |
+
List[Tensor]:
|
| 738 |
+
List of denoised video tensors with original input shapes [C_out, F, H / 8, W / 8]
|
| 739 |
+
"""
|
| 740 |
+
if self.model_type == 'i2v':
|
| 741 |
+
assert clip_fea is not None and y is not None
|
| 742 |
+
# params
|
| 743 |
+
device = self.patch_embedding.weight.device
|
| 744 |
+
if self.freqs.device != device:
|
| 745 |
+
self.freqs = self.freqs.to(device)
|
| 746 |
+
|
| 747 |
+
if block_mode == 'input':
|
| 748 |
+
if y is not None:
|
| 749 |
+
x = [torch.cat([u, v], dim=0) for u, v in zip(x, y)]
|
| 750 |
+
|
| 751 |
+
# embeddings
|
| 752 |
+
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
|
| 753 |
+
bsz, cch, tlen, hh, ww = x[0].shape
|
| 754 |
+
patched_x_shape = torch.tensor([bsz, cch, tlen, hh, ww], dtype=torch.int64, device=device)
|
| 755 |
+
else:
|
| 756 |
+
bsz, cch, tlen, hh, ww = [int(i) for i in patched_x_shape.tolist()]
|
| 757 |
+
x = [u.permute(1,0).reshape(bsz, cch, tlen, hh, ww) for u in x]
|
| 758 |
+
|
| 759 |
+
grid_sizes = torch.stack(
|
| 760 |
+
[torch.tensor(u.shape[2:], dtype=torch.long) for u in x])
|
| 761 |
+
x = [u.flatten(2).transpose(1, 2) for u in x]
|
| 762 |
+
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long)
|
| 763 |
+
assert seq_lens.max() <= seq_len
|
| 764 |
+
x = torch.cat(x)
|
| 765 |
+
"""
|
| 766 |
+
torch.cat([
|
| 767 |
+
torch.cat([u, u.new_zeros(1, seq_len - u.size(1), u.size(2))],
|
| 768 |
+
dim=1) for u in x
|
| 769 |
+
])
|
| 770 |
+
"""
|
| 771 |
+
|
| 772 |
+
# time embeddings
|
| 773 |
+
# with amp.autocast(dtype=torch.float32):
|
| 774 |
+
e = self.time_embedding(
|
| 775 |
+
sinusoidal_embedding_1d(self.freq_dim, t.flatten()).type_as(x))
|
| 776 |
+
e0 = self.time_projection(e).unflatten(
|
| 777 |
+
1, (6, self.dim)).unflatten(dim=0, sizes=t.shape)
|
| 778 |
+
# assert e.dtype == torch.float32 and e0.dtype == torch.float32
|
| 779 |
+
|
| 780 |
+
# context
|
| 781 |
+
context_lens = None
|
| 782 |
+
context = self.text_embedding(
|
| 783 |
+
torch.stack([
|
| 784 |
+
torch.cat(
|
| 785 |
+
[u, u.new_zeros(self.text_len - u.size(0), u.size(1))])
|
| 786 |
+
for u in context
|
| 787 |
+
]))
|
| 788 |
+
|
| 789 |
+
if clip_fea is not None:
|
| 790 |
+
context_clip = self.img_emb(clip_fea) # bs x 257 x dim
|
| 791 |
+
context = torch.concat([context_clip, context], dim=1)
|
| 792 |
+
|
| 793 |
+
# arguments
|
| 794 |
+
kwargs = dict(
|
| 795 |
+
e=e0,
|
| 796 |
+
seq_lens=seq_lens,
|
| 797 |
+
grid_sizes=grid_sizes,
|
| 798 |
+
freqs=self.freqs,
|
| 799 |
+
context=context,
|
| 800 |
+
context_lens=context_lens,
|
| 801 |
+
block_mask=self.block_mask
|
| 802 |
+
)
|
| 803 |
+
if kv_cache is not None:
|
| 804 |
+
kwargs["causal_rope_cache"] = _prepare_causal_rope_cache(
|
| 805 |
+
grid_sizes,
|
| 806 |
+
self.freqs,
|
| 807 |
+
start_frame=current_start // math.prod(grid_sizes[0][1:]).item(),
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
def create_custom_forward(module):
|
| 811 |
+
def custom_forward(*inputs, **kwargs):
|
| 812 |
+
return module(*inputs, **kwargs)
|
| 813 |
+
return custom_forward
|
| 814 |
+
|
| 815 |
+
for block_index, block in enumerate(self.blocks):
|
| 816 |
+
if torch.is_grad_enabled() and self.gradient_checkpointing:
|
| 817 |
+
assert False
|
| 818 |
+
else:
|
| 819 |
+
if (block_mode == 'output' or block_mode == 'middle') and block_index < block_num[0]:
|
| 820 |
+
continue
|
| 821 |
+
if (block_mode == 'input' or block_mode == 'middle') and block_index == block_num[-1]:
|
| 822 |
+
return x, patched_x_shape
|
| 823 |
+
kwargs.update(
|
| 824 |
+
{
|
| 825 |
+
"kv_cache": kv_cache[block_index],
|
| 826 |
+
"crossattn_cache": crossattn_cache[block_index],
|
| 827 |
+
"current_start": current_start,
|
| 828 |
+
"current_end": current_end
|
| 829 |
+
}
|
| 830 |
+
)
|
| 831 |
+
x = block(x, **kwargs)
|
| 832 |
+
if block_mode == 'input' and block_num[-1] == len(self.blocks):
|
| 833 |
+
return x, patched_x_shape
|
| 834 |
+
|
| 835 |
+
# head
|
| 836 |
+
x = self.head(x, e.unflatten(dim=0, sizes=t.shape).unsqueeze(2))
|
| 837 |
+
|
| 838 |
+
# unpatchify
|
| 839 |
+
x = self.unpatchify(x, grid_sizes)
|
| 840 |
+
return torch.stack(x)
|
| 841 |
+
|
| 842 |
+
def _forward_train(
|
| 843 |
+
self,
|
| 844 |
+
x,
|
| 845 |
+
t,
|
| 846 |
+
context,
|
| 847 |
+
seq_len,
|
| 848 |
+
clip_fea=None,
|
| 849 |
+
y=None,
|
| 850 |
+
):
|
| 851 |
+
r"""
|
| 852 |
+
Forward pass through the diffusion model
|
| 853 |
+
|
| 854 |
+
Args:
|
| 855 |
+
x (List[Tensor]):
|
| 856 |
+
List of input video tensors, each with shape [C_in, F, H, W]
|
| 857 |
+
t (Tensor):
|
| 858 |
+
Diffusion timesteps tensor of shape [B]
|
| 859 |
+
context (List[Tensor]):
|
| 860 |
+
List of text embeddings each with shape [L, C]
|
| 861 |
+
seq_len (`int`):
|
| 862 |
+
Maximum sequence length for positional encoding
|
| 863 |
+
clip_fea (Tensor, *optional*):
|
| 864 |
+
CLIP image features for image-to-video mode
|
| 865 |
+
y (List[Tensor], *optional*):
|
| 866 |
+
Conditional video inputs for image-to-video mode, same shape as x
|
| 867 |
+
|
| 868 |
+
Returns:
|
| 869 |
+
List[Tensor]:
|
| 870 |
+
List of denoised video tensors with original input shapes [C_out, F, H / 8, W / 8]
|
| 871 |
+
"""
|
| 872 |
+
if self.model_type == 'i2v':
|
| 873 |
+
assert clip_fea is not None and y is not None
|
| 874 |
+
# params
|
| 875 |
+
device = self.patch_embedding.weight.device
|
| 876 |
+
if self.freqs.device != device:
|
| 877 |
+
self.freqs = self.freqs.to(device)
|
| 878 |
+
|
| 879 |
+
# Construct blockwise causal attn mask
|
| 880 |
+
if self.block_mask is None:
|
| 881 |
+
self.block_mask = self._prepare_blockwise_causal_attn_mask(
|
| 882 |
+
device, num_frames=x.shape[2],
|
| 883 |
+
frame_seqlen=x.shape[-2] *
|
| 884 |
+
x.shape[-1] // (self.patch_size[1] * self.patch_size[2]),
|
| 885 |
+
num_frame_per_block=self.num_frame_per_block
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
if y is not None:
|
| 889 |
+
x = [torch.cat([u, v], dim=0) for u, v in zip(x, y)]
|
| 890 |
+
|
| 891 |
+
# embeddings
|
| 892 |
+
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
|
| 893 |
+
grid_sizes = torch.stack(
|
| 894 |
+
[torch.tensor(u.shape[2:], dtype=torch.long) for u in x])
|
| 895 |
+
x = [u.flatten(2).transpose(1, 2) for u in x]
|
| 896 |
+
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long)
|
| 897 |
+
assert seq_lens.max() <= seq_len
|
| 898 |
+
x = torch.cat([
|
| 899 |
+
torch.cat([u, u.new_zeros(1, seq_len - u.size(1), u.size(2))],
|
| 900 |
+
dim=1) for u in x
|
| 901 |
+
])
|
| 902 |
+
|
| 903 |
+
# time embeddings
|
| 904 |
+
# with amp.autocast(dtype=torch.float32):
|
| 905 |
+
e = self.time_embedding(
|
| 906 |
+
sinusoidal_embedding_1d(self.freq_dim, t.flatten()).type_as(x))
|
| 907 |
+
e0 = self.time_projection(e).unflatten(
|
| 908 |
+
1, (6, self.dim)).unflatten(dim=0, sizes=t.shape)
|
| 909 |
+
# assert e.dtype == torch.float32 and e0.dtype == torch.float32
|
| 910 |
+
|
| 911 |
+
# context
|
| 912 |
+
context_lens = None
|
| 913 |
+
context = self.text_embedding(
|
| 914 |
+
torch.stack([
|
| 915 |
+
torch.cat(
|
| 916 |
+
[u, u.new_zeros(self.text_len - u.size(0), u.size(1))])
|
| 917 |
+
for u in context
|
| 918 |
+
]))
|
| 919 |
+
|
| 920 |
+
if clip_fea is not None:
|
| 921 |
+
context_clip = self.img_emb(clip_fea) # bs x 257 x dim
|
| 922 |
+
context = torch.concat([context_clip, context], dim=1)
|
| 923 |
+
|
| 924 |
+
# arguments
|
| 925 |
+
kwargs = dict(
|
| 926 |
+
e=e0,
|
| 927 |
+
seq_lens=seq_lens,
|
| 928 |
+
grid_sizes=grid_sizes,
|
| 929 |
+
freqs=self.freqs,
|
| 930 |
+
context=context,
|
| 931 |
+
context_lens=context_lens,
|
| 932 |
+
block_mask=self.block_mask)
|
| 933 |
+
|
| 934 |
+
def create_custom_forward(module):
|
| 935 |
+
def custom_forward(*inputs, **kwargs):
|
| 936 |
+
return module(*inputs, **kwargs)
|
| 937 |
+
return custom_forward
|
| 938 |
+
|
| 939 |
+
for block_index, block in enumerate(self.blocks):
|
| 940 |
+
if torch.is_grad_enabled() and self.gradient_checkpointing:
|
| 941 |
+
x = torch.utils.checkpoint.checkpoint(
|
| 942 |
+
create_custom_forward(block),
|
| 943 |
+
x, **kwargs,
|
| 944 |
+
use_reentrant=False,
|
| 945 |
+
)
|
| 946 |
+
else:
|
| 947 |
+
x = block(x, **kwargs)
|
| 948 |
+
|
| 949 |
+
# head
|
| 950 |
+
x = self.head(x, e.unflatten(dim=0, sizes=t.shape).unsqueeze(2))
|
| 951 |
+
|
| 952 |
+
# unpatchify
|
| 953 |
+
x = self.unpatchify(x, grid_sizes)
|
| 954 |
+
return torch.stack(x)
|
| 955 |
+
|
| 956 |
+
def forward(
|
| 957 |
+
self,
|
| 958 |
+
*args,
|
| 959 |
+
**kwargs
|
| 960 |
+
):
|
| 961 |
+
if kwargs.get('kv_cache', None) is not None:
|
| 962 |
+
return self._forward_inference(*args, **kwargs)
|
| 963 |
+
else:
|
| 964 |
+
return self._forward_train(*args, **kwargs)
|
| 965 |
+
|
| 966 |
+
def unpatchify(self, x, grid_sizes):
|
| 967 |
+
r"""
|
| 968 |
+
Reconstruct video tensors from patch embeddings.
|
| 969 |
+
|
| 970 |
+
Args:
|
| 971 |
+
x (List[Tensor]):
|
| 972 |
+
List of patchified features, each with shape [L, C_out * prod(patch_size)]
|
| 973 |
+
grid_sizes (Tensor):
|
| 974 |
+
Original spatial-temporal grid dimensions before patching,
|
| 975 |
+
shape [B, 3] (3 dimensions correspond to F_patches, H_patches, W_patches)
|
| 976 |
+
|
| 977 |
+
Returns:
|
| 978 |
+
List[Tensor]:
|
| 979 |
+
Reconstructed video tensors with shape [C_out, F, H / 8, W / 8]
|
| 980 |
+
"""
|
| 981 |
+
|
| 982 |
+
c = self.out_dim
|
| 983 |
+
out = []
|
| 984 |
+
for u, v in zip(x, grid_sizes.tolist()):
|
| 985 |
+
u = u[:math.prod(v)].view(*v, *self.patch_size, c)
|
| 986 |
+
u = torch.einsum('fhwpqrc->cfphqwr', u)
|
| 987 |
+
u = u.reshape(c, *[i * j for i, j in zip(v, self.patch_size)])
|
| 988 |
+
out.append(u)
|
| 989 |
+
return out
|
| 990 |
+
|
| 991 |
+
def init_weights(self):
|
| 992 |
+
r"""
|
| 993 |
+
Initialize model parameters using Xavier initialization.
|
| 994 |
+
"""
|
| 995 |
+
|
| 996 |
+
# basic init
|
| 997 |
+
for m in self.modules():
|
| 998 |
+
if isinstance(m, nn.Linear):
|
| 999 |
+
nn.init.xavier_uniform_(m.weight)
|
| 1000 |
+
if m.bias is not None:
|
| 1001 |
+
nn.init.zeros_(m.bias)
|
| 1002 |
+
|
| 1003 |
+
# init embeddings
|
| 1004 |
+
nn.init.xavier_uniform_(self.patch_embedding.weight.flatten(1))
|
| 1005 |
+
for m in self.text_embedding.modules():
|
| 1006 |
+
if isinstance(m, nn.Linear):
|
| 1007 |
+
nn.init.normal_(m.weight, std=.02)
|
| 1008 |
+
for m in self.time_embedding.modules():
|
| 1009 |
+
if isinstance(m, nn.Linear):
|
| 1010 |
+
nn.init.normal_(m.weight, std=.02)
|
| 1011 |
+
|
| 1012 |
+
# init output layer
|
| 1013 |
+
nn.init.zeros_(self.head.head.weight)
|
models/wan/causal_stream_inference.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models import (
|
| 2 |
+
get_diffusion_wrapper,
|
| 3 |
+
get_text_encoder_wrapper,
|
| 4 |
+
get_vae_wrapper
|
| 5 |
+
)
|
| 6 |
+
from models.wan.taehv_wrapper import TAEHVWanVAEWrapper
|
| 7 |
+
from typing import List
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
LOGGER = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
class CausalStreamInferencePipeline(torch.nn.Module):
|
| 15 |
+
def __init__(self, args, device):
|
| 16 |
+
super().__init__()
|
| 17 |
+
model_type = args.model_type
|
| 18 |
+
self.device = device
|
| 19 |
+
# Step 1: Initialize all models
|
| 20 |
+
self.generator_model_name = getattr(
|
| 21 |
+
args, "generator_name", args.model_name)
|
| 22 |
+
self.generator = get_diffusion_wrapper(
|
| 23 |
+
model_name=self.generator_model_name)(model_type=model_type)
|
| 24 |
+
self.text_encoder = get_text_encoder_wrapper(
|
| 25 |
+
model_name=args.model_name)(model_type=model_type)
|
| 26 |
+
if getattr(args, "use_taehv", False):
|
| 27 |
+
LOGGER.info("Using TAEHV VAE wrapper for Wan inference")
|
| 28 |
+
self.vae = TAEHVWanVAEWrapper(
|
| 29 |
+
model_type=model_type,
|
| 30 |
+
checkpoint_path=getattr(args, "taehv_checkpoint_path", None),
|
| 31 |
+
use_tensorrt=getattr(args, "use_tensorrt", False),
|
| 32 |
+
)
|
| 33 |
+
else:
|
| 34 |
+
self.vae = get_vae_wrapper(model_name=args.model_name)(model_type=model_type)
|
| 35 |
+
|
| 36 |
+
# Step 2: Initialize all causal hyperparmeters
|
| 37 |
+
self._init_denoising_step_list(args, device)
|
| 38 |
+
|
| 39 |
+
if model_type == "T2V-1.3B":
|
| 40 |
+
self.num_transformer_blocks = 30
|
| 41 |
+
self.num_heads = 12
|
| 42 |
+
elif model_type == "T2V-14B":
|
| 43 |
+
self.num_transformer_blocks = 40
|
| 44 |
+
self.num_heads = 40
|
| 45 |
+
else:
|
| 46 |
+
raise ValueError(f"Model type {model_type} not supported")
|
| 47 |
+
scale_size = 16
|
| 48 |
+
self.height = args.height//scale_size*2
|
| 49 |
+
self.width = args.width//scale_size*2
|
| 50 |
+
self.frame_seq_length = (args.height//scale_size) * (args.width//scale_size)
|
| 51 |
+
self.num_kv_cache = args.num_kv_cache
|
| 52 |
+
self.kv_cache_length = self.frame_seq_length*self.num_kv_cache
|
| 53 |
+
self.num_sink_tokens = args.num_sink_tokens
|
| 54 |
+
self.adapt_sink_threshold = args.adapt_sink_threshold
|
| 55 |
+
|
| 56 |
+
self.conditional_dict = None
|
| 57 |
+
self.kv_cache1 = None
|
| 58 |
+
self.kv_cache2 = None
|
| 59 |
+
self.hidden_states = None
|
| 60 |
+
self.block_x = None
|
| 61 |
+
self.args = args
|
| 62 |
+
self.num_frame_per_block = getattr(
|
| 63 |
+
args, "num_frame_per_block", 1)
|
| 64 |
+
|
| 65 |
+
LOGGER.info("KV inference with %s frames per block", self.num_frame_per_block)
|
| 66 |
+
|
| 67 |
+
if self.num_frame_per_block > 1:
|
| 68 |
+
self.generator.model.num_frame_per_block = self.num_frame_per_block
|
| 69 |
+
|
| 70 |
+
self.generator.model.to(self.device)
|
| 71 |
+
|
| 72 |
+
def _init_denoising_step_list(self, args, device):
|
| 73 |
+
self.denoising_step_list = torch.tensor(
|
| 74 |
+
args.denoising_step_list, dtype=torch.long, device=device)
|
| 75 |
+
assert self.denoising_step_list[-1] == 0
|
| 76 |
+
if not args.t2v:
|
| 77 |
+
# remove the last timestep (which equals zero)
|
| 78 |
+
self.denoising_step_list = self.denoising_step_list[:-1]
|
| 79 |
+
|
| 80 |
+
self.scheduler = self.generator.get_scheduler()
|
| 81 |
+
if args.warp_denoising_step: # Warp the denoising step according to the scheduler time shift
|
| 82 |
+
timesteps = torch.cat(
|
| 83 |
+
(self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))
|
| 84 |
+
).to(device)
|
| 85 |
+
self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
|
| 86 |
+
|
| 87 |
+
def _initialize_kv_cache(self, batch_size, dtype, device):
|
| 88 |
+
"""
|
| 89 |
+
Initialize a Per-GPU KV cache for the Wan model.
|
| 90 |
+
"""
|
| 91 |
+
kv_cache1 = []
|
| 92 |
+
|
| 93 |
+
for i in range(self.num_transformer_blocks):
|
| 94 |
+
cache_length = self.kv_cache_length
|
| 95 |
+
self.generator.model.blocks[i].self_attn.sink_size = self.num_sink_tokens
|
| 96 |
+
self.generator.model.blocks[i].self_attn.adapt_sink_thr = self.adapt_sink_threshold
|
| 97 |
+
|
| 98 |
+
kv_cache1.append({
|
| 99 |
+
"k": torch.zeros([batch_size, cache_length, self.num_heads, 128], dtype=dtype, device=device),
|
| 100 |
+
"v": torch.zeros([batch_size, cache_length, self.num_heads, 128], dtype=dtype, device=device),
|
| 101 |
+
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 102 |
+
"local_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 103 |
+
"total_steps": len(self.denoising_step_list),
|
| 104 |
+
"current_step": len(self.denoising_step_list),
|
| 105 |
+
})
|
| 106 |
+
|
| 107 |
+
self.kv_cache1 = kv_cache1 # always store the clean cache
|
| 108 |
+
|
| 109 |
+
def _initialize_crossattn_cache(self, batch_size, dtype, device):
|
| 110 |
+
"""
|
| 111 |
+
Initialize a Per-GPU cross-attention cache for the Wan model.
|
| 112 |
+
"""
|
| 113 |
+
crossattn_cache = []
|
| 114 |
+
|
| 115 |
+
for _ in range(self.num_transformer_blocks):
|
| 116 |
+
crossattn_cache.append({
|
| 117 |
+
"k": torch.zeros([batch_size, 512, self.num_heads, 128], dtype=dtype, device=device),
|
| 118 |
+
"v": torch.zeros([batch_size, 512, self.num_heads, 128], dtype=dtype, device=device),
|
| 119 |
+
"is_init": False,
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
self.crossattn_cache = crossattn_cache # always store the clean cache
|
| 123 |
+
|
| 124 |
+
def prepare(
|
| 125 |
+
self,
|
| 126 |
+
text_prompts: List[str],
|
| 127 |
+
device: torch.device,
|
| 128 |
+
dtype: torch.dtype,
|
| 129 |
+
block_mode: str='input',
|
| 130 |
+
noise: torch.Tensor = None,
|
| 131 |
+
current_start: int = 0,
|
| 132 |
+
current_end: int = None,
|
| 133 |
+
block_num: torch.Tensor = None,
|
| 134 |
+
batch_denoise: bool=True,
|
| 135 |
+
):
|
| 136 |
+
self.device = device
|
| 137 |
+
batch_size = noise.shape[0]
|
| 138 |
+
|
| 139 |
+
self.conditional_dict = self.text_encoder(
|
| 140 |
+
text_prompts=text_prompts
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Step 1: Initialize KV cache
|
| 144 |
+
if self.kv_cache1 is None:
|
| 145 |
+
self._initialize_kv_cache(
|
| 146 |
+
batch_size=batch_size,
|
| 147 |
+
dtype=dtype,
|
| 148 |
+
device=device
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
self._initialize_crossattn_cache(
|
| 152 |
+
batch_size=batch_size,
|
| 153 |
+
dtype=dtype,
|
| 154 |
+
device=device
|
| 155 |
+
)
|
| 156 |
+
else:
|
| 157 |
+
# reset cross attn cache
|
| 158 |
+
for block_index in range(self.num_transformer_blocks):
|
| 159 |
+
self.crossattn_cache[block_index]["is_init"] = False
|
| 160 |
+
|
| 161 |
+
current_start = torch.tensor([current_start], dtype=torch.long, device=device)
|
| 162 |
+
current_end = torch.tensor([current_end], dtype=torch.long, device=device)
|
| 163 |
+
|
| 164 |
+
for index, current_timestep in enumerate(self.denoising_step_list):
|
| 165 |
+
# set current timestep
|
| 166 |
+
timestep = torch.ones(
|
| 167 |
+
[batch_size, noise.shape[1]], device=noise.device, dtype=torch.int64) * current_timestep
|
| 168 |
+
|
| 169 |
+
if index < len(self.denoising_step_list) - 1:
|
| 170 |
+
denoised_pred = self.generator(
|
| 171 |
+
noisy_image_or_video=noise,
|
| 172 |
+
conditional_dict=self.conditional_dict,
|
| 173 |
+
timestep=timestep,
|
| 174 |
+
kv_cache=self.kv_cache1,
|
| 175 |
+
crossattn_cache=self.crossattn_cache,
|
| 176 |
+
current_start=current_start,
|
| 177 |
+
current_end=current_end
|
| 178 |
+
)
|
| 179 |
+
next_timestep = self.denoising_step_list[index + 1]
|
| 180 |
+
noise = self.scheduler.add_noise(
|
| 181 |
+
denoised_pred.flatten(0, 1),
|
| 182 |
+
torch.randn_like(denoised_pred.flatten(0, 1)),
|
| 183 |
+
next_timestep *
|
| 184 |
+
torch.ones([batch_size], device=noise.device,
|
| 185 |
+
dtype=torch.long)
|
| 186 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 187 |
+
else:
|
| 188 |
+
# for getting real output
|
| 189 |
+
denoised_pred = self.generator(
|
| 190 |
+
noisy_image_or_video=noise,
|
| 191 |
+
conditional_dict=self.conditional_dict,
|
| 192 |
+
timestep=timestep,
|
| 193 |
+
kv_cache=self.kv_cache1,
|
| 194 |
+
crossattn_cache=self.crossattn_cache,
|
| 195 |
+
current_start=current_start,
|
| 196 |
+
current_end=current_end
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
if not batch_denoise:
|
| 200 |
+
return denoised_pred
|
| 201 |
+
|
| 202 |
+
# Pre-allocate hidden_states tensor to avoid memory allocation during inference
|
| 203 |
+
self.batch_size = len(self.denoising_step_list)
|
| 204 |
+
|
| 205 |
+
# Determine which blocks to keep based on block_num range
|
| 206 |
+
blocks_to_keep = []
|
| 207 |
+
if block_num is not None:
|
| 208 |
+
start_block, end_block = block_num[0].item(), block_num[1].item()
|
| 209 |
+
blocks_to_keep = list(range(start_block, end_block))
|
| 210 |
+
else:
|
| 211 |
+
blocks_to_keep = list(range(self.num_transformer_blocks))
|
| 212 |
+
|
| 213 |
+
# Process only the blocks in the specified range
|
| 214 |
+
for i in range(self.num_transformer_blocks):
|
| 215 |
+
if dist.is_initialized():
|
| 216 |
+
dist.broadcast(self.crossattn_cache[i]['k'], src=0)
|
| 217 |
+
dist.broadcast(self.crossattn_cache[i]['v'], src=0)
|
| 218 |
+
dist.broadcast(self.kv_cache1[i]['k'], src=0)
|
| 219 |
+
dist.broadcast(self.kv_cache1[i]['v'], src=0)
|
| 220 |
+
|
| 221 |
+
self.kv_cache1[i]['k'] = self.kv_cache1[i]['k'].repeat(self.batch_size, 1, 1, 1)
|
| 222 |
+
self.kv_cache1[i]['v'] = self.kv_cache1[i]['v'].repeat(self.batch_size, 1, 1, 1)
|
| 223 |
+
|
| 224 |
+
self.kv_cache1[i]['global_end_index'] = self.kv_cache1[i]['global_end_index'].repeat(self.batch_size)
|
| 225 |
+
self.kv_cache1[i]['local_end_index'] = self.kv_cache1[i]['local_end_index'].repeat(self.batch_size)
|
| 226 |
+
|
| 227 |
+
self.crossattn_cache[i]['k'] = self.crossattn_cache[i]['k'].expand(self.batch_size, -1, -1, -1)
|
| 228 |
+
self.crossattn_cache[i]['v'] = self.crossattn_cache[i]['v'].expand(self.batch_size, -1, -1, -1)
|
| 229 |
+
|
| 230 |
+
# Remove blocks outside the range
|
| 231 |
+
if block_num is not None:
|
| 232 |
+
for i in range(self.num_transformer_blocks):
|
| 233 |
+
if i not in blocks_to_keep:
|
| 234 |
+
self.kv_cache1[i]['k'] = self.kv_cache1[i]['k'].cpu()
|
| 235 |
+
self.kv_cache1[i]['v'] = self.kv_cache1[i]['v'].cpu()
|
| 236 |
+
|
| 237 |
+
self.hidden_states = torch.zeros(
|
| 238 |
+
(self.batch_size, self.num_frame_per_block, *noise.shape[2:]), dtype=noise.dtype, device=device
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
if block_mode in ['output', 'middle']:
|
| 242 |
+
self.block_x = torch.zeros(
|
| 243 |
+
(self.batch_size, self.frame_seq_length, self.num_heads*128), dtype=noise.dtype, device=device
|
| 244 |
+
)
|
| 245 |
+
else:
|
| 246 |
+
self.block_x = None
|
| 247 |
+
|
| 248 |
+
self.kv_cache_starts = torch.ones(self.batch_size, dtype=torch.long, device=device) * current_end
|
| 249 |
+
self.kv_cache_ends = torch.ones(self.batch_size, dtype=torch.long, device=device) * current_end + self.frame_seq_length
|
| 250 |
+
|
| 251 |
+
self.timestep = self.denoising_step_list
|
| 252 |
+
|
| 253 |
+
self.conditional_dict['prompt_embeds'] = self.conditional_dict['prompt_embeds'].repeat(self.batch_size, 1, 1)
|
| 254 |
+
|
| 255 |
+
return denoised_pred
|
| 256 |
+
|
| 257 |
+
def inference_stream(
|
| 258 |
+
self,
|
| 259 |
+
noise: torch.Tensor,
|
| 260 |
+
current_start: int,
|
| 261 |
+
current_end: int,
|
| 262 |
+
current_step: int,
|
| 263 |
+
) -> torch.Tensor:
|
| 264 |
+
|
| 265 |
+
self.hidden_states[1:] = self.hidden_states[:-1].clone()
|
| 266 |
+
self.hidden_states[0] = noise[0]
|
| 267 |
+
|
| 268 |
+
self.kv_cache_starts[1:] = self.kv_cache_starts[:-1].clone()
|
| 269 |
+
self.kv_cache_starts[0] = current_start
|
| 270 |
+
|
| 271 |
+
self.kv_cache_ends[1:] = self.kv_cache_ends[:-1].clone()
|
| 272 |
+
self.kv_cache_ends[0] = current_end
|
| 273 |
+
|
| 274 |
+
if current_step is not None:
|
| 275 |
+
self.timestep[0] = current_step
|
| 276 |
+
|
| 277 |
+
self.hidden_states = self.generator(
|
| 278 |
+
noisy_image_or_video=self.hidden_states,
|
| 279 |
+
conditional_dict=self.conditional_dict,
|
| 280 |
+
timestep=self.timestep.unsqueeze(1).expand(-1, self.hidden_states.shape[1]),
|
| 281 |
+
kv_cache=self.kv_cache1,
|
| 282 |
+
crossattn_cache=self.crossattn_cache,
|
| 283 |
+
current_start=self.kv_cache_starts,
|
| 284 |
+
current_end=self.kv_cache_ends,
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
for i in range(len(self.denoising_step_list) - 1):
|
| 288 |
+
self.hidden_states[[i]] = self.scheduler.add_noise(
|
| 289 |
+
self.hidden_states[[i]],
|
| 290 |
+
torch.randn_like(self.hidden_states[[i]]),
|
| 291 |
+
self.denoising_step_list[i + 1] *
|
| 292 |
+
torch.ones([1], device=self.hidden_states.device,
|
| 293 |
+
dtype=torch.long)
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
return self.hidden_states
|
| 297 |
+
|
| 298 |
+
def inference_wo_batch(
|
| 299 |
+
self,
|
| 300 |
+
noise: torch.Tensor,
|
| 301 |
+
current_start: int,
|
| 302 |
+
current_end: int,
|
| 303 |
+
current_step: int,
|
| 304 |
+
) -> torch.Tensor:
|
| 305 |
+
|
| 306 |
+
batch_size = noise.shape[0]
|
| 307 |
+
|
| 308 |
+
current_start = torch.ones(batch_size, dtype=torch.long, device=self.device) * current_start
|
| 309 |
+
current_end = torch.ones(batch_size, dtype=torch.long, device=self.device) * current_end
|
| 310 |
+
|
| 311 |
+
# Step 2.1: Spatial denoising loop
|
| 312 |
+
self.denoising_step_list[0] = current_step
|
| 313 |
+
for index, current_timestep in enumerate(self.denoising_step_list):
|
| 314 |
+
# set current timestep
|
| 315 |
+
timestep = torch.ones(
|
| 316 |
+
[batch_size, noise.shape[1]], device=noise.device, dtype=torch.int64) * current_timestep
|
| 317 |
+
|
| 318 |
+
if index < len(self.denoising_step_list) - 1:
|
| 319 |
+
denoised_pred = self.generator(
|
| 320 |
+
noisy_image_or_video=noise,
|
| 321 |
+
conditional_dict=self.conditional_dict,
|
| 322 |
+
timestep=timestep,
|
| 323 |
+
kv_cache=self.kv_cache1,
|
| 324 |
+
crossattn_cache=self.crossattn_cache,
|
| 325 |
+
current_start=current_start,
|
| 326 |
+
current_end=current_end
|
| 327 |
+
)
|
| 328 |
+
next_timestep = self.denoising_step_list[index + 1]
|
| 329 |
+
noise = self.scheduler.add_noise(
|
| 330 |
+
denoised_pred.flatten(0, 1),
|
| 331 |
+
torch.randn_like(denoised_pred.flatten(0, 1)),
|
| 332 |
+
next_timestep *
|
| 333 |
+
torch.ones([batch_size], device=noise.device,
|
| 334 |
+
dtype=torch.long)
|
| 335 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 336 |
+
else:
|
| 337 |
+
# for getting real output
|
| 338 |
+
denoised_pred = self.generator(
|
| 339 |
+
noisy_image_or_video=noise,
|
| 340 |
+
conditional_dict=self.conditional_dict,
|
| 341 |
+
timestep=timestep,
|
| 342 |
+
kv_cache=self.kv_cache1,
|
| 343 |
+
crossattn_cache=self.crossattn_cache,
|
| 344 |
+
current_start=current_start,
|
| 345 |
+
current_end=current_end
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
return denoised_pred
|
| 349 |
+
|
| 350 |
+
def inference(
|
| 351 |
+
self,
|
| 352 |
+
noise: torch.Tensor,
|
| 353 |
+
current_start: int,
|
| 354 |
+
current_end: int,
|
| 355 |
+
current_step: int,
|
| 356 |
+
block_mode: str='input',
|
| 357 |
+
block_num=None,
|
| 358 |
+
patched_x_shape: torch.Tensor=None,
|
| 359 |
+
block_x: torch.Tensor=None,
|
| 360 |
+
) -> torch.Tensor:
|
| 361 |
+
|
| 362 |
+
if block_mode == 'input':
|
| 363 |
+
self.hidden_states[1:] = self.hidden_states[:-1].clone()
|
| 364 |
+
self.hidden_states[0] = noise[0]
|
| 365 |
+
|
| 366 |
+
self.kv_cache_starts[1:] = self.kv_cache_starts[:-1].clone()
|
| 367 |
+
self.kv_cache_starts[0] = current_start
|
| 368 |
+
|
| 369 |
+
self.kv_cache_ends[1:] = self.kv_cache_ends[:-1].clone()
|
| 370 |
+
self.kv_cache_ends[0] = current_end
|
| 371 |
+
else:
|
| 372 |
+
self.block_x.copy_(block_x)
|
| 373 |
+
self.hidden_states.copy_(noise)
|
| 374 |
+
self.kv_cache_starts.copy_(current_start)
|
| 375 |
+
self.kv_cache_ends.copy_(current_end)
|
| 376 |
+
|
| 377 |
+
if current_step is not None:
|
| 378 |
+
self.timestep[0] = current_step
|
| 379 |
+
|
| 380 |
+
if block_mode == 'output':
|
| 381 |
+
denoised_pred = self.generator.forward_output(
|
| 382 |
+
noisy_image_or_video=self.hidden_states,
|
| 383 |
+
conditional_dict=self.conditional_dict,
|
| 384 |
+
timestep=self.timestep.unsqueeze(1).expand(-1, self.hidden_states.shape[1]),
|
| 385 |
+
kv_cache=self.kv_cache1,
|
| 386 |
+
crossattn_cache=self.crossattn_cache,
|
| 387 |
+
current_start=self.kv_cache_starts,
|
| 388 |
+
current_end=self.kv_cache_ends,
|
| 389 |
+
block_mode=block_mode,
|
| 390 |
+
block_num=block_num,
|
| 391 |
+
patched_x_shape=patched_x_shape,
|
| 392 |
+
block_x=self.block_x
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
for i in range(len(self.denoising_step_list) - 1):
|
| 396 |
+
denoised_pred[[i]] = self.scheduler.add_noise(
|
| 397 |
+
denoised_pred[[i]],
|
| 398 |
+
torch.randn_like(denoised_pred[[i]]),
|
| 399 |
+
self.denoising_step_list[i + 1] *
|
| 400 |
+
torch.ones([1], device=denoised_pred.device,
|
| 401 |
+
dtype=torch.long)
|
| 402 |
+
)
|
| 403 |
+
patched_x_shape = None
|
| 404 |
+
|
| 405 |
+
else:
|
| 406 |
+
denoised_pred, patched_x_shape = self.generator.forward_input(
|
| 407 |
+
noisy_image_or_video=self.hidden_states,
|
| 408 |
+
conditional_dict=self.conditional_dict,
|
| 409 |
+
timestep=self.timestep.unsqueeze(1).expand(-1, self.hidden_states.shape[1]),
|
| 410 |
+
kv_cache=self.kv_cache1,
|
| 411 |
+
crossattn_cache=self.crossattn_cache,
|
| 412 |
+
current_start=self.kv_cache_starts,
|
| 413 |
+
current_end=self.kv_cache_ends,
|
| 414 |
+
block_mode=block_mode,
|
| 415 |
+
block_num=block_num,
|
| 416 |
+
patched_x_shape=patched_x_shape,
|
| 417 |
+
block_x=self.block_x,
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
return denoised_pred, patched_x_shape
|
models/wan/flow_match.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
The following code is copied from https://github.com/modelscope/DiffSynth-Studio/blob/main/diffsynth/schedulers/flow_match.py
|
| 3 |
+
"""
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class FlowMatchScheduler():
|
| 8 |
+
|
| 9 |
+
def __init__(self, num_inference_steps=100, num_train_timesteps=1000, shift=3.0, sigma_max=1.0, sigma_min=0.003 / 1.002, inverse_timesteps=False, extra_one_step=False, reverse_sigmas=False):
|
| 10 |
+
self.num_train_timesteps = num_train_timesteps
|
| 11 |
+
self.shift = shift
|
| 12 |
+
self.sigma_max = sigma_max
|
| 13 |
+
self.sigma_min = sigma_min
|
| 14 |
+
self.inverse_timesteps = inverse_timesteps
|
| 15 |
+
self.extra_one_step = extra_one_step
|
| 16 |
+
self.reverse_sigmas = reverse_sigmas
|
| 17 |
+
self.set_timesteps(num_inference_steps)
|
| 18 |
+
|
| 19 |
+
def set_timesteps(self, num_inference_steps=100, denoising_strength=1.0, training=False):
|
| 20 |
+
sigma_start = self.sigma_min + \
|
| 21 |
+
(self.sigma_max - self.sigma_min) * denoising_strength
|
| 22 |
+
if self.extra_one_step:
|
| 23 |
+
self.sigmas = torch.linspace(
|
| 24 |
+
sigma_start, self.sigma_min, num_inference_steps + 1)[:-1]
|
| 25 |
+
else:
|
| 26 |
+
self.sigmas = torch.linspace(
|
| 27 |
+
sigma_start, self.sigma_min, num_inference_steps)
|
| 28 |
+
if self.inverse_timesteps:
|
| 29 |
+
self.sigmas = torch.flip(self.sigmas, dims=[0])
|
| 30 |
+
self.sigmas = self.shift * self.sigmas / \
|
| 31 |
+
(1 + (self.shift - 1) * self.sigmas)
|
| 32 |
+
if self.reverse_sigmas:
|
| 33 |
+
self.sigmas = 1 - self.sigmas
|
| 34 |
+
self.timesteps = self.sigmas * self.num_train_timesteps
|
| 35 |
+
if training:
|
| 36 |
+
x = self.timesteps
|
| 37 |
+
y = torch.exp(-2 * ((x - num_inference_steps / 2) /
|
| 38 |
+
num_inference_steps) ** 2)
|
| 39 |
+
y_shifted = y - y.min()
|
| 40 |
+
bsmntw_weighing = y_shifted * \
|
| 41 |
+
(num_inference_steps / y_shifted.sum())
|
| 42 |
+
self.linear_timesteps_weights = bsmntw_weighing
|
| 43 |
+
|
| 44 |
+
def step(self, model_output, timestep, sample, to_final=False):
|
| 45 |
+
self.sigmas = self.sigmas.to(model_output.device)
|
| 46 |
+
self.timesteps = self.timesteps.to(model_output.device)
|
| 47 |
+
timestep_id = torch.argmin(
|
| 48 |
+
(self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
|
| 49 |
+
sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
|
| 50 |
+
if to_final or (timestep_id + 1 >= len(self.timesteps)).any():
|
| 51 |
+
sigma_ = 1 if (
|
| 52 |
+
self.inverse_timesteps or self.reverse_sigmas) else 0
|
| 53 |
+
else:
|
| 54 |
+
sigma_ = self.sigmas[timestep_id + 1].reshape(-1, 1, 1, 1)
|
| 55 |
+
prev_sample = sample + model_output * (sigma_ - sigma)
|
| 56 |
+
return prev_sample
|
| 57 |
+
|
| 58 |
+
def add_noise(self, original_samples, noise, timestep):
|
| 59 |
+
"""
|
| 60 |
+
Diffusion forward corruption process.
|
| 61 |
+
Input:
|
| 62 |
+
- clean_latent: the clean latent with shape [B, C, H, W]
|
| 63 |
+
- noise: the noise with shape [B, C, H, W]
|
| 64 |
+
- timestep: the timestep with shape [B]
|
| 65 |
+
Output: the corrupted latent with shape [B, C, H, W]
|
| 66 |
+
"""
|
| 67 |
+
self.sigmas = self.sigmas.to(noise.device)
|
| 68 |
+
self.timesteps = self.timesteps.to(noise.device)
|
| 69 |
+
timestep_id = torch.argmin(
|
| 70 |
+
(self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
|
| 71 |
+
sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
|
| 72 |
+
sample = (1 - sigma) * original_samples + sigma * noise
|
| 73 |
+
return sample.type_as(noise)
|
| 74 |
+
|
| 75 |
+
def training_target(self, sample, noise, timestep):
|
| 76 |
+
target = noise - sample
|
| 77 |
+
return target
|
| 78 |
+
|
| 79 |
+
def training_weight(self, timestep):
|
| 80 |
+
timestep_id = torch.argmin(
|
| 81 |
+
(self.timesteps - timestep.to(self.timesteps.device)).abs())
|
| 82 |
+
weights = self.linear_timesteps_weights[timestep_id]
|
| 83 |
+
return weights
|
models/wan/taehv_wrapper.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TAEHV-based Wan VAE wrapper used by offline inference with --use_taehv."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
import urllib.request
|
| 8 |
+
from collections import namedtuple
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from types import SimpleNamespace
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn as nn
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
|
| 16 |
+
from models.model_interface import VAEInterface
|
| 17 |
+
from models.wan.wan_wrapper import WanVAEWrapper
|
| 18 |
+
|
| 19 |
+
DecoderResult = namedtuple("DecoderResult", ("frame", "memory"))
|
| 20 |
+
TWorkItem = namedtuple("TWorkItem", ("input_tensor", "block_index"))
|
| 21 |
+
LOGGER = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
import tensorrt as trt
|
| 25 |
+
except ImportError:
|
| 26 |
+
trt = None
|
| 27 |
+
|
| 28 |
+
def _resolve_project_root() -> Path:
|
| 29 |
+
env_root = os.environ.get("STREAMDIFFUSIONV2_ROOT")
|
| 30 |
+
if env_root:
|
| 31 |
+
return Path(env_root).expanduser().resolve()
|
| 32 |
+
|
| 33 |
+
repo_root = Path(__file__).resolve().parents[2]
|
| 34 |
+
if (repo_root / "ckpts").exists():
|
| 35 |
+
return repo_root
|
| 36 |
+
|
| 37 |
+
cwd = Path.cwd().resolve()
|
| 38 |
+
if (cwd / "ckpts").exists():
|
| 39 |
+
return cwd
|
| 40 |
+
|
| 41 |
+
return repo_root
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
PROJECT_ROOT = _resolve_project_root()
|
| 45 |
+
DEFAULT_TAEHV_CHECKPOINT = str(PROJECT_ROOT / "ckpts" / "taew2_1.pth")
|
| 46 |
+
DEFAULT_TAEHV_URL = "https://github.com/madebyollin/taehv/raw/main/taew2_1.pth"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def conv(n_in, n_out, **kwargs):
|
| 50 |
+
return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Clamp(nn.Module):
|
| 54 |
+
def forward(self, x):
|
| 55 |
+
return torch.tanh(x / 3) * 3
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class MemBlock(nn.Module):
|
| 59 |
+
def __init__(self, n_in, n_out):
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.conv = nn.Sequential(
|
| 62 |
+
conv(n_in * 2, n_out),
|
| 63 |
+
nn.ReLU(inplace=True),
|
| 64 |
+
conv(n_out, n_out),
|
| 65 |
+
nn.ReLU(inplace=True),
|
| 66 |
+
conv(n_out, n_out),
|
| 67 |
+
)
|
| 68 |
+
self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
|
| 69 |
+
self.act = nn.ReLU(inplace=True)
|
| 70 |
+
|
| 71 |
+
def forward(self, x, past):
|
| 72 |
+
return self.act(self.conv(torch.cat([x, past], 1)) + self.skip(x))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class TPool(nn.Module):
|
| 76 |
+
def __init__(self, n_f, stride):
|
| 77 |
+
super().__init__()
|
| 78 |
+
self.stride = stride
|
| 79 |
+
self.conv = nn.Conv2d(n_f * stride, n_f, 1, bias=False)
|
| 80 |
+
|
| 81 |
+
def forward(self, x):
|
| 82 |
+
_nt, c, h, w = x.shape
|
| 83 |
+
return self.conv(x.reshape(-1, self.stride * c, h, w))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class TGrow(nn.Module):
|
| 87 |
+
def __init__(self, n_f, stride):
|
| 88 |
+
super().__init__()
|
| 89 |
+
self.stride = stride
|
| 90 |
+
self.conv = nn.Conv2d(n_f, n_f * stride, 1, bias=False)
|
| 91 |
+
|
| 92 |
+
def forward(self, x):
|
| 93 |
+
_nt, c, h, w = x.shape
|
| 94 |
+
x = self.conv(x)
|
| 95 |
+
return x.reshape(-1, c, h, w)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def apply_model_with_memblocks(model, x, parallel, show_progress_bar):
|
| 99 |
+
assert x.ndim == 5, f"TAEHV operates on NTCHW tensors, but got {x.ndim}-dim tensor"
|
| 100 |
+
n, t, c, h, w = x.shape
|
| 101 |
+
if parallel:
|
| 102 |
+
x = x.reshape(n * t, c, h, w)
|
| 103 |
+
for block in model:
|
| 104 |
+
if isinstance(block, MemBlock):
|
| 105 |
+
nt, c, h, w = x.shape
|
| 106 |
+
t = nt // n
|
| 107 |
+
_x = x.reshape(n, t, c, h, w)
|
| 108 |
+
mem = F.pad(_x, (0, 0, 0, 0, 0, 0, 1, 0), value=0)[:, :t].reshape(x.shape)
|
| 109 |
+
x = block(x, mem)
|
| 110 |
+
else:
|
| 111 |
+
x = block(x)
|
| 112 |
+
nt, c, h, w = x.shape
|
| 113 |
+
t = nt // n
|
| 114 |
+
x = x.view(n, t, c, h, w)
|
| 115 |
+
else:
|
| 116 |
+
out = []
|
| 117 |
+
work_queue = [TWorkItem(xt, 0) for xt in x.reshape(n, t * c, h, w).chunk(t, dim=1)]
|
| 118 |
+
mem = [None] * len(model)
|
| 119 |
+
while work_queue:
|
| 120 |
+
xt, block_index = work_queue.pop(0)
|
| 121 |
+
if block_index == len(model):
|
| 122 |
+
out.append(xt)
|
| 123 |
+
continue
|
| 124 |
+
|
| 125 |
+
block = model[block_index]
|
| 126 |
+
if isinstance(block, MemBlock):
|
| 127 |
+
if mem[block_index] is None:
|
| 128 |
+
xt_new = block(xt, xt * 0)
|
| 129 |
+
mem[block_index] = xt
|
| 130 |
+
else:
|
| 131 |
+
xt_new = block(xt, mem[block_index])
|
| 132 |
+
mem[block_index].copy_(xt)
|
| 133 |
+
work_queue.insert(0, TWorkItem(xt_new, block_index + 1))
|
| 134 |
+
elif isinstance(block, TPool):
|
| 135 |
+
if mem[block_index] is None:
|
| 136 |
+
mem[block_index] = []
|
| 137 |
+
mem[block_index].append(xt)
|
| 138 |
+
if len(mem[block_index]) == block.stride:
|
| 139 |
+
n, c, h, w = xt.shape
|
| 140 |
+
xt = block(torch.cat(mem[block_index], 1).view(n * block.stride, c, h, w))
|
| 141 |
+
mem[block_index] = []
|
| 142 |
+
work_queue.insert(0, TWorkItem(xt, block_index + 1))
|
| 143 |
+
elif isinstance(block, TGrow):
|
| 144 |
+
xt = block(xt)
|
| 145 |
+
n_out, c_out, h_out, w_out = xt.shape
|
| 146 |
+
batch_size = n_out // block.stride
|
| 147 |
+
grown = xt.view(batch_size, block.stride * c_out, h_out, w_out)
|
| 148 |
+
for xt_next in reversed(grown.chunk(block.stride, dim=1)):
|
| 149 |
+
work_queue.insert(0, TWorkItem(xt_next, block_index + 1))
|
| 150 |
+
else:
|
| 151 |
+
xt = block(xt)
|
| 152 |
+
work_queue.insert(0, TWorkItem(xt, block_index + 1))
|
| 153 |
+
x = torch.stack(out, 1)
|
| 154 |
+
return x
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class TAEHV(nn.Module):
|
| 158 |
+
latent_channels = 16
|
| 159 |
+
image_channels = 3
|
| 160 |
+
|
| 161 |
+
def __init__(self, checkpoint_path=DEFAULT_TAEHV_CHECKPOINT, decoder_time_upscale=(True, True), decoder_space_upscale=(True, True, True)):
|
| 162 |
+
super().__init__()
|
| 163 |
+
self.encoder = nn.Sequential(
|
| 164 |
+
conv(TAEHV.image_channels, 64),
|
| 165 |
+
nn.ReLU(inplace=True),
|
| 166 |
+
TPool(64, 2),
|
| 167 |
+
conv(64, 64, stride=2, bias=False),
|
| 168 |
+
MemBlock(64, 64),
|
| 169 |
+
MemBlock(64, 64),
|
| 170 |
+
MemBlock(64, 64),
|
| 171 |
+
TPool(64, 2),
|
| 172 |
+
conv(64, 64, stride=2, bias=False),
|
| 173 |
+
MemBlock(64, 64),
|
| 174 |
+
MemBlock(64, 64),
|
| 175 |
+
MemBlock(64, 64),
|
| 176 |
+
TPool(64, 1),
|
| 177 |
+
conv(64, 64, stride=2, bias=False),
|
| 178 |
+
MemBlock(64, 64),
|
| 179 |
+
MemBlock(64, 64),
|
| 180 |
+
MemBlock(64, 64),
|
| 181 |
+
conv(64, TAEHV.latent_channels),
|
| 182 |
+
)
|
| 183 |
+
n_f = [256, 128, 64, 64]
|
| 184 |
+
self.frames_to_trim = 2 ** sum(decoder_time_upscale) - 1
|
| 185 |
+
self.decoder = nn.Sequential(
|
| 186 |
+
Clamp(),
|
| 187 |
+
conv(TAEHV.latent_channels, n_f[0]),
|
| 188 |
+
nn.ReLU(inplace=True),
|
| 189 |
+
MemBlock(n_f[0], n_f[0]),
|
| 190 |
+
MemBlock(n_f[0], n_f[0]),
|
| 191 |
+
MemBlock(n_f[0], n_f[0]),
|
| 192 |
+
nn.Upsample(scale_factor=2 if decoder_space_upscale[0] else 1),
|
| 193 |
+
TGrow(n_f[0], 1),
|
| 194 |
+
conv(n_f[0], n_f[1], bias=False),
|
| 195 |
+
MemBlock(n_f[1], n_f[1]),
|
| 196 |
+
MemBlock(n_f[1], n_f[1]),
|
| 197 |
+
MemBlock(n_f[1], n_f[1]),
|
| 198 |
+
nn.Upsample(scale_factor=2 if decoder_space_upscale[1] else 1),
|
| 199 |
+
TGrow(n_f[1], 2 if decoder_time_upscale[0] else 1),
|
| 200 |
+
conv(n_f[1], n_f[2], bias=False),
|
| 201 |
+
MemBlock(n_f[2], n_f[2]),
|
| 202 |
+
MemBlock(n_f[2], n_f[2]),
|
| 203 |
+
MemBlock(n_f[2], n_f[2]),
|
| 204 |
+
nn.Upsample(scale_factor=2 if decoder_space_upscale[2] else 1),
|
| 205 |
+
TGrow(n_f[2], 2 if decoder_time_upscale[1] else 1),
|
| 206 |
+
conv(n_f[2], n_f[3], bias=False),
|
| 207 |
+
nn.ReLU(inplace=True),
|
| 208 |
+
conv(n_f[3], TAEHV.image_channels),
|
| 209 |
+
)
|
| 210 |
+
self.load_state_dict(self.patch_tgrow_layers(torch.load(checkpoint_path, map_location="cpu", weights_only=True)))
|
| 211 |
+
|
| 212 |
+
def patch_tgrow_layers(self, state_dict):
|
| 213 |
+
new_state_dict = self.state_dict()
|
| 214 |
+
for index, layer in enumerate(self.decoder):
|
| 215 |
+
if isinstance(layer, TGrow):
|
| 216 |
+
key = f"decoder.{index}.conv.weight"
|
| 217 |
+
if state_dict[key].shape[0] > new_state_dict[key].shape[0]:
|
| 218 |
+
state_dict[key] = state_dict[key][-new_state_dict[key].shape[0]:]
|
| 219 |
+
return state_dict
|
| 220 |
+
|
| 221 |
+
def encode_video(self, x, parallel=True, show_progress_bar=False):
|
| 222 |
+
return apply_model_with_memblocks(self.encoder, x, parallel, show_progress_bar)
|
| 223 |
+
|
| 224 |
+
def decode_video(self, x, parallel=True, show_progress_bar=False):
|
| 225 |
+
return apply_model_with_memblocks(self.decoder, x, parallel, show_progress_bar)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class TAEHVParallelDecoderModule(nn.Module):
|
| 229 |
+
"""Shape-specialized decoder graph that is friendly to ONNX/TensorRT export."""
|
| 230 |
+
|
| 231 |
+
def __init__(self, decoder: nn.Module):
|
| 232 |
+
super().__init__()
|
| 233 |
+
self.decoder = decoder
|
| 234 |
+
|
| 235 |
+
def forward(self, latent: torch.Tensor) -> torch.Tensor:
|
| 236 |
+
return apply_model_with_memblocks(
|
| 237 |
+
self.decoder,
|
| 238 |
+
latent,
|
| 239 |
+
parallel=True,
|
| 240 |
+
show_progress_bar=False,
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
class TAEHVTensorRTDecoder:
|
| 245 |
+
"""Cache TensorRT engines for the common fixed TAEHV decoder shapes."""
|
| 246 |
+
|
| 247 |
+
def __init__(
|
| 248 |
+
self,
|
| 249 |
+
decoder_module: nn.Module,
|
| 250 |
+
cache_dir: str | Path,
|
| 251 |
+
workspace_bytes: int,
|
| 252 |
+
) -> None:
|
| 253 |
+
if trt is None:
|
| 254 |
+
raise ImportError("TensorRT is not installed, but TensorRT decode was requested.")
|
| 255 |
+
|
| 256 |
+
self.decoder_module = decoder_module.eval()
|
| 257 |
+
self.cache_dir = Path(cache_dir)
|
| 258 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 259 |
+
self.workspace_bytes = int(workspace_bytes)
|
| 260 |
+
self.logger = trt.Logger(trt.Logger.WARNING)
|
| 261 |
+
self.runtime = trt.Runtime(self.logger)
|
| 262 |
+
self._engine_cache: dict[tuple[int, ...], tuple[object, object, str, str]] = {}
|
| 263 |
+
self._stream_cache: dict[int, torch.cuda.Stream] = {}
|
| 264 |
+
|
| 265 |
+
def _shape_tag(self, shape: tuple[int, ...]) -> str:
|
| 266 |
+
return "x".join(str(dim) for dim in shape)
|
| 267 |
+
|
| 268 |
+
def _engine_path(self, shape: tuple[int, ...]) -> Path:
|
| 269 |
+
return self.cache_dir / f"taehv_decoder_trt_{self._shape_tag(shape)}.plan"
|
| 270 |
+
|
| 271 |
+
def _onnx_path(self, shape: tuple[int, ...]) -> Path:
|
| 272 |
+
return self.cache_dir / f"taehv_decoder_trt_{self._shape_tag(shape)}.onnx"
|
| 273 |
+
|
| 274 |
+
def _build_engine(self, shape: tuple[int, ...], device: torch.device) -> bytes:
|
| 275 |
+
onnx_path = self._onnx_path(shape)
|
| 276 |
+
engine_path = self._engine_path(shape)
|
| 277 |
+
|
| 278 |
+
sample = torch.randn(shape, device=device, dtype=torch.float16)
|
| 279 |
+
with torch.no_grad():
|
| 280 |
+
torch.onnx.export(
|
| 281 |
+
self.decoder_module,
|
| 282 |
+
(sample,),
|
| 283 |
+
onnx_path.as_posix(),
|
| 284 |
+
input_names=["latent"],
|
| 285 |
+
output_names=["video"],
|
| 286 |
+
opset_version=18,
|
| 287 |
+
do_constant_folding=True,
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
builder = trt.Builder(self.logger)
|
| 291 |
+
network = builder.create_network(
|
| 292 |
+
1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
|
| 293 |
+
)
|
| 294 |
+
parser = trt.OnnxParser(network, self.logger)
|
| 295 |
+
with open(onnx_path, "rb") as handle:
|
| 296 |
+
if not parser.parse(handle.read()):
|
| 297 |
+
errors = "\n".join(str(parser.get_error(i)) for i in range(parser.num_errors))
|
| 298 |
+
raise RuntimeError(f"Failed to parse ONNX for TAEHV TensorRT engine:\n{errors}")
|
| 299 |
+
|
| 300 |
+
config = builder.create_builder_config()
|
| 301 |
+
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, self.workspace_bytes)
|
| 302 |
+
config.set_flag(trt.BuilderFlag.FP16)
|
| 303 |
+
serialized = builder.build_serialized_network(network, config)
|
| 304 |
+
if serialized is None:
|
| 305 |
+
raise RuntimeError("TensorRT failed to build the TAEHV decoder engine")
|
| 306 |
+
engine_bytes = bytes(serialized)
|
| 307 |
+
engine_path.write_bytes(engine_bytes)
|
| 308 |
+
return engine_bytes
|
| 309 |
+
|
| 310 |
+
def _load_engine(self, shape: tuple[int, ...], device: torch.device):
|
| 311 |
+
cached = self._engine_cache.get(shape)
|
| 312 |
+
if cached is not None:
|
| 313 |
+
return cached
|
| 314 |
+
|
| 315 |
+
engine_path = self._engine_path(shape)
|
| 316 |
+
if engine_path.exists():
|
| 317 |
+
engine_bytes = engine_path.read_bytes()
|
| 318 |
+
else:
|
| 319 |
+
LOGGER.info("Building TensorRT engine for TAEHV decoder shape %s", shape)
|
| 320 |
+
engine_bytes = self._build_engine(shape, device)
|
| 321 |
+
|
| 322 |
+
engine = self.runtime.deserialize_cuda_engine(engine_bytes)
|
| 323 |
+
if engine is None:
|
| 324 |
+
raise RuntimeError(f"Failed to deserialize TensorRT engine for TAEHV shape {shape}")
|
| 325 |
+
context = engine.create_execution_context()
|
| 326 |
+
input_name = ""
|
| 327 |
+
output_name = ""
|
| 328 |
+
for index in range(engine.num_io_tensors):
|
| 329 |
+
name = engine.get_tensor_name(index)
|
| 330 |
+
mode = engine.get_tensor_mode(name)
|
| 331 |
+
if mode == trt.TensorIOMode.INPUT:
|
| 332 |
+
input_name = name
|
| 333 |
+
elif mode == trt.TensorIOMode.OUTPUT:
|
| 334 |
+
output_name = name
|
| 335 |
+
if not input_name or not output_name:
|
| 336 |
+
raise RuntimeError("Failed to discover TensorRT decoder tensor names")
|
| 337 |
+
|
| 338 |
+
cached = (engine, context, input_name, output_name)
|
| 339 |
+
self._engine_cache[shape] = cached
|
| 340 |
+
return cached
|
| 341 |
+
|
| 342 |
+
def decode(self, latent: torch.Tensor) -> torch.Tensor:
|
| 343 |
+
latent = latent.contiguous().to(dtype=torch.float16)
|
| 344 |
+
shape = tuple(int(dim) for dim in latent.shape)
|
| 345 |
+
_engine, context, input_name, output_name = self._load_engine(shape, latent.device)
|
| 346 |
+
|
| 347 |
+
if not context.set_input_shape(input_name, shape):
|
| 348 |
+
raise RuntimeError(f"TensorRT rejected decoder input shape {shape}")
|
| 349 |
+
output_shape = tuple(int(dim) for dim in context.get_tensor_shape(output_name))
|
| 350 |
+
output = torch.empty(output_shape, device=latent.device, dtype=latent.dtype)
|
| 351 |
+
|
| 352 |
+
context.set_tensor_address(input_name, int(latent.data_ptr()))
|
| 353 |
+
context.set_tensor_address(output_name, int(output.data_ptr()))
|
| 354 |
+
device_index = latent.device.index
|
| 355 |
+
if device_index is None:
|
| 356 |
+
raise RuntimeError("TensorRT decode requires an explicit CUDA device index")
|
| 357 |
+
stream = self._stream_cache.get(device_index)
|
| 358 |
+
if stream is None:
|
| 359 |
+
stream = torch.cuda.Stream(device=latent.device)
|
| 360 |
+
self._stream_cache[device_index] = stream
|
| 361 |
+
current_stream = torch.cuda.current_stream(device=latent.device)
|
| 362 |
+
stream.wait_stream(current_stream)
|
| 363 |
+
ok = context.execute_async_v3(stream.cuda_stream)
|
| 364 |
+
if not ok:
|
| 365 |
+
raise RuntimeError("TensorRT execution failed for the TAEHV decoder")
|
| 366 |
+
current_stream.wait_stream(stream)
|
| 367 |
+
return output
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
class TAEHVWanVAEWrapper(VAEInterface):
|
| 371 |
+
"""Wan stream encoder with a TAEHV decoder for faster pixel reconstruction."""
|
| 372 |
+
|
| 373 |
+
def __init__(
|
| 374 |
+
self,
|
| 375 |
+
model_type: str = "T2V-1.3B",
|
| 376 |
+
checkpoint_path: str | None = None,
|
| 377 |
+
auto_download: bool = True,
|
| 378 |
+
parallel_decode: bool = False,
|
| 379 |
+
use_tensorrt: bool = False,
|
| 380 |
+
tensorrt_cache_dir: str | None = None,
|
| 381 |
+
tensorrt_workspace_bytes: int = 4 << 30,
|
| 382 |
+
):
|
| 383 |
+
super().__init__()
|
| 384 |
+
self.checkpoint_path = checkpoint_path or DEFAULT_TAEHV_CHECKPOINT
|
| 385 |
+
self.use_tensorrt = use_tensorrt
|
| 386 |
+
self.parallel_decode = parallel_decode or use_tensorrt
|
| 387 |
+
self.model = SimpleNamespace(first_encode=True, first_decode=True)
|
| 388 |
+
self.decode_context_latents = 3
|
| 389 |
+
self._decode_latent_cache: torch.Tensor | None = None
|
| 390 |
+
self.encoder_vae = WanVAEWrapper(model_type=model_type)
|
| 391 |
+
self.taehv = TAEHV(checkpoint_path=self._resolve_checkpoint(auto_download))
|
| 392 |
+
self._tensorrt_cache_dir = tensorrt_cache_dir or (PROJECT_ROOT / "ckpts" / "taehv_trt")
|
| 393 |
+
self._tensorrt_workspace_bytes = int(tensorrt_workspace_bytes)
|
| 394 |
+
self._tensorrt_decoder: TAEHVTensorRTDecoder | None = None
|
| 395 |
+
self._tensorrt_failed = False
|
| 396 |
+
|
| 397 |
+
def _resolve_checkpoint(self, auto_download: bool) -> str:
|
| 398 |
+
if os.path.exists(self.checkpoint_path):
|
| 399 |
+
return self.checkpoint_path
|
| 400 |
+
|
| 401 |
+
if not auto_download:
|
| 402 |
+
raise FileNotFoundError(f"TAEHV checkpoint not found: {self.checkpoint_path}")
|
| 403 |
+
|
| 404 |
+
os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True)
|
| 405 |
+
urllib.request.urlretrieve(DEFAULT_TAEHV_URL, self.checkpoint_path)
|
| 406 |
+
return self.checkpoint_path
|
| 407 |
+
|
| 408 |
+
def to(self, *args, **kwargs):
|
| 409 |
+
device = kwargs.get("device")
|
| 410 |
+
dtype = kwargs.get("dtype")
|
| 411 |
+
|
| 412 |
+
if args:
|
| 413 |
+
if len(args) >= 1 and not isinstance(args[0], torch.dtype):
|
| 414 |
+
device = args[0]
|
| 415 |
+
if len(args) >= 2 and isinstance(args[1], torch.dtype):
|
| 416 |
+
dtype = args[1]
|
| 417 |
+
elif len(args) == 1 and isinstance(args[0], torch.dtype):
|
| 418 |
+
dtype = args[0]
|
| 419 |
+
|
| 420 |
+
encoder_kwargs = {}
|
| 421 |
+
if device is not None:
|
| 422 |
+
encoder_kwargs["device"] = device
|
| 423 |
+
if dtype is not None:
|
| 424 |
+
encoder_kwargs["dtype"] = dtype
|
| 425 |
+
self.encoder_vae.to(**encoder_kwargs)
|
| 426 |
+
|
| 427 |
+
taehv_kwargs = {"dtype": torch.float16}
|
| 428 |
+
if device is not None:
|
| 429 |
+
taehv_kwargs["device"] = device
|
| 430 |
+
self.taehv.to(**taehv_kwargs)
|
| 431 |
+
if self.use_tensorrt and not self._tensorrt_failed:
|
| 432 |
+
self._tensorrt_decoder = TAEHVTensorRTDecoder(
|
| 433 |
+
decoder_module=TAEHVParallelDecoderModule(self.taehv.decoder).to(**taehv_kwargs).eval(),
|
| 434 |
+
cache_dir=self._tensorrt_cache_dir,
|
| 435 |
+
workspace_bytes=self._tensorrt_workspace_bytes,
|
| 436 |
+
)
|
| 437 |
+
return self
|
| 438 |
+
|
| 439 |
+
def _pixels_to_unit_range(self, video: torch.Tensor) -> torch.Tensor:
|
| 440 |
+
return (video * 0.5 + 0.5).clamp(0, 1)
|
| 441 |
+
|
| 442 |
+
def _unit_range_to_pixels(self, video: torch.Tensor) -> torch.Tensor:
|
| 443 |
+
return video.mul(2).sub(1).clamp(-1, 1)
|
| 444 |
+
|
| 445 |
+
def _to_ntchw(self, video: torch.Tensor) -> torch.Tensor:
|
| 446 |
+
return video.permute(0, 2, 1, 3, 4).contiguous()
|
| 447 |
+
|
| 448 |
+
def _to_ncthw(self, video: torch.Tensor) -> torch.Tensor:
|
| 449 |
+
return video.permute(0, 2, 1, 3, 4).contiguous()
|
| 450 |
+
|
| 451 |
+
def _decode_video(self, latent: torch.Tensor) -> torch.Tensor:
|
| 452 |
+
if self.use_tensorrt and self._tensorrt_decoder is not None and not self._tensorrt_failed:
|
| 453 |
+
try:
|
| 454 |
+
return self._tensorrt_decoder.decode(latent)
|
| 455 |
+
except Exception as exc:
|
| 456 |
+
self._tensorrt_failed = True
|
| 457 |
+
self._tensorrt_decoder = None
|
| 458 |
+
LOGGER.warning(
|
| 459 |
+
"TAEHV TensorRT decode failed for shape %s, falling back to PyTorch parallel decode: %s",
|
| 460 |
+
tuple(latent.shape),
|
| 461 |
+
exc,
|
| 462 |
+
)
|
| 463 |
+
return self.taehv.decode_video(
|
| 464 |
+
latent,
|
| 465 |
+
parallel=self.parallel_decode,
|
| 466 |
+
show_progress_bar=False,
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
def decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor:
|
| 470 |
+
video = self._decode_video(latent)
|
| 471 |
+
return self._unit_range_to_pixels(video)
|
| 472 |
+
|
| 473 |
+
def decode(self, latent: torch.Tensor) -> torch.Tensor:
|
| 474 |
+
return self.decode_to_pixel(latent)
|
| 475 |
+
|
| 476 |
+
def stream_encode(self, video: torch.Tensor, is_scale=False) -> torch.Tensor:
|
| 477 |
+
self.encoder_vae.model.first_encode = self.model.first_encode
|
| 478 |
+
latent = self.encoder_vae.stream_encode(video, is_scale=is_scale)
|
| 479 |
+
self.model.first_encode = self.encoder_vae.model.first_encode
|
| 480 |
+
return latent
|
| 481 |
+
|
| 482 |
+
def stream_decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor:
|
| 483 |
+
model_dtype = next(self.taehv.parameters()).dtype
|
| 484 |
+
latent = latent.to(dtype=model_dtype)
|
| 485 |
+
|
| 486 |
+
# Match Self-Forcing's TAEHV usage: keep a short latent prefix so each
|
| 487 |
+
# incremental decode sees recent temporal context, then trim the
|
| 488 |
+
# already-emitted frames from the pixel output.
|
| 489 |
+
if self.model.first_decode:
|
| 490 |
+
self.model.first_decode = False
|
| 491 |
+
self._decode_latent_cache = None
|
| 492 |
+
decode_latent = latent
|
| 493 |
+
emitted_latents = max(latent.shape[1] - 1, 0)
|
| 494 |
+
else:
|
| 495 |
+
context = self._decode_latent_cache
|
| 496 |
+
decode_latent = latent if context is None else torch.cat([context.to(device=latent.device, dtype=model_dtype), latent], dim=1)
|
| 497 |
+
emitted_latents = latent.shape[1]
|
| 498 |
+
|
| 499 |
+
self._decode_latent_cache = decode_latent[:, -min(self.decode_context_latents, decode_latent.shape[1]):].detach().clone()
|
| 500 |
+
video = self._decode_video(decode_latent)
|
| 501 |
+
if emitted_latents:
|
| 502 |
+
video = video[:, -emitted_latents * 4:, :, :, :]
|
| 503 |
+
else:
|
| 504 |
+
video = video[:, 0:0, :, :, :]
|
| 505 |
+
return self._unit_range_to_pixels(video).to(dtype=latent.dtype)
|
models/wan/wan_base/README.md
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Code in this folder is modified from https://github.com/Wan-Video/Wan2.1
|
| 2 |
+
Apache-2.0 License
|
models/wan/wan_base/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Minimal Wan base package used by StreamDiffusionV2 inference."""
|
models/wan/wan_base/modules/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .attention import flash_attention
|
| 2 |
+
from .model import WanModel
|
| 3 |
+
from .t5 import T5Decoder, T5Encoder, T5EncoderModel, T5Model
|
| 4 |
+
from .tokenizers import HuggingfaceTokenizer
|
| 5 |
+
from .vae import WanVAE
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
'WanVAE',
|
| 9 |
+
'WanModel',
|
| 10 |
+
'T5Model',
|
| 11 |
+
'T5Encoder',
|
| 12 |
+
'T5Decoder',
|
| 13 |
+
'T5EncoderModel',
|
| 14 |
+
'HuggingfaceTokenizer',
|
| 15 |
+
'flash_attention',
|
| 16 |
+
]
|
models/wan/wan_base/modules/attention.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
try:
|
| 5 |
+
import flash_attn_interface
|
| 6 |
+
FLASH_ATTN_3_AVAILABLE = True
|
| 7 |
+
except (ImportError, ModuleNotFoundError):
|
| 8 |
+
FLASH_ATTN_3_AVAILABLE = False
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
import flash_attn
|
| 12 |
+
FLASH_ATTN_2_AVAILABLE = hasattr(flash_attn, "flash_attn_varlen_func")
|
| 13 |
+
except (ImportError, ModuleNotFoundError):
|
| 14 |
+
FLASH_ATTN_2_AVAILABLE = False
|
| 15 |
+
|
| 16 |
+
import warnings
|
| 17 |
+
|
| 18 |
+
__all__ = [
|
| 19 |
+
'flash_attention',
|
| 20 |
+
'attention',
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _prepare_sdpa_inputs(q, k, v, dtype):
|
| 25 |
+
q = q.transpose(1, 2)
|
| 26 |
+
k = k.transpose(1, 2)
|
| 27 |
+
v = v.transpose(1, 2)
|
| 28 |
+
|
| 29 |
+
if q.device.type == 'cpu' and dtype in (torch.float16, torch.bfloat16):
|
| 30 |
+
q = q.float()
|
| 31 |
+
k = k.float()
|
| 32 |
+
v = v.float()
|
| 33 |
+
else:
|
| 34 |
+
q = q.to(dtype)
|
| 35 |
+
k = k.to(dtype)
|
| 36 |
+
v = v.to(dtype)
|
| 37 |
+
|
| 38 |
+
return q, k, v
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _build_length_mask(batch_size, q_len, k_len, device, q_lens, k_lens, causal, window_size):
|
| 42 |
+
mask = torch.ones((batch_size, q_len, k_len), dtype=torch.bool, device=device)
|
| 43 |
+
|
| 44 |
+
q_idx = torch.arange(q_len, device=device).view(1, q_len, 1)
|
| 45 |
+
k_idx = torch.arange(k_len, device=device).view(1, 1, k_len)
|
| 46 |
+
|
| 47 |
+
if q_lens is not None:
|
| 48 |
+
q_lens = q_lens.to(device=device, dtype=torch.long)
|
| 49 |
+
mask = mask & (q_idx < q_lens.view(batch_size, 1, 1))
|
| 50 |
+
|
| 51 |
+
if k_lens is not None:
|
| 52 |
+
k_lens = k_lens.to(device=device, dtype=torch.long)
|
| 53 |
+
mask = mask & (k_idx < k_lens.view(batch_size, 1, 1))
|
| 54 |
+
|
| 55 |
+
if causal:
|
| 56 |
+
mask = mask & (k_idx <= q_idx)
|
| 57 |
+
|
| 58 |
+
if window_size != (-1, -1):
|
| 59 |
+
left, right = window_size
|
| 60 |
+
if left >= 0:
|
| 61 |
+
mask = mask & (k_idx >= q_idx - left)
|
| 62 |
+
if right >= 0:
|
| 63 |
+
mask = mask & (k_idx <= q_idx + right)
|
| 64 |
+
|
| 65 |
+
return mask.unsqueeze(1)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _merge_sdpa_masks(length_mask, attn_mask, dtype):
|
| 69 |
+
if attn_mask is None:
|
| 70 |
+
return length_mask
|
| 71 |
+
|
| 72 |
+
if attn_mask.dtype == torch.bool:
|
| 73 |
+
return length_mask & attn_mask
|
| 74 |
+
|
| 75 |
+
additive_mask = torch.zeros_like(length_mask, dtype=dtype)
|
| 76 |
+
additive_mask = additive_mask.masked_fill(~length_mask, float('-inf'))
|
| 77 |
+
return additive_mask + attn_mask.to(dtype)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _sdpa_attention_fallback(
|
| 81 |
+
q,
|
| 82 |
+
k,
|
| 83 |
+
v,
|
| 84 |
+
q_lens=None,
|
| 85 |
+
k_lens=None,
|
| 86 |
+
dropout_p=0.,
|
| 87 |
+
softmax_scale=None,
|
| 88 |
+
q_scale=None,
|
| 89 |
+
causal=False,
|
| 90 |
+
window_size=(-1, -1),
|
| 91 |
+
dtype=torch.bfloat16,
|
| 92 |
+
attn_mask=None,
|
| 93 |
+
):
|
| 94 |
+
out_dtype = q.dtype
|
| 95 |
+
batch_size, q_len, k_len = q.size(0), q.size(1), k.size(1)
|
| 96 |
+
|
| 97 |
+
q, k, v = _prepare_sdpa_inputs(q, k, v, dtype)
|
| 98 |
+
|
| 99 |
+
total_scale = 1.0
|
| 100 |
+
if q_scale is not None:
|
| 101 |
+
total_scale *= q_scale
|
| 102 |
+
if softmax_scale is not None:
|
| 103 |
+
total_scale *= softmax_scale
|
| 104 |
+
if total_scale != 1.0:
|
| 105 |
+
q = q * total_scale
|
| 106 |
+
|
| 107 |
+
mask = _build_length_mask(
|
| 108 |
+
batch_size=batch_size,
|
| 109 |
+
q_len=q_len,
|
| 110 |
+
k_len=k_len,
|
| 111 |
+
device=q.device,
|
| 112 |
+
q_lens=q_lens,
|
| 113 |
+
k_lens=k_lens,
|
| 114 |
+
causal=causal,
|
| 115 |
+
window_size=window_size,
|
| 116 |
+
)
|
| 117 |
+
mask = _merge_sdpa_masks(mask, attn_mask, q.dtype)
|
| 118 |
+
|
| 119 |
+
out = torch.nn.functional.scaled_dot_product_attention(
|
| 120 |
+
q,
|
| 121 |
+
k,
|
| 122 |
+
v,
|
| 123 |
+
attn_mask=mask,
|
| 124 |
+
is_causal=False,
|
| 125 |
+
dropout_p=dropout_p,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if q_lens is not None:
|
| 129 |
+
q_valid = (
|
| 130 |
+
torch.arange(q_len, device=out.device).view(1, q_len, 1)
|
| 131 |
+
< q_lens.to(device=out.device, dtype=torch.long).view(batch_size, 1, 1)
|
| 132 |
+
).unsqueeze(1)
|
| 133 |
+
out = out.masked_fill(~q_valid, 0)
|
| 134 |
+
|
| 135 |
+
return out.transpose(1, 2).contiguous().to(out_dtype)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def flash_attention(
|
| 139 |
+
q,
|
| 140 |
+
k,
|
| 141 |
+
v,
|
| 142 |
+
q_lens=None,
|
| 143 |
+
k_lens=None,
|
| 144 |
+
dropout_p=0.,
|
| 145 |
+
softmax_scale=None,
|
| 146 |
+
q_scale=None,
|
| 147 |
+
causal=False,
|
| 148 |
+
window_size=(-1, -1),
|
| 149 |
+
deterministic=False,
|
| 150 |
+
dtype=torch.bfloat16,
|
| 151 |
+
version=None,
|
| 152 |
+
):
|
| 153 |
+
"""
|
| 154 |
+
q: [B, Lq, Nq, C1].
|
| 155 |
+
k: [B, Lk, Nk, C1].
|
| 156 |
+
v: [B, Lk, Nk, C2]. Nq must be divisible by Nk.
|
| 157 |
+
q_lens: [B].
|
| 158 |
+
k_lens: [B].
|
| 159 |
+
dropout_p: float. Dropout probability.
|
| 160 |
+
softmax_scale: float. The scaling of QK^T before applying softmax.
|
| 161 |
+
causal: bool. Whether to apply causal attention mask.
|
| 162 |
+
window_size: (left right). If not (-1, -1), apply sliding window local attention.
|
| 163 |
+
deterministic: bool. If True, slightly slower and uses more memory.
|
| 164 |
+
dtype: torch.dtype. Apply when dtype of q/k/v is not float16/bfloat16.
|
| 165 |
+
"""
|
| 166 |
+
half_dtypes = (torch.float16, torch.bfloat16)
|
| 167 |
+
assert dtype in half_dtypes
|
| 168 |
+
if not (FLASH_ATTN_2_AVAILABLE or FLASH_ATTN_3_AVAILABLE):
|
| 169 |
+
warnings.warn(
|
| 170 |
+
'flash_attn is not installed; falling back to scaled_dot_product_attention.',
|
| 171 |
+
stacklevel=2,
|
| 172 |
+
)
|
| 173 |
+
return _sdpa_attention_fallback(
|
| 174 |
+
q=q,
|
| 175 |
+
k=k,
|
| 176 |
+
v=v,
|
| 177 |
+
q_lens=q_lens,
|
| 178 |
+
k_lens=k_lens,
|
| 179 |
+
dropout_p=dropout_p,
|
| 180 |
+
softmax_scale=softmax_scale,
|
| 181 |
+
q_scale=q_scale,
|
| 182 |
+
causal=causal,
|
| 183 |
+
window_size=window_size,
|
| 184 |
+
dtype=dtype,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
assert q.device.type == 'cuda' and q.size(-1) <= 256
|
| 188 |
+
|
| 189 |
+
# params
|
| 190 |
+
b, lq, lk, out_dtype = q.size(0), q.size(1), k.size(1), q.dtype
|
| 191 |
+
|
| 192 |
+
def half(x):
|
| 193 |
+
return x if x.dtype in half_dtypes else x.to(dtype)
|
| 194 |
+
|
| 195 |
+
# preprocess query
|
| 196 |
+
if q_lens is None:
|
| 197 |
+
q = half(q.flatten(0, 1))
|
| 198 |
+
q_lens = torch.tensor(
|
| 199 |
+
[lq] * b, dtype=torch.int32).to(
|
| 200 |
+
device=q.device, non_blocking=True)
|
| 201 |
+
else:
|
| 202 |
+
q = half(torch.cat([u[:v] for u, v in zip(q, q_lens)]))
|
| 203 |
+
|
| 204 |
+
# preprocess key, value
|
| 205 |
+
if k_lens is None:
|
| 206 |
+
k = half(k.flatten(0, 1))
|
| 207 |
+
v = half(v.flatten(0, 1))
|
| 208 |
+
k_lens = torch.tensor(
|
| 209 |
+
[lk] * b, dtype=torch.int32).to(
|
| 210 |
+
device=k.device, non_blocking=True)
|
| 211 |
+
else:
|
| 212 |
+
k = half(torch.cat([u[:v] for u, v in zip(k, k_lens)]))
|
| 213 |
+
v = half(torch.cat([u[:v] for u, v in zip(v, k_lens)]))
|
| 214 |
+
|
| 215 |
+
q = q.to(v.dtype)
|
| 216 |
+
k = k.to(v.dtype)
|
| 217 |
+
|
| 218 |
+
if q_scale is not None:
|
| 219 |
+
q = q * q_scale
|
| 220 |
+
|
| 221 |
+
if version is not None and version == 3 and not FLASH_ATTN_3_AVAILABLE:
|
| 222 |
+
warnings.warn(
|
| 223 |
+
'Flash attention 3 is not available, use flash attention 2 instead.'
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# apply attention
|
| 227 |
+
if (version is None or version == 3) and FLASH_ATTN_3_AVAILABLE:
|
| 228 |
+
# Note: dropout_p, window_size are not supported in FA3 now.
|
| 229 |
+
x = flash_attn_interface.flash_attn_varlen_func(
|
| 230 |
+
q=q,
|
| 231 |
+
k=k,
|
| 232 |
+
v=v,
|
| 233 |
+
cu_seqlens_q=torch.cat([q_lens.new_zeros([1]), q_lens]).cumsum(
|
| 234 |
+
0, dtype=torch.int32).to(q.device, non_blocking=True),
|
| 235 |
+
cu_seqlens_k=torch.cat([k_lens.new_zeros([1]), k_lens]).cumsum(
|
| 236 |
+
0, dtype=torch.int32).to(q.device, non_blocking=True),
|
| 237 |
+
max_seqlen_q=lq,
|
| 238 |
+
max_seqlen_k=lk,
|
| 239 |
+
softmax_scale=softmax_scale,
|
| 240 |
+
causal=causal,
|
| 241 |
+
deterministic=deterministic)[0].unflatten(0, (b, lq))
|
| 242 |
+
else:
|
| 243 |
+
assert FLASH_ATTN_2_AVAILABLE
|
| 244 |
+
x = flash_attn.flash_attn_varlen_func(
|
| 245 |
+
q=q,
|
| 246 |
+
k=k,
|
| 247 |
+
v=v,
|
| 248 |
+
cu_seqlens_q=torch.cat([q_lens.new_zeros([1]), q_lens]).cumsum(
|
| 249 |
+
0, dtype=torch.int32).to(q.device, non_blocking=True),
|
| 250 |
+
cu_seqlens_k=torch.cat([k_lens.new_zeros([1]), k_lens]).cumsum(
|
| 251 |
+
0, dtype=torch.int32).to(q.device, non_blocking=True),
|
| 252 |
+
max_seqlen_q=lq,
|
| 253 |
+
max_seqlen_k=lk,
|
| 254 |
+
dropout_p=dropout_p,
|
| 255 |
+
softmax_scale=softmax_scale,
|
| 256 |
+
causal=causal,
|
| 257 |
+
window_size=window_size,
|
| 258 |
+
deterministic=deterministic).unflatten(0, (b, lq))
|
| 259 |
+
|
| 260 |
+
# output
|
| 261 |
+
return x.type(out_dtype)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def attention(
|
| 265 |
+
q,
|
| 266 |
+
k,
|
| 267 |
+
v,
|
| 268 |
+
q_lens=None,
|
| 269 |
+
k_lens=None,
|
| 270 |
+
dropout_p=0.,
|
| 271 |
+
softmax_scale=None,
|
| 272 |
+
q_scale=None,
|
| 273 |
+
causal=False,
|
| 274 |
+
window_size=(-1, -1),
|
| 275 |
+
deterministic=False,
|
| 276 |
+
dtype=torch.bfloat16,
|
| 277 |
+
fa_version=None,
|
| 278 |
+
attn_mask=None,
|
| 279 |
+
):
|
| 280 |
+
if FLASH_ATTN_2_AVAILABLE or FLASH_ATTN_3_AVAILABLE:
|
| 281 |
+
return flash_attention(
|
| 282 |
+
q=q,
|
| 283 |
+
k=k,
|
| 284 |
+
v=v,
|
| 285 |
+
q_lens=q_lens,
|
| 286 |
+
k_lens=k_lens,
|
| 287 |
+
dropout_p=dropout_p,
|
| 288 |
+
softmax_scale=softmax_scale,
|
| 289 |
+
q_scale=q_scale,
|
| 290 |
+
causal=causal,
|
| 291 |
+
window_size=window_size,
|
| 292 |
+
deterministic=deterministic,
|
| 293 |
+
dtype=dtype,
|
| 294 |
+
version=fa_version,
|
| 295 |
+
)
|
| 296 |
+
else:
|
| 297 |
+
return _sdpa_attention_fallback(
|
| 298 |
+
q=q,
|
| 299 |
+
k=k,
|
| 300 |
+
v=v,
|
| 301 |
+
q_lens=q_lens,
|
| 302 |
+
k_lens=k_lens,
|
| 303 |
+
dropout_p=dropout_p,
|
| 304 |
+
softmax_scale=softmax_scale,
|
| 305 |
+
q_scale=q_scale,
|
| 306 |
+
causal=causal,
|
| 307 |
+
window_size=window_size,
|
| 308 |
+
dtype=dtype,
|
| 309 |
+
attn_mask=attn_mask,
|
| 310 |
+
)
|
models/wan/wan_base/modules/model.py
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.cuda.amp as amp
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
| 8 |
+
from diffusers.models.modeling_utils import ModelMixin
|
| 9 |
+
|
| 10 |
+
from .attention import flash_attention
|
| 11 |
+
|
| 12 |
+
__all__ = ['WanModel']
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def sinusoidal_embedding_1d(dim, position):
|
| 16 |
+
# preprocess
|
| 17 |
+
assert dim % 2 == 0
|
| 18 |
+
half = dim // 2
|
| 19 |
+
position = position.type(torch.float64)
|
| 20 |
+
|
| 21 |
+
# calculation
|
| 22 |
+
sinusoid = torch.outer(
|
| 23 |
+
position, torch.pow(10000, -torch.arange(half).to(position).div(half)))
|
| 24 |
+
x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
|
| 25 |
+
return x
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# @amp.autocast(enabled=False)
|
| 29 |
+
def rope_params(max_seq_len, dim, theta=10000):
|
| 30 |
+
assert dim % 2 == 0
|
| 31 |
+
freqs = torch.outer(
|
| 32 |
+
torch.arange(max_seq_len),
|
| 33 |
+
1.0 / torch.pow(theta,
|
| 34 |
+
torch.arange(0, dim, 2).to(torch.float64).div(dim)))
|
| 35 |
+
freqs = torch.polar(torch.ones_like(freqs), freqs)
|
| 36 |
+
return freqs
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# @amp.autocast(enabled=False)
|
| 40 |
+
def rope_apply(x, grid_sizes, freqs):
|
| 41 |
+
n, c = x.size(2), x.size(3) // 2
|
| 42 |
+
|
| 43 |
+
# split freqs
|
| 44 |
+
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
|
| 45 |
+
|
| 46 |
+
# loop over samples
|
| 47 |
+
output = []
|
| 48 |
+
for i, (f, h, w) in enumerate(grid_sizes.tolist()):
|
| 49 |
+
seq_len = f * h * w
|
| 50 |
+
|
| 51 |
+
# precompute multipliers
|
| 52 |
+
x_i = torch.view_as_complex(x[i, :seq_len].to(torch.float64).reshape(
|
| 53 |
+
seq_len, n, -1, 2))
|
| 54 |
+
freqs_i = torch.cat([
|
| 55 |
+
freqs[0][:f].view(f, 1, 1, -1).expand(f, h, w, -1),
|
| 56 |
+
freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
|
| 57 |
+
freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1)
|
| 58 |
+
],
|
| 59 |
+
dim=-1).reshape(seq_len, 1, -1)
|
| 60 |
+
|
| 61 |
+
# apply rotary embedding
|
| 62 |
+
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
|
| 63 |
+
x_i = torch.cat([x_i, x[i, seq_len:]])
|
| 64 |
+
|
| 65 |
+
# append to collection
|
| 66 |
+
output.append(x_i)
|
| 67 |
+
return torch.stack(output).type_as(x)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class WanRMSNorm(nn.Module):
|
| 71 |
+
|
| 72 |
+
def __init__(self, dim, eps=1e-5):
|
| 73 |
+
super().__init__()
|
| 74 |
+
self.dim = dim
|
| 75 |
+
self.eps = eps
|
| 76 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 77 |
+
|
| 78 |
+
def forward(self, x):
|
| 79 |
+
r"""
|
| 80 |
+
Args:
|
| 81 |
+
x(Tensor): Shape [B, L, C]
|
| 82 |
+
"""
|
| 83 |
+
return self._norm(x.float()).type_as(x) * self.weight
|
| 84 |
+
|
| 85 |
+
def _norm(self, x):
|
| 86 |
+
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class WanLayerNorm(nn.LayerNorm):
|
| 90 |
+
|
| 91 |
+
def __init__(self, dim, eps=1e-6, elementwise_affine=False):
|
| 92 |
+
super().__init__(dim, elementwise_affine=elementwise_affine, eps=eps)
|
| 93 |
+
|
| 94 |
+
def forward(self, x):
|
| 95 |
+
r"""
|
| 96 |
+
Args:
|
| 97 |
+
x(Tensor): Shape [B, L, C]
|
| 98 |
+
"""
|
| 99 |
+
return super().forward(x).type_as(x)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class WanSelfAttention(nn.Module):
|
| 103 |
+
|
| 104 |
+
def __init__(self,
|
| 105 |
+
dim,
|
| 106 |
+
num_heads,
|
| 107 |
+
window_size=(-1, -1),
|
| 108 |
+
qk_norm=True,
|
| 109 |
+
eps=1e-6):
|
| 110 |
+
assert dim % num_heads == 0
|
| 111 |
+
super().__init__()
|
| 112 |
+
self.dim = dim
|
| 113 |
+
self.num_heads = num_heads
|
| 114 |
+
self.head_dim = dim // num_heads
|
| 115 |
+
self.window_size = window_size
|
| 116 |
+
self.qk_norm = qk_norm
|
| 117 |
+
self.eps = eps
|
| 118 |
+
|
| 119 |
+
# layers
|
| 120 |
+
self.q = nn.Linear(dim, dim)
|
| 121 |
+
self.k = nn.Linear(dim, dim)
|
| 122 |
+
self.v = nn.Linear(dim, dim)
|
| 123 |
+
self.o = nn.Linear(dim, dim)
|
| 124 |
+
self.norm_q = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
|
| 125 |
+
self.norm_k = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
|
| 126 |
+
|
| 127 |
+
def forward(self, x, seq_lens, grid_sizes, freqs):
|
| 128 |
+
r"""
|
| 129 |
+
Args:
|
| 130 |
+
x(Tensor): Shape [B, L, num_heads, C / num_heads]
|
| 131 |
+
seq_lens(Tensor): Shape [B]
|
| 132 |
+
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
|
| 133 |
+
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
|
| 134 |
+
"""
|
| 135 |
+
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
|
| 136 |
+
|
| 137 |
+
# query, key, value function
|
| 138 |
+
def qkv_fn(x):
|
| 139 |
+
q = self.norm_q(self.q(x)).view(b, s, n, d)
|
| 140 |
+
k = self.norm_k(self.k(x)).view(b, s, n, d)
|
| 141 |
+
v = self.v(x).view(b, s, n, d)
|
| 142 |
+
return q, k, v
|
| 143 |
+
|
| 144 |
+
q, k, v = qkv_fn(x)
|
| 145 |
+
|
| 146 |
+
x = flash_attention(
|
| 147 |
+
q=rope_apply(q, grid_sizes, freqs),
|
| 148 |
+
k=rope_apply(k, grid_sizes, freqs),
|
| 149 |
+
v=v,
|
| 150 |
+
k_lens=seq_lens,
|
| 151 |
+
window_size=self.window_size)
|
| 152 |
+
|
| 153 |
+
# output
|
| 154 |
+
x = x.flatten(2)
|
| 155 |
+
x = self.o(x)
|
| 156 |
+
return x
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class WanT2VCrossAttention(WanSelfAttention):
|
| 160 |
+
|
| 161 |
+
def forward(self, x, context, context_lens, crossattn_cache=None):
|
| 162 |
+
r"""
|
| 163 |
+
Args:
|
| 164 |
+
x(Tensor): Shape [B, L1, C]
|
| 165 |
+
context(Tensor): Shape [B, L2, C]
|
| 166 |
+
context_lens(Tensor): Shape [B]
|
| 167 |
+
crossattn_cache (List[dict], *optional*): Contains the cached key and value tensors for context embedding.
|
| 168 |
+
"""
|
| 169 |
+
b, n, d = x.size(0), self.num_heads, self.head_dim
|
| 170 |
+
|
| 171 |
+
# compute query, key, value
|
| 172 |
+
q = self.norm_q(self.q(x)).view(b, -1, n, d)
|
| 173 |
+
|
| 174 |
+
if crossattn_cache is not None:
|
| 175 |
+
if not crossattn_cache["is_init"]:
|
| 176 |
+
crossattn_cache["is_init"] = True
|
| 177 |
+
k = self.norm_k(self.k(context)).view(b, -1, n, d)
|
| 178 |
+
v = self.v(context).view(b, -1, n, d)
|
| 179 |
+
crossattn_cache["k"] = k
|
| 180 |
+
crossattn_cache["v"] = v
|
| 181 |
+
else:
|
| 182 |
+
k = crossattn_cache["k"]
|
| 183 |
+
v = crossattn_cache["v"]
|
| 184 |
+
else:
|
| 185 |
+
k = self.norm_k(self.k(context)).view(b, -1, n, d)
|
| 186 |
+
v = self.v(context).view(b, -1, n, d)
|
| 187 |
+
|
| 188 |
+
# compute attention
|
| 189 |
+
x = flash_attention(q, k, v, k_lens=context_lens)
|
| 190 |
+
|
| 191 |
+
# output
|
| 192 |
+
x = x.flatten(2)
|
| 193 |
+
x = self.o(x)
|
| 194 |
+
return x
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class WanI2VCrossAttention(WanSelfAttention):
|
| 198 |
+
|
| 199 |
+
def __init__(self,
|
| 200 |
+
dim,
|
| 201 |
+
num_heads,
|
| 202 |
+
window_size=(-1, -1),
|
| 203 |
+
qk_norm=True,
|
| 204 |
+
eps=1e-6):
|
| 205 |
+
super().__init__(dim, num_heads, window_size, qk_norm, eps)
|
| 206 |
+
|
| 207 |
+
self.k_img = nn.Linear(dim, dim)
|
| 208 |
+
self.v_img = nn.Linear(dim, dim)
|
| 209 |
+
# self.alpha = nn.Parameter(torch.zeros((1, )))
|
| 210 |
+
self.norm_k_img = WanRMSNorm(
|
| 211 |
+
dim, eps=eps) if qk_norm else nn.Identity()
|
| 212 |
+
|
| 213 |
+
def forward(self, x, context, context_lens):
|
| 214 |
+
r"""
|
| 215 |
+
Args:
|
| 216 |
+
x(Tensor): Shape [B, L1, C]
|
| 217 |
+
context(Tensor): Shape [B, L2, C]
|
| 218 |
+
context_lens(Tensor): Shape [B]
|
| 219 |
+
"""
|
| 220 |
+
context_img = context[:, :257]
|
| 221 |
+
context = context[:, 257:]
|
| 222 |
+
b, n, d = x.size(0), self.num_heads, self.head_dim
|
| 223 |
+
|
| 224 |
+
# compute query, key, value
|
| 225 |
+
q = self.norm_q(self.q(x)).view(b, -1, n, d)
|
| 226 |
+
k = self.norm_k(self.k(context)).view(b, -1, n, d)
|
| 227 |
+
v = self.v(context).view(b, -1, n, d)
|
| 228 |
+
k_img = self.norm_k_img(self.k_img(context_img)).view(b, -1, n, d)
|
| 229 |
+
v_img = self.v_img(context_img).view(b, -1, n, d)
|
| 230 |
+
img_x = flash_attention(q, k_img, v_img, k_lens=None)
|
| 231 |
+
# compute attention
|
| 232 |
+
x = flash_attention(q, k, v, k_lens=context_lens)
|
| 233 |
+
|
| 234 |
+
# output
|
| 235 |
+
x = x.flatten(2)
|
| 236 |
+
img_x = img_x.flatten(2)
|
| 237 |
+
x = x + img_x
|
| 238 |
+
x = self.o(x)
|
| 239 |
+
return x
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
WAN_CROSSATTENTION_CLASSES = {
|
| 243 |
+
't2v_cross_attn': WanT2VCrossAttention,
|
| 244 |
+
'i2v_cross_attn': WanI2VCrossAttention,
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class WanAttentionBlock(nn.Module):
|
| 249 |
+
|
| 250 |
+
def __init__(self,
|
| 251 |
+
cross_attn_type,
|
| 252 |
+
dim,
|
| 253 |
+
ffn_dim,
|
| 254 |
+
num_heads,
|
| 255 |
+
window_size=(-1, -1),
|
| 256 |
+
qk_norm=True,
|
| 257 |
+
cross_attn_norm=False,
|
| 258 |
+
eps=1e-6):
|
| 259 |
+
super().__init__()
|
| 260 |
+
self.dim = dim
|
| 261 |
+
self.ffn_dim = ffn_dim
|
| 262 |
+
self.num_heads = num_heads
|
| 263 |
+
self.window_size = window_size
|
| 264 |
+
self.qk_norm = qk_norm
|
| 265 |
+
self.cross_attn_norm = cross_attn_norm
|
| 266 |
+
self.eps = eps
|
| 267 |
+
|
| 268 |
+
# layers
|
| 269 |
+
self.norm1 = WanLayerNorm(dim, eps)
|
| 270 |
+
self.self_attn = WanSelfAttention(dim, num_heads, window_size, qk_norm,
|
| 271 |
+
eps)
|
| 272 |
+
self.norm3 = WanLayerNorm(
|
| 273 |
+
dim, eps,
|
| 274 |
+
elementwise_affine=True) if cross_attn_norm else nn.Identity()
|
| 275 |
+
self.cross_attn = WAN_CROSSATTENTION_CLASSES[cross_attn_type](dim,
|
| 276 |
+
num_heads,
|
| 277 |
+
(-1, -1),
|
| 278 |
+
qk_norm,
|
| 279 |
+
eps)
|
| 280 |
+
self.norm2 = WanLayerNorm(dim, eps)
|
| 281 |
+
self.ffn = nn.Sequential(
|
| 282 |
+
nn.Linear(dim, ffn_dim), nn.GELU(approximate='tanh'),
|
| 283 |
+
nn.Linear(ffn_dim, dim))
|
| 284 |
+
|
| 285 |
+
# modulation
|
| 286 |
+
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
|
| 287 |
+
|
| 288 |
+
def forward(
|
| 289 |
+
self,
|
| 290 |
+
x,
|
| 291 |
+
e,
|
| 292 |
+
seq_lens,
|
| 293 |
+
grid_sizes,
|
| 294 |
+
freqs,
|
| 295 |
+
context,
|
| 296 |
+
context_lens,
|
| 297 |
+
):
|
| 298 |
+
r"""
|
| 299 |
+
Args:
|
| 300 |
+
x(Tensor): Shape [B, L, C]
|
| 301 |
+
e(Tensor): Shape [B, 6, C]
|
| 302 |
+
seq_lens(Tensor): Shape [B], length of each sequence in batch
|
| 303 |
+
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
|
| 304 |
+
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
|
| 305 |
+
"""
|
| 306 |
+
# assert e.dtype == torch.float32
|
| 307 |
+
# with amp.autocast(dtype=torch.float32):
|
| 308 |
+
e = (self.modulation + e).chunk(6, dim=1)
|
| 309 |
+
# assert e[0].dtype == torch.float32
|
| 310 |
+
|
| 311 |
+
# self-attention
|
| 312 |
+
y = self.self_attn(
|
| 313 |
+
self.norm1(x) * (1 + e[1]) + e[0], seq_lens, grid_sizes,
|
| 314 |
+
freqs)
|
| 315 |
+
# with amp.autocast(dtype=torch.float32):
|
| 316 |
+
x = x + y * e[2]
|
| 317 |
+
|
| 318 |
+
# cross-attention & ffn function
|
| 319 |
+
def cross_attn_ffn(x, context, context_lens, e):
|
| 320 |
+
x = x + self.cross_attn(self.norm3(x), context, context_lens)
|
| 321 |
+
y = self.ffn(self.norm2(x) * (1 + e[4]) + e[3])
|
| 322 |
+
# with amp.autocast(dtype=torch.float32):
|
| 323 |
+
x = x + y * e[5]
|
| 324 |
+
return x
|
| 325 |
+
|
| 326 |
+
x = cross_attn_ffn(x, context, context_lens, e)
|
| 327 |
+
return x
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class Head(nn.Module):
|
| 331 |
+
|
| 332 |
+
def __init__(self, dim, out_dim, patch_size, eps=1e-6):
|
| 333 |
+
super().__init__()
|
| 334 |
+
self.dim = dim
|
| 335 |
+
self.out_dim = out_dim
|
| 336 |
+
self.patch_size = patch_size
|
| 337 |
+
self.eps = eps
|
| 338 |
+
|
| 339 |
+
# layers
|
| 340 |
+
out_dim = math.prod(patch_size) * out_dim
|
| 341 |
+
self.norm = WanLayerNorm(dim, eps)
|
| 342 |
+
self.head = nn.Linear(dim, out_dim)
|
| 343 |
+
|
| 344 |
+
# modulation
|
| 345 |
+
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
|
| 346 |
+
|
| 347 |
+
def forward(self, x, e):
|
| 348 |
+
r"""
|
| 349 |
+
Args:
|
| 350 |
+
x(Tensor): Shape [B, L1, C]
|
| 351 |
+
e(Tensor): Shape [B, C]
|
| 352 |
+
"""
|
| 353 |
+
# assert e.dtype == torch.float32
|
| 354 |
+
# with amp.autocast(dtype=torch.float32):
|
| 355 |
+
e = (self.modulation + e.unsqueeze(1)).chunk(2, dim=1)
|
| 356 |
+
x = (self.head(self.norm(x) * (1 + e[1]) + e[0]))
|
| 357 |
+
return x
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
class MLPProj(torch.nn.Module):
|
| 361 |
+
|
| 362 |
+
def __init__(self, in_dim, out_dim):
|
| 363 |
+
super().__init__()
|
| 364 |
+
|
| 365 |
+
self.proj = torch.nn.Sequential(
|
| 366 |
+
torch.nn.LayerNorm(in_dim), torch.nn.Linear(in_dim, in_dim),
|
| 367 |
+
torch.nn.GELU(), torch.nn.Linear(in_dim, out_dim),
|
| 368 |
+
torch.nn.LayerNorm(out_dim))
|
| 369 |
+
|
| 370 |
+
def forward(self, image_embeds):
|
| 371 |
+
clip_extra_context_tokens = self.proj(image_embeds)
|
| 372 |
+
return clip_extra_context_tokens
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
class WanModel(ModelMixin, ConfigMixin):
|
| 376 |
+
r"""
|
| 377 |
+
Wan diffusion backbone supporting both text-to-video and image-to-video.
|
| 378 |
+
"""
|
| 379 |
+
|
| 380 |
+
ignore_for_config = [
|
| 381 |
+
'patch_size', 'cross_attn_norm', 'qk_norm', 'text_dim', 'window_size'
|
| 382 |
+
]
|
| 383 |
+
_no_split_modules = ['WanAttentionBlock']
|
| 384 |
+
_supports_gradient_checkpointing = True
|
| 385 |
+
|
| 386 |
+
@register_to_config
|
| 387 |
+
def __init__(self,
|
| 388 |
+
model_type='t2v',
|
| 389 |
+
patch_size=(1, 2, 2),
|
| 390 |
+
text_len=512,
|
| 391 |
+
in_dim=16,
|
| 392 |
+
dim=2048,
|
| 393 |
+
ffn_dim=8192,
|
| 394 |
+
freq_dim=256,
|
| 395 |
+
text_dim=4096,
|
| 396 |
+
out_dim=16,
|
| 397 |
+
num_heads=16,
|
| 398 |
+
num_layers=32,
|
| 399 |
+
window_size=(-1, -1),
|
| 400 |
+
qk_norm=True,
|
| 401 |
+
cross_attn_norm=True,
|
| 402 |
+
eps=1e-6):
|
| 403 |
+
r"""
|
| 404 |
+
Initialize the diffusion model backbone.
|
| 405 |
+
|
| 406 |
+
Args:
|
| 407 |
+
model_type (`str`, *optional*, defaults to 't2v'):
|
| 408 |
+
Model variant - 't2v' (text-to-video) or 'i2v' (image-to-video)
|
| 409 |
+
patch_size (`tuple`, *optional*, defaults to (1, 2, 2)):
|
| 410 |
+
3D patch dimensions for video embedding (t_patch, h_patch, w_patch)
|
| 411 |
+
text_len (`int`, *optional*, defaults to 512):
|
| 412 |
+
Fixed length for text embeddings
|
| 413 |
+
in_dim (`int`, *optional*, defaults to 16):
|
| 414 |
+
Input video channels (C_in)
|
| 415 |
+
dim (`int`, *optional*, defaults to 2048):
|
| 416 |
+
Hidden dimension of the transformer
|
| 417 |
+
ffn_dim (`int`, *optional*, defaults to 8192):
|
| 418 |
+
Intermediate dimension in feed-forward network
|
| 419 |
+
freq_dim (`int`, *optional*, defaults to 256):
|
| 420 |
+
Dimension for sinusoidal time embeddings
|
| 421 |
+
text_dim (`int`, *optional*, defaults to 4096):
|
| 422 |
+
Input dimension for text embeddings
|
| 423 |
+
out_dim (`int`, *optional*, defaults to 16):
|
| 424 |
+
Output video channels (C_out)
|
| 425 |
+
num_heads (`int`, *optional*, defaults to 16):
|
| 426 |
+
Number of attention heads
|
| 427 |
+
num_layers (`int`, *optional*, defaults to 32):
|
| 428 |
+
Number of transformer blocks
|
| 429 |
+
window_size (`tuple`, *optional*, defaults to (-1, -1)):
|
| 430 |
+
Window size for local attention (-1 indicates global attention)
|
| 431 |
+
qk_norm (`bool`, *optional*, defaults to True):
|
| 432 |
+
Enable query/key normalization
|
| 433 |
+
cross_attn_norm (`bool`, *optional*, defaults to False):
|
| 434 |
+
Enable cross-attention normalization
|
| 435 |
+
eps (`float`, *optional*, defaults to 1e-6):
|
| 436 |
+
Epsilon value for normalization layers
|
| 437 |
+
"""
|
| 438 |
+
|
| 439 |
+
super().__init__()
|
| 440 |
+
|
| 441 |
+
assert model_type in ['t2v', 'i2v']
|
| 442 |
+
self.model_type = model_type
|
| 443 |
+
|
| 444 |
+
self.patch_size = patch_size
|
| 445 |
+
self.text_len = text_len
|
| 446 |
+
self.in_dim = in_dim
|
| 447 |
+
self.dim = dim
|
| 448 |
+
self.ffn_dim = ffn_dim
|
| 449 |
+
self.freq_dim = freq_dim
|
| 450 |
+
self.text_dim = text_dim
|
| 451 |
+
self.out_dim = out_dim
|
| 452 |
+
self.num_heads = num_heads
|
| 453 |
+
self.num_layers = num_layers
|
| 454 |
+
self.window_size = window_size
|
| 455 |
+
self.qk_norm = qk_norm
|
| 456 |
+
self.cross_attn_norm = cross_attn_norm
|
| 457 |
+
self.eps = eps
|
| 458 |
+
|
| 459 |
+
# embeddings
|
| 460 |
+
self.patch_embedding = nn.Conv3d(
|
| 461 |
+
in_dim, dim, kernel_size=patch_size, stride=patch_size)
|
| 462 |
+
self.text_embedding = nn.Sequential(
|
| 463 |
+
nn.Linear(text_dim, dim), nn.GELU(approximate='tanh'),
|
| 464 |
+
nn.Linear(dim, dim))
|
| 465 |
+
|
| 466 |
+
self.time_embedding = nn.Sequential(
|
| 467 |
+
nn.Linear(freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
|
| 468 |
+
self.time_projection = nn.Sequential(
|
| 469 |
+
nn.SiLU(), nn.Linear(dim, dim * 6))
|
| 470 |
+
|
| 471 |
+
# blocks
|
| 472 |
+
cross_attn_type = 't2v_cross_attn' if model_type == 't2v' else 'i2v_cross_attn'
|
| 473 |
+
self.blocks = nn.ModuleList([
|
| 474 |
+
WanAttentionBlock(cross_attn_type, dim, ffn_dim, num_heads,
|
| 475 |
+
window_size, qk_norm, cross_attn_norm, eps)
|
| 476 |
+
for _ in range(num_layers)
|
| 477 |
+
])
|
| 478 |
+
|
| 479 |
+
# head
|
| 480 |
+
self.head = Head(dim, out_dim, patch_size, eps)
|
| 481 |
+
|
| 482 |
+
# buffers (don't use register_buffer otherwise dtype will be changed in to())
|
| 483 |
+
assert (dim % num_heads) == 0 and (dim // num_heads) % 2 == 0
|
| 484 |
+
d = dim // num_heads
|
| 485 |
+
self.freqs = torch.cat([
|
| 486 |
+
rope_params(1024, d - 4 * (d // 6)),
|
| 487 |
+
rope_params(1024, 2 * (d // 6)),
|
| 488 |
+
rope_params(1024, 2 * (d // 6))
|
| 489 |
+
],
|
| 490 |
+
dim=1)
|
| 491 |
+
|
| 492 |
+
if model_type == 'i2v':
|
| 493 |
+
self.img_emb = MLPProj(1280, dim)
|
| 494 |
+
|
| 495 |
+
# initialize weights
|
| 496 |
+
self.init_weights()
|
| 497 |
+
|
| 498 |
+
self.gradient_checkpointing = False
|
| 499 |
+
|
| 500 |
+
def _set_gradient_checkpointing(self, module, value=False):
|
| 501 |
+
self.gradient_checkpointing = value
|
| 502 |
+
|
| 503 |
+
def forward(
|
| 504 |
+
self,
|
| 505 |
+
x,
|
| 506 |
+
t,
|
| 507 |
+
context,
|
| 508 |
+
seq_len,
|
| 509 |
+
clip_fea=None,
|
| 510 |
+
y=None,
|
| 511 |
+
):
|
| 512 |
+
r"""
|
| 513 |
+
Forward pass through the diffusion model
|
| 514 |
+
|
| 515 |
+
Args:
|
| 516 |
+
x (List[Tensor]):
|
| 517 |
+
List of input video tensors, each with shape [C_in, F, H, W]
|
| 518 |
+
t (Tensor):
|
| 519 |
+
Diffusion timesteps tensor of shape [B]
|
| 520 |
+
context (List[Tensor]):
|
| 521 |
+
List of text embeddings each with shape [L, C]
|
| 522 |
+
seq_len (`int`):
|
| 523 |
+
Maximum sequence length for positional encoding
|
| 524 |
+
clip_fea (Tensor, *optional*):
|
| 525 |
+
CLIP image features for image-to-video mode
|
| 526 |
+
y (List[Tensor], *optional*):
|
| 527 |
+
Conditional video inputs for image-to-video mode, same shape as x
|
| 528 |
+
|
| 529 |
+
Returns:
|
| 530 |
+
List[Tensor]:
|
| 531 |
+
List of denoised video tensors with original input shapes [C_out, F, H / 8, W / 8]
|
| 532 |
+
"""
|
| 533 |
+
if self.model_type == 'i2v':
|
| 534 |
+
assert clip_fea is not None and y is not None
|
| 535 |
+
# params
|
| 536 |
+
device = self.patch_embedding.weight.device
|
| 537 |
+
if self.freqs.device != device:
|
| 538 |
+
self.freqs = self.freqs.to(device)
|
| 539 |
+
|
| 540 |
+
if y is not None:
|
| 541 |
+
x = [torch.cat([u, v], dim=0) for u, v in zip(x, y)]
|
| 542 |
+
|
| 543 |
+
# embeddings
|
| 544 |
+
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
|
| 545 |
+
grid_sizes = torch.stack(
|
| 546 |
+
[torch.tensor(u.shape[2:], dtype=torch.long) for u in x])
|
| 547 |
+
x = [u.flatten(2).transpose(1, 2) for u in x]
|
| 548 |
+
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long)
|
| 549 |
+
assert seq_lens.max() <= seq_len
|
| 550 |
+
x = torch.cat([
|
| 551 |
+
torch.cat([u, u.new_zeros(1, seq_len - u.size(1), u.size(2))],
|
| 552 |
+
dim=1) for u in x
|
| 553 |
+
])
|
| 554 |
+
|
| 555 |
+
# time embeddings
|
| 556 |
+
# with amp.autocast(dtype=torch.float32):
|
| 557 |
+
e = self.time_embedding(
|
| 558 |
+
sinusoidal_embedding_1d(self.freq_dim, t).type_as(x))
|
| 559 |
+
e0 = self.time_projection(e).unflatten(1, (6, self.dim))
|
| 560 |
+
# assert e.dtype == torch.float32 and e0.dtype == torch.float32
|
| 561 |
+
|
| 562 |
+
# context
|
| 563 |
+
context_lens = None
|
| 564 |
+
context = self.text_embedding(
|
| 565 |
+
torch.stack([
|
| 566 |
+
torch.cat(
|
| 567 |
+
[u, u.new_zeros(self.text_len - u.size(0), u.size(1))])
|
| 568 |
+
for u in context
|
| 569 |
+
]))
|
| 570 |
+
|
| 571 |
+
if clip_fea is not None:
|
| 572 |
+
context_clip = self.img_emb(clip_fea) # bs x 257 x dim
|
| 573 |
+
context = torch.concat([context_clip, context], dim=1)
|
| 574 |
+
|
| 575 |
+
# arguments
|
| 576 |
+
kwargs = dict(
|
| 577 |
+
e=e0,
|
| 578 |
+
seq_lens=seq_lens,
|
| 579 |
+
grid_sizes=grid_sizes,
|
| 580 |
+
freqs=self.freqs,
|
| 581 |
+
context=context,
|
| 582 |
+
context_lens=context_lens)
|
| 583 |
+
|
| 584 |
+
def create_custom_forward(module):
|
| 585 |
+
def custom_forward(*inputs, **kwargs):
|
| 586 |
+
return module(*inputs, **kwargs)
|
| 587 |
+
return custom_forward
|
| 588 |
+
|
| 589 |
+
for block in self.blocks:
|
| 590 |
+
if torch.is_grad_enabled() and self.gradient_checkpointing:
|
| 591 |
+
x = torch.utils.checkpoint.checkpoint(
|
| 592 |
+
create_custom_forward(block),
|
| 593 |
+
x, **kwargs,
|
| 594 |
+
use_reentrant=False,
|
| 595 |
+
)
|
| 596 |
+
else:
|
| 597 |
+
x = block(x, **kwargs)
|
| 598 |
+
|
| 599 |
+
# head
|
| 600 |
+
x = self.head(x, e)
|
| 601 |
+
|
| 602 |
+
# unpatchify
|
| 603 |
+
x = self.unpatchify(x, grid_sizes)
|
| 604 |
+
return torch.stack(x)
|
| 605 |
+
|
| 606 |
+
def unpatchify(self, x, grid_sizes):
|
| 607 |
+
r"""
|
| 608 |
+
Reconstruct video tensors from patch embeddings.
|
| 609 |
+
|
| 610 |
+
Args:
|
| 611 |
+
x (List[Tensor]):
|
| 612 |
+
List of patchified features, each with shape [L, C_out * prod(patch_size)]
|
| 613 |
+
grid_sizes (Tensor):
|
| 614 |
+
Original spatial-temporal grid dimensions before patching,
|
| 615 |
+
shape [B, 3] (3 dimensions correspond to F_patches, H_patches, W_patches)
|
| 616 |
+
|
| 617 |
+
Returns:
|
| 618 |
+
List[Tensor]:
|
| 619 |
+
Reconstructed video tensors with shape [C_out, F, H / 8, W / 8]
|
| 620 |
+
"""
|
| 621 |
+
|
| 622 |
+
c = self.out_dim
|
| 623 |
+
out = []
|
| 624 |
+
for u, v in zip(x, grid_sizes.tolist()):
|
| 625 |
+
u = u[:math.prod(v)].view(*v, *self.patch_size, c)
|
| 626 |
+
u = torch.einsum('fhwpqrc->cfphqwr', u)
|
| 627 |
+
u = u.reshape(c, *[i * j for i, j in zip(v, self.patch_size)])
|
| 628 |
+
out.append(u)
|
| 629 |
+
return out
|
| 630 |
+
|
| 631 |
+
def init_weights(self):
|
| 632 |
+
r"""
|
| 633 |
+
Initialize model parameters using Xavier initialization.
|
| 634 |
+
"""
|
| 635 |
+
|
| 636 |
+
# basic init
|
| 637 |
+
for m in self.modules():
|
| 638 |
+
if isinstance(m, nn.Linear):
|
| 639 |
+
nn.init.xavier_uniform_(m.weight)
|
| 640 |
+
if m.bias is not None:
|
| 641 |
+
nn.init.zeros_(m.bias)
|
| 642 |
+
|
| 643 |
+
# init embeddings
|
| 644 |
+
nn.init.xavier_uniform_(self.patch_embedding.weight.flatten(1))
|
| 645 |
+
for m in self.text_embedding.modules():
|
| 646 |
+
if isinstance(m, nn.Linear):
|
| 647 |
+
nn.init.normal_(m.weight, std=.02)
|
| 648 |
+
for m in self.time_embedding.modules():
|
| 649 |
+
if isinstance(m, nn.Linear):
|
| 650 |
+
nn.init.normal_(m.weight, std=.02)
|
| 651 |
+
|
| 652 |
+
# init output layer
|
| 653 |
+
nn.init.zeros_(self.head.head.weight)
|
models/wan/wan_base/modules/t5.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified from transformers.models.t5.modeling_t5
|
| 2 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 3 |
+
import logging
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from .tokenizers import HuggingfaceTokenizer
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
'T5Model',
|
| 14 |
+
'T5Encoder',
|
| 15 |
+
'T5Decoder',
|
| 16 |
+
'T5EncoderModel',
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def fp16_clamp(x):
|
| 21 |
+
if x.dtype == torch.float16 and torch.isinf(x).any():
|
| 22 |
+
clamp = torch.finfo(x.dtype).max - 1000
|
| 23 |
+
x = torch.clamp(x, min=-clamp, max=clamp)
|
| 24 |
+
return x
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def init_weights(m):
|
| 28 |
+
if isinstance(m, T5LayerNorm):
|
| 29 |
+
nn.init.ones_(m.weight)
|
| 30 |
+
elif isinstance(m, T5Model):
|
| 31 |
+
nn.init.normal_(m.token_embedding.weight, std=1.0)
|
| 32 |
+
elif isinstance(m, T5FeedForward):
|
| 33 |
+
nn.init.normal_(m.gate[0].weight, std=m.dim**-0.5)
|
| 34 |
+
nn.init.normal_(m.fc1.weight, std=m.dim**-0.5)
|
| 35 |
+
nn.init.normal_(m.fc2.weight, std=m.dim_ffn**-0.5)
|
| 36 |
+
elif isinstance(m, T5Attention):
|
| 37 |
+
nn.init.normal_(m.q.weight, std=(m.dim * m.dim_attn)**-0.5)
|
| 38 |
+
nn.init.normal_(m.k.weight, std=m.dim**-0.5)
|
| 39 |
+
nn.init.normal_(m.v.weight, std=m.dim**-0.5)
|
| 40 |
+
nn.init.normal_(m.o.weight, std=(m.num_heads * m.dim_attn)**-0.5)
|
| 41 |
+
elif isinstance(m, T5RelativeEmbedding):
|
| 42 |
+
nn.init.normal_(
|
| 43 |
+
m.embedding.weight, std=(2 * m.num_buckets * m.num_heads)**-0.5)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class GELU(nn.Module):
|
| 47 |
+
|
| 48 |
+
def forward(self, x):
|
| 49 |
+
return 0.5 * x * (1.0 + torch.tanh(
|
| 50 |
+
math.sqrt(2.0 / math.pi) * (x + 0.044715 * torch.pow(x, 3.0))))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class T5LayerNorm(nn.Module):
|
| 54 |
+
|
| 55 |
+
def __init__(self, dim, eps=1e-6):
|
| 56 |
+
super(T5LayerNorm, self).__init__()
|
| 57 |
+
self.dim = dim
|
| 58 |
+
self.eps = eps
|
| 59 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 60 |
+
|
| 61 |
+
def forward(self, x):
|
| 62 |
+
x = x * torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) +
|
| 63 |
+
self.eps)
|
| 64 |
+
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
| 65 |
+
x = x.type_as(self.weight)
|
| 66 |
+
return self.weight * x
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class T5Attention(nn.Module):
|
| 70 |
+
|
| 71 |
+
def __init__(self, dim, dim_attn, num_heads, dropout=0.1):
|
| 72 |
+
assert dim_attn % num_heads == 0
|
| 73 |
+
super(T5Attention, self).__init__()
|
| 74 |
+
self.dim = dim
|
| 75 |
+
self.dim_attn = dim_attn
|
| 76 |
+
self.num_heads = num_heads
|
| 77 |
+
self.head_dim = dim_attn // num_heads
|
| 78 |
+
|
| 79 |
+
# layers
|
| 80 |
+
self.q = nn.Linear(dim, dim_attn, bias=False)
|
| 81 |
+
self.k = nn.Linear(dim, dim_attn, bias=False)
|
| 82 |
+
self.v = nn.Linear(dim, dim_attn, bias=False)
|
| 83 |
+
self.o = nn.Linear(dim_attn, dim, bias=False)
|
| 84 |
+
self.dropout = nn.Dropout(dropout)
|
| 85 |
+
|
| 86 |
+
def forward(self, x, context=None, mask=None, pos_bias=None):
|
| 87 |
+
"""
|
| 88 |
+
x: [B, L1, C].
|
| 89 |
+
context: [B, L2, C] or None.
|
| 90 |
+
mask: [B, L2] or [B, L1, L2] or None.
|
| 91 |
+
"""
|
| 92 |
+
# check inputs
|
| 93 |
+
context = x if context is None else context
|
| 94 |
+
b, n, c = x.size(0), self.num_heads, self.head_dim
|
| 95 |
+
|
| 96 |
+
# compute query, key, value
|
| 97 |
+
q = self.q(x).view(b, -1, n, c)
|
| 98 |
+
k = self.k(context).view(b, -1, n, c)
|
| 99 |
+
v = self.v(context).view(b, -1, n, c)
|
| 100 |
+
|
| 101 |
+
# attention bias
|
| 102 |
+
attn_bias = x.new_zeros(b, n, q.size(1), k.size(1))
|
| 103 |
+
if pos_bias is not None:
|
| 104 |
+
attn_bias += pos_bias
|
| 105 |
+
if mask is not None:
|
| 106 |
+
assert mask.ndim in [2, 3]
|
| 107 |
+
mask = mask.view(b, 1, 1,
|
| 108 |
+
-1) if mask.ndim == 2 else mask.unsqueeze(1)
|
| 109 |
+
attn_bias.masked_fill_(mask == 0, torch.finfo(x.dtype).min)
|
| 110 |
+
|
| 111 |
+
# compute attention (T5 does not use scaling)
|
| 112 |
+
attn = torch.einsum('binc,bjnc->bnij', q, k) + attn_bias
|
| 113 |
+
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
|
| 114 |
+
x = torch.einsum('bnij,bjnc->binc', attn, v)
|
| 115 |
+
|
| 116 |
+
# output
|
| 117 |
+
x = x.reshape(b, -1, n * c)
|
| 118 |
+
x = self.o(x)
|
| 119 |
+
x = self.dropout(x)
|
| 120 |
+
return x
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class T5FeedForward(nn.Module):
|
| 124 |
+
|
| 125 |
+
def __init__(self, dim, dim_ffn, dropout=0.1):
|
| 126 |
+
super(T5FeedForward, self).__init__()
|
| 127 |
+
self.dim = dim
|
| 128 |
+
self.dim_ffn = dim_ffn
|
| 129 |
+
|
| 130 |
+
# layers
|
| 131 |
+
self.gate = nn.Sequential(nn.Linear(dim, dim_ffn, bias=False), GELU())
|
| 132 |
+
self.fc1 = nn.Linear(dim, dim_ffn, bias=False)
|
| 133 |
+
self.fc2 = nn.Linear(dim_ffn, dim, bias=False)
|
| 134 |
+
self.dropout = nn.Dropout(dropout)
|
| 135 |
+
|
| 136 |
+
def forward(self, x):
|
| 137 |
+
x = self.fc1(x) * self.gate(x)
|
| 138 |
+
x = self.dropout(x)
|
| 139 |
+
x = self.fc2(x)
|
| 140 |
+
x = self.dropout(x)
|
| 141 |
+
return x
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class T5SelfAttention(nn.Module):
|
| 145 |
+
|
| 146 |
+
def __init__(self,
|
| 147 |
+
dim,
|
| 148 |
+
dim_attn,
|
| 149 |
+
dim_ffn,
|
| 150 |
+
num_heads,
|
| 151 |
+
num_buckets,
|
| 152 |
+
shared_pos=True,
|
| 153 |
+
dropout=0.1):
|
| 154 |
+
super(T5SelfAttention, self).__init__()
|
| 155 |
+
self.dim = dim
|
| 156 |
+
self.dim_attn = dim_attn
|
| 157 |
+
self.dim_ffn = dim_ffn
|
| 158 |
+
self.num_heads = num_heads
|
| 159 |
+
self.num_buckets = num_buckets
|
| 160 |
+
self.shared_pos = shared_pos
|
| 161 |
+
|
| 162 |
+
# layers
|
| 163 |
+
self.norm1 = T5LayerNorm(dim)
|
| 164 |
+
self.attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
| 165 |
+
self.norm2 = T5LayerNorm(dim)
|
| 166 |
+
self.ffn = T5FeedForward(dim, dim_ffn, dropout)
|
| 167 |
+
self.pos_embedding = None if shared_pos else T5RelativeEmbedding(
|
| 168 |
+
num_buckets, num_heads, bidirectional=True)
|
| 169 |
+
|
| 170 |
+
def forward(self, x, mask=None, pos_bias=None):
|
| 171 |
+
e = pos_bias if self.shared_pos else self.pos_embedding(
|
| 172 |
+
x.size(1), x.size(1))
|
| 173 |
+
x = fp16_clamp(x + self.attn(self.norm1(x), mask=mask, pos_bias=e))
|
| 174 |
+
x = fp16_clamp(x + self.ffn(self.norm2(x)))
|
| 175 |
+
return x
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class T5CrossAttention(nn.Module):
|
| 179 |
+
|
| 180 |
+
def __init__(self,
|
| 181 |
+
dim,
|
| 182 |
+
dim_attn,
|
| 183 |
+
dim_ffn,
|
| 184 |
+
num_heads,
|
| 185 |
+
num_buckets,
|
| 186 |
+
shared_pos=True,
|
| 187 |
+
dropout=0.1):
|
| 188 |
+
super(T5CrossAttention, self).__init__()
|
| 189 |
+
self.dim = dim
|
| 190 |
+
self.dim_attn = dim_attn
|
| 191 |
+
self.dim_ffn = dim_ffn
|
| 192 |
+
self.num_heads = num_heads
|
| 193 |
+
self.num_buckets = num_buckets
|
| 194 |
+
self.shared_pos = shared_pos
|
| 195 |
+
|
| 196 |
+
# layers
|
| 197 |
+
self.norm1 = T5LayerNorm(dim)
|
| 198 |
+
self.self_attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
| 199 |
+
self.norm2 = T5LayerNorm(dim)
|
| 200 |
+
self.cross_attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
| 201 |
+
self.norm3 = T5LayerNorm(dim)
|
| 202 |
+
self.ffn = T5FeedForward(dim, dim_ffn, dropout)
|
| 203 |
+
self.pos_embedding = None if shared_pos else T5RelativeEmbedding(
|
| 204 |
+
num_buckets, num_heads, bidirectional=False)
|
| 205 |
+
|
| 206 |
+
def forward(self,
|
| 207 |
+
x,
|
| 208 |
+
mask=None,
|
| 209 |
+
encoder_states=None,
|
| 210 |
+
encoder_mask=None,
|
| 211 |
+
pos_bias=None):
|
| 212 |
+
e = pos_bias if self.shared_pos else self.pos_embedding(
|
| 213 |
+
x.size(1), x.size(1))
|
| 214 |
+
x = fp16_clamp(x + self.self_attn(self.norm1(x), mask=mask, pos_bias=e))
|
| 215 |
+
x = fp16_clamp(x + self.cross_attn(
|
| 216 |
+
self.norm2(x), context=encoder_states, mask=encoder_mask))
|
| 217 |
+
x = fp16_clamp(x + self.ffn(self.norm3(x)))
|
| 218 |
+
return x
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class T5RelativeEmbedding(nn.Module):
|
| 222 |
+
|
| 223 |
+
def __init__(self, num_buckets, num_heads, bidirectional, max_dist=128):
|
| 224 |
+
super(T5RelativeEmbedding, self).__init__()
|
| 225 |
+
self.num_buckets = num_buckets
|
| 226 |
+
self.num_heads = num_heads
|
| 227 |
+
self.bidirectional = bidirectional
|
| 228 |
+
self.max_dist = max_dist
|
| 229 |
+
|
| 230 |
+
# layers
|
| 231 |
+
self.embedding = nn.Embedding(num_buckets, num_heads)
|
| 232 |
+
|
| 233 |
+
def forward(self, lq, lk):
|
| 234 |
+
device = self.embedding.weight.device
|
| 235 |
+
# rel_pos = torch.arange(lk).unsqueeze(0).to(device) - \
|
| 236 |
+
# torch.arange(lq).unsqueeze(1).to(device)
|
| 237 |
+
rel_pos = torch.arange(lk, device=device).unsqueeze(0) - \
|
| 238 |
+
torch.arange(lq, device=device).unsqueeze(1)
|
| 239 |
+
rel_pos = self._relative_position_bucket(rel_pos)
|
| 240 |
+
rel_pos_embeds = self.embedding(rel_pos)
|
| 241 |
+
rel_pos_embeds = rel_pos_embeds.permute(2, 0, 1).unsqueeze(
|
| 242 |
+
0) # [1, N, Lq, Lk]
|
| 243 |
+
return rel_pos_embeds.contiguous()
|
| 244 |
+
|
| 245 |
+
def _relative_position_bucket(self, rel_pos):
|
| 246 |
+
# preprocess
|
| 247 |
+
if self.bidirectional:
|
| 248 |
+
num_buckets = self.num_buckets // 2
|
| 249 |
+
rel_buckets = (rel_pos > 0).long() * num_buckets
|
| 250 |
+
rel_pos = torch.abs(rel_pos)
|
| 251 |
+
else:
|
| 252 |
+
num_buckets = self.num_buckets
|
| 253 |
+
rel_buckets = 0
|
| 254 |
+
rel_pos = -torch.min(rel_pos, torch.zeros_like(rel_pos))
|
| 255 |
+
|
| 256 |
+
# embeddings for small and large positions
|
| 257 |
+
max_exact = num_buckets // 2
|
| 258 |
+
rel_pos_large = max_exact + (torch.log(rel_pos.float() / max_exact) /
|
| 259 |
+
math.log(self.max_dist / max_exact) *
|
| 260 |
+
(num_buckets - max_exact)).long()
|
| 261 |
+
rel_pos_large = torch.min(
|
| 262 |
+
rel_pos_large, torch.full_like(rel_pos_large, num_buckets - 1))
|
| 263 |
+
rel_buckets += torch.where(rel_pos < max_exact, rel_pos, rel_pos_large)
|
| 264 |
+
return rel_buckets
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class T5Encoder(nn.Module):
|
| 268 |
+
|
| 269 |
+
def __init__(self,
|
| 270 |
+
vocab,
|
| 271 |
+
dim,
|
| 272 |
+
dim_attn,
|
| 273 |
+
dim_ffn,
|
| 274 |
+
num_heads,
|
| 275 |
+
num_layers,
|
| 276 |
+
num_buckets,
|
| 277 |
+
shared_pos=True,
|
| 278 |
+
dropout=0.1):
|
| 279 |
+
super(T5Encoder, self).__init__()
|
| 280 |
+
self.dim = dim
|
| 281 |
+
self.dim_attn = dim_attn
|
| 282 |
+
self.dim_ffn = dim_ffn
|
| 283 |
+
self.num_heads = num_heads
|
| 284 |
+
self.num_layers = num_layers
|
| 285 |
+
self.num_buckets = num_buckets
|
| 286 |
+
self.shared_pos = shared_pos
|
| 287 |
+
|
| 288 |
+
# layers
|
| 289 |
+
self.token_embedding = vocab if isinstance(vocab, nn.Embedding) \
|
| 290 |
+
else nn.Embedding(vocab, dim)
|
| 291 |
+
self.pos_embedding = T5RelativeEmbedding(
|
| 292 |
+
num_buckets, num_heads, bidirectional=True) if shared_pos else None
|
| 293 |
+
self.dropout = nn.Dropout(dropout)
|
| 294 |
+
self.blocks = nn.ModuleList([
|
| 295 |
+
T5SelfAttention(dim, dim_attn, dim_ffn, num_heads, num_buckets,
|
| 296 |
+
shared_pos, dropout) for _ in range(num_layers)
|
| 297 |
+
])
|
| 298 |
+
self.norm = T5LayerNorm(dim)
|
| 299 |
+
|
| 300 |
+
# initialize weights
|
| 301 |
+
self.apply(init_weights)
|
| 302 |
+
|
| 303 |
+
def forward(self, ids, mask=None):
|
| 304 |
+
x = self.token_embedding(ids)
|
| 305 |
+
x = self.dropout(x)
|
| 306 |
+
e = self.pos_embedding(x.size(1),
|
| 307 |
+
x.size(1)) if self.shared_pos else None
|
| 308 |
+
for block in self.blocks:
|
| 309 |
+
x = block(x, mask, pos_bias=e)
|
| 310 |
+
x = self.norm(x)
|
| 311 |
+
x = self.dropout(x)
|
| 312 |
+
return x
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
class T5Decoder(nn.Module):
|
| 316 |
+
|
| 317 |
+
def __init__(self,
|
| 318 |
+
vocab,
|
| 319 |
+
dim,
|
| 320 |
+
dim_attn,
|
| 321 |
+
dim_ffn,
|
| 322 |
+
num_heads,
|
| 323 |
+
num_layers,
|
| 324 |
+
num_buckets,
|
| 325 |
+
shared_pos=True,
|
| 326 |
+
dropout=0.1):
|
| 327 |
+
super(T5Decoder, self).__init__()
|
| 328 |
+
self.dim = dim
|
| 329 |
+
self.dim_attn = dim_attn
|
| 330 |
+
self.dim_ffn = dim_ffn
|
| 331 |
+
self.num_heads = num_heads
|
| 332 |
+
self.num_layers = num_layers
|
| 333 |
+
self.num_buckets = num_buckets
|
| 334 |
+
self.shared_pos = shared_pos
|
| 335 |
+
|
| 336 |
+
# layers
|
| 337 |
+
self.token_embedding = vocab if isinstance(vocab, nn.Embedding) \
|
| 338 |
+
else nn.Embedding(vocab, dim)
|
| 339 |
+
self.pos_embedding = T5RelativeEmbedding(
|
| 340 |
+
num_buckets, num_heads, bidirectional=False) if shared_pos else None
|
| 341 |
+
self.dropout = nn.Dropout(dropout)
|
| 342 |
+
self.blocks = nn.ModuleList([
|
| 343 |
+
T5CrossAttention(dim, dim_attn, dim_ffn, num_heads, num_buckets,
|
| 344 |
+
shared_pos, dropout) for _ in range(num_layers)
|
| 345 |
+
])
|
| 346 |
+
self.norm = T5LayerNorm(dim)
|
| 347 |
+
|
| 348 |
+
# initialize weights
|
| 349 |
+
self.apply(init_weights)
|
| 350 |
+
|
| 351 |
+
def forward(self, ids, mask=None, encoder_states=None, encoder_mask=None):
|
| 352 |
+
b, s = ids.size()
|
| 353 |
+
|
| 354 |
+
# causal mask
|
| 355 |
+
if mask is None:
|
| 356 |
+
mask = torch.tril(torch.ones(1, s, s).to(ids.device))
|
| 357 |
+
elif mask.ndim == 2:
|
| 358 |
+
mask = torch.tril(mask.unsqueeze(1).expand(-1, s, -1))
|
| 359 |
+
|
| 360 |
+
# layers
|
| 361 |
+
x = self.token_embedding(ids)
|
| 362 |
+
x = self.dropout(x)
|
| 363 |
+
e = self.pos_embedding(x.size(1),
|
| 364 |
+
x.size(1)) if self.shared_pos else None
|
| 365 |
+
for block in self.blocks:
|
| 366 |
+
x = block(x, mask, encoder_states, encoder_mask, pos_bias=e)
|
| 367 |
+
x = self.norm(x)
|
| 368 |
+
x = self.dropout(x)
|
| 369 |
+
return x
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
class T5Model(nn.Module):
|
| 373 |
+
|
| 374 |
+
def __init__(self,
|
| 375 |
+
vocab_size,
|
| 376 |
+
dim,
|
| 377 |
+
dim_attn,
|
| 378 |
+
dim_ffn,
|
| 379 |
+
num_heads,
|
| 380 |
+
encoder_layers,
|
| 381 |
+
decoder_layers,
|
| 382 |
+
num_buckets,
|
| 383 |
+
shared_pos=True,
|
| 384 |
+
dropout=0.1):
|
| 385 |
+
super(T5Model, self).__init__()
|
| 386 |
+
self.vocab_size = vocab_size
|
| 387 |
+
self.dim = dim
|
| 388 |
+
self.dim_attn = dim_attn
|
| 389 |
+
self.dim_ffn = dim_ffn
|
| 390 |
+
self.num_heads = num_heads
|
| 391 |
+
self.encoder_layers = encoder_layers
|
| 392 |
+
self.decoder_layers = decoder_layers
|
| 393 |
+
self.num_buckets = num_buckets
|
| 394 |
+
|
| 395 |
+
# layers
|
| 396 |
+
self.token_embedding = nn.Embedding(vocab_size, dim)
|
| 397 |
+
self.encoder = T5Encoder(self.token_embedding, dim, dim_attn, dim_ffn,
|
| 398 |
+
num_heads, encoder_layers, num_buckets,
|
| 399 |
+
shared_pos, dropout)
|
| 400 |
+
self.decoder = T5Decoder(self.token_embedding, dim, dim_attn, dim_ffn,
|
| 401 |
+
num_heads, decoder_layers, num_buckets,
|
| 402 |
+
shared_pos, dropout)
|
| 403 |
+
self.head = nn.Linear(dim, vocab_size, bias=False)
|
| 404 |
+
|
| 405 |
+
# initialize weights
|
| 406 |
+
self.apply(init_weights)
|
| 407 |
+
|
| 408 |
+
def forward(self, encoder_ids, encoder_mask, decoder_ids, decoder_mask):
|
| 409 |
+
x = self.encoder(encoder_ids, encoder_mask)
|
| 410 |
+
x = self.decoder(decoder_ids, decoder_mask, x, encoder_mask)
|
| 411 |
+
x = self.head(x)
|
| 412 |
+
return x
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def _t5(name,
|
| 416 |
+
encoder_only=False,
|
| 417 |
+
decoder_only=False,
|
| 418 |
+
return_tokenizer=False,
|
| 419 |
+
tokenizer_kwargs={},
|
| 420 |
+
dtype=torch.float32,
|
| 421 |
+
device='cpu',
|
| 422 |
+
**kwargs):
|
| 423 |
+
# sanity check
|
| 424 |
+
assert not (encoder_only and decoder_only)
|
| 425 |
+
|
| 426 |
+
# params
|
| 427 |
+
if encoder_only:
|
| 428 |
+
model_cls = T5Encoder
|
| 429 |
+
kwargs['vocab'] = kwargs.pop('vocab_size')
|
| 430 |
+
kwargs['num_layers'] = kwargs.pop('encoder_layers')
|
| 431 |
+
_ = kwargs.pop('decoder_layers')
|
| 432 |
+
elif decoder_only:
|
| 433 |
+
model_cls = T5Decoder
|
| 434 |
+
kwargs['vocab'] = kwargs.pop('vocab_size')
|
| 435 |
+
kwargs['num_layers'] = kwargs.pop('decoder_layers')
|
| 436 |
+
_ = kwargs.pop('encoder_layers')
|
| 437 |
+
else:
|
| 438 |
+
model_cls = T5Model
|
| 439 |
+
|
| 440 |
+
# init model
|
| 441 |
+
with torch.device(device):
|
| 442 |
+
model = model_cls(**kwargs)
|
| 443 |
+
|
| 444 |
+
# set device
|
| 445 |
+
model = model.to(dtype=dtype, device=device)
|
| 446 |
+
|
| 447 |
+
# init tokenizer
|
| 448 |
+
if return_tokenizer:
|
| 449 |
+
from .tokenizers import HuggingfaceTokenizer
|
| 450 |
+
tokenizer = HuggingfaceTokenizer(f'google/{name}', **tokenizer_kwargs)
|
| 451 |
+
return model, tokenizer
|
| 452 |
+
else:
|
| 453 |
+
return model
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def umt5_xxl(**kwargs):
|
| 457 |
+
cfg = dict(
|
| 458 |
+
vocab_size=256384,
|
| 459 |
+
dim=4096,
|
| 460 |
+
dim_attn=4096,
|
| 461 |
+
dim_ffn=10240,
|
| 462 |
+
num_heads=64,
|
| 463 |
+
encoder_layers=24,
|
| 464 |
+
decoder_layers=24,
|
| 465 |
+
num_buckets=32,
|
| 466 |
+
shared_pos=False,
|
| 467 |
+
dropout=0.1)
|
| 468 |
+
cfg.update(**kwargs)
|
| 469 |
+
return _t5('umt5-xxl', **cfg)
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
class T5EncoderModel:
|
| 473 |
+
|
| 474 |
+
def __init__(
|
| 475 |
+
self,
|
| 476 |
+
text_len,
|
| 477 |
+
dtype=torch.bfloat16,
|
| 478 |
+
device=None,
|
| 479 |
+
checkpoint_path=None,
|
| 480 |
+
tokenizer_path=None,
|
| 481 |
+
shard_fn=None,
|
| 482 |
+
):
|
| 483 |
+
self.text_len = text_len
|
| 484 |
+
self.dtype = dtype
|
| 485 |
+
if device is None:
|
| 486 |
+
device = torch.cuda.current_device()
|
| 487 |
+
self.device = device
|
| 488 |
+
self.checkpoint_path = checkpoint_path
|
| 489 |
+
self.tokenizer_path = tokenizer_path
|
| 490 |
+
|
| 491 |
+
# init model
|
| 492 |
+
model = umt5_xxl(
|
| 493 |
+
encoder_only=True,
|
| 494 |
+
return_tokenizer=False,
|
| 495 |
+
dtype=dtype,
|
| 496 |
+
device=device).eval().requires_grad_(False)
|
| 497 |
+
logging.info(f'loading {checkpoint_path}')
|
| 498 |
+
model.load_state_dict(torch.load(checkpoint_path, map_location='cpu'))
|
| 499 |
+
self.model = model
|
| 500 |
+
if shard_fn is not None:
|
| 501 |
+
self.model = shard_fn(self.model, sync_module_states=False)
|
| 502 |
+
else:
|
| 503 |
+
self.model.to(self.device)
|
| 504 |
+
# init tokenizer
|
| 505 |
+
self.tokenizer = HuggingfaceTokenizer(
|
| 506 |
+
name=tokenizer_path, seq_len=text_len, clean='whitespace')
|
| 507 |
+
|
| 508 |
+
def __call__(self, texts, device):
|
| 509 |
+
ids, mask = self.tokenizer(
|
| 510 |
+
texts, return_mask=True, add_special_tokens=True)
|
| 511 |
+
ids = ids.to(device)
|
| 512 |
+
mask = mask.to(device)
|
| 513 |
+
seq_lens = mask.gt(0).sum(dim=1).long()
|
| 514 |
+
context = self.model(ids, mask)
|
| 515 |
+
return [u[:v] for u, v in zip(context, seq_lens)]
|
models/wan/wan_base/modules/tokenizers.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 2 |
+
import html
|
| 3 |
+
import string
|
| 4 |
+
|
| 5 |
+
import ftfy
|
| 6 |
+
import regex as re
|
| 7 |
+
from transformers import AutoTokenizer
|
| 8 |
+
|
| 9 |
+
__all__ = ['HuggingfaceTokenizer']
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def basic_clean(text):
|
| 13 |
+
text = ftfy.fix_text(text)
|
| 14 |
+
text = html.unescape(html.unescape(text))
|
| 15 |
+
return text.strip()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def whitespace_clean(text):
|
| 19 |
+
text = re.sub(r'\s+', ' ', text)
|
| 20 |
+
text = text.strip()
|
| 21 |
+
return text
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def canonicalize(text, keep_punctuation_exact_string=None):
|
| 25 |
+
text = text.replace('_', ' ')
|
| 26 |
+
if keep_punctuation_exact_string:
|
| 27 |
+
text = keep_punctuation_exact_string.join(
|
| 28 |
+
part.translate(str.maketrans('', '', string.punctuation))
|
| 29 |
+
for part in text.split(keep_punctuation_exact_string))
|
| 30 |
+
else:
|
| 31 |
+
text = text.translate(str.maketrans('', '', string.punctuation))
|
| 32 |
+
text = text.lower()
|
| 33 |
+
text = re.sub(r'\s+', ' ', text)
|
| 34 |
+
return text.strip()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class HuggingfaceTokenizer:
|
| 38 |
+
|
| 39 |
+
def __init__(self, name, seq_len=None, clean=None, **kwargs):
|
| 40 |
+
assert clean in (None, 'whitespace', 'lower', 'canonicalize')
|
| 41 |
+
self.name = name
|
| 42 |
+
self.seq_len = seq_len
|
| 43 |
+
self.clean = clean
|
| 44 |
+
|
| 45 |
+
# init tokenizer
|
| 46 |
+
self.tokenizer = AutoTokenizer.from_pretrained(name, **kwargs)
|
| 47 |
+
self.vocab_size = self.tokenizer.vocab_size
|
| 48 |
+
|
| 49 |
+
def __call__(self, sequence, **kwargs):
|
| 50 |
+
return_mask = kwargs.pop('return_mask', False)
|
| 51 |
+
|
| 52 |
+
# arguments
|
| 53 |
+
_kwargs = {'return_tensors': 'pt'}
|
| 54 |
+
if self.seq_len is not None:
|
| 55 |
+
_kwargs.update({
|
| 56 |
+
'padding': 'max_length',
|
| 57 |
+
'truncation': True,
|
| 58 |
+
'max_length': self.seq_len
|
| 59 |
+
})
|
| 60 |
+
_kwargs.update(**kwargs)
|
| 61 |
+
|
| 62 |
+
# tokenization
|
| 63 |
+
if isinstance(sequence, str):
|
| 64 |
+
sequence = [sequence]
|
| 65 |
+
if self.clean:
|
| 66 |
+
sequence = [self._clean(u) for u in sequence]
|
| 67 |
+
ids = self.tokenizer(sequence, **_kwargs)
|
| 68 |
+
|
| 69 |
+
# output
|
| 70 |
+
if return_mask:
|
| 71 |
+
return ids.input_ids, ids.attention_mask
|
| 72 |
+
else:
|
| 73 |
+
return ids.input_ids
|
| 74 |
+
|
| 75 |
+
def _clean(self, text):
|
| 76 |
+
if self.clean == 'whitespace':
|
| 77 |
+
text = whitespace_clean(basic_clean(text))
|
| 78 |
+
elif self.clean == 'lower':
|
| 79 |
+
text = whitespace_clean(basic_clean(text)).lower()
|
| 80 |
+
elif self.clean == 'canonicalize':
|
| 81 |
+
text = canonicalize(basic_clean(text))
|
| 82 |
+
return text
|
models/wan/wan_base/modules/vae.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.cuda.amp as amp
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from einops import rearrange
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
'WanVAE',
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
CACHE_T = 2
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class CausalConv3d(nn.Conv3d):
|
| 18 |
+
"""
|
| 19 |
+
Causal 3d convolusion.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, *args, **kwargs):
|
| 23 |
+
super().__init__(*args, **kwargs)
|
| 24 |
+
self._padding = (self.padding[2], self.padding[2], self.padding[1],
|
| 25 |
+
self.padding[1], 2 * self.padding[0], 0)
|
| 26 |
+
self.padding = (0, 0, 0)
|
| 27 |
+
|
| 28 |
+
def forward(self, x, cache_x=None):
|
| 29 |
+
padding = list(self._padding)
|
| 30 |
+
if cache_x is not None and self._padding[4] > 0:
|
| 31 |
+
cache_x = cache_x.to(x.device)
|
| 32 |
+
x = torch.cat([cache_x, x], dim=2)
|
| 33 |
+
padding[4] -= cache_x.shape[2]
|
| 34 |
+
x = F.pad(x, padding)
|
| 35 |
+
|
| 36 |
+
return super().forward(x)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class RMS_norm(nn.Module):
|
| 40 |
+
|
| 41 |
+
def __init__(self, dim, channel_first=True, images=True, bias=False):
|
| 42 |
+
super().__init__()
|
| 43 |
+
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
|
| 44 |
+
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
|
| 45 |
+
|
| 46 |
+
self.channel_first = channel_first
|
| 47 |
+
self.scale = dim**0.5
|
| 48 |
+
self.gamma = nn.Parameter(torch.ones(shape))
|
| 49 |
+
self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.
|
| 50 |
+
|
| 51 |
+
def forward(self, x):
|
| 52 |
+
return F.normalize(
|
| 53 |
+
x, dim=(1 if self.channel_first else
|
| 54 |
+
-1)) * self.scale * self.gamma + self.bias
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class Upsample(nn.Upsample):
|
| 58 |
+
|
| 59 |
+
def forward(self, x):
|
| 60 |
+
"""
|
| 61 |
+
Fix bfloat16 support for nearest neighbor interpolation.
|
| 62 |
+
"""
|
| 63 |
+
return super().forward(x.float()).type_as(x)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class Resample(nn.Module):
|
| 67 |
+
|
| 68 |
+
def __init__(self, dim, mode):
|
| 69 |
+
assert mode in ('none', 'upsample2d', 'upsample3d', 'downsample2d',
|
| 70 |
+
'downsample3d')
|
| 71 |
+
super().__init__()
|
| 72 |
+
self.dim = dim
|
| 73 |
+
self.mode = mode
|
| 74 |
+
|
| 75 |
+
# layers
|
| 76 |
+
if mode == 'upsample2d':
|
| 77 |
+
self.resample = nn.Sequential(
|
| 78 |
+
Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
|
| 79 |
+
nn.Conv2d(dim, dim // 2, 3, padding=1))
|
| 80 |
+
elif mode == 'upsample3d':
|
| 81 |
+
self.resample = nn.Sequential(
|
| 82 |
+
Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
|
| 83 |
+
nn.Conv2d(dim, dim // 2, 3, padding=1))
|
| 84 |
+
self.time_conv = CausalConv3d(
|
| 85 |
+
dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
|
| 86 |
+
|
| 87 |
+
elif mode == 'downsample2d':
|
| 88 |
+
self.resample = nn.Sequential(
|
| 89 |
+
nn.ZeroPad2d((0, 1, 0, 1)),
|
| 90 |
+
nn.Conv2d(dim, dim, 3, stride=(2, 2)))
|
| 91 |
+
elif mode == 'downsample3d':
|
| 92 |
+
self.resample = nn.Sequential(
|
| 93 |
+
nn.ZeroPad2d((0, 1, 0, 1)),
|
| 94 |
+
nn.Conv2d(dim, dim, 3, stride=(2, 2)))
|
| 95 |
+
self.time_conv = CausalConv3d(
|
| 96 |
+
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
|
| 97 |
+
|
| 98 |
+
else:
|
| 99 |
+
self.resample = nn.Identity()
|
| 100 |
+
|
| 101 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 102 |
+
b, c, t, h, w = x.size()
|
| 103 |
+
if self.mode == 'upsample3d':
|
| 104 |
+
if feat_cache is not None:
|
| 105 |
+
idx = feat_idx[0]
|
| 106 |
+
if feat_cache[idx] is None:
|
| 107 |
+
feat_cache[idx] = 'Rep'
|
| 108 |
+
feat_idx[0] += 1
|
| 109 |
+
else:
|
| 110 |
+
|
| 111 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 112 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[
|
| 113 |
+
idx] is not None and feat_cache[idx] != 'Rep':
|
| 114 |
+
# cache last frame of last two chunk
|
| 115 |
+
cache_x = torch.cat([
|
| 116 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 117 |
+
cache_x.device), cache_x
|
| 118 |
+
],
|
| 119 |
+
dim=2)
|
| 120 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[
|
| 121 |
+
idx] is not None and feat_cache[idx] == 'Rep':
|
| 122 |
+
cache_x = torch.cat([
|
| 123 |
+
torch.zeros_like(cache_x).to(cache_x.device),
|
| 124 |
+
cache_x
|
| 125 |
+
],
|
| 126 |
+
dim=2)
|
| 127 |
+
if feat_cache[idx] == 'Rep':
|
| 128 |
+
x = self.time_conv(x)
|
| 129 |
+
else:
|
| 130 |
+
x = self.time_conv(x, feat_cache[idx])
|
| 131 |
+
feat_cache[idx] = cache_x
|
| 132 |
+
feat_idx[0] += 1
|
| 133 |
+
|
| 134 |
+
x = x.reshape(b, 2, c, t, h, w)
|
| 135 |
+
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
|
| 136 |
+
3)
|
| 137 |
+
x = x.reshape(b, c, t * 2, h, w)
|
| 138 |
+
t = x.shape[2]
|
| 139 |
+
x = rearrange(x, 'b c t h w -> (b t) c h w')
|
| 140 |
+
x = self.resample(x)
|
| 141 |
+
x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
|
| 142 |
+
|
| 143 |
+
if self.mode == 'downsample3d':
|
| 144 |
+
if feat_cache is not None:
|
| 145 |
+
idx = feat_idx[0]
|
| 146 |
+
if feat_cache[idx] is None:
|
| 147 |
+
feat_cache[idx] = x.clone()
|
| 148 |
+
feat_idx[0] += 1
|
| 149 |
+
else:
|
| 150 |
+
|
| 151 |
+
cache_x = x[:, :, -1:, :, :].clone()
|
| 152 |
+
# if cache_x.shape[2] < CACHE_T and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
|
| 153 |
+
# # cache last frame of last two chunk
|
| 154 |
+
# cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
|
| 155 |
+
|
| 156 |
+
x = self.time_conv(
|
| 157 |
+
torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
|
| 158 |
+
feat_cache[idx] = cache_x
|
| 159 |
+
feat_idx[0] += 1
|
| 160 |
+
return x
|
| 161 |
+
|
| 162 |
+
def init_weight(self, conv):
|
| 163 |
+
conv_weight = conv.weight
|
| 164 |
+
nn.init.zeros_(conv_weight)
|
| 165 |
+
c1, c2, t, h, w = conv_weight.size()
|
| 166 |
+
one_matrix = torch.eye(c1, c2)
|
| 167 |
+
init_matrix = one_matrix
|
| 168 |
+
nn.init.zeros_(conv_weight)
|
| 169 |
+
# conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
|
| 170 |
+
conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
|
| 171 |
+
conv.weight.data.copy_(conv_weight)
|
| 172 |
+
nn.init.zeros_(conv.bias.data)
|
| 173 |
+
|
| 174 |
+
def init_weight2(self, conv):
|
| 175 |
+
conv_weight = conv.weight.data
|
| 176 |
+
nn.init.zeros_(conv_weight)
|
| 177 |
+
c1, c2, t, h, w = conv_weight.size()
|
| 178 |
+
init_matrix = torch.eye(c1 // 2, c2)
|
| 179 |
+
# init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
|
| 180 |
+
conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
|
| 181 |
+
conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
|
| 182 |
+
conv.weight.data.copy_(conv_weight)
|
| 183 |
+
nn.init.zeros_(conv.bias.data)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class ResidualBlock(nn.Module):
|
| 187 |
+
|
| 188 |
+
def __init__(self, in_dim, out_dim, dropout=0.0):
|
| 189 |
+
super().__init__()
|
| 190 |
+
self.in_dim = in_dim
|
| 191 |
+
self.out_dim = out_dim
|
| 192 |
+
|
| 193 |
+
# layers
|
| 194 |
+
self.residual = nn.Sequential(
|
| 195 |
+
RMS_norm(in_dim, images=False), nn.SiLU(),
|
| 196 |
+
CausalConv3d(in_dim, out_dim, 3, padding=1),
|
| 197 |
+
RMS_norm(out_dim, images=False), nn.SiLU(), nn.Dropout(dropout),
|
| 198 |
+
CausalConv3d(out_dim, out_dim, 3, padding=1))
|
| 199 |
+
self.shortcut = CausalConv3d(in_dim, out_dim, 1) \
|
| 200 |
+
if in_dim != out_dim else nn.Identity()
|
| 201 |
+
|
| 202 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 203 |
+
h = self.shortcut(x)
|
| 204 |
+
for layer in self.residual:
|
| 205 |
+
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
| 206 |
+
idx = feat_idx[0]
|
| 207 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 208 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[idx] is not None:
|
| 209 |
+
# cache last frame of last two chunk
|
| 210 |
+
cache_x = torch.cat([
|
| 211 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 212 |
+
cache_x.device), cache_x
|
| 213 |
+
],
|
| 214 |
+
dim=2)
|
| 215 |
+
x = layer(x, feat_cache[idx])
|
| 216 |
+
feat_cache[idx] = cache_x
|
| 217 |
+
feat_idx[0] += 1
|
| 218 |
+
else:
|
| 219 |
+
x = layer(x)
|
| 220 |
+
return x + h
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class AttentionBlock(nn.Module):
|
| 224 |
+
"""
|
| 225 |
+
Causal self-attention with a single head.
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
def __init__(self, dim):
|
| 229 |
+
super().__init__()
|
| 230 |
+
self.dim = dim
|
| 231 |
+
|
| 232 |
+
# layers
|
| 233 |
+
self.norm = RMS_norm(dim)
|
| 234 |
+
self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
|
| 235 |
+
self.proj = nn.Conv2d(dim, dim, 1)
|
| 236 |
+
|
| 237 |
+
# zero out the last layer params
|
| 238 |
+
nn.init.zeros_(self.proj.weight)
|
| 239 |
+
|
| 240 |
+
def forward(self, x):
|
| 241 |
+
identity = x
|
| 242 |
+
b, c, t, h, w = x.size()
|
| 243 |
+
x = rearrange(x, 'b c t h w -> (b t) c h w')
|
| 244 |
+
x = self.norm(x)
|
| 245 |
+
# compute query, key, value
|
| 246 |
+
q, k, v = self.to_qkv(x).reshape(b * t, 1, c * 3,
|
| 247 |
+
-1).permute(0, 1, 3,
|
| 248 |
+
2).contiguous().chunk(
|
| 249 |
+
3, dim=-1)
|
| 250 |
+
|
| 251 |
+
# apply attention
|
| 252 |
+
x = F.scaled_dot_product_attention(
|
| 253 |
+
q,
|
| 254 |
+
k,
|
| 255 |
+
v,
|
| 256 |
+
)
|
| 257 |
+
x = x.squeeze(1).permute(0, 2, 1).reshape(b * t, c, h, w)
|
| 258 |
+
|
| 259 |
+
# output
|
| 260 |
+
x = self.proj(x)
|
| 261 |
+
x = rearrange(x, '(b t) c h w-> b c t h w', t=t)
|
| 262 |
+
return x + identity
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class Encoder3d(nn.Module):
|
| 266 |
+
|
| 267 |
+
def __init__(self,
|
| 268 |
+
dim=128,
|
| 269 |
+
z_dim=4,
|
| 270 |
+
dim_mult=[1, 2, 4, 4],
|
| 271 |
+
num_res_blocks=2,
|
| 272 |
+
attn_scales=[],
|
| 273 |
+
temperal_downsample=[True, True, False],
|
| 274 |
+
dropout=0.0):
|
| 275 |
+
super().__init__()
|
| 276 |
+
self.dim = dim
|
| 277 |
+
self.z_dim = z_dim
|
| 278 |
+
self.dim_mult = dim_mult
|
| 279 |
+
self.num_res_blocks = num_res_blocks
|
| 280 |
+
self.attn_scales = attn_scales
|
| 281 |
+
self.temperal_downsample = temperal_downsample
|
| 282 |
+
|
| 283 |
+
# dimensions
|
| 284 |
+
dims = [dim * u for u in [1] + dim_mult]
|
| 285 |
+
scale = 1.0
|
| 286 |
+
|
| 287 |
+
# init block
|
| 288 |
+
self.conv1 = CausalConv3d(3, dims[0], 3, padding=1)
|
| 289 |
+
|
| 290 |
+
# downsample blocks
|
| 291 |
+
downsamples = []
|
| 292 |
+
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
|
| 293 |
+
# residual (+attention) blocks
|
| 294 |
+
for _ in range(num_res_blocks):
|
| 295 |
+
downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
| 296 |
+
if scale in attn_scales:
|
| 297 |
+
downsamples.append(AttentionBlock(out_dim))
|
| 298 |
+
in_dim = out_dim
|
| 299 |
+
|
| 300 |
+
# downsample block
|
| 301 |
+
if i != len(dim_mult) - 1:
|
| 302 |
+
mode = 'downsample3d' if temperal_downsample[
|
| 303 |
+
i] else 'downsample2d'
|
| 304 |
+
downsamples.append(Resample(out_dim, mode=mode))
|
| 305 |
+
scale /= 2.0
|
| 306 |
+
self.downsamples = nn.Sequential(*downsamples)
|
| 307 |
+
|
| 308 |
+
# middle blocks
|
| 309 |
+
self.middle = nn.Sequential(
|
| 310 |
+
ResidualBlock(out_dim, out_dim, dropout), AttentionBlock(out_dim),
|
| 311 |
+
ResidualBlock(out_dim, out_dim, dropout))
|
| 312 |
+
|
| 313 |
+
# output blocks
|
| 314 |
+
self.head = nn.Sequential(
|
| 315 |
+
RMS_norm(out_dim, images=False), nn.SiLU(),
|
| 316 |
+
CausalConv3d(out_dim, z_dim, 3, padding=1))
|
| 317 |
+
|
| 318 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 319 |
+
if feat_cache is not None:
|
| 320 |
+
idx = feat_idx[0]
|
| 321 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 322 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[idx] is not None:
|
| 323 |
+
# cache last frame of last two chunk
|
| 324 |
+
cache_x = torch.cat([
|
| 325 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 326 |
+
cache_x.device), cache_x
|
| 327 |
+
],
|
| 328 |
+
dim=2)
|
| 329 |
+
x = self.conv1(x, feat_cache[idx])
|
| 330 |
+
feat_cache[idx] = cache_x
|
| 331 |
+
feat_idx[0] += 1
|
| 332 |
+
else:
|
| 333 |
+
x = self.conv1(x)
|
| 334 |
+
|
| 335 |
+
# downsamples
|
| 336 |
+
for layer in self.downsamples:
|
| 337 |
+
if feat_cache is not None:
|
| 338 |
+
x = layer(x, feat_cache, feat_idx)
|
| 339 |
+
else:
|
| 340 |
+
x = layer(x)
|
| 341 |
+
|
| 342 |
+
# middle
|
| 343 |
+
for layer in self.middle:
|
| 344 |
+
if isinstance(layer, ResidualBlock) and feat_cache is not None:
|
| 345 |
+
x = layer(x, feat_cache, feat_idx)
|
| 346 |
+
else:
|
| 347 |
+
x = layer(x)
|
| 348 |
+
|
| 349 |
+
# head
|
| 350 |
+
for layer in self.head:
|
| 351 |
+
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
| 352 |
+
idx = feat_idx[0]
|
| 353 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 354 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[idx] is not None:
|
| 355 |
+
# cache last frame of last two chunk
|
| 356 |
+
cache_x = torch.cat([
|
| 357 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 358 |
+
cache_x.device), cache_x
|
| 359 |
+
],
|
| 360 |
+
dim=2)
|
| 361 |
+
x = layer(x, feat_cache[idx])
|
| 362 |
+
feat_cache[idx] = cache_x
|
| 363 |
+
feat_idx[0] += 1
|
| 364 |
+
else:
|
| 365 |
+
x = layer(x)
|
| 366 |
+
return x
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
class Decoder3d(nn.Module):
|
| 370 |
+
|
| 371 |
+
def __init__(self,
|
| 372 |
+
dim=128,
|
| 373 |
+
z_dim=4,
|
| 374 |
+
dim_mult=[1, 2, 4, 4],
|
| 375 |
+
num_res_blocks=2,
|
| 376 |
+
attn_scales=[],
|
| 377 |
+
temperal_upsample=[False, True, True],
|
| 378 |
+
dropout=0.0):
|
| 379 |
+
super().__init__()
|
| 380 |
+
self.dim = dim
|
| 381 |
+
self.z_dim = z_dim
|
| 382 |
+
self.dim_mult = dim_mult
|
| 383 |
+
self.num_res_blocks = num_res_blocks
|
| 384 |
+
self.attn_scales = attn_scales
|
| 385 |
+
self.temperal_upsample = temperal_upsample
|
| 386 |
+
|
| 387 |
+
# dimensions
|
| 388 |
+
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
|
| 389 |
+
scale = 1.0 / 2**(len(dim_mult) - 2)
|
| 390 |
+
|
| 391 |
+
# init block
|
| 392 |
+
self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
|
| 393 |
+
|
| 394 |
+
# middle blocks
|
| 395 |
+
self.middle = nn.Sequential(
|
| 396 |
+
ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
|
| 397 |
+
ResidualBlock(dims[0], dims[0], dropout))
|
| 398 |
+
|
| 399 |
+
# upsample blocks
|
| 400 |
+
upsamples = []
|
| 401 |
+
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
|
| 402 |
+
# residual (+attention) blocks
|
| 403 |
+
if i == 1 or i == 2 or i == 3:
|
| 404 |
+
in_dim = in_dim // 2
|
| 405 |
+
for _ in range(num_res_blocks + 1):
|
| 406 |
+
upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
| 407 |
+
if scale in attn_scales:
|
| 408 |
+
upsamples.append(AttentionBlock(out_dim))
|
| 409 |
+
in_dim = out_dim
|
| 410 |
+
|
| 411 |
+
# upsample block
|
| 412 |
+
if i != len(dim_mult) - 1:
|
| 413 |
+
mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
|
| 414 |
+
upsamples.append(Resample(out_dim, mode=mode))
|
| 415 |
+
scale *= 2.0
|
| 416 |
+
self.upsamples = nn.Sequential(*upsamples)
|
| 417 |
+
|
| 418 |
+
# output blocks
|
| 419 |
+
self.head = nn.Sequential(
|
| 420 |
+
RMS_norm(out_dim, images=False), nn.SiLU(),
|
| 421 |
+
CausalConv3d(out_dim, 3, 3, padding=1))
|
| 422 |
+
|
| 423 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 424 |
+
# conv1
|
| 425 |
+
if feat_cache is not None:
|
| 426 |
+
idx = feat_idx[0]
|
| 427 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 428 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[idx] is not None:
|
| 429 |
+
# cache last frame of last two chunk
|
| 430 |
+
cache_x = torch.cat([
|
| 431 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 432 |
+
cache_x.device), cache_x
|
| 433 |
+
],
|
| 434 |
+
dim=2)
|
| 435 |
+
x = self.conv1(x, feat_cache[idx])
|
| 436 |
+
feat_cache[idx] = cache_x
|
| 437 |
+
feat_idx[0] += 1
|
| 438 |
+
else:
|
| 439 |
+
x = self.conv1(x)
|
| 440 |
+
|
| 441 |
+
# middle
|
| 442 |
+
for layer in self.middle:
|
| 443 |
+
if isinstance(layer, ResidualBlock) and feat_cache is not None:
|
| 444 |
+
x = layer(x, feat_cache, feat_idx)
|
| 445 |
+
else:
|
| 446 |
+
x = layer(x)
|
| 447 |
+
|
| 448 |
+
# upsamples
|
| 449 |
+
for layer in self.upsamples:
|
| 450 |
+
if feat_cache is not None:
|
| 451 |
+
x = layer(x, feat_cache, feat_idx)
|
| 452 |
+
else:
|
| 453 |
+
x = layer(x)
|
| 454 |
+
|
| 455 |
+
# head
|
| 456 |
+
for layer in self.head:
|
| 457 |
+
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
| 458 |
+
idx = feat_idx[0]
|
| 459 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 460 |
+
if cache_x.shape[2] < CACHE_T and feat_cache[idx] is not None:
|
| 461 |
+
# cache last frame of last two chunk
|
| 462 |
+
cache_x = torch.cat([
|
| 463 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 464 |
+
cache_x.device), cache_x
|
| 465 |
+
],
|
| 466 |
+
dim=2)
|
| 467 |
+
x = layer(x, feat_cache[idx])
|
| 468 |
+
feat_cache[idx] = cache_x
|
| 469 |
+
feat_idx[0] += 1
|
| 470 |
+
else:
|
| 471 |
+
x = layer(x)
|
| 472 |
+
return x
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def count_conv3d(model):
|
| 476 |
+
count = 0
|
| 477 |
+
for m in model.modules():
|
| 478 |
+
if isinstance(m, CausalConv3d):
|
| 479 |
+
count += 1
|
| 480 |
+
return count
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
class WanVAE_(nn.Module):
|
| 484 |
+
|
| 485 |
+
def __init__(self,
|
| 486 |
+
dim=128,
|
| 487 |
+
z_dim=4,
|
| 488 |
+
dim_mult=[1, 2, 4, 4],
|
| 489 |
+
num_res_blocks=2,
|
| 490 |
+
attn_scales=[],
|
| 491 |
+
temperal_downsample=[True, True, False],
|
| 492 |
+
dropout=0.0):
|
| 493 |
+
super().__init__()
|
| 494 |
+
self.dim = dim
|
| 495 |
+
self.z_dim = z_dim
|
| 496 |
+
self.dim_mult = dim_mult
|
| 497 |
+
self.num_res_blocks = num_res_blocks
|
| 498 |
+
self.attn_scales = attn_scales
|
| 499 |
+
self.temperal_downsample = temperal_downsample
|
| 500 |
+
self.temperal_upsample = temperal_downsample[::-1]
|
| 501 |
+
|
| 502 |
+
# modules
|
| 503 |
+
self.encoder = Encoder3d(dim, z_dim * 2, dim_mult, num_res_blocks,
|
| 504 |
+
attn_scales, self.temperal_downsample, dropout)
|
| 505 |
+
self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
|
| 506 |
+
self.conv2 = CausalConv3d(z_dim, z_dim, 1)
|
| 507 |
+
self.decoder = Decoder3d(dim, z_dim, dim_mult, num_res_blocks,
|
| 508 |
+
attn_scales, self.temperal_upsample, dropout)
|
| 509 |
+
self.first_encode = True
|
| 510 |
+
self.first_decode = True
|
| 511 |
+
|
| 512 |
+
def forward(self, x):
|
| 513 |
+
mu, log_var = self.encode(x)
|
| 514 |
+
z = self.reparameterize(mu, log_var)
|
| 515 |
+
x_recon = self.decode(z)
|
| 516 |
+
return x_recon, mu, log_var
|
| 517 |
+
|
| 518 |
+
def encode(self, x, scale):
|
| 519 |
+
self.clear_cache()
|
| 520 |
+
# cache
|
| 521 |
+
t = x.shape[2]
|
| 522 |
+
iter_ = 1 + (t - 1) // 4
|
| 523 |
+
# 对encode输入的x,按时间拆分为1、4、4、4....
|
| 524 |
+
for i in range(iter_):
|
| 525 |
+
self._enc_conv_idx = [0]
|
| 526 |
+
if i == 0:
|
| 527 |
+
out = self.encoder(
|
| 528 |
+
x[:, :, :1, :, :],
|
| 529 |
+
feat_cache=self._enc_feat_map,
|
| 530 |
+
feat_idx=self._enc_conv_idx)
|
| 531 |
+
else:
|
| 532 |
+
out_ = self.encoder(
|
| 533 |
+
x[:, :, 1 + 4 * (i - 1):1 + 4 * i, :, :],
|
| 534 |
+
feat_cache=self._enc_feat_map,
|
| 535 |
+
feat_idx=self._enc_conv_idx)
|
| 536 |
+
out = torch.cat([out, out_], 2)
|
| 537 |
+
mu, log_var = self.conv1(out).chunk(2, dim=1)
|
| 538 |
+
if isinstance(scale[0], torch.Tensor):
|
| 539 |
+
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
|
| 540 |
+
1, self.z_dim, 1, 1, 1)
|
| 541 |
+
else:
|
| 542 |
+
mu = (mu - scale[0]) * scale[1]
|
| 543 |
+
self.clear_cache()
|
| 544 |
+
return mu
|
| 545 |
+
|
| 546 |
+
def stream_encode(self, x, scale):
|
| 547 |
+
# cache
|
| 548 |
+
t = x.shape[2]
|
| 549 |
+
if self.first_encode:
|
| 550 |
+
self.first_encode = False
|
| 551 |
+
self.clear_cache_encode()
|
| 552 |
+
self._enc_conv_idx = [0]
|
| 553 |
+
out = self.encoder(
|
| 554 |
+
x[:, :, :1, :, :],
|
| 555 |
+
feat_cache=self._enc_feat_map,
|
| 556 |
+
feat_idx=self._enc_conv_idx,
|
| 557 |
+
)
|
| 558 |
+
self._enc_conv_idx = [0]
|
| 559 |
+
out_ = self.encoder(
|
| 560 |
+
x[:, :, 1:, :, :],
|
| 561 |
+
feat_cache=self._enc_feat_map,
|
| 562 |
+
feat_idx=self._enc_conv_idx,
|
| 563 |
+
)
|
| 564 |
+
out = torch.cat([out, out_], 2)
|
| 565 |
+
else:
|
| 566 |
+
out=[]
|
| 567 |
+
for i in range(t//4):
|
| 568 |
+
self._enc_conv_idx = [0]
|
| 569 |
+
out.append(self.encoder(
|
| 570 |
+
x[:, :, i*4:(i+1)*4, :, :],
|
| 571 |
+
feat_cache=self._enc_feat_map,
|
| 572 |
+
feat_idx=self._enc_conv_idx,
|
| 573 |
+
))
|
| 574 |
+
out = torch.cat(out, 2)
|
| 575 |
+
mu, log_var = self.conv1(out).chunk(2, dim=1)
|
| 576 |
+
if scale is not None:
|
| 577 |
+
if isinstance(scale[0], torch.Tensor):
|
| 578 |
+
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
|
| 579 |
+
1, self.z_dim, 1, 1, 1)
|
| 580 |
+
else:
|
| 581 |
+
mu = (mu - scale[0]) * scale[1]
|
| 582 |
+
# self.clear_cache()
|
| 583 |
+
return mu
|
| 584 |
+
|
| 585 |
+
def decode(self, z, scale):
|
| 586 |
+
self.clear_cache()
|
| 587 |
+
# z: [b,c,t,h,w]
|
| 588 |
+
if isinstance(scale[0], torch.Tensor):
|
| 589 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 590 |
+
1, self.z_dim, 1, 1, 1)
|
| 591 |
+
else:
|
| 592 |
+
z = z / scale[1] + scale[0]
|
| 593 |
+
iter_ = z.shape[2]
|
| 594 |
+
x = self.conv2(z)
|
| 595 |
+
for i in range(iter_):
|
| 596 |
+
self._conv_idx = [0]
|
| 597 |
+
if i == 0:
|
| 598 |
+
out = self.decoder(
|
| 599 |
+
x[:, :, i:i + 1, :, :],
|
| 600 |
+
feat_cache=self._feat_map,
|
| 601 |
+
feat_idx=self._conv_idx)
|
| 602 |
+
else:
|
| 603 |
+
out_ = self.decoder(
|
| 604 |
+
x[:, :, i:i + 1, :, :],
|
| 605 |
+
feat_cache=self._feat_map,
|
| 606 |
+
feat_idx=self._conv_idx)
|
| 607 |
+
out = torch.cat([out, out_], 2)
|
| 608 |
+
self.clear_cache()
|
| 609 |
+
return out
|
| 610 |
+
|
| 611 |
+
def stream_decode(self, z, scale):
|
| 612 |
+
# z: [b,c,t,h,w]
|
| 613 |
+
t=z.shape[2]
|
| 614 |
+
if isinstance(scale[0], torch.Tensor):
|
| 615 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 616 |
+
1, self.z_dim, 1, 1, 1)
|
| 617 |
+
else:
|
| 618 |
+
z = z / scale[1] + scale[0]
|
| 619 |
+
x = self.conv2(z)
|
| 620 |
+
if self.first_decode:
|
| 621 |
+
self.first_decode = False
|
| 622 |
+
self.clear_cache_decode()
|
| 623 |
+
self.first_batch = False
|
| 624 |
+
self._conv_idx = [0]
|
| 625 |
+
out = self.decoder(
|
| 626 |
+
x[:, :, :1, :, :],
|
| 627 |
+
feat_cache=self._feat_map,
|
| 628 |
+
feat_idx=self._conv_idx,
|
| 629 |
+
)
|
| 630 |
+
self._conv_idx = [0]
|
| 631 |
+
out_ = self.decoder(
|
| 632 |
+
x[:, :, 1:, :, :],
|
| 633 |
+
feat_cache=self._feat_map,
|
| 634 |
+
feat_idx=self._conv_idx,
|
| 635 |
+
)
|
| 636 |
+
out = torch.cat([out, out_], 2)
|
| 637 |
+
else:
|
| 638 |
+
out = []
|
| 639 |
+
for i in range(t):
|
| 640 |
+
self._conv_idx = [0]
|
| 641 |
+
out.append(self.decoder(
|
| 642 |
+
x[:, :, i:(i+1), :, :],
|
| 643 |
+
feat_cache=self._feat_map,
|
| 644 |
+
feat_idx=self._conv_idx,
|
| 645 |
+
))
|
| 646 |
+
out = torch.cat(out, 2)
|
| 647 |
+
# self.clear_cache()
|
| 648 |
+
return out
|
| 649 |
+
|
| 650 |
+
def reparameterize(self, mu, log_var):
|
| 651 |
+
std = torch.exp(0.5 * log_var)
|
| 652 |
+
eps = torch.randn_like(std)
|
| 653 |
+
return eps * std + mu
|
| 654 |
+
|
| 655 |
+
def sample(self, imgs, deterministic=False):
|
| 656 |
+
mu, log_var = self.encode(imgs)
|
| 657 |
+
if deterministic:
|
| 658 |
+
return mu
|
| 659 |
+
std = torch.exp(0.5 * log_var.clamp(-30.0, 20.0))
|
| 660 |
+
return mu + std * torch.randn_like(std)
|
| 661 |
+
|
| 662 |
+
def clear_cache(self):
|
| 663 |
+
self._conv_num = count_conv3d(self.decoder)
|
| 664 |
+
self._conv_idx = [0]
|
| 665 |
+
self._feat_map = [None] * self._conv_num
|
| 666 |
+
# cache encode
|
| 667 |
+
self._enc_conv_num = count_conv3d(self.encoder)
|
| 668 |
+
self._enc_conv_idx = [0]
|
| 669 |
+
self._enc_feat_map = [None] * self._enc_conv_num
|
| 670 |
+
|
| 671 |
+
def clear_cache_decode(self):
|
| 672 |
+
self._conv_num = count_conv3d(self.decoder)
|
| 673 |
+
self._conv_idx = [0]
|
| 674 |
+
self._feat_map = [None] * self._conv_num
|
| 675 |
+
|
| 676 |
+
def clear_cache_encode(self):
|
| 677 |
+
self._enc_conv_num = count_conv3d(self.encoder)
|
| 678 |
+
self._enc_conv_idx = [0]
|
| 679 |
+
self._enc_feat_map = [None] * self._enc_conv_num
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def _video_vae(pretrained_path=None, z_dim=None, device='cpu', **kwargs):
|
| 684 |
+
"""
|
| 685 |
+
Autoencoder3d adapted from Stable Diffusion 1.x, 2.x and XL.
|
| 686 |
+
"""
|
| 687 |
+
# params
|
| 688 |
+
cfg = dict(
|
| 689 |
+
dim=96,
|
| 690 |
+
z_dim=z_dim,
|
| 691 |
+
dim_mult=[1, 2, 4, 4],
|
| 692 |
+
num_res_blocks=2,
|
| 693 |
+
attn_scales=[],
|
| 694 |
+
temperal_downsample=[False, True, True],
|
| 695 |
+
dropout=0.0)
|
| 696 |
+
cfg.update(**kwargs)
|
| 697 |
+
|
| 698 |
+
# init model
|
| 699 |
+
with torch.device('meta'):
|
| 700 |
+
model = WanVAE_(**cfg)
|
| 701 |
+
|
| 702 |
+
# load checkpoint
|
| 703 |
+
logging.info(f'loading {pretrained_path}')
|
| 704 |
+
model.load_state_dict(
|
| 705 |
+
torch.load(pretrained_path, map_location=device), assign=True)
|
| 706 |
+
|
| 707 |
+
return model
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
class WanVAE:
|
| 711 |
+
|
| 712 |
+
def __init__(self,
|
| 713 |
+
z_dim=16,
|
| 714 |
+
vae_pth='cache/vae_step_411000.pth',
|
| 715 |
+
dtype=torch.float,
|
| 716 |
+
device="cuda"):
|
| 717 |
+
self.dtype = dtype
|
| 718 |
+
self.device = device
|
| 719 |
+
|
| 720 |
+
mean = [
|
| 721 |
+
-0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
|
| 722 |
+
0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
|
| 723 |
+
]
|
| 724 |
+
std = [
|
| 725 |
+
2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
|
| 726 |
+
3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
|
| 727 |
+
]
|
| 728 |
+
self.mean = torch.tensor(mean, dtype=dtype, device=device)
|
| 729 |
+
self.std = torch.tensor(std, dtype=dtype, device=device)
|
| 730 |
+
self.scale = [self.mean, 1.0 / self.std]
|
| 731 |
+
|
| 732 |
+
# init model
|
| 733 |
+
self.model = _video_vae(
|
| 734 |
+
pretrained_path=vae_pth,
|
| 735 |
+
z_dim=z_dim,
|
| 736 |
+
).eval().requires_grad_(False).to(device)
|
| 737 |
+
|
| 738 |
+
def encode(self, videos):
|
| 739 |
+
"""
|
| 740 |
+
videos: A list of videos each with shape [C, T, H, W].
|
| 741 |
+
"""
|
| 742 |
+
with amp.autocast(dtype=self.dtype):
|
| 743 |
+
return [
|
| 744 |
+
self.model.encode(u.unsqueeze(0), self.scale).float().squeeze(0)
|
| 745 |
+
for u in videos
|
| 746 |
+
]
|
| 747 |
+
|
| 748 |
+
def decode(self, zs):
|
| 749 |
+
with amp.autocast(dtype=self.dtype):
|
| 750 |
+
return [
|
| 751 |
+
self.model.decode(u.unsqueeze(0),
|
| 752 |
+
self.scale).float().clamp_(-1, 1).squeeze(0)
|
| 753 |
+
for u in zs
|
| 754 |
+
]
|
models/wan/wan_wrapper.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models.model_interface import (
|
| 2 |
+
DiffusionModelInterface,
|
| 3 |
+
TextEncoderInterface,
|
| 4 |
+
VAEInterface
|
| 5 |
+
)
|
| 6 |
+
from models.wan.wan_base.modules.tokenizers import HuggingfaceTokenizer
|
| 7 |
+
from models.wan.wan_base.modules.model import WanModel
|
| 8 |
+
from models.wan.wan_base.modules.vae import _video_vae
|
| 9 |
+
from models.wan.wan_base.modules.t5 import umt5_xxl
|
| 10 |
+
from models.wan.flow_match import FlowMatchScheduler
|
| 11 |
+
from models.wan.causal_model import CausalWanModel
|
| 12 |
+
from typing import List, Tuple, Dict, Optional
|
| 13 |
+
import torch
|
| 14 |
+
import os
|
| 15 |
+
import torch.distributed as dist
|
| 16 |
+
import time
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _resolve_project_root() -> Path:
|
| 21 |
+
env_root = os.environ.get("STREAMDIFFUSIONV2_ROOT")
|
| 22 |
+
if env_root:
|
| 23 |
+
return Path(env_root).expanduser().resolve()
|
| 24 |
+
|
| 25 |
+
repo_root = Path(__file__).resolve().parents[2]
|
| 26 |
+
if (repo_root / "wan_models").exists():
|
| 27 |
+
return repo_root
|
| 28 |
+
|
| 29 |
+
cwd = Path.cwd().resolve()
|
| 30 |
+
if (cwd / "wan_models").exists():
|
| 31 |
+
return cwd
|
| 32 |
+
|
| 33 |
+
return repo_root
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
PROJECT_ROOT = _resolve_project_root()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class WanTextEncoder(TextEncoderInterface):
|
| 40 |
+
def __init__(self, model_type="T2V-1.3B") -> None:
|
| 41 |
+
super().__init__()
|
| 42 |
+
|
| 43 |
+
self.text_encoder = umt5_xxl(
|
| 44 |
+
encoder_only=True,
|
| 45 |
+
return_tokenizer=False,
|
| 46 |
+
dtype=torch.float32,
|
| 47 |
+
device=torch.device('cpu')
|
| 48 |
+
).eval().requires_grad_(False)
|
| 49 |
+
self.text_encoder.load_state_dict(
|
| 50 |
+
torch.load(
|
| 51 |
+
PROJECT_ROOT / f"wan_models/Wan2.1-{model_type}/models_t5_umt5-xxl-enc-bf16.pth",
|
| 52 |
+
map_location='cpu', weights_only=False
|
| 53 |
+
)
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
self.tokenizer = HuggingfaceTokenizer(
|
| 57 |
+
name=str(PROJECT_ROOT / f"wan_models/Wan2.1-{model_type}/google/umt5-xxl/"), seq_len=512, clean='whitespace')
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def device(self):
|
| 61 |
+
return next(self.parameters()).device
|
| 62 |
+
|
| 63 |
+
def forward(self, text_prompts: List[str]) -> dict:
|
| 64 |
+
ids, mask = self.tokenizer(
|
| 65 |
+
text_prompts, return_mask=True, add_special_tokens=True)
|
| 66 |
+
ids = ids.to(self.device)
|
| 67 |
+
mask = mask.to(self.device)
|
| 68 |
+
seq_lens = mask.gt(0).sum(dim=1).long()
|
| 69 |
+
context = self.text_encoder(ids, mask)
|
| 70 |
+
|
| 71 |
+
for u, v in zip(context, seq_lens):
|
| 72 |
+
u[v:] = 0.0 # set padding to 0.0
|
| 73 |
+
|
| 74 |
+
return {
|
| 75 |
+
"prompt_embeds": context
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class WanVAEWrapper(VAEInterface):
|
| 80 |
+
def __init__(self, model_type="T2V-1.3B"):
|
| 81 |
+
super().__init__()
|
| 82 |
+
mean = [
|
| 83 |
+
-0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
|
| 84 |
+
0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
|
| 85 |
+
]
|
| 86 |
+
std = [
|
| 87 |
+
2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
|
| 88 |
+
3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
|
| 89 |
+
]
|
| 90 |
+
self.mean = torch.tensor(mean, dtype=torch.float32)
|
| 91 |
+
self.std = torch.tensor(std, dtype=torch.float32)
|
| 92 |
+
|
| 93 |
+
# init model
|
| 94 |
+
self.model = _video_vae(
|
| 95 |
+
pretrained_path=str(PROJECT_ROOT / f"wan_models/Wan2.1-{model_type}/Wan2.1_VAE.pth"),
|
| 96 |
+
z_dim=16,
|
| 97 |
+
).eval().requires_grad_(False)
|
| 98 |
+
|
| 99 |
+
def decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor:
|
| 100 |
+
# from [batch_size, num_frames, num_channels, height, width]
|
| 101 |
+
# to [batch_size, num_channels, num_frames, height, width]
|
| 102 |
+
zs = latent.permute(0, 2, 1, 3, 4)
|
| 103 |
+
|
| 104 |
+
device, dtype = latent.device, latent.dtype
|
| 105 |
+
scale = [self.mean.to(device=device, dtype=dtype),
|
| 106 |
+
1.0 / self.std.to(device=device, dtype=dtype)]
|
| 107 |
+
|
| 108 |
+
output = [
|
| 109 |
+
self.model.decode(u.unsqueeze(0),
|
| 110 |
+
scale).float().clamp_(-1, 1).squeeze(0)
|
| 111 |
+
for u in zs
|
| 112 |
+
]
|
| 113 |
+
output = torch.stack(output, dim=0)
|
| 114 |
+
# from [batch_size, num_channels, num_frames, height, width]
|
| 115 |
+
# to [batch_size, num_frames, num_channels, height, width]
|
| 116 |
+
output = output.permute(0, 2, 1, 3, 4)
|
| 117 |
+
return output
|
| 118 |
+
|
| 119 |
+
def decode(self, latent: torch.Tensor) -> torch.Tensor:
|
| 120 |
+
# from [batch_size, num_frames, num_channels, height, width]
|
| 121 |
+
# to [batch_size, num_channels, num_frames, height, width]
|
| 122 |
+
zs = latent.permute(0, 2, 1, 3, 4)
|
| 123 |
+
|
| 124 |
+
device, dtype = latent.device, latent.dtype
|
| 125 |
+
scale = [self.mean.to(device=device, dtype=dtype),
|
| 126 |
+
1.0 / self.std.to(device=device, dtype=dtype)]
|
| 127 |
+
|
| 128 |
+
output = self.model.decode(zs, scale).clamp_(-1, 1)
|
| 129 |
+
# from [batch_size, num_channels, num_frames, height, width]
|
| 130 |
+
# to [batch_size, num_frames, num_channels, height, width]
|
| 131 |
+
# output = output.permute(0, 2, 1, 3, 4)
|
| 132 |
+
return output
|
| 133 |
+
|
| 134 |
+
def stream_encode(self, video: torch.Tensor, is_scale=False) -> torch.Tensor:
|
| 135 |
+
if is_scale:
|
| 136 |
+
device, dtype = video.device, video.dtype
|
| 137 |
+
scale = [self.mean.to(device=device, dtype=dtype),
|
| 138 |
+
1.0 / self.std.to(device=device, dtype=dtype)]
|
| 139 |
+
else:
|
| 140 |
+
scale = None
|
| 141 |
+
return self.model.stream_encode(video, scale)
|
| 142 |
+
|
| 143 |
+
def stream_decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor:
|
| 144 |
+
zs = latent.permute(0, 2, 1, 3, 4)
|
| 145 |
+
zs = zs.to(device=latent.device, dtype=torch.bfloat16)
|
| 146 |
+
device, dtype = latent.device, latent.dtype
|
| 147 |
+
scale = [self.mean.to(device=device, dtype=dtype),
|
| 148 |
+
1.0 / self.std.to(device=device, dtype=dtype)]
|
| 149 |
+
output = self.model.stream_decode(zs, scale).float().clamp_(-1, 1)
|
| 150 |
+
output = output.permute(0, 2, 1, 3, 4)
|
| 151 |
+
return output
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class WanDiffusionWrapper(DiffusionModelInterface):
|
| 155 |
+
def __init__(self, model_type="T2V-1.3B"):
|
| 156 |
+
super().__init__()
|
| 157 |
+
|
| 158 |
+
self.model = WanModel.from_pretrained(str(PROJECT_ROOT / f"wan_models/Wan2.1-{model_type}/"))
|
| 159 |
+
self.model.eval()
|
| 160 |
+
|
| 161 |
+
self.uniform_timestep = True
|
| 162 |
+
|
| 163 |
+
self.scheduler = FlowMatchScheduler(
|
| 164 |
+
shift=8.0, sigma_min=0.0, extra_one_step=True
|
| 165 |
+
)
|
| 166 |
+
self.scheduler.set_timesteps(1000, training=True)
|
| 167 |
+
|
| 168 |
+
self.seq_len = 32760 # [1, 21, 16, 60, 104]
|
| 169 |
+
super().post_init()
|
| 170 |
+
|
| 171 |
+
def enable_gradient_checkpointing(self) -> None:
|
| 172 |
+
self.model.enable_gradient_checkpointing()
|
| 173 |
+
|
| 174 |
+
def _convert_flow_pred_to_x0(self, flow_pred: torch.Tensor, xt: torch.Tensor, timestep: torch.Tensor) -> torch.Tensor:
|
| 175 |
+
"""
|
| 176 |
+
Convert flow matching's prediction to x0 prediction.
|
| 177 |
+
flow_pred: the prediction with shape [B, C, H, W]
|
| 178 |
+
xt: the input noisy data with shape [B, C, H, W]
|
| 179 |
+
timestep: the timestep with shape [B]
|
| 180 |
+
|
| 181 |
+
pred = noise - x0
|
| 182 |
+
x_t = (1-sigma_t) * x0 + sigma_t * noise
|
| 183 |
+
we have x0 = x_t - sigma_t * pred
|
| 184 |
+
see derivations https://chatgpt.com/share/67bf8589-3d04-8008-bc6e-4cf1a24e2d0e
|
| 185 |
+
"""
|
| 186 |
+
# use higher precision for calculations
|
| 187 |
+
original_dtype = flow_pred.dtype
|
| 188 |
+
flow_pred, xt, sigmas, timesteps = map(
|
| 189 |
+
lambda x: x.double().to(flow_pred.device), [flow_pred, xt,
|
| 190 |
+
self.scheduler.sigmas,
|
| 191 |
+
self.scheduler.timesteps]
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
timestep_id = torch.argmin(
|
| 195 |
+
(timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
|
| 196 |
+
sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
|
| 197 |
+
x0_pred = xt - sigma_t * flow_pred
|
| 198 |
+
return x0_pred.to(original_dtype)
|
| 199 |
+
|
| 200 |
+
@staticmethod
|
| 201 |
+
def _convert_x0_to_flow_pred(scheduler, x0_pred: torch.Tensor, xt: torch.Tensor, timestep: torch.Tensor) -> torch.Tensor:
|
| 202 |
+
"""
|
| 203 |
+
Convert x0 prediction to flow matching's prediction.
|
| 204 |
+
x0_pred: the x0 prediction with shape [B, C, H, W]
|
| 205 |
+
xt: the input noisy data with shape [B, C, H, W]
|
| 206 |
+
timestep: the timestep with shape [B]
|
| 207 |
+
|
| 208 |
+
pred = (x_t - x_0) / sigma_t
|
| 209 |
+
"""
|
| 210 |
+
# use higher precision for calculations
|
| 211 |
+
original_dtype = x0_pred.dtype
|
| 212 |
+
x0_pred, xt, sigmas, timesteps = map(
|
| 213 |
+
lambda x: x.double().to(x0_pred.device), [x0_pred, xt,
|
| 214 |
+
scheduler.sigmas,
|
| 215 |
+
scheduler.timesteps]
|
| 216 |
+
)
|
| 217 |
+
timestep_id = torch.argmin(
|
| 218 |
+
(timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
|
| 219 |
+
sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
|
| 220 |
+
flow_pred = (xt - x0_pred) / sigma_t
|
| 221 |
+
return flow_pred.to(original_dtype)
|
| 222 |
+
|
| 223 |
+
def forward(
|
| 224 |
+
self, noisy_image_or_video: torch.Tensor, conditional_dict: dict,
|
| 225 |
+
timestep: torch.Tensor, kv_cache: Optional[List[dict]] = None,
|
| 226 |
+
crossattn_cache: Optional[List[dict]] = None,
|
| 227 |
+
current_start: Optional[int] = None,
|
| 228 |
+
current_end: Optional[int] = None
|
| 229 |
+
) -> torch.Tensor:
|
| 230 |
+
prompt_embeds = conditional_dict["prompt_embeds"]
|
| 231 |
+
|
| 232 |
+
# [B, F] -> [B]
|
| 233 |
+
if self.uniform_timestep:
|
| 234 |
+
input_timestep = timestep[:, 0]
|
| 235 |
+
else:
|
| 236 |
+
input_timestep = timestep
|
| 237 |
+
|
| 238 |
+
if kv_cache is not None:
|
| 239 |
+
flow_pred = self.model(
|
| 240 |
+
noisy_image_or_video.permute(0, 2, 1, 3, 4),
|
| 241 |
+
t=input_timestep, context=prompt_embeds,
|
| 242 |
+
seq_len=self.seq_len,
|
| 243 |
+
kv_cache=kv_cache,
|
| 244 |
+
crossattn_cache=crossattn_cache,
|
| 245 |
+
current_start=current_start,
|
| 246 |
+
current_end=current_end
|
| 247 |
+
).permute(0, 2, 1, 3, 4)
|
| 248 |
+
else:
|
| 249 |
+
flow_pred = self.model(
|
| 250 |
+
noisy_image_or_video.permute(0, 2, 1, 3, 4),
|
| 251 |
+
t=input_timestep, context=prompt_embeds,
|
| 252 |
+
seq_len=self.seq_len
|
| 253 |
+
).permute(0, 2, 1, 3, 4)
|
| 254 |
+
|
| 255 |
+
pred_x0 = self._convert_flow_pred_to_x0(
|
| 256 |
+
flow_pred=flow_pred.flatten(0, 1),
|
| 257 |
+
xt=noisy_image_or_video.flatten(0, 1),
|
| 258 |
+
timestep=timestep.flatten(0, 1)
|
| 259 |
+
).unflatten(0, flow_pred.shape[:2])
|
| 260 |
+
|
| 261 |
+
return pred_x0
|
| 262 |
+
|
| 263 |
+
def forward_input(
|
| 264 |
+
self, noisy_image_or_video: torch.Tensor, conditional_dict: dict,
|
| 265 |
+
timestep: torch.Tensor,block_mode: str='input', block_num = None, kv_cache: Optional[List[dict]] = None,
|
| 266 |
+
crossattn_cache: Optional[List[dict]] = None,
|
| 267 |
+
current_start: Optional[int] = None,
|
| 268 |
+
current_end: Optional[int] = None,
|
| 269 |
+
patched_x_shape: torch.Tensor = None,
|
| 270 |
+
block_x: torch.Tensor = None,
|
| 271 |
+
) -> torch.Tensor:
|
| 272 |
+
assert kv_cache is not None, "kv_cache must be provided"
|
| 273 |
+
|
| 274 |
+
prompt_embeds = conditional_dict["prompt_embeds"]
|
| 275 |
+
|
| 276 |
+
# [B, F] -> [B]
|
| 277 |
+
if self.uniform_timestep:
|
| 278 |
+
input_timestep = timestep[:, 0]
|
| 279 |
+
else:
|
| 280 |
+
input_timestep = timestep
|
| 281 |
+
|
| 282 |
+
if block_x is not None and block_mode == 'middle':
|
| 283 |
+
noisy_image_or_video = block_x
|
| 284 |
+
else:
|
| 285 |
+
noisy_image_or_video = noisy_image_or_video.permute(0, 2, 1, 3, 4)
|
| 286 |
+
|
| 287 |
+
output, patched_x_shape = self.model(
|
| 288 |
+
noisy_image_or_video,
|
| 289 |
+
t=input_timestep, context=prompt_embeds,
|
| 290 |
+
seq_len=self.seq_len,
|
| 291 |
+
kv_cache=kv_cache,
|
| 292 |
+
crossattn_cache=crossattn_cache,
|
| 293 |
+
current_start=current_start,
|
| 294 |
+
current_end=current_end,
|
| 295 |
+
block_mode=block_mode,
|
| 296 |
+
block_num=block_num,
|
| 297 |
+
patched_x_shape=patched_x_shape,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
return output, patched_x_shape
|
| 301 |
+
|
| 302 |
+
def forward_output(
|
| 303 |
+
self, noisy_image_or_video: torch.Tensor, conditional_dict: dict,
|
| 304 |
+
timestep: torch.Tensor, block_mode: str='output', block_num = None, kv_cache: Optional[List[dict]] = None,
|
| 305 |
+
crossattn_cache: Optional[List[dict]] = None,
|
| 306 |
+
current_start: Optional[int] = None,
|
| 307 |
+
current_end: Optional[int] = None,
|
| 308 |
+
patched_x_shape: torch.Tensor = None,
|
| 309 |
+
block_x: torch.Tensor = None,
|
| 310 |
+
) -> torch.Tensor:
|
| 311 |
+
assert kv_cache is not None, "kv_cache must be provided"
|
| 312 |
+
|
| 313 |
+
prompt_embeds = conditional_dict["prompt_embeds"]
|
| 314 |
+
|
| 315 |
+
# [B, F] -> [B]
|
| 316 |
+
if self.uniform_timestep:
|
| 317 |
+
input_timestep = timestep[:, 0]
|
| 318 |
+
else:
|
| 319 |
+
input_timestep = timestep
|
| 320 |
+
|
| 321 |
+
flow_pred = self.model(
|
| 322 |
+
block_x,
|
| 323 |
+
t=input_timestep, context=prompt_embeds,
|
| 324 |
+
seq_len=self.seq_len,
|
| 325 |
+
kv_cache=kv_cache,
|
| 326 |
+
crossattn_cache=crossattn_cache,
|
| 327 |
+
current_start=current_start,
|
| 328 |
+
current_end=current_end,
|
| 329 |
+
block_mode=block_mode,
|
| 330 |
+
block_num=block_num,
|
| 331 |
+
patched_x_shape=patched_x_shape,
|
| 332 |
+
).permute(0, 2, 1, 3, 4)
|
| 333 |
+
|
| 334 |
+
pred_x0 = self._convert_flow_pred_to_x0(
|
| 335 |
+
flow_pred=flow_pred.flatten(0, 1),
|
| 336 |
+
xt=noisy_image_or_video.flatten(0, 1),
|
| 337 |
+
timestep=timestep.flatten(0, 1)
|
| 338 |
+
).unflatten(0, flow_pred.shape[:2])
|
| 339 |
+
|
| 340 |
+
return pred_x0
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
class CausalWanDiffusionWrapper(WanDiffusionWrapper):
|
| 344 |
+
def __init__(self, model_type="T2V-1.3B"):
|
| 345 |
+
super().__init__()
|
| 346 |
+
|
| 347 |
+
self.model = CausalWanModel.from_pretrained(
|
| 348 |
+
str(PROJECT_ROOT / f"wan_models/Wan2.1-{model_type}/"))
|
| 349 |
+
self.model.eval()
|
| 350 |
+
|
| 351 |
+
self.uniform_timestep = False
|
requirements.txt
CHANGED
|
@@ -1 +1,12 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diffusers==0.35.1
|
| 2 |
+
accelerate
|
| 3 |
+
av
|
| 4 |
+
einops
|
| 5 |
+
ftfy
|
| 6 |
+
imageio
|
| 7 |
+
imageio-ffmpeg
|
| 8 |
+
omegaconf
|
| 9 |
+
markdown2
|
| 10 |
+
numpy<2
|
| 11 |
+
sentencepiece
|
| 12 |
+
regex
|
streamdiffusionv2/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public StreamDiffusionV2 Python API."""
|
| 2 |
+
|
| 3 |
+
from streamdiffusionv2.pipeline import (
|
| 4 |
+
DenoisedChunk,
|
| 5 |
+
EncodedChunk,
|
| 6 |
+
StreamDiffusionV2Pipeline,
|
| 7 |
+
VideoChunk,
|
| 8 |
+
export_video,
|
| 9 |
+
load_video,
|
| 10 |
+
)
|
| 11 |
+
from streamv2v.api import StreamVideoToVideo, run_video_to_video
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"DenoisedChunk",
|
| 15 |
+
"EncodedChunk",
|
| 16 |
+
"StreamDiffusionV2Pipeline",
|
| 17 |
+
"StreamVideoToVideo",
|
| 18 |
+
"VideoChunk",
|
| 19 |
+
"export_video",
|
| 20 |
+
"load_video",
|
| 21 |
+
"run_video_to_video",
|
| 22 |
+
]
|
streamdiffusionv2/pipeline.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Readable staged video-to-video API for StreamDiffusionV2."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from contextlib import ExitStack
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from importlib.resources import as_file, files
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Literal
|
| 10 |
+
|
| 11 |
+
from diffusers.utils import export_to_video as diffusers_export_to_video
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
from models.util import set_seed
|
| 16 |
+
from streamv2v.inference import (
|
| 17 |
+
SingleGPUInferencePipeline as StreamBatchInferencePipeline,
|
| 18 |
+
compute_noise_scale_and_step,
|
| 19 |
+
)
|
| 20 |
+
from streamv2v.inference_common import load_mp4_as_tensor, merge_cli_config, normalize_acceleration_flags
|
| 21 |
+
from streamv2v.inference_wo_batch import SingleGPUInferencePipeline as StreamNoBatchInferencePipeline
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
SingleMode = Literal["single", "single-wo"]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class VideoChunk:
|
| 29 |
+
"""One video chunk prepared for the encode -> denoise -> decode loop."""
|
| 30 |
+
|
| 31 |
+
frames: torch.Tensor
|
| 32 |
+
start_idx: int
|
| 33 |
+
end_idx: int
|
| 34 |
+
current_start: int
|
| 35 |
+
current_end: int
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class EncodedChunk:
|
| 40 |
+
"""Encoded latent chunk plus the schedule metadata needed for denoising."""
|
| 41 |
+
|
| 42 |
+
noisy_latents: torch.Tensor
|
| 43 |
+
current_start: int
|
| 44 |
+
current_end: int
|
| 45 |
+
noise_scale: float
|
| 46 |
+
current_step: int | None = None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class DenoisedChunk:
|
| 51 |
+
"""Denoised latent chunk ready for VAE decoding."""
|
| 52 |
+
|
| 53 |
+
denoised_pred: torch.Tensor
|
| 54 |
+
last_frame_only: bool
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _resolve_default_config_path(resource_stack: ExitStack) -> str:
|
| 58 |
+
resource = files("streamv2v.configs").joinpath("wan_causal_dmd_v2v.yaml")
|
| 59 |
+
return str(resource_stack.enter_context(as_file(resource)))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _resolve_device(device: str | torch.device | None) -> torch.device:
|
| 63 |
+
cuda_available = torch.cuda.is_available()
|
| 64 |
+
if device is None:
|
| 65 |
+
return torch.device("cuda" if cuda_available else "cpu")
|
| 66 |
+
resolved = torch.device(device)
|
| 67 |
+
if resolved.type == "cuda" and not cuda_available:
|
| 68 |
+
raise RuntimeError("CUDA is not available in the current Python environment")
|
| 69 |
+
if resolved.type == "cuda" and resolved.index is not None:
|
| 70 |
+
torch.cuda.set_device(resolved.index)
|
| 71 |
+
return resolved
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _normalize_video_tensor(
|
| 75 |
+
video: str | Path | torch.Tensor,
|
| 76 |
+
*,
|
| 77 |
+
height: int,
|
| 78 |
+
width: int,
|
| 79 |
+
device: torch.device,
|
| 80 |
+
) -> torch.Tensor:
|
| 81 |
+
if isinstance(video, (str, Path)):
|
| 82 |
+
tensor = load_mp4_as_tensor(str(video), resize_hw=(height, width)).unsqueeze(0)
|
| 83 |
+
else:
|
| 84 |
+
tensor = video
|
| 85 |
+
if tensor.ndim == 4:
|
| 86 |
+
tensor = tensor.unsqueeze(0)
|
| 87 |
+
if tensor.ndim != 5:
|
| 88 |
+
raise ValueError("video tensor must have shape [B, C, T, H, W] or [C, T, H, W]")
|
| 89 |
+
if tensor.dtype != torch.bfloat16:
|
| 90 |
+
tensor = tensor.to(dtype=torch.bfloat16)
|
| 91 |
+
return tensor.to(device)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def load_video(video_path: str, *, height: int = 480, width: int = 832) -> torch.Tensor:
|
| 95 |
+
"""Load a video file as a normalized tensor with shape [C, T, H, W]."""
|
| 96 |
+
return load_mp4_as_tensor(video_path, resize_hw=(height, width))
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def export_video(video: np.ndarray, output_path: str, *, fps: int = 16) -> str:
|
| 100 |
+
"""Write a `[T, H, W, C]` float video array to an mp4 file."""
|
| 101 |
+
output_file = Path(output_path)
|
| 102 |
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
| 103 |
+
diffusers_export_to_video(video, str(output_file), fps=fps)
|
| 104 |
+
return str(output_file)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class StreamDiffusionV2Pipeline:
|
| 108 |
+
"""Readable staged single-GPU API that mirrors the offline inference flow."""
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
checkpoint_folder: str,
|
| 113 |
+
*,
|
| 114 |
+
mode: SingleMode = "single",
|
| 115 |
+
config_path: str | None = None,
|
| 116 |
+
device: str | torch.device | None = None,
|
| 117 |
+
noise_scale: float = 0.8,
|
| 118 |
+
height: int = 480,
|
| 119 |
+
width: int = 832,
|
| 120 |
+
fps: int = 16,
|
| 121 |
+
step: int = 2,
|
| 122 |
+
seed: int = 0,
|
| 123 |
+
model_type: str = "T2V-1.3B",
|
| 124 |
+
use_taehv: bool = False,
|
| 125 |
+
use_tensorrt: bool = False,
|
| 126 |
+
fast: bool = False,
|
| 127 |
+
profile: bool = False,
|
| 128 |
+
) -> None:
|
| 129 |
+
if mode not in {"single", "single-wo"}:
|
| 130 |
+
raise ValueError("StreamDiffusionV2Pipeline only supports 'single' and 'single-wo'")
|
| 131 |
+
|
| 132 |
+
self._resource_stack = ExitStack()
|
| 133 |
+
self.mode = mode
|
| 134 |
+
self.device = _resolve_device(device)
|
| 135 |
+
self.checkpoint_folder = checkpoint_folder
|
| 136 |
+
self.noise_scale = float(noise_scale)
|
| 137 |
+
self.height = int(height)
|
| 138 |
+
self.width = int(width)
|
| 139 |
+
self.fps = int(fps)
|
| 140 |
+
self.seed = int(seed)
|
| 141 |
+
self.step = int(step)
|
| 142 |
+
self.profile = bool(profile)
|
| 143 |
+
self.model_type = model_type
|
| 144 |
+
self.prompt: str | None = None
|
| 145 |
+
|
| 146 |
+
resolved_config_path = config_path or _resolve_default_config_path(self._resource_stack)
|
| 147 |
+
self.config_path = resolved_config_path
|
| 148 |
+
flags = normalize_acceleration_flags(
|
| 149 |
+
{
|
| 150 |
+
"use_taehv": use_taehv,
|
| 151 |
+
"use_tensorrt": use_tensorrt,
|
| 152 |
+
"fast": fast,
|
| 153 |
+
}
|
| 154 |
+
)
|
| 155 |
+
self.use_taehv = bool(flags["use_taehv"])
|
| 156 |
+
self.use_tensorrt = bool(flags["use_tensorrt"])
|
| 157 |
+
self.fast = bool(flags["fast"])
|
| 158 |
+
config_args = {
|
| 159 |
+
"config_path": resolved_config_path,
|
| 160 |
+
"checkpoint_folder": checkpoint_folder,
|
| 161 |
+
"noise_scale": noise_scale,
|
| 162 |
+
"height": height,
|
| 163 |
+
"width": width,
|
| 164 |
+
"fps": fps,
|
| 165 |
+
"step": step,
|
| 166 |
+
"seed": seed,
|
| 167 |
+
"model_type": model_type,
|
| 168 |
+
"profile": profile,
|
| 169 |
+
"use_taehv": self.use_taehv,
|
| 170 |
+
"use_tensorrt": self.use_tensorrt,
|
| 171 |
+
"fast": self.fast,
|
| 172 |
+
"t2v": False,
|
| 173 |
+
"target_fps": None,
|
| 174 |
+
"fixed_noise_scale": False,
|
| 175 |
+
"num_frames": 81,
|
| 176 |
+
}
|
| 177 |
+
self.config = merge_cli_config(resolved_config_path, config_args)
|
| 178 |
+
|
| 179 |
+
manager_cls = (
|
| 180 |
+
StreamBatchInferencePipeline if mode == "single" else StreamNoBatchInferencePipeline
|
| 181 |
+
)
|
| 182 |
+
torch.set_grad_enabled(False)
|
| 183 |
+
set_seed(self.seed)
|
| 184 |
+
self.pipeline_manager = manager_cls(self.config, self.device)
|
| 185 |
+
self.pipeline_manager.load_model(checkpoint_folder)
|
| 186 |
+
self.chunk_size = 4 * self.config.num_frame_per_block
|
| 187 |
+
self.num_steps = len(self.pipeline_manager.pipeline.denoising_step_list)
|
| 188 |
+
self._next_chunk_index = 0
|
| 189 |
+
|
| 190 |
+
def close(self) -> None:
|
| 191 |
+
self._resource_stack.close()
|
| 192 |
+
|
| 193 |
+
def __enter__(self) -> "StreamDiffusionV2Pipeline":
|
| 194 |
+
return self
|
| 195 |
+
|
| 196 |
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
| 197 |
+
self.close()
|
| 198 |
+
|
| 199 |
+
def enable_acceleration(
|
| 200 |
+
self,
|
| 201 |
+
*,
|
| 202 |
+
use_taehv: bool = False,
|
| 203 |
+
use_tensorrt: bool = False,
|
| 204 |
+
fast: bool = False,
|
| 205 |
+
) -> "StreamDiffusionV2Pipeline":
|
| 206 |
+
"""Rebuild the pipeline with the requested acceleration flags."""
|
| 207 |
+
replacement = StreamDiffusionV2Pipeline(
|
| 208 |
+
checkpoint_folder=self.checkpoint_folder,
|
| 209 |
+
mode=self.mode,
|
| 210 |
+
config_path=self.config_path,
|
| 211 |
+
device=self.device,
|
| 212 |
+
noise_scale=self.noise_scale,
|
| 213 |
+
height=self.height,
|
| 214 |
+
width=self.width,
|
| 215 |
+
fps=self.fps,
|
| 216 |
+
step=self.step,
|
| 217 |
+
seed=self.seed,
|
| 218 |
+
model_type=self.model_type,
|
| 219 |
+
use_taehv=use_taehv,
|
| 220 |
+
use_tensorrt=use_tensorrt,
|
| 221 |
+
fast=fast,
|
| 222 |
+
profile=self.profile,
|
| 223 |
+
)
|
| 224 |
+
self.close()
|
| 225 |
+
self.__dict__.update(replacement.__dict__)
|
| 226 |
+
return self
|
| 227 |
+
|
| 228 |
+
def prepare(self, prompt: str) -> None:
|
| 229 |
+
"""Reset the stream state and store the prompt for the next denoising pass."""
|
| 230 |
+
self.prompt = prompt
|
| 231 |
+
self.pipeline_manager.reset_stream_state(reset_vae_flags=True)
|
| 232 |
+
self.pipeline_manager.processed = 0
|
| 233 |
+
self._next_chunk_index = 0
|
| 234 |
+
|
| 235 |
+
def chunk_video(self, video: str | Path | torch.Tensor) -> list[VideoChunk]:
|
| 236 |
+
"""Split a full input video into the same chunks used by the offline inference loop."""
|
| 237 |
+
input_video = _normalize_video_tensor(
|
| 238 |
+
video,
|
| 239 |
+
height=self.height,
|
| 240 |
+
width=self.width,
|
| 241 |
+
device=self.device,
|
| 242 |
+
)
|
| 243 |
+
_, _, total_frames, _, _ = input_video.shape
|
| 244 |
+
if total_frames < 1 + self.chunk_size:
|
| 245 |
+
raise ValueError(f"video must contain at least {1 + self.chunk_size} frames")
|
| 246 |
+
|
| 247 |
+
chunks: list[VideoChunk] = []
|
| 248 |
+
start_idx = 0
|
| 249 |
+
end_idx = 1 + self.chunk_size
|
| 250 |
+
current_start = 0
|
| 251 |
+
current_end = self.pipeline_manager.pipeline.frame_seq_length * (1 + self.chunk_size // 4)
|
| 252 |
+
|
| 253 |
+
chunks.append(
|
| 254 |
+
VideoChunk(
|
| 255 |
+
frames=input_video[:, :, start_idx:end_idx],
|
| 256 |
+
start_idx=start_idx,
|
| 257 |
+
end_idx=end_idx,
|
| 258 |
+
current_start=current_start,
|
| 259 |
+
current_end=current_end,
|
| 260 |
+
)
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
while True:
|
| 264 |
+
start_idx = end_idx
|
| 265 |
+
end_idx = end_idx + self.chunk_size
|
| 266 |
+
if end_idx > total_frames:
|
| 267 |
+
break
|
| 268 |
+
current_start = current_end
|
| 269 |
+
current_end = current_end + (self.chunk_size // 4) * self.pipeline_manager.pipeline.frame_seq_length
|
| 270 |
+
chunks.append(
|
| 271 |
+
VideoChunk(
|
| 272 |
+
frames=input_video[:, :, start_idx:end_idx],
|
| 273 |
+
start_idx=start_idx,
|
| 274 |
+
end_idx=end_idx,
|
| 275 |
+
current_start=current_start,
|
| 276 |
+
current_end=current_end,
|
| 277 |
+
)
|
| 278 |
+
)
|
| 279 |
+
return chunks
|
| 280 |
+
|
| 281 |
+
@torch.inference_mode()
|
| 282 |
+
def encode_chunk(
|
| 283 |
+
self,
|
| 284 |
+
input_video: str | Path | torch.Tensor,
|
| 285 |
+
chunk: VideoChunk,
|
| 286 |
+
*,
|
| 287 |
+
previous_noise_scale: float | None = None,
|
| 288 |
+
initial_noise_scale: float | None = None,
|
| 289 |
+
) -> EncodedChunk:
|
| 290 |
+
"""Encode one chunk in the same style as the offline inference loop."""
|
| 291 |
+
full_video = _normalize_video_tensor(
|
| 292 |
+
input_video,
|
| 293 |
+
height=self.height,
|
| 294 |
+
width=self.width,
|
| 295 |
+
device=self.device,
|
| 296 |
+
)
|
| 297 |
+
noise_scale = self.noise_scale if previous_noise_scale is None else float(previous_noise_scale)
|
| 298 |
+
init_noise_scale = self.noise_scale if initial_noise_scale is None else float(initial_noise_scale)
|
| 299 |
+
current_step = None
|
| 300 |
+
|
| 301 |
+
if chunk.start_idx != 0:
|
| 302 |
+
noise_scale, current_step = compute_noise_scale_and_step(
|
| 303 |
+
full_video,
|
| 304 |
+
chunk.end_idx,
|
| 305 |
+
self.chunk_size,
|
| 306 |
+
noise_scale,
|
| 307 |
+
init_noise_scale,
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
latents = self.pipeline_manager._timed_stream_encode(chunk.frames)
|
| 311 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 312 |
+
noise = torch.randn_like(latents)
|
| 313 |
+
return EncodedChunk(
|
| 314 |
+
noisy_latents=noise * noise_scale + latents * (1 - noise_scale),
|
| 315 |
+
current_start=chunk.current_start,
|
| 316 |
+
current_end=chunk.current_end,
|
| 317 |
+
noise_scale=float(noise_scale),
|
| 318 |
+
current_step=current_step,
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
@torch.inference_mode()
|
| 322 |
+
def encode_video(self, video: str | Path | torch.Tensor) -> list[EncodedChunk]:
|
| 323 |
+
"""Encode a full input video into noisy latent chunks."""
|
| 324 |
+
chunks: list[EncodedChunk] = []
|
| 325 |
+
noise_scale = float(self.noise_scale)
|
| 326 |
+
init_noise_scale = noise_scale
|
| 327 |
+
video_chunks = self.chunk_video(video)
|
| 328 |
+
full_video = _normalize_video_tensor(
|
| 329 |
+
video,
|
| 330 |
+
height=self.height,
|
| 331 |
+
width=self.width,
|
| 332 |
+
device=self.device,
|
| 333 |
+
)
|
| 334 |
+
for chunk in video_chunks:
|
| 335 |
+
encoded_chunk = self.encode_chunk(
|
| 336 |
+
full_video,
|
| 337 |
+
chunk,
|
| 338 |
+
previous_noise_scale=noise_scale,
|
| 339 |
+
initial_noise_scale=init_noise_scale,
|
| 340 |
+
)
|
| 341 |
+
noise_scale = encoded_chunk.noise_scale
|
| 342 |
+
chunks.append(encoded_chunk)
|
| 343 |
+
return chunks
|
| 344 |
+
|
| 345 |
+
@torch.inference_mode()
|
| 346 |
+
def denoise_chunks(self, chunks: list[EncodedChunk]) -> list[DenoisedChunk]:
|
| 347 |
+
"""Run DiT denoising over the encoded chunks."""
|
| 348 |
+
if not chunks:
|
| 349 |
+
raise ValueError("chunks must not be empty")
|
| 350 |
+
if self.prompt is None:
|
| 351 |
+
raise RuntimeError("Call prepare(prompt) before denoise_chunks(...)")
|
| 352 |
+
|
| 353 |
+
self.prepare(self.prompt)
|
| 354 |
+
outputs: list[DenoisedChunk] = []
|
| 355 |
+
for chunk in chunks:
|
| 356 |
+
denoised_chunk = self.denoise_chunk(chunk)
|
| 357 |
+
if denoised_chunk is not None:
|
| 358 |
+
outputs.append(denoised_chunk)
|
| 359 |
+
return outputs
|
| 360 |
+
|
| 361 |
+
@torch.inference_mode()
|
| 362 |
+
def denoise_chunk(self, chunk: EncodedChunk) -> DenoisedChunk | None:
|
| 363 |
+
"""Run DiT on one encoded chunk and return a decodable latent when available."""
|
| 364 |
+
if self.prompt is None:
|
| 365 |
+
raise RuntimeError("Call prepare(prompt) before denoise_chunk(...)")
|
| 366 |
+
|
| 367 |
+
if self._next_chunk_index == 0:
|
| 368 |
+
if self.mode == "single":
|
| 369 |
+
denoised_pred = self.pipeline_manager.prepare_pipeline(
|
| 370 |
+
text_prompts=[self.prompt],
|
| 371 |
+
noise=chunk.noisy_latents,
|
| 372 |
+
current_start=chunk.current_start,
|
| 373 |
+
current_end=chunk.current_end,
|
| 374 |
+
)
|
| 375 |
+
else:
|
| 376 |
+
denoised_pred = self.pipeline_manager.prepare_pipeline(
|
| 377 |
+
text_prompts=[self.prompt],
|
| 378 |
+
noise=chunk.noisy_latents,
|
| 379 |
+
current_start=chunk.current_start,
|
| 380 |
+
current_end=chunk.current_end,
|
| 381 |
+
batch_denoise=False,
|
| 382 |
+
)
|
| 383 |
+
self._next_chunk_index += 1
|
| 384 |
+
return DenoisedChunk(denoised_pred=denoised_pred, last_frame_only=False)
|
| 385 |
+
|
| 386 |
+
current_start = chunk.current_start
|
| 387 |
+
current_end = chunk.current_end
|
| 388 |
+
|
| 389 |
+
if current_start // self.pipeline_manager.pipeline.frame_seq_length >= self.pipeline_manager.t_refresh:
|
| 390 |
+
current_start = self.pipeline_manager.pipeline.kv_cache_length - self.pipeline_manager.pipeline.frame_seq_length
|
| 391 |
+
current_end = current_start + (self.chunk_size // 4) * self.pipeline_manager.pipeline.frame_seq_length
|
| 392 |
+
|
| 393 |
+
if self.mode == "single":
|
| 394 |
+
denoised_pred = self.pipeline_manager.pipeline.inference_stream(
|
| 395 |
+
noise=chunk.noisy_latents,
|
| 396 |
+
current_start=current_start,
|
| 397 |
+
current_end=current_end,
|
| 398 |
+
current_step=chunk.current_step,
|
| 399 |
+
)
|
| 400 |
+
self.pipeline_manager.processed += 1
|
| 401 |
+
self._next_chunk_index += 1
|
| 402 |
+
if self.pipeline_manager.processed < self.num_steps:
|
| 403 |
+
return None
|
| 404 |
+
return DenoisedChunk(denoised_pred=denoised_pred, last_frame_only=True)
|
| 405 |
+
|
| 406 |
+
denoised_pred = self.pipeline_manager.pipeline.inference_wo_batch(
|
| 407 |
+
noise=chunk.noisy_latents,
|
| 408 |
+
current_start=current_start,
|
| 409 |
+
current_end=current_end,
|
| 410 |
+
current_step=chunk.current_step,
|
| 411 |
+
)
|
| 412 |
+
self.pipeline_manager.processed += 1
|
| 413 |
+
self._next_chunk_index += 1
|
| 414 |
+
return DenoisedChunk(denoised_pred=denoised_pred, last_frame_only=True)
|
| 415 |
+
|
| 416 |
+
@torch.inference_mode()
|
| 417 |
+
def decode_chunks(self, chunks: list[DenoisedChunk]) -> np.ndarray:
|
| 418 |
+
"""Decode denoised latent chunks into a `[T, H, W, C]` video array."""
|
| 419 |
+
if not chunks:
|
| 420 |
+
raise ValueError("chunks must not be empty")
|
| 421 |
+
decoded = [self.decode_chunk(chunk) for chunk in chunks]
|
| 422 |
+
return np.concatenate(decoded, axis=0)
|
| 423 |
+
|
| 424 |
+
@torch.inference_mode()
|
| 425 |
+
def decode_chunk(self, chunk: DenoisedChunk) -> np.ndarray:
|
| 426 |
+
"""Decode one denoised latent chunk into `[T, H, W, C]` frames."""
|
| 427 |
+
return self.pipeline_manager._decode_video_array(
|
| 428 |
+
chunk.denoised_pred,
|
| 429 |
+
last_frame_only=chunk.last_frame_only,
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
@torch.inference_mode()
|
| 433 |
+
def __call__(self, video: str | Path | torch.Tensor) -> np.ndarray:
|
| 434 |
+
"""Run the full staged pipeline after `prepare(prompt)` has been called."""
|
| 435 |
+
encoded = self.encode_video(video)
|
| 436 |
+
denoised = self.denoise_chunks(encoded)
|
| 437 |
+
return self.decode_chunks(denoised)
|
streamv2v/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""StreamDiffusionV2 inference package."""
|
| 2 |
+
|
| 3 |
+
from streamv2v.api import StreamVideoToVideo, run_video_to_video
|
| 4 |
+
from streamv2v.inference_common import load_mp4_as_tensor
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"StreamVideoToVideo",
|
| 8 |
+
"load_mp4_as_tensor",
|
| 9 |
+
"run_video_to_video",
|
| 10 |
+
"inference",
|
| 11 |
+
"inference_common",
|
| 12 |
+
"inference_pipe",
|
| 13 |
+
"inference_wo_batch",
|
| 14 |
+
]
|
streamv2v/api.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public Python API for simple offline video-to-video inference."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from contextlib import ExitStack
|
| 6 |
+
from importlib.resources import as_file, files
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import shutil
|
| 10 |
+
import socket
|
| 11 |
+
import subprocess
|
| 12 |
+
import sys
|
| 13 |
+
import tempfile
|
| 14 |
+
from typing import Literal, Sequence
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from streamv2v.inference_common import load_mp4_as_tensor, normalize_acceleration_flags
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
InferenceMode = Literal["single", "single-wo", "pipe"]
|
| 22 |
+
|
| 23 |
+
_SINGLE_MODE_TO_MODULE = {
|
| 24 |
+
"single": "streamv2v.inference",
|
| 25 |
+
"single-wo": "streamv2v.inference_wo_batch",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _resolve_default_config_path(resource_stack: ExitStack) -> str:
|
| 30 |
+
resource = files("streamv2v.configs").joinpath("wan_causal_dmd_v2v.yaml")
|
| 31 |
+
return str(resource_stack.enter_context(as_file(resource)))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _normalize_gpu_ids(gpu_ids: int | Sequence[int] | None) -> list[int] | None:
|
| 35 |
+
if gpu_ids is None:
|
| 36 |
+
return None
|
| 37 |
+
if isinstance(gpu_ids, int):
|
| 38 |
+
return [gpu_ids]
|
| 39 |
+
return [int(gpu_id) for gpu_id in gpu_ids]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _normalize_device_gpu_id(device: str | torch.device | None) -> list[int] | None:
|
| 43 |
+
if device is None:
|
| 44 |
+
return None
|
| 45 |
+
device_str = str(device)
|
| 46 |
+
if not device_str.startswith("cuda:"):
|
| 47 |
+
return None
|
| 48 |
+
return [int(device_str.split(":", 1)[1])]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _resolve_single_gpu_id(gpu_ids: list[int] | None) -> int | None:
|
| 52 |
+
if gpu_ids is None:
|
| 53 |
+
return None
|
| 54 |
+
if len(gpu_ids) != 1:
|
| 55 |
+
raise ValueError("single and single-wo modes accept exactly one GPU id")
|
| 56 |
+
return int(gpu_ids[0])
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _pick_free_port() -> int:
|
| 60 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
| 61 |
+
sock.bind(("127.0.0.1", 0))
|
| 62 |
+
return int(sock.getsockname()[1])
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _build_common_args(
|
| 66 |
+
*,
|
| 67 |
+
config_path: str,
|
| 68 |
+
checkpoint_folder: str,
|
| 69 |
+
video_path: str,
|
| 70 |
+
prompt_file_path: str,
|
| 71 |
+
output_folder: str,
|
| 72 |
+
noise_scale: float,
|
| 73 |
+
height: int,
|
| 74 |
+
width: int,
|
| 75 |
+
fps: int,
|
| 76 |
+
step: int,
|
| 77 |
+
seed: int,
|
| 78 |
+
model_type: str,
|
| 79 |
+
profile: bool,
|
| 80 |
+
use_taehv: bool,
|
| 81 |
+
use_tensorrt: bool,
|
| 82 |
+
fast: bool,
|
| 83 |
+
) -> list[str]:
|
| 84 |
+
args = [
|
| 85 |
+
"--config_path",
|
| 86 |
+
config_path,
|
| 87 |
+
"--checkpoint_folder",
|
| 88 |
+
checkpoint_folder,
|
| 89 |
+
"--output_folder",
|
| 90 |
+
output_folder,
|
| 91 |
+
"--prompt_file_path",
|
| 92 |
+
prompt_file_path,
|
| 93 |
+
"--video_path",
|
| 94 |
+
video_path,
|
| 95 |
+
"--noise_scale",
|
| 96 |
+
str(noise_scale),
|
| 97 |
+
"--height",
|
| 98 |
+
str(height),
|
| 99 |
+
"--width",
|
| 100 |
+
str(width),
|
| 101 |
+
"--fps",
|
| 102 |
+
str(fps),
|
| 103 |
+
"--step",
|
| 104 |
+
str(step),
|
| 105 |
+
"--seed",
|
| 106 |
+
str(seed),
|
| 107 |
+
"--model_type",
|
| 108 |
+
model_type,
|
| 109 |
+
]
|
| 110 |
+
if profile:
|
| 111 |
+
args.append("--profile")
|
| 112 |
+
if use_taehv:
|
| 113 |
+
args.append("--use_taehv")
|
| 114 |
+
if use_tensorrt:
|
| 115 |
+
args.append("--use_tensorrt")
|
| 116 |
+
if fast:
|
| 117 |
+
args.append("--fast")
|
| 118 |
+
return args
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class StreamVideoToVideo:
|
| 122 |
+
"""Convenience wrapper around the offline Python entrypoints."""
|
| 123 |
+
|
| 124 |
+
def __init__(
|
| 125 |
+
self,
|
| 126 |
+
checkpoint_folder: str,
|
| 127 |
+
mode: InferenceMode = "single",
|
| 128 |
+
*,
|
| 129 |
+
config_path: str | None = None,
|
| 130 |
+
device: str | torch.device | None = None,
|
| 131 |
+
gpu_ids: int | Sequence[int] | None = None,
|
| 132 |
+
num_gpus: int | None = None,
|
| 133 |
+
noise_scale: float = 0.8,
|
| 134 |
+
height: int = 480,
|
| 135 |
+
width: int = 832,
|
| 136 |
+
fps: int = 16,
|
| 137 |
+
step: int = 2,
|
| 138 |
+
seed: int = 0,
|
| 139 |
+
model_type: str = "T2V-1.3B",
|
| 140 |
+
use_taehv: bool = False,
|
| 141 |
+
use_tensorrt: bool = False,
|
| 142 |
+
fast: bool = False,
|
| 143 |
+
profile: bool = False,
|
| 144 |
+
schedule_block: bool = False,
|
| 145 |
+
) -> None:
|
| 146 |
+
self.checkpoint_folder = checkpoint_folder
|
| 147 |
+
self.mode = mode
|
| 148 |
+
self.config_path = config_path
|
| 149 |
+
self.device = device
|
| 150 |
+
self.gpu_ids = gpu_ids
|
| 151 |
+
self.num_gpus = num_gpus
|
| 152 |
+
self.noise_scale = noise_scale
|
| 153 |
+
self.height = height
|
| 154 |
+
self.width = width
|
| 155 |
+
self.fps = fps
|
| 156 |
+
self.step = step
|
| 157 |
+
self.seed = seed
|
| 158 |
+
self.model_type = model_type
|
| 159 |
+
self.use_taehv = use_taehv
|
| 160 |
+
self.use_tensorrt = use_tensorrt
|
| 161 |
+
self.fast = fast
|
| 162 |
+
self.profile = profile
|
| 163 |
+
self.schedule_block = schedule_block
|
| 164 |
+
|
| 165 |
+
def generate(self, video_path: str, prompt: str) -> torch.Tensor:
|
| 166 |
+
with tempfile.TemporaryDirectory(prefix="streamv2v_generate_") as temp_dir:
|
| 167 |
+
output_path = os.path.join(temp_dir, "output.mp4")
|
| 168 |
+
self.run_video(video_path=video_path, prompt=prompt, output_path=output_path)
|
| 169 |
+
return load_mp4_as_tensor(output_path, normalize=False)
|
| 170 |
+
|
| 171 |
+
def run_video(self, video_path: str, prompt: str, output_path: str) -> str:
|
| 172 |
+
return run_video_to_video(
|
| 173 |
+
checkpoint_folder=self.checkpoint_folder,
|
| 174 |
+
video_path=video_path,
|
| 175 |
+
prompt=prompt,
|
| 176 |
+
output_path=output_path,
|
| 177 |
+
mode=self.mode,
|
| 178 |
+
config_path=self.config_path,
|
| 179 |
+
device=self.device,
|
| 180 |
+
gpu_ids=self.gpu_ids,
|
| 181 |
+
num_gpus=self.num_gpus,
|
| 182 |
+
noise_scale=self.noise_scale,
|
| 183 |
+
height=self.height,
|
| 184 |
+
width=self.width,
|
| 185 |
+
fps=self.fps,
|
| 186 |
+
step=self.step,
|
| 187 |
+
seed=self.seed,
|
| 188 |
+
model_type=self.model_type,
|
| 189 |
+
use_taehv=self.use_taehv,
|
| 190 |
+
use_tensorrt=self.use_tensorrt,
|
| 191 |
+
fast=self.fast,
|
| 192 |
+
profile=self.profile,
|
| 193 |
+
schedule_block=self.schedule_block,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def run_video_to_video(
|
| 198 |
+
*,
|
| 199 |
+
checkpoint_folder: str,
|
| 200 |
+
video_path: str,
|
| 201 |
+
prompt: str,
|
| 202 |
+
output_path: str,
|
| 203 |
+
mode: InferenceMode = "single",
|
| 204 |
+
config_path: str | None = None,
|
| 205 |
+
device: str | torch.device | None = None,
|
| 206 |
+
gpu_ids: int | Sequence[int] | None = None,
|
| 207 |
+
num_gpus: int | None = None,
|
| 208 |
+
noise_scale: float = 0.8,
|
| 209 |
+
height: int = 480,
|
| 210 |
+
width: int = 832,
|
| 211 |
+
fps: int = 16,
|
| 212 |
+
step: int = 2,
|
| 213 |
+
seed: int = 0,
|
| 214 |
+
model_type: str = "T2V-1.3B",
|
| 215 |
+
use_taehv: bool = False,
|
| 216 |
+
use_tensorrt: bool = False,
|
| 217 |
+
fast: bool = False,
|
| 218 |
+
profile: bool = False,
|
| 219 |
+
schedule_block: bool = False,
|
| 220 |
+
) -> str:
|
| 221 |
+
"""Run offline video-to-video inference from Python."""
|
| 222 |
+
flags = normalize_acceleration_flags(
|
| 223 |
+
{
|
| 224 |
+
"use_taehv": use_taehv,
|
| 225 |
+
"use_tensorrt": use_tensorrt,
|
| 226 |
+
"fast": fast,
|
| 227 |
+
}
|
| 228 |
+
)
|
| 229 |
+
use_taehv = bool(flags["use_taehv"])
|
| 230 |
+
use_tensorrt = bool(flags["use_tensorrt"])
|
| 231 |
+
fast = bool(flags["fast"])
|
| 232 |
+
|
| 233 |
+
requested_gpu_ids = _normalize_gpu_ids(gpu_ids)
|
| 234 |
+
device_gpu_ids = _normalize_device_gpu_id(device)
|
| 235 |
+
if requested_gpu_ids is None and device_gpu_ids is not None:
|
| 236 |
+
requested_gpu_ids = device_gpu_ids
|
| 237 |
+
|
| 238 |
+
if mode == "pipe":
|
| 239 |
+
if num_gpus is None:
|
| 240 |
+
num_gpus = len(requested_gpu_ids) if requested_gpu_ids is not None else 2
|
| 241 |
+
if requested_gpu_ids is not None and len(requested_gpu_ids) != num_gpus:
|
| 242 |
+
raise ValueError("num_gpus must match len(gpu_ids) for pipe mode")
|
| 243 |
+
elif num_gpus is not None and num_gpus != 1:
|
| 244 |
+
raise ValueError("num_gpus is only used for pipe mode")
|
| 245 |
+
|
| 246 |
+
resource_stack = ExitStack()
|
| 247 |
+
try:
|
| 248 |
+
resolved_config_path = config_path or _resolve_default_config_path(resource_stack)
|
| 249 |
+
output_file = Path(output_path)
|
| 250 |
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
| 251 |
+
|
| 252 |
+
with tempfile.TemporaryDirectory(prefix="streamv2v_api_") as temp_dir:
|
| 253 |
+
temp_dir_path = Path(temp_dir)
|
| 254 |
+
prompt_path = temp_dir_path / "prompt.txt"
|
| 255 |
+
prompt_path.write_text(prompt + "\n", encoding="utf-8")
|
| 256 |
+
temp_output_dir = temp_dir_path / "outputs"
|
| 257 |
+
temp_output_dir.mkdir(parents=True, exist_ok=True)
|
| 258 |
+
|
| 259 |
+
common_args = _build_common_args(
|
| 260 |
+
config_path=resolved_config_path,
|
| 261 |
+
checkpoint_folder=checkpoint_folder,
|
| 262 |
+
video_path=video_path,
|
| 263 |
+
prompt_file_path=str(prompt_path),
|
| 264 |
+
output_folder=str(temp_output_dir),
|
| 265 |
+
noise_scale=noise_scale,
|
| 266 |
+
height=height,
|
| 267 |
+
width=width,
|
| 268 |
+
fps=fps,
|
| 269 |
+
step=step,
|
| 270 |
+
seed=seed,
|
| 271 |
+
model_type=model_type,
|
| 272 |
+
profile=profile,
|
| 273 |
+
use_taehv=use_taehv,
|
| 274 |
+
use_tensorrt=use_tensorrt,
|
| 275 |
+
fast=fast,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
env = os.environ.copy()
|
| 279 |
+
if requested_gpu_ids is not None and mode == "pipe":
|
| 280 |
+
env["CUDA_VISIBLE_DEVICES"] = ",".join(str(gpu_id) for gpu_id in requested_gpu_ids)
|
| 281 |
+
|
| 282 |
+
if mode == "pipe":
|
| 283 |
+
cmd = [
|
| 284 |
+
sys.executable,
|
| 285 |
+
"-m",
|
| 286 |
+
"torch.distributed.run",
|
| 287 |
+
f"--nproc_per_node={num_gpus}",
|
| 288 |
+
f"--master_port={_pick_free_port()}",
|
| 289 |
+
"-m",
|
| 290 |
+
"streamv2v.inference_pipe",
|
| 291 |
+
*common_args,
|
| 292 |
+
]
|
| 293 |
+
if schedule_block:
|
| 294 |
+
cmd.append("--schedule_block")
|
| 295 |
+
else:
|
| 296 |
+
module_name = _SINGLE_MODE_TO_MODULE.get(mode)
|
| 297 |
+
if module_name is None:
|
| 298 |
+
raise ValueError(f"Unsupported mode: {mode}")
|
| 299 |
+
cmd = [sys.executable, "-m", module_name, *common_args]
|
| 300 |
+
single_gpu_id = _resolve_single_gpu_id(requested_gpu_ids)
|
| 301 |
+
if single_gpu_id is not None:
|
| 302 |
+
cmd.extend(["--gpu_id", str(single_gpu_id)])
|
| 303 |
+
|
| 304 |
+
subprocess.run(cmd, env=env, check=True)
|
| 305 |
+
generated_path = temp_output_dir / "output_000.mp4"
|
| 306 |
+
shutil.copy2(generated_path, output_file)
|
| 307 |
+
return str(output_file)
|
| 308 |
+
finally:
|
| 309 |
+
resource_stack.close()
|
streamv2v/communication/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Communication module for distributed inference pipeline.
|
| 3 |
+
|
| 4 |
+
This module provides abstractions for distributed communication operations,
|
| 5 |
+
model data transfer, and buffer management in the StreamDiffusionV2 pipeline.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from .distributed_communicator import DistributedCommunicator
|
| 9 |
+
from .model_data_transfer import ModelDataTransfer
|
| 10 |
+
from .buffer_manager import BufferManager
|
| 11 |
+
from .data_containers import LatentData, KVCacheData, CommunicationConfig
|
| 12 |
+
from .kv_cache_manager import KVCacheManager
|
| 13 |
+
from .utils import CommunicationTags, init_distributed, setup_logging, compute_balanced_split
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
'DistributedCommunicator',
|
| 17 |
+
'ModelDataTransfer',
|
| 18 |
+
'BufferManager',
|
| 19 |
+
'LatentData',
|
| 20 |
+
'KVCacheData',
|
| 21 |
+
'CommunicationConfig',
|
| 22 |
+
'KVCacheManager',
|
| 23 |
+
'CommunicationTags',
|
| 24 |
+
'init_distributed',
|
| 25 |
+
'setup_logging',
|
| 26 |
+
'compute_balanced_split'
|
| 27 |
+
]
|
streamv2v/communication/buffer_manager.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Buffer manager for efficient GPU memory management.
|
| 3 |
+
|
| 4 |
+
This module provides a buffer pool manager to avoid repeated GPU memory allocations
|
| 5 |
+
during distributed communication operations.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from typing import Dict, List, Tuple, Optional
|
| 10 |
+
import threading
|
| 11 |
+
import logging
|
| 12 |
+
from .data_containers import CommunicationConfig
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
BufferKey = Tuple[Tuple[int, ...], torch.dtype]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class BufferManager:
|
| 19 |
+
"""
|
| 20 |
+
Manages GPU buffer pools to avoid repeated allocations.
|
| 21 |
+
|
| 22 |
+
This class maintains pools of pre-allocated GPU tensors that can be reused
|
| 23 |
+
across communication operations, reducing memory allocation overhead.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, device: torch.device, config: Optional[CommunicationConfig] = None):
|
| 27 |
+
"""
|
| 28 |
+
Initialize the buffer manager.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
device: GPU device for buffer allocation
|
| 32 |
+
config: Communication configuration
|
| 33 |
+
"""
|
| 34 |
+
self.device = device
|
| 35 |
+
self.config = config or CommunicationConfig()
|
| 36 |
+
|
| 37 |
+
# Buffer pools: {(shape, dtype): [tensor1, tensor2, ...]}
|
| 38 |
+
self.free_buffers: Dict[BufferKey, List[torch.Tensor]] = {}
|
| 39 |
+
self.free_buffers_origin: Dict[BufferKey, List[torch.Tensor]] = {}
|
| 40 |
+
self.free_buffers_kv: Dict[BufferKey, List[torch.Tensor]] = {}
|
| 41 |
+
self.free_buffers_misc: Dict[BufferKey, List[torch.Tensor]] = {}
|
| 42 |
+
|
| 43 |
+
# Thread safety
|
| 44 |
+
self._lock = threading.Lock()
|
| 45 |
+
|
| 46 |
+
# Statistics
|
| 47 |
+
self.allocation_count = 0
|
| 48 |
+
self.reuse_count = 0
|
| 49 |
+
self.total_allocated_memory = 0
|
| 50 |
+
|
| 51 |
+
# Setup logging
|
| 52 |
+
self.logger = logging.getLogger(f"BufferManager_{device}")
|
| 53 |
+
self.logger.propagate = False
|
| 54 |
+
if not self.logger.handlers:
|
| 55 |
+
handler = logging.StreamHandler()
|
| 56 |
+
# handler.setLevel(logging.DEBUG)
|
| 57 |
+
formatter = logging.Formatter(
|
| 58 |
+
f'[BufferManager {device}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 59 |
+
)
|
| 60 |
+
handler.setFormatter(formatter)
|
| 61 |
+
self.logger.addHandler(handler)
|
| 62 |
+
# self.logger.setLevel(logging.DEBUG)
|
| 63 |
+
|
| 64 |
+
def get_buffer(self, shape: Tuple[int, ...], dtype: torch.dtype,
|
| 65 |
+
buffer_type: str = "latent") -> torch.Tensor:
|
| 66 |
+
"""
|
| 67 |
+
Get or allocate a buffer with the specified shape and dtype.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
shape: Tensor shape
|
| 71 |
+
dtype: Tensor data type
|
| 72 |
+
buffer_type: Type of buffer ("latent", "origin", "kv")
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Tensor buffer
|
| 76 |
+
"""
|
| 77 |
+
with self._lock:
|
| 78 |
+
# Select the appropriate buffer pool
|
| 79 |
+
if buffer_type == "latent":
|
| 80 |
+
buffer_pool = self.free_buffers
|
| 81 |
+
elif buffer_type == "origin":
|
| 82 |
+
buffer_pool = self.free_buffers_origin
|
| 83 |
+
elif buffer_type == "kv":
|
| 84 |
+
buffer_pool = self.free_buffers_kv
|
| 85 |
+
elif buffer_type == "misc":
|
| 86 |
+
buffer_pool = self.free_buffers_misc
|
| 87 |
+
else:
|
| 88 |
+
raise ValueError(f"Unknown buffer type: {buffer_type}")
|
| 89 |
+
|
| 90 |
+
# Try to reuse existing buffer
|
| 91 |
+
key = (tuple(shape), dtype)
|
| 92 |
+
|
| 93 |
+
if self.config.enable_buffer_reuse and key in buffer_pool and len(buffer_pool[key]) > 0:
|
| 94 |
+
buffer = buffer_pool[key].pop()
|
| 95 |
+
self.reuse_count += 1
|
| 96 |
+
self.logger.debug(f"Reused buffer of shape {shape}, dtype {dtype}, type {buffer_type}")
|
| 97 |
+
return buffer
|
| 98 |
+
|
| 99 |
+
# Allocate new buffer
|
| 100 |
+
buffer = torch.empty(shape, dtype=dtype, device=self.device)
|
| 101 |
+
self.allocation_count += 1
|
| 102 |
+
self.total_allocated_memory += buffer.numel() * buffer.element_size()
|
| 103 |
+
|
| 104 |
+
self.logger.debug(f"Allocated new buffer of shape {shape}, dtype {dtype}, type {buffer_type}")
|
| 105 |
+
return buffer
|
| 106 |
+
|
| 107 |
+
def return_buffer(self, tensor: torch.Tensor, buffer_type: str = "latent") -> None:
|
| 108 |
+
"""
|
| 109 |
+
Return a buffer to the pool for reuse.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
tensor: Tensor to return
|
| 113 |
+
buffer_type: Type of buffer ("latent", "origin", "kv")
|
| 114 |
+
"""
|
| 115 |
+
if not self.config.enable_buffer_reuse:
|
| 116 |
+
return
|
| 117 |
+
|
| 118 |
+
with self._lock:
|
| 119 |
+
# Select the appropriate buffer pool
|
| 120 |
+
if buffer_type == "latent":
|
| 121 |
+
buffer_pool = self.free_buffers
|
| 122 |
+
elif buffer_type == "origin":
|
| 123 |
+
buffer_pool = self.free_buffers_origin
|
| 124 |
+
elif buffer_type == "kv":
|
| 125 |
+
buffer_pool = self.free_buffers_kv
|
| 126 |
+
elif buffer_type == "misc":
|
| 127 |
+
buffer_pool = self.free_buffers_misc
|
| 128 |
+
else:
|
| 129 |
+
raise ValueError(f"Unknown buffer type: {buffer_type}")
|
| 130 |
+
|
| 131 |
+
key = (tuple(tensor.shape), tensor.dtype)
|
| 132 |
+
|
| 133 |
+
# Initialize pool for this shape if it doesn't exist
|
| 134 |
+
if key not in buffer_pool:
|
| 135 |
+
buffer_pool[key] = []
|
| 136 |
+
|
| 137 |
+
# Add buffer to pool if not at capacity
|
| 138 |
+
if len(buffer_pool[key]) < self.config.buffer_pool_size:
|
| 139 |
+
buffer_pool[key].append(tensor)
|
| 140 |
+
self.logger.debug(
|
| 141 |
+
f"Returned buffer of shape {tuple(tensor.shape)}, dtype {tensor.dtype}, type {buffer_type}"
|
| 142 |
+
)
|
| 143 |
+
else:
|
| 144 |
+
self.logger.debug(
|
| 145 |
+
f"Buffer pool full for shape {tuple(tensor.shape)}, dtype {tensor.dtype}, type {buffer_type}, discarding"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
def clear_buffers(self, buffer_type: Optional[str] = None) -> None:
|
| 149 |
+
"""
|
| 150 |
+
Clear buffer pools to free memory.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
buffer_type: Specific buffer type to clear, or None to clear all
|
| 154 |
+
"""
|
| 155 |
+
with self._lock:
|
| 156 |
+
if buffer_type is None:
|
| 157 |
+
# Clear all buffer pools
|
| 158 |
+
self.free_buffers.clear()
|
| 159 |
+
self.free_buffers_origin.clear()
|
| 160 |
+
self.free_buffers_kv.clear()
|
| 161 |
+
self.free_buffers_misc.clear()
|
| 162 |
+
self.logger.info("Cleared all buffer pools")
|
| 163 |
+
else:
|
| 164 |
+
# Clear specific buffer pool
|
| 165 |
+
if buffer_type == "latent":
|
| 166 |
+
self.free_buffers.clear()
|
| 167 |
+
elif buffer_type == "origin":
|
| 168 |
+
self.free_buffers_origin.clear()
|
| 169 |
+
elif buffer_type == "kv":
|
| 170 |
+
self.free_buffers_kv.clear()
|
| 171 |
+
elif buffer_type == "misc":
|
| 172 |
+
self.free_buffers_misc.clear()
|
| 173 |
+
else:
|
| 174 |
+
raise ValueError(f"Unknown buffer type: {buffer_type}")
|
| 175 |
+
self.logger.info(f"Cleared {buffer_type} buffer pool")
|
| 176 |
+
|
| 177 |
+
def get_statistics(self) -> Dict[str, any]:
|
| 178 |
+
"""
|
| 179 |
+
Get buffer manager statistics.
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
Dictionary containing statistics
|
| 183 |
+
"""
|
| 184 |
+
with self._lock:
|
| 185 |
+
total_free_buffers = sum(len(pool) for pool in self.free_buffers.values())
|
| 186 |
+
total_free_buffers_origin = sum(len(pool) for pool in self.free_buffers_origin.values())
|
| 187 |
+
total_free_buffers_kv = sum(len(pool) for pool in self.free_buffers_kv.values())
|
| 188 |
+
total_free_buffers_misc = sum(len(pool) for pool in self.free_buffers_misc.values())
|
| 189 |
+
|
| 190 |
+
return {
|
| 191 |
+
"allocation_count": self.allocation_count,
|
| 192 |
+
"reuse_count": self.reuse_count,
|
| 193 |
+
"total_allocated_memory_bytes": self.total_allocated_memory,
|
| 194 |
+
"total_free_buffers": total_free_buffers,
|
| 195 |
+
"total_free_buffers_origin": total_free_buffers_origin,
|
| 196 |
+
"total_free_buffers_kv": total_free_buffers_kv,
|
| 197 |
+
"total_free_buffers_misc": total_free_buffers_misc,
|
| 198 |
+
"reuse_rate": self.reuse_count / max(1, self.allocation_count),
|
| 199 |
+
"buffer_pool_size": self.config.buffer_pool_size,
|
| 200 |
+
"enable_buffer_reuse": self.config.enable_buffer_reuse
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
def print_statistics(self) -> None:
|
| 204 |
+
"""Print buffer manager statistics."""
|
| 205 |
+
stats = self.get_statistics()
|
| 206 |
+
self.logger.info("Buffer Manager Statistics:")
|
| 207 |
+
for key, value in stats.items():
|
| 208 |
+
self.logger.info(f" {key}: {value}")
|
| 209 |
+
|
| 210 |
+
def preallocate_buffers(self, common_shapes: List[Tuple[Tuple[int, ...], torch.dtype, str]],
|
| 211 |
+
count_per_shape: int = 5) -> None:
|
| 212 |
+
"""
|
| 213 |
+
Preallocate buffers for common shapes to reduce allocation overhead.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
common_shapes: List of (shape, dtype, buffer_type) tuples
|
| 217 |
+
count_per_shape: Number of buffers to preallocate per shape
|
| 218 |
+
"""
|
| 219 |
+
with self._lock:
|
| 220 |
+
for shape, dtype, buffer_type in common_shapes:
|
| 221 |
+
for _ in range(count_per_shape):
|
| 222 |
+
buffer = torch.empty(shape, dtype=dtype, device=self.device)
|
| 223 |
+
|
| 224 |
+
# Select the appropriate buffer pool
|
| 225 |
+
if buffer_type == "latent":
|
| 226 |
+
buffer_pool = self.free_buffers
|
| 227 |
+
elif buffer_type == "origin":
|
| 228 |
+
buffer_pool = self.free_buffers_origin
|
| 229 |
+
elif buffer_type == "kv":
|
| 230 |
+
buffer_pool = self.free_buffers_kv
|
| 231 |
+
elif buffer_type == "misc":
|
| 232 |
+
buffer_pool = self.free_buffers_misc
|
| 233 |
+
else:
|
| 234 |
+
raise ValueError(f"Unknown buffer type: {buffer_type}")
|
| 235 |
+
|
| 236 |
+
# Initialize pool for this shape if it doesn't exist
|
| 237 |
+
key = (tuple(shape), dtype)
|
| 238 |
+
if key not in buffer_pool:
|
| 239 |
+
buffer_pool[key] = []
|
| 240 |
+
|
| 241 |
+
buffer_pool[key].append(buffer)
|
| 242 |
+
self.allocation_count += 1
|
| 243 |
+
self.total_allocated_memory += buffer.numel() * buffer.element_size()
|
| 244 |
+
|
| 245 |
+
self.logger.info(f"Preallocated {len(common_shapes) * count_per_shape} buffers")
|
| 246 |
+
|
| 247 |
+
def __del__(self):
|
| 248 |
+
"""Cleanup when the buffer manager is destroyed."""
|
| 249 |
+
try:
|
| 250 |
+
self.clear_buffers()
|
| 251 |
+
except Exception:
|
| 252 |
+
pass
|
streamv2v/communication/data_containers.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data containers for communication operations.
|
| 3 |
+
|
| 4 |
+
This module defines data structures used for communication between distributed ranks.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Optional, List, Dict, Any
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class LatentData:
|
| 14 |
+
"""
|
| 15 |
+
Container for latent data and related information.
|
| 16 |
+
|
| 17 |
+
This class encapsulates all the data that needs to be transferred between ranks
|
| 18 |
+
during the inference pipeline.
|
| 19 |
+
"""
|
| 20 |
+
chunk_idx: int
|
| 21 |
+
latents: torch.Tensor
|
| 22 |
+
original_latents: torch.Tensor
|
| 23 |
+
current_start: torch.Tensor
|
| 24 |
+
current_end: torch.Tensor
|
| 25 |
+
current_step: int
|
| 26 |
+
patched_x_shape: torch.Tensor
|
| 27 |
+
|
| 28 |
+
def __post_init__(self):
|
| 29 |
+
"""Validate tensor shapes and types after initialization."""
|
| 30 |
+
if not isinstance(self.latents, torch.Tensor):
|
| 31 |
+
raise TypeError("latents must be a torch.Tensor")
|
| 32 |
+
if not isinstance(self.original_latents, torch.Tensor):
|
| 33 |
+
raise TypeError("original_latents must be a torch.Tensor")
|
| 34 |
+
if not isinstance(self.current_start, torch.Tensor):
|
| 35 |
+
raise TypeError("current_start must be a torch.Tensor")
|
| 36 |
+
if not isinstance(self.current_end, torch.Tensor):
|
| 37 |
+
raise TypeError("current_end must be a torch.Tensor")
|
| 38 |
+
if not isinstance(self.patched_x_shape, torch.Tensor):
|
| 39 |
+
raise TypeError("patched_x_shape must be a torch.Tensor")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class KVCacheData:
|
| 44 |
+
"""
|
| 45 |
+
Container for KV cache data.
|
| 46 |
+
|
| 47 |
+
This class encapsulates key-value cache information for transformer blocks.
|
| 48 |
+
"""
|
| 49 |
+
block_index: int
|
| 50 |
+
k_cache: torch.Tensor
|
| 51 |
+
v_cache: torch.Tensor
|
| 52 |
+
global_end_index: torch.Tensor
|
| 53 |
+
local_end_index: torch.Tensor
|
| 54 |
+
|
| 55 |
+
def __post_init__(self):
|
| 56 |
+
"""Validate tensor shapes and types after initialization."""
|
| 57 |
+
if not isinstance(self.k_cache, torch.Tensor):
|
| 58 |
+
raise TypeError("k_cache must be a torch.Tensor")
|
| 59 |
+
if not isinstance(self.v_cache, torch.Tensor):
|
| 60 |
+
raise TypeError("v_cache must be a torch.Tensor")
|
| 61 |
+
if not isinstance(self.global_end_index, torch.Tensor):
|
| 62 |
+
raise TypeError("global_end_index must be a torch.Tensor")
|
| 63 |
+
if not isinstance(self.local_end_index, torch.Tensor):
|
| 64 |
+
raise TypeError("local_end_index must be a torch.Tensor")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class CommunicationConfig:
|
| 69 |
+
"""
|
| 70 |
+
Configuration for communication operations.
|
| 71 |
+
|
| 72 |
+
This class holds configuration parameters for distributed communication.
|
| 73 |
+
"""
|
| 74 |
+
max_outstanding: int = 1
|
| 75 |
+
buffer_pool_size: int = 10
|
| 76 |
+
enable_buffer_reuse: bool = True
|
| 77 |
+
communication_timeout: float = 30.0
|
| 78 |
+
enable_async_communication: bool = True
|
| 79 |
+
|
| 80 |
+
def __post_init__(self):
|
| 81 |
+
"""Validate configuration parameters."""
|
| 82 |
+
if self.max_outstanding < 1:
|
| 83 |
+
raise ValueError("max_outstanding must be at least 1")
|
| 84 |
+
if self.buffer_pool_size < 1:
|
| 85 |
+
raise ValueError("buffer_pool_size must be at least 1")
|
| 86 |
+
if self.communication_timeout <= 0:
|
| 87 |
+
raise ValueError("communication_timeout must be positive")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@dataclass
|
| 91 |
+
class BlockInterval:
|
| 92 |
+
"""
|
| 93 |
+
Container for block interval information.
|
| 94 |
+
|
| 95 |
+
This class represents a block interval [start, end) for a specific rank.
|
| 96 |
+
"""
|
| 97 |
+
start: int
|
| 98 |
+
end: int
|
| 99 |
+
rank: int
|
| 100 |
+
|
| 101 |
+
def __post_init__(self):
|
| 102 |
+
"""Validate block interval parameters."""
|
| 103 |
+
if self.start < 0:
|
| 104 |
+
raise ValueError("start must be non-negative")
|
| 105 |
+
if self.end <= self.start:
|
| 106 |
+
raise ValueError("end must be greater than start")
|
| 107 |
+
if self.rank < 0:
|
| 108 |
+
raise ValueError("rank must be non-negative")
|
| 109 |
+
|
| 110 |
+
@property
|
| 111 |
+
def size(self) -> int:
|
| 112 |
+
"""Get the size of the block interval."""
|
| 113 |
+
return self.end - self.start
|
| 114 |
+
|
| 115 |
+
def contains(self, block_index: int) -> bool:
|
| 116 |
+
"""Check if the block interval contains the given block index."""
|
| 117 |
+
return self.start <= block_index < self.end
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@dataclass
|
| 121 |
+
class PerformanceMetrics:
|
| 122 |
+
"""
|
| 123 |
+
Container for performance metrics.
|
| 124 |
+
|
| 125 |
+
This class holds timing and performance information for communication operations.
|
| 126 |
+
"""
|
| 127 |
+
dit_time: float
|
| 128 |
+
total_time: float
|
| 129 |
+
communication_time: float
|
| 130 |
+
buffer_allocation_time: float
|
| 131 |
+
|
| 132 |
+
def __post_init__(self):
|
| 133 |
+
"""Validate performance metrics."""
|
| 134 |
+
if self.dit_time < 0:
|
| 135 |
+
raise ValueError("dit_time must be non-negative")
|
| 136 |
+
if self.total_time < 0:
|
| 137 |
+
raise ValueError("total_time must be non-negative")
|
| 138 |
+
if self.communication_time < 0:
|
| 139 |
+
raise ValueError("communication_time must be non-negative")
|
| 140 |
+
if self.buffer_allocation_time < 0:
|
| 141 |
+
raise ValueError("buffer_allocation_time must be non-negative")
|
| 142 |
+
|
| 143 |
+
@property
|
| 144 |
+
def efficiency(self) -> float:
|
| 145 |
+
"""Calculate communication efficiency (computation time / total time)."""
|
| 146 |
+
if self.total_time == 0:
|
| 147 |
+
return 0.0
|
| 148 |
+
return (self.total_time - self.communication_time) / self.total_time
|
streamv2v/communication/distributed_communicator.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Distributed communication abstraction layer.
|
| 3 |
+
|
| 4 |
+
This module provides a high-level interface for distributed communication operations,
|
| 5 |
+
encapsulating the low-level PyTorch distributed primitives.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from typing import List, Tuple, Optional, Any
|
| 11 |
+
import logging
|
| 12 |
+
import time
|
| 13 |
+
from .utils import CommunicationTags, get_next_rank, get_prev_rank, CommunicationTimer
|
| 14 |
+
from .data_containers import CommunicationConfig
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class DistributedCommunicator:
|
| 18 |
+
"""
|
| 19 |
+
High-level interface for distributed communication operations.
|
| 20 |
+
|
| 21 |
+
This class encapsulates all distributed communication operations, providing
|
| 22 |
+
a clean interface for sending and receiving tensors between ranks.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, rank: int, world_size: int, device: torch.device,
|
| 26 |
+
config: Optional[CommunicationConfig] = None):
|
| 27 |
+
"""
|
| 28 |
+
Initialize the distributed communicator.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
rank: Current rank
|
| 32 |
+
world_size: Total number of ranks
|
| 33 |
+
device: GPU device for communication
|
| 34 |
+
config: Communication configuration
|
| 35 |
+
"""
|
| 36 |
+
self.rank = rank
|
| 37 |
+
self.world_size = world_size
|
| 38 |
+
self.device = device
|
| 39 |
+
self.config = config or CommunicationConfig()
|
| 40 |
+
|
| 41 |
+
# Track outstanding operations
|
| 42 |
+
self.outstanding_operations: List[Any] = []
|
| 43 |
+
|
| 44 |
+
# Setup logging
|
| 45 |
+
self.logger = logging.getLogger(f"DistributedCommunicator_rank_{rank}")
|
| 46 |
+
self.logger.propagate = False
|
| 47 |
+
if not self.logger.handlers:
|
| 48 |
+
handler = logging.StreamHandler()
|
| 49 |
+
formatter = logging.Formatter(
|
| 50 |
+
f'[Rank {rank}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 51 |
+
)
|
| 52 |
+
handler.setFormatter(formatter)
|
| 53 |
+
self.logger.addHandler(handler)
|
| 54 |
+
|
| 55 |
+
# Validate distributed is initialized
|
| 56 |
+
if not dist.is_initialized():
|
| 57 |
+
raise RuntimeError("Distributed not initialized. Call init_distributed() first.")
|
| 58 |
+
|
| 59 |
+
def send_tensor_async(self, tensor: torch.Tensor, dst: int, tag: int) -> Any:
|
| 60 |
+
"""
|
| 61 |
+
Asynchronously send a tensor to the specified destination.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
tensor: Tensor to send
|
| 65 |
+
dst: Destination rank
|
| 66 |
+
tag: Communication tag
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
Work object for the send operation
|
| 70 |
+
"""
|
| 71 |
+
if tensor.device != self.device:
|
| 72 |
+
raise ValueError(f"Tensor device {tensor.device} doesn't match communicator device {self.device}")
|
| 73 |
+
|
| 74 |
+
work = dist.isend(tensor, dst=dst, tag=tag)
|
| 75 |
+
self.outstanding_operations.append(work)
|
| 76 |
+
|
| 77 |
+
self.logger.debug(f"Started async send to rank {dst} with tag {tag}, tensor shape: {tensor.shape}")
|
| 78 |
+
return work
|
| 79 |
+
|
| 80 |
+
def recv_tensor(self, src: int, tag: int, shape: Tuple[int, ...],
|
| 81 |
+
dtype: torch.dtype) -> torch.Tensor:
|
| 82 |
+
"""
|
| 83 |
+
Receive a tensor from the specified source.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
src: Source rank
|
| 87 |
+
tag: Communication tag
|
| 88 |
+
shape: Expected tensor shape
|
| 89 |
+
dtype: Expected tensor dtype
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
Received tensor
|
| 93 |
+
"""
|
| 94 |
+
tensor = torch.empty(shape, dtype=dtype, device=self.device)
|
| 95 |
+
|
| 96 |
+
with CommunicationTimer(f"recv_tensor from rank {src}", self.logger):
|
| 97 |
+
dist.recv(tensor, src=src, tag=tag)
|
| 98 |
+
|
| 99 |
+
self.logger.debug(f"Received tensor from rank {src} with tag {tag}, shape: {tensor.shape}")
|
| 100 |
+
return tensor
|
| 101 |
+
|
| 102 |
+
def send_header_and_tensor_async(self, header: torch.Tensor, tensor: torch.Tensor,
|
| 103 |
+
dst: int, tag_header: int, tag_tensor: int) -> Tuple[Any, Any]:
|
| 104 |
+
"""
|
| 105 |
+
Asynchronously send a header and tensor pair.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
header: Header tensor containing metadata
|
| 109 |
+
tensor: Data tensor
|
| 110 |
+
dst: Destination rank
|
| 111 |
+
tag_header: Tag for header
|
| 112 |
+
tag_tensor: Tag for tensor
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
Tuple of (header_work, tensor_work)
|
| 116 |
+
"""
|
| 117 |
+
if header.device != self.device or tensor.device != self.device:
|
| 118 |
+
raise ValueError("Header and tensor must be on the same device as communicator")
|
| 119 |
+
|
| 120 |
+
header_work = dist.isend(header, dst=dst, tag=tag_header)
|
| 121 |
+
tensor_work = dist.isend(tensor, dst=dst, tag=tag_tensor)
|
| 122 |
+
|
| 123 |
+
self.outstanding_operations.extend([header_work, tensor_work])
|
| 124 |
+
|
| 125 |
+
self.logger.debug(f"Started async send of header+tensor to rank {dst}, "
|
| 126 |
+
f"header shape: {header.shape}, tensor shape: {tensor.shape}")
|
| 127 |
+
return header_work, tensor_work
|
| 128 |
+
|
| 129 |
+
def recv_header_and_tensor(self, src: int, tag_header: int, tag_tensor: int, header_len: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 130 |
+
"""
|
| 131 |
+
Receive a header and tensor pair.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
src: Source rank
|
| 135 |
+
tag_header: Tag for header
|
| 136 |
+
tag_tensor: Tag for tensor
|
| 137 |
+
header_len: Length of header tensor to receive
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
Tuple of (header, tensor)
|
| 141 |
+
"""
|
| 142 |
+
with CommunicationTimer(f"recv_header_and_tensor from rank {src}", self.logger):
|
| 143 |
+
# First receive the header to get tensor shape (length can vary)
|
| 144 |
+
header = torch.empty(header_len, dtype=torch.int64, device=self.device)
|
| 145 |
+
dist.recv(header, src=src, tag=tag_header)
|
| 146 |
+
|
| 147 |
+
# Parse header to get tensor shape
|
| 148 |
+
chunk_idx, shape = self._parse_header(header)
|
| 149 |
+
|
| 150 |
+
# Receive the tensor
|
| 151 |
+
tensor = torch.empty(shape, dtype=torch.bfloat16, device=self.device)
|
| 152 |
+
dist.recv(tensor, src=src, tag=tag_tensor)
|
| 153 |
+
|
| 154 |
+
self.logger.debug(f"Received header+tensor from rank {src}, "
|
| 155 |
+
f"header: {header.tolist()}, tensor shape: {tensor.shape}")
|
| 156 |
+
return header, tensor
|
| 157 |
+
|
| 158 |
+
def send_latent_data_async(self, chunk_idx: int, latents: torch.Tensor,
|
| 159 |
+
original_latents: torch.Tensor, patched_x_shape: torch.Tensor,
|
| 160 |
+
current_start: torch.Tensor, current_end: torch.Tensor,
|
| 161 |
+
current_step: int) -> List[Any]:
|
| 162 |
+
"""
|
| 163 |
+
Asynchronously send all latent data components.
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
chunk_idx: Chunk index
|
| 167 |
+
latents: Latent tensor
|
| 168 |
+
original_latents: Original latent tensor
|
| 169 |
+
patched_x_shape: Patched x shape tensor
|
| 170 |
+
current_start: Current start indices
|
| 171 |
+
current_end: Current end indices
|
| 172 |
+
current_step: Current step
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
List of work objects for all send operations
|
| 176 |
+
"""
|
| 177 |
+
dst = get_next_rank(self.rank, self.world_size)
|
| 178 |
+
work_objects = []
|
| 179 |
+
|
| 180 |
+
# Create headers
|
| 181 |
+
latent_header = self._create_header(chunk_idx, latents.shape)
|
| 182 |
+
origin_header = self._create_header(chunk_idx, original_latents.shape)
|
| 183 |
+
|
| 184 |
+
# Create start/end/step tensor
|
| 185 |
+
start_end_step = torch.cat([
|
| 186 |
+
current_start,
|
| 187 |
+
current_end,
|
| 188 |
+
torch.tensor([current_step], dtype=torch.int64, device=self.device)
|
| 189 |
+
], dim=0)
|
| 190 |
+
|
| 191 |
+
# Send all components asynchronously
|
| 192 |
+
work_objects.extend(self.send_header_and_tensor_async(
|
| 193 |
+
latent_header, latents, dst, CommunicationTags.LATENT_HDR, CommunicationTags.LATENT_PAY
|
| 194 |
+
))
|
| 195 |
+
|
| 196 |
+
work_objects.extend(self.send_header_and_tensor_async(
|
| 197 |
+
origin_header, original_latents, dst,
|
| 198 |
+
CommunicationTags.LATENT_ORIGIN_HDR, CommunicationTags.LATENT_ORIGIN_PAY
|
| 199 |
+
))
|
| 200 |
+
|
| 201 |
+
work_objects.append(self.send_tensor_async(
|
| 202 |
+
patched_x_shape, dst, CommunicationTags.PATCHED_X_SHAPE
|
| 203 |
+
))
|
| 204 |
+
|
| 205 |
+
work_objects.append(self.send_tensor_async(
|
| 206 |
+
start_end_step, dst, CommunicationTags.START_END_STEP
|
| 207 |
+
))
|
| 208 |
+
|
| 209 |
+
self.logger.debug(f"Started async send of latent data to rank {dst}, chunk_idx: {chunk_idx}")
|
| 210 |
+
return work_objects
|
| 211 |
+
|
| 212 |
+
def recv_latent_data_async(self, num_steps: int, buffer_manager) -> Tuple[int, torch.Tensor, torch.Tensor,
|
| 213 |
+
torch.Tensor, torch.Tensor, int, torch.Tensor]:
|
| 214 |
+
"""
|
| 215 |
+
Asynchronously receive all latent data components.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
num_steps: Number of denoising steps
|
| 219 |
+
buffer_manager: Buffer manager for tensor allocation
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
Tuple of (chunk_idx, latents, original_latents, current_start, current_end, current_step, patched_x_shape)
|
| 223 |
+
"""
|
| 224 |
+
src = get_prev_rank(self.rank, self.world_size)
|
| 225 |
+
|
| 226 |
+
with CommunicationTimer(f"recv_latent_data_async from rank {src}", self.logger):
|
| 227 |
+
# Receive latent header (length 4): [i, bsz, slen, cch]
|
| 228 |
+
latent_header = buffer_manager.get_buffer((4,), torch.int64, "misc")
|
| 229 |
+
dist.recv(latent_header, src=src, tag=CommunicationTags.LATENT_HDR)
|
| 230 |
+
chunk_idx, latent_shape = self._parse_header(latent_header)
|
| 231 |
+
# header no longer needed
|
| 232 |
+
buffer_manager.return_buffer(latent_header, "misc")
|
| 233 |
+
# Allocate or reuse buffer for latents: shape (bsz, slen, cch)
|
| 234 |
+
latents = buffer_manager.get_buffer(tuple(latent_shape), torch.bfloat16, "latent")
|
| 235 |
+
dist.recv(latents, src=src, tag=CommunicationTags.LATENT_PAY)
|
| 236 |
+
|
| 237 |
+
# Receive original latent header (length 6): [i, bsz, cch, tlen, hh, ww]
|
| 238 |
+
origin_header = buffer_manager.get_buffer((6,), torch.int64, "misc")
|
| 239 |
+
dist.recv(origin_header, src=src, tag=CommunicationTags.LATENT_ORIGIN_HDR)
|
| 240 |
+
_, origin_shape = self._parse_header(origin_header)
|
| 241 |
+
# header no longer needed
|
| 242 |
+
buffer_manager.return_buffer(origin_header, "misc")
|
| 243 |
+
# Allocate or reuse buffer for original latents: shape (bsz, cch, tlen, hh, ww)
|
| 244 |
+
original_latents = buffer_manager.get_buffer(tuple(origin_shape), torch.bfloat16, "origin")
|
| 245 |
+
dist.recv(original_latents, src=src, tag=CommunicationTags.LATENT_ORIGIN_PAY)
|
| 246 |
+
|
| 247 |
+
# Receive patched_x_shape (length 5, int64)
|
| 248 |
+
patched_x_shape = buffer_manager.get_buffer((5,), torch.int64, "misc")
|
| 249 |
+
dist.recv(patched_x_shape, src=src, tag=CommunicationTags.PATCHED_X_SHAPE)
|
| 250 |
+
|
| 251 |
+
# Receive start_end_step (length 2*num_steps+1, int64)
|
| 252 |
+
start_end_step = buffer_manager.get_buffer((2 * num_steps + 1,), torch.int64, "misc")
|
| 253 |
+
dist.recv(start_end_step, src=src, tag=CommunicationTags.START_END_STEP)
|
| 254 |
+
|
| 255 |
+
# Parse start/end/step into dedicated misc buffers, then release the combined vector
|
| 256 |
+
current_start = buffer_manager.get_buffer((num_steps,), torch.int64, "misc")
|
| 257 |
+
current_end = buffer_manager.get_buffer((num_steps,), torch.int64, "misc")
|
| 258 |
+
current_start.copy_(start_end_step[:num_steps])
|
| 259 |
+
current_end.copy_(start_end_step[num_steps:-1])
|
| 260 |
+
current_step = int(start_end_step[-1].item())
|
| 261 |
+
# Release the temporary combined buffer
|
| 262 |
+
buffer_manager.return_buffer(start_end_step, "misc")
|
| 263 |
+
|
| 264 |
+
self.logger.debug(f"Received latent data from rank {src}, chunk_idx: {chunk_idx}")
|
| 265 |
+
return chunk_idx, latents, original_latents, current_start, current_end, current_step, patched_x_shape
|
| 266 |
+
|
| 267 |
+
def send_prompt_async(self, prompt: str, device: torch.device) -> List[Any]:
|
| 268 |
+
work_objects = []
|
| 269 |
+
dst = get_next_rank(self.rank, self.world_size)
|
| 270 |
+
|
| 271 |
+
# Encode to bytes
|
| 272 |
+
encoded = prompt.encode("utf-8")
|
| 273 |
+
data = torch.ByteTensor(list(encoded)).to(device)
|
| 274 |
+
|
| 275 |
+
# Send length first
|
| 276 |
+
length = torch.tensor([len(data)], dtype=torch.int64, device=data.device)
|
| 277 |
+
work_objects.append(dist.isend(length, dst=dst, tag=CommunicationTags.UPDATED_PROMPT_LENGTH))
|
| 278 |
+
|
| 279 |
+
# Then send the content
|
| 280 |
+
work_objects.append(dist.isend(data, dst=dst, tag=CommunicationTags.UPDATED_PROMPT))
|
| 281 |
+
|
| 282 |
+
return work_objects
|
| 283 |
+
|
| 284 |
+
def recv_prompt_async(self) -> str:
|
| 285 |
+
src = get_prev_rank(self.rank, self.world_size)
|
| 286 |
+
|
| 287 |
+
# Receive length first
|
| 288 |
+
length = torch.empty(1, dtype=torch.int64, device=self.device)
|
| 289 |
+
dist.recv(length, src=src, tag=CommunicationTags.UPDATED_PROMPT_LENGTH)
|
| 290 |
+
|
| 291 |
+
# Then receive the content
|
| 292 |
+
prompt = torch.empty(length.item(), dtype=torch.uint8, device=self.device)
|
| 293 |
+
dist.recv(prompt, src=src, tag=CommunicationTags.UPDATED_PROMPT)
|
| 294 |
+
|
| 295 |
+
return bytes(prompt.cpu().tolist()).decode("utf-8")
|
| 296 |
+
|
| 297 |
+
def broadcast_tensor(self, tensor: torch.Tensor, src: int) -> None:
|
| 298 |
+
"""
|
| 299 |
+
Broadcast a tensor from source to all ranks.
|
| 300 |
+
|
| 301 |
+
Args:
|
| 302 |
+
tensor: Tensor to broadcast
|
| 303 |
+
src: Source rank
|
| 304 |
+
"""
|
| 305 |
+
with CommunicationTimer(f"broadcast_tensor from rank {src}", self.logger):
|
| 306 |
+
dist.broadcast(tensor, src=src)
|
| 307 |
+
|
| 308 |
+
self.logger.debug(f"Broadcasted tensor from rank {src}, shape: {tensor.shape}")
|
| 309 |
+
|
| 310 |
+
def all_gather_tensors(self, tensor: torch.Tensor) -> List[torch.Tensor]:
|
| 311 |
+
"""
|
| 312 |
+
Gather tensors from all ranks.
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
tensor: Local tensor to gather
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
List of tensors from all ranks
|
| 319 |
+
"""
|
| 320 |
+
with CommunicationTimer("all_gather_tensors", self.logger):
|
| 321 |
+
gather_list = [torch.zeros_like(tensor) for _ in range(self.world_size)]
|
| 322 |
+
dist.all_gather(gather_list, tensor)
|
| 323 |
+
|
| 324 |
+
self.logger.debug(f"Gathered tensors from all ranks, local shape: {tensor.shape}")
|
| 325 |
+
return gather_list
|
| 326 |
+
|
| 327 |
+
def wait_for_outstanding(self, max_outstanding: Optional[int] = None) -> None:
|
| 328 |
+
"""
|
| 329 |
+
Wait for outstanding operations to complete.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
max_outstanding: Maximum number of outstanding operations to keep
|
| 333 |
+
"""
|
| 334 |
+
max_outstanding = max_outstanding or self.config.max_outstanding
|
| 335 |
+
|
| 336 |
+
while len(self.outstanding_operations) >= max_outstanding:
|
| 337 |
+
if not self.outstanding_operations:
|
| 338 |
+
break
|
| 339 |
+
|
| 340 |
+
# Wait for the oldest operation
|
| 341 |
+
oldest_operations = self.outstanding_operations.pop(0)
|
| 342 |
+
|
| 343 |
+
# Handle both single work objects and lists of work objects
|
| 344 |
+
if isinstance(oldest_operations, (list, tuple)):
|
| 345 |
+
for work in oldest_operations:
|
| 346 |
+
try:
|
| 347 |
+
work.wait()
|
| 348 |
+
except Exception as e:
|
| 349 |
+
self.logger.error(f"Error waiting for outstanding operation: {e}")
|
| 350 |
+
raise
|
| 351 |
+
else:
|
| 352 |
+
try:
|
| 353 |
+
oldest_operations.wait()
|
| 354 |
+
except Exception as e:
|
| 355 |
+
self.logger.error(f"Error waiting for outstanding operation: {e}")
|
| 356 |
+
raise
|
| 357 |
+
|
| 358 |
+
self.logger.debug(f"Outstanding operations: {len(self.outstanding_operations)}")
|
| 359 |
+
|
| 360 |
+
def barrier(self) -> None:
|
| 361 |
+
"""Synchronize all ranks."""
|
| 362 |
+
with CommunicationTimer("barrier", self.logger):
|
| 363 |
+
dist.barrier()
|
| 364 |
+
|
| 365 |
+
def _create_header(self, chunk_idx: int, shape: Tuple[int, ...]) -> torch.Tensor:
|
| 366 |
+
"""Create a header tensor for communication."""
|
| 367 |
+
header_data = [chunk_idx] + list(shape)
|
| 368 |
+
return torch.tensor(header_data, dtype=torch.int64, device=self.device)
|
| 369 |
+
|
| 370 |
+
def _parse_header(self, header: torch.Tensor) -> Tuple[int, Tuple[int, ...]]:
|
| 371 |
+
"""Parse a header tensor to extract metadata."""
|
| 372 |
+
header_list = header.tolist()
|
| 373 |
+
chunk_idx = int(header_list[0])
|
| 374 |
+
shape = tuple(int(x) for x in header_list[1:])
|
| 375 |
+
return chunk_idx, shape
|
| 376 |
+
|
| 377 |
+
def get_statistics(self) -> dict:
|
| 378 |
+
"""Get communication statistics."""
|
| 379 |
+
return {
|
| 380 |
+
"rank": self.rank,
|
| 381 |
+
"world_size": self.world_size,
|
| 382 |
+
"outstanding_operations": len(self.outstanding_operations),
|
| 383 |
+
"max_outstanding": self.config.max_outstanding,
|
| 384 |
+
"device": str(self.device)
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
def print_statistics(self) -> None:
|
| 388 |
+
"""Print communication statistics."""
|
| 389 |
+
stats = self.get_statistics()
|
| 390 |
+
self.logger.info("Distributed Communicator Statistics:")
|
| 391 |
+
for key, value in stats.items():
|
| 392 |
+
self.logger.info(f" {key}: {value}")
|
streamv2v/communication/kv_cache_manager.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KV Cache management for distributed inference.
|
| 3 |
+
|
| 4 |
+
This module provides functionality for managing and rebalancing KV caches
|
| 5 |
+
across distributed ranks during inference.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from typing import List, Dict, Tuple, Optional
|
| 11 |
+
import logging
|
| 12 |
+
from .utils import CommunicationTags, CommunicationTimer
|
| 13 |
+
from .data_containers import KVCacheData, BlockInterval
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class KVCacheManager:
|
| 17 |
+
"""
|
| 18 |
+
Manages KV cache operations for distributed inference.
|
| 19 |
+
|
| 20 |
+
This class handles KV cache broadcasting, rebalancing, and ownership
|
| 21 |
+
management across distributed ranks.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, pipeline, device: torch.device):
|
| 25 |
+
"""
|
| 26 |
+
Initialize the KV cache manager.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
pipeline: The inference pipeline containing KV caches
|
| 30 |
+
device: GPU device for operations
|
| 31 |
+
"""
|
| 32 |
+
self.pipeline = pipeline
|
| 33 |
+
self.device = device
|
| 34 |
+
self.frame_seq_length = pipeline.frame_seq_length
|
| 35 |
+
self.time_step_length = len(pipeline.denoising_step_list)
|
| 36 |
+
|
| 37 |
+
# Setup logging
|
| 38 |
+
self.logger = logging.getLogger(f"KVCacheManager_{device}")
|
| 39 |
+
self.logger.propagate = False
|
| 40 |
+
if not self.logger.handlers:
|
| 41 |
+
handler = logging.StreamHandler()
|
| 42 |
+
formatter = logging.Formatter(
|
| 43 |
+
f'[KVCacheManager {device}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 44 |
+
)
|
| 45 |
+
handler.setFormatter(formatter)
|
| 46 |
+
self.logger.addHandler(handler)
|
| 47 |
+
|
| 48 |
+
def broadcast_kv_blocks(self, block_indices: List[int], donor_rank: int) -> None:
|
| 49 |
+
"""
|
| 50 |
+
Broadcast kv_cache1 entries for the specified block indices from donor_rank to all ranks.
|
| 51 |
+
|
| 52 |
+
This ensures the receiver rank has the up-to-date KV cache when ownership moves.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
block_indices: List of block indices to broadcast
|
| 56 |
+
donor_rank: Rank that owns the KV cache data
|
| 57 |
+
"""
|
| 58 |
+
if len(block_indices) == 0:
|
| 59 |
+
return
|
| 60 |
+
|
| 61 |
+
rank = dist.get_rank()
|
| 62 |
+
|
| 63 |
+
with CommunicationTimer(f"broadcast_kv_blocks from rank {donor_rank}", self.logger):
|
| 64 |
+
for bi in block_indices:
|
| 65 |
+
# Broadcast key cache
|
| 66 |
+
if self.pipeline.kv_cache1[bi]['k'].device != self.device:
|
| 67 |
+
self.pipeline.kv_cache1[bi]['k'] = self.pipeline.kv_cache1[bi]['k'].to(self.device)
|
| 68 |
+
self.pipeline.kv_cache1[bi]['v'] = self.pipeline.kv_cache1[bi]['v'].to(self.device)
|
| 69 |
+
|
| 70 |
+
dist.barrier()
|
| 71 |
+
|
| 72 |
+
dist.broadcast(self.pipeline.kv_cache1[bi]['k'], src=donor_rank)
|
| 73 |
+
# Broadcast value cache
|
| 74 |
+
dist.broadcast(self.pipeline.kv_cache1[bi]['v'], src=donor_rank)
|
| 75 |
+
# Broadcast global end index
|
| 76 |
+
dist.broadcast(self.pipeline.kv_cache1[bi]['global_end_index'], src=donor_rank)
|
| 77 |
+
# Broadcast local end index
|
| 78 |
+
dist.broadcast(self.pipeline.kv_cache1[bi]['local_end_index'], src=donor_rank)
|
| 79 |
+
|
| 80 |
+
# Adjust global_end_index for the receiving rank
|
| 81 |
+
if donor_rank > rank:
|
| 82 |
+
self.pipeline.kv_cache1[bi]['global_end_index'] += self.frame_seq_length * (donor_rank - rank) * self.time_step_length
|
| 83 |
+
|
| 84 |
+
self.logger.debug(f"Broadcasted KV cache for blocks {block_indices} from rank {donor_rank}")
|
| 85 |
+
|
| 86 |
+
def compute_block_owners(self, block_intervals: torch.Tensor, total_blocks: int) -> torch.Tensor:
|
| 87 |
+
"""
|
| 88 |
+
Given block intervals in [start, end) format for all ranks, return a tensor
|
| 89 |
+
where each entry is the owner rank of that block index.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
block_intervals: Block intervals for all ranks [world_size, 2]
|
| 93 |
+
total_blocks: Total number of blocks
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
Tensor of length total_blocks with owner ranks
|
| 97 |
+
"""
|
| 98 |
+
world_size = block_intervals.shape[0]
|
| 99 |
+
owners = torch.full((total_blocks,), -1, dtype=torch.int64, device=block_intervals.device)
|
| 100 |
+
|
| 101 |
+
for r in range(world_size):
|
| 102 |
+
s = int(block_intervals[r, 0].item())
|
| 103 |
+
e = int(block_intervals[r, 1].item())
|
| 104 |
+
if e > s:
|
| 105 |
+
owners[s:e] = r
|
| 106 |
+
|
| 107 |
+
self.logger.debug(f"Computed block owners: {owners.tolist()}")
|
| 108 |
+
return owners
|
| 109 |
+
|
| 110 |
+
def rebalance_kv_cache_by_diff(self, old_block_intervals: torch.Tensor,
|
| 111 |
+
new_block_intervals: torch.Tensor, total_blocks: int) -> None:
|
| 112 |
+
"""
|
| 113 |
+
Compare ownership from old to new intervals and broadcast KV cache for blocks whose owner changes.
|
| 114 |
+
|
| 115 |
+
For each moved block i, use the previous owner's rank as src to broadcast
|
| 116 |
+
pipeline.kv_cache1[i]['k'/'v'/...] to all ranks so the new owner has the correct state.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
old_block_intervals: Previous block intervals [world_size, 2]
|
| 120 |
+
new_block_intervals: New block intervals [world_size, 2]
|
| 121 |
+
total_blocks: Total number of blocks
|
| 122 |
+
"""
|
| 123 |
+
with CommunicationTimer("rebalance_kv_cache_by_diff", self.logger):
|
| 124 |
+
old_owners = self.compute_block_owners(old_block_intervals, total_blocks)
|
| 125 |
+
new_owners = self.compute_block_owners(new_block_intervals, total_blocks)
|
| 126 |
+
|
| 127 |
+
# Find blocks that changed ownership
|
| 128 |
+
moved_by_src = {}
|
| 129 |
+
for i in range(total_blocks):
|
| 130 |
+
o = int(old_owners[i].item())
|
| 131 |
+
n = int(new_owners[i].item())
|
| 132 |
+
if o != n and o >= 0:
|
| 133 |
+
if o not in moved_by_src:
|
| 134 |
+
moved_by_src[o] = []
|
| 135 |
+
moved_by_src[o].append(i)
|
| 136 |
+
|
| 137 |
+
# Synchronize before broadcasting
|
| 138 |
+
dist.barrier()
|
| 139 |
+
|
| 140 |
+
# Broadcast per donor rank (can batch multiple blocks per src)
|
| 141 |
+
for src, blocks in moved_by_src.items():
|
| 142 |
+
self.broadcast_kv_blocks(blocks, donor_rank=src)
|
| 143 |
+
|
| 144 |
+
self.logger.info(f"Rebalanced KV cache: {len(moved_by_src)} ranks had ownership changes")
|
| 145 |
+
|
| 146 |
+
def get_kv_cache_statistics(self, block_intervals: torch.Tensor, total_blocks: int) -> Dict[str, any]:
|
| 147 |
+
"""
|
| 148 |
+
Get statistics about KV cache distribution.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
block_intervals: Current block intervals [world_size, 2]
|
| 152 |
+
total_blocks: Total number of blocks
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
Dictionary containing KV cache statistics
|
| 156 |
+
"""
|
| 157 |
+
owners = self.compute_block_owners(block_intervals, total_blocks)
|
| 158 |
+
|
| 159 |
+
# Count blocks per rank
|
| 160 |
+
block_counts = {}
|
| 161 |
+
for rank in range(block_intervals.shape[0]):
|
| 162 |
+
block_counts[rank] = int((owners == rank).sum().item())
|
| 163 |
+
|
| 164 |
+
# Calculate memory usage per rank (approximate)
|
| 165 |
+
memory_per_block = 0
|
| 166 |
+
if hasattr(self.pipeline, 'kv_cache1') and len(self.pipeline.kv_cache1) > 0:
|
| 167 |
+
# Estimate memory per block based on first block
|
| 168 |
+
first_block = self.pipeline.kv_cache1[0]
|
| 169 |
+
if 'k' in first_block and 'v' in first_block:
|
| 170 |
+
k_memory = first_block['k'].numel() * first_block['k'].element_size()
|
| 171 |
+
v_memory = first_block['v'].numel() * first_block['v'].element_size()
|
| 172 |
+
memory_per_block = k_memory + v_memory
|
| 173 |
+
|
| 174 |
+
memory_usage = {rank: block_counts[rank] * memory_per_block for rank in block_counts}
|
| 175 |
+
|
| 176 |
+
return {
|
| 177 |
+
"block_counts": block_counts,
|
| 178 |
+
"memory_usage_bytes": memory_usage,
|
| 179 |
+
"total_blocks": total_blocks,
|
| 180 |
+
"memory_per_block_bytes": memory_per_block,
|
| 181 |
+
"frame_seq_length": self.frame_seq_length
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
def print_kv_cache_statistics(self, block_intervals: torch.Tensor, total_blocks: int) -> None:
|
| 185 |
+
"""
|
| 186 |
+
Print KV cache statistics.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
block_intervals: Current block intervals [world_size, 2]
|
| 190 |
+
total_blocks: Total number of blocks
|
| 191 |
+
"""
|
| 192 |
+
stats = self.get_kv_cache_statistics(block_intervals, total_blocks)
|
| 193 |
+
|
| 194 |
+
self.logger.info("KV Cache Statistics:")
|
| 195 |
+
self.logger.info(f" Total blocks: {stats['total_blocks']}")
|
| 196 |
+
self.logger.info(f" Memory per block: {stats['memory_per_block_bytes']} bytes")
|
| 197 |
+
self.logger.info(f" Frame sequence length: {stats['frame_seq_length']}")
|
| 198 |
+
|
| 199 |
+
self.logger.info(" Block distribution:")
|
| 200 |
+
for rank, count in stats['block_counts'].items():
|
| 201 |
+
memory_mb = stats['memory_usage_bytes'][rank] / (1024 * 1024)
|
| 202 |
+
self.logger.info(f" Rank {rank}: {count} blocks, {memory_mb:.2f} MB")
|
| 203 |
+
|
| 204 |
+
def validate_kv_cache_consistency(self, block_intervals: torch.Tensor, total_blocks: int) -> bool:
|
| 205 |
+
"""
|
| 206 |
+
Validate that KV cache ownership is consistent with block intervals.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
block_intervals: Current block intervals [world_size, 2]
|
| 210 |
+
total_blocks: Total number of blocks
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
True if consistent, False otherwise
|
| 214 |
+
"""
|
| 215 |
+
owners = self.compute_block_owners(block_intervals, total_blocks)
|
| 216 |
+
|
| 217 |
+
# Check that all blocks have owners
|
| 218 |
+
unowned_blocks = (owners == -1).sum().item()
|
| 219 |
+
if unowned_blocks > 0:
|
| 220 |
+
self.logger.error(f"Found {unowned_blocks} unowned blocks")
|
| 221 |
+
return False
|
| 222 |
+
|
| 223 |
+
# Check that block intervals are contiguous and non-overlapping
|
| 224 |
+
for rank in range(block_intervals.shape[0]):
|
| 225 |
+
start = int(block_intervals[rank, 0].item())
|
| 226 |
+
end = int(block_intervals[rank, 1].item())
|
| 227 |
+
|
| 228 |
+
if start < 0 or end > total_blocks or start >= end:
|
| 229 |
+
self.logger.error(f"Invalid block interval for rank {rank}: [{start}, {end})")
|
| 230 |
+
return False
|
| 231 |
+
|
| 232 |
+
# Check that all blocks in this interval are owned by this rank
|
| 233 |
+
for block_idx in range(start, end):
|
| 234 |
+
if int(owners[block_idx].item()) != rank:
|
| 235 |
+
self.logger.error(f"Block {block_idx} not owned by rank {rank}")
|
| 236 |
+
return False
|
| 237 |
+
|
| 238 |
+
self.logger.debug("KV cache consistency validation passed")
|
| 239 |
+
return True
|
| 240 |
+
|
| 241 |
+
def cleanup_kv_cache(self, block_intervals: torch.Tensor, total_blocks: int) -> None:
|
| 242 |
+
"""
|
| 243 |
+
Clean up KV cache for blocks not owned by current rank.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
block_intervals: Current block intervals [world_size, 2]
|
| 247 |
+
total_blocks: Total number of blocks
|
| 248 |
+
"""
|
| 249 |
+
rank = dist.get_rank()
|
| 250 |
+
owners = self.compute_block_owners(block_intervals, total_blocks)
|
| 251 |
+
|
| 252 |
+
cleaned_blocks = 0
|
| 253 |
+
for block_idx in range(total_blocks):
|
| 254 |
+
if int(owners[block_idx].item()) != rank:
|
| 255 |
+
# Clear KV cache for blocks not owned by this rank
|
| 256 |
+
if hasattr(self.pipeline, 'kv_cache1') and block_idx < len(self.pipeline.kv_cache1):
|
| 257 |
+
if 'k' in self.pipeline.kv_cache1[block_idx]:
|
| 258 |
+
self.pipeline.kv_cache1[block_idx]['k'].zero_()
|
| 259 |
+
if 'v' in self.pipeline.kv_cache1[block_idx]:
|
| 260 |
+
self.pipeline.kv_cache1[block_idx]['v'].zero_()
|
| 261 |
+
cleaned_blocks += 1
|
| 262 |
+
|
| 263 |
+
self.logger.info(f"Cleaned up KV cache for {cleaned_blocks} blocks not owned by rank {rank}")
|
streamv2v/communication/model_data_transfer.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model data transfer abstraction layer.
|
| 3 |
+
|
| 4 |
+
This module provides high-level interfaces for transferring model data
|
| 5 |
+
between distributed ranks during inference.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from typing import List, Tuple, Optional, Any
|
| 10 |
+
import logging
|
| 11 |
+
from .distributed_communicator import DistributedCommunicator
|
| 12 |
+
from .buffer_manager import BufferManager
|
| 13 |
+
from .kv_cache_manager import KVCacheManager
|
| 14 |
+
from .data_containers import LatentData, CommunicationConfig, PerformanceMetrics
|
| 15 |
+
from .utils import CommunicationTimer
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ModelDataTransfer:
|
| 19 |
+
"""
|
| 20 |
+
High-level interface for model data transfer operations.
|
| 21 |
+
|
| 22 |
+
This class encapsulates all model-related data transfer operations,
|
| 23 |
+
providing a clean interface for sending and receiving latent data,
|
| 24 |
+
KV caches, and other model state between ranks.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, communicator: DistributedCommunicator,
|
| 28 |
+
buffer_manager: BufferManager,
|
| 29 |
+
kv_cache_manager: Optional[KVCacheManager] = None,
|
| 30 |
+
config: Optional[CommunicationConfig] = None):
|
| 31 |
+
"""
|
| 32 |
+
Initialize the model data transfer manager.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
communicator: Distributed communicator instance
|
| 36 |
+
buffer_manager: Buffer manager for tensor allocation
|
| 37 |
+
kv_cache_manager: KV cache manager (optional)
|
| 38 |
+
config: Communication configuration
|
| 39 |
+
"""
|
| 40 |
+
self.comm = communicator
|
| 41 |
+
self.buffer_mgr = buffer_manager
|
| 42 |
+
self.kv_cache_mgr = kv_cache_manager
|
| 43 |
+
self.config = config or CommunicationConfig()
|
| 44 |
+
|
| 45 |
+
# Setup logging
|
| 46 |
+
self.logger = logging.getLogger(f"ModelDataTransfer_rank_{communicator.rank}")
|
| 47 |
+
self.logger.propagate = False
|
| 48 |
+
if not self.logger.handlers:
|
| 49 |
+
handler = logging.StreamHandler()
|
| 50 |
+
formatter = logging.Formatter(
|
| 51 |
+
f'[Rank {communicator.rank}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 52 |
+
)
|
| 53 |
+
handler.setFormatter(formatter)
|
| 54 |
+
self.logger.addHandler(handler)
|
| 55 |
+
|
| 56 |
+
# Performance tracking
|
| 57 |
+
self.transfer_count = 0
|
| 58 |
+
self.total_transfer_time = 0.0
|
| 59 |
+
|
| 60 |
+
def send_latent_data_async(self, chunk_idx: int, latents: torch.Tensor,
|
| 61 |
+
original_latents: torch.Tensor, patched_x_shape: torch.Tensor,
|
| 62 |
+
current_start: torch.Tensor, current_end: torch.Tensor,
|
| 63 |
+
current_step: int) -> List[Any]:
|
| 64 |
+
"""
|
| 65 |
+
Asynchronously send latent data to the next rank.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
chunk_idx: Chunk index
|
| 69 |
+
latents: Latent tensor
|
| 70 |
+
original_latents: Original latent tensor
|
| 71 |
+
patched_x_shape: Patched x shape tensor
|
| 72 |
+
current_start: Current start indices
|
| 73 |
+
current_end: Current end indices
|
| 74 |
+
current_step: Current step
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
List of work objects for all send operations
|
| 78 |
+
"""
|
| 79 |
+
with CommunicationTimer(f"send_latent_data_async chunk_{chunk_idx}", self.logger):
|
| 80 |
+
work_objects = self.comm.send_latent_data_async(
|
| 81 |
+
chunk_idx=chunk_idx,
|
| 82 |
+
latents=latents,
|
| 83 |
+
original_latents=original_latents,
|
| 84 |
+
patched_x_shape=patched_x_shape,
|
| 85 |
+
current_start=current_start,
|
| 86 |
+
current_end=current_end,
|
| 87 |
+
current_step=current_step
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
self.transfer_count += 1
|
| 91 |
+
self.logger.debug(f"Sent latent data for chunk {chunk_idx}")
|
| 92 |
+
return work_objects
|
| 93 |
+
|
| 94 |
+
def receive_latent_data_async(self, num_steps: int) -> LatentData:
|
| 95 |
+
"""
|
| 96 |
+
Asynchronously receive latent data from the previous rank.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
num_steps: Number of denoising steps
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
LatentData object containing all received data
|
| 103 |
+
"""
|
| 104 |
+
with CommunicationTimer("receive_latent_data_async", self.logger):
|
| 105 |
+
chunk_idx, latents, original_latents, current_start, current_end, current_step, patched_x_shape = \
|
| 106 |
+
self.comm.recv_latent_data_async(num_steps, self.buffer_mgr)
|
| 107 |
+
|
| 108 |
+
self.transfer_count += 1
|
| 109 |
+
self.logger.debug(f"Received latent data for chunk {chunk_idx}")
|
| 110 |
+
|
| 111 |
+
return LatentData(
|
| 112 |
+
chunk_idx=chunk_idx,
|
| 113 |
+
latents=latents,
|
| 114 |
+
original_latents=original_latents,
|
| 115 |
+
current_start=current_start,
|
| 116 |
+
current_end=current_end,
|
| 117 |
+
current_step=current_step,
|
| 118 |
+
patched_x_shape=patched_x_shape
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
def release_latent_data(self, latent_data: Optional[LatentData]) -> None:
|
| 122 |
+
"""Return received latent-data buffers to the buffer pool."""
|
| 123 |
+
if latent_data is None or self.buffer_mgr is None:
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
self.buffer_mgr.return_buffer(latent_data.latents, "latent")
|
| 127 |
+
self.buffer_mgr.return_buffer(latent_data.original_latents, "origin")
|
| 128 |
+
self.buffer_mgr.return_buffer(latent_data.patched_x_shape, "misc")
|
| 129 |
+
self.buffer_mgr.return_buffer(latent_data.current_start, "misc")
|
| 130 |
+
self.buffer_mgr.return_buffer(latent_data.current_end, "misc")
|
| 131 |
+
|
| 132 |
+
def send_prompt_async(self, prompt: str, device: torch.device) -> List[Any]:
|
| 133 |
+
return self.comm.send_prompt_async(prompt, device)
|
| 134 |
+
|
| 135 |
+
def recv_prompt_async(self) -> str:
|
| 136 |
+
return self.comm.recv_prompt_async()
|
| 137 |
+
|
| 138 |
+
def send_kv_cache_blocks(self, block_indices: List[int], donor_rank: int) -> None:
|
| 139 |
+
"""
|
| 140 |
+
Send KV cache blocks to all ranks.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
block_indices: List of block indices to send
|
| 144 |
+
donor_rank: Rank that owns the KV cache data
|
| 145 |
+
"""
|
| 146 |
+
if self.kv_cache_mgr is None:
|
| 147 |
+
raise RuntimeError("KV cache manager not initialized")
|
| 148 |
+
|
| 149 |
+
with CommunicationTimer(f"send_kv_cache_blocks {len(block_indices)} blocks", self.logger):
|
| 150 |
+
self.kv_cache_mgr.broadcast_kv_blocks(block_indices, donor_rank)
|
| 151 |
+
|
| 152 |
+
self.logger.debug(f"Sent KV cache blocks {block_indices} from rank {donor_rank}")
|
| 153 |
+
|
| 154 |
+
def rebalance_kv_cache(self, old_intervals: torch.Tensor,
|
| 155 |
+
new_intervals: torch.Tensor, total_blocks: int) -> None:
|
| 156 |
+
"""
|
| 157 |
+
Rebalance KV cache ownership based on new block intervals.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
old_intervals: Previous block intervals [world_size, 2]
|
| 161 |
+
new_intervals: New block intervals [world_size, 2]
|
| 162 |
+
total_blocks: Total number of blocks
|
| 163 |
+
"""
|
| 164 |
+
if self.kv_cache_mgr is None:
|
| 165 |
+
raise RuntimeError("KV cache manager not initialized")
|
| 166 |
+
|
| 167 |
+
with CommunicationTimer("rebalance_kv_cache", self.logger):
|
| 168 |
+
self.kv_cache_mgr.rebalance_kv_cache_by_diff(old_intervals, new_intervals, total_blocks)
|
| 169 |
+
|
| 170 |
+
self.logger.info("Rebalanced KV cache ownership")
|
| 171 |
+
|
| 172 |
+
def broadcast_tensor(self, tensor: torch.Tensor, src: int) -> None:
|
| 173 |
+
"""
|
| 174 |
+
Broadcast a tensor from source to all ranks.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
tensor: Tensor to broadcast
|
| 178 |
+
src: Source rank
|
| 179 |
+
"""
|
| 180 |
+
with CommunicationTimer(f"broadcast_tensor from rank {src}", self.logger):
|
| 181 |
+
self.comm.broadcast_tensor(tensor, src)
|
| 182 |
+
|
| 183 |
+
self.logger.debug(f"Broadcasted tensor from rank {src}, shape: {tensor.shape}")
|
| 184 |
+
|
| 185 |
+
def all_gather_tensors(self, tensor: torch.Tensor) -> List[torch.Tensor]:
|
| 186 |
+
"""
|
| 187 |
+
Gather tensors from all ranks.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
tensor: Local tensor to gather
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
List of tensors from all ranks
|
| 194 |
+
"""
|
| 195 |
+
with CommunicationTimer("all_gather_tensors", self.logger):
|
| 196 |
+
gather_list = self.comm.all_gather_tensors(tensor)
|
| 197 |
+
|
| 198 |
+
self.logger.debug(f"Gathered tensors from all ranks, local shape: {tensor.shape}")
|
| 199 |
+
return gather_list
|
| 200 |
+
|
| 201 |
+
def wait_for_outstanding(self, max_outstanding: Optional[int] = None) -> None:
|
| 202 |
+
"""
|
| 203 |
+
Wait for outstanding operations to complete.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
max_outstanding: Maximum number of outstanding operations to keep
|
| 207 |
+
"""
|
| 208 |
+
with CommunicationTimer("wait_for_outstanding", self.logger):
|
| 209 |
+
self.comm.wait_for_outstanding(max_outstanding)
|
| 210 |
+
|
| 211 |
+
def barrier(self) -> None:
|
| 212 |
+
"""Synchronize all ranks."""
|
| 213 |
+
with CommunicationTimer("barrier", self.logger):
|
| 214 |
+
self.comm.barrier()
|
| 215 |
+
|
| 216 |
+
def get_performance_metrics(self) -> PerformanceMetrics:
|
| 217 |
+
"""
|
| 218 |
+
Get performance metrics for data transfer operations.
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
PerformanceMetrics object containing timing information
|
| 222 |
+
"""
|
| 223 |
+
# This is a simplified version - in practice, you'd want to track
|
| 224 |
+
# more detailed timing information
|
| 225 |
+
avg_transfer_time = self.total_transfer_time / max(1, self.transfer_count)
|
| 226 |
+
|
| 227 |
+
return PerformanceMetrics(
|
| 228 |
+
dit_time=0.0, # Would be filled by caller
|
| 229 |
+
total_time=0.0, # Would be filled by caller
|
| 230 |
+
communication_time=avg_transfer_time,
|
| 231 |
+
buffer_allocation_time=0.0 # Would be tracked by buffer manager
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
def get_statistics(self) -> dict:
|
| 235 |
+
"""
|
| 236 |
+
Get transfer statistics.
|
| 237 |
+
|
| 238 |
+
Returns:
|
| 239 |
+
Dictionary containing transfer statistics
|
| 240 |
+
"""
|
| 241 |
+
return {
|
| 242 |
+
"transfer_count": self.transfer_count,
|
| 243 |
+
"total_transfer_time": self.total_transfer_time,
|
| 244 |
+
"avg_transfer_time": self.total_transfer_time / max(1, self.transfer_count),
|
| 245 |
+
"communicator_stats": self.comm.get_statistics(),
|
| 246 |
+
"buffer_manager_stats": self.buffer_mgr.get_statistics() if self.buffer_mgr else None
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def print_statistics(self) -> None:
|
| 250 |
+
"""Print transfer statistics."""
|
| 251 |
+
stats = self.get_statistics()
|
| 252 |
+
self.logger.info("Model Data Transfer Statistics:")
|
| 253 |
+
for key, value in stats.items():
|
| 254 |
+
if key == "communicator_stats" or key == "buffer_manager_stats":
|
| 255 |
+
if value:
|
| 256 |
+
self.logger.info(f" {key}:")
|
| 257 |
+
for sub_key, sub_value in value.items():
|
| 258 |
+
self.logger.info(f" {sub_key}: {sub_value}")
|
| 259 |
+
else:
|
| 260 |
+
self.logger.info(f" {key}: {value}")
|
| 261 |
+
|
| 262 |
+
def cleanup(self) -> None:
|
| 263 |
+
"""Clean up resources."""
|
| 264 |
+
if self.buffer_mgr:
|
| 265 |
+
self.buffer_mgr.clear_buffers()
|
| 266 |
+
self.logger.info("Model data transfer cleanup completed")
|
| 267 |
+
|
| 268 |
+
def __del__(self):
|
| 269 |
+
"""Cleanup when the transfer manager is destroyed."""
|
| 270 |
+
try:
|
| 271 |
+
self.cleanup()
|
| 272 |
+
except Exception:
|
| 273 |
+
pass
|
streamv2v/communication/test_communication.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test cases for the communication module.
|
| 3 |
+
|
| 4 |
+
This module provides comprehensive tests for all communication abstractions.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import unittest
|
| 8 |
+
import logging
|
| 9 |
+
import torch
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
import tempfile
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
from unittest.mock import Mock, patch, MagicMock
|
| 15 |
+
|
| 16 |
+
# Add the parent directory to the path to import our modules
|
| 17 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 18 |
+
|
| 19 |
+
from communication.data_containers import LatentData, KVCacheData, CommunicationConfig, BlockInterval, PerformanceMetrics
|
| 20 |
+
from communication.buffer_manager import BufferManager
|
| 21 |
+
from communication.utils import CommunicationTags, setup_logging, compute_balanced_split
|
| 22 |
+
from communication.distributed_communicator import DistributedCommunicator
|
| 23 |
+
from communication.kv_cache_manager import KVCacheManager
|
| 24 |
+
from communication.model_data_transfer import ModelDataTransfer
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class TestDataContainers(unittest.TestCase):
|
| 28 |
+
"""Test cases for data container classes."""
|
| 29 |
+
|
| 30 |
+
def setUp(self):
|
| 31 |
+
"""Set up test fixtures."""
|
| 32 |
+
self.device = torch.device('cpu')
|
| 33 |
+
self.sample_latents = torch.randn(1, 4, 16, 16, device=self.device)
|
| 34 |
+
self.sample_original_latents = torch.randn(1, 4, 16, 16, 16, device=self.device)
|
| 35 |
+
self.sample_current_start = torch.tensor([0, 1, 2], device=self.device)
|
| 36 |
+
self.sample_current_end = torch.tensor([1, 2, 3], device=self.device)
|
| 37 |
+
self.sample_patched_x_shape = torch.tensor([1, 4, 16, 16, 16], device=self.device)
|
| 38 |
+
|
| 39 |
+
def test_latent_data_creation(self):
|
| 40 |
+
"""Test LatentData creation and validation."""
|
| 41 |
+
latent_data = LatentData(
|
| 42 |
+
chunk_idx=0,
|
| 43 |
+
latents=self.sample_latents,
|
| 44 |
+
original_latents=self.sample_original_latents,
|
| 45 |
+
current_start=self.sample_current_start,
|
| 46 |
+
current_end=self.sample_current_end,
|
| 47 |
+
current_step=100,
|
| 48 |
+
patched_x_shape=self.sample_patched_x_shape
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
self.assertEqual(latent_data.chunk_idx, 0)
|
| 52 |
+
self.assertEqual(latent_data.current_step, 100)
|
| 53 |
+
self.assertTrue(torch.equal(latent_data.latents, self.sample_latents))
|
| 54 |
+
|
| 55 |
+
def test_latent_data_validation(self):
|
| 56 |
+
"""Test LatentData validation with invalid inputs."""
|
| 57 |
+
with self.assertRaises(TypeError):
|
| 58 |
+
LatentData(
|
| 59 |
+
chunk_idx=0,
|
| 60 |
+
latents="invalid", # Should be torch.Tensor
|
| 61 |
+
original_latents=self.sample_original_latents,
|
| 62 |
+
current_start=self.sample_current_start,
|
| 63 |
+
current_end=self.sample_current_end,
|
| 64 |
+
current_step=100,
|
| 65 |
+
patched_x_shape=self.sample_patched_x_shape
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def test_communication_config(self):
|
| 69 |
+
"""Test CommunicationConfig creation and validation."""
|
| 70 |
+
config = CommunicationConfig(
|
| 71 |
+
max_outstanding=5,
|
| 72 |
+
buffer_pool_size=20,
|
| 73 |
+
enable_buffer_reuse=True,
|
| 74 |
+
communication_timeout=60.0
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
self.assertEqual(config.max_outstanding, 5)
|
| 78 |
+
self.assertEqual(config.buffer_pool_size, 20)
|
| 79 |
+
self.assertTrue(config.enable_buffer_reuse)
|
| 80 |
+
self.assertEqual(config.communication_timeout, 60.0)
|
| 81 |
+
|
| 82 |
+
def test_communication_config_validation(self):
|
| 83 |
+
"""Test CommunicationConfig validation with invalid inputs."""
|
| 84 |
+
with self.assertRaises(ValueError):
|
| 85 |
+
CommunicationConfig(max_outstanding=0) # Should be at least 1
|
| 86 |
+
|
| 87 |
+
with self.assertRaises(ValueError):
|
| 88 |
+
CommunicationConfig(buffer_pool_size=0) # Should be at least 1
|
| 89 |
+
|
| 90 |
+
with self.assertRaises(ValueError):
|
| 91 |
+
CommunicationConfig(communication_timeout=0) # Should be positive
|
| 92 |
+
|
| 93 |
+
def test_block_interval(self):
|
| 94 |
+
"""Test BlockInterval creation and methods."""
|
| 95 |
+
interval = BlockInterval(start=0, end=10, rank=0)
|
| 96 |
+
|
| 97 |
+
self.assertEqual(interval.start, 0)
|
| 98 |
+
self.assertEqual(interval.end, 10)
|
| 99 |
+
self.assertEqual(interval.rank, 0)
|
| 100 |
+
self.assertEqual(interval.size, 10)
|
| 101 |
+
self.assertTrue(interval.contains(5))
|
| 102 |
+
self.assertFalse(interval.contains(10))
|
| 103 |
+
self.assertFalse(interval.contains(-1))
|
| 104 |
+
|
| 105 |
+
def test_block_interval_validation(self):
|
| 106 |
+
"""Test BlockInterval validation with invalid inputs."""
|
| 107 |
+
with self.assertRaises(ValueError):
|
| 108 |
+
BlockInterval(start=-1, end=10, rank=0) # Start should be non-negative
|
| 109 |
+
|
| 110 |
+
with self.assertRaises(ValueError):
|
| 111 |
+
BlockInterval(start=10, end=5, rank=0) # End should be greater than start
|
| 112 |
+
|
| 113 |
+
with self.assertRaises(ValueError):
|
| 114 |
+
BlockInterval(start=0, end=10, rank=-1) # Rank should be non-negative
|
| 115 |
+
|
| 116 |
+
def test_performance_metrics(self):
|
| 117 |
+
"""Test PerformanceMetrics creation and methods."""
|
| 118 |
+
metrics = PerformanceMetrics(
|
| 119 |
+
dit_time=1.0,
|
| 120 |
+
total_time=2.0,
|
| 121 |
+
communication_time=0.5,
|
| 122 |
+
buffer_allocation_time=0.1
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
self.assertEqual(metrics.dit_time, 1.0)
|
| 126 |
+
self.assertEqual(metrics.total_time, 2.0)
|
| 127 |
+
self.assertEqual(metrics.communication_time, 0.5)
|
| 128 |
+
self.assertEqual(metrics.buffer_allocation_time, 0.1)
|
| 129 |
+
self.assertEqual(metrics.efficiency, 0.75) # (2.0 - 0.5) / 2.0
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class TestBufferManager(unittest.TestCase):
|
| 133 |
+
"""Test cases for BufferManager."""
|
| 134 |
+
|
| 135 |
+
def setUp(self):
|
| 136 |
+
"""Set up test fixtures."""
|
| 137 |
+
self.device = torch.device('cpu')
|
| 138 |
+
self.config = CommunicationConfig(buffer_pool_size=5)
|
| 139 |
+
self.buffer_manager = BufferManager(self.device, self.config)
|
| 140 |
+
|
| 141 |
+
def test_buffer_allocation(self):
|
| 142 |
+
"""Test buffer allocation and reuse."""
|
| 143 |
+
shape = (1, 4, 16, 16)
|
| 144 |
+
dtype = torch.float32
|
| 145 |
+
|
| 146 |
+
# Allocate a buffer
|
| 147 |
+
buffer1 = self.buffer_manager.get_buffer(shape, dtype, "latent")
|
| 148 |
+
self.assertEqual(buffer1.shape, shape)
|
| 149 |
+
self.assertEqual(buffer1.dtype, dtype)
|
| 150 |
+
self.assertEqual(buffer1.device, self.device)
|
| 151 |
+
|
| 152 |
+
# Return the buffer
|
| 153 |
+
self.buffer_manager.return_buffer(buffer1, "latent")
|
| 154 |
+
|
| 155 |
+
# Get another buffer of the same shape - should reuse
|
| 156 |
+
buffer2 = self.buffer_manager.get_buffer(shape, dtype, "latent")
|
| 157 |
+
self.assertEqual(buffer2.shape, shape)
|
| 158 |
+
self.assertEqual(buffer2.dtype, dtype)
|
| 159 |
+
|
| 160 |
+
def test_buffer_statistics(self):
|
| 161 |
+
"""Test buffer manager statistics."""
|
| 162 |
+
shape = (1, 4, 16, 16)
|
| 163 |
+
dtype = torch.float32
|
| 164 |
+
|
| 165 |
+
# Allocate and return some buffers
|
| 166 |
+
buffer1 = self.buffer_manager.get_buffer(shape, dtype, "latent")
|
| 167 |
+
self.buffer_manager.return_buffer(buffer1, "latent")
|
| 168 |
+
|
| 169 |
+
buffer2 = self.buffer_manager.get_buffer(shape, dtype, "latent")
|
| 170 |
+
self.buffer_manager.return_buffer(buffer2, "latent")
|
| 171 |
+
|
| 172 |
+
stats = self.buffer_manager.get_statistics()
|
| 173 |
+
self.assertEqual(stats['allocation_count'], 1)
|
| 174 |
+
self.assertEqual(stats['reuse_count'], 1)
|
| 175 |
+
self.assertGreater(stats['total_allocated_memory_bytes'], 0)
|
| 176 |
+
|
| 177 |
+
def test_buffer_cleanup(self):
|
| 178 |
+
"""Test buffer cleanup."""
|
| 179 |
+
shape = (1, 4, 16, 16)
|
| 180 |
+
dtype = torch.float32
|
| 181 |
+
|
| 182 |
+
# Allocate and return some buffers
|
| 183 |
+
buffer1 = self.buffer_manager.get_buffer(shape, dtype, "latent")
|
| 184 |
+
self.buffer_manager.return_buffer(buffer1, "latent")
|
| 185 |
+
|
| 186 |
+
# Clear buffers
|
| 187 |
+
self.buffer_manager.clear_buffers("latent")
|
| 188 |
+
|
| 189 |
+
stats = self.buffer_manager.get_statistics()
|
| 190 |
+
self.assertEqual(stats['total_free_buffers'], 0)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class TestUtils(unittest.TestCase):
|
| 194 |
+
"""Test cases for utility functions."""
|
| 195 |
+
|
| 196 |
+
def test_compute_balanced_split(self):
|
| 197 |
+
"""Test the compute_balanced_split function."""
|
| 198 |
+
total_blocks = 30
|
| 199 |
+
rank_times = [1.0, 2.0, 1.5] # Rank 1 is slower
|
| 200 |
+
dit_times = [0.8, 1.6, 1.2]
|
| 201 |
+
current_block_nums = [[0, 10], [10, 20], [20, 30]]
|
| 202 |
+
|
| 203 |
+
new_block_nums = compute_balanced_split(total_blocks, rank_times, dit_times, current_block_nums)
|
| 204 |
+
|
| 205 |
+
# Should have same number of ranks
|
| 206 |
+
self.assertEqual(len(new_block_nums), len(current_block_nums))
|
| 207 |
+
|
| 208 |
+
# Should sum to total_blocks
|
| 209 |
+
total_allocated = sum(end - start for start, end in new_block_nums)
|
| 210 |
+
self.assertEqual(total_allocated, total_blocks)
|
| 211 |
+
|
| 212 |
+
# Should be contiguous
|
| 213 |
+
for i in range(len(new_block_nums) - 1):
|
| 214 |
+
self.assertEqual(new_block_nums[i][1], new_block_nums[i + 1][0])
|
| 215 |
+
|
| 216 |
+
def test_compute_balanced_split_edge_cases(self):
|
| 217 |
+
"""Test compute_balanced_split with edge cases."""
|
| 218 |
+
# Empty input
|
| 219 |
+
result = compute_balanced_split(0, [], [], [])
|
| 220 |
+
self.assertEqual(result, [])
|
| 221 |
+
|
| 222 |
+
# Single rank
|
| 223 |
+
result = compute_balanced_split(10, [1.0], [0.8], [[0, 10]])
|
| 224 |
+
self.assertEqual(result, [[0, 10]])
|
| 225 |
+
|
| 226 |
+
# Invalid input lengths
|
| 227 |
+
result = compute_balanced_split(10, [1.0], [0.8], [[0, 10], [10, 20]])
|
| 228 |
+
self.assertEqual(result, [[0, 10], [10, 20]]) # Should return original
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class TestDistributedCommunicator(unittest.TestCase):
|
| 232 |
+
"""Test cases for DistributedCommunicator."""
|
| 233 |
+
|
| 234 |
+
def setUp(self):
|
| 235 |
+
"""Set up test fixtures."""
|
| 236 |
+
self.device = torch.device('cpu')
|
| 237 |
+
self.config = CommunicationConfig()
|
| 238 |
+
|
| 239 |
+
# Mock distributed environment
|
| 240 |
+
with patch('torch.distributed.is_initialized', return_value=True):
|
| 241 |
+
self.communicator = DistributedCommunicator(0, 2, self.device, self.config)
|
| 242 |
+
|
| 243 |
+
def test_communicator_initialization(self):
|
| 244 |
+
"""Test communicator initialization."""
|
| 245 |
+
self.assertEqual(self.communicator.rank, 0)
|
| 246 |
+
self.assertEqual(self.communicator.world_size, 2)
|
| 247 |
+
self.assertEqual(self.communicator.device, self.device)
|
| 248 |
+
|
| 249 |
+
def test_communicator_initialization_without_distributed(self):
|
| 250 |
+
"""Test communicator initialization without distributed."""
|
| 251 |
+
with patch('torch.distributed.is_initialized', return_value=False):
|
| 252 |
+
with self.assertRaises(RuntimeError):
|
| 253 |
+
DistributedCommunicator(0, 2, self.device, self.config)
|
| 254 |
+
|
| 255 |
+
def test_create_header(self):
|
| 256 |
+
"""Test header creation and parsing."""
|
| 257 |
+
chunk_idx = 5
|
| 258 |
+
shape = (1, 4, 16, 16)
|
| 259 |
+
|
| 260 |
+
header = self.communicator._create_header(chunk_idx, shape)
|
| 261 |
+
self.assertEqual(header.shape, (5,)) # chunk_idx + 4 shape dimensions
|
| 262 |
+
self.assertEqual(header.dtype, torch.int64)
|
| 263 |
+
|
| 264 |
+
parsed_chunk_idx, parsed_shape = self.communicator._parse_header(header)
|
| 265 |
+
self.assertEqual(parsed_chunk_idx, chunk_idx)
|
| 266 |
+
self.assertEqual(parsed_shape, shape)
|
| 267 |
+
|
| 268 |
+
def test_communicator_statistics(self):
|
| 269 |
+
"""Test communicator statistics."""
|
| 270 |
+
stats = self.communicator.get_statistics()
|
| 271 |
+
|
| 272 |
+
self.assertEqual(stats['rank'], 0)
|
| 273 |
+
self.assertEqual(stats['world_size'], 2)
|
| 274 |
+
self.assertEqual(stats['outstanding_operations'], 0)
|
| 275 |
+
self.assertEqual(stats['max_outstanding'], 1)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class TestKVCacheManager(unittest.TestCase):
|
| 279 |
+
"""Test cases for KVCacheManager."""
|
| 280 |
+
|
| 281 |
+
def setUp(self):
|
| 282 |
+
"""Set up test fixtures."""
|
| 283 |
+
self.device = torch.device('cpu')
|
| 284 |
+
|
| 285 |
+
# Mock pipeline with KV cache
|
| 286 |
+
self.mock_pipeline = Mock()
|
| 287 |
+
self.mock_pipeline.frame_seq_length = 16
|
| 288 |
+
self.mock_pipeline.denoising_step_list = [700, 500, 0]
|
| 289 |
+
self.mock_pipeline.kv_cache1 = [
|
| 290 |
+
{
|
| 291 |
+
'k': torch.randn(1, 8, 16, 64, device=self.device),
|
| 292 |
+
'v': torch.randn(1, 8, 16, 64, device=self.device),
|
| 293 |
+
'global_end_index': torch.tensor([16], device=self.device),
|
| 294 |
+
'local_end_index': torch.tensor([16], device=self.device)
|
| 295 |
+
}
|
| 296 |
+
for _ in range(30)
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
+
self.kv_cache_manager = KVCacheManager(self.mock_pipeline, self.device)
|
| 300 |
+
|
| 301 |
+
def test_compute_block_owners(self):
|
| 302 |
+
"""Test block owner computation."""
|
| 303 |
+
block_intervals = torch.tensor([[0, 10], [10, 20], [20, 30]], device=self.device)
|
| 304 |
+
total_blocks = 30
|
| 305 |
+
|
| 306 |
+
owners = self.kv_cache_manager.compute_block_owners(block_intervals, total_blocks)
|
| 307 |
+
|
| 308 |
+
self.assertEqual(owners.shape, (30,))
|
| 309 |
+
self.assertTrue(torch.all(owners[:10] == 0))
|
| 310 |
+
self.assertTrue(torch.all(owners[10:20] == 1))
|
| 311 |
+
self.assertTrue(torch.all(owners[20:30] == 2))
|
| 312 |
+
|
| 313 |
+
def test_kv_cache_statistics(self):
|
| 314 |
+
"""Test KV cache statistics."""
|
| 315 |
+
block_intervals = torch.tensor([[0, 10], [10, 20], [20, 30]], device=self.device)
|
| 316 |
+
total_blocks = 30
|
| 317 |
+
|
| 318 |
+
stats = self.kv_cache_manager.get_kv_cache_statistics(block_intervals, total_blocks)
|
| 319 |
+
|
| 320 |
+
self.assertEqual(stats['total_blocks'], 30)
|
| 321 |
+
self.assertEqual(stats['block_counts'][0], 10)
|
| 322 |
+
self.assertEqual(stats['block_counts'][1], 10)
|
| 323 |
+
self.assertEqual(stats['block_counts'][2], 10)
|
| 324 |
+
self.assertGreater(stats['memory_per_block_bytes'], 0)
|
| 325 |
+
|
| 326 |
+
def test_validate_kv_cache_consistency(self):
|
| 327 |
+
"""Test KV cache consistency validation."""
|
| 328 |
+
block_intervals = torch.tensor([[0, 10], [10, 20], [20, 30]], device=self.device)
|
| 329 |
+
total_blocks = 30
|
| 330 |
+
|
| 331 |
+
is_consistent = self.kv_cache_manager.validate_kv_cache_consistency(block_intervals, total_blocks)
|
| 332 |
+
self.assertTrue(is_consistent)
|
| 333 |
+
|
| 334 |
+
# Test with invalid intervals
|
| 335 |
+
invalid_intervals = torch.tensor([[0, 10], [10, 20], [20, 25]], device=self.device) # Missing blocks
|
| 336 |
+
is_consistent = self.kv_cache_manager.validate_kv_cache_consistency(invalid_intervals, total_blocks)
|
| 337 |
+
self.assertFalse(is_consistent)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class TestModelDataTransfer(unittest.TestCase):
|
| 341 |
+
"""Test cases for ModelDataTransfer."""
|
| 342 |
+
|
| 343 |
+
def setUp(self):
|
| 344 |
+
"""Set up test fixtures."""
|
| 345 |
+
self.device = torch.device('cpu')
|
| 346 |
+
self.config = CommunicationConfig()
|
| 347 |
+
|
| 348 |
+
# Mock components
|
| 349 |
+
with patch('torch.distributed.is_initialized', return_value=True):
|
| 350 |
+
self.communicator = DistributedCommunicator(0, 2, self.device, self.config)
|
| 351 |
+
|
| 352 |
+
self.buffer_manager = BufferManager(self.device, self.config)
|
| 353 |
+
self.mock_pipeline = Mock()
|
| 354 |
+
self.mock_pipeline.frame_seq_length = 16
|
| 355 |
+
self.mock_pipeline.denoising_step_list = [700, 500, 0]
|
| 356 |
+
self.mock_pipeline.kv_cache1 = []
|
| 357 |
+
self.kv_cache_manager = KVCacheManager(self.mock_pipeline, self.device)
|
| 358 |
+
|
| 359 |
+
self.data_transfer = ModelDataTransfer(
|
| 360 |
+
self.communicator,
|
| 361 |
+
self.buffer_manager,
|
| 362 |
+
self.kv_cache_manager,
|
| 363 |
+
self.config
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
def test_data_transfer_initialization(self):
|
| 367 |
+
"""Test data transfer initialization."""
|
| 368 |
+
self.assertEqual(self.data_transfer.comm, self.communicator)
|
| 369 |
+
self.assertEqual(self.data_transfer.buffer_mgr, self.buffer_manager)
|
| 370 |
+
self.assertEqual(self.data_transfer.kv_cache_mgr, self.kv_cache_manager)
|
| 371 |
+
self.assertEqual(self.data_transfer.transfer_count, 0)
|
| 372 |
+
|
| 373 |
+
def test_data_transfer_statistics(self):
|
| 374 |
+
"""Test data transfer statistics."""
|
| 375 |
+
stats = self.data_transfer.get_statistics()
|
| 376 |
+
|
| 377 |
+
self.assertEqual(stats['transfer_count'], 0)
|
| 378 |
+
self.assertEqual(stats['total_transfer_time'], 0.0)
|
| 379 |
+
self.assertIsNotNone(stats['communicator_stats'])
|
| 380 |
+
self.assertIsNotNone(stats['buffer_manager_stats'])
|
| 381 |
+
|
| 382 |
+
def test_cleanup(self):
|
| 383 |
+
"""Test data transfer cleanup."""
|
| 384 |
+
# Should not raise any exceptions
|
| 385 |
+
self.data_transfer.cleanup()
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
if __name__ == '__main__':
|
| 389 |
+
# Set up logging for tests
|
| 390 |
+
logging.basicConfig(level=logging.INFO)
|
| 391 |
+
|
| 392 |
+
# Run tests
|
| 393 |
+
unittest.main(verbosity=2)
|
streamv2v/communication/utils.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions and constants for communication operations.
|
| 3 |
+
|
| 4 |
+
This module provides utility functions and constants used across the communication module.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
from typing import List, Tuple, Optional
|
| 10 |
+
import time
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
# Communication tags for different types of data
|
| 14 |
+
class CommunicationTags:
|
| 15 |
+
"""Constants for communication tags."""
|
| 16 |
+
LATENT_HDR = 11001
|
| 17 |
+
LATENT_PAY = 11002
|
| 18 |
+
START_END_STEP = 11003
|
| 19 |
+
PATCHED_X_SHAPE = 11004
|
| 20 |
+
LATENT_ORIGIN_HDR = 11005
|
| 21 |
+
LATENT_ORIGIN_PAY = 11006
|
| 22 |
+
KV_CACHE_K = 11007
|
| 23 |
+
KV_CACHE_V = 11008
|
| 24 |
+
KV_CACHE_GLOBAL_END = 11009
|
| 25 |
+
KV_CACHE_LOCAL_END = 11010
|
| 26 |
+
BLOCK_INTERVALS = 11011
|
| 27 |
+
PERFORMANCE_METRICS = 11012
|
| 28 |
+
UPDATED_PROMPT_LENGTH = 11013
|
| 29 |
+
UPDATED_PROMPT = 11014
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def init_distributed():
|
| 33 |
+
"""
|
| 34 |
+
Initialize distributed communication.
|
| 35 |
+
|
| 36 |
+
This function initializes the distributed process group if not already initialized.
|
| 37 |
+
"""
|
| 38 |
+
if not dist.is_initialized():
|
| 39 |
+
backend = "nccl"
|
| 40 |
+
dist.init_process_group(backend=backend)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_rank_info() -> Tuple[int, int]:
|
| 44 |
+
"""
|
| 45 |
+
Get current rank and world size.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
Tuple of (rank, world_size)
|
| 49 |
+
"""
|
| 50 |
+
if not dist.is_initialized():
|
| 51 |
+
raise RuntimeError("Distributed not initialized")
|
| 52 |
+
return dist.get_rank(), dist.get_world_size()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_next_rank(rank: int, world_size: int) -> int:
|
| 56 |
+
"""
|
| 57 |
+
Get the next rank in the ring topology.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
rank: Current rank
|
| 61 |
+
world_size: Total number of ranks
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Next rank in the ring
|
| 65 |
+
"""
|
| 66 |
+
return (rank + 1) % world_size
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def get_prev_rank(rank: int, world_size: int) -> int:
|
| 70 |
+
"""
|
| 71 |
+
Get the previous rank in the ring topology.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
rank: Current rank
|
| 75 |
+
world_size: Total number of ranks
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Previous rank in the ring
|
| 79 |
+
"""
|
| 80 |
+
return (rank - 1) % world_size
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def create_tensor_header(shape: Tuple[int, ...], dtype: torch.dtype,
|
| 84 |
+
chunk_idx: int, device: torch.device) -> torch.Tensor:
|
| 85 |
+
"""
|
| 86 |
+
Create a header tensor for communication.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
shape: Shape of the tensor to be sent
|
| 90 |
+
dtype: Data type of the tensor
|
| 91 |
+
chunk_idx: Chunk index
|
| 92 |
+
device: Device where the header will be created
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
Header tensor containing metadata
|
| 96 |
+
"""
|
| 97 |
+
header_data = [chunk_idx] + list(shape)
|
| 98 |
+
return torch.tensor(header_data, dtype=torch.int64, device=device)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def parse_tensor_header(header: torch.Tensor) -> Tuple[int, Tuple[int, ...]]:
|
| 102 |
+
"""
|
| 103 |
+
Parse a header tensor to extract metadata.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
header: Header tensor
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Tuple of (chunk_idx, shape)
|
| 110 |
+
"""
|
| 111 |
+
header_list = header.tolist()
|
| 112 |
+
chunk_idx = int(header_list[0])
|
| 113 |
+
shape = tuple(int(x) for x in header_list[1:])
|
| 114 |
+
return chunk_idx, shape
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def validate_tensor_for_communication(tensor: torch.Tensor,
|
| 118 |
+
expected_device: torch.device,
|
| 119 |
+
expected_dtype: torch.dtype) -> None:
|
| 120 |
+
"""
|
| 121 |
+
Validate tensor properties for communication.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
tensor: Tensor to validate
|
| 125 |
+
expected_device: Expected device
|
| 126 |
+
expected_dtype: Expected data type
|
| 127 |
+
|
| 128 |
+
Raises:
|
| 129 |
+
ValueError: If tensor properties don't match expectations
|
| 130 |
+
"""
|
| 131 |
+
if not isinstance(tensor, torch.Tensor):
|
| 132 |
+
raise ValueError("Input must be a torch.Tensor")
|
| 133 |
+
|
| 134 |
+
if tensor.device != expected_device:
|
| 135 |
+
raise ValueError(f"Tensor device {tensor.device} doesn't match expected {expected_device}")
|
| 136 |
+
|
| 137 |
+
if tensor.dtype != expected_dtype:
|
| 138 |
+
raise ValueError(f"Tensor dtype {tensor.dtype} doesn't match expected {expected_dtype}")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def compute_balanced_split(total_blocks: int, rank_times: List[float],
|
| 142 |
+
dit_times: List[float],
|
| 143 |
+
current_block_nums: List[List[int]]) -> List[List[int]]:
|
| 144 |
+
"""
|
| 145 |
+
Compute new block splits for all ranks to balance total rank times.
|
| 146 |
+
|
| 147 |
+
This function is moved from the original file to provide better organization.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
total_blocks: Total number of DiT blocks
|
| 151 |
+
rank_times: List of total iteration times for each rank [t_rank0, t_rank1, ..., t_rankN] (DiT + VAE time)
|
| 152 |
+
dit_times: List of pure DiT inference times for each rank [dit_rank0, dit_rank1, ..., dit_rankN] (DiT time only)
|
| 153 |
+
current_block_nums: List of current block_num format for each rank [[rank0_blocks], [rank1_blocks], ...]
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
List of new block_num format for each rank, matching the original format:
|
| 157 |
+
- For world_size == 2: [[end_idx_rank0], [start_idx_rank1]]
|
| 158 |
+
- For world_size > 2: [[end_idx_rank0], [start1, end1], [start2, end2], ..., [start_idx_last]]
|
| 159 |
+
Note: Numbers are shared across ranks (rank0_end = rank1_start, rank1_end = rank2_start, etc.)
|
| 160 |
+
"""
|
| 161 |
+
num_ranks = len(rank_times)
|
| 162 |
+
if num_ranks == 0 or num_ranks != len(current_block_nums) or num_ranks != len(dit_times):
|
| 163 |
+
return current_block_nums
|
| 164 |
+
|
| 165 |
+
# Edge case: if we have more ranks than blocks, we can't guarantee 1 block per rank
|
| 166 |
+
if num_ranks > total_blocks:
|
| 167 |
+
# Fall back to original behavior for this edge case
|
| 168 |
+
return current_block_nums
|
| 169 |
+
|
| 170 |
+
# Step 1: Calculate total DiT time and per-block DiT time
|
| 171 |
+
total_dit_time = sum(dit_times)
|
| 172 |
+
dit_time_per_block = total_dit_time / total_blocks
|
| 173 |
+
|
| 174 |
+
# Step 2: Calculate average rank time
|
| 175 |
+
avg_rank_time = sum(rank_times) / num_ranks
|
| 176 |
+
|
| 177 |
+
# Step 3: Extract current block counts from current_block_nums (all ranks use [start, end) now)
|
| 178 |
+
current_block_counts = []
|
| 179 |
+
for block_num in current_block_nums:
|
| 180 |
+
# block_num: [start, end) exclusive end
|
| 181 |
+
start_idx, end_idx = int(block_num[0]), int(block_num[1])
|
| 182 |
+
current_block_counts.append(max(0, end_idx - start_idx))
|
| 183 |
+
|
| 184 |
+
# Step 4: Calculate target block counts based on time differences
|
| 185 |
+
target_blocks = []
|
| 186 |
+
for i in range(num_ranks):
|
| 187 |
+
time_diff = avg_rank_time - rank_times[i] # positive = needs more time, negative = needs less time
|
| 188 |
+
block_adjustment = time_diff / dit_time_per_block # convert time difference to block count
|
| 189 |
+
target_count = current_block_counts[i] + block_adjustment
|
| 190 |
+
# Ensure each rank gets at least 1 block (minimum allocation)
|
| 191 |
+
target_count = max(1, int(round(target_count)))
|
| 192 |
+
target_blocks.append(target_count)
|
| 193 |
+
|
| 194 |
+
# Step 5: Adjust to ensure total blocks sum to total_blocks while maintaining minimum 1 block per rank
|
| 195 |
+
current_total = sum(target_blocks)
|
| 196 |
+
if current_total != total_blocks:
|
| 197 |
+
diff = total_blocks - current_total
|
| 198 |
+
# When adding, give to ranks with smallest counts first; when removing, take from largest counts first
|
| 199 |
+
if diff > 0:
|
| 200 |
+
order = sorted(range(num_ranks), key=lambda i: (target_blocks[i], i))
|
| 201 |
+
else:
|
| 202 |
+
order = sorted(range(num_ranks), key=lambda i: (target_blocks[i], i), reverse=True)
|
| 203 |
+
i = 0
|
| 204 |
+
while diff != 0 and num_ranks > 0:
|
| 205 |
+
idx = order[i % num_ranks]
|
| 206 |
+
if diff > 0:
|
| 207 |
+
target_blocks[idx] += 1
|
| 208 |
+
diff -= 1
|
| 209 |
+
else:
|
| 210 |
+
# Only remove blocks if rank has more than 1 block (maintain minimum allocation)
|
| 211 |
+
if target_blocks[idx] > 1:
|
| 212 |
+
target_blocks[idx] -= 1
|
| 213 |
+
diff += 1
|
| 214 |
+
i += 1
|
| 215 |
+
|
| 216 |
+
# Step 6: Convert target block counts to contiguous [start, end) intervals from 0 to total_blocks
|
| 217 |
+
new_block_nums = []
|
| 218 |
+
running_start = 0
|
| 219 |
+
for i in range(num_ranks):
|
| 220 |
+
block_count = int(target_blocks[i])
|
| 221 |
+
start_idx = running_start
|
| 222 |
+
end_idx = start_idx + block_count
|
| 223 |
+
# Guard (should not trigger if sums are correct)
|
| 224 |
+
if end_idx > total_blocks:
|
| 225 |
+
end_idx = total_blocks
|
| 226 |
+
new_block_nums.append([start_idx, end_idx])
|
| 227 |
+
running_start = end_idx
|
| 228 |
+
|
| 229 |
+
return new_block_nums
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def setup_logging(rank: int, log_level: int = logging.INFO) -> logging.Logger:
|
| 233 |
+
"""
|
| 234 |
+
Setup logging for the current rank.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
rank: Current rank
|
| 238 |
+
log_level: Logging level
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
Configured logger
|
| 242 |
+
"""
|
| 243 |
+
logger = logging.getLogger(f"rank_{rank}")
|
| 244 |
+
logger.setLevel(log_level)
|
| 245 |
+
# Prevent messages from propagating to the root logger (avoid double prints)
|
| 246 |
+
logger.propagate = False
|
| 247 |
+
|
| 248 |
+
if not logger.handlers:
|
| 249 |
+
handler = logging.StreamHandler()
|
| 250 |
+
formatter = logging.Formatter(
|
| 251 |
+
f'[Rank {rank}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 252 |
+
)
|
| 253 |
+
handler.setFormatter(formatter)
|
| 254 |
+
logger.addHandler(handler)
|
| 255 |
+
|
| 256 |
+
return logger
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class CommunicationTimer:
|
| 260 |
+
"""
|
| 261 |
+
Timer for measuring communication performance.
|
| 262 |
+
|
| 263 |
+
This class provides context manager functionality for timing communication operations.
|
| 264 |
+
"""
|
| 265 |
+
|
| 266 |
+
def __init__(self, operation_name: str, logger: Optional[logging.Logger] = None):
|
| 267 |
+
self.operation_name = operation_name
|
| 268 |
+
self.logger = logger
|
| 269 |
+
self.start_time = None
|
| 270 |
+
self.end_time = None
|
| 271 |
+
|
| 272 |
+
def __enter__(self):
|
| 273 |
+
self.start_time = time.time()
|
| 274 |
+
return self
|
| 275 |
+
|
| 276 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 277 |
+
self.end_time = time.time()
|
| 278 |
+
duration = self.end_time - self.start_time
|
| 279 |
+
|
| 280 |
+
if self.logger:
|
| 281 |
+
self.logger.info(f"{self.operation_name} took {duration:.4f} seconds")
|
| 282 |
+
|
| 283 |
+
@property
|
| 284 |
+
def duration(self) -> float:
|
| 285 |
+
"""Get the duration of the timed operation."""
|
| 286 |
+
if self.start_time is None or self.end_time is None:
|
| 287 |
+
return 0.0
|
| 288 |
+
return self.end_time - self.start_time
|
streamv2v/configs/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Packaged default configs for the public Python API."""
|
| 2 |
+
|
streamv2v/configs/wan_causal_dmd_v2v.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_name: wan
|
| 2 |
+
generator_name: causal_wan
|
| 3 |
+
generator_ckpt: "ckpts/autoregressive_checkpoint/model.pt"
|
| 4 |
+
generator_fsdp_wrap_strategy: size
|
| 5 |
+
real_score_fsdp_wrap_strategy: size
|
| 6 |
+
fake_score_fsdp_wrap_strategy: size
|
| 7 |
+
text_encoder_fsdp_wrap_strategy: size
|
| 8 |
+
generator_grad:
|
| 9 |
+
model: true
|
| 10 |
+
real_score_grad:
|
| 11 |
+
model: false
|
| 12 |
+
fake_score_grad:
|
| 13 |
+
model: true
|
| 14 |
+
denoising_step_list:
|
| 15 |
+
- 700
|
| 16 |
+
- 500
|
| 17 |
+
- 400
|
| 18 |
+
- 200
|
| 19 |
+
- 0
|
| 20 |
+
num_train_timestep: 1000
|
| 21 |
+
timestep_shift: 8.0
|
| 22 |
+
real_guidance_scale: 3.5
|
| 23 |
+
generator_task: causal_video
|
| 24 |
+
real_task_type: bidirectional_video
|
| 25 |
+
fake_task_type: bidirectional_video
|
| 26 |
+
denoising_loss_type: flow
|
| 27 |
+
mixed_precision: true
|
| 28 |
+
seed: 0
|
| 29 |
+
wandb_host: WANDB_HOST
|
| 30 |
+
wandb_key: WANDB_KEY
|
| 31 |
+
wandb_entity: tyin
|
| 32 |
+
wandb_project: causvid
|
| 33 |
+
wandb_name: wan_causal_dmd
|
| 34 |
+
sharding_strategy: hybrid_full
|
| 35 |
+
lr: 2.0e-06
|
| 36 |
+
beta1: 0.9
|
| 37 |
+
beta2: 0.999
|
| 38 |
+
data_path: mixkit_latents_lmdb
|
| 39 |
+
batch_size: 1
|
| 40 |
+
log_iters: 200
|
| 41 |
+
negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
|
| 42 |
+
dfake_gen_update_ratio: 5
|
| 43 |
+
image_or_video_shape:
|
| 44 |
+
- 1
|
| 45 |
+
- 21
|
| 46 |
+
- 16
|
| 47 |
+
- 60
|
| 48 |
+
- 104
|
| 49 |
+
output_path: /mnt/localssd/wan_causal_dmd
|
| 50 |
+
distillation_loss: dmd
|
| 51 |
+
gradient_checkpointing: true
|
| 52 |
+
backward_simulation: false
|
| 53 |
+
num_frame_per_block: 1
|
| 54 |
+
num_kv_cache: 6
|
| 55 |
+
num_sink_tokens: 3
|
| 56 |
+
adapt_sink_threshold: 0.2
|
| 57 |
+
warp_denoising_step: false
|
streamv2v/configs/wan_causal_dmd_v2v_fast.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_name: wan
|
| 2 |
+
generator_name: causal_wan
|
| 3 |
+
generator_ckpt: "ckpts/autoregressive_checkpoint/model.pt"
|
| 4 |
+
generator_fsdp_wrap_strategy: size
|
| 5 |
+
real_score_fsdp_wrap_strategy: size
|
| 6 |
+
fake_score_fsdp_wrap_strategy: size
|
| 7 |
+
text_encoder_fsdp_wrap_strategy: size
|
| 8 |
+
generator_grad:
|
| 9 |
+
model: true
|
| 10 |
+
real_score_grad:
|
| 11 |
+
model: false
|
| 12 |
+
fake_score_grad:
|
| 13 |
+
model: true
|
| 14 |
+
denoising_step_list:
|
| 15 |
+
- 700
|
| 16 |
+
- 500
|
| 17 |
+
- 400
|
| 18 |
+
- 200
|
| 19 |
+
- 0
|
| 20 |
+
num_train_timestep: 1000
|
| 21 |
+
timestep_shift: 8.0
|
| 22 |
+
real_guidance_scale: 3.5
|
| 23 |
+
generator_task: causal_video
|
| 24 |
+
real_task_type: bidirectional_video
|
| 25 |
+
fake_task_type: bidirectional_video
|
| 26 |
+
denoising_loss_type: flow
|
| 27 |
+
mixed_precision: true
|
| 28 |
+
seed: 0
|
| 29 |
+
wandb_host: WANDB_HOST
|
| 30 |
+
wandb_key: WANDB_KEY
|
| 31 |
+
wandb_entity: tyin
|
| 32 |
+
wandb_project: causvid
|
| 33 |
+
wandb_name: wan_causal_dmd
|
| 34 |
+
sharding_strategy: hybrid_full
|
| 35 |
+
lr: 2.0e-06
|
| 36 |
+
beta1: 0.9
|
| 37 |
+
beta2: 0.999
|
| 38 |
+
data_path: mixkit_latents_lmdb
|
| 39 |
+
batch_size: 1
|
| 40 |
+
log_iters: 200
|
| 41 |
+
negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
|
| 42 |
+
dfake_gen_update_ratio: 5
|
| 43 |
+
image_or_video_shape:
|
| 44 |
+
- 1
|
| 45 |
+
- 21
|
| 46 |
+
- 16
|
| 47 |
+
- 60
|
| 48 |
+
- 104
|
| 49 |
+
output_path: /mnt/localssd/wan_causal_dmd
|
| 50 |
+
distillation_loss: dmd
|
| 51 |
+
gradient_checkpointing: true
|
| 52 |
+
backward_simulation: false
|
| 53 |
+
num_frame_per_block: 1
|
| 54 |
+
num_kv_cache: 5
|
| 55 |
+
num_sink_tokens: 2
|
| 56 |
+
adapt_sink_threshold: -1
|
| 57 |
+
warp_denoising_step: false
|
streamv2v/inference.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Single GPU Inference Pipeline - Refactored from inference_pipe.py
|
| 3 |
+
|
| 4 |
+
This file extracts core logic from multi-GPU inference code to implement a complete
|
| 5 |
+
inference pipeline on a single GPU:
|
| 6 |
+
1. VAE encode input video
|
| 7 |
+
2. DiT inference (using input mode, processing all 30 blocks)
|
| 8 |
+
3. VAE decode output video
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from models.wan.causal_stream_inference import CausalStreamInferencePipeline
|
| 12 |
+
from models.util import set_seed
|
| 13 |
+
from diffusers.utils import export_to_video
|
| 14 |
+
from models.data import TextDataset
|
| 15 |
+
import argparse
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
import torch
|
| 18 |
+
import os
|
| 19 |
+
import time
|
| 20 |
+
import numpy as np
|
| 21 |
+
import logging
|
| 22 |
+
from typing import List
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from streamv2v.inference_common import (
|
| 26 |
+
load_generator_state_dict,
|
| 27 |
+
load_mp4_as_tensor,
|
| 28 |
+
merge_cli_config,
|
| 29 |
+
)
|
| 30 |
+
except ModuleNotFoundError:
|
| 31 |
+
from inference_common import (
|
| 32 |
+
load_generator_state_dict,
|
| 33 |
+
load_mp4_as_tensor,
|
| 34 |
+
merge_cli_config,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
LOGGER = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass
|
| 41 |
+
class SingleGPUStreamSession:
|
| 42 |
+
prompt: str
|
| 43 |
+
noise_scale: float
|
| 44 |
+
init_noise_scale: float
|
| 45 |
+
chunk_size: int
|
| 46 |
+
current_start: int
|
| 47 |
+
current_end: int
|
| 48 |
+
last_image: torch.Tensor
|
| 49 |
+
processed: int = 0
|
| 50 |
+
|
| 51 |
+
def compute_noise_scale_and_step(input_video_original: torch.Tensor, end_idx: int, chunk_size: int, noise_scale: float, init_noise_scale: float):
|
| 52 |
+
"""Compute adaptive noise scale and current step based on video content."""
|
| 53 |
+
l2_dist=(input_video_original[:,:,end_idx-chunk_size:end_idx]-input_video_original[:,:,end_idx-chunk_size-1:end_idx-1])**2
|
| 54 |
+
l2_dist = (torch.sqrt(l2_dist.mean(dim=(0,1,3,4))).max()/0.2).clamp(0,1)
|
| 55 |
+
new_noise_scale = (init_noise_scale-0.1*l2_dist.item())*0.9+noise_scale*0.1
|
| 56 |
+
current_step = int(1000*new_noise_scale)-100
|
| 57 |
+
return new_noise_scale, current_step
|
| 58 |
+
|
| 59 |
+
class SingleGPUInferencePipeline:
|
| 60 |
+
"""
|
| 61 |
+
Single GPU Inference Pipeline Manager
|
| 62 |
+
|
| 63 |
+
This class encapsulates the complete inference logic on a single GPU,
|
| 64 |
+
including encoding, inference, and decoding.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __init__(self, config, device: torch.device):
|
| 68 |
+
"""
|
| 69 |
+
Initialize the single GPU inference pipeline manager.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
config: Configuration object
|
| 73 |
+
device: GPU device
|
| 74 |
+
"""
|
| 75 |
+
self.config = config
|
| 76 |
+
self.device = device
|
| 77 |
+
|
| 78 |
+
# Setup logging
|
| 79 |
+
self.logger = logging.getLogger("SingleGPUInference")
|
| 80 |
+
self.logger.setLevel(logging.INFO)
|
| 81 |
+
# Prevent messages from propagating to the root logger (avoid double prints)
|
| 82 |
+
self.logger.propagate = False
|
| 83 |
+
|
| 84 |
+
if not self.logger.handlers:
|
| 85 |
+
handler = logging.StreamHandler()
|
| 86 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 87 |
+
handler.setFormatter(formatter)
|
| 88 |
+
self.logger.addHandler(handler)
|
| 89 |
+
|
| 90 |
+
# Initialize pipeline
|
| 91 |
+
self.pipeline = CausalStreamInferencePipeline(config, device=str(device))
|
| 92 |
+
self.pipeline.to(device=str(device), dtype=torch.bfloat16)
|
| 93 |
+
|
| 94 |
+
# Performance tracking
|
| 95 |
+
self.t_dit = 100.0
|
| 96 |
+
self.t_total = 100.0
|
| 97 |
+
self.processed = 0
|
| 98 |
+
self.processed_offset = 3
|
| 99 |
+
self.base_chunk_size = 4
|
| 100 |
+
self.t_refresh = 50
|
| 101 |
+
|
| 102 |
+
self.t2v = config.t2v
|
| 103 |
+
self.profile = bool(config.get("profile", False))
|
| 104 |
+
self.encode_fps_list: list[float] = []
|
| 105 |
+
self.decode_fps_list: list[float] = []
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
self.logger.info("Single GPU inference pipeline manager initialized")
|
| 109 |
+
|
| 110 |
+
def load_model(self, checkpoint_folder: str):
|
| 111 |
+
"""Load the model from checkpoint."""
|
| 112 |
+
ckpt_path, state_dict = load_generator_state_dict(checkpoint_folder)
|
| 113 |
+
self.logger.info(f"Loading checkpoint from {ckpt_path}")
|
| 114 |
+
|
| 115 |
+
# Load into the pipeline generator
|
| 116 |
+
try:
|
| 117 |
+
self.pipeline.generator.load_state_dict(state_dict, strict=True)
|
| 118 |
+
except RuntimeError as e:
|
| 119 |
+
# Try non-strict load as a fallback and report
|
| 120 |
+
self.logger.warning(f"Strict load_state_dict failed: {e}; retrying with strict=False")
|
| 121 |
+
self.pipeline.generator.load_state_dict(state_dict, strict=False)
|
| 122 |
+
|
| 123 |
+
def prepare_pipeline(self, text_prompts: list, noise: torch.Tensor, current_start: int, current_end: int):
|
| 124 |
+
"""Prepare the pipeline for inference."""
|
| 125 |
+
# Use the original prepare method which now handles distributed environment gracefully
|
| 126 |
+
denoised_pred = self.pipeline.prepare(
|
| 127 |
+
text_prompts=text_prompts,
|
| 128 |
+
device=self.device,
|
| 129 |
+
dtype=torch.bfloat16,
|
| 130 |
+
block_mode='input',
|
| 131 |
+
noise=noise,
|
| 132 |
+
current_start=current_start,
|
| 133 |
+
current_end=current_end
|
| 134 |
+
)
|
| 135 |
+
return denoised_pred
|
| 136 |
+
|
| 137 |
+
def _sync_for_timing(self):
|
| 138 |
+
if self.profile:
|
| 139 |
+
torch.cuda.synchronize()
|
| 140 |
+
|
| 141 |
+
def _record_stage_fps(self, values: list[float], num_frames: int, elapsed: float) -> None:
|
| 142 |
+
if self.profile and elapsed > 0 and num_frames > 0:
|
| 143 |
+
values.append(num_frames / elapsed)
|
| 144 |
+
|
| 145 |
+
def _timed_stream_encode(self, images: torch.Tensor) -> torch.Tensor:
|
| 146 |
+
self._sync_for_timing()
|
| 147 |
+
start_time = time.time()
|
| 148 |
+
latents = self.pipeline.vae.stream_encode(images)
|
| 149 |
+
self._sync_for_timing()
|
| 150 |
+
self._record_stage_fps(self.encode_fps_list, int(images.shape[2]), time.time() - start_time)
|
| 151 |
+
return latents
|
| 152 |
+
|
| 153 |
+
def _timed_stream_decode(self, denoised_pred: torch.Tensor) -> torch.Tensor:
|
| 154 |
+
self._sync_for_timing()
|
| 155 |
+
start_time = time.time()
|
| 156 |
+
video = self.pipeline.vae.stream_decode_to_pixel(denoised_pred)
|
| 157 |
+
self._sync_for_timing()
|
| 158 |
+
self._record_stage_fps(self.decode_fps_list, int(video.shape[1]), time.time() - start_time)
|
| 159 |
+
return video
|
| 160 |
+
|
| 161 |
+
def reset_stream_state(self, reset_vae_flags: bool = True) -> None:
|
| 162 |
+
"""Reset cached model state before starting a new streaming session."""
|
| 163 |
+
if reset_vae_flags:
|
| 164 |
+
self.pipeline.vae.model.first_encode = True
|
| 165 |
+
self.pipeline.vae.model.first_decode = True
|
| 166 |
+
|
| 167 |
+
self.pipeline.kv_cache1 = None
|
| 168 |
+
self.pipeline.crossattn_cache = None
|
| 169 |
+
self.pipeline.block_x = None
|
| 170 |
+
self.pipeline.hidden_states = None
|
| 171 |
+
self.processed = 0
|
| 172 |
+
|
| 173 |
+
def _encode_noisy_latents(self, images: torch.Tensor, noise_scale: float) -> torch.Tensor:
|
| 174 |
+
latents = self._timed_stream_encode(images)
|
| 175 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 176 |
+
noise = torch.randn_like(latents)
|
| 177 |
+
return noise * noise_scale + latents * (1 - noise_scale)
|
| 178 |
+
|
| 179 |
+
def _decode_video_array(self, denoised_pred: torch.Tensor, last_frame_only: bool = False) -> np.ndarray:
|
| 180 |
+
if last_frame_only:
|
| 181 |
+
denoised_pred = denoised_pred[[-1]]
|
| 182 |
+
|
| 183 |
+
video = self._timed_stream_decode(denoised_pred)
|
| 184 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 185 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 186 |
+
return video.detach().cpu().float().numpy()
|
| 187 |
+
|
| 188 |
+
def start_stream_session(self, prompt: str, images: torch.Tensor, noise_scale: float) -> tuple[SingleGPUStreamSession, np.ndarray]:
|
| 189 |
+
"""Initialize a streaming session and return the first decoded frames."""
|
| 190 |
+
self.reset_stream_state(reset_vae_flags=True)
|
| 191 |
+
|
| 192 |
+
chunk_size = self.base_chunk_size * self.pipeline.num_frame_per_block
|
| 193 |
+
current_start = 0
|
| 194 |
+
current_end = self.pipeline.frame_seq_length * (1 + chunk_size // self.base_chunk_size)
|
| 195 |
+
|
| 196 |
+
noisy_latents = self._encode_noisy_latents(images, noise_scale)
|
| 197 |
+
denoised_pred = self.prepare_pipeline(
|
| 198 |
+
text_prompts=[prompt],
|
| 199 |
+
noise=noisy_latents,
|
| 200 |
+
current_start=current_start,
|
| 201 |
+
current_end=current_end,
|
| 202 |
+
)
|
| 203 |
+
initial_video = self._decode_video_array(denoised_pred, last_frame_only=False)
|
| 204 |
+
|
| 205 |
+
session = SingleGPUStreamSession(
|
| 206 |
+
prompt=prompt,
|
| 207 |
+
noise_scale=noise_scale,
|
| 208 |
+
init_noise_scale=noise_scale,
|
| 209 |
+
chunk_size=chunk_size,
|
| 210 |
+
current_start=current_end,
|
| 211 |
+
current_end=current_end + (chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length,
|
| 212 |
+
last_image=images[:, :, [-1]],
|
| 213 |
+
processed=0,
|
| 214 |
+
)
|
| 215 |
+
return session, initial_video
|
| 216 |
+
|
| 217 |
+
def run_stream_batch(self, session: SingleGPUStreamSession, images: torch.Tensor, queue_wait_time: float | None = None) -> List[np.ndarray]:
|
| 218 |
+
"""Process one or more chunk-aligned frame groups for an active streaming session."""
|
| 219 |
+
num_frames = images.shape[2]
|
| 220 |
+
input_batch = num_frames // session.chunk_size
|
| 221 |
+
noise_scale, current_step = compute_noise_scale_and_step(
|
| 222 |
+
input_video_original=torch.cat([session.last_image, images], dim=2),
|
| 223 |
+
end_idx=num_frames + 1,
|
| 224 |
+
chunk_size=num_frames,
|
| 225 |
+
noise_scale=float(session.noise_scale),
|
| 226 |
+
init_noise_scale=float(session.init_noise_scale),
|
| 227 |
+
)
|
| 228 |
+
noisy_latents = self._encode_noisy_latents(images, noise_scale)
|
| 229 |
+
|
| 230 |
+
outputs: List[np.ndarray] = []
|
| 231 |
+
num_steps = len(self.pipeline.denoising_step_list)
|
| 232 |
+
|
| 233 |
+
for batch_idx in range(input_batch):
|
| 234 |
+
if session.current_start // self.pipeline.frame_seq_length >= self.t_refresh:
|
| 235 |
+
session.current_start = self.pipeline.kv_cache_length - self.pipeline.frame_seq_length
|
| 236 |
+
session.current_end = session.current_start + (session.chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 237 |
+
|
| 238 |
+
denoised_pred = self.pipeline.inference_stream(
|
| 239 |
+
noise=noisy_latents[:, batch_idx].unsqueeze(1),
|
| 240 |
+
current_start=session.current_start,
|
| 241 |
+
current_end=session.current_end,
|
| 242 |
+
current_step=current_step,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
session.processed += 1
|
| 246 |
+
self.processed = session.processed
|
| 247 |
+
|
| 248 |
+
if session.processed >= num_steps:
|
| 249 |
+
outputs.append(self._decode_video_array(denoised_pred, last_frame_only=True))
|
| 250 |
+
|
| 251 |
+
session.current_start = session.current_end
|
| 252 |
+
session.current_end += (session.chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 253 |
+
|
| 254 |
+
session.last_image = images[:, :, [-1]]
|
| 255 |
+
session.noise_scale = noise_scale
|
| 256 |
+
return outputs
|
| 257 |
+
|
| 258 |
+
def run_inference(
|
| 259 |
+
self,
|
| 260 |
+
input_video_original: torch.Tensor,
|
| 261 |
+
prompts: list,
|
| 262 |
+
num_chunks: int,
|
| 263 |
+
chunk_size: int,
|
| 264 |
+
noise_scale: float,
|
| 265 |
+
output_folder: str,
|
| 266 |
+
fps: int,
|
| 267 |
+
target_fps:int,
|
| 268 |
+
num_steps: int,
|
| 269 |
+
):
|
| 270 |
+
"""
|
| 271 |
+
Run the complete single GPU inference pipeline.
|
| 272 |
+
|
| 273 |
+
This method integrates the complete encoding, inference, and decoding pipeline.
|
| 274 |
+
"""
|
| 275 |
+
self.logger.info("Starting single GPU inference pipeline")
|
| 276 |
+
|
| 277 |
+
os.makedirs(output_folder, exist_ok=True)
|
| 278 |
+
results = {}
|
| 279 |
+
save_results = 0
|
| 280 |
+
|
| 281 |
+
fps_list = []
|
| 282 |
+
dit_fps_list = []
|
| 283 |
+
self.encode_fps_list = []
|
| 284 |
+
self.decode_fps_list = []
|
| 285 |
+
|
| 286 |
+
# Initialize variables
|
| 287 |
+
start_idx = 0
|
| 288 |
+
if self.t2v:
|
| 289 |
+
end_idx = 1 + chunk_size - 4
|
| 290 |
+
else:
|
| 291 |
+
end_idx = 1 + chunk_size
|
| 292 |
+
current_start = 0
|
| 293 |
+
current_end = self.pipeline.frame_seq_length * (1+(end_idx-1)//4)
|
| 294 |
+
|
| 295 |
+
self._sync_for_timing()
|
| 296 |
+
start_time = time.time()
|
| 297 |
+
|
| 298 |
+
# Process first chunk (initialization)
|
| 299 |
+
if not self.t2v:
|
| 300 |
+
inp = input_video_original[:, :, start_idx:end_idx]
|
| 301 |
+
|
| 302 |
+
# VAE encoding
|
| 303 |
+
latents = self._timed_stream_encode(inp)
|
| 304 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 305 |
+
|
| 306 |
+
noise = torch.randn_like(latents)
|
| 307 |
+
noisy_latents = noise * noise_scale + latents * (1 - noise_scale)
|
| 308 |
+
else:
|
| 309 |
+
noisy_latents = torch.randn(1,self.pipeline.num_frame_per_block,16,self.pipeline.height,self.pipeline.width, device=self.device, dtype=torch.bfloat16)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
# Prepare pipeline
|
| 313 |
+
denoised_pred = self.prepare_pipeline(
|
| 314 |
+
text_prompts=prompts,
|
| 315 |
+
noise=noisy_latents,
|
| 316 |
+
current_start=current_start,
|
| 317 |
+
current_end=current_end
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
# Save first result - only start decoding after num_steps
|
| 321 |
+
video = self._timed_stream_decode(denoised_pred)
|
| 322 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 323 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 324 |
+
results[save_results] = video.cpu().float().numpy()
|
| 325 |
+
self.logger.info(
|
| 326 |
+
"Prepared initial chunk: start=%s, end=%s, start_idx=%s, save_results=%s, frames=%s",
|
| 327 |
+
current_start,
|
| 328 |
+
current_end,
|
| 329 |
+
start_idx,
|
| 330 |
+
save_results,
|
| 331 |
+
video.shape[0],
|
| 332 |
+
)
|
| 333 |
+
save_results += 1
|
| 334 |
+
|
| 335 |
+
init_noise_scale = noise_scale
|
| 336 |
+
|
| 337 |
+
# Process remaining chunks
|
| 338 |
+
while self.processed < num_chunks + num_steps - 1:
|
| 339 |
+
# Update indices
|
| 340 |
+
start_idx = end_idx
|
| 341 |
+
end_idx = end_idx + chunk_size
|
| 342 |
+
current_start = current_end
|
| 343 |
+
current_end = current_end + (chunk_size // 4) * self.pipeline.frame_seq_length
|
| 344 |
+
|
| 345 |
+
if not self.t2v and end_idx <= input_video_original.shape[2]:
|
| 346 |
+
inp = input_video_original[:, :, start_idx:end_idx]
|
| 347 |
+
|
| 348 |
+
noise_scale, current_step = compute_noise_scale_and_step(
|
| 349 |
+
input_video_original, end_idx, chunk_size, noise_scale, init_noise_scale
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
# VAE encoding
|
| 353 |
+
latents = self._timed_stream_encode(inp)
|
| 354 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 355 |
+
|
| 356 |
+
noise = torch.randn_like(latents)
|
| 357 |
+
noisy_latents = noise * noise_scale + latents * (1 - noise_scale)
|
| 358 |
+
else:
|
| 359 |
+
noisy_latents = torch.randn(1,self.pipeline.num_frame_per_block,16,self.pipeline.height,self.pipeline.width, device=self.device, dtype=torch.bfloat16)
|
| 360 |
+
current_step = None # Use default steps
|
| 361 |
+
|
| 362 |
+
self._sync_for_timing()
|
| 363 |
+
dit_start_time = time.time()
|
| 364 |
+
|
| 365 |
+
# DiT inference - using input mode to process all 30 blocks
|
| 366 |
+
denoised_pred = self.pipeline.inference_stream(
|
| 367 |
+
noise=noisy_latents,
|
| 368 |
+
current_start=current_start,
|
| 369 |
+
current_end=current_end,
|
| 370 |
+
current_step=current_step,
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
if self.processed > self.processed_offset:
|
| 374 |
+
self._sync_for_timing()
|
| 375 |
+
if self.profile:
|
| 376 |
+
dit_fps_list.append(chunk_size / (time.time() - dit_start_time))
|
| 377 |
+
|
| 378 |
+
self.processed += 1
|
| 379 |
+
|
| 380 |
+
# VAE decoding - only start decoding after num_steps
|
| 381 |
+
if self.processed >= num_steps:
|
| 382 |
+
if self.t2v and self.processed == num_steps:
|
| 383 |
+
continue
|
| 384 |
+
video = self._timed_stream_decode(denoised_pred[[-1]])
|
| 385 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 386 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 387 |
+
results[save_results] = video.cpu().float().numpy()
|
| 388 |
+
save_results += 1
|
| 389 |
+
|
| 390 |
+
# Update timing
|
| 391 |
+
if self.profile:
|
| 392 |
+
self._sync_for_timing()
|
| 393 |
+
end_time = time.time()
|
| 394 |
+
t = end_time - start_time
|
| 395 |
+
fps_test = chunk_size / t
|
| 396 |
+
fps_list.append(fps_test)
|
| 397 |
+
self.logger.info(f"Processed {self.processed}, time: {t:.4f} s, FPS: {fps_test:.4f}")
|
| 398 |
+
else:
|
| 399 |
+
fps_test = None
|
| 400 |
+
|
| 401 |
+
if self.processed == num_steps + self.processed_offset and target_fps is not None and fps_test is not None and fps_test < target_fps:
|
| 402 |
+
max_chunk_size = (self.pipeline.num_kv_cache - self.pipeline.num_sink_tokens - 1) * self.base_chunk_size
|
| 403 |
+
num_chunks=(num_chunks-self.processed-num_steps+1)//(max_chunk_size//chunk_size)+self.processed-num_steps+1
|
| 404 |
+
self.pipeline.hidden_states=self.pipeline.hidden_states.repeat(1,max_chunk_size//chunk_size,1,1,1)
|
| 405 |
+
chunk_size = max_chunk_size
|
| 406 |
+
self.logger.info(f"Adjust chunk size to {chunk_size}")
|
| 407 |
+
|
| 408 |
+
if self.profile:
|
| 409 |
+
start_time = end_time
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
# Save final video
|
| 413 |
+
video_list = [results[i] for i in range(num_chunks)]
|
| 414 |
+
video = np.concatenate(video_list, axis=0)
|
| 415 |
+
if self.profile and fps_list:
|
| 416 |
+
fps_avg = np.mean(np.array(fps_list))
|
| 417 |
+
dit_avg = np.mean(np.array(dit_fps_list)) if dit_fps_list else 0.0
|
| 418 |
+
encode_avg = np.mean(np.array(self.encode_fps_list)) if self.encode_fps_list else 0.0
|
| 419 |
+
decode_avg = np.mean(np.array(self.decode_fps_list)) if self.decode_fps_list else 0.0
|
| 420 |
+
self.logger.info(f"VAE Encode Average FPS: {encode_avg:.4f}")
|
| 421 |
+
self.logger.info(f"DiT Average FPS: {dit_avg:.4f}")
|
| 422 |
+
self.logger.info(f"VAE Decode Average FPS: {decode_avg:.4f}")
|
| 423 |
+
self.logger.info(f"Video shape: {video.shape}, Average FPS: {fps_avg:.4f}")
|
| 424 |
+
else:
|
| 425 |
+
self.logger.info(f"Video shape: {video.shape}")
|
| 426 |
+
|
| 427 |
+
output_path = os.path.join(output_folder, f"output_{0:03d}.mp4")
|
| 428 |
+
export_to_video(video, output_path, fps=fps)
|
| 429 |
+
self.logger.info(f"Video saved to: {output_path}")
|
| 430 |
+
|
| 431 |
+
self.logger.info("Single GPU inference pipeline completed")
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def main():
|
| 435 |
+
"""Main function for the single GPU inference pipeline."""
|
| 436 |
+
parser = argparse.ArgumentParser()
|
| 437 |
+
parser.add_argument("--config_path", type=str, required=True, help="Configuration file path")
|
| 438 |
+
parser.add_argument("--checkpoint_folder", type=str, required=True, help="Checkpoint folder path")
|
| 439 |
+
parser.add_argument("--output_folder", type=str, required=True, help="Output folder path")
|
| 440 |
+
parser.add_argument("--prompt_file_path", type=str, required=True, help="Prompt file path")
|
| 441 |
+
parser.add_argument("--video_path", type=str, required=False, default=None, help="Input video path")
|
| 442 |
+
parser.add_argument("--noise_scale", type=float, default=0.8, help="Noise scale")
|
| 443 |
+
parser.add_argument("--height", type=int, default=480, help="Video height")
|
| 444 |
+
parser.add_argument("--width", type=int, default=832, help="Video width")
|
| 445 |
+
parser.add_argument("--fps", type=int, default=16, help="Output video fps")
|
| 446 |
+
parser.add_argument("--step", type=int, default=2, help="Step")
|
| 447 |
+
parser.add_argument("--seed", type=int, default=0, help="Random seed")
|
| 448 |
+
parser.add_argument("--gpu_id", type=int, default=None, help="CUDA device index for single-GPU inference")
|
| 449 |
+
parser.add_argument("--model_type", type=str, default="T2V-1.3B", help="Model type (e.g., T2V-1.3B)")
|
| 450 |
+
parser.add_argument("--num_frames", type=int, default=81, help="Video length (number of frames)")
|
| 451 |
+
parser.add_argument("--fixed_noise_scale", action="store_true", default=False)
|
| 452 |
+
parser.add_argument("--t2v", action="store_true", default=False)
|
| 453 |
+
parser.add_argument("--target_fps", type=int, required=False, default=None, help="Video length (number of frames)")
|
| 454 |
+
parser.add_argument("--profile", action="store_true", default=False, help="Enable synchronized throughput logging")
|
| 455 |
+
parser.add_argument("--use_taehv", action="store_true", default=False, help="Use the lightweight TAEHV VAE for encode/decode")
|
| 456 |
+
parser.add_argument("--use_tensorrt", "--use_taehv_tensorrt", dest="use_tensorrt", action="store_true", default=False, help="Enable available TensorRT acceleration paths")
|
| 457 |
+
parser.add_argument("--fast", action="store_true", default=False, help="Enable the fast path: --use_taehv --use_tensorrt")
|
| 458 |
+
args = parser.parse_args()
|
| 459 |
+
|
| 460 |
+
torch.set_grad_enabled(False)
|
| 461 |
+
|
| 462 |
+
# Auto-detect device
|
| 463 |
+
if torch.cuda.is_available():
|
| 464 |
+
if args.gpu_id is not None:
|
| 465 |
+
torch.cuda.set_device(args.gpu_id)
|
| 466 |
+
device = torch.device(f"cuda:{args.gpu_id}")
|
| 467 |
+
else:
|
| 468 |
+
device = torch.device("cuda")
|
| 469 |
+
else:
|
| 470 |
+
device = torch.device("cpu")
|
| 471 |
+
|
| 472 |
+
# Load configuration
|
| 473 |
+
config = merge_cli_config(args.config_path, args)
|
| 474 |
+
|
| 475 |
+
set_seed(args.seed)
|
| 476 |
+
|
| 477 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 478 |
+
LOGGER.info("Denoising Step List: %s", list(config.denoising_step_list))
|
| 479 |
+
|
| 480 |
+
# Load input video
|
| 481 |
+
if not args.t2v:
|
| 482 |
+
input_video_original = load_mp4_as_tensor(args.video_path, resize_hw=(args.height, args.width)).unsqueeze(0)
|
| 483 |
+
LOGGER.info("Input video tensor shape: %s", tuple(input_video_original.shape))
|
| 484 |
+
b, c, t, h, w = input_video_original.shape
|
| 485 |
+
if input_video_original.dtype != torch.bfloat16:
|
| 486 |
+
input_video_original = input_video_original.to(dtype=torch.bfloat16).to(device)
|
| 487 |
+
else:
|
| 488 |
+
input_video_original = None
|
| 489 |
+
t = args.num_frames
|
| 490 |
+
|
| 491 |
+
# Calculate number of chunks
|
| 492 |
+
chunk_size = 4 * config.num_frame_per_block
|
| 493 |
+
num_chunks = (t - 1) // chunk_size
|
| 494 |
+
|
| 495 |
+
if args.t2v:
|
| 496 |
+
num_chunks+=1
|
| 497 |
+
# Initialize pipeline manager
|
| 498 |
+
pipeline_manager = SingleGPUInferencePipeline(config, device)
|
| 499 |
+
pipeline_manager.load_model(args.checkpoint_folder)
|
| 500 |
+
|
| 501 |
+
# Load prompts
|
| 502 |
+
dataset = TextDataset(args.prompt_file_path)
|
| 503 |
+
prompts = [dataset[0]]
|
| 504 |
+
num_steps = len(pipeline_manager.pipeline.denoising_step_list)
|
| 505 |
+
|
| 506 |
+
# Run inference
|
| 507 |
+
try:
|
| 508 |
+
pipeline_manager.run_inference(
|
| 509 |
+
input_video_original,
|
| 510 |
+
prompts,
|
| 511 |
+
num_chunks,
|
| 512 |
+
chunk_size,
|
| 513 |
+
args.noise_scale,
|
| 514 |
+
args.output_folder,
|
| 515 |
+
args.fps,
|
| 516 |
+
args.target_fps,
|
| 517 |
+
num_steps,
|
| 518 |
+
)
|
| 519 |
+
except Exception as e:
|
| 520 |
+
LOGGER.exception("Error occurred during inference: %s", e)
|
| 521 |
+
raise
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
if __name__ == "__main__":
|
| 525 |
+
main()
|
streamv2v/inference_common.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared helpers for the StreamDiffusionV2 inference entrypoints."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import av
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
import torchvision
|
| 10 |
+
import torchvision.transforms.functional as TF
|
| 11 |
+
from einops import rearrange
|
| 12 |
+
from omegaconf import OmegaConf
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _read_video_with_av(video_path: str) -> torch.Tensor:
|
| 16 |
+
"""Read a video with PyAV when torchvision's legacy video API is absent."""
|
| 17 |
+
frames = []
|
| 18 |
+
with av.open(video_path) as container:
|
| 19 |
+
stream = container.streams.video[0]
|
| 20 |
+
for frame in container.decode(stream):
|
| 21 |
+
frames.append(frame.to_rgb().to_ndarray())
|
| 22 |
+
|
| 23 |
+
if not frames:
|
| 24 |
+
raise ValueError(f"No video frames decoded from {video_path}")
|
| 25 |
+
|
| 26 |
+
video = np.stack(frames, axis=0)
|
| 27 |
+
return torch.from_numpy(video).permute(0, 3, 1, 2).contiguous()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def load_mp4_as_tensor(
|
| 31 |
+
video_path: str,
|
| 32 |
+
max_frames: int = None,
|
| 33 |
+
resize_hw: tuple[int, int] = None,
|
| 34 |
+
normalize: bool = True,
|
| 35 |
+
) -> torch.Tensor:
|
| 36 |
+
"""Load an mp4 video as a tensor with shape [C, T, H, W]."""
|
| 37 |
+
assert os.path.exists(video_path), f"Video file not found: {video_path}"
|
| 38 |
+
|
| 39 |
+
if hasattr(torchvision.io, "read_video"):
|
| 40 |
+
video, _, _ = torchvision.io.read_video(video_path, output_format="TCHW")
|
| 41 |
+
else:
|
| 42 |
+
video = _read_video_with_av(video_path)
|
| 43 |
+
if max_frames is not None:
|
| 44 |
+
video = video[:max_frames]
|
| 45 |
+
|
| 46 |
+
video = rearrange(video, "t c h w -> c t h w")
|
| 47 |
+
if resize_hw is not None:
|
| 48 |
+
_, t, _, _ = video.shape
|
| 49 |
+
video = torch.stack(
|
| 50 |
+
[TF.resize(video[:, i], resize_hw, antialias=True) for i in range(t)],
|
| 51 |
+
dim=1,
|
| 52 |
+
)
|
| 53 |
+
if video.dtype != torch.float32:
|
| 54 |
+
video = video.float()
|
| 55 |
+
if normalize:
|
| 56 |
+
video = video / 127.5 - 1.0
|
| 57 |
+
|
| 58 |
+
return video
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def resolve_config_path(config_path: str, args) -> str:
|
| 62 |
+
"""Select an alternate config file when runtime flags imply one."""
|
| 63 |
+
fast = bool(args.get("fast", False)) if isinstance(args, dict) else bool(getattr(args, "fast", False))
|
| 64 |
+
if not fast:
|
| 65 |
+
return config_path
|
| 66 |
+
|
| 67 |
+
base_name = os.path.basename(config_path)
|
| 68 |
+
if base_name != "wan_causal_dmd_v2v.yaml":
|
| 69 |
+
return config_path
|
| 70 |
+
|
| 71 |
+
fast_config_path = os.path.join(os.path.dirname(config_path), "wan_causal_dmd_v2v_fast.yaml")
|
| 72 |
+
return fast_config_path if os.path.exists(fast_config_path) else config_path
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def merge_cli_config(config_path: str, args) -> OmegaConf:
|
| 76 |
+
"""Load a YAML config and overlay CLI arguments onto it."""
|
| 77 |
+
config_path = resolve_config_path(config_path, args)
|
| 78 |
+
config = OmegaConf.load(config_path)
|
| 79 |
+
cli_config = OmegaConf.create(vars(args) if not isinstance(args, dict) else args)
|
| 80 |
+
config = OmegaConf.merge(config, cli_config)
|
| 81 |
+
config = normalize_acceleration_flags(config)
|
| 82 |
+
|
| 83 |
+
# CLI --step should always select the first N non-zero denoising steps from
|
| 84 |
+
# the canonical YAML schedule, then append the terminal zero step back.
|
| 85 |
+
full_denoising_list = list(config.denoising_step_list)
|
| 86 |
+
non_terminal_steps = [step for step in full_denoising_list if int(step) != 0]
|
| 87 |
+
step_value = int(config.step)
|
| 88 |
+
config.denoising_step_list = non_terminal_steps[:step_value]
|
| 89 |
+
config.denoising_step_list.append(0)
|
| 90 |
+
return config
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def load_generator_state_dict(checkpoint_folder: str):
|
| 94 |
+
"""Load the generator weights from a checkpoint folder."""
|
| 95 |
+
ckpt_path = os.path.join(checkpoint_folder, "model.pt")
|
| 96 |
+
checkpoint = torch.load(ckpt_path, map_location="cpu")
|
| 97 |
+
|
| 98 |
+
def add_model_prefix(state_dict):
|
| 99 |
+
return {
|
| 100 |
+
key if key.startswith("model.") else f"model.{key}": value
|
| 101 |
+
for key, value in state_dict.items()
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if isinstance(checkpoint, dict):
|
| 105 |
+
for key in ("generator", "generator_ema", "state_dict"):
|
| 106 |
+
if key in checkpoint:
|
| 107 |
+
return ckpt_path, add_model_prefix(checkpoint[key])
|
| 108 |
+
|
| 109 |
+
return ckpt_path, add_model_prefix(checkpoint)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _get_flag(config: Any, key: str, default=False):
|
| 113 |
+
if isinstance(config, dict):
|
| 114 |
+
return config.get(key, default)
|
| 115 |
+
return getattr(config, key, default)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _set_flag(config: Any, key: str, value) -> None:
|
| 119 |
+
if isinstance(config, dict):
|
| 120 |
+
config[key] = value
|
| 121 |
+
else:
|
| 122 |
+
setattr(config, key, value)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def normalize_acceleration_flags(config):
|
| 126 |
+
"""Apply shared CLI/runtime flag semantics for fast and TensorRT modes."""
|
| 127 |
+
use_taehv = bool(_get_flag(config, "use_taehv", False))
|
| 128 |
+
use_tensorrt = bool(_get_flag(config, "use_tensorrt", False))
|
| 129 |
+
fast = bool(_get_flag(config, "fast", False))
|
| 130 |
+
|
| 131 |
+
if fast:
|
| 132 |
+
use_taehv = True
|
| 133 |
+
use_tensorrt = True
|
| 134 |
+
|
| 135 |
+
# The current TensorRT path is implemented on top of the TAEHV decoder.
|
| 136 |
+
if use_tensorrt:
|
| 137 |
+
use_taehv = True
|
| 138 |
+
|
| 139 |
+
_set_flag(config, "use_taehv", use_taehv)
|
| 140 |
+
_set_flag(config, "use_tensorrt", use_tensorrt)
|
| 141 |
+
_set_flag(config, "fast", fast)
|
| 142 |
+
return config
|
streamv2v/inference_pipe.py
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Refactored multi-rank inference pipeline with communication abstractions.
|
| 3 |
+
|
| 4 |
+
This is a refactored version of inference_pipe_multi.py that uses the new
|
| 5 |
+
communication abstraction layers for better code organization and maintainability.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from models.wan.causal_stream_inference import CausalStreamInferencePipeline
|
| 9 |
+
from models.util import set_seed
|
| 10 |
+
from diffusers.utils import export_to_video
|
| 11 |
+
from models.data import TextDataset
|
| 12 |
+
import argparse
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
import torch
|
| 15 |
+
import torch.distributed as dist
|
| 16 |
+
import os
|
| 17 |
+
import time
|
| 18 |
+
import numpy as np
|
| 19 |
+
import logging
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from streamv2v.inference import compute_noise_scale_and_step
|
| 23 |
+
from streamv2v.communication import (
|
| 24 |
+
DistributedCommunicator,
|
| 25 |
+
ModelDataTransfer,
|
| 26 |
+
BufferManager,
|
| 27 |
+
KVCacheManager,
|
| 28 |
+
CommunicationConfig,
|
| 29 |
+
init_distributed,
|
| 30 |
+
setup_logging,
|
| 31 |
+
compute_balanced_split
|
| 32 |
+
)
|
| 33 |
+
from streamv2v.inference_common import (
|
| 34 |
+
load_generator_state_dict,
|
| 35 |
+
load_mp4_as_tensor,
|
| 36 |
+
merge_cli_config,
|
| 37 |
+
)
|
| 38 |
+
except ModuleNotFoundError:
|
| 39 |
+
from inference import compute_noise_scale_and_step
|
| 40 |
+
from communication import (
|
| 41 |
+
DistributedCommunicator,
|
| 42 |
+
ModelDataTransfer,
|
| 43 |
+
BufferManager,
|
| 44 |
+
KVCacheManager,
|
| 45 |
+
CommunicationConfig,
|
| 46 |
+
init_distributed,
|
| 47 |
+
setup_logging,
|
| 48 |
+
compute_balanced_split
|
| 49 |
+
)
|
| 50 |
+
from inference_common import (
|
| 51 |
+
load_generator_state_dict,
|
| 52 |
+
load_mp4_as_tensor,
|
| 53 |
+
merge_cli_config,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
LOGGER = logging.getLogger(__name__)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def compute_default_block_distribution(total_blocks: int, world_size: int) -> list[list[int]]:
|
| 60 |
+
"""Split transformer blocks into contiguous ranges for each rank."""
|
| 61 |
+
if world_size == 2:
|
| 62 |
+
midpoint = total_blocks // 2
|
| 63 |
+
return [[0, midpoint], [midpoint, total_blocks]]
|
| 64 |
+
|
| 65 |
+
base = total_blocks // world_size
|
| 66 |
+
rem = total_blocks % world_size
|
| 67 |
+
start = 0
|
| 68 |
+
block_ranges = []
|
| 69 |
+
for rank in range(world_size):
|
| 70 |
+
size = base + (1 if rank < rem else 0)
|
| 71 |
+
end = start + size if rank < world_size - 1 else total_blocks
|
| 72 |
+
block_ranges.append([start, end])
|
| 73 |
+
start = end
|
| 74 |
+
return block_ranges
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@dataclass
|
| 78 |
+
class MultiGPUDemoInputSession:
|
| 79 |
+
prompt: str
|
| 80 |
+
noise_scale: float
|
| 81 |
+
init_noise_scale: float
|
| 82 |
+
chunk_size: int
|
| 83 |
+
current_start: int
|
| 84 |
+
current_end: int
|
| 85 |
+
last_image: torch.Tensor
|
| 86 |
+
chunk_idx: int = 0
|
| 87 |
+
input_batch: int = 0
|
| 88 |
+
current_step: int = 0
|
| 89 |
+
noisy_latents: torch.Tensor | None = None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class InferencePipelineManager:
|
| 93 |
+
"""
|
| 94 |
+
Manages the inference pipeline with communication abstractions.
|
| 95 |
+
|
| 96 |
+
This class encapsulates the main inference logic and uses the communication
|
| 97 |
+
abstractions for distributed operations.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def __init__(self, config, device: torch.device, rank: int, world_size: int):
|
| 101 |
+
"""
|
| 102 |
+
Initialize the inference pipeline manager.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
config: Configuration object
|
| 106 |
+
device: GPU device
|
| 107 |
+
rank: Current rank
|
| 108 |
+
world_size: Total number of ranks
|
| 109 |
+
"""
|
| 110 |
+
self.config = config
|
| 111 |
+
self.device = device
|
| 112 |
+
self.rank = rank
|
| 113 |
+
self.world_size = world_size
|
| 114 |
+
|
| 115 |
+
self.com_stream = torch.cuda.Stream()
|
| 116 |
+
self.control_stream = torch.cuda.Stream()
|
| 117 |
+
|
| 118 |
+
# Setup logging
|
| 119 |
+
self.logger = setup_logging(rank)
|
| 120 |
+
|
| 121 |
+
# Initialize communication components
|
| 122 |
+
comm_config = CommunicationConfig(
|
| 123 |
+
max_outstanding=config.get('max_outstanding', 1),
|
| 124 |
+
buffer_pool_size=config.get('buffer_pool_size', 10),
|
| 125 |
+
enable_buffer_reuse=config.get('enable_buffer_reuse', True)
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
self.communicator = DistributedCommunicator(rank, world_size, device, comm_config)
|
| 129 |
+
self.buffer_manager = BufferManager(device, comm_config)
|
| 130 |
+
|
| 131 |
+
# Initialize pipeline
|
| 132 |
+
self.pipeline = CausalStreamInferencePipeline(config, device=str(device))
|
| 133 |
+
self.pipeline.to(device=str(device), dtype=torch.bfloat16)
|
| 134 |
+
|
| 135 |
+
# Initialize KV cache manager
|
| 136 |
+
self.kv_cache_manager = KVCacheManager(self.pipeline, device)
|
| 137 |
+
|
| 138 |
+
# Initialize model data transfer
|
| 139 |
+
self.data_transfer = ModelDataTransfer(
|
| 140 |
+
self.communicator,
|
| 141 |
+
self.buffer_manager,
|
| 142 |
+
self.kv_cache_manager,
|
| 143 |
+
comm_config
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Performance tracking
|
| 147 |
+
self.t_dit = 100.0
|
| 148 |
+
self.t_total = 100.0
|
| 149 |
+
self.processed = 0
|
| 150 |
+
self.schedule_step = (self.world_size + len(config.denoising_step_list)) * 2
|
| 151 |
+
self.processed_offset = 3
|
| 152 |
+
self.base_chunk_size = 4
|
| 153 |
+
self.t_refresh = 50
|
| 154 |
+
self.profile = bool(config.get('profile', False))
|
| 155 |
+
self.encode_fps_list: list[float] = []
|
| 156 |
+
self.decode_fps_list: list[float] = []
|
| 157 |
+
|
| 158 |
+
self.logger.info(f"Initialized InferencePipelineManager for rank {rank}")
|
| 159 |
+
|
| 160 |
+
def load_model(self, checkpoint_folder: str):
|
| 161 |
+
"""Load the model from checkpoint."""
|
| 162 |
+
ckpt_path, state_dict = load_generator_state_dict(checkpoint_folder)
|
| 163 |
+
try:
|
| 164 |
+
self.pipeline.generator.load_state_dict(state_dict, strict=True)
|
| 165 |
+
except RuntimeError as exc:
|
| 166 |
+
self.logger.warning(f"Strict load_state_dict failed: {exc}; retrying with strict=False")
|
| 167 |
+
self.pipeline.generator.load_state_dict(state_dict, strict=False)
|
| 168 |
+
self.logger.info(f"Model loaded successfully from {ckpt_path}")
|
| 169 |
+
|
| 170 |
+
def prepare_pipeline(self, text_prompts: list, noise: torch.Tensor,
|
| 171 |
+
block_mode: str, current_start: int, current_end: int, block_num: torch.Tensor):
|
| 172 |
+
"""Prepare the pipeline for inference."""
|
| 173 |
+
denoised_pred = self.pipeline.prepare(
|
| 174 |
+
text_prompts=text_prompts,
|
| 175 |
+
device=self.device,
|
| 176 |
+
dtype=torch.bfloat16,
|
| 177 |
+
noise=noise,
|
| 178 |
+
block_mode=block_mode,
|
| 179 |
+
current_start=current_start,
|
| 180 |
+
current_end=current_end,
|
| 181 |
+
block_num=block_num
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Broadcast the prepared result from rank 0
|
| 185 |
+
self.data_transfer.broadcast_tensor(denoised_pred, src=0)
|
| 186 |
+
return denoised_pred
|
| 187 |
+
|
| 188 |
+
def _wait_for_outstanding(self, outstanding: list) -> None:
|
| 189 |
+
"""Keep the number of queued async sends bounded."""
|
| 190 |
+
while len(outstanding) >= self.config.get('max_outstanding', 1):
|
| 191 |
+
oldest = outstanding.pop(0)
|
| 192 |
+
for work in oldest:
|
| 193 |
+
work.wait()
|
| 194 |
+
|
| 195 |
+
def _drain_outstanding(self, outstanding: list) -> None:
|
| 196 |
+
"""Wait for all queued async sends to complete."""
|
| 197 |
+
while outstanding:
|
| 198 |
+
oldest = outstanding.pop(0)
|
| 199 |
+
for work in oldest:
|
| 200 |
+
work.wait()
|
| 201 |
+
|
| 202 |
+
def _maybe_schedule_blocks(self, schedule_block: bool, threshold: int, block_num: torch.Tensor, total_blocks: int) -> bool:
|
| 203 |
+
"""Run one-time block rebalancing when the warmup threshold is reached."""
|
| 204 |
+
if schedule_block and self.processed >= threshold:
|
| 205 |
+
self._handle_block_scheduling(block_num, total_blocks)
|
| 206 |
+
return False
|
| 207 |
+
return schedule_block
|
| 208 |
+
|
| 209 |
+
def _receive_latent_data(self, previous_latent_data, num_steps: int):
|
| 210 |
+
"""Release the previous payload and receive the next one from the upstream rank."""
|
| 211 |
+
with torch.cuda.stream(self.com_stream):
|
| 212 |
+
if previous_latent_data is not None:
|
| 213 |
+
self.data_transfer.release_latent_data(previous_latent_data)
|
| 214 |
+
latent_data = self.data_transfer.receive_latent_data_async(num_steps)
|
| 215 |
+
torch.cuda.current_stream().wait_stream(self.com_stream)
|
| 216 |
+
return latent_data
|
| 217 |
+
|
| 218 |
+
def _run_worker_stage(self, role: str, latent_data, block_num: torch.Tensor):
|
| 219 |
+
"""Execute the local DiT blocks for a middle or output rank."""
|
| 220 |
+
return self.pipeline.inference(
|
| 221 |
+
noise=latent_data.original_latents,
|
| 222 |
+
current_start=latent_data.current_start,
|
| 223 |
+
current_end=latent_data.current_end,
|
| 224 |
+
current_step=latent_data.current_step,
|
| 225 |
+
block_mode=role,
|
| 226 |
+
block_num=block_num,
|
| 227 |
+
patched_x_shape=latent_data.patched_x_shape,
|
| 228 |
+
block_x=latent_data.latents,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def _send_worker_result(self, role: str, outstanding: list, latent_data, denoised_pred: torch.Tensor) -> None:
|
| 232 |
+
"""Forward the payload that should continue around the pipeline ring."""
|
| 233 |
+
if role == 'output':
|
| 234 |
+
latents = latent_data.latents
|
| 235 |
+
original_latents = denoised_pred
|
| 236 |
+
else:
|
| 237 |
+
latents = denoised_pred
|
| 238 |
+
original_latents = latent_data.original_latents
|
| 239 |
+
|
| 240 |
+
with torch.cuda.stream(self.com_stream):
|
| 241 |
+
work_objects = self.data_transfer.send_latent_data_async(
|
| 242 |
+
chunk_idx=latent_data.chunk_idx,
|
| 243 |
+
latents=latents,
|
| 244 |
+
original_latents=original_latents,
|
| 245 |
+
patched_x_shape=latent_data.patched_x_shape,
|
| 246 |
+
current_start=latent_data.current_start,
|
| 247 |
+
current_end=latent_data.current_end,
|
| 248 |
+
current_step=latent_data.current_step
|
| 249 |
+
)
|
| 250 |
+
outstanding.append(work_objects)
|
| 251 |
+
|
| 252 |
+
def _decode_prediction(self, denoised_pred: torch.Tensor) -> np.ndarray:
|
| 253 |
+
"""Decode the newest latent prediction into pixel-space frames."""
|
| 254 |
+
video = self._timed_stream_decode(denoised_pred[[-1]])
|
| 255 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 256 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 257 |
+
return video.cpu().float().numpy()
|
| 258 |
+
|
| 259 |
+
def _rank_loop_complete(self, num_chunks: int, num_steps: int) -> bool:
|
| 260 |
+
"""Return whether a non-output rank has processed all required chunks."""
|
| 261 |
+
return (
|
| 262 |
+
self.processed + self.processed_offset
|
| 263 |
+
>= num_chunks + num_steps * self.world_size + self.world_size - self.rank - 1
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
def _safe_mean(self, values: list) -> float:
|
| 267 |
+
if not values:
|
| 268 |
+
return 0.0
|
| 269 |
+
return float(np.mean(np.array(values)))
|
| 270 |
+
|
| 271 |
+
def _record_stage_fps(self, values: list[float], num_frames: int, elapsed: float) -> None:
|
| 272 |
+
if self.profile and elapsed > 0 and num_frames > 0:
|
| 273 |
+
values.append(num_frames / elapsed)
|
| 274 |
+
|
| 275 |
+
def _timing_enabled(self, schedule_block: bool = False) -> bool:
|
| 276 |
+
"""Only force GPU synchronization when profiling or schedule calibration needs it."""
|
| 277 |
+
return self.profile or schedule_block
|
| 278 |
+
|
| 279 |
+
def _sync_for_timing(self, schedule_block: bool = False) -> None:
|
| 280 |
+
if self._timing_enabled(schedule_block):
|
| 281 |
+
torch.cuda.synchronize()
|
| 282 |
+
|
| 283 |
+
def _timed_stream_encode(self, images: torch.Tensor) -> torch.Tensor:
|
| 284 |
+
self._sync_for_timing()
|
| 285 |
+
start_time = time.time()
|
| 286 |
+
latents = self.pipeline.vae.stream_encode(images)
|
| 287 |
+
self._sync_for_timing()
|
| 288 |
+
self._record_stage_fps(self.encode_fps_list, int(images.shape[2]), time.time() - start_time)
|
| 289 |
+
return latents
|
| 290 |
+
|
| 291 |
+
def _timed_stream_decode(self, denoised_pred: torch.Tensor) -> torch.Tensor:
|
| 292 |
+
self._sync_for_timing()
|
| 293 |
+
start_time = time.time()
|
| 294 |
+
video = self.pipeline.vae.stream_decode_to_pixel(denoised_pred)
|
| 295 |
+
self._sync_for_timing()
|
| 296 |
+
self._record_stage_fps(self.decode_fps_list, int(video.shape[1]), time.time() - start_time)
|
| 297 |
+
return video
|
| 298 |
+
|
| 299 |
+
def reset_stream_state(self, reset_encode: bool = False, reset_decode: bool = False) -> None:
|
| 300 |
+
"""Reset cached inference state before starting a new prompt/session."""
|
| 301 |
+
self.pipeline.kv_cache1 = None
|
| 302 |
+
self.pipeline.crossattn_cache = None
|
| 303 |
+
self.pipeline.block_x = None
|
| 304 |
+
self.pipeline.hidden_states = None
|
| 305 |
+
self.processed = 0
|
| 306 |
+
|
| 307 |
+
if reset_encode:
|
| 308 |
+
self.pipeline.vae.model.first_encode = True
|
| 309 |
+
if reset_decode:
|
| 310 |
+
self.pipeline.vae.model.first_decode = True
|
| 311 |
+
|
| 312 |
+
def _broadcast_initial_noise(self, noisy_latents: torch.Tensor) -> None:
|
| 313 |
+
latents_shape = torch.tensor(noisy_latents.shape, dtype=torch.int64, device=self.device)
|
| 314 |
+
self.communicator.broadcast_tensor(latents_shape, src=0)
|
| 315 |
+
self.communicator.broadcast_tensor(noisy_latents, src=0)
|
| 316 |
+
|
| 317 |
+
def _receive_initial_noise(self) -> torch.Tensor:
|
| 318 |
+
latents_shape = torch.zeros(5, dtype=torch.int64, device=self.device)
|
| 319 |
+
self.communicator.broadcast_tensor(latents_shape, src=0)
|
| 320 |
+
noisy_latents = torch.zeros(tuple(latents_shape.tolist()), dtype=torch.bfloat16, device=self.device)
|
| 321 |
+
self.communicator.broadcast_tensor(noisy_latents, src=0)
|
| 322 |
+
return noisy_latents
|
| 323 |
+
|
| 324 |
+
def get_demo_chunk_size(self) -> int:
|
| 325 |
+
"""Return the demo stream chunk size in frames."""
|
| 326 |
+
return self.base_chunk_size * self.pipeline.num_frame_per_block
|
| 327 |
+
|
| 328 |
+
def get_demo_first_batch_num_frames(self) -> int:
|
| 329 |
+
"""Return the number of frames required to initialize a demo stream."""
|
| 330 |
+
return 1 + self.get_demo_chunk_size()
|
| 331 |
+
|
| 332 |
+
def prepare_demo_input_session(self, images: torch.Tensor, prompt: str, block_num: torch.Tensor, noise_scale: float) -> None:
|
| 333 |
+
"""Initialize rank 0 for demo streaming and broadcast the first noisy latents."""
|
| 334 |
+
self.reset_stream_state(reset_encode=True)
|
| 335 |
+
torch.cuda.empty_cache()
|
| 336 |
+
|
| 337 |
+
latents = self._timed_stream_encode(images)
|
| 338 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 339 |
+
noise = torch.randn_like(latents)
|
| 340 |
+
noisy_latents = noise * noise_scale + latents * (1 - noise_scale)
|
| 341 |
+
|
| 342 |
+
self._broadcast_initial_noise(noisy_latents)
|
| 343 |
+
self.prepare_pipeline(
|
| 344 |
+
text_prompts=[prompt],
|
| 345 |
+
noise=noisy_latents,
|
| 346 |
+
block_mode='input',
|
| 347 |
+
current_start=0,
|
| 348 |
+
current_end=self.pipeline.frame_seq_length * 2,
|
| 349 |
+
block_num=block_num,
|
| 350 |
+
)
|
| 351 |
+
torch.cuda.empty_cache()
|
| 352 |
+
dist.barrier()
|
| 353 |
+
|
| 354 |
+
def start_demo_input_stream_session(
|
| 355 |
+
self,
|
| 356 |
+
prompt: str,
|
| 357 |
+
images: torch.Tensor,
|
| 358 |
+
block_num: torch.Tensor,
|
| 359 |
+
noise_scale: float,
|
| 360 |
+
) -> MultiGPUDemoInputSession:
|
| 361 |
+
"""Initialize rank 0 and return the demo stream session state."""
|
| 362 |
+
chunk_size = self.get_demo_chunk_size()
|
| 363 |
+
self.prepare_demo_input_session(images, prompt, block_num, noise_scale)
|
| 364 |
+
current_start = self.pipeline.frame_seq_length * (1 + chunk_size // self.base_chunk_size)
|
| 365 |
+
current_end = current_start + (chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 366 |
+
return MultiGPUDemoInputSession(
|
| 367 |
+
prompt=prompt,
|
| 368 |
+
noise_scale=noise_scale,
|
| 369 |
+
init_noise_scale=noise_scale,
|
| 370 |
+
chunk_size=chunk_size,
|
| 371 |
+
current_start=current_start,
|
| 372 |
+
current_end=current_end,
|
| 373 |
+
last_image=images[:, :, [-1]],
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
def prepare_demo_worker_session(self, prompt: str, block_mode: str, block_num: torch.Tensor, decode_initial: bool = False):
|
| 377 |
+
"""Initialize a non-input rank for demo streaming from the broadcast first chunk."""
|
| 378 |
+
self.reset_stream_state(reset_decode=(block_mode == 'output'))
|
| 379 |
+
torch.cuda.empty_cache()
|
| 380 |
+
|
| 381 |
+
noisy_latents = self._receive_initial_noise()
|
| 382 |
+
denoised_pred = self.prepare_pipeline(
|
| 383 |
+
text_prompts=[prompt],
|
| 384 |
+
noise=noisy_latents,
|
| 385 |
+
block_mode=block_mode,
|
| 386 |
+
current_start=0,
|
| 387 |
+
current_end=self.pipeline.frame_seq_length * 2,
|
| 388 |
+
block_num=block_num,
|
| 389 |
+
)
|
| 390 |
+
torch.cuda.empty_cache()
|
| 391 |
+
dist.barrier()
|
| 392 |
+
|
| 393 |
+
if decode_initial:
|
| 394 |
+
return self._decode_prediction(denoised_pred)
|
| 395 |
+
return None
|
| 396 |
+
|
| 397 |
+
def maybe_refresh_demo_input_window(self, session: MultiGPUDemoInputSession) -> None:
|
| 398 |
+
"""Wrap the KV-cache window once the streaming refresh threshold is reached."""
|
| 399 |
+
if session.current_start // self.pipeline.frame_seq_length >= self.t_refresh:
|
| 400 |
+
session.current_start = self.pipeline.kv_cache_length - self.pipeline.frame_seq_length
|
| 401 |
+
session.current_end = session.current_start + (session.chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 402 |
+
|
| 403 |
+
def prepare_demo_input_batch(self, session: MultiGPUDemoInputSession, images: torch.Tensor) -> None:
|
| 404 |
+
"""Encode one demo chunk and update the session with the current denoising step."""
|
| 405 |
+
num_frames = images.shape[2]
|
| 406 |
+
session.input_batch = num_frames // session.chunk_size
|
| 407 |
+
session.noise_scale, session.current_step = compute_noise_scale_and_step(
|
| 408 |
+
input_video_original=torch.cat([session.last_image, images], dim=2),
|
| 409 |
+
end_idx=num_frames + 1,
|
| 410 |
+
chunk_size=num_frames,
|
| 411 |
+
noise_scale=float(session.noise_scale),
|
| 412 |
+
init_noise_scale=float(session.init_noise_scale),
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
latents = self._timed_stream_encode(images)
|
| 416 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 417 |
+
noise = torch.randn_like(latents)
|
| 418 |
+
session.noisy_latents = noise * session.noise_scale + latents * (1 - session.noise_scale)
|
| 419 |
+
|
| 420 |
+
def run_demo_input_step(
|
| 421 |
+
self,
|
| 422 |
+
session: MultiGPUDemoInputSession,
|
| 423 |
+
block_num: torch.Tensor,
|
| 424 |
+
previous_latent_data=None,
|
| 425 |
+
):
|
| 426 |
+
"""Run one rank-0 demo step from the current session batch."""
|
| 427 |
+
if session.noisy_latents is None or session.input_batch <= 0:
|
| 428 |
+
raise RuntimeError("demo input batch was not prepared before run_demo_input_step")
|
| 429 |
+
|
| 430 |
+
denoised_pred, patched_x_shape = self.run_input_stage(
|
| 431 |
+
noisy_latents=session.noisy_latents[:, -session.input_batch].unsqueeze(1),
|
| 432 |
+
current_start=session.current_start,
|
| 433 |
+
current_end=session.current_end,
|
| 434 |
+
current_step=session.current_step,
|
| 435 |
+
block_num=block_num,
|
| 436 |
+
previous_latent_data=previous_latent_data,
|
| 437 |
+
)
|
| 438 |
+
session.input_batch -= 1
|
| 439 |
+
return denoised_pred, patched_x_shape
|
| 440 |
+
|
| 441 |
+
def advance_demo_input_stream_session(self, session: MultiGPUDemoInputSession, images: torch.Tensor) -> None:
|
| 442 |
+
"""Advance the demo stream session after a chunk has been queued downstream."""
|
| 443 |
+
session.last_image = images[:, :, [-1]]
|
| 444 |
+
session.chunk_idx += 1
|
| 445 |
+
session.current_start = session.current_end
|
| 446 |
+
session.current_end += (session.chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 447 |
+
|
| 448 |
+
def send_demo_input_prompt_update(
|
| 449 |
+
self,
|
| 450 |
+
prompt: str,
|
| 451 |
+
device: torch.device,
|
| 452 |
+
num_steps: int,
|
| 453 |
+
chunk_idx: int,
|
| 454 |
+
denoised_pred: torch.Tensor,
|
| 455 |
+
patched_x_shape: torch.Tensor,
|
| 456 |
+
current_step: int,
|
| 457 |
+
) -> None:
|
| 458 |
+
"""Signal a prompt restart from rank 0 and drain in-flight returns from downstream ranks."""
|
| 459 |
+
with torch.cuda.stream(self.com_stream):
|
| 460 |
+
self.data_transfer.send_latent_data_async(
|
| 461 |
+
chunk_idx=-1,
|
| 462 |
+
latents=denoised_pred.new_zeros([1] * denoised_pred.ndim),
|
| 463 |
+
original_latents=self.pipeline.hidden_states.new_zeros([1] * self.pipeline.hidden_states.ndim),
|
| 464 |
+
patched_x_shape=patched_x_shape,
|
| 465 |
+
current_start=self.pipeline.kv_cache_starts,
|
| 466 |
+
current_end=self.pipeline.kv_cache_ends,
|
| 467 |
+
current_step=int(current_step),
|
| 468 |
+
)
|
| 469 |
+
self.data_transfer.send_prompt_async(prompt, device)
|
| 470 |
+
for _ in range(min(chunk_idx, self.world_size - 1)):
|
| 471 |
+
pending_data = self.data_transfer.receive_latent_data_async(num_steps)
|
| 472 |
+
self.data_transfer.release_latent_data(pending_data)
|
| 473 |
+
|
| 474 |
+
def send_demo_middle_prompt_update(
|
| 475 |
+
self,
|
| 476 |
+
prompt: str,
|
| 477 |
+
device: torch.device,
|
| 478 |
+
denoised_pred: torch.Tensor | None,
|
| 479 |
+
latent_data,
|
| 480 |
+
) -> None:
|
| 481 |
+
"""Forward a prompt restart from a middle rank to the next rank."""
|
| 482 |
+
sentinel_source = denoised_pred if denoised_pred is not None else latent_data.latents
|
| 483 |
+
with torch.cuda.stream(self.com_stream):
|
| 484 |
+
self.data_transfer.send_latent_data_async(
|
| 485 |
+
chunk_idx=-1,
|
| 486 |
+
latents=sentinel_source.new_zeros([1] * sentinel_source.ndim),
|
| 487 |
+
original_latents=latent_data.original_latents,
|
| 488 |
+
patched_x_shape=latent_data.patched_x_shape,
|
| 489 |
+
current_start=latent_data.current_start,
|
| 490 |
+
current_end=latent_data.current_end,
|
| 491 |
+
current_step=int(latent_data.current_step),
|
| 492 |
+
)
|
| 493 |
+
self.data_transfer.send_prompt_async(prompt, device)
|
| 494 |
+
|
| 495 |
+
def run_input_stage(self, noisy_latents: torch.Tensor, current_start: int, current_end: int, current_step: int, block_num: torch.Tensor, previous_latent_data=None):
|
| 496 |
+
"""Run the rank-0 stage for one streaming chunk."""
|
| 497 |
+
if previous_latent_data is not None and self.processed >= self.world_size:
|
| 498 |
+
self.pipeline.hidden_states.copy_(previous_latent_data.original_latents)
|
| 499 |
+
self.pipeline.kv_cache_starts.copy_(previous_latent_data.current_start)
|
| 500 |
+
self.pipeline.kv_cache_ends.copy_(previous_latent_data.current_end)
|
| 501 |
+
|
| 502 |
+
return self.pipeline.inference(
|
| 503 |
+
noise=noisy_latents,
|
| 504 |
+
current_start=current_start,
|
| 505 |
+
current_end=current_end,
|
| 506 |
+
current_step=current_step,
|
| 507 |
+
block_mode='input',
|
| 508 |
+
block_num=block_num,
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
def run_rank_0_loop(self, input_video_original: torch.Tensor, prompts: list,
|
| 512 |
+
num_chunks: int, num_steps: int, chunk_size: int,
|
| 513 |
+
block_num: torch.Tensor, noise_scale: float,
|
| 514 |
+
schedule_block: bool, total_blocks: int):
|
| 515 |
+
"""
|
| 516 |
+
Run the main loop for rank 0 (encoder + async send).
|
| 517 |
+
|
| 518 |
+
This method encapsulates the rank 0 logic using the communication abstractions.
|
| 519 |
+
"""
|
| 520 |
+
self.logger.info("Starting rank 0 inference loop")
|
| 521 |
+
|
| 522 |
+
# Initialize variables
|
| 523 |
+
start_idx = 0
|
| 524 |
+
end_idx = 1 + chunk_size
|
| 525 |
+
current_start = 0
|
| 526 |
+
current_end = self.pipeline.frame_seq_length * (1+chunk_size//self.base_chunk_size)
|
| 527 |
+
init_noise_scale = noise_scale
|
| 528 |
+
|
| 529 |
+
outstanding = []
|
| 530 |
+
latent_data = None
|
| 531 |
+
|
| 532 |
+
self._sync_for_timing(schedule_block)
|
| 533 |
+
start_time = time.time()
|
| 534 |
+
|
| 535 |
+
while True:
|
| 536 |
+
# Process new chunk if available
|
| 537 |
+
start_idx = end_idx
|
| 538 |
+
end_idx = end_idx + chunk_size
|
| 539 |
+
current_start = current_end
|
| 540 |
+
current_end = current_end + (chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 541 |
+
|
| 542 |
+
if schedule_block:
|
| 543 |
+
self._sync_for_timing(schedule_block)
|
| 544 |
+
start_vae = time.time()
|
| 545 |
+
|
| 546 |
+
if end_idx <= input_video_original.shape[2]:
|
| 547 |
+
inp = input_video_original[:, :, start_idx:end_idx]
|
| 548 |
+
|
| 549 |
+
noise_scale, current_step = compute_noise_scale_and_step(
|
| 550 |
+
input_video_original, end_idx, chunk_size, noise_scale, init_noise_scale
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
latents = self._timed_stream_encode(inp)
|
| 554 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 555 |
+
|
| 556 |
+
noise = torch.randn_like(latents)
|
| 557 |
+
noisy_latents = noise * noise_scale + latents * (1 - noise_scale)
|
| 558 |
+
|
| 559 |
+
# if current_start//self.pipeline.frame_seq_length >= self.t_refresh:
|
| 560 |
+
# current_start = self.pipeline.kv_cache_length - self.pipeline.frame_seq_length
|
| 561 |
+
# current_end = current_start + (chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 562 |
+
|
| 563 |
+
# Measure DiT time if scheduling is enabled
|
| 564 |
+
if schedule_block:
|
| 565 |
+
self._sync_for_timing(schedule_block)
|
| 566 |
+
start_dit = time.time()
|
| 567 |
+
t_vae = start_dit - start_vae
|
| 568 |
+
|
| 569 |
+
# Run inference
|
| 570 |
+
denoised_pred, patched_x_shape = self.pipeline.inference(
|
| 571 |
+
noise=noisy_latents,
|
| 572 |
+
current_start=current_start,
|
| 573 |
+
current_end=current_end,
|
| 574 |
+
current_step=current_step,
|
| 575 |
+
block_mode='input',
|
| 576 |
+
block_num=block_num[self.rank],
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
# Update DiT timing
|
| 580 |
+
if schedule_block:
|
| 581 |
+
self._sync_for_timing(schedule_block)
|
| 582 |
+
temp = time.time() - start_dit
|
| 583 |
+
if temp < self.t_dit:
|
| 584 |
+
self.t_dit = temp
|
| 585 |
+
|
| 586 |
+
self.processed += 1
|
| 587 |
+
|
| 588 |
+
with torch.cuda.stream(self.com_stream):
|
| 589 |
+
if self.processed >= self.world_size:
|
| 590 |
+
if latent_data is not None:
|
| 591 |
+
self.data_transfer.release_latent_data(latent_data)
|
| 592 |
+
|
| 593 |
+
# Receive data from previous rank
|
| 594 |
+
latent_data = self.data_transfer.receive_latent_data_async(num_steps)
|
| 595 |
+
|
| 596 |
+
torch.cuda.current_stream().wait_stream(self.com_stream)
|
| 597 |
+
|
| 598 |
+
# Wait for outstanding operations
|
| 599 |
+
self._wait_for_outstanding(outstanding)
|
| 600 |
+
|
| 601 |
+
# Send data to next rank
|
| 602 |
+
with torch.cuda.stream(self.com_stream):
|
| 603 |
+
work_objects = self.data_transfer.send_latent_data_async(
|
| 604 |
+
chunk_idx=start_idx,
|
| 605 |
+
latents=denoised_pred,
|
| 606 |
+
original_latents=self.pipeline.hidden_states,
|
| 607 |
+
patched_x_shape=patched_x_shape,
|
| 608 |
+
current_start=self.pipeline.kv_cache_starts,
|
| 609 |
+
current_end=self.pipeline.kv_cache_ends,
|
| 610 |
+
current_step=current_step
|
| 611 |
+
)
|
| 612 |
+
outstanding.append(work_objects)
|
| 613 |
+
# Handle block scheduling
|
| 614 |
+
if schedule_block and self.processed >= self.schedule_step:
|
| 615 |
+
self._handle_block_scheduling(block_num, total_blocks)
|
| 616 |
+
schedule_block = False
|
| 617 |
+
|
| 618 |
+
# Update timing and check completion
|
| 619 |
+
if self._timing_enabled(schedule_block):
|
| 620 |
+
self._sync_for_timing(schedule_block)
|
| 621 |
+
end_time = time.time()
|
| 622 |
+
t = end_time - start_time
|
| 623 |
+
self.logger.info(f"Encode {self.processed}, time: {t:.4f} s, fps: {inp.shape[2]/t:.4f}")
|
| 624 |
+
|
| 625 |
+
if schedule_block:
|
| 626 |
+
t_total = self.t_dit + t_vae
|
| 627 |
+
if t_total < self.t_total:
|
| 628 |
+
self.t_total = t_total
|
| 629 |
+
start_time = end_time
|
| 630 |
+
|
| 631 |
+
if self.processed >= self.world_size:
|
| 632 |
+
self.pipeline.hidden_states.copy_(latent_data.original_latents)
|
| 633 |
+
self.pipeline.kv_cache_starts.copy_(latent_data.current_start)
|
| 634 |
+
self.pipeline.kv_cache_ends.copy_(latent_data.current_end)
|
| 635 |
+
|
| 636 |
+
if self.processed + self.processed_offset >= num_chunks + num_steps * self.world_size + self.world_size - self.rank - 1:
|
| 637 |
+
break
|
| 638 |
+
|
| 639 |
+
if latent_data is not None:
|
| 640 |
+
self.data_transfer.release_latent_data(latent_data)
|
| 641 |
+
self._drain_outstanding(outstanding)
|
| 642 |
+
self.logger.info(f"VAE Encode Average FPS: {self._safe_mean(self.encode_fps_list):.4f}")
|
| 643 |
+
self.logger.info("Rank 0 inference loop completed")
|
| 644 |
+
|
| 645 |
+
def run_final_rank_loop(self, num_chunks: int, num_steps: int, chunk_size: int,
|
| 646 |
+
block_num: torch.Tensor, output_folder: str, fps: int,
|
| 647 |
+
schedule_block: bool, total_blocks: int, results: dict):
|
| 648 |
+
"""Run the worker loop for the output rank."""
|
| 649 |
+
self.run_worker_rank_loop(
|
| 650 |
+
role='output',
|
| 651 |
+
num_chunks=num_chunks,
|
| 652 |
+
num_steps=num_steps,
|
| 653 |
+
chunk_size=chunk_size,
|
| 654 |
+
block_num=block_num,
|
| 655 |
+
schedule_block=schedule_block,
|
| 656 |
+
total_blocks=total_blocks,
|
| 657 |
+
output_folder=output_folder,
|
| 658 |
+
fps=fps,
|
| 659 |
+
results=results,
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
def run_middle_rank_loop(self, num_chunks: int, num_steps: int, chunk_size: int,
|
| 663 |
+
block_num: torch.Tensor, schedule_block: bool, total_blocks: int):
|
| 664 |
+
"""Run the worker loop for a middle rank."""
|
| 665 |
+
self.run_worker_rank_loop(
|
| 666 |
+
role='middle',
|
| 667 |
+
num_chunks=num_chunks,
|
| 668 |
+
num_steps=num_steps,
|
| 669 |
+
chunk_size=chunk_size,
|
| 670 |
+
block_num=block_num,
|
| 671 |
+
schedule_block=schedule_block,
|
| 672 |
+
total_blocks=total_blocks,
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
def run_worker_rank_loop(
|
| 676 |
+
self,
|
| 677 |
+
role: str,
|
| 678 |
+
num_chunks: int,
|
| 679 |
+
num_steps: int,
|
| 680 |
+
chunk_size: int,
|
| 681 |
+
block_num: torch.Tensor,
|
| 682 |
+
schedule_block: bool,
|
| 683 |
+
total_blocks: int,
|
| 684 |
+
output_folder: str = None,
|
| 685 |
+
fps: int = None,
|
| 686 |
+
results: dict = None,
|
| 687 |
+
):
|
| 688 |
+
"""Run the shared receive -> infer -> forward loop for middle and output ranks."""
|
| 689 |
+
if role not in {'middle', 'output'}:
|
| 690 |
+
raise ValueError(f"Unsupported worker role: {role}")
|
| 691 |
+
|
| 692 |
+
self.logger.info(f"Starting {role} rank inference loop")
|
| 693 |
+
|
| 694 |
+
if role == 'output':
|
| 695 |
+
if output_folder is None or fps is None or results is None:
|
| 696 |
+
raise ValueError("output rank requires output_folder, fps, and results")
|
| 697 |
+
os.makedirs(output_folder, exist_ok=True)
|
| 698 |
+
save_results = 1
|
| 699 |
+
|
| 700 |
+
outstanding = []
|
| 701 |
+
fps_list = []
|
| 702 |
+
latent_data = None
|
| 703 |
+
|
| 704 |
+
self._sync_for_timing(schedule_block)
|
| 705 |
+
start_time = time.time()
|
| 706 |
+
|
| 707 |
+
while True:
|
| 708 |
+
latent_data = self._receive_latent_data(latent_data, num_steps)
|
| 709 |
+
schedule_block = self._maybe_schedule_blocks(
|
| 710 |
+
schedule_block,
|
| 711 |
+
self.schedule_step - self.rank,
|
| 712 |
+
block_num,
|
| 713 |
+
total_blocks,
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
if schedule_block:
|
| 717 |
+
self._sync_for_timing(schedule_block)
|
| 718 |
+
start_dit = time.time()
|
| 719 |
+
|
| 720 |
+
denoised_pred, _ = self._run_worker_stage(role, latent_data, block_num[self.rank])
|
| 721 |
+
|
| 722 |
+
if schedule_block:
|
| 723 |
+
self._sync_for_timing(schedule_block)
|
| 724 |
+
temp = time.time() - start_dit
|
| 725 |
+
if temp < self.t_dit:
|
| 726 |
+
self.t_dit = temp
|
| 727 |
+
|
| 728 |
+
self.processed += 1
|
| 729 |
+
self._wait_for_outstanding(outstanding)
|
| 730 |
+
self._send_worker_result(role, outstanding, latent_data, denoised_pred)
|
| 731 |
+
|
| 732 |
+
if role == 'output':
|
| 733 |
+
if self.processed >= num_steps * self.world_size - 1:
|
| 734 |
+
if schedule_block:
|
| 735 |
+
self._sync_for_timing(schedule_block)
|
| 736 |
+
start_vae = time.time()
|
| 737 |
+
|
| 738 |
+
video = self._timed_stream_decode(denoised_pred[[-1]])
|
| 739 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 740 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 741 |
+
results[save_results] = video.cpu().float().numpy()
|
| 742 |
+
|
| 743 |
+
if self._timing_enabled(schedule_block):
|
| 744 |
+
self._sync_for_timing(schedule_block)
|
| 745 |
+
end_time = time.time()
|
| 746 |
+
elapsed = end_time - start_time
|
| 747 |
+
fps_test = video.shape[0] / elapsed
|
| 748 |
+
if self.processed > self.schedule_step:
|
| 749 |
+
fps_list.append(fps_test)
|
| 750 |
+
self.logger.info(f"Decode {self.processed}, time: {elapsed:.4f} s, FPS: {fps_test:.4f}")
|
| 751 |
+
|
| 752 |
+
if schedule_block:
|
| 753 |
+
t_vae = end_time - start_vae
|
| 754 |
+
t_total = t_vae + self.t_dit
|
| 755 |
+
if t_total < self.t_total:
|
| 756 |
+
self.t_total = t_total
|
| 757 |
+
start_time = end_time
|
| 758 |
+
save_results += 1
|
| 759 |
+
|
| 760 |
+
if save_results >= num_chunks:
|
| 761 |
+
break
|
| 762 |
+
else:
|
| 763 |
+
if self._timing_enabled(schedule_block):
|
| 764 |
+
self._sync_for_timing(schedule_block)
|
| 765 |
+
end_time = time.time()
|
| 766 |
+
elapsed = end_time - start_time
|
| 767 |
+
fps_test = chunk_size / elapsed
|
| 768 |
+
|
| 769 |
+
if self.processed > self.schedule_step:
|
| 770 |
+
fps_list.append(fps_test)
|
| 771 |
+
|
| 772 |
+
if schedule_block:
|
| 773 |
+
t_total = self.t_dit
|
| 774 |
+
if t_total < self.t_total:
|
| 775 |
+
self.t_total = t_total
|
| 776 |
+
|
| 777 |
+
self.logger.info(f"Middle {self.processed}, time: {elapsed:.4f} s, fps: {fps_test:.4f}")
|
| 778 |
+
start_time = end_time
|
| 779 |
+
|
| 780 |
+
if self._rank_loop_complete(num_chunks, num_steps):
|
| 781 |
+
break
|
| 782 |
+
|
| 783 |
+
if latent_data is not None:
|
| 784 |
+
self.data_transfer.release_latent_data(latent_data)
|
| 785 |
+
self._drain_outstanding(outstanding)
|
| 786 |
+
|
| 787 |
+
if role == 'output':
|
| 788 |
+
video_list = [results[i] for i in range(num_chunks)]
|
| 789 |
+
video = np.concatenate(video_list, axis=0)
|
| 790 |
+
fps_avg = self._safe_mean(fps_list)
|
| 791 |
+
self.logger.info(f"Video shape: {video.shape}, Average FPS: {fps_avg:.4f}")
|
| 792 |
+
self.logger.info(f"VAE Decode Average FPS: {self._safe_mean(self.decode_fps_list):.4f}")
|
| 793 |
+
|
| 794 |
+
output_path = os.path.join(output_folder, f"output_{0:03d}.mp4")
|
| 795 |
+
export_to_video(video, output_path, fps=fps)
|
| 796 |
+
self.logger.info(f"Video saved to: {output_path} (Press Ctrl+C to force exit)")
|
| 797 |
+
return
|
| 798 |
+
|
| 799 |
+
self.logger.info(f"DiT Average FPS: {self._safe_mean(fps_list):.4f}")
|
| 800 |
+
self.logger.info(f"Rank {self.rank} inference loop completed")
|
| 801 |
+
|
| 802 |
+
def _handle_block_scheduling(self, block_num: torch.Tensor, total_blocks: int):
|
| 803 |
+
"""Handle block scheduling and rebalancing."""
|
| 804 |
+
self.logger.info(f"Scheduling block in {self.processed}")
|
| 805 |
+
|
| 806 |
+
# Gather timing information from all ranks
|
| 807 |
+
t_total_tensor = torch.tensor(self.t_total, dtype=torch.float32, device=self.device)
|
| 808 |
+
t_dit_tensor = torch.tensor(self.t_dit, dtype=torch.float32, device=self.device)
|
| 809 |
+
|
| 810 |
+
gather_blocks = [torch.zeros_like(t_dit_tensor, dtype=torch.float32, device=self.device)
|
| 811 |
+
for _ in range(self.world_size)]
|
| 812 |
+
|
| 813 |
+
dist.all_gather(gather_blocks, t_dit_tensor)
|
| 814 |
+
t_dit_list = [t_dit_i.item() for t_dit_i in gather_blocks]
|
| 815 |
+
|
| 816 |
+
dist.all_gather(gather_blocks, t_total_tensor)
|
| 817 |
+
t_list = [t_i.item() for t_i in gather_blocks]
|
| 818 |
+
|
| 819 |
+
# Compute new block distribution
|
| 820 |
+
new_block_num = torch.tensor(
|
| 821 |
+
compute_balanced_split(total_blocks, t_list, t_dit_list, block_num.tolist()),
|
| 822 |
+
dtype=torch.int64, device=self.device
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
self.logger.info(f"New block distribution: {new_block_num[self.rank].tolist()}")
|
| 826 |
+
|
| 827 |
+
# Broadcast new block distribution
|
| 828 |
+
dist.broadcast(new_block_num, src=self.world_size - 1)
|
| 829 |
+
|
| 830 |
+
# Rebalance KV cache
|
| 831 |
+
self.data_transfer.rebalance_kv_cache(block_num, new_block_num, total_blocks)
|
| 832 |
+
|
| 833 |
+
# Update block_num
|
| 834 |
+
block_num.copy_(new_block_num)
|
| 835 |
+
|
| 836 |
+
start_block, end_block = block_num[self.rank][0].item(), block_num[self.rank][1].item()
|
| 837 |
+
blocks_to_keep = list(range(start_block, end_block))
|
| 838 |
+
for i in range(self.pipeline.num_transformer_blocks):
|
| 839 |
+
if i not in blocks_to_keep:
|
| 840 |
+
self.pipeline.kv_cache1[i]['k'] = self.pipeline.kv_cache1[i]['k'].cpu()
|
| 841 |
+
self.pipeline.kv_cache1[i]['v'] = self.pipeline.kv_cache1[i]['v'].cpu()
|
| 842 |
+
|
| 843 |
+
self.logger.info("Block scheduling completed")
|
| 844 |
+
|
| 845 |
+
def cleanup(self):
|
| 846 |
+
"""Clean up resources."""
|
| 847 |
+
self.data_transfer.cleanup()
|
| 848 |
+
self.logger.info("InferencePipelineManager cleanup completed")
|
| 849 |
+
|
| 850 |
+
|
| 851 |
+
def main():
|
| 852 |
+
"""Main function for the refactored inference pipeline."""
|
| 853 |
+
parser = argparse.ArgumentParser()
|
| 854 |
+
parser.add_argument("--config_path", type=str)
|
| 855 |
+
parser.add_argument("--checkpoint_folder", type=str)
|
| 856 |
+
parser.add_argument("--output_folder", type=str)
|
| 857 |
+
parser.add_argument("--prompt_file_path", type=str)
|
| 858 |
+
parser.add_argument("--video_path", type=str)
|
| 859 |
+
parser.add_argument("--noise_scale", type=float, default=0.8)
|
| 860 |
+
parser.add_argument("--height", type=int, default=480)
|
| 861 |
+
parser.add_argument("--width", type=int, default=832)
|
| 862 |
+
parser.add_argument("--fps", type=int, default=30)
|
| 863 |
+
parser.add_argument("--max_outstanding", type=int, default=1, help="max number of outstanding sends/recv to keep")
|
| 864 |
+
parser.add_argument("--dit_fsdp", action="store_true", default=False)
|
| 865 |
+
parser.add_argument("--t5_fsdp", action="store_true", default=False)
|
| 866 |
+
parser.add_argument("--ulysses_size", type=int, default=1)
|
| 867 |
+
parser.add_argument("--ring_size", type=int, default=1)
|
| 868 |
+
parser.add_argument("--step", type=int, default=2)
|
| 869 |
+
parser.add_argument("--seed", type=int, default=0, help="Random seed")
|
| 870 |
+
parser.add_argument("--schedule_block", action="store_true", default=False)
|
| 871 |
+
parser.add_argument("--profile", action="store_true", default=False, help="Enable synchronized throughput logging")
|
| 872 |
+
parser.add_argument("--t2v", action="store_true", default=False)
|
| 873 |
+
parser.add_argument("--model_type", type=str, default="T2V-1.3B", help="Model type (e.g., T2V-1.3B)")
|
| 874 |
+
parser.add_argument("--use_taehv", action="store_true", default=False, help="Use the lightweight TAEHV VAE for encode/decode")
|
| 875 |
+
parser.add_argument("--use_tensorrt", "--use_taehv_tensorrt", dest="use_tensorrt", action="store_true", default=False, help="Enable available TensorRT acceleration paths")
|
| 876 |
+
parser.add_argument("--fast", action="store_true", default=False, help="Enable the fast path: --use_taehv --use_tensorrt")
|
| 877 |
+
|
| 878 |
+
args = parser.parse_args()
|
| 879 |
+
|
| 880 |
+
torch.set_grad_enabled(False)
|
| 881 |
+
init_distributed()
|
| 882 |
+
|
| 883 |
+
rank = dist.get_rank()
|
| 884 |
+
world_size = dist.get_world_size()
|
| 885 |
+
local_rank = int(os.environ.get("LOCAL_RANK", rank))
|
| 886 |
+
|
| 887 |
+
assert world_size >= 2, "world_size must be at least 2"
|
| 888 |
+
|
| 889 |
+
torch.cuda.set_device(local_rank)
|
| 890 |
+
device = torch.device(f"cuda:{local_rank}")
|
| 891 |
+
|
| 892 |
+
# Load configuration
|
| 893 |
+
config = merge_cli_config(args.config_path, args)
|
| 894 |
+
|
| 895 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 896 |
+
LOGGER.info("Denoising Step List: %s", list(config.denoising_step_list))
|
| 897 |
+
|
| 898 |
+
set_seed(args.seed)
|
| 899 |
+
|
| 900 |
+
# Load input video
|
| 901 |
+
input_video_original = load_mp4_as_tensor(args.video_path, resize_hw=(args.height, args.width)).unsqueeze(0)
|
| 902 |
+
if input_video_original.dtype != torch.bfloat16:
|
| 903 |
+
input_video_original = input_video_original.to(dtype=torch.bfloat16).to(device)
|
| 904 |
+
|
| 905 |
+
LOGGER.info("Input video tensor shape: %s", tuple(input_video_original.shape))
|
| 906 |
+
b, c, t, h, w = input_video_original.shape
|
| 907 |
+
|
| 908 |
+
# Calculate number of chunks
|
| 909 |
+
chunk_size = 4 * config.num_frame_per_block
|
| 910 |
+
if rank == 0:
|
| 911 |
+
num_chunks = (t - 1) // chunk_size
|
| 912 |
+
else:
|
| 913 |
+
num_chunks = 0
|
| 914 |
+
num_chunks_tensor = torch.tensor([num_chunks], dtype=torch.int64, device=device)
|
| 915 |
+
dist.broadcast(num_chunks_tensor, src=0)
|
| 916 |
+
num_chunks = int(num_chunks_tensor.item())
|
| 917 |
+
|
| 918 |
+
# Initialize pipeline manager
|
| 919 |
+
pipeline_manager = InferencePipelineManager(config, device, rank, world_size)
|
| 920 |
+
pipeline_manager.load_model(args.checkpoint_folder)
|
| 921 |
+
|
| 922 |
+
# Load prompts
|
| 923 |
+
dataset = TextDataset(args.prompt_file_path)
|
| 924 |
+
prompts = [dataset[0]]
|
| 925 |
+
num_steps = len(pipeline_manager.pipeline.denoising_step_list)
|
| 926 |
+
|
| 927 |
+
# Determine block mode and setup block distribution
|
| 928 |
+
if rank == 0:
|
| 929 |
+
block_mode = 'input'
|
| 930 |
+
elif rank == world_size - 1:
|
| 931 |
+
block_mode = 'output'
|
| 932 |
+
else:
|
| 933 |
+
block_mode = 'middle'
|
| 934 |
+
|
| 935 |
+
# Setup block distribution
|
| 936 |
+
total_blocks = pipeline_manager.pipeline.num_transformer_blocks
|
| 937 |
+
total_block_num = compute_default_block_distribution(total_blocks, world_size)
|
| 938 |
+
|
| 939 |
+
block_num = torch.tensor(total_block_num, dtype=torch.int64, device=device)
|
| 940 |
+
|
| 941 |
+
# Prepare pipeline
|
| 942 |
+
start_idx = 0
|
| 943 |
+
end_idx = 5
|
| 944 |
+
current_start = 0
|
| 945 |
+
current_end = pipeline_manager.pipeline.frame_seq_length * 2
|
| 946 |
+
|
| 947 |
+
inp = input_video_original[:, :, start_idx:end_idx]
|
| 948 |
+
|
| 949 |
+
# Only rank 0 performs VAE encoding operation
|
| 950 |
+
if rank == 0:
|
| 951 |
+
latents = pipeline_manager._timed_stream_encode(inp)
|
| 952 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 953 |
+
noise = torch.randn_like(latents)
|
| 954 |
+
noisy_latents = noise * args.noise_scale + latents * (1 - args.noise_scale)
|
| 955 |
+
|
| 956 |
+
# First broadcast the shape information
|
| 957 |
+
latents_shape = torch.tensor(latents.shape, dtype=torch.int64, device=device)
|
| 958 |
+
pipeline_manager.communicator.broadcast_tensor(latents_shape, src=0)
|
| 959 |
+
# Then broadcast noisy_latents
|
| 960 |
+
pipeline_manager.communicator.broadcast_tensor(noisy_latents, src=0)
|
| 961 |
+
else:
|
| 962 |
+
# Other ranks receive shape info first
|
| 963 |
+
latents_shape = torch.zeros(5, dtype=torch.int64, device=device)
|
| 964 |
+
pipeline_manager.communicator.broadcast_tensor(latents_shape, src=0)
|
| 965 |
+
# Create tensor with same shape for receiving broadcast data
|
| 966 |
+
noisy_latents = torch.zeros(tuple(latents_shape.tolist()), dtype=torch.bfloat16, device=device)
|
| 967 |
+
# Receive the broadcasted noisy_latents
|
| 968 |
+
pipeline_manager.communicator.broadcast_tensor(noisy_latents, src=0)
|
| 969 |
+
|
| 970 |
+
denoised_pred = pipeline_manager.prepare_pipeline(
|
| 971 |
+
text_prompts=prompts,
|
| 972 |
+
noise=noisy_latents,
|
| 973 |
+
block_mode=block_mode,
|
| 974 |
+
current_start=current_start,
|
| 975 |
+
current_end=current_end,
|
| 976 |
+
block_num=block_num[rank],
|
| 977 |
+
)
|
| 978 |
+
|
| 979 |
+
# Clear unused GPU memory
|
| 980 |
+
torch.cuda.empty_cache()
|
| 981 |
+
|
| 982 |
+
# Save initial result for final rank
|
| 983 |
+
if rank == world_size - 1:
|
| 984 |
+
results = {}
|
| 985 |
+
video = pipeline_manager._timed_stream_decode(denoised_pred)
|
| 986 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 987 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 988 |
+
results[0] = video.cpu().float().numpy()
|
| 989 |
+
|
| 990 |
+
dist.barrier()
|
| 991 |
+
pipeline_manager.logger.info(f"Prepared, Block num: {block_num[rank].tolist()}")
|
| 992 |
+
|
| 993 |
+
used_mem = torch.cuda.memory_allocated(device) / 1024 / 1024 / 1024
|
| 994 |
+
total_mem = torch.cuda.get_device_properties(device).total_memory / 1024 / 1024 / 1024
|
| 995 |
+
pipeline_manager.logger.info(f"Current GPU memory usage: {used_mem:.2f} GB / {total_mem:.2f} GB")
|
| 996 |
+
|
| 997 |
+
# Run appropriate loop based on rank
|
| 998 |
+
try:
|
| 999 |
+
if rank == 0:
|
| 1000 |
+
pipeline_manager.run_rank_0_loop(
|
| 1001 |
+
input_video_original, prompts, num_chunks, num_steps, chunk_size,
|
| 1002 |
+
block_num, args.noise_scale, args.schedule_block, total_blocks
|
| 1003 |
+
)
|
| 1004 |
+
elif rank == world_size - 1:
|
| 1005 |
+
pipeline_manager.run_final_rank_loop(
|
| 1006 |
+
num_chunks, num_steps, chunk_size, block_num, args.output_folder,
|
| 1007 |
+
args.fps, args.schedule_block, total_blocks, results
|
| 1008 |
+
)
|
| 1009 |
+
else:
|
| 1010 |
+
pipeline_manager.run_middle_rank_loop(
|
| 1011 |
+
num_chunks, num_steps, chunk_size, block_num, args.schedule_block, total_blocks
|
| 1012 |
+
)
|
| 1013 |
+
finally:
|
| 1014 |
+
# Cleanup
|
| 1015 |
+
pipeline_manager.cleanup()
|
| 1016 |
+
|
| 1017 |
+
dist.barrier()
|
| 1018 |
+
dist.destroy_process_group()
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
if __name__ == "__main__":
|
| 1022 |
+
main()
|
streamv2v/inference_wo_batch.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Single GPU Inference Pipeline - Refactored from inference_pipe.py
|
| 3 |
+
|
| 4 |
+
This file extracts core logic from multi-GPU inference code to implement a complete
|
| 5 |
+
inference pipeline on a single GPU:
|
| 6 |
+
1. VAE encode input video
|
| 7 |
+
2. DiT inference (using input mode, processing all 30 blocks)
|
| 8 |
+
3. VAE decode output video
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from models.wan.causal_stream_inference import CausalStreamInferencePipeline
|
| 12 |
+
from models.util import set_seed
|
| 13 |
+
from diffusers.utils import export_to_video
|
| 14 |
+
from models.data import TextDataset
|
| 15 |
+
import argparse
|
| 16 |
+
import torch
|
| 17 |
+
import os
|
| 18 |
+
import time
|
| 19 |
+
import numpy as np
|
| 20 |
+
import logging
|
| 21 |
+
from typing import List
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
from streamv2v.inference import compute_noise_scale_and_step, SingleGPUStreamSession
|
| 25 |
+
from streamv2v.inference_common import (
|
| 26 |
+
load_generator_state_dict,
|
| 27 |
+
load_mp4_as_tensor,
|
| 28 |
+
merge_cli_config,
|
| 29 |
+
)
|
| 30 |
+
except ModuleNotFoundError:
|
| 31 |
+
from inference import compute_noise_scale_and_step, SingleGPUStreamSession
|
| 32 |
+
from inference_common import (
|
| 33 |
+
load_generator_state_dict,
|
| 34 |
+
load_mp4_as_tensor,
|
| 35 |
+
merge_cli_config,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
LOGGER = logging.getLogger(__name__)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class SingleGPUInferencePipeline:
|
| 42 |
+
"""
|
| 43 |
+
Single GPU Inference Pipeline Manager
|
| 44 |
+
|
| 45 |
+
This class encapsulates the complete inference logic on a single GPU,
|
| 46 |
+
including encoding, inference, and decoding.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, config, device: torch.device):
|
| 50 |
+
"""
|
| 51 |
+
Initialize the single GPU inference pipeline manager.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
config: Configuration object
|
| 55 |
+
device: GPU device
|
| 56 |
+
"""
|
| 57 |
+
self.config = config
|
| 58 |
+
self.device = device
|
| 59 |
+
|
| 60 |
+
# Setup logging
|
| 61 |
+
self.logger = logging.getLogger("SingleGPUInference")
|
| 62 |
+
self.logger.setLevel(logging.INFO)
|
| 63 |
+
# Prevent messages from propagating to the root logger (avoid double prints)
|
| 64 |
+
self.logger.propagate = False
|
| 65 |
+
|
| 66 |
+
if not self.logger.handlers:
|
| 67 |
+
handler = logging.StreamHandler()
|
| 68 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 69 |
+
handler.setFormatter(formatter)
|
| 70 |
+
self.logger.addHandler(handler)
|
| 71 |
+
|
| 72 |
+
# Initialize pipeline
|
| 73 |
+
self.pipeline = CausalStreamInferencePipeline(config, device=str(device))
|
| 74 |
+
self.pipeline.to(device=str(device), dtype=torch.bfloat16)
|
| 75 |
+
|
| 76 |
+
# Performance tracking
|
| 77 |
+
self.t_dit = 100.0
|
| 78 |
+
self.t_total = 100.0
|
| 79 |
+
self.processed = 0
|
| 80 |
+
self.processed_offset = 3
|
| 81 |
+
self.base_chunk_size = 4
|
| 82 |
+
self.t_refresh = 50
|
| 83 |
+
self.profile = bool(config.get("profile", False))
|
| 84 |
+
self.encode_fps_list: list[float] = []
|
| 85 |
+
self.decode_fps_list: list[float] = []
|
| 86 |
+
self._canonical_denoising_step_list = self.pipeline.denoising_step_list.clone()
|
| 87 |
+
|
| 88 |
+
self.logger.info("Single GPU inference pipeline manager initialized")
|
| 89 |
+
|
| 90 |
+
def load_model(self, checkpoint_folder: str):
|
| 91 |
+
"""Load the model from checkpoint."""
|
| 92 |
+
ckpt_path, state_dict = load_generator_state_dict(checkpoint_folder)
|
| 93 |
+
try:
|
| 94 |
+
self.pipeline.generator.load_state_dict(state_dict, strict=True)
|
| 95 |
+
except RuntimeError as exc:
|
| 96 |
+
self.logger.warning(f"Strict load_state_dict failed: {exc}; retrying with strict=False")
|
| 97 |
+
self.pipeline.generator.load_state_dict(state_dict, strict=False)
|
| 98 |
+
self.logger.info(f"Model loaded successfully from {ckpt_path}")
|
| 99 |
+
|
| 100 |
+
def prepare_pipeline(self, text_prompts: list, noise: torch.Tensor,
|
| 101 |
+
current_start: int, current_end: int, batch_denoise: bool=True):
|
| 102 |
+
"""Prepare the pipeline for inference."""
|
| 103 |
+
# Use the original prepare method which now handles distributed environment gracefully
|
| 104 |
+
denoised_pred = self.pipeline.prepare(
|
| 105 |
+
text_prompts=text_prompts,
|
| 106 |
+
device=self.device,
|
| 107 |
+
dtype=torch.bfloat16,
|
| 108 |
+
block_mode='input',
|
| 109 |
+
noise=noise,
|
| 110 |
+
current_start=current_start,
|
| 111 |
+
current_end=current_end,
|
| 112 |
+
batch_denoise=batch_denoise,
|
| 113 |
+
)
|
| 114 |
+
return denoised_pred
|
| 115 |
+
|
| 116 |
+
def _sync_for_timing(self):
|
| 117 |
+
if self.profile:
|
| 118 |
+
torch.cuda.synchronize()
|
| 119 |
+
|
| 120 |
+
def _record_stage_fps(self, values: list[float], num_frames: int, elapsed: float) -> None:
|
| 121 |
+
if self.profile and elapsed > 0 and num_frames > 0:
|
| 122 |
+
values.append(num_frames / elapsed)
|
| 123 |
+
|
| 124 |
+
def _timed_stream_encode(self, images: torch.Tensor) -> torch.Tensor:
|
| 125 |
+
self._sync_for_timing()
|
| 126 |
+
start_time = time.time()
|
| 127 |
+
latents = self.pipeline.vae.stream_encode(images)
|
| 128 |
+
self._sync_for_timing()
|
| 129 |
+
self._record_stage_fps(self.encode_fps_list, int(images.shape[2]), time.time() - start_time)
|
| 130 |
+
return latents
|
| 131 |
+
|
| 132 |
+
def _timed_stream_decode(self, denoised_pred: torch.Tensor) -> torch.Tensor:
|
| 133 |
+
self._sync_for_timing()
|
| 134 |
+
start_time = time.time()
|
| 135 |
+
video = self.pipeline.vae.stream_decode_to_pixel(denoised_pred)
|
| 136 |
+
self._sync_for_timing()
|
| 137 |
+
self._record_stage_fps(self.decode_fps_list, int(video.shape[1]), time.time() - start_time)
|
| 138 |
+
return video
|
| 139 |
+
|
| 140 |
+
def reset_stream_state(self, reset_vae_flags: bool = True) -> None:
|
| 141 |
+
"""Reset cached state before starting a new no-batch stream session."""
|
| 142 |
+
if reset_vae_flags:
|
| 143 |
+
self.pipeline.vae.model.first_encode = True
|
| 144 |
+
self.pipeline.vae.model.first_decode = True
|
| 145 |
+
|
| 146 |
+
self.pipeline.kv_cache1 = None
|
| 147 |
+
self.pipeline.crossattn_cache = None
|
| 148 |
+
self.pipeline.hidden_states = None
|
| 149 |
+
self.pipeline.block_x = None
|
| 150 |
+
self.processed = 0
|
| 151 |
+
|
| 152 |
+
def _encode_noisy_latents(self, images: torch.Tensor, noise_scale: float) -> torch.Tensor:
|
| 153 |
+
latents = self._timed_stream_encode(images)
|
| 154 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 155 |
+
noise = torch.randn_like(latents)
|
| 156 |
+
return noise * noise_scale + latents * (1 - noise_scale)
|
| 157 |
+
|
| 158 |
+
def _decode_video_array(self, denoised_pred: torch.Tensor, last_frame_only: bool = False) -> np.ndarray:
|
| 159 |
+
if last_frame_only:
|
| 160 |
+
denoised_pred = denoised_pred[[-1]]
|
| 161 |
+
|
| 162 |
+
video = self._timed_stream_decode(denoised_pred)
|
| 163 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 164 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 165 |
+
return video.detach().cpu().float().numpy()
|
| 166 |
+
|
| 167 |
+
def start_stream_session(self, prompt: str, images: torch.Tensor, noise_scale: float) -> tuple[SingleGPUStreamSession, np.ndarray]:
|
| 168 |
+
"""Initialize a no-batch streaming session and return the first decoded frames."""
|
| 169 |
+
self.reset_stream_state(reset_vae_flags=True)
|
| 170 |
+
self.pipeline.denoising_step_list = self._canonical_denoising_step_list.clone()
|
| 171 |
+
|
| 172 |
+
chunk_size = self.base_chunk_size * self.pipeline.num_frame_per_block
|
| 173 |
+
current_start = 0
|
| 174 |
+
current_end = self.pipeline.frame_seq_length * (1 + chunk_size // self.base_chunk_size)
|
| 175 |
+
|
| 176 |
+
noisy_latents = self._encode_noisy_latents(images, noise_scale)
|
| 177 |
+
denoised_pred = self.prepare_pipeline(
|
| 178 |
+
text_prompts=[prompt],
|
| 179 |
+
noise=noisy_latents,
|
| 180 |
+
current_start=current_start,
|
| 181 |
+
current_end=current_end,
|
| 182 |
+
batch_denoise=False,
|
| 183 |
+
)
|
| 184 |
+
initial_video = self._decode_video_array(denoised_pred, last_frame_only=False)
|
| 185 |
+
|
| 186 |
+
session = SingleGPUStreamSession(
|
| 187 |
+
prompt=prompt,
|
| 188 |
+
noise_scale=noise_scale,
|
| 189 |
+
init_noise_scale=noise_scale,
|
| 190 |
+
chunk_size=chunk_size,
|
| 191 |
+
current_start=current_end,
|
| 192 |
+
current_end=current_end + (chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length,
|
| 193 |
+
last_image=images[:, :, [-1]],
|
| 194 |
+
processed=0,
|
| 195 |
+
)
|
| 196 |
+
return session, initial_video
|
| 197 |
+
|
| 198 |
+
def run_stream_batch(self, session: SingleGPUStreamSession, images: torch.Tensor, queue_wait_time: float | None = None) -> List[np.ndarray]:
|
| 199 |
+
"""Process one or more chunk-aligned frame groups for an active no-batch stream session."""
|
| 200 |
+
num_frames = images.shape[2]
|
| 201 |
+
input_batch = num_frames // session.chunk_size
|
| 202 |
+
noise_scale, current_step = compute_noise_scale_and_step(
|
| 203 |
+
input_video_original=torch.cat([session.last_image, images], dim=2),
|
| 204 |
+
end_idx=num_frames + 1,
|
| 205 |
+
chunk_size=num_frames,
|
| 206 |
+
noise_scale=float(session.noise_scale),
|
| 207 |
+
init_noise_scale=float(session.init_noise_scale),
|
| 208 |
+
)
|
| 209 |
+
noisy_latents = self._encode_noisy_latents(images, noise_scale)
|
| 210 |
+
|
| 211 |
+
outputs: List[np.ndarray] = []
|
| 212 |
+
|
| 213 |
+
for batch_idx in range(input_batch):
|
| 214 |
+
if session.current_start // self.pipeline.frame_seq_length >= self.t_refresh:
|
| 215 |
+
session.current_start = self.pipeline.kv_cache_length - self.pipeline.frame_seq_length
|
| 216 |
+
session.current_end = session.current_start + (session.chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 217 |
+
|
| 218 |
+
denoised_pred = self.pipeline.inference_wo_batch(
|
| 219 |
+
noise=noisy_latents[:, batch_idx].unsqueeze(1),
|
| 220 |
+
current_start=session.current_start,
|
| 221 |
+
current_end=session.current_end,
|
| 222 |
+
current_step=current_step,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
session.processed += 1
|
| 226 |
+
self.processed = session.processed
|
| 227 |
+
outputs.append(self._decode_video_array(denoised_pred, last_frame_only=True))
|
| 228 |
+
|
| 229 |
+
session.current_start = session.current_end
|
| 230 |
+
session.current_end += (session.chunk_size // self.base_chunk_size) * self.pipeline.frame_seq_length
|
| 231 |
+
|
| 232 |
+
session.last_image = images[:, :, [-1]]
|
| 233 |
+
session.noise_scale = noise_scale
|
| 234 |
+
return outputs
|
| 235 |
+
|
| 236 |
+
def run_inference(self, input_video_original: torch.Tensor, prompts: list,
|
| 237 |
+
num_chunks: int, chunk_size: int, noise_scale: float,
|
| 238 |
+
output_folder: str, fps: int, num_steps: int):
|
| 239 |
+
"""
|
| 240 |
+
Run the complete single GPU inference pipeline.
|
| 241 |
+
|
| 242 |
+
This method integrates the complete encoding, inference, and decoding pipeline.
|
| 243 |
+
"""
|
| 244 |
+
self.logger.info("Starting single GPU inference pipeline")
|
| 245 |
+
|
| 246 |
+
os.makedirs(output_folder, exist_ok=True)
|
| 247 |
+
results = {}
|
| 248 |
+
save_results = 0
|
| 249 |
+
|
| 250 |
+
fps_list = []
|
| 251 |
+
dit_fps_list = []
|
| 252 |
+
self.encode_fps_list = []
|
| 253 |
+
self.decode_fps_list = []
|
| 254 |
+
|
| 255 |
+
# Initialize variables
|
| 256 |
+
start_idx = 0
|
| 257 |
+
end_idx = 1 + chunk_size
|
| 258 |
+
current_start = 0
|
| 259 |
+
current_end = self.pipeline.frame_seq_length * (1+chunk_size//4)
|
| 260 |
+
init_noise_scale = noise_scale
|
| 261 |
+
|
| 262 |
+
self._sync_for_timing()
|
| 263 |
+
start_time = time.time()
|
| 264 |
+
|
| 265 |
+
# Process first chunk (initialization)
|
| 266 |
+
if end_idx <= input_video_original.shape[2]:
|
| 267 |
+
inp = input_video_original[:, :, start_idx:end_idx]
|
| 268 |
+
|
| 269 |
+
# VAE encoding
|
| 270 |
+
latents = self._timed_stream_encode(inp)
|
| 271 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 272 |
+
|
| 273 |
+
noise = torch.randn_like(latents)
|
| 274 |
+
noisy_latents = noise * noise_scale + latents * (1 - noise_scale)
|
| 275 |
+
|
| 276 |
+
# Prepare pipeline
|
| 277 |
+
denoised_pred = self.prepare_pipeline(
|
| 278 |
+
text_prompts=prompts,
|
| 279 |
+
noise=noisy_latents,
|
| 280 |
+
current_start=current_start,
|
| 281 |
+
current_end=current_end,
|
| 282 |
+
batch_denoise=False,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# Save first result - only start decoding after num_steps
|
| 286 |
+
video = self._timed_stream_decode(denoised_pred)
|
| 287 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 288 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 289 |
+
results[save_results] = video.cpu().float().numpy()
|
| 290 |
+
save_results += 1
|
| 291 |
+
|
| 292 |
+
# Process remaining chunks
|
| 293 |
+
while self.processed < num_chunks + num_steps - 1:
|
| 294 |
+
# Update indices
|
| 295 |
+
start_idx = end_idx
|
| 296 |
+
end_idx = end_idx + chunk_size
|
| 297 |
+
current_start = current_end
|
| 298 |
+
current_end = current_end + (chunk_size // 4) * self.pipeline.frame_seq_length
|
| 299 |
+
|
| 300 |
+
if end_idx <= input_video_original.shape[2]:
|
| 301 |
+
inp = input_video_original[:, :, start_idx:end_idx]
|
| 302 |
+
|
| 303 |
+
noise_scale, current_step = compute_noise_scale_and_step(
|
| 304 |
+
input_video_original, end_idx, chunk_size, noise_scale, init_noise_scale
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# VAE encoding
|
| 308 |
+
latents = self._timed_stream_encode(inp)
|
| 309 |
+
latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)
|
| 310 |
+
|
| 311 |
+
noise = torch.randn_like(latents)
|
| 312 |
+
noisy_latents = noise * noise_scale + latents * (1 - noise_scale)
|
| 313 |
+
|
| 314 |
+
if current_start//self.pipeline.frame_seq_length >= 50:
|
| 315 |
+
current_start = self.pipeline.kv_cache_length - self.pipeline.frame_seq_length
|
| 316 |
+
current_end = current_start + (chunk_size // 4) * self.pipeline.frame_seq_length
|
| 317 |
+
|
| 318 |
+
self._sync_for_timing()
|
| 319 |
+
dit_start_time = time.time()
|
| 320 |
+
# DiT inference - using input mode to process all 30 blocks
|
| 321 |
+
denoised_pred = self.pipeline.inference_wo_batch(
|
| 322 |
+
noise=noisy_latents,
|
| 323 |
+
current_start=current_start,
|
| 324 |
+
current_end=current_end,
|
| 325 |
+
current_step=current_step,
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
if self.processed >self.processed_offset:
|
| 329 |
+
self._sync_for_timing()
|
| 330 |
+
if self.profile:
|
| 331 |
+
dit_fps_list.append(chunk_size / (time.time() - dit_start_time))
|
| 332 |
+
|
| 333 |
+
self.processed += 1
|
| 334 |
+
|
| 335 |
+
# VAE decoding - only start decoding after num_steps
|
| 336 |
+
video = self._timed_stream_decode(denoised_pred[[-1]])
|
| 337 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 338 |
+
video = video[0].permute(0, 2, 3, 1).contiguous()
|
| 339 |
+
|
| 340 |
+
results[save_results] = video.cpu().float().numpy()
|
| 341 |
+
save_results += 1
|
| 342 |
+
|
| 343 |
+
# Update timing
|
| 344 |
+
if self.profile:
|
| 345 |
+
self._sync_for_timing()
|
| 346 |
+
end_time = time.time()
|
| 347 |
+
t = end_time - start_time
|
| 348 |
+
fps_test = inp.shape[2] / t
|
| 349 |
+
fps_list.append(fps_test)
|
| 350 |
+
self.logger.info(f"Processed {self.processed}, time: {t:.4f} s, FPS: {fps_test:.4f}")
|
| 351 |
+
start_time = end_time
|
| 352 |
+
|
| 353 |
+
# Save final video
|
| 354 |
+
video_list = [results[i] for i in range(num_chunks)]
|
| 355 |
+
video = np.concatenate(video_list, axis=0)
|
| 356 |
+
if self.profile and fps_list:
|
| 357 |
+
fps_avg = np.mean(np.array(fps_list))
|
| 358 |
+
dit_avg = np.mean(np.array(dit_fps_list)) if dit_fps_list else 0.0
|
| 359 |
+
encode_avg = np.mean(np.array(self.encode_fps_list)) if self.encode_fps_list else 0.0
|
| 360 |
+
decode_avg = np.mean(np.array(self.decode_fps_list)) if self.decode_fps_list else 0.0
|
| 361 |
+
self.logger.info(f"VAE Encode Average FPS: {encode_avg:.4f}")
|
| 362 |
+
self.logger.info(f"DiT Average FPS: {dit_avg:.4f}")
|
| 363 |
+
self.logger.info(f"VAE Decode Average FPS: {decode_avg:.4f}")
|
| 364 |
+
self.logger.info(f"Video shape: {video.shape}, Average FPS: {fps_avg:.4f}")
|
| 365 |
+
else:
|
| 366 |
+
self.logger.info(f"Video shape: {video.shape}")
|
| 367 |
+
|
| 368 |
+
output_path = os.path.join(output_folder, f"output_{0:03d}.mp4")
|
| 369 |
+
export_to_video(video, output_path, fps=fps)
|
| 370 |
+
self.logger.info(f"Video saved to: {output_path}")
|
| 371 |
+
|
| 372 |
+
self.logger.info("Single GPU inference pipeline completed")
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def main():
|
| 376 |
+
"""Main function for the single GPU inference pipeline."""
|
| 377 |
+
parser = argparse.ArgumentParser()
|
| 378 |
+
parser.add_argument("--config_path", type=str, required=True, help="Configuration file path")
|
| 379 |
+
parser.add_argument("--checkpoint_folder", type=str, required=True, help="Checkpoint folder path")
|
| 380 |
+
parser.add_argument("--output_folder", type=str, required=True, help="Output folder path")
|
| 381 |
+
parser.add_argument("--prompt_file_path", type=str, required=True, help="Prompt file path")
|
| 382 |
+
parser.add_argument("--video_path", type=str, required=True, help="Input video path")
|
| 383 |
+
parser.add_argument("--noise_scale", type=float, default=0.700, help="Noise scale")
|
| 384 |
+
parser.add_argument("--height", type=int, default=480, help="Video height")
|
| 385 |
+
parser.add_argument("--width", type=int, default=832, help="Video width")
|
| 386 |
+
parser.add_argument("--fps", type=int, default=16, help="Output video fps")
|
| 387 |
+
parser.add_argument("--step", type=int, default=2, help="Step")
|
| 388 |
+
parser.add_argument("--seed", type=int, default=0, help="Random seed")
|
| 389 |
+
parser.add_argument("--gpu_id", type=int, default=None, help="CUDA device index for single-GPU inference")
|
| 390 |
+
parser.add_argument("--t2v", action="store_true", default=False)
|
| 391 |
+
parser.add_argument("--model_type", type=str, default="T2V-1.3B", help="Model type (e.g., T2V-1.3B)")
|
| 392 |
+
parser.add_argument("--profile", action="store_true", default=False, help="Enable synchronized throughput logging")
|
| 393 |
+
parser.add_argument("--use_taehv", action="store_true", default=False, help="Use the lightweight TAEHV VAE for encode/decode")
|
| 394 |
+
parser.add_argument("--use_tensorrt", "--use_taehv_tensorrt", dest="use_tensorrt", action="store_true", default=False, help="Enable available TensorRT acceleration paths")
|
| 395 |
+
parser.add_argument("--fast", action="store_true", default=False, help="Enable the fast path: --use_taehv --use_tensorrt")
|
| 396 |
+
args = parser.parse_args()
|
| 397 |
+
|
| 398 |
+
torch.set_grad_enabled(False)
|
| 399 |
+
|
| 400 |
+
# Auto-detect device
|
| 401 |
+
if torch.cuda.is_available():
|
| 402 |
+
if args.gpu_id is not None:
|
| 403 |
+
torch.cuda.set_device(args.gpu_id)
|
| 404 |
+
device = torch.device(f"cuda:{args.gpu_id}")
|
| 405 |
+
else:
|
| 406 |
+
device = torch.device("cuda")
|
| 407 |
+
else:
|
| 408 |
+
device = torch.device("cpu")
|
| 409 |
+
|
| 410 |
+
# Load configuration
|
| 411 |
+
config = merge_cli_config(args.config_path, args)
|
| 412 |
+
|
| 413 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 414 |
+
LOGGER.info("Denoising Step List: %s", list(config.denoising_step_list))
|
| 415 |
+
|
| 416 |
+
set_seed(args.seed)
|
| 417 |
+
|
| 418 |
+
# Load input video
|
| 419 |
+
input_video_original = load_mp4_as_tensor(args.video_path, resize_hw=(args.height, args.width)).unsqueeze(0)
|
| 420 |
+
if input_video_original.dtype != torch.bfloat16:
|
| 421 |
+
input_video_original = input_video_original.to(dtype=torch.bfloat16).to(device)
|
| 422 |
+
|
| 423 |
+
LOGGER.info("Input video tensor shape: %s", tuple(input_video_original.shape))
|
| 424 |
+
b, c, t, h, w = input_video_original.shape
|
| 425 |
+
|
| 426 |
+
# Calculate number of chunks
|
| 427 |
+
chunk_size = 4 * config.num_frame_per_block
|
| 428 |
+
num_chunks = (t - 1) // chunk_size
|
| 429 |
+
|
| 430 |
+
# Initialize pipeline manager
|
| 431 |
+
pipeline_manager = SingleGPUInferencePipeline(config, device)
|
| 432 |
+
pipeline_manager.load_model(args.checkpoint_folder)
|
| 433 |
+
|
| 434 |
+
# Load prompts
|
| 435 |
+
dataset = TextDataset(args.prompt_file_path)
|
| 436 |
+
prompts = [dataset[0]]
|
| 437 |
+
num_steps = len(pipeline_manager.pipeline.denoising_step_list)
|
| 438 |
+
|
| 439 |
+
# Run inference
|
| 440 |
+
try:
|
| 441 |
+
pipeline_manager.run_inference(
|
| 442 |
+
input_video_original, prompts, num_chunks, chunk_size,
|
| 443 |
+
args.noise_scale, args.output_folder, args.fps, num_steps
|
| 444 |
+
)
|
| 445 |
+
except Exception as e:
|
| 446 |
+
LOGGER.exception("Error occurred during inference: %s", e)
|
| 447 |
+
raise
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
if __name__ == "__main__":
|
| 451 |
+
main()
|