mirror of
https://github.com/hanzoai/live.git
synced 2026-10-30 18:29:41 +00:00
393 lines
14 KiB
Python
393 lines
14 KiB
Python
# Contains code based on https://github.com/NVlabs/LongLive
|
|
import logging
|
|
|
|
import torch
|
|
|
|
from ..process import postprocess_chunk
|
|
|
|
# The VAE compresses a pixel frame into a latent frame which consists of patches
|
|
# The patch embedding converts spatial patches into tokens
|
|
# The VAE does 8x spatial downsampling
|
|
# The patch embedding does 2x spatial downsampling
|
|
# Thus, we end up spatially scaling down by 16
|
|
SCALE_SIZE = 16
|
|
|
|
# The VAE does 8x spatial downsampling
|
|
VAE_SPATIAL_DOWNSAMPLE_FACTOR = 8
|
|
|
|
# https://github.com/hanzoai/live/blob/a6a7aa1d7a3be60d3b444e254f83a9fd09e9151a/pipelines/base/wan2_1/modules/causal_model.py#L117
|
|
MAX_ROPE_FREQ_TABLE_SEQ_LEN = 1024
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class InferencePipeline(torch.nn.Module):
|
|
def __init__(
|
|
self,
|
|
config,
|
|
generator,
|
|
text_encoder,
|
|
vae,
|
|
low_memory: bool = False,
|
|
seed: int = 42,
|
|
):
|
|
super().__init__()
|
|
|
|
# The height and width must be divisible by SCALE_SIZE
|
|
req_height = config.get("height", 512)
|
|
req_width = config.get("width", 512)
|
|
self.height = round(req_height / SCALE_SIZE) * SCALE_SIZE
|
|
self.width = round(req_width / SCALE_SIZE) * SCALE_SIZE
|
|
|
|
self.generator = generator
|
|
self.text_encoder = text_encoder
|
|
self.vae = vae
|
|
self.low_memory = low_memory
|
|
self.base_seed = seed
|
|
self.scheduler = self.generator.get_scheduler()
|
|
self.denoising_step_list = torch.tensor(
|
|
config.denoising_step_list, dtype=torch.long
|
|
)
|
|
if config.warp_denoising_step:
|
|
timesteps = torch.cat(
|
|
(self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))
|
|
)
|
|
self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
|
|
|
|
self.num_transformer_blocks = 30
|
|
self.frame_seq_length = (self.height // SCALE_SIZE) * (self.width // SCALE_SIZE)
|
|
|
|
self.kv_cache1 = None
|
|
self.config = config
|
|
self.batch_size = 1
|
|
self.local_attn_size = config.model_kwargs.local_attn_size
|
|
self.recache_buffer = None
|
|
self.num_frame_per_block = getattr(config, "num_frame_per_block", 1)
|
|
|
|
print(f"KV inference with {self.num_frame_per_block} frames per block")
|
|
|
|
if self.num_frame_per_block > 1:
|
|
self.generator.model.num_frame_per_block = self.num_frame_per_block
|
|
|
|
self.conditional_dict = None
|
|
self.current_start = 0
|
|
|
|
@torch.no_grad()
|
|
def prepare(
|
|
self,
|
|
prompts: list[str] = None,
|
|
denoising_step_list: list[int] = None,
|
|
init_cache: bool = False,
|
|
):
|
|
# CausalWanModel uses a RoPE frequency table with a max sequence length of 1024
|
|
# This means that it has positions for 1024 latent frames
|
|
# current_start is used to index into this table
|
|
# We need to make sure that current_start does not shift past the max sequence length of the RoPE frequency table
|
|
# when slicing here https://github.com/hanzoai/live/blob/a6a7aa1d7a3be60d3b444e254f83a9fd09e9151a/pipelines/base/wan2_1/modules/causal_model.py#L52
|
|
# When we hit the limit we reset the caches and indices
|
|
max_current_start = MAX_ROPE_FREQ_TABLE_SEQ_LEN - self.num_frame_per_block
|
|
if self.current_start >= max_current_start:
|
|
init_cache = True
|
|
|
|
if prompts is not None:
|
|
# Make sure text encoder is on right device
|
|
generator_device = next(self.generator.model.parameters()).device
|
|
self.text_encoder = self.text_encoder.to(generator_device)
|
|
|
|
self.conditional_dict = self.text_encoder(text_prompts=prompts)
|
|
if self.batch_size > 1:
|
|
self.conditional_dict["prompt_embeds"] = self.conditional_dict[
|
|
"prompt_embeds"
|
|
].repeat(self.batch_size, 1, 1)
|
|
|
|
# If in low memory mode offload text encoder to CPU
|
|
if self.low_memory:
|
|
self.text_encoder = self.text_encoder.to(torch.device("cpu"))
|
|
|
|
if not init_cache and self.current_start > 0:
|
|
self._recache_frames()
|
|
|
|
if denoising_step_list is not None:
|
|
self.denoising_step_list = torch.tensor(
|
|
denoising_step_list, dtype=torch.long
|
|
)
|
|
|
|
if not init_cache:
|
|
return
|
|
|
|
self.current_start = 0
|
|
|
|
# Only support local attention
|
|
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
|
|
|
generator_param = next(self.generator.model.parameters())
|
|
self._initialize_kv_cache(
|
|
batch_size=self.batch_size,
|
|
dtype=generator_param.dtype,
|
|
device=generator_param.device,
|
|
kv_cache_size_override=kv_cache_size,
|
|
)
|
|
self._initialize_crossattn_cache(
|
|
batch_size=self.batch_size,
|
|
dtype=generator_param.dtype,
|
|
device=generator_param.device,
|
|
)
|
|
|
|
self.generator.model.local_attn_size = self.local_attn_size
|
|
self._set_all_modules_max_attention_size(self.local_attn_size)
|
|
|
|
self.vae.clear_cache()
|
|
|
|
# Initialize recache buffer
|
|
latent_height = self.height // VAE_SPATIAL_DOWNSAMPLE_FACTOR
|
|
latent_width = self.width // VAE_SPATIAL_DOWNSAMPLE_FACTOR
|
|
self.recache_buffer = torch.zeros(
|
|
[
|
|
self.batch_size,
|
|
self.local_attn_size,
|
|
16,
|
|
latent_height,
|
|
latent_width,
|
|
],
|
|
dtype=generator_param.dtype,
|
|
device=generator_param.device
|
|
if not self.low_memory
|
|
else torch.device("cpu"),
|
|
)
|
|
|
|
@torch.no_grad()
|
|
def __call__(
|
|
self, _: torch.Tensor | list[torch.Tensor] | None = None
|
|
) -> torch.Tensor:
|
|
# Ignore input
|
|
|
|
latent_height = self.height // VAE_SPATIAL_DOWNSAMPLE_FACTOR
|
|
latent_width = self.width // VAE_SPATIAL_DOWNSAMPLE_FACTOR
|
|
generator_param = next(self.generator.model.parameters())
|
|
|
|
# Create generator from seed for reproducible generation
|
|
# Derive unique seed per block of latents using current_start as offset
|
|
frame_seed = self.base_seed + self.current_start
|
|
rng = torch.Generator(device=generator_param.device).manual_seed(frame_seed)
|
|
|
|
noise = torch.randn(
|
|
[
|
|
self.batch_size,
|
|
self.num_frame_per_block,
|
|
16,
|
|
latent_height,
|
|
latent_width,
|
|
],
|
|
device=generator_param.device,
|
|
dtype=generator_param.dtype,
|
|
generator=rng,
|
|
)
|
|
|
|
for index, current_timestep in enumerate(self.denoising_step_list):
|
|
timestep = (
|
|
torch.ones(
|
|
[self.batch_size, self.num_frame_per_block],
|
|
device=noise.device,
|
|
dtype=torch.int64,
|
|
)
|
|
* current_timestep
|
|
)
|
|
|
|
if index < len(self.denoising_step_list) - 1:
|
|
_, denoised_pred = self.generator(
|
|
noisy_image_or_video=noise,
|
|
conditional_dict=self.conditional_dict,
|
|
timestep=timestep,
|
|
kv_cache=self.kv_cache1,
|
|
crossattn_cache=self.crossattn_cache,
|
|
current_start=self.current_start * self.frame_seq_length,
|
|
)
|
|
next_timestep = self.denoising_step_list[index + 1]
|
|
# Create noise with same shape and properties as denoised_pred
|
|
flattened_pred = denoised_pred.flatten(0, 1)
|
|
random_noise = torch.randn(
|
|
flattened_pred.shape,
|
|
device=flattened_pred.device,
|
|
dtype=flattened_pred.dtype,
|
|
generator=rng,
|
|
)
|
|
noise = self.scheduler.add_noise(
|
|
flattened_pred,
|
|
random_noise,
|
|
next_timestep
|
|
* torch.ones(
|
|
[self.batch_size * self.num_frame_per_block],
|
|
device=noise.device,
|
|
dtype=torch.long,
|
|
),
|
|
).unflatten(0, denoised_pred.shape[:2])
|
|
else:
|
|
_, denoised_pred = self.generator(
|
|
noisy_image_or_video=noise,
|
|
conditional_dict=self.conditional_dict,
|
|
timestep=timestep,
|
|
kv_cache=self.kv_cache1,
|
|
crossattn_cache=self.crossattn_cache,
|
|
current_start=self.current_start * self.frame_seq_length,
|
|
)
|
|
|
|
# rerun with clean context to update cache
|
|
context_timestep = torch.ones_like(timestep) * 0
|
|
self.generator(
|
|
noisy_image_or_video=denoised_pred,
|
|
conditional_dict=self.conditional_dict,
|
|
timestep=context_timestep,
|
|
kv_cache=self.kv_cache1,
|
|
crossattn_cache=self.crossattn_cache,
|
|
current_start=self.current_start * self.frame_seq_length,
|
|
)
|
|
|
|
# Push the generated latents to the recache buffer (sliding window)
|
|
# Shift buffer left, append new frames at end
|
|
self.recache_buffer = torch.cat(
|
|
[
|
|
self.recache_buffer[:, self.num_frame_per_block :],
|
|
denoised_pred.to(self.recache_buffer.device),
|
|
],
|
|
dim=1,
|
|
)
|
|
|
|
self.current_start += self.num_frame_per_block
|
|
|
|
output = self.vae.decode_to_pixel(denoised_pred, use_cache=True)
|
|
return postprocess_chunk(output)
|
|
|
|
def _initialize_kv_cache(
|
|
self, batch_size, dtype, device, kv_cache_size_override: int | None = None
|
|
):
|
|
"""
|
|
Initialize a Per-GPU KV cache for the Wan model.
|
|
"""
|
|
kv_cache1 = []
|
|
# Determine cache size
|
|
if kv_cache_size_override is not None:
|
|
kv_cache_size = kv_cache_size_override
|
|
else:
|
|
if self.local_attn_size != -1:
|
|
# Local attention: cache only needs to store the window
|
|
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
|
else:
|
|
# Global attention: default cache for 21 frames (backward compatibility)
|
|
kv_cache_size = 32760
|
|
|
|
for _ in range(self.num_transformer_blocks):
|
|
kv_cache1.append(
|
|
{
|
|
"k": torch.zeros(
|
|
[batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device
|
|
),
|
|
"v": torch.zeros(
|
|
[batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device
|
|
),
|
|
"global_end_index": torch.tensor(
|
|
[0], dtype=torch.long, device=device
|
|
),
|
|
"local_end_index": torch.tensor(
|
|
[0], dtype=torch.long, device=device
|
|
),
|
|
}
|
|
)
|
|
|
|
self.kv_cache1 = kv_cache1 # always store the clean cache
|
|
|
|
def _initialize_crossattn_cache(self, batch_size, dtype, device):
|
|
"""
|
|
Initialize a Per-GPU cross-attention cache for the Wan model.
|
|
"""
|
|
crossattn_cache = []
|
|
|
|
for _ in range(self.num_transformer_blocks):
|
|
crossattn_cache.append(
|
|
{
|
|
"k": torch.zeros(
|
|
[batch_size, 512, 12, 128], dtype=dtype, device=device
|
|
),
|
|
"v": torch.zeros(
|
|
[batch_size, 512, 12, 128], dtype=dtype, device=device
|
|
),
|
|
"is_init": False,
|
|
}
|
|
)
|
|
self.crossattn_cache = crossattn_cache
|
|
|
|
def _set_all_modules_max_attention_size(self, local_attn_size_value: int):
|
|
"""
|
|
Set max_attention_size on all submodules that define it.
|
|
"""
|
|
target_size = int(local_attn_size_value) * self.frame_seq_length
|
|
|
|
updated_modules = []
|
|
# Update root model if applicable
|
|
if hasattr(self.generator.model, "max_attention_size"):
|
|
self.generator.model.max_attention_size = target_size
|
|
updated_modules.append("<root_model>")
|
|
|
|
# Update all child modules
|
|
for name, module in self.generator.model.named_modules():
|
|
if hasattr(module, "max_attention_size"):
|
|
module.max_attention_size = target_size
|
|
updated_modules.append(name if name else module.__class__.__name__)
|
|
|
|
def _recache_frames(self):
|
|
# Reset kv cache
|
|
for block_idx in range(self.num_transformer_blocks):
|
|
cache = self.kv_cache1[block_idx]
|
|
cache["k"].zero_()
|
|
cache["v"].zero_()
|
|
|
|
# Reset cross-attention cache
|
|
for blk in self.crossattn_cache:
|
|
blk["k"].zero_()
|
|
blk["v"].zero_()
|
|
blk["is_init"] = False
|
|
|
|
# Get the number of frames to recache (min of what we've generated and buffer size)
|
|
num_recache_frames = min(self.current_start, self.local_attn_size)
|
|
recache_start = self.current_start - num_recache_frames
|
|
|
|
# With sliding window, most recent frames are always at the end
|
|
generator_device = next(self.generator.model.parameters()).device
|
|
recache_frames = (
|
|
self.recache_buffer[:, -num_recache_frames:]
|
|
.contiguous()
|
|
.to(generator_device)
|
|
)
|
|
|
|
# Prepare blockwise causal mask
|
|
block_mask = self.generator.model._prepare_blockwise_causal_attn_mask(
|
|
device=recache_frames.device,
|
|
num_frames=num_recache_frames,
|
|
frame_seqlen=self.frame_seq_length,
|
|
num_frame_per_block=self.num_frame_per_block,
|
|
local_attn_size=self.local_attn_size,
|
|
)
|
|
self.generator.model.block_mask = block_mask
|
|
|
|
context_timestep = (
|
|
torch.ones(
|
|
[self.batch_size, num_recache_frames],
|
|
device=recache_frames.device,
|
|
dtype=torch.int64,
|
|
)
|
|
* 0
|
|
)
|
|
self.generator(
|
|
noisy_image_or_video=recache_frames,
|
|
conditional_dict=self.conditional_dict,
|
|
timestep=context_timestep,
|
|
kv_cache=self.kv_cache1,
|
|
crossattn_cache=self.crossattn_cache,
|
|
current_start=recache_start * self.frame_seq_length,
|
|
)
|
|
|
|
# Reset cross-attention cache
|
|
for blk in self.crossattn_cache:
|
|
blk["k"].zero_()
|
|
blk["v"].zero_()
|
|
blk["is_init"] = False
|