372 lines
16 KiB
Python
372 lines
16 KiB
Python
# Copyright 2024 NVIDIA CORPORATION & AFFILIATES
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Fixed-RoPE streaming sampler for standalone Sana V2V modules."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import os
|
|
|
|
import torch
|
|
from diffusers import FlowMatchEulerDiscreteScheduler
|
|
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
|
from diffusers.pipelines.stable_diffusion_3.pipeline_stable_diffusion_3 import retrieve_timesteps
|
|
from tqdm import tqdm
|
|
|
|
|
|
class SANAStreamingSampler:
|
|
"""Self-forcing streaming sampler that only supports fixed RoPE caching.
|
|
|
|
The sampler expects attention modules to be built directly in their runtime
|
|
cache format:
|
|
- state-cache blocks expose ``fixed_rope_cache_type = "state"``
|
|
- softmax blocks expose ``fixed_rope_cache_type = "softmax"``
|
|
|
|
It does not dynamically swap attention classes.
|
|
"""
|
|
|
|
_STATE_CACHE_CLASS_NAMES = {
|
|
"V2VStateCachedBiGDNAttention",
|
|
"QuantizedStateCachedGDN",
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
model_fn,
|
|
condition,
|
|
uncondition,
|
|
cfg_scale,
|
|
flow_shift=3.0,
|
|
model_kwargs=None,
|
|
base_chunk_frames=10,
|
|
num_cached_blocks=-1,
|
|
cache_strategy="fixed_rope",
|
|
efficient_cache=False,
|
|
**kwargs,
|
|
):
|
|
if cache_strategy not in ("fixed_rope", ""):
|
|
raise ValueError(f"SANAStreamingSampler only supports fixed_rope, got {cache_strategy!r}")
|
|
self.model = model_fn
|
|
self.condition = condition
|
|
self.uncondition = uncondition
|
|
self.cfg_scale = cfg_scale
|
|
self.model_kwargs = model_kwargs or {}
|
|
self.mask = self.model_kwargs.pop("mask", None)
|
|
self.flow_shift = flow_shift
|
|
self.base_chunk_frames = base_chunk_frames
|
|
self.num_cached_blocks = num_cached_blocks
|
|
self.efficient_cache = efficient_cache
|
|
self.sink_token = kwargs.get("sink_token", False)
|
|
self._fixed_rope_full_history_softmax_cache = self.num_cached_blocks < 0
|
|
self.block_is_state_cached = self._detect_cache_blocks()
|
|
self.num_model_blocks = len(self.block_is_state_cached)
|
|
|
|
def _model_blocks(self):
|
|
model = self.model.module if hasattr(self.model, "module") else self.model
|
|
if hasattr(model, "blocks"):
|
|
return model.blocks
|
|
if hasattr(model, "transformer_blocks"):
|
|
return model.transformer_blocks
|
|
if hasattr(model, "layers"):
|
|
return model.layers
|
|
raise ValueError("Model does not have blocks/transformer_blocks/layers")
|
|
|
|
def _detect_cache_blocks(self) -> list[bool]:
|
|
block_is_state_cached = []
|
|
for block in self._model_blocks():
|
|
attn = getattr(block, "attn", None)
|
|
cls_name = type(attn).__name__
|
|
cache_type = getattr(attn, "fixed_rope_cache_type", None)
|
|
if cache_type == "state":
|
|
block_is_state_cached.append(True)
|
|
elif cache_type == "softmax":
|
|
block_is_state_cached.append(False)
|
|
else:
|
|
block_is_state_cached.append(cls_name in self._STATE_CACHE_CLASS_NAMES)
|
|
return block_is_state_cached
|
|
|
|
def create_autoregressive_segments(self, total_frames):
|
|
remained_frames = total_frames % self.base_chunk_frames
|
|
num_chunks = total_frames // self.base_chunk_frames
|
|
chunk_indices = [0]
|
|
for i in range(num_chunks):
|
|
cur_idx = chunk_indices[-1] + self.base_chunk_frames
|
|
if i == 0:
|
|
cur_idx += remained_frames
|
|
chunk_indices.append(cur_idx)
|
|
return chunk_indices
|
|
|
|
def _initialize_kv_cache(self, num_chunks: int):
|
|
return [[[None] * 6 for _ in range(self.num_model_blocks)] for _ in range(num_chunks)]
|
|
|
|
def _accumulate_fixed_rope_kv_cache(self, kv_cache, chunk_idx):
|
|
cur_kv_cache = kv_cache[chunk_idx]
|
|
start_chunk_idx = max(chunk_idx - self.num_cached_blocks, 0) if self.num_cached_blocks > 0 else 0
|
|
num_cached_frames = 0
|
|
sink_num = 0
|
|
|
|
for block_id, is_state_cached in enumerate(self.block_is_state_cached):
|
|
if is_state_cached:
|
|
prev = kv_cache[chunk_idx - 1][block_id]
|
|
cur_kv_cache[block_id][0] = prev[0]
|
|
cur_kv_cache[block_id][1] = prev[1]
|
|
cur_kv_cache[block_id][-1] = prev[-1]
|
|
continue
|
|
|
|
if self._fixed_rope_full_history_softmax_cache:
|
|
prev = kv_cache[chunk_idx - 1][block_id]
|
|
previous_q, previous_k, previous_v = prev[0], prev[1], prev[2]
|
|
previous_tconv = prev[-1]
|
|
cur_kv_cache[block_id] = [previous_q, previous_k, previous_v, None, None, previous_tconv]
|
|
if previous_q is not None:
|
|
hw = getattr(self, "_spatial_hw", 0)
|
|
if hw > 0:
|
|
num_cached_frames = previous_q.shape[-1] // hw
|
|
continue
|
|
|
|
previous_q, previous_k, previous_v = None, None, None
|
|
previous_tconv = None
|
|
valid_cached_chunks = list(range(start_chunk_idx, chunk_idx))
|
|
|
|
if self.num_cached_blocks > 0 and self.sink_token:
|
|
window_start_chunk = max(chunk_idx - self.num_cached_blocks + 1, 0)
|
|
if window_start_chunk > 0:
|
|
valid_cached_chunks = [0] + list(range(window_start_chunk, chunk_idx))
|
|
if sink_num == 0:
|
|
sink_num = self._chunk_indices[1] - self._chunk_indices[0]
|
|
|
|
for cache_idx in range(chunk_idx):
|
|
if cache_idx not in valid_cached_chunks:
|
|
kv_cache[cache_idx][block_id] = [None] * 6
|
|
continue
|
|
|
|
prev = kv_cache[cache_idx][block_id]
|
|
if prev[0] is not None:
|
|
if previous_q is None:
|
|
previous_q = prev[0].clone()
|
|
previous_k = prev[1].clone()
|
|
previous_v = prev[2].clone()
|
|
else:
|
|
previous_q = torch.cat([previous_q, prev[0]], dim=-1)
|
|
previous_k = torch.cat([previous_k, prev[1]], dim=-1)
|
|
previous_v = torch.cat([previous_v, prev[2]], dim=-1)
|
|
|
|
if prev[-1] is not None:
|
|
if previous_tconv is None:
|
|
previous_tconv = prev[-1].clone()
|
|
else:
|
|
previous_tconv = torch.cat([previous_tconv, prev[-1]], dim=2)
|
|
|
|
cur_kv_cache[block_id] = [previous_q, previous_k, previous_v, None, None, previous_tconv]
|
|
if previous_q is not None:
|
|
hw = getattr(self, "_spatial_hw", 0)
|
|
if hw > 0:
|
|
num_cached_frames = previous_q.shape[-1] // hw
|
|
|
|
return cur_kv_cache, chunk_idx - start_chunk_idx, sink_num, num_cached_frames
|
|
|
|
def accumulate_kv_cache(self, kv_cache, chunk_idx):
|
|
if chunk_idx == 0:
|
|
return kv_cache[0], 0, 0, 0
|
|
return self._accumulate_fixed_rope_kv_cache(kv_cache, chunk_idx)
|
|
|
|
def _promote_fixed_rope_full_history_cache(self, kv_cache, chunk_idx):
|
|
if not self._fixed_rope_full_history_softmax_cache or chunk_idx == 0:
|
|
return
|
|
|
|
for block_id, is_state_cached in enumerate(self.block_is_state_cached):
|
|
if is_state_cached:
|
|
continue
|
|
|
|
prev = kv_cache[chunk_idx - 1][block_id]
|
|
cur = kv_cache[chunk_idx][block_id]
|
|
|
|
if prev[0] is not None and cur[0] is not None:
|
|
cur[0] = torch.cat([prev[0], cur[0]], dim=-1)
|
|
cur[1] = torch.cat([prev[1], cur[1]], dim=-1)
|
|
cur[2] = torch.cat([prev[2], cur[2]], dim=-1)
|
|
elif prev[0] is not None:
|
|
cur[0], cur[1], cur[2] = prev[0], prev[1], prev[2]
|
|
|
|
if prev[-1] is not None and cur[-1] is not None:
|
|
cur[-1] = torch.cat([prev[-1], cur[-1]], dim=2)
|
|
elif prev[-1] is not None:
|
|
cur[-1] = prev[-1]
|
|
|
|
kv_cache[chunk_idx - 1][block_id] = [None] * len(prev)
|
|
|
|
@staticmethod
|
|
def _expand_per_chunk(tensor, batch_size, num_chunks, name, allow_no_batch=False):
|
|
if tensor is None:
|
|
return None
|
|
shape = tensor.shape
|
|
if tensor.dim() >= 2 and shape[0] == batch_size and shape[1] == num_chunks:
|
|
return tensor
|
|
if shape[0] == batch_size:
|
|
return tensor.unsqueeze(1).expand(batch_size, num_chunks, *shape[1:])
|
|
if shape[0] == num_chunks and batch_size == 1:
|
|
return tensor.unsqueeze(0)
|
|
if allow_no_batch:
|
|
return tensor.unsqueeze(0).unsqueeze(0).expand(batch_size, num_chunks, *shape)
|
|
raise AssertionError(
|
|
f"{name} shape {tuple(shape)} incompatible with batch_size={batch_size}, num_chunks={num_chunks}"
|
|
)
|
|
|
|
@staticmethod
|
|
def _timesteps_for_steps(scheduler, steps, device):
|
|
timesteps, _ = retrieve_timesteps(scheduler, steps, device, None)
|
|
if steps == 4:
|
|
timesteps = torch.tensor([1000, 961, 893, 743], device=device)
|
|
scheduler.timesteps = timesteps
|
|
scheduler.sigmas = torch.cat([timesteps / 1000, torch.zeros(1, device=device)])
|
|
elif steps == 2:
|
|
timesteps = torch.tensor([1000, 743], device=device)
|
|
scheduler.timesteps = timesteps
|
|
scheduler.sigmas = torch.cat([timesteps / 1000, torch.zeros(1, device=device)])
|
|
return timesteps
|
|
|
|
@torch.no_grad()
|
|
def sample(self, latents, steps=50, **kwargs):
|
|
device = self.condition.device
|
|
do_classifier_free_guidance = self.cfg_scale > 1
|
|
batch_size, _, total_frames, height, width = latents.shape
|
|
self._spatial_hw = height * width
|
|
|
|
if total_frames <= self.base_chunk_frames:
|
|
raise ValueError("Use the standard flow sampler for short videos")
|
|
|
|
chunk_indices = self.create_autoregressive_segments(total_frames)
|
|
self._chunk_indices = chunk_indices
|
|
num_chunks = len(chunk_indices) - 1
|
|
kv_cache = self._initialize_kv_cache(num_chunks)
|
|
|
|
cond_per_chunk = self._expand_per_chunk(self.condition, batch_size, num_chunks, "condition")
|
|
mask_per_chunk = self._expand_per_chunk(self.mask, batch_size, num_chunks, "mask", allow_no_batch=True)
|
|
uncond = self.uncondition
|
|
if uncond.shape[0] == 1 and batch_size > 1:
|
|
uncond = uncond.expand(batch_size, *uncond.shape[1:])
|
|
elif uncond.shape[0] not in (1, batch_size):
|
|
raise AssertionError(f"uncondition first dim must be 1 or batch_size={batch_size}, got {uncond.shape[0]}")
|
|
|
|
data_info = self.model_kwargs.pop("data_info", {})
|
|
image_vae_embeds = data_info.get("image_vae_embeds", None)
|
|
|
|
for chunk_idx in tqdm(
|
|
range(num_chunks),
|
|
disable=os.getenv("DPM_TQDM", "False") == "True",
|
|
desc="Processing chunks",
|
|
):
|
|
chunk_kv_cache, _, sink_num, num_cached_frames = self.accumulate_kv_cache(kv_cache, chunk_idx)
|
|
|
|
prompt_embeds = cond_per_chunk[:, chunk_idx]
|
|
if do_classifier_free_guidance:
|
|
prompt_embeds = torch.cat([uncond, prompt_embeds], dim=0)
|
|
|
|
mask = mask_per_chunk[:, chunk_idx] if mask_per_chunk is not None else None
|
|
scheduler = FlowMatchEulerDiscreteScheduler(shift=self.flow_shift)
|
|
timesteps = self._timesteps_for_steps(scheduler, steps, device)
|
|
|
|
start_f = chunk_indices[chunk_idx]
|
|
end_f = chunk_indices[chunk_idx + 1]
|
|
current_num_frames = end_f - start_f
|
|
cache_start_chunk_idx = max(chunk_idx - self.num_cached_blocks, 0) if self.num_cached_blocks > 0 else 0
|
|
|
|
frame_index = None
|
|
if sink_num > 0:
|
|
sink_fi = torch.arange(sink_num, device=device)
|
|
non_sink_count = num_cached_frames - sink_num + current_num_frames
|
|
window_start_f = end_f - non_sink_count
|
|
remaining_fi = torch.arange(window_start_f, end_f, device=device)
|
|
frame_index = torch.cat([sink_fi, remaining_fi], dim=0)
|
|
rope_start_f = 0
|
|
rope_end_f = end_f
|
|
else:
|
|
rope_start_f = chunk_indices[cache_start_chunk_idx]
|
|
rope_end_f = end_f
|
|
|
|
local_data_info = copy.deepcopy(data_info)
|
|
if image_vae_embeds is not None:
|
|
local_data_info["image_vae_embeds"] = image_vae_embeds[:, :, start_f:end_f]
|
|
|
|
is_last_chunk = chunk_idx == num_chunks - 1
|
|
for step_idx, t in enumerate(timesteps):
|
|
is_last_step = step_idx == len(timesteps) - 1
|
|
save_cache_now = self.efficient_cache and is_last_step and not is_last_chunk
|
|
latent_model_input = (
|
|
torch.cat([latents[:, :, start_f:end_f]] * 2)
|
|
if do_classifier_free_guidance
|
|
else latents[:, :, start_f:end_f]
|
|
)
|
|
timestep = t.expand(latent_model_input.shape[0])
|
|
|
|
noise_pred, step_kv_cache = self.model(
|
|
latent_model_input,
|
|
timestep,
|
|
prompt_embeds,
|
|
start_f=rope_start_f,
|
|
end_f=rope_end_f,
|
|
frame_index=frame_index,
|
|
save_kv_cache=save_cache_now,
|
|
kv_cache=chunk_kv_cache,
|
|
mask=mask,
|
|
data_info=local_data_info,
|
|
**self.model_kwargs,
|
|
)
|
|
|
|
if save_cache_now:
|
|
kv_cache[chunk_idx] = step_kv_cache
|
|
self._promote_fixed_rope_full_history_cache(kv_cache, chunk_idx)
|
|
|
|
if isinstance(noise_pred, Transformer2DModelOutput):
|
|
noise_pred = noise_pred[0]
|
|
|
|
if do_classifier_free_guidance:
|
|
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
|
|
noise_pred = noise_pred_uncond + self.cfg_scale * (noise_pred_text - noise_pred_uncond)
|
|
|
|
latents_dtype = latents.dtype
|
|
latents[:, :, start_f:end_f] = scheduler.step(
|
|
noise_pred, t, latents[:, :, start_f:end_f], return_dict=False
|
|
)[0]
|
|
if latents.dtype != latents_dtype:
|
|
latents = latents.to(latents_dtype)
|
|
|
|
if not self.efficient_cache and not is_last_chunk:
|
|
latent_model_input = (
|
|
torch.cat([latents[:, :, start_f:end_f]] * 2)
|
|
if do_classifier_free_guidance
|
|
else latents[:, :, start_f:end_f]
|
|
)
|
|
timestep = torch.zeros(latent_model_input.shape[0], device=device)
|
|
_, updated_kv_cache = self.model(
|
|
latent_model_input,
|
|
timestep,
|
|
prompt_embeds,
|
|
start_f=rope_start_f,
|
|
end_f=rope_end_f,
|
|
frame_index=frame_index,
|
|
save_kv_cache=True,
|
|
kv_cache=chunk_kv_cache,
|
|
mask=mask,
|
|
data_info=local_data_info,
|
|
**self.model_kwargs,
|
|
)
|
|
kv_cache[chunk_idx] = updated_kv_cache
|
|
self._promote_fixed_rope_full_history_cache(kv_cache, chunk_idx)
|
|
|
|
return latents
|