Files
nvlabs--longlive/pipeline/causal_diffusion_inference_sp.py
2026-07-13 12:31:40 +08:00

255 lines
9.6 KiB
Python

# Copyright 2024-2025 LongLive Authors. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Sequence-parallel causal diffusion inference pipeline for Wan2.2-TI2V-5B."""
from typing import List, Optional
import types
import torch
import torch.distributed as dist
from pipeline.causal_diffusion_inference import CausalDiffusionInferencePipeline
from utils.config import wan_default_config
from utils.scheduler import FlowMatchScheduler, SchedulerInterface
from wan_5b.distributed.sp_ulysses_inference import (
get_sp_world_size,
init_sequence_parallel,
is_sp_enabled,
sp_print,
)
def _model_kw(model_kwargs, key, default=None):
if isinstance(model_kwargs, dict):
return model_kwargs.get(key, default)
return getattr(model_kwargs, key, default)
class SPWanDiffusionWrapper5B(torch.nn.Module):
"""Wan 5B diffusion wrapper backed by the Ulysses SP causal model."""
def __init__(
self,
model_name="Wan2.2-TI2V-5B",
timestep_shift=5.0,
local_attn_size=-1,
sink_size=0,
num_frame_per_block=1,
t_scale=1.0,
rope_method="linear",
original_seq_len=None,
):
super().__init__()
if model_name != "Wan2.2-TI2V-5B":
raise ValueError(f"SP inference only supports Wan2.2-TI2V-5B, got {model_name}")
from wan_5b.modules.causal_model_sp_ulysses import UlyssesSPCausalWanModel
sp_print("Using Ulysses SP model for Wan2.2-TI2V-5B")
self.model = UlyssesSPCausalWanModel(
model_type="ti2v",
in_dim=48,
dim=3072,
ffn_dim=14336,
out_dim=48,
num_heads=24,
num_layers=30,
local_attn_size=local_attn_size,
sink_size=sink_size,
num_frame_per_block=num_frame_per_block,
)
self.model.eval()
self.model.t_scale = t_scale
self.model.rope_method = rope_method
self.model.original_seq_len = original_seq_len
self.uniform_timestep = False
self.scheduler = FlowMatchScheduler(
shift=timestep_shift, sigma_min=0.0, extra_one_step=True
)
self.scheduler.set_timesteps(1000, training=True)
self.seq_len = 28160
self._compiled_model_call = None
self.get_scheduler()
def get_scheduler(self) -> SchedulerInterface:
scheduler = self.scheduler
scheduler.convert_x0_to_noise = types.MethodType(
SchedulerInterface.convert_x0_to_noise, scheduler
)
scheduler.convert_noise_to_x0 = types.MethodType(
SchedulerInterface.convert_noise_to_x0, scheduler
)
scheduler.convert_velocity_to_x0 = types.MethodType(
SchedulerInterface.convert_velocity_to_x0, scheduler
)
self.scheduler = scheduler
return scheduler
def configure_torch_compile(
self,
*,
backend: str = "inductor",
mode: str | None = "max-autotune-no-cudagraphs",
fullgraph: bool = False,
dynamic: bool | None = False,
options: dict | None = None,
suppress_errors: bool = True,
) -> bool:
from utils.torch_compile_utils import configure_module_call_torch_compile
self._compiled_model_call = configure_module_call_torch_compile(
self.model,
name="SPWanDiffusionWrapper5B.model",
backend=backend,
mode=mode,
fullgraph=fullgraph,
dynamic=dynamic,
options=options,
suppress_errors=suppress_errors,
)
return self._compiled_model_call is not None
def _call_model(self, *args, **kwargs):
if self._compiled_model_call is not None:
return self._compiled_model_call(*args, **kwargs)
return self.model(*args, **kwargs)
def _convert_flow_pred_to_x0(
self, flow_pred: torch.Tensor, xt: torch.Tensor, timestep: torch.Tensor
) -> torch.Tensor:
original_dtype = flow_pred.dtype
flow_pred, xt, sigmas, timesteps = map(
lambda x: x.double().to(flow_pred.device),
[flow_pred, xt, self.scheduler.sigmas, self.scheduler.timesteps],
)
timestep_id = torch.argmin(
(timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
return (xt - sigma_t * flow_pred).to(original_dtype)
def forward(
self,
noisy_image_or_video: torch.Tensor,
conditional_dict: dict,
timestep: torch.Tensor,
kv_cache: Optional[List[dict]] = None,
crossattn_cache: Optional[List[dict]] = None,
current_start: Optional[int] = None,
cache_start: Optional[int] = None,
**_,
) -> torch.Tensor:
prompt_embeds = conditional_dict["prompt_embeds"]
input_timestep = timestep[:, 0] if self.uniform_timestep else timestep
flow_pred = self._call_model(
noisy_image_or_video.permute(0, 2, 1, 3, 4),
t=input_timestep,
context=prompt_embeds,
seq_len=self.seq_len,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start=current_start if current_start is not None else 0,
cache_start=cache_start if cache_start is not None else 0,
).permute(0, 2, 1, 3, 4)
pred_x0 = self._convert_flow_pred_to_x0(
flow_pred=flow_pred.flatten(0, 1),
xt=noisy_image_or_video.flatten(0, 1),
timestep=timestep.flatten(0, 1),
).unflatten(0, flow_pred.shape[:2])
return flow_pred, pred_x0
class CausalDiffusionInferencePipelineSP(CausalDiffusionInferencePipeline):
"""LongLive2.0 diffusion inference pipeline using Ulysses sequence parallelism."""
def __init__(
self,
args,
device,
generator=None,
text_encoder=None,
vae=None,
sp_group=None,
dp_rank: int = 0,
):
if dist.is_initialized():
init_sequence_parallel(group=sp_group)
self.dp_rank = dp_rank
if generator is None:
model_kwargs = getattr(args, "model_kwargs", {})
generator = SPWanDiffusionWrapper5B(
model_name=_model_kw(model_kwargs, "model_name", "Wan2.2-TI2V-5B"),
timestep_shift=_model_kw(model_kwargs, "timestep_shift", 5.0),
local_attn_size=_model_kw(model_kwargs, "local_attn_size", -1),
sink_size=_model_kw(model_kwargs, "sink_size", 0),
num_frame_per_block=getattr(args, "num_frame_per_block", 1),
t_scale=getattr(args, "t_scale", 1.0),
rope_method=getattr(args, "rope_method", "linear"),
original_seq_len=getattr(args, "original_seq_len", None),
)
super().__init__(
args=args,
device=device,
generator=generator,
text_encoder=text_encoder,
vae=vae,
)
if self.quantize_kv:
raise ValueError("kv_quant is not supported in Ulysses SP inference.")
sp_print(
f"SP diffusion pipeline initialized: nfpb={self.num_frame_per_block}, "
f"sp_world={get_sp_world_size() if is_sp_enabled() else 1}, dp_rank={dp_rank}"
)
def _initialize_kv_cache(self, batch_size, dtype, device):
"""Initialize head-split KV caches for Ulysses SP."""
kv_cache_pos = []
kv_cache_neg = []
num_heads = wan_default_config[self.model_name]["num_heads"]
head_dim = wan_default_config[self.model_name]["head_dim"]
sp_world_size = get_sp_world_size() if is_sp_enabled() else 1
if num_heads % sp_world_size != 0:
raise ValueError(
f"num_heads ({num_heads}) must be divisible by sp_world_size ({sp_world_size})"
)
cache_heads = num_heads // sp_world_size
if self.local_attn_size != -1:
kv_cache_size = self.local_attn_size * self.frame_seq_length
else:
kv_cache_size = 3 * self.num_frame_per_block * self.frame_seq_length
block_token_size = self.num_frame_per_block * self.frame_seq_length
max_blocks = kv_cache_size // block_token_size
sp_print(
f"Initializing SP KV cache: size={kv_cache_size}, heads/rank={cache_heads}, "
f"block_token_size={block_token_size}"
)
for _ in range(self.num_transformer_blocks):
entry = {
"k": torch.zeros(
[batch_size, kv_cache_size, cache_heads, head_dim],
dtype=dtype, device=device,
),
"v": torch.zeros(
[batch_size, kv_cache_size, cache_heads, head_dim],
dtype=dtype, device=device,
),
"quantized": False,
"block_token_size": block_token_size,
"max_blocks": max_blocks,
"num_heads": cache_heads,
"num_filled_blocks": 0,
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
"local_end_index": torch.tensor([0], dtype=torch.long, device=device),
"pinned_start": torch.tensor([-1], dtype=torch.long, device=device),
"pinned_len": torch.tensor([0], dtype=torch.long, device=device),
}
kv_cache_pos.append({key: value.clone() if torch.is_tensor(value) else value for key, value in entry.items()})
kv_cache_neg.append({key: value.clone() if torch.is_tensor(value) else value for key, value in entry.items()})
self.kv_cache_pos = kv_cache_pos
self.kv_cache_neg = kv_cache_neg