multimodalart HF Staff commited on
Commit
5c93746
·
verified ·
1 Parent(s): e88b235

Upload folder using huggingface_hub

Browse files
Files changed (39) hide show
  1. models/__init__.py +38 -0
  2. models/data.py +17 -0
  3. models/model_interface.py +114 -0
  4. models/scheduler.py +103 -0
  5. models/util.py +17 -0
  6. models/wan/__init__.py +0 -0
  7. models/wan/causal_model.py +1013 -0
  8. models/wan/causal_stream_inference.py +420 -0
  9. models/wan/flow_match.py +83 -0
  10. models/wan/taehv_wrapper.py +505 -0
  11. models/wan/wan_base/README.md +2 -0
  12. models/wan/wan_base/__init__.py +1 -0
  13. models/wan/wan_base/modules/__init__.py +16 -0
  14. models/wan/wan_base/modules/attention.py +310 -0
  15. models/wan/wan_base/modules/model.py +653 -0
  16. models/wan/wan_base/modules/t5.py +515 -0
  17. models/wan/wan_base/modules/tokenizers.py +82 -0
  18. models/wan/wan_base/modules/vae.py +754 -0
  19. models/wan/wan_wrapper.py +351 -0
  20. requirements.txt +12 -1
  21. streamdiffusionv2/__init__.py +22 -0
  22. streamdiffusionv2/pipeline.py +437 -0
  23. streamv2v/__init__.py +14 -0
  24. streamv2v/api.py +309 -0
  25. streamv2v/communication/__init__.py +27 -0
  26. streamv2v/communication/buffer_manager.py +252 -0
  27. streamv2v/communication/data_containers.py +148 -0
  28. streamv2v/communication/distributed_communicator.py +392 -0
  29. streamv2v/communication/kv_cache_manager.py +263 -0
  30. streamv2v/communication/model_data_transfer.py +273 -0
  31. streamv2v/communication/test_communication.py +393 -0
  32. streamv2v/communication/utils.py +288 -0
  33. streamv2v/configs/__init__.py +2 -0
  34. streamv2v/configs/wan_causal_dmd_v2v.yaml +57 -0
  35. streamv2v/configs/wan_causal_dmd_v2v_fast.yaml +57 -0
  36. streamv2v/inference.py +525 -0
  37. streamv2v/inference_common.py +142 -0
  38. streamv2v/inference_pipe.py +1022 -0
  39. 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
- streamdiffusionv2
 
 
 
 
 
 
 
 
 
 
 
 
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()