2631 lines
111 KiB
Python
2631 lines
111 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
|
||
|
||
"""Diffusers-backed LTX-2 refiner used by Sana-WM inference.
|
||
|
||
The Sana-WM refiner checkpoint is a standard LTX-2 transformer plus text
|
||
connectors. Diffusers already owns those modules, but its public transformer
|
||
forward always runs the audio stream and does not expose the streaming
|
||
sink/current video self-attention mask that this refiner was trained with.
|
||
|
||
This wrapper keeps the custom surface narrow: load diffusers components, encode
|
||
the prompt through Gemma + ``LTX2TextConnectors``, and run a video-only forward
|
||
through the diffusers transformer blocks. The only local attention code is the
|
||
streaming sink/current split, implemented with diffusers attention modules
|
||
without materializing the full sequence-by-sequence mask.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import gc
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import re
|
||
import time
|
||
import types
|
||
from contextlib import nullcontext
|
||
from pathlib import Path
|
||
|
||
import torch
|
||
import torch.nn.functional as F
|
||
from torch import nn
|
||
|
||
STAGE_2_DISTILLED_SIGMA_VALUES: tuple[float, ...] = (0.909375, 0.725, 0.421875, 0.0)
|
||
|
||
|
||
class DiffusersLTX2Refiner(nn.Module):
|
||
"""Small Sana-WM adapter around diffusers LTX-2 modules."""
|
||
|
||
def __init__(
|
||
self,
|
||
refiner_root: str | Path,
|
||
gemma_root: str | Path,
|
||
*,
|
||
dtype: torch.dtype,
|
||
device: torch.device | str,
|
||
text_max_sequence_length: int = 1024,
|
||
) -> None:
|
||
super().__init__()
|
||
self.refiner_root = Path(refiner_root)
|
||
self.gemma_root = Path(gemma_root)
|
||
self.dtype = dtype
|
||
self.device = torch.device(device)
|
||
self.text_max_sequence_length = int(text_max_sequence_length)
|
||
self._te_nvfp4_requested = _env_flag("SANA_WM_REFINER_NVFP4")
|
||
self._te_nvfp4_recipe = None
|
||
self._te_nvfp4_converted = False
|
||
self._self_qkv_fused = False
|
||
self._attention_backend = os.environ.get("SANA_WM_REFINER_ATTN_BACKEND", "").strip()
|
||
self._uniform_timestep_cache: dict[tuple[int, int, float, str], tuple[torch.Tensor, torch.Tensor]] = {}
|
||
|
||
self.transformer, self.connectors = self._load_diffusers_components()
|
||
|
||
def _load_diffusers_components(self) -> tuple[nn.Module, nn.Module]:
|
||
from diffusers.models.transformers.transformer_ltx2 import LTX2VideoTransformer3DModel
|
||
from diffusers.pipelines.ltx2 import LTX2TextConnectors
|
||
|
||
cache_path = self._prepared_transformer_cache_path()
|
||
if cache_path is not None and cache_path.is_file():
|
||
t0 = time.perf_counter()
|
||
print(f"[refiner-cache] loading prepared transformer from {cache_path}", flush=True)
|
||
try:
|
||
transformer = torch.load(cache_path, map_location=self.device, weights_only=False).eval()
|
||
self._te_nvfp4_converted = bool(self._te_nvfp4_requested)
|
||
self._self_qkv_fused = _env_flag("SANA_WM_REFINER_FUSE_SELF_QKV")
|
||
self._te_nvfp4_recipe = self._make_nvfp4_recipe() if self._te_nvfp4_converted else None
|
||
print(f"[refiner-cache] loaded prepared transformer in {time.perf_counter() - t0:.1f}s", flush=True)
|
||
except Exception as exc:
|
||
print(f"[refiner-cache] failed to load {cache_path}: {exc}; rebuilding", flush=True)
|
||
transformer = LTX2VideoTransformer3DModel.from_pretrained(
|
||
self.refiner_root,
|
||
subfolder="transformer",
|
||
torch_dtype=self.dtype,
|
||
).eval()
|
||
else:
|
||
transformer = LTX2VideoTransformer3DModel.from_pretrained(
|
||
self.refiner_root,
|
||
subfolder="transformer",
|
||
torch_dtype=self.dtype,
|
||
).eval()
|
||
if not self._te_nvfp4_requested and os.environ.get("SANA_WM_REFINER_FP8_STORAGE", "").lower() in {
|
||
"1",
|
||
"true",
|
||
"yes",
|
||
"on",
|
||
}:
|
||
skip_patterns = None
|
||
extra_skip_patterns = _env_tuple("SANA_WM_REFINER_FP8_SKIP_PATTERNS")
|
||
if extra_skip_patterns:
|
||
from diffusers.hooks.layerwise_casting import DEFAULT_SKIP_MODULES_PATTERN
|
||
|
||
skip_patterns = tuple(dict.fromkeys((*DEFAULT_SKIP_MODULES_PATTERN, *extra_skip_patterns)))
|
||
transformer.enable_layerwise_casting(
|
||
storage_dtype=torch.float8_e4m3fn,
|
||
compute_dtype=self.dtype,
|
||
skip_modules_pattern=skip_patterns,
|
||
)
|
||
connectors = LTX2TextConnectors.from_pretrained(
|
||
self.refiner_root,
|
||
subfolder="connectors",
|
||
torch_dtype=self.dtype,
|
||
).eval()
|
||
return transformer, connectors
|
||
|
||
def _make_nvfp4_recipe(self):
|
||
"""Build the refiner quant recipe. Default NVFP4 (W4A4, Blackwell);
|
||
``SANA_WM_REFINER_QUANT=fp8block`` selects FP8 block scaling (W8A8), which
|
||
also runs on Hopper."""
|
||
import transformer_engine.common.recipe as te_recipe
|
||
|
||
quant = os.environ.get("SANA_WM_REFINER_QUANT", "nvfp4").strip().lower()
|
||
if quant in {"fp8block", "fp8", "float8block"}:
|
||
return te_recipe.Float8BlockScaling()
|
||
return te_recipe.NVFP4BlockScaling(
|
||
disable_rht=True,
|
||
disable_stochastic_rounding=True,
|
||
)
|
||
|
||
def _prepared_transformer_cache_path(self) -> Path | None:
|
||
root = _prepared_module_cache_root()
|
||
if root is None or not self._te_nvfp4_requested:
|
||
return None
|
||
payload = {
|
||
"kind": "refiner_transformer_prepared_v2",
|
||
"refiner_root": _path_fingerprint(self.refiner_root / "transformer"),
|
||
"dtype": str(self.dtype),
|
||
"torch": torch.__version__,
|
||
"refiner_nvfp4": os.environ.get("SANA_WM_REFINER_NVFP4", ""),
|
||
"refiner_quant": os.environ.get("SANA_WM_REFINER_QUANT", ""),
|
||
"refiner_nvfp4_skip_patterns": os.environ.get("SANA_WM_REFINER_NVFP4_SKIP_PATTERNS", ""),
|
||
"refiner_fuse_self_qkv": os.environ.get("SANA_WM_REFINER_FUSE_SELF_QKV", ""),
|
||
"te_cpu_staging": os.environ.get("SANA_WM_TE_NVFP4_CPU_STAGING", ""),
|
||
}
|
||
try:
|
||
import transformer_engine
|
||
|
||
payload["transformer_engine"] = getattr(transformer_engine, "__version__", "unknown")
|
||
except Exception:
|
||
payload["transformer_engine"] = "unavailable"
|
||
return root / "refiner" / f"{_prepared_module_cache_hash(payload)}.pt"
|
||
|
||
def _save_prepared_transformer_cache(self) -> None:
|
||
if os.environ.get("SANA_WM_PREPARED_MODULE_CACHE_SAVE", "1").strip().lower() in {
|
||
"",
|
||
"0",
|
||
"false",
|
||
"no",
|
||
"off",
|
||
}:
|
||
return
|
||
cache_path = self._prepared_transformer_cache_path()
|
||
if cache_path is None or cache_path.is_file():
|
||
return
|
||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||
tmp_path = cache_path.with_suffix(cache_path.suffix + f".tmp.{os.getpid()}")
|
||
t0 = time.perf_counter()
|
||
print(f"[refiner-cache] saving prepared transformer to {cache_path}", flush=True)
|
||
restore = _strip_local_callables_for_pickle(self.transformer)
|
||
if restore:
|
||
print(f"[refiner-cache] stripped {len(restore)} init-only callables before save", flush=True)
|
||
try:
|
||
torch.save(self.transformer, tmp_path)
|
||
os.replace(tmp_path, cache_path)
|
||
except Exception as exc:
|
||
print(f"[refiner-cache] failed to save {cache_path}: {exc}", flush=True)
|
||
finally:
|
||
_restore_stripped_pickle_values(restore)
|
||
try:
|
||
if tmp_path.exists():
|
||
tmp_path.unlink()
|
||
except OSError:
|
||
pass
|
||
if cache_path.is_file():
|
||
print(f"[refiner-cache] saved prepared transformer in {time.perf_counter() - t0:.1f}s", flush=True)
|
||
|
||
def prepare_transformer_nvfp4(self) -> None:
|
||
"""Lazily replace eligible refiner Linear layers with TE NVFP4 Linear modules."""
|
||
self._prepare_self_qkv_fusion()
|
||
if not self._te_nvfp4_requested or self._te_nvfp4_converted:
|
||
return
|
||
|
||
recipe = self._make_nvfp4_recipe()
|
||
converted, skipped = _replace_linear_with_te_nvfp4(
|
||
self.transformer,
|
||
recipe=recipe,
|
||
params_dtype=self.dtype,
|
||
skip_patterns=tuple(
|
||
dict.fromkeys(
|
||
(
|
||
"^proj_in$",
|
||
"^proj_out$",
|
||
"(^|\\.)audio_",
|
||
"audio_to_video",
|
||
"video_to_audio",
|
||
"av_cross_attn",
|
||
"caption_projection",
|
||
"time_embed",
|
||
*_env_tuple("SANA_WM_REFINER_NVFP4_SKIP_PATTERNS"),
|
||
)
|
||
)
|
||
),
|
||
)
|
||
if converted <= 0:
|
||
raise RuntimeError(f"SANA_WM_REFINER_NVFP4=1 converted no Linear layers; skipped={skipped}.")
|
||
self._te_nvfp4_recipe = recipe
|
||
self._te_nvfp4_converted = True
|
||
_empty_cuda_cache()
|
||
self._save_prepared_transformer_cache()
|
||
|
||
def _prepare_self_qkv_fusion(self) -> None:
|
||
if self._self_qkv_fused or not _env_flag("SANA_WM_REFINER_FUSE_SELF_QKV"):
|
||
return
|
||
converted = _fuse_refiner_self_qkv(self.transformer)
|
||
if converted <= 0:
|
||
raise RuntimeError("SANA_WM_REFINER_FUSE_SELF_QKV=1 fused no self-attention QKV modules.")
|
||
self._self_qkv_fused = True
|
||
print(f"[refiner-fuse-qkv] fused {converted} self-attention QKV groups", flush=True)
|
||
|
||
def offload_video_unused_audio_modules(self, device: torch.device | str = "cpu") -> None:
|
||
"""Keep LTX-2 audio-only branches off GPU for this wrapper's video-only forward."""
|
||
_offload_video_unused_audio_modules(self.transformer, device)
|
||
_empty_cuda_cache()
|
||
|
||
def move_video_modules(self, device: torch.device | str) -> None:
|
||
"""Move only the modules and direct parameters used by the video-only forward."""
|
||
_move_ltx2_video_modules_to(self.transformer, device)
|
||
_empty_cuda_cache()
|
||
|
||
def _nvfp4_autocast(self):
|
||
if not self._te_nvfp4_converted:
|
||
return nullcontext()
|
||
import transformer_engine.pytorch as te
|
||
|
||
return te.fp8_autocast(enabled=True, fp8_recipe=self._te_nvfp4_recipe)
|
||
|
||
def _attention_backend_context(self):
|
||
if not self._attention_backend:
|
||
return nullcontext()
|
||
from diffusers.models.attention_dispatch import attention_backend
|
||
|
||
return attention_backend(self._attention_backend)
|
||
|
||
def _uniform_timestep_tensors(
|
||
self,
|
||
*,
|
||
batch_size: int,
|
||
seq_len: int,
|
||
sigma: float,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
sigma_value = float(sigma)
|
||
if not _env_flag("SANA_WM_REFINER_TIMESTEP_CACHE"):
|
||
raw_sigma = torch.full(
|
||
(int(batch_size), int(seq_len), 1), sigma_value, dtype=torch.float32, device=self.device
|
||
)
|
||
model_timestep = raw_sigma.squeeze(-1) * float(self.transformer.config.timestep_scale_multiplier)
|
||
return model_timestep, raw_sigma
|
||
key = (int(batch_size), int(seq_len), sigma_value, str(self.device))
|
||
cached = self._uniform_timestep_cache.get(key)
|
||
if cached is not None:
|
||
return cached
|
||
raw_sigma = torch.full((int(batch_size), int(seq_len), 1), sigma_value, dtype=torch.float32, device=self.device)
|
||
model_timestep = raw_sigma.squeeze(-1) * float(self.transformer.config.timestep_scale_multiplier)
|
||
cached = (model_timestep, raw_sigma)
|
||
self._uniform_timestep_cache[key] = cached
|
||
return cached
|
||
|
||
@torch.inference_mode()
|
||
def refine_latents(
|
||
self,
|
||
sana_latent: torch.Tensor,
|
||
prompt: str,
|
||
*,
|
||
fps: float,
|
||
sink_size: int = 1,
|
||
seed: int = 42,
|
||
progress: bool = True,
|
||
block_size: int | None = None,
|
||
kv_max_frames: int = 11,
|
||
sigmas: tuple[float, ...] = STAGE_2_DISTILLED_SIGMA_VALUES,
|
||
) -> torch.Tensor:
|
||
"""Run the LTX-2 refiner and return refined VAE latents.
|
||
|
||
When ``block_size`` is ``None`` (default), uses the legacy single-shot
|
||
path that denoises all current frames jointly. When ``block_size`` is
|
||
set (canonical: 3), runs the chunk-causal AR recipe with sliding-window
|
||
attention over ``[source_sink + recent_history + active_block]``,
|
||
matching tian's ``run_reforcing_inference`` contract — the model was
|
||
trained to refine ``block_size`` frames at a time with clean prior
|
||
context, and feeding the full sequence at once is out-of-distribution.
|
||
|
||
Args:
|
||
sana_latent: ``(B, C, F, H, W)`` stage-1 latent.
|
||
prompt: text prompt.
|
||
fps: video frame rate (drives LTX-2 RoPE temporal scaling).
|
||
sink_size: how many leading raw ``z_sana`` frames to anchor as the
|
||
attention sink (canonical: 1).
|
||
seed: noise seed for the FM endpoint.
|
||
progress: show a tqdm bar.
|
||
block_size: latent frames per AR block (canonical: 3). ``None``
|
||
disables AR mode.
|
||
kv_max_frames: maximum context+active frames retained in the
|
||
sliding window when AR mode is active (canonical: 11 =
|
||
1 sink + 10 recent).
|
||
sigmas: descending Euler schedule terminating at 0.0 (canonical
|
||
3-step distilled: ``(0.909375, 0.725, 0.421875, 0.0)``).
|
||
"""
|
||
if sana_latent.shape[2] <= sink_size:
|
||
raise ValueError(f"Stage-1 latent has {sana_latent.shape[2]} frames but sink_size={sink_size}.")
|
||
|
||
self.transformer.to("cpu")
|
||
_empty_cuda_cache()
|
||
prompt_embeds, prompt_attention_mask = self._encode_prompt(prompt)
|
||
|
||
self.move_video_modules(self.device)
|
||
self.offload_video_unused_audio_modules("cpu")
|
||
self.prepare_transformer_nvfp4()
|
||
z = sana_latent.to(device=self.device, dtype=self.dtype)
|
||
sigmas_t = torch.tensor(sigmas, dtype=torch.float32, device=self.device)
|
||
start_sigma = float(sigmas_t[0])
|
||
|
||
if block_size is not None:
|
||
return self._refine_latents_ar(
|
||
z=z,
|
||
prompt_embeds=prompt_embeds,
|
||
prompt_attention_mask=prompt_attention_mask,
|
||
fps=fps,
|
||
sigmas=sigmas_t,
|
||
source_sink_frames=int(sink_size),
|
||
block_size=int(block_size),
|
||
kv_max_frames=int(kv_max_frames),
|
||
seed=int(seed),
|
||
progress=bool(progress),
|
||
)
|
||
|
||
sink = z[:, :, :sink_size].contiguous()
|
||
current = z[:, :, sink_size:].contiguous()
|
||
generator = torch.Generator(device=self.device).manual_seed(int(seed))
|
||
eps = torch.randn(current.shape, generator=generator, device=self.device, dtype=self.dtype)
|
||
noisy = (1.0 - start_sigma) * current + start_sigma * eps
|
||
|
||
iterator = range(len(sigmas_t) - 1)
|
||
if progress:
|
||
from tqdm.auto import tqdm
|
||
|
||
iterator = tqdm(iterator, desc="refiner", unit="step")
|
||
|
||
for step_index in iterator:
|
||
sigma = sigmas_t[step_index]
|
||
denoised = self._predict_current_x0(
|
||
sink=sink,
|
||
noisy_current=noisy,
|
||
prompt_embeds=prompt_embeds,
|
||
prompt_attention_mask=prompt_attention_mask,
|
||
sigma=sigma,
|
||
fps=fps,
|
||
)
|
||
noisy_tokens = _pack_latents(
|
||
noisy,
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
)
|
||
velocity = (noisy_tokens.float() - denoised.float()) / sigma.float()
|
||
next_tokens = noisy_tokens.float() + velocity * (sigmas_t[step_index + 1] - sigma).float()
|
||
noisy = _unpack_latents(
|
||
next_tokens.to(self.dtype),
|
||
num_frames=noisy.shape[2],
|
||
height=noisy.shape[3],
|
||
width=noisy.shape[4],
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
)
|
||
|
||
return torch.cat([sink, noisy], dim=2)
|
||
|
||
@torch.inference_mode()
|
||
def _refine_latents_ar(
|
||
self,
|
||
*,
|
||
z: torch.Tensor,
|
||
prompt_embeds: torch.Tensor,
|
||
prompt_attention_mask: torch.Tensor,
|
||
fps: float,
|
||
sigmas: torch.Tensor,
|
||
source_sink_frames: int,
|
||
block_size: int,
|
||
kv_max_frames: int,
|
||
seed: int,
|
||
progress: bool,
|
||
) -> torch.Tensor:
|
||
"""Chunk-causal AR refinement — thin wrapper around ``RefinerChunkRunner``.
|
||
|
||
Implements the canonical ``rf_shifted_sink`` KV-cache contract end-to-end:
|
||
|
||
1. Pre-capture **pre-RoPE** sink K/V from raw ``z_sana[:source_sink_frames]``
|
||
at σ=0 (``_kv_cache_capture`` hook). The sink frames themselves are
|
||
**never refined** — they sit unchanged in the output volume.
|
||
2. AR blocks cover frames ``[source_sink_frames, T_full)`` in
|
||
``block_size``-frame chunks. For each block:
|
||
- Initialize ``x_t = (1-σ₀)·z_sana_block + σ₀·ε`` (single eps per block).
|
||
- 3-step deterministic Euler. Each step injects the per-layer prefix
|
||
``{sink_k_pre, sink_v, sink_pe, history_k, history_v}`` where
|
||
``sink_pe`` is rebuilt at ``sink_rope_offset = active_start -
|
||
history_frames - source_sink_frames`` so the sink slides to sit
|
||
immediately before the bounded working cache (official RF layout).
|
||
- Capture **post-RoPE** K/V from the refined block under the same
|
||
prefix (``_tf_capture_kv`` hook); append to ``history_kv_post`` and
|
||
trim to ``kv_max_frames - source_sink_frames``.
|
||
|
||
For the chunk-pipelined interactive path, build a ``RefinerChunkRunner``
|
||
directly and feed one block at a time as stage-1 yields it.
|
||
|
||
The returned tensor has the same shape ``(B, C, T_full, H, W)`` as
|
||
``z``; the first ``source_sink_frames`` slots carry the raw sink
|
||
latents unchanged, the rest carry the refined output.
|
||
"""
|
||
runner = RefinerChunkRunner(
|
||
self,
|
||
prompt_embeds=prompt_embeds,
|
||
prompt_attention_mask=prompt_attention_mask,
|
||
fps=fps,
|
||
sigmas=sigmas,
|
||
source_sink_frames=int(source_sink_frames),
|
||
block_size=int(block_size),
|
||
kv_max_frames=int(kv_max_frames),
|
||
seed=int(seed),
|
||
spatial_shape=(int(z.shape[3]), int(z.shape[4])),
|
||
)
|
||
|
||
T_full = z.shape[2]
|
||
sink_size = int(source_sink_frames)
|
||
# Output keeps the raw sink prefix verbatim; AR blocks fill frames
|
||
# [sink_size, T_full).
|
||
output = z.clone()
|
||
n_active = max(T_full - sink_size, 0)
|
||
n_blocks = (n_active + block_size - 1) // block_size if n_active > 0 else 0
|
||
iterator = range(n_blocks)
|
||
if progress:
|
||
from tqdm.auto import tqdm
|
||
|
||
iterator = tqdm(iterator, desc="refiner-ar", unit="block")
|
||
|
||
for block_idx in iterator:
|
||
block_start = sink_size + block_idx * block_size
|
||
block_end = min(block_start + block_size, T_full)
|
||
clean_block = z[:, :, block_start:block_end]
|
||
refined = runner.refine_block(
|
||
block_idx=block_idx,
|
||
clean_block=clean_block,
|
||
block_start=block_start,
|
||
block_end=block_end,
|
||
sink_seed_frames=(z[:, :, :sink_size] if block_idx == 0 else None),
|
||
)
|
||
output[:, :, block_start:block_end] = refined
|
||
|
||
return output
|
||
|
||
def _predict_x0_active_block(
|
||
self,
|
||
*,
|
||
active: torch.Tensor, # (B, C, N_active, H, W) at σ_cur
|
||
active_positions: list[int],
|
||
sigma_cur: float,
|
||
prompt_embeds: torch.Tensor,
|
||
prompt_attention_mask: torch.Tensor,
|
||
fps: float,
|
||
kv_prefix_per_layer: list[dict[str, object]] | None,
|
||
active_video_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||
capture_post_kv: bool = False,
|
||
capture_layer_mask: list[bool] | None = None,
|
||
) -> torch.Tensor | tuple[torch.Tensor, list[tuple[torch.Tensor, torch.Tensor] | None]]:
|
||
"""Forward through the transformer on the ACTIVE BLOCK ONLY and return x0.
|
||
|
||
The active block's Q attends to ``[prefix, current]`` K/V via the
|
||
``_tf_kv_prefix`` hook on every self-attention block. All active tokens
|
||
carry the same ``sigma_cur`` (matching tian's per-block uniform σ).
|
||
"""
|
||
latent_tokens = _pack_latents(
|
||
active,
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
)
|
||
batch_size, seq_len, _ = latent_tokens.shape
|
||
# Use a per-token uniform sigma for the active block.
|
||
model_timestep, raw_sigma = self._uniform_timestep_tensors(
|
||
batch_size=int(batch_size),
|
||
seq_len=int(seq_len),
|
||
sigma=float(sigma_cur),
|
||
)
|
||
|
||
video_rotary_emb = active_video_rotary_emb
|
||
if video_rotary_emb is None:
|
||
video_rotary_emb = _build_rotary_emb_for_absolute_positions(
|
||
transformer=self.transformer,
|
||
batch_size=batch_size,
|
||
frame_positions=active_positions,
|
||
height=int(active.shape[3]),
|
||
width=int(active.shape[4]),
|
||
device=self.device,
|
||
fps=float(fps),
|
||
)
|
||
# Replace the per-frame uniform-σ adaLN time embedding with the active
|
||
# block's mean sigma (= sigma_cur here), mirroring tian's prompt_sigma
|
||
# `mean_active` mode.
|
||
_set_kv_prefix_on_blocks(self.transformer, kv_prefix_per_layer)
|
||
if capture_post_kv:
|
||
_set_capture_flag_on_blocks(self.transformer, "post_rope", enable=True, layer_mask=capture_layer_mask)
|
||
try:
|
||
velocity = self._forward_video_only_with_rope(
|
||
hidden_states=latent_tokens,
|
||
encoder_hidden_states=prompt_embeds,
|
||
timestep=model_timestep,
|
||
encoder_attention_mask=prompt_attention_mask,
|
||
video_rotary_emb=video_rotary_emb,
|
||
n_context_tokens=0,
|
||
)
|
||
finally:
|
||
if capture_post_kv:
|
||
_set_capture_flag_on_blocks(self.transformer, "post_rope", enable=False)
|
||
_clear_kv_prefix_on_blocks(self.transformer)
|
||
captured_kv = (
|
||
_collect_captured_kv_from_blocks(self.transformer, "post_rope", layer_mask=capture_layer_mask)
|
||
if capture_post_kv
|
||
else None
|
||
)
|
||
|
||
# FM x0 prediction: x_t - σ_cur · v.
|
||
denoised_tokens = latent_tokens.float() - velocity.float() * raw_sigma
|
||
denoised = _unpack_latents(
|
||
denoised_tokens.to(self.dtype),
|
||
num_frames=int(active.shape[2]),
|
||
height=int(active.shape[3]),
|
||
width=int(active.shape[4]),
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
)
|
||
if captured_kv is not None:
|
||
return denoised, captured_kv
|
||
return denoised
|
||
|
||
@torch.inference_mode()
|
||
def _capture_block_kv(
|
||
self,
|
||
*,
|
||
clean_block: torch.Tensor, # (B, C, N, H, W) treated as σ=0 (clean) input
|
||
frame_positions: list[int],
|
||
prompt_embeds: torch.Tensor,
|
||
prompt_attention_mask: torch.Tensor,
|
||
fps: float,
|
||
capture_mode: str, # "pre_rope" or "post_rope"
|
||
kv_prefix_per_layer: list[dict[str, object]] | None,
|
||
capture_layer_mask: list[bool] | None = None,
|
||
video_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||
) -> list[tuple[torch.Tensor, torch.Tensor] | None]:
|
||
"""Run one forward at σ=0 with capture hooks; return per-layer (K, V).
|
||
|
||
``capture_mode='pre_rope'`` uses the ``_kv_cache_capture`` hook (stored
|
||
before RoPE so a future window can re-RoPE the sink to its shifted
|
||
offset). ``capture_mode='post_rope'`` uses ``_tf_capture_kv`` (stored
|
||
with RoPE already baked at the block's absolute positions, ready to
|
||
concatenate into the next window's prefix).
|
||
"""
|
||
latent_tokens = _pack_latents(
|
||
clean_block,
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
)
|
||
batch_size, seq_len, _ = latent_tokens.shape
|
||
model_timestep = torch.zeros(batch_size, seq_len, dtype=torch.float32, device=self.device)
|
||
|
||
if video_rotary_emb is None:
|
||
video_rotary_emb = _build_rotary_emb_for_absolute_positions(
|
||
transformer=self.transformer,
|
||
batch_size=batch_size,
|
||
frame_positions=frame_positions,
|
||
height=int(clean_block.shape[3]),
|
||
width=int(clean_block.shape[4]),
|
||
device=self.device,
|
||
fps=float(fps),
|
||
)
|
||
|
||
stop_after_layer = None
|
||
stop_after_capture_kv_layer = None
|
||
if capture_layer_mask is not None and not all(capture_layer_mask):
|
||
stop_after_layer = max(idx for idx, keep in enumerate(capture_layer_mask) if keep)
|
||
if _env_flag("SANA_WM_REFINER_CAPTURE_KV_ONLY_LAST"):
|
||
if capture_layer_mask is None:
|
||
stop_after_capture_kv_layer = len(self.transformer.transformer_blocks) - 1
|
||
else:
|
||
stop_after_capture_kv_layer = max(idx for idx, keep in enumerate(capture_layer_mask) if keep)
|
||
stop_after_layer = None
|
||
|
||
_set_kv_prefix_on_blocks(self.transformer, kv_prefix_per_layer)
|
||
_set_capture_flag_on_blocks(self.transformer, capture_mode, enable=True, layer_mask=capture_layer_mask)
|
||
try:
|
||
_ = self._forward_video_only_with_rope(
|
||
hidden_states=latent_tokens,
|
||
encoder_hidden_states=prompt_embeds,
|
||
timestep=model_timestep,
|
||
encoder_attention_mask=prompt_attention_mask,
|
||
video_rotary_emb=video_rotary_emb,
|
||
n_context_tokens=0,
|
||
skip_output_projection=True,
|
||
stop_after_layer=stop_after_layer,
|
||
stop_after_capture_kv_layer=stop_after_capture_kv_layer,
|
||
)
|
||
finally:
|
||
_set_capture_flag_on_blocks(self.transformer, capture_mode, enable=False)
|
||
_clear_kv_prefix_on_blocks(self.transformer)
|
||
|
||
return _collect_captured_kv_from_blocks(self.transformer, capture_mode, layer_mask=capture_layer_mask)
|
||
|
||
@torch.inference_mode()
|
||
def _encode_prompt(self, prompt: str) -> tuple[torch.Tensor, torch.Tensor]:
|
||
from transformers import AutoTokenizer, Gemma3ForConditionalGeneration
|
||
|
||
tokenizer = AutoTokenizer.from_pretrained(self.gemma_root)
|
||
tokenizer.padding_side = "left"
|
||
if tokenizer.pad_token is None:
|
||
tokenizer.pad_token = tokenizer.eos_token
|
||
|
||
text_inputs = tokenizer(
|
||
[prompt.strip()],
|
||
padding="max_length",
|
||
max_length=self.text_max_sequence_length,
|
||
truncation=True,
|
||
add_special_tokens=True,
|
||
return_tensors="pt",
|
||
)
|
||
input_ids = text_inputs.input_ids.to(self.device)
|
||
attention_mask = text_inputs.attention_mask.to(self.device)
|
||
|
||
text_encoder = Gemma3ForConditionalGeneration.from_pretrained(
|
||
self.gemma_root,
|
||
torch_dtype=self.dtype,
|
||
low_cpu_mem_usage=True,
|
||
).eval()
|
||
text_encoder.to(self.device)
|
||
text_backbone = getattr(text_encoder, "model", text_encoder)
|
||
outputs = text_backbone(input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True)
|
||
hidden_states = torch.stack(outputs.hidden_states, dim=-1)
|
||
sequence_lengths = attention_mask.sum(dim=-1)
|
||
del text_encoder, text_backbone, outputs
|
||
_empty_cuda_cache()
|
||
|
||
prompt_embeds = _pack_text_embeds(
|
||
hidden_states,
|
||
sequence_lengths,
|
||
device=self.device,
|
||
padding_side=tokenizer.padding_side,
|
||
).to(dtype=self.dtype)
|
||
|
||
del hidden_states
|
||
_empty_cuda_cache()
|
||
|
||
self.connectors.to(self.device)
|
||
connector_prompt_embeds, _, connector_attention_mask = self.connectors(prompt_embeds, attention_mask)
|
||
self.connectors.to("cpu")
|
||
del prompt_embeds, attention_mask
|
||
_empty_cuda_cache()
|
||
|
||
return connector_prompt_embeds.to(device=self.device, dtype=self.dtype), connector_attention_mask.to(
|
||
device=self.device
|
||
)
|
||
|
||
def _predict_current_x0(
|
||
self,
|
||
*,
|
||
sink: torch.Tensor,
|
||
noisy_current: torch.Tensor,
|
||
prompt_embeds: torch.Tensor,
|
||
prompt_attention_mask: torch.Tensor,
|
||
sigma: torch.Tensor,
|
||
fps: float,
|
||
) -> torch.Tensor:
|
||
full_latent = torch.cat([sink, noisy_current], dim=2)
|
||
batch_size, _, num_frames, height, width = full_latent.shape
|
||
latent_tokens = _pack_latents(
|
||
full_latent,
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
)
|
||
n_context_tokens = _pack_latents(
|
||
sink,
|
||
patch_size=self.transformer.config.patch_size,
|
||
patch_size_t=self.transformer.config.patch_size_t,
|
||
).shape[1]
|
||
|
||
raw_timestep = torch.zeros(batch_size, latent_tokens.shape[1], 1, dtype=torch.float32, device=self.device)
|
||
raw_timestep[:, n_context_tokens:, 0] = sigma.float()
|
||
model_timestep = raw_timestep.squeeze(-1) * float(self.transformer.config.timestep_scale_multiplier)
|
||
|
||
velocity = self._forward_video_only(
|
||
hidden_states=latent_tokens,
|
||
encoder_hidden_states=prompt_embeds,
|
||
timestep=model_timestep,
|
||
encoder_attention_mask=prompt_attention_mask,
|
||
num_frames=num_frames,
|
||
height=height,
|
||
width=width,
|
||
fps=fps,
|
||
n_context_tokens=n_context_tokens,
|
||
)
|
||
denoised = latent_tokens.float() - velocity.float() * raw_timestep
|
||
return denoised[:, n_context_tokens:, :].to(self.dtype)
|
||
|
||
def _forward_video_only_with_rope(
|
||
self,
|
||
*,
|
||
hidden_states: torch.Tensor,
|
||
encoder_hidden_states: torch.Tensor,
|
||
timestep: torch.Tensor,
|
||
encoder_attention_mask: torch.Tensor | None,
|
||
video_rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||
n_context_tokens: int,
|
||
skip_output_projection: bool = False,
|
||
stop_after_layer: int | None = None,
|
||
stop_after_capture_kv_layer: int | None = None,
|
||
) -> torch.Tensor:
|
||
"""Shared body of ``_forward_video_only`` that takes a pre-built RoPE.
|
||
|
||
Used by the AR refinement path where each block forward needs custom
|
||
per-frame absolute positions in the source video.
|
||
"""
|
||
transformer = self.transformer
|
||
batch_size = hidden_states.size(0)
|
||
seq_len = int(hidden_states.shape[1])
|
||
profiler = None
|
||
if _refiner_layer_profile_enabled():
|
||
forward_kind = "capture" if skip_output_projection else "predict"
|
||
prefix_tokens = _current_refiner_prefix_tokens(transformer)
|
||
profiler = _RefinerLayerCudaProfiler(
|
||
enabled=True,
|
||
device=self.device,
|
||
label=f"{forward_kind} seq={seq_len} prefix={prefix_tokens}",
|
||
)
|
||
|
||
with _profile_section(profiler, "mask_prepare"):
|
||
encoder_attention_mask = _prepare_encoder_attention_mask(encoder_attention_mask, hidden_states.dtype)
|
||
|
||
with _profile_section(profiler, "proj_in"):
|
||
hidden_states = transformer.proj_in(hidden_states)
|
||
with _profile_section(profiler, "time_embed"):
|
||
temb, embedded_timestep = transformer.time_embed(
|
||
timestep.flatten(),
|
||
batch_size=batch_size,
|
||
hidden_dtype=hidden_states.dtype,
|
||
)
|
||
temb = temb.view(batch_size, -1, temb.size(-1))
|
||
embedded_timestep = embedded_timestep.view(batch_size, -1, embedded_timestep.size(-1))
|
||
|
||
with _profile_section(profiler, "caption_projection"):
|
||
if _has_cross_attention_kv_cache(transformer):
|
||
encoder_hidden_states = None
|
||
else:
|
||
encoder_hidden_states = transformer.caption_projection(encoder_hidden_states)
|
||
encoder_hidden_states = encoder_hidden_states.view(batch_size, -1, hidden_states.size(-1))
|
||
|
||
with self._attention_backend_context(), self._nvfp4_autocast():
|
||
for layer_idx, block in enumerate(transformer.transformer_blocks):
|
||
capture_kv_only = stop_after_capture_kv_layer is not None and layer_idx >= int(
|
||
stop_after_capture_kv_layer
|
||
)
|
||
hidden_states = _forward_video_block(
|
||
block=block,
|
||
hidden_states=hidden_states,
|
||
encoder_hidden_states=encoder_hidden_states,
|
||
temb=temb,
|
||
video_rotary_emb=video_rotary_emb,
|
||
encoder_attention_mask=encoder_attention_mask,
|
||
n_context_tokens=n_context_tokens,
|
||
profiler=profiler,
|
||
capture_kv_only=capture_kv_only,
|
||
)
|
||
if capture_kv_only:
|
||
break
|
||
if stop_after_layer is not None and layer_idx >= int(stop_after_layer):
|
||
break
|
||
|
||
if skip_output_projection:
|
||
if profiler is not None:
|
||
profiler.finish()
|
||
return hidden_states
|
||
|
||
with _profile_section(profiler, "proj_out"):
|
||
scale_shift_values = transformer.scale_shift_table[None, None] + embedded_timestep[:, :, None]
|
||
shift, scale = scale_shift_values[:, :, 0], scale_shift_values[:, :, 1]
|
||
hidden_states = transformer.norm_out(hidden_states)
|
||
hidden_states = hidden_states * (1 + scale) + shift
|
||
hidden_states = transformer.proj_out(hidden_states)
|
||
if profiler is not None:
|
||
profiler.finish()
|
||
return hidden_states
|
||
|
||
def _forward_video_only(
|
||
self,
|
||
*,
|
||
hidden_states: torch.Tensor,
|
||
encoder_hidden_states: torch.Tensor,
|
||
timestep: torch.Tensor,
|
||
encoder_attention_mask: torch.Tensor | None,
|
||
num_frames: int,
|
||
height: int,
|
||
width: int,
|
||
fps: float,
|
||
n_context_tokens: int,
|
||
) -> torch.Tensor:
|
||
transformer = self.transformer
|
||
batch_size = hidden_states.size(0)
|
||
seq_len = int(hidden_states.shape[1])
|
||
profiler = None
|
||
if _refiner_layer_profile_enabled():
|
||
profiler = _RefinerLayerCudaProfiler(
|
||
enabled=True,
|
||
device=self.device,
|
||
label=f"legacy seq={seq_len} prefix={int(n_context_tokens)}",
|
||
)
|
||
|
||
with _profile_section(profiler, "mask_prepare"):
|
||
encoder_attention_mask = _prepare_encoder_attention_mask(encoder_attention_mask, hidden_states.dtype)
|
||
|
||
with _profile_section(profiler, "rope"):
|
||
video_coords = transformer.rope.prepare_video_coords(
|
||
batch_size, num_frames, height, width, hidden_states.device, fps=fps
|
||
)
|
||
video_rotary_emb = transformer.rope(video_coords, device=hidden_states.device)
|
||
|
||
with _profile_section(profiler, "proj_in"):
|
||
hidden_states = transformer.proj_in(hidden_states)
|
||
with _profile_section(profiler, "time_embed"):
|
||
temb, embedded_timestep = transformer.time_embed(
|
||
timestep.flatten(),
|
||
batch_size=batch_size,
|
||
hidden_dtype=hidden_states.dtype,
|
||
)
|
||
temb = temb.view(batch_size, -1, temb.size(-1))
|
||
embedded_timestep = embedded_timestep.view(batch_size, -1, embedded_timestep.size(-1))
|
||
|
||
with _profile_section(profiler, "caption_projection"):
|
||
if _has_cross_attention_kv_cache(transformer):
|
||
encoder_hidden_states = None
|
||
else:
|
||
encoder_hidden_states = transformer.caption_projection(encoder_hidden_states)
|
||
encoder_hidden_states = encoder_hidden_states.view(batch_size, -1, hidden_states.size(-1))
|
||
|
||
with self._attention_backend_context(), self._nvfp4_autocast():
|
||
for block in transformer.transformer_blocks:
|
||
hidden_states = _forward_video_block(
|
||
block=block,
|
||
hidden_states=hidden_states,
|
||
encoder_hidden_states=encoder_hidden_states,
|
||
temb=temb,
|
||
video_rotary_emb=video_rotary_emb,
|
||
encoder_attention_mask=encoder_attention_mask,
|
||
n_context_tokens=n_context_tokens,
|
||
profiler=profiler,
|
||
)
|
||
|
||
with _profile_section(profiler, "proj_out"):
|
||
scale_shift_values = transformer.scale_shift_table[None, None] + embedded_timestep[:, :, None]
|
||
shift, scale = scale_shift_values[:, :, 0], scale_shift_values[:, :, 1]
|
||
hidden_states = transformer.norm_out(hidden_states)
|
||
hidden_states = hidden_states * (1 + scale) + shift
|
||
hidden_states = transformer.proj_out(hidden_states)
|
||
if profiler is not None:
|
||
profiler.finish()
|
||
return hidden_states
|
||
|
||
|
||
class RefinerChunkRunner:
|
||
"""Stateful per-AR-block driver for ``DiffusersLTX2Refiner``.
|
||
|
||
Owns the rolling KV state that the chunk-causal AR recipe accumulates as
|
||
refiner blocks complete:
|
||
|
||
* ``_sink_kv_pre``: per-layer pre-RoPE K/V captured from the first
|
||
``source_sink_frames`` raw stage-1 latents at σ=0. Lazily filled on the
|
||
first call to :meth:`refine_block` (the orchestrator only has the first
|
||
stage-1 chunk in hand by then).
|
||
* ``_history_kv_post``: per-layer post-RoPE K/V of every refined block
|
||
already produced, trimmed to ``kv_max_frames - source_sink_frames``
|
||
frames so the sliding window stays bounded.
|
||
* ``_history_frames``: number of frames currently in
|
||
``_history_kv_post`` (drives token-level trim).
|
||
|
||
The numerical contract is identical to a single in-place call to
|
||
``_refine_latents_ar``: same RNG-seeded epsilon stream consumed
|
||
block-by-block, same ``rf_shifted_sink`` per-window prefix dict, same
|
||
3-step deterministic Euler, same post-RoPE capture under that prefix. The
|
||
orchestrator can therefore call :meth:`refine_block` once per stage-1 chunk
|
||
without changing inference semantics, and concurrently launch the
|
||
downstream causal-VAE decode on a separate CUDA stream while the next
|
||
block's refinement runs on the refiner stream.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
refiner: DiffusersLTX2Refiner,
|
||
*,
|
||
prompt_embeds: torch.Tensor,
|
||
prompt_attention_mask: torch.Tensor,
|
||
fps: float,
|
||
sigmas: torch.Tensor,
|
||
source_sink_frames: int,
|
||
block_size: int,
|
||
kv_max_frames: int,
|
||
seed: int,
|
||
spatial_shape: tuple[int, int],
|
||
n_active_frames: int | None = None,
|
||
latent_channels: int | None = None,
|
||
batch_size: int = 1,
|
||
) -> None:
|
||
self._refiner = refiner
|
||
self._prompt_embeds = prompt_embeds
|
||
self._prompt_attention_mask = prompt_attention_mask
|
||
self._fps = float(fps)
|
||
self._sigmas = sigmas
|
||
self._sigma_values = [float(v) for v in sigmas.detach().float().cpu()]
|
||
self._sigma_pairs = list(zip(self._sigma_values[:-1], self._sigma_values[1:]))
|
||
self._sigma_max = self._sigma_values[0]
|
||
self._n_steps = int(sigmas.numel() - 1)
|
||
self._source_sink_frames = int(source_sink_frames)
|
||
self._block_size = int(block_size)
|
||
self._kv_max_frames = int(kv_max_frames)
|
||
self._max_history_frames = int(kv_max_frames) - int(source_sink_frames)
|
||
self._device = refiner.device
|
||
self._dtype = refiner.dtype
|
||
self._generator = torch.Generator(device=self._device).manual_seed(int(seed))
|
||
self._kv_cache_storage_dtype = _resolve_kv_cache_storage_dtype()
|
||
|
||
transformer = refiner.transformer
|
||
self._n_layers = len(transformer.transformer_blocks)
|
||
H, W = spatial_shape
|
||
self._H, self._W = int(H), int(W)
|
||
self._tokens_per_frame = (
|
||
int(H // transformer.config.patch_size)
|
||
* int(W // transformer.config.patch_size)
|
||
* int(transformer.config.patch_size_t)
|
||
)
|
||
self._precomputed_eps_blocks: list[torch.Tensor] | None = None
|
||
if (
|
||
_env_flag("SANA_WM_REFINER_PREGENERATE_NOISE")
|
||
and n_active_frames is not None
|
||
and latent_channels is not None
|
||
):
|
||
n_active = int(n_active_frames)
|
||
channels = int(latent_channels)
|
||
batch = int(batch_size)
|
||
n_blocks = (n_active + self._block_size - 1) // self._block_size if n_active > 0 else 0
|
||
self._precomputed_eps_blocks = []
|
||
for block_idx in range(n_blocks):
|
||
active_len = min(self._block_size, n_active - block_idx * self._block_size)
|
||
self._precomputed_eps_blocks.append(
|
||
torch.randn(
|
||
(batch, channels, active_len, self._H, self._W),
|
||
generator=self._generator,
|
||
device=self._device,
|
||
dtype=self._dtype,
|
||
)
|
||
)
|
||
print(f"[refiner-noise] precomputed {len(self._precomputed_eps_blocks)} eps blocks", flush=True)
|
||
if _env_flag("SANA_WM_REFINER_CROSS_ATTN_KV_CACHE"):
|
||
with refiner._nvfp4_autocast():
|
||
_set_cross_attention_kv_cache(refiner.transformer, prompt_embeds, prompt_attention_mask)
|
||
else:
|
||
_clear_cross_attention_kv_cache(refiner.transformer)
|
||
|
||
self._sink_kv_pre: list[tuple[torch.Tensor, torch.Tensor]] | None = None
|
||
self._history_kv_post: list[tuple[torch.Tensor, torch.Tensor] | None] = [None] * self._n_layers
|
||
self._history_frames: int = 0
|
||
self._history_layer_mask = _refiner_history_layer_mask(self._n_layers)
|
||
self._exact_capture_layer_mask = _refiner_exact_capture_layer_mask(
|
||
self._n_layers,
|
||
default_mask=self._history_layer_mask,
|
||
)
|
||
if not all(self._history_layer_mask):
|
||
kept = sum(1 for keep in self._history_layer_mask if keep)
|
||
print(
|
||
f"[refiner-history] recent history enabled on {kept}/{self._n_layers} layers",
|
||
flush=True,
|
||
)
|
||
if self._exact_capture_layer_mask != self._history_layer_mask:
|
||
kept = sum(1 for keep in self._exact_capture_layer_mask if keep)
|
||
print(
|
||
f"[refiner-history] exact post-capture on {kept}/{self._n_layers} layers",
|
||
flush=True,
|
||
)
|
||
|
||
@torch.inference_mode()
|
||
def pre_capture_sink(self, sink_seed_frames: torch.Tensor) -> None:
|
||
"""Capture the source-sink K/V before the first active refiner block.
|
||
|
||
The sink is just the conditioning latent frame and does not depend on
|
||
stage-1 sampling. Scheduling this on the refiner stream lets it overlap
|
||
with stage-1 chunk 0 while preserving the exact same cached K/V that
|
||
``refine_block`` would have produced lazily.
|
||
"""
|
||
if self._sink_kv_pre is not None:
|
||
return
|
||
if sink_seed_frames is None:
|
||
raise ValueError("pre_capture_sink requires sink_seed_frames.")
|
||
if sink_seed_frames.shape[2] != self._source_sink_frames:
|
||
raise ValueError(
|
||
f"sink_seed_frames has {sink_seed_frames.shape[2]} frames "
|
||
f"but source_sink_frames={self._source_sink_frames}."
|
||
)
|
||
source_sink = sink_seed_frames.contiguous()
|
||
self._sink_kv_pre = [
|
||
_store_kv_pair(pair, self._kv_cache_storage_dtype)
|
||
for pair in self._refiner._capture_block_kv(
|
||
clean_block=source_sink,
|
||
frame_positions=list(range(self._source_sink_frames)),
|
||
prompt_embeds=self._prompt_embeds,
|
||
prompt_attention_mask=self._prompt_attention_mask,
|
||
fps=self._fps,
|
||
capture_mode="pre_rope",
|
||
kv_prefix_per_layer=None,
|
||
)
|
||
]
|
||
|
||
@torch.inference_mode()
|
||
def refine_block(
|
||
self,
|
||
*,
|
||
block_idx: int,
|
||
clean_block: torch.Tensor,
|
||
block_start: int,
|
||
block_end: int,
|
||
sink_seed_frames: torch.Tensor | None = None,
|
||
) -> torch.Tensor:
|
||
"""Refine one AR block; advance internal KV state.
|
||
|
||
Args:
|
||
block_idx: 0-based block index in the AR schedule. Used only for
|
||
bookkeeping; positional state derives from ``block_start``.
|
||
clean_block: ``(B, C, active_len, H, W)`` clean stage-1 latents
|
||
covering frames ``[block_start, block_end)``. The active block
|
||
is what actually gets refined; sink frames live outside the
|
||
active range and are passed via ``sink_seed_frames`` on the
|
||
first call.
|
||
block_start: absolute latent-frame index of the active block's
|
||
first frame (drives the ``rf_shifted_sink`` RoPE offset).
|
||
Must be >= ``source_sink_frames`` so the sink doesn't overlap
|
||
the active region.
|
||
block_end: absolute latent-frame index just past the active block.
|
||
sink_seed_frames: ``(B, C, source_sink_frames, H, W)`` raw sink
|
||
latents used once on the very first ``refine_block`` call to
|
||
pre-capture the pre-RoPE sink K/V at ``sigma=0`` with frame
|
||
positions ``[0, source_sink_frames)``. Required on the first
|
||
call; ignored thereafter. The orchestrator owns these — they
|
||
are typically the first ``source_sink_frames`` of stage-1's
|
||
first chunk.
|
||
|
||
Returns:
|
||
``(B, C, active_len, H, W)`` refined latents for this block.
|
||
"""
|
||
refiner = self._refiner
|
||
device = self._device
|
||
profiler = _RefinerCudaProfiler(enabled=_refiner_profile_enabled(), device=device, block_idx=int(block_idx))
|
||
B = int(clean_block.shape[0])
|
||
active_len = block_end - block_start
|
||
if block_start < self._source_sink_frames:
|
||
raise ValueError(
|
||
f"block_start={block_start} overlaps the source sink "
|
||
f"(source_sink_frames={self._source_sink_frames})."
|
||
)
|
||
|
||
# 1) On the first call: pre-capture PRE-RoPE sink K/V from the supplied
|
||
# raw sink latents at sigma=0 with absolute positions [0, sink_size).
|
||
if self._sink_kv_pre is None:
|
||
with profiler.section("sink_capture"):
|
||
if sink_seed_frames is None:
|
||
raise ValueError("First refine_block call requires sink_seed_frames " "(raw stage-1 sink latents).")
|
||
if sink_seed_frames.shape[2] != self._source_sink_frames:
|
||
raise ValueError(
|
||
f"sink_seed_frames has {sink_seed_frames.shape[2]} frames "
|
||
f"but source_sink_frames={self._source_sink_frames}."
|
||
)
|
||
self.pre_capture_sink(sink_seed_frames)
|
||
|
||
# 2) Build per-window kv_prefix dict per layer.
|
||
with profiler.section("prefix_build"):
|
||
sink_rope_offset_history = block_start - self._history_frames - self._source_sink_frames
|
||
sink_rope_offset_no_history = block_start - self._source_sink_frames
|
||
sink_pe_history = _build_rotary_emb_for_absolute_positions(
|
||
transformer=refiner.transformer,
|
||
batch_size=B,
|
||
frame_positions=list(
|
||
range(sink_rope_offset_history, sink_rope_offset_history + self._source_sink_frames)
|
||
),
|
||
height=self._H,
|
||
width=self._W,
|
||
device=device,
|
||
fps=self._fps,
|
||
)
|
||
sink_pe_no_history = sink_pe_history
|
||
if sink_rope_offset_no_history != sink_rope_offset_history:
|
||
sink_pe_no_history = _build_rotary_emb_for_absolute_positions(
|
||
transformer=refiner.transformer,
|
||
batch_size=B,
|
||
frame_positions=list(
|
||
range(sink_rope_offset_no_history, sink_rope_offset_no_history + self._source_sink_frames)
|
||
),
|
||
height=self._H,
|
||
width=self._W,
|
||
device=device,
|
||
fps=self._fps,
|
||
)
|
||
kv_prefix_per_layer: list[dict[str, object]] = []
|
||
preconcat_prefix = _env_flag("SANA_WM_REFINER_PRECONCAT_PREFIX")
|
||
empty_cache_before_prefix = _env_flag("SANA_WM_REFINER_EMPTY_CACHE_BEFORE_PREFIX")
|
||
for layer_idx in range(self._n_layers):
|
||
hk = self._history_kv_post[layer_idx]
|
||
use_history = bool(self._history_layer_mask[layer_idx] and hk is not None and hk[0].shape[1] > 0)
|
||
sink_pe = sink_pe_history if use_history else sink_pe_no_history
|
||
prefix: dict[str, object] = {
|
||
"mode": "rf_shifted_sink",
|
||
"sink_k_pre": self._sink_kv_pre[layer_idx][0],
|
||
"sink_v": self._sink_kv_pre[layer_idx][1],
|
||
"sink_pe": sink_pe,
|
||
"history_k": (hk[0] if use_history else None),
|
||
"history_v": (hk[1] if use_history else None),
|
||
}
|
||
if preconcat_prefix:
|
||
prefix_k_parts: list[torch.Tensor] = []
|
||
prefix_v_parts: list[torch.Tensor] = []
|
||
sink_k_pre, sink_v = self._sink_kv_pre[layer_idx]
|
||
if sink_k_pre.shape[1] > 0 and sink_v.shape[1] > 0:
|
||
attn = refiner.transformer.transformer_blocks[layer_idx].attn1
|
||
sink_k = _apply_refiner_rotary(attn, sink_k_pre.to(self._dtype), sink_pe)
|
||
prefix_k_parts.append(sink_k)
|
||
prefix_v_parts.append(sink_v.to(self._dtype))
|
||
if use_history:
|
||
prefix_k_parts.append(hk[0].to(self._dtype))
|
||
prefix_v_parts.append(hk[1].to(self._dtype))
|
||
if prefix_k_parts:
|
||
if empty_cache_before_prefix and device.type == "cuda":
|
||
torch.cuda.empty_cache()
|
||
prefix_k = torch.cat(prefix_k_parts, dim=1)
|
||
prefix_v = torch.cat(prefix_v_parts, dim=1)
|
||
prefix["prefix_k"] = prefix_k
|
||
prefix["prefix_v"] = prefix_v
|
||
kv_prefix_per_layer.append(prefix)
|
||
|
||
# 3) FM endpoint at sigma=sigma0: single epsilon per block.
|
||
with profiler.section("noise_init"):
|
||
eps = None
|
||
if self._precomputed_eps_blocks is not None and int(block_idx) < len(self._precomputed_eps_blocks):
|
||
candidate_eps = self._precomputed_eps_blocks[int(block_idx)]
|
||
if tuple(candidate_eps.shape) == tuple(clean_block.shape):
|
||
eps = candidate_eps
|
||
if eps is None:
|
||
eps = torch.randn(clean_block.shape, generator=self._generator, device=device, dtype=self._dtype)
|
||
x_t = ((1.0 - self._sigma_max) * clean_block.float() + self._sigma_max * eps.float()).to(self._dtype)
|
||
|
||
with profiler.section("active_rope"):
|
||
active_positions = list(range(int(block_start), int(block_end)))
|
||
active_video_rotary_emb = _build_rotary_emb_for_absolute_positions(
|
||
transformer=refiner.transformer,
|
||
batch_size=B,
|
||
frame_positions=active_positions,
|
||
height=self._H,
|
||
width=self._W,
|
||
device=device,
|
||
fps=self._fps,
|
||
)
|
||
fast_kv_capture = _refiner_fast_kv_capture_mode()
|
||
reuse_final_predict_kv = fast_kv_capture == "last_predict" and not _refiner_fast_kv_needs_clean_block(
|
||
int(block_idx)
|
||
)
|
||
fill_missing_predict_kv = fast_kv_capture == "fill_missing"
|
||
captured_kv_post: list[tuple[torch.Tensor, torch.Tensor] | None] | None = None
|
||
n_sigma_pairs = len(self._sigma_pairs)
|
||
for step_idx, (sigma_cur, sigma_next) in enumerate(self._sigma_pairs):
|
||
with profiler.section(f"denoise_step{step_idx}"):
|
||
capture_predict_kv = bool(
|
||
(reuse_final_predict_kv or fill_missing_predict_kv) and step_idx == n_sigma_pairs - 1
|
||
)
|
||
pred_result = refiner._predict_x0_active_block(
|
||
active=x_t,
|
||
active_positions=active_positions,
|
||
sigma_cur=sigma_cur,
|
||
prompt_embeds=self._prompt_embeds,
|
||
prompt_attention_mask=self._prompt_attention_mask,
|
||
fps=self._fps,
|
||
kv_prefix_per_layer=kv_prefix_per_layer,
|
||
active_video_rotary_emb=active_video_rotary_emb,
|
||
capture_post_kv=capture_predict_kv,
|
||
capture_layer_mask=self._history_layer_mask,
|
||
)
|
||
if isinstance(pred_result, tuple):
|
||
pred_x0, captured_kv_post = pred_result
|
||
if fill_missing_predict_kv and captured_kv_post is not None:
|
||
captured_kv_post = [
|
||
(
|
||
None
|
||
if self._exact_capture_layer_mask[layer_idx]
|
||
else (_store_kv_pair(pair, self._kv_cache_storage_dtype) if pair is not None else None)
|
||
)
|
||
for layer_idx, pair in enumerate(captured_kv_post)
|
||
]
|
||
else:
|
||
pred_x0 = pred_result
|
||
if sigma_cur <= 1.0e-6:
|
||
x_t = pred_x0.to(self._dtype)
|
||
else:
|
||
ratio = sigma_next / sigma_cur
|
||
x_t = (ratio * x_t.float() + (1.0 - ratio) * pred_x0.float()).to(self._dtype)
|
||
pred_x0 = None
|
||
|
||
if self._max_history_frames <= 0:
|
||
with profiler.section("history_update"):
|
||
self._history_frames = 0
|
||
for layer_idx in range(self._n_layers):
|
||
self._history_kv_post[layer_idx] = None
|
||
profiler.finish()
|
||
return x_t
|
||
|
||
# 4) Capture POST-RoPE K/V for this refined block under the same prefix.
|
||
with profiler.section("post_capture"):
|
||
if reuse_final_predict_kv:
|
||
if captured_kv_post is None:
|
||
raise RuntimeError("SANA_WM_REFINER_FAST_KV_CAPTURE=last_predict did not capture post-RoPE K/V.")
|
||
block_kv_post = captured_kv_post
|
||
else:
|
||
if _refiner_empty_cache_before_capture() and device.type == "cuda":
|
||
torch.cuda.empty_cache()
|
||
block_kv_post = refiner._capture_block_kv(
|
||
clean_block=x_t,
|
||
frame_positions=active_positions,
|
||
prompt_embeds=self._prompt_embeds,
|
||
prompt_attention_mask=self._prompt_attention_mask,
|
||
fps=self._fps,
|
||
capture_mode="post_rope",
|
||
kv_prefix_per_layer=kv_prefix_per_layer,
|
||
capture_layer_mask=self._exact_capture_layer_mask,
|
||
video_rotary_emb=active_video_rotary_emb,
|
||
)
|
||
if fill_missing_predict_kv:
|
||
if captured_kv_post is None:
|
||
raise RuntimeError("SANA_WM_REFINER_FAST_KV_CAPTURE=fill_missing did not capture fallback K/V.")
|
||
block_kv_post = [
|
||
exact_pair if self._exact_capture_layer_mask[layer_idx] else captured_kv_post[layer_idx]
|
||
for layer_idx, exact_pair in enumerate(block_kv_post)
|
||
]
|
||
with profiler.section("history_update"):
|
||
for layer_idx in range(self._n_layers):
|
||
if not self._history_layer_mask[layer_idx]:
|
||
self._history_kv_post[layer_idx] = None
|
||
continue
|
||
raw_pair = block_kv_post[layer_idx]
|
||
if raw_pair is None:
|
||
raise RuntimeError(f"Missing post-RoPE K/V capture for history layer {layer_idx}.")
|
||
raw_k, raw_v = raw_pair
|
||
new_k = _store_kv_tensor(raw_k, self._kv_cache_storage_dtype)
|
||
new_v = _store_kv_tensor(raw_v, self._kv_cache_storage_dtype)
|
||
block_kv_post[layer_idx] = (new_k, new_v)
|
||
old = self._history_kv_post[layer_idx]
|
||
if old is None:
|
||
if self._max_history_frames > 0 and active_len > self._max_history_frames:
|
||
keep_tokens = self._max_history_frames * self._tokens_per_frame
|
||
self._history_kv_post[layer_idx] = (new_k[:, -keep_tokens:], new_v[:, -keep_tokens:])
|
||
else:
|
||
self._history_kv_post[layer_idx] = (new_k, new_v)
|
||
else:
|
||
if self._max_history_frames > 0:
|
||
keep_old_frames = max(0, self._max_history_frames - active_len)
|
||
keep_old_tokens = keep_old_frames * self._tokens_per_frame
|
||
old = (
|
||
old[0][:, -keep_old_tokens:] if keep_old_tokens > 0 else old[0][:, :0],
|
||
old[1][:, -keep_old_tokens:] if keep_old_tokens > 0 else old[1][:, :0],
|
||
)
|
||
self._history_kv_post[layer_idx] = (
|
||
torch.cat([old[0], new_k], dim=1),
|
||
torch.cat([old[1], new_v], dim=1),
|
||
)
|
||
raw_k = None
|
||
raw_v = None
|
||
self._history_frames += active_len
|
||
|
||
if self._max_history_frames > 0 and self._history_frames > self._max_history_frames:
|
||
keep_tokens = self._max_history_frames * self._tokens_per_frame
|
||
for layer_idx in range(self._n_layers):
|
||
hk = self._history_kv_post[layer_idx]
|
||
if hk is not None:
|
||
self._history_kv_post[layer_idx] = (hk[0][:, -keep_tokens:], hk[1][:, -keep_tokens:])
|
||
self._history_frames = self._max_history_frames
|
||
|
||
profiler.finish()
|
||
return x_t
|
||
|
||
|
||
def _build_rotary_emb_for_absolute_positions(
|
||
*,
|
||
transformer: nn.Module,
|
||
batch_size: int,
|
||
frame_positions: list[int],
|
||
height: int,
|
||
width: int,
|
||
device: torch.device,
|
||
fps: float,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
"""Reimplement ``LTX2VideoRotaryPosEmbed.prepare_video_coords`` with explicit per-frame positions.
|
||
|
||
The default helper assumes contiguous ``torch.arange(num_frames)`` which is
|
||
fine for bidirectional inference; the sliding-window AR refiner needs to
|
||
keep each frame's absolute index in the source video so RoPE captures the
|
||
correct temporal phase across the sink + recent + active window.
|
||
"""
|
||
rope = transformer.rope
|
||
patch_size_t = int(rope.patch_size_t)
|
||
patch_size = int(rope.patch_size)
|
||
f_positions = torch.tensor(frame_positions, dtype=torch.float32, device=device)
|
||
if patch_size_t > 1:
|
||
# Each patch covers ``patch_size_t`` latent frames; pick the start of each patch.
|
||
f_positions = f_positions[::patch_size_t]
|
||
int(f_positions.shape[0])
|
||
grid_h = torch.arange(start=0, end=height, step=patch_size, dtype=torch.float32, device=device)
|
||
grid_w = torch.arange(start=0, end=width, step=patch_size, dtype=torch.float32, device=device)
|
||
grid = torch.meshgrid(f_positions, grid_h, grid_w, indexing="ij")
|
||
grid = torch.stack(grid, dim=0) # [3, N_F, N_H, N_W]
|
||
|
||
patch_size_delta = torch.tensor((patch_size_t, patch_size, patch_size), dtype=grid.dtype, device=device)
|
||
patch_ends = grid + patch_size_delta.view(3, 1, 1, 1)
|
||
latent_coords = torch.stack([grid, patch_ends], dim=-1)
|
||
latent_coords = latent_coords.flatten(1, 3).unsqueeze(0).repeat(batch_size, 1, 1, 1)
|
||
|
||
scale_tensor = torch.tensor(rope.scale_factors, device=device)
|
||
broadcast_shape = [1] * latent_coords.ndim
|
||
broadcast_shape[1] = -1
|
||
pixel_coords = latent_coords * scale_tensor.view(*broadcast_shape)
|
||
pixel_coords[:, 0, ...] = (pixel_coords[:, 0, ...] + rope.causal_offset - rope.scale_factors[0]).clamp(min=0)
|
||
pixel_coords[:, 0, ...] = pixel_coords[:, 0, ...] / float(fps)
|
||
return rope(pixel_coords, device=device)
|
||
|
||
|
||
def _forward_video_block(
|
||
*,
|
||
block: nn.Module,
|
||
hidden_states: torch.Tensor,
|
||
encoder_hidden_states: torch.Tensor | None,
|
||
temb: torch.Tensor,
|
||
video_rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||
encoder_attention_mask: torch.Tensor | None,
|
||
n_context_tokens: int,
|
||
profiler: _RefinerLayerCudaProfiler | None = None,
|
||
capture_kv_only: bool = False,
|
||
) -> torch.Tensor:
|
||
batch_size = hidden_states.size(0)
|
||
|
||
if profiler is None:
|
||
norm_hidden_states = block.norm1(hidden_states)
|
||
num_ada_params = block.scale_shift_table.shape[0]
|
||
ada_values = block.scale_shift_table[None, None].to(temb.device) + temb.reshape(
|
||
batch_size, temb.size(1), num_ada_params, -1
|
||
)
|
||
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ada_values.unbind(dim=2)
|
||
norm_hidden_states = norm_hidden_states * (1 + scale_msa) + shift_msa
|
||
|
||
if capture_kv_only:
|
||
_capture_streaming_self_attention_kv(
|
||
attn=block.attn1,
|
||
hidden_states=norm_hidden_states,
|
||
query_rotary_emb=video_rotary_emb,
|
||
)
|
||
return hidden_states
|
||
|
||
attn_hidden_states = _streaming_self_attention(
|
||
attn=block.attn1,
|
||
hidden_states=norm_hidden_states,
|
||
query_rotary_emb=video_rotary_emb,
|
||
n_context_tokens=n_context_tokens,
|
||
)
|
||
hidden_states = hidden_states + attn_hidden_states * gate_msa
|
||
|
||
norm_hidden_states = block.norm2(hidden_states)
|
||
cross_kv_cache = getattr(block.attn2, "_sana_cross_attn_kv_cache", None)
|
||
if cross_kv_cache is None:
|
||
attn_hidden_states = block.attn2(
|
||
norm_hidden_states,
|
||
encoder_hidden_states=encoder_hidden_states,
|
||
query_rotary_emb=None,
|
||
attention_mask=encoder_attention_mask,
|
||
)
|
||
else:
|
||
attn_hidden_states = _cross_attention_with_cached_kv(block.attn2, norm_hidden_states, cross_kv_cache)
|
||
hidden_states = hidden_states + attn_hidden_states
|
||
|
||
norm_hidden_states = block.norm3(hidden_states) * (1 + scale_mlp) + shift_mlp
|
||
hidden_states = hidden_states + block.ff(norm_hidden_states) * gate_mlp
|
||
return hidden_states
|
||
|
||
with _profile_section(profiler, "norm_adaln"):
|
||
norm_hidden_states = block.norm1(hidden_states)
|
||
num_ada_params = block.scale_shift_table.shape[0]
|
||
ada_values = block.scale_shift_table[None, None].to(temb.device) + temb.reshape(
|
||
batch_size, temb.size(1), num_ada_params, -1
|
||
)
|
||
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ada_values.unbind(dim=2)
|
||
norm_hidden_states = norm_hidden_states * (1 + scale_msa) + shift_msa
|
||
|
||
with _profile_section(profiler, "self_attn"):
|
||
if capture_kv_only:
|
||
_capture_streaming_self_attention_kv(
|
||
attn=block.attn1,
|
||
hidden_states=norm_hidden_states,
|
||
query_rotary_emb=video_rotary_emb,
|
||
)
|
||
return hidden_states
|
||
else:
|
||
attn_hidden_states = _streaming_self_attention(
|
||
attn=block.attn1,
|
||
hidden_states=norm_hidden_states,
|
||
query_rotary_emb=video_rotary_emb,
|
||
n_context_tokens=n_context_tokens,
|
||
)
|
||
hidden_states = hidden_states + attn_hidden_states * gate_msa
|
||
|
||
with _profile_section(profiler, "cross_attn"):
|
||
norm_hidden_states = block.norm2(hidden_states)
|
||
cross_kv_cache = getattr(block.attn2, "_sana_cross_attn_kv_cache", None)
|
||
if cross_kv_cache is None:
|
||
attn_hidden_states = block.attn2(
|
||
norm_hidden_states,
|
||
encoder_hidden_states=encoder_hidden_states,
|
||
query_rotary_emb=None,
|
||
attention_mask=encoder_attention_mask,
|
||
)
|
||
else:
|
||
attn_hidden_states = _cross_attention_with_cached_kv(block.attn2, norm_hidden_states, cross_kv_cache)
|
||
hidden_states = hidden_states + attn_hidden_states
|
||
|
||
with _profile_section(profiler, "ffn"):
|
||
norm_hidden_states = block.norm3(hidden_states) * (1 + scale_mlp) + shift_mlp
|
||
hidden_states = hidden_states + block.ff(norm_hidden_states) * gate_mlp
|
||
return hidden_states
|
||
|
||
|
||
def _streaming_self_attention(
|
||
*,
|
||
attn: nn.Module,
|
||
hidden_states: torch.Tensor,
|
||
query_rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||
n_context_tokens: int,
|
||
) -> torch.Tensor:
|
||
"""LTX-2 self-attention with sink/current streaming mask + AR KV-cache hooks.
|
||
|
||
Two modes are layered on top of vanilla diffusers self-attention, selected by
|
||
``n_context_tokens`` and per-block hook attributes (set by the AR refiner):
|
||
|
||
* ``n_context_tokens > 0`` (legacy single-shot path) — sink queries attend
|
||
sink only, current queries attend ``[sink + current]`` via two SDPA calls.
|
||
|
||
* ``n_context_tokens == 0`` (AR mode) — Q comes from the active block only;
|
||
the per-block ``_tf_kv_prefix`` dict (``rf_shifted_sink``) supplies the
|
||
pre-RoPE sink K/V (re-RoPE'd here with its sliding offset PE) and the
|
||
post-RoPE recent-history K/V, concatenated before SDPA. The
|
||
``_kv_cache_capture`` and ``_tf_capture_kv`` hooks record K/V into the
|
||
module for the AR orchestrator to read back.
|
||
"""
|
||
from diffusers.models.transformers.transformer_ltx2 import apply_interleaved_rotary_emb, apply_split_rotary_emb
|
||
|
||
gate_logits = attn.to_gate_logits(hidden_states) if attn.to_gate_logits is not None else None
|
||
|
||
fused_qkv = getattr(attn, "_sana_fused_qkv", None)
|
||
if fused_qkv is not None:
|
||
query, key, value = fused_qkv(hidden_states)
|
||
else:
|
||
query = attn.to_q(hidden_states)
|
||
key = attn.to_k(hidden_states)
|
||
value = attn.to_v(hidden_states)
|
||
|
||
query = attn.norm_q(query)
|
||
key = attn.norm_k(key)
|
||
|
||
# KV-cache capture / inject hooks for ``rf_shifted_sink`` AR refinement.
|
||
# Mirrors tian's ``diffusion/vendors/ltx/ltx_core/model/transformer/attention.py``:
|
||
# - ``_kv_cache_capture`` saves PRE-RoPE (post-norm) K/V so a future window
|
||
# can re-apply RoPE at its shifted sink offset.
|
||
# - ``_tf_capture_kv`` saves POST-RoPE K/V so the next window can directly
|
||
# concatenate the recent history.
|
||
# - ``_tf_kv_prefix`` (a dict with ``mode='rf_shifted_sink'``) prepends a
|
||
# re-RoPE'd sink + already-post-RoPE recent history before SDPA.
|
||
if getattr(attn, "_kv_cache_capture", False):
|
||
attn._cached_kv_pre = (_capture_kv_tensor(key), _capture_kv_tensor(value))
|
||
|
||
if attn.rope_type == "interleaved":
|
||
query = apply_interleaved_rotary_emb(query, query_rotary_emb)
|
||
key = apply_interleaved_rotary_emb(key, query_rotary_emb)
|
||
elif attn.rope_type == "split":
|
||
query = apply_split_rotary_emb(query, query_rotary_emb)
|
||
key = apply_split_rotary_emb(key, query_rotary_emb)
|
||
else:
|
||
raise ValueError(f"Unsupported LTX-2 RoPE type: {attn.rope_type}")
|
||
|
||
if getattr(attn, "_tf_capture_kv", False):
|
||
attn._cached_kv_post = (_capture_kv_tensor(key), _capture_kv_tensor(value))
|
||
|
||
tf_prefix = getattr(attn, "_tf_kv_prefix", None)
|
||
if isinstance(tf_prefix, dict) and tf_prefix.get("mode") == "rf_shifted_sink":
|
||
prefix_k = tf_prefix.get("prefix_k")
|
||
prefix_v = tf_prefix.get("prefix_v")
|
||
if prefix_k is not None and prefix_v is not None:
|
||
key = torch.cat([prefix_k.to(key.dtype), key], dim=1)
|
||
value = torch.cat([prefix_v.to(value.dtype), value], dim=1)
|
||
else:
|
||
prefix_k_parts: list[torch.Tensor] = []
|
||
prefix_v_parts: list[torch.Tensor] = []
|
||
sink_k_pre = tf_prefix.get("sink_k_pre")
|
||
sink_v = tf_prefix.get("sink_v")
|
||
if sink_k_pre is not None and sink_v is not None and sink_k_pre.shape[1] > 0:
|
||
sink_pe = tf_prefix.get("sink_pe")
|
||
if sink_pe is None:
|
||
raise RuntimeError("rf_shifted_sink prefix requires a sink_pe RoPE tuple.")
|
||
sink_k_pre_dt = sink_k_pre.to(key.dtype)
|
||
if attn.rope_type == "interleaved":
|
||
sink_k = apply_interleaved_rotary_emb(sink_k_pre_dt, sink_pe)
|
||
else:
|
||
sink_k = apply_split_rotary_emb(sink_k_pre_dt, sink_pe)
|
||
prefix_k_parts.append(sink_k)
|
||
prefix_v_parts.append(sink_v.to(value.dtype))
|
||
history_k = tf_prefix.get("history_k")
|
||
history_v = tf_prefix.get("history_v")
|
||
if history_k is not None and history_v is not None and history_k.shape[1] > 0:
|
||
prefix_k_parts.append(history_k.to(key.dtype))
|
||
prefix_v_parts.append(history_v.to(value.dtype))
|
||
if prefix_k_parts:
|
||
key = torch.cat([*prefix_k_parts, key], dim=1)
|
||
value = torch.cat([*prefix_v_parts, value], dim=1)
|
||
|
||
query = query.unflatten(2, (attn.heads, -1))
|
||
key = key.unflatten(2, (attn.heads, -1))
|
||
value = value.unflatten(2, (attn.heads, -1))
|
||
|
||
processor = attn.processor
|
||
backend = getattr(processor, "_attention_backend", None)
|
||
parallel_config = getattr(processor, "_parallel_config", None)
|
||
|
||
# AR mode (n_context_tokens == 0): Q from active block attends to the
|
||
# injected prefix + current K/V in one SDPA call. Legacy single-shot
|
||
# mode keeps the sink-self / current-cross split.
|
||
if n_context_tokens <= 0 or n_context_tokens >= query.shape[1]:
|
||
hidden_states = _refiner_attention(
|
||
query,
|
||
key,
|
||
value,
|
||
backend=backend,
|
||
parallel_config=parallel_config,
|
||
)
|
||
else:
|
||
context_hidden_states = _refiner_attention(
|
||
query[:, :n_context_tokens],
|
||
key[:, :n_context_tokens],
|
||
value[:, :n_context_tokens],
|
||
backend=backend,
|
||
parallel_config=parallel_config,
|
||
)
|
||
current_hidden_states = _refiner_attention(
|
||
query[:, n_context_tokens:],
|
||
key,
|
||
value,
|
||
backend=backend,
|
||
parallel_config=parallel_config,
|
||
)
|
||
hidden_states = torch.cat([context_hidden_states, current_hidden_states], dim=1)
|
||
|
||
hidden_states = hidden_states.flatten(2, 3).to(query.dtype)
|
||
|
||
if gate_logits is not None:
|
||
hidden_states = hidden_states.unflatten(2, (attn.heads, -1))
|
||
gates = 2.0 * torch.sigmoid(gate_logits)
|
||
hidden_states = hidden_states * gates.unsqueeze(-1)
|
||
hidden_states = hidden_states.flatten(2, 3)
|
||
|
||
hidden_states = attn.to_out[0](hidden_states)
|
||
hidden_states = attn.to_out[1](hidden_states)
|
||
return hidden_states
|
||
|
||
|
||
def _capture_streaming_self_attention_kv(
|
||
*,
|
||
attn: nn.Module,
|
||
hidden_states: torch.Tensor,
|
||
query_rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||
) -> None:
|
||
"""Capture the current layer self-attention K/V without computing attention output."""
|
||
from diffusers.models.transformers.transformer_ltx2 import apply_interleaved_rotary_emb, apply_split_rotary_emb
|
||
|
||
fused_qkv = getattr(attn, "_sana_fused_qkv", None)
|
||
if fused_qkv is not None:
|
||
_, key, value = fused_qkv(hidden_states)
|
||
else:
|
||
key = attn.to_k(hidden_states)
|
||
value = attn.to_v(hidden_states)
|
||
|
||
key = attn.norm_k(key)
|
||
|
||
if getattr(attn, "_kv_cache_capture", False):
|
||
attn._cached_kv_pre = (_capture_kv_tensor(key), _capture_kv_tensor(value))
|
||
|
||
if attn.rope_type == "interleaved":
|
||
key = apply_interleaved_rotary_emb(key, query_rotary_emb)
|
||
elif attn.rope_type == "split":
|
||
key = apply_split_rotary_emb(key, query_rotary_emb)
|
||
else:
|
||
raise ValueError(f"Unsupported LTX-2 RoPE type: {attn.rope_type}")
|
||
|
||
if getattr(attn, "_tf_capture_kv", False):
|
||
attn._cached_kv_post = (_capture_kv_tensor(key), _capture_kv_tensor(value))
|
||
|
||
|
||
def _apply_refiner_rotary(
|
||
attn: nn.Module,
|
||
tensor: torch.Tensor,
|
||
rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||
) -> torch.Tensor:
|
||
from diffusers.models.transformers.transformer_ltx2 import apply_interleaved_rotary_emb, apply_split_rotary_emb
|
||
|
||
if attn.rope_type == "interleaved":
|
||
return apply_interleaved_rotary_emb(tensor, rotary_emb)
|
||
if attn.rope_type == "split":
|
||
return apply_split_rotary_emb(tensor, rotary_emb)
|
||
raise ValueError(f"Unsupported LTX-2 RoPE type: {attn.rope_type}")
|
||
|
||
|
||
def _refiner_attention(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
*,
|
||
backend: object,
|
||
parallel_config: object,
|
||
) -> torch.Tensor:
|
||
kernel = _refiner_self_attn_kernel()
|
||
if kernel in {"flash_attn", "flash-attn", "fa2"}:
|
||
return _flash_attn_func()(query, key, value, dropout_p=0.0, causal=False)
|
||
if kernel in {"sdpa", "torch_sdpa", "pytorch_sdpa"}:
|
||
hidden_states = F.scaled_dot_product_attention(
|
||
query.transpose(1, 2),
|
||
key.transpose(1, 2),
|
||
value.transpose(1, 2),
|
||
attn_mask=None,
|
||
dropout_p=0.0,
|
||
is_causal=False,
|
||
)
|
||
return hidden_states.transpose(1, 2)
|
||
if kernel and kernel not in {"default", "dispatch", "diffusers", "0", "off"}:
|
||
raise ValueError(f"Unsupported SANA_WM_REFINER_SELF_ATTN_KERNEL={kernel!r}.")
|
||
from diffusers.models.attention_dispatch import dispatch_attention_fn
|
||
|
||
return dispatch_attention_fn(
|
||
query,
|
||
key,
|
||
value,
|
||
attn_mask=None,
|
||
dropout_p=0.0,
|
||
is_causal=False,
|
||
backend=backend,
|
||
parallel_config=parallel_config,
|
||
)
|
||
|
||
|
||
def _refiner_self_attn_kernel() -> str:
|
||
return os.environ.get("SANA_WM_REFINER_SELF_ATTN_KERNEL", "").strip().lower()
|
||
|
||
|
||
_FLASH_ATTN_FUNC = None
|
||
|
||
|
||
def _flash_attn_func():
|
||
global _FLASH_ATTN_FUNC
|
||
if _FLASH_ATTN_FUNC is None:
|
||
from flash_attn import flash_attn_func
|
||
|
||
_FLASH_ATTN_FUNC = flash_attn_func
|
||
return _FLASH_ATTN_FUNC
|
||
|
||
|
||
def _set_cross_attention_kv_cache(
|
||
transformer: nn.Module,
|
||
prompt_embeds: torch.Tensor,
|
||
prompt_attention_mask: torch.Tensor | None,
|
||
) -> None:
|
||
blocks = transformer.transformer_blocks
|
||
if not blocks:
|
||
return
|
||
batch_size = int(prompt_embeds.shape[0])
|
||
hidden_dim = int(blocks[0].attn2.to_k.in_features)
|
||
encoder_hidden_states = transformer.caption_projection(prompt_embeds)
|
||
encoder_hidden_states = encoder_hidden_states.view(batch_size, -1, hidden_dim)
|
||
encoder_attention_mask = _prepare_encoder_attention_mask(prompt_attention_mask, encoder_hidden_states.dtype)
|
||
|
||
for block in blocks:
|
||
attn = block.attn2
|
||
cross_hidden = encoder_hidden_states
|
||
if getattr(attn, "norm_cross", False):
|
||
cross_hidden = attn.norm_encoder_hidden_states(cross_hidden)
|
||
|
||
key = attn.to_k(cross_hidden)
|
||
value = attn.to_v(cross_hidden)
|
||
if attn.norm_k is not None:
|
||
key = attn.norm_k(key)
|
||
inner_dim = int(key.shape[-1])
|
||
head_dim = inner_dim // int(attn.heads)
|
||
key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||
value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||
attn._sana_cross_attn_kv_cache = (key.detach(), value.detach(), encoder_attention_mask)
|
||
|
||
|
||
def _clear_cross_attention_kv_cache(transformer: nn.Module) -> None:
|
||
for block in transformer.transformer_blocks:
|
||
if hasattr(block.attn2, "_sana_cross_attn_kv_cache"):
|
||
block.attn2._sana_cross_attn_kv_cache = None
|
||
|
||
|
||
def _has_cross_attention_kv_cache(transformer: nn.Module) -> bool:
|
||
blocks = getattr(transformer, "transformer_blocks", None)
|
||
if not blocks:
|
||
return False
|
||
return getattr(blocks[0].attn2, "_sana_cross_attn_kv_cache", None) is not None
|
||
|
||
|
||
def _cross_attention_with_cached_kv(
|
||
attn: nn.Module,
|
||
hidden_states: torch.Tensor,
|
||
cache: tuple[torch.Tensor, torch.Tensor, torch.Tensor | None],
|
||
) -> torch.Tensor:
|
||
key, value, attention_mask = cache
|
||
residual = hidden_states
|
||
input_ndim = hidden_states.ndim
|
||
|
||
spatial_norm = getattr(attn, "spatial_norm", None)
|
||
if spatial_norm is not None:
|
||
hidden_states = spatial_norm(hidden_states, None)
|
||
|
||
if input_ndim == 4:
|
||
batch_size, channel, height, width = hidden_states.shape
|
||
hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2)
|
||
else:
|
||
batch_size = int(hidden_states.shape[0])
|
||
channel = height = width = None
|
||
|
||
if attention_mask is not None:
|
||
source_length = int(key.shape[2])
|
||
prepare_attention_mask = getattr(attn, "prepare_attention_mask", None)
|
||
if prepare_attention_mask is not None:
|
||
attn_mask = prepare_attention_mask(attention_mask, source_length, batch_size)
|
||
attn_mask = attn_mask.view(batch_size, attn.heads, -1, attn_mask.shape[-1])
|
||
elif attention_mask.ndim == 3:
|
||
attn_mask = attention_mask[:, None, :, :]
|
||
elif attention_mask.ndim == 2:
|
||
attn_mask = attention_mask[:, None, None, :]
|
||
else:
|
||
attn_mask = attention_mask
|
||
else:
|
||
attn_mask = None
|
||
|
||
group_norm = getattr(attn, "group_norm", None)
|
||
if group_norm is not None:
|
||
hidden_states = group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
|
||
|
||
query = attn.to_q(hidden_states)
|
||
if attn.norm_q is not None:
|
||
query = attn.norm_q(query)
|
||
head_dim = int(key.shape[-1])
|
||
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||
|
||
hidden_states = F.scaled_dot_product_attention(
|
||
query,
|
||
key.to(query.dtype),
|
||
value.to(query.dtype),
|
||
attn_mask=attn_mask,
|
||
dropout_p=0.0,
|
||
is_causal=False,
|
||
)
|
||
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
|
||
hidden_states = hidden_states.to(query.dtype)
|
||
hidden_states = attn.to_out[0](hidden_states)
|
||
hidden_states = attn.to_out[1](hidden_states)
|
||
|
||
if input_ndim == 4:
|
||
hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width)
|
||
|
||
if getattr(attn, "residual_connection", False):
|
||
hidden_states = hidden_states + residual
|
||
return hidden_states / float(getattr(attn, "rescale_output_factor", 1.0))
|
||
|
||
|
||
def _set_kv_prefix_on_blocks(
|
||
transformer: nn.Module,
|
||
kv_prefix_per_layer: list[dict[str, object]] | None,
|
||
) -> None:
|
||
"""Mirror tian's ``_inject_kv_prefix``: attach a per-layer prefix dict to each ``attn1``."""
|
||
blocks = transformer.transformer_blocks
|
||
if kv_prefix_per_layer is None:
|
||
_clear_kv_prefix_on_blocks(transformer)
|
||
return
|
||
if len(kv_prefix_per_layer) != len(blocks):
|
||
raise RuntimeError(
|
||
f"kv_prefix_per_layer has {len(kv_prefix_per_layer)} entries but transformer has {len(blocks)} blocks."
|
||
)
|
||
for block, prefix in zip(blocks, kv_prefix_per_layer):
|
||
block.attn1._tf_kv_prefix = prefix
|
||
|
||
|
||
def _clear_kv_prefix_on_blocks(transformer: nn.Module) -> None:
|
||
for block in transformer.transformer_blocks:
|
||
block.attn1._tf_kv_prefix = None
|
||
|
||
|
||
def _set_capture_flag_on_blocks(
|
||
transformer: nn.Module,
|
||
mode: str,
|
||
*,
|
||
enable: bool,
|
||
layer_mask: list[bool] | None = None,
|
||
) -> None:
|
||
"""Toggle ``_kv_cache_capture`` (pre-RoPE) or ``_tf_capture_kv`` (post-RoPE) per block."""
|
||
if mode == "pre_rope":
|
||
attr = "_kv_cache_capture"
|
||
clear_attr = "_cached_kv_pre"
|
||
elif mode == "post_rope":
|
||
attr = "_tf_capture_kv"
|
||
clear_attr = "_cached_kv_post"
|
||
else:
|
||
raise ValueError(f"capture_mode must be 'pre_rope' or 'post_rope', got {mode!r}")
|
||
blocks = transformer.transformer_blocks
|
||
if layer_mask is not None and len(layer_mask) != len(blocks):
|
||
raise RuntimeError(f"layer_mask has {len(layer_mask)} entries but transformer has {len(blocks)} blocks.")
|
||
for layer_idx, block in enumerate(blocks):
|
||
enabled = bool(enable and (layer_mask is None or layer_mask[layer_idx]))
|
||
setattr(block.attn1, attr, enabled)
|
||
if enabled:
|
||
# Clear any previous capture so the next forward writes a fresh value.
|
||
if hasattr(block.attn1, clear_attr):
|
||
setattr(block.attn1, clear_attr, None)
|
||
|
||
|
||
def _collect_captured_kv_from_blocks(
|
||
transformer: nn.Module,
|
||
mode: str,
|
||
layer_mask: list[bool] | None = None,
|
||
) -> list[tuple[torch.Tensor, torch.Tensor] | None]:
|
||
attr = "_cached_kv_pre" if mode == "pre_rope" else "_cached_kv_post"
|
||
blocks = transformer.transformer_blocks
|
||
if layer_mask is not None and len(layer_mask) != len(blocks):
|
||
raise RuntimeError(f"layer_mask has {len(layer_mask)} entries but transformer has {len(blocks)} blocks.")
|
||
out: list[tuple[torch.Tensor, torch.Tensor] | None] = []
|
||
for layer_idx, block in enumerate(blocks):
|
||
if layer_mask is not None and not layer_mask[layer_idx]:
|
||
out.append(None)
|
||
if hasattr(block.attn1, attr):
|
||
setattr(block.attn1, attr, None)
|
||
continue
|
||
cached = getattr(block.attn1, attr, None)
|
||
if cached is None:
|
||
raise RuntimeError(f"Expected {attr!r} on attn1 after capture forward, but found None.")
|
||
out.append(cached)
|
||
# Release the reference so the orchestrator owns the only handle.
|
||
setattr(block.attn1, attr, None)
|
||
return out
|
||
|
||
|
||
def _pack_text_embeds(
|
||
text_hidden_states: torch.Tensor,
|
||
sequence_lengths: torch.Tensor,
|
||
device: str | torch.device,
|
||
padding_side: str = "left",
|
||
scale_factor: int = 8,
|
||
eps: float = 1e-6,
|
||
) -> torch.Tensor:
|
||
batch_size, seq_len, hidden_dim, _ = text_hidden_states.shape
|
||
original_dtype = text_hidden_states.dtype
|
||
|
||
token_indices = torch.arange(seq_len, device=device).unsqueeze(0)
|
||
if padding_side == "right":
|
||
mask = token_indices < sequence_lengths[:, None]
|
||
elif padding_side == "left":
|
||
start_indices = seq_len - sequence_lengths[:, None]
|
||
mask = token_indices >= start_indices
|
||
else:
|
||
raise ValueError(f"padding_side must be 'left' or 'right', got {padding_side}")
|
||
mask = mask[:, :, None, None]
|
||
|
||
masked_text_hidden_states = text_hidden_states.masked_fill(~mask, 0.0)
|
||
num_valid_positions = (sequence_lengths * hidden_dim).view(batch_size, 1, 1, 1)
|
||
masked_mean = masked_text_hidden_states.sum(dim=(1, 2), keepdim=True) / (num_valid_positions + eps)
|
||
|
||
x_min = text_hidden_states.masked_fill(~mask, float("inf")).amin(dim=(1, 2), keepdim=True)
|
||
x_max = text_hidden_states.masked_fill(~mask, float("-inf")).amax(dim=(1, 2), keepdim=True)
|
||
|
||
normalized_hidden_states = (text_hidden_states - masked_mean) / (x_max - x_min + eps)
|
||
normalized_hidden_states = normalized_hidden_states * scale_factor
|
||
normalized_hidden_states = normalized_hidden_states.flatten(2)
|
||
mask_flat = mask.squeeze(-1).expand(-1, -1, normalized_hidden_states.shape[-1])
|
||
normalized_hidden_states = normalized_hidden_states.masked_fill(~mask_flat, 0.0)
|
||
return normalized_hidden_states.to(dtype=original_dtype)
|
||
|
||
|
||
def _pack_latents(latents: torch.Tensor, patch_size: int = 1, patch_size_t: int = 1) -> torch.Tensor:
|
||
batch_size, _, num_frames, height, width = latents.shape
|
||
post_patch_num_frames = num_frames // patch_size_t
|
||
post_patch_height = height // patch_size
|
||
post_patch_width = width // patch_size
|
||
latents = latents.reshape(
|
||
batch_size,
|
||
-1,
|
||
post_patch_num_frames,
|
||
patch_size_t,
|
||
post_patch_height,
|
||
patch_size,
|
||
post_patch_width,
|
||
patch_size,
|
||
)
|
||
latents = latents.permute(0, 2, 4, 6, 1, 3, 5, 7).flatten(4, 7).flatten(1, 3)
|
||
return latents
|
||
|
||
|
||
def _unpack_latents(
|
||
latents: torch.Tensor,
|
||
num_frames: int,
|
||
height: int,
|
||
width: int,
|
||
patch_size: int = 1,
|
||
patch_size_t: int = 1,
|
||
) -> torch.Tensor:
|
||
batch_size = latents.size(0)
|
||
latents = latents.reshape(batch_size, num_frames, height, width, -1, patch_size_t, patch_size, patch_size)
|
||
latents = latents.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3)
|
||
return latents
|
||
|
||
|
||
def _prepare_encoder_attention_mask(mask: torch.Tensor | None, dtype: torch.dtype) -> torch.Tensor | None:
|
||
if mask is None:
|
||
return None
|
||
if mask.ndim != 2:
|
||
return mask
|
||
if bool(torch.all(mask)):
|
||
return None
|
||
return ((1 - mask.to(dtype)) * -10000.0).unsqueeze(1)
|
||
|
||
|
||
def _resolve_kv_cache_storage_dtype() -> torch.dtype | None:
|
||
raw = os.environ.get("SANA_WM_REFINER_KV_CACHE_DTYPE", "").strip().lower()
|
||
if not raw or raw in {"bf16", "bfloat16", "none", "off", "0"}:
|
||
return None
|
||
if raw in {"fp8", "fp8_e4m3", "fp8_e4m3fn", "float8_e4m3fn", "e4m3"}:
|
||
return torch.float8_e4m3fn
|
||
if raw in {"fp8_e5m2", "float8_e5m2", "e5m2"}:
|
||
return torch.float8_e5m2
|
||
raise ValueError(f"Unsupported SANA_WM_REFINER_KV_CACHE_DTYPE={raw!r}.")
|
||
|
||
|
||
def _refiner_fast_kv_capture_mode() -> str:
|
||
raw = os.environ.get("SANA_WM_REFINER_FAST_KV_CAPTURE", "").strip().lower()
|
||
if not raw or raw in {"clean", "exact", "off", "0"}:
|
||
return "clean"
|
||
# Reuses K/V from the final denoise prediction. This avoids the extra
|
||
# post-refine capture forward, but the cached history is approximate.
|
||
if raw in {"last_predict", "reuse_last_predict", "final_predict"}:
|
||
return "last_predict"
|
||
# Hybrid mode: run exact post-capture only for
|
||
# SANA_WM_REFINER_EXACT_CAPTURE_LAYERS, and fill the remaining history
|
||
# layers from the final denoise prediction K/V. This is approximate only
|
||
# for layers outside the exact-capture mask.
|
||
if raw in {"fill_missing", "fill-missing", "hybrid_fill", "hybrid"}:
|
||
return "fill_missing"
|
||
raise ValueError(f"Unsupported SANA_WM_REFINER_FAST_KV_CAPTURE={raw!r}.")
|
||
|
||
|
||
def _refiner_fast_kv_needs_clean_block(block_idx: int) -> bool:
|
||
raw = os.environ.get("SANA_WM_REFINER_FAST_KV_CLEAN_INTERVAL", "").strip()
|
||
if not raw:
|
||
return False
|
||
interval = int(raw)
|
||
if interval <= 0:
|
||
return False
|
||
# Keep block 0 exact so the sink/first active history starts clean, then
|
||
# refresh periodically to bound drift in long videos.
|
||
return block_idx == 0 or ((block_idx + 1) % interval == 0)
|
||
|
||
|
||
def _refiner_history_layer_mask(n_layers: int) -> list[bool]:
|
||
raw_layers = os.environ.get("SANA_WM_REFINER_HISTORY_LAYERS", "").strip()
|
||
if raw_layers:
|
||
mask = [False] * int(n_layers)
|
||
for item in raw_layers.split(","):
|
||
item = item.strip()
|
||
if not item:
|
||
continue
|
||
if item.lower() == "last":
|
||
mask[-1] = True
|
||
continue
|
||
if "-" in item:
|
||
start_raw, end_raw = item.split("-", 1)
|
||
start = int(start_raw)
|
||
end = int(end_raw)
|
||
if start < 0:
|
||
start += n_layers
|
||
if end < 0:
|
||
end += n_layers
|
||
for idx in range(max(0, start), min(n_layers - 1, end) + 1):
|
||
mask[idx] = True
|
||
continue
|
||
idx = int(item)
|
||
if idx < 0:
|
||
idx += n_layers
|
||
if idx < 0 or idx >= n_layers:
|
||
raise ValueError(f"SANA_WM_REFINER_HISTORY_LAYERS index {item!r} outside 0..{n_layers - 1}.")
|
||
mask[idx] = True
|
||
if not any(mask):
|
||
raise ValueError("SANA_WM_REFINER_HISTORY_LAYERS selected no layers.")
|
||
return mask
|
||
|
||
stride_raw = os.environ.get("SANA_WM_REFINER_HISTORY_LAYER_STRIDE", "").strip()
|
||
if not stride_raw:
|
||
return [True] * int(n_layers)
|
||
stride = int(stride_raw)
|
||
if stride <= 1:
|
||
return [True] * int(n_layers)
|
||
offset = int(os.environ.get("SANA_WM_REFINER_HISTORY_LAYER_OFFSET", "0"))
|
||
mask = [((idx - offset) % stride == 0) for idx in range(int(n_layers))]
|
||
if _env_flag_default_true("SANA_WM_REFINER_HISTORY_KEEP_LAST"):
|
||
mask[-1] = True
|
||
if not any(mask):
|
||
mask[-1] = True
|
||
return mask
|
||
|
||
|
||
def _refiner_exact_capture_layer_mask(n_layers: int, *, default_mask: list[bool]) -> list[bool]:
|
||
raw_layers = os.environ.get("SANA_WM_REFINER_EXACT_CAPTURE_LAYERS", "").strip()
|
||
if not raw_layers:
|
||
return list(default_mask)
|
||
mask = [False] * int(n_layers)
|
||
for item in raw_layers.split(","):
|
||
item = item.strip()
|
||
if not item:
|
||
continue
|
||
if item.lower() == "last":
|
||
mask[-1] = True
|
||
continue
|
||
if "-" in item:
|
||
start_raw, end_raw = item.split("-", 1)
|
||
start = int(start_raw)
|
||
end = int(end_raw)
|
||
if start < 0:
|
||
start += n_layers
|
||
if end < 0:
|
||
end += n_layers
|
||
for idx in range(max(0, start), min(n_layers - 1, end) + 1):
|
||
mask[idx] = True
|
||
continue
|
||
idx = int(item)
|
||
if idx < 0:
|
||
idx += n_layers
|
||
if idx < 0 or idx >= n_layers:
|
||
raise ValueError(f"SANA_WM_REFINER_EXACT_CAPTURE_LAYERS index {item!r} outside 0..{n_layers - 1}.")
|
||
mask[idx] = True
|
||
if not any(mask):
|
||
raise ValueError("SANA_WM_REFINER_EXACT_CAPTURE_LAYERS selected no layers.")
|
||
return mask
|
||
|
||
|
||
def _refiner_empty_cache_before_capture() -> bool:
|
||
raw = os.environ.get("SANA_WM_REFINER_EMPTY_CACHE_BEFORE_CAPTURE", "1").strip().lower()
|
||
return raw not in {"0", "false", "no", "off"}
|
||
|
||
|
||
def _refiner_profile_enabled() -> bool:
|
||
return _env_flag("SANA_WM_REFINER_PROFILE")
|
||
|
||
|
||
def _refiner_layer_profile_enabled() -> bool:
|
||
return _env_flag("SANA_WM_REFINER_LAYER_PROFILE")
|
||
|
||
|
||
class _RefinerCudaProfiler:
|
||
"""Tiny env-gated CUDA event profiler for one refiner AR block."""
|
||
|
||
def __init__(self, *, enabled: bool, device: torch.device, block_idx: int) -> None:
|
||
self.enabled = bool(enabled and device.type == "cuda")
|
||
self.device = device
|
||
self.block_idx = int(block_idx)
|
||
self._events: list[tuple[str, torch.cuda.Event, torch.cuda.Event]] = []
|
||
self._block_start: torch.cuda.Event | None = None
|
||
self._block_end: torch.cuda.Event | None = None
|
||
if self.enabled:
|
||
stream = torch.cuda.current_stream(device)
|
||
self._block_start = torch.cuda.Event(enable_timing=True)
|
||
self._block_end = torch.cuda.Event(enable_timing=True)
|
||
self._block_start.record(stream)
|
||
|
||
def section(self, name: str):
|
||
if not self.enabled:
|
||
return nullcontext()
|
||
return _RefinerCudaProfileSection(self, name)
|
||
|
||
def _record_section(self, name: str, start: torch.cuda.Event, end: torch.cuda.Event) -> None:
|
||
self._events.append((name, start, end))
|
||
|
||
def finish(self) -> None:
|
||
if not self.enabled:
|
||
return
|
||
stream = torch.cuda.current_stream(self.device)
|
||
assert self._block_start is not None and self._block_end is not None
|
||
self._block_end.record(stream)
|
||
self._block_end.synchronize()
|
||
|
||
totals_ms: dict[str, float] = {}
|
||
counts: dict[str, int] = {}
|
||
for name, start, end in self._events:
|
||
elapsed_ms = float(start.elapsed_time(end))
|
||
totals_ms[name] = totals_ms.get(name, 0.0) + elapsed_ms
|
||
counts[name] = counts.get(name, 0) + 1
|
||
|
||
block_total_ms = float(self._block_start.elapsed_time(self._block_end))
|
||
parts = [f"block_total={block_total_ms / 1000.0:.6f}s"]
|
||
for name, elapsed_ms in totals_ms.items():
|
||
count_suffix = f"x{counts[name]}" if counts[name] != 1 else ""
|
||
parts.append(f"{name}={elapsed_ms / 1000.0:.6f}s{count_suffix}")
|
||
print(f"[refiner-profile] block={self.block_idx} " + " ".join(parts), flush=True)
|
||
|
||
|
||
class _RefinerCudaProfileSection:
|
||
def __init__(self, profiler: _RefinerCudaProfiler, name: str) -> None:
|
||
self._profiler = profiler
|
||
self._name = str(name)
|
||
self._start: torch.cuda.Event | None = None
|
||
self._end: torch.cuda.Event | None = None
|
||
|
||
def __enter__(self):
|
||
stream = torch.cuda.current_stream(self._profiler.device)
|
||
self._start = torch.cuda.Event(enable_timing=True)
|
||
self._end = torch.cuda.Event(enable_timing=True)
|
||
self._start.record(stream)
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc, tb) -> bool:
|
||
assert self._start is not None and self._end is not None
|
||
stream = torch.cuda.current_stream(self._profiler.device)
|
||
self._end.record(stream)
|
||
self._profiler._record_section(self._name, self._start, self._end)
|
||
return False
|
||
|
||
|
||
def _current_refiner_prefix_tokens(transformer: nn.Module) -> int:
|
||
blocks = getattr(transformer, "transformer_blocks", None)
|
||
if not blocks:
|
||
return 0
|
||
prefix = getattr(blocks[0].attn1, "_tf_kv_prefix", None)
|
||
if not isinstance(prefix, dict):
|
||
return 0
|
||
prefix_k = prefix.get("prefix_k")
|
||
if isinstance(prefix_k, torch.Tensor):
|
||
return int(prefix_k.shape[1])
|
||
total = 0
|
||
sink_k_pre = prefix.get("sink_k_pre")
|
||
if isinstance(sink_k_pre, torch.Tensor):
|
||
total += int(sink_k_pre.shape[1])
|
||
history_k = prefix.get("history_k")
|
||
if isinstance(history_k, torch.Tensor):
|
||
total += int(history_k.shape[1])
|
||
return total
|
||
|
||
|
||
def _profile_section(profiler: _RefinerLayerCudaProfiler | None, name: str):
|
||
if profiler is None:
|
||
return nullcontext()
|
||
return profiler.section(name)
|
||
|
||
|
||
class _RefinerLayerCudaProfiler:
|
||
"""Env-gated CUDA event profiler for one transformer forward."""
|
||
|
||
def __init__(self, *, enabled: bool, device: torch.device, label: str) -> None:
|
||
self.enabled = bool(enabled and device.type == "cuda")
|
||
self.device = device
|
||
self.label = str(label)
|
||
self._events: list[tuple[str, torch.cuda.Event, torch.cuda.Event]] = []
|
||
self._start: torch.cuda.Event | None = None
|
||
self._end: torch.cuda.Event | None = None
|
||
if self.enabled:
|
||
stream = torch.cuda.current_stream(device)
|
||
self._start = torch.cuda.Event(enable_timing=True)
|
||
self._end = torch.cuda.Event(enable_timing=True)
|
||
self._start.record(stream)
|
||
|
||
def section(self, name: str):
|
||
if not self.enabled:
|
||
return nullcontext()
|
||
return _RefinerLayerCudaProfileSection(self, name)
|
||
|
||
def _record_section(self, name: str, start: torch.cuda.Event, end: torch.cuda.Event) -> None:
|
||
self._events.append((name, start, end))
|
||
|
||
def finish(self) -> None:
|
||
if not self.enabled:
|
||
return
|
||
stream = torch.cuda.current_stream(self.device)
|
||
assert self._start is not None and self._end is not None
|
||
self._end.record(stream)
|
||
self._end.synchronize()
|
||
totals_ms: dict[str, float] = {}
|
||
counts: dict[str, int] = {}
|
||
for name, start, end in self._events:
|
||
elapsed_ms = float(start.elapsed_time(end))
|
||
totals_ms[name] = totals_ms.get(name, 0.0) + elapsed_ms
|
||
counts[name] = counts.get(name, 0) + 1
|
||
total_ms = float(self._start.elapsed_time(self._end))
|
||
parts = [f"total={total_ms / 1000.0:.6f}s"]
|
||
for name, elapsed_ms in totals_ms.items():
|
||
count_suffix = f"x{counts[name]}" if counts[name] != 1 else ""
|
||
parts.append(f"{name}={elapsed_ms / 1000.0:.6f}s{count_suffix}")
|
||
print(f"[refiner-layer-profile] {self.label} " + " ".join(parts), flush=True)
|
||
|
||
|
||
class _RefinerLayerCudaProfileSection:
|
||
def __init__(self, profiler: _RefinerLayerCudaProfiler, name: str) -> None:
|
||
self._profiler = profiler
|
||
self._name = str(name)
|
||
self._start: torch.cuda.Event | None = None
|
||
self._end: torch.cuda.Event | None = None
|
||
|
||
def __enter__(self):
|
||
stream = torch.cuda.current_stream(self._profiler.device)
|
||
self._start = torch.cuda.Event(enable_timing=True)
|
||
self._end = torch.cuda.Event(enable_timing=True)
|
||
self._start.record(stream)
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc, tb) -> bool:
|
||
assert self._start is not None and self._end is not None
|
||
stream = torch.cuda.current_stream(self._profiler.device)
|
||
self._end.record(stream)
|
||
self._profiler._record_section(self._name, self._start, self._end)
|
||
return False
|
||
|
||
|
||
def _store_kv_tensor(tensor: torch.Tensor, dtype: torch.dtype | None) -> torch.Tensor:
|
||
if dtype is None:
|
||
return tensor
|
||
return tensor.to(dtype)
|
||
|
||
|
||
def _store_kv_pair(
|
||
pair: tuple[torch.Tensor, torch.Tensor],
|
||
dtype: torch.dtype | None,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
return (_store_kv_tensor(pair[0], dtype), _store_kv_tensor(pair[1], dtype))
|
||
|
||
|
||
def _capture_kv_tensor(tensor: torch.Tensor) -> torch.Tensor:
|
||
captured = tensor.detach()
|
||
if _env_flag("SANA_WM_REFINER_NO_CLONE_CAPTURED_KV"):
|
||
return captured
|
||
return captured.clone()
|
||
|
||
|
||
def _env_tuple(name: str) -> tuple[str, ...]:
|
||
raw = os.environ.get(name, "")
|
||
return tuple(item.strip() for item in raw.split(",") if item.strip())
|
||
|
||
|
||
def _env_flag(name: str) -> bool:
|
||
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
|
||
|
||
|
||
def _env_flag_default_true(name: str) -> bool:
|
||
return os.environ.get(name, "1").strip().lower() not in {"", "0", "false", "no", "off"}
|
||
|
||
|
||
def _prepared_module_cache_root() -> Path | None:
|
||
if os.environ.get("SANA_WM_PREPARED_MODULE_CACHE", "").strip().lower() not in {"1", "true", "yes", "on"}:
|
||
return None
|
||
root = os.environ.get("SANA_WM_PREPARED_MODULE_CACHE_DIR", "").strip()
|
||
return Path(root).expanduser() if root else Path.home() / ".cache" / "sana_wm_prepared_modules"
|
||
|
||
|
||
def _prepared_module_cache_hash(payload: dict[str, object]) -> str:
|
||
blob = json.dumps(payload, sort_keys=True, default=str, separators=(",", ":")).encode("utf-8")
|
||
return hashlib.sha256(blob).hexdigest()[:20]
|
||
|
||
|
||
def _path_fingerprint(path: str | Path) -> dict[str, object]:
|
||
raw = str(path)
|
||
try:
|
||
resolved = Path(raw).expanduser().resolve()
|
||
except Exception:
|
||
return {"path": raw}
|
||
if resolved.is_dir():
|
||
markers = []
|
||
for rel in ("config.json", "diffusion_pytorch_model.safetensors", "model.safetensors"):
|
||
item = resolved / rel
|
||
try:
|
||
stat = item.stat()
|
||
except OSError:
|
||
continue
|
||
markers.append((rel, int(stat.st_size), int(stat.st_mtime_ns)))
|
||
return {"path": str(resolved), "markers": markers}
|
||
try:
|
||
stat = resolved.stat()
|
||
except OSError:
|
||
return {"path": str(resolved)}
|
||
return {"path": str(resolved), "size": int(stat.st_size), "mtime_ns": int(stat.st_mtime_ns)}
|
||
|
||
|
||
def _is_local_callable_for_pickle(value: object) -> bool:
|
||
if isinstance(value, types.MethodType):
|
||
value = value.__func__
|
||
if not isinstance(value, types.FunctionType):
|
||
return False
|
||
qualname = getattr(value, "__qualname__", "")
|
||
return "<locals>" in qualname or getattr(value, "__name__", "") == "<lambda>"
|
||
|
||
|
||
def _strip_local_callables_for_pickle(root: object) -> list[tuple[object, object, object, str]]:
|
||
"""Temporarily remove TE init closures that are not used after construction."""
|
||
|
||
restore: list[tuple[object, object, object, str]] = []
|
||
seen: set[int] = set()
|
||
leaf_types = (str, bytes, int, float, bool, type(None), Path, torch.device, torch.dtype)
|
||
|
||
def set_value(owner: object, key: object, old_value: object, new_value: object, kind: str) -> None:
|
||
if kind == "dict":
|
||
owner[key] = new_value
|
||
elif kind == "list":
|
||
owner[key] = new_value
|
||
else:
|
||
setattr(owner, str(key), new_value)
|
||
restore.append((owner, key, old_value, kind))
|
||
|
||
def scrub_value(value: object) -> tuple[object, bool]:
|
||
if _is_local_callable_for_pickle(value):
|
||
return None, True
|
||
if hasattr(value, "_replace") and hasattr(value, "init_fn"):
|
||
updates = {}
|
||
if _is_local_callable_for_pickle(getattr(value, "init_fn", None)):
|
||
updates["init_fn"] = None
|
||
if _is_local_callable_for_pickle(getattr(value, "get_rng_state_tracker", None)):
|
||
updates["get_rng_state_tracker"] = None
|
||
if updates:
|
||
return value._replace(**updates), True
|
||
return value, False
|
||
|
||
def walk(obj: object) -> None:
|
||
if isinstance(obj, leaf_types) or isinstance(obj, (torch.Tensor, nn.Parameter)):
|
||
return
|
||
obj_id = id(obj)
|
||
if obj_id in seen:
|
||
return
|
||
seen.add(obj_id)
|
||
|
||
if isinstance(obj, dict):
|
||
for key, value in list(obj.items()):
|
||
new_value, changed = scrub_value(value)
|
||
if changed:
|
||
set_value(obj, key, value, new_value, "dict")
|
||
else:
|
||
walk(value)
|
||
return
|
||
if isinstance(obj, list):
|
||
for index, value in enumerate(list(obj)):
|
||
new_value, changed = scrub_value(value)
|
||
if changed:
|
||
set_value(obj, index, value, new_value, "list")
|
||
else:
|
||
walk(value)
|
||
return
|
||
if isinstance(obj, tuple):
|
||
return
|
||
|
||
try:
|
||
items = list(vars(obj).items())
|
||
except TypeError:
|
||
return
|
||
for key, value in items:
|
||
if key.startswith("__"):
|
||
continue
|
||
new_value, changed = scrub_value(value)
|
||
if changed:
|
||
set_value(obj, key, value, new_value, "attr")
|
||
elif key not in {"_parameters", "_buffers"}:
|
||
walk(value)
|
||
|
||
walk(root)
|
||
return restore
|
||
|
||
|
||
def _restore_stripped_pickle_values(restore: list[tuple[object, object, object, str]]) -> None:
|
||
for owner, key, value, kind in reversed(restore):
|
||
if kind == "dict":
|
||
owner[key] = value
|
||
elif kind == "list":
|
||
owner[key] = value
|
||
else:
|
||
setattr(owner, str(key), value)
|
||
|
||
|
||
def _te_module_name_variants(name: str) -> tuple[str, ...]:
|
||
if not _env_flag("SANA_WM_TE_NVFP4_NORMALIZE_MODULE_NAMES"):
|
||
return (name,)
|
||
variants = {name}
|
||
stripped = name
|
||
while stripped.startswith("_orig_mod."):
|
||
stripped = stripped[len("_orig_mod.") :]
|
||
variants.add(stripped)
|
||
variants.add(name.replace("._orig_mod.", "."))
|
||
variants.add(name.replace("_orig_mod.", ""))
|
||
return tuple(dict.fromkeys(item for item in variants if item))
|
||
|
||
|
||
def _te_name_matches(patterns: tuple[str, ...], name: str) -> bool:
|
||
return any(re.search(pattern, candidate) for pattern in patterns for candidate in _te_module_name_variants(name))
|
||
|
||
|
||
class _FusedQKVLinear(nn.Module):
|
||
def __init__(self, to_q: nn.Linear, to_k: nn.Linear, to_v: nn.Linear) -> None:
|
||
super().__init__()
|
||
if to_q.in_features != to_k.in_features or to_q.in_features != to_v.in_features:
|
||
raise ValueError("Cannot fuse QKV with mismatched input dimensions.")
|
||
device = to_q.weight.device
|
||
dtype = to_q.weight.dtype
|
||
out_features = to_q.out_features + to_k.out_features + to_v.out_features
|
||
use_bias = to_q.bias is not None or to_k.bias is not None or to_v.bias is not None
|
||
self.linear = nn.Linear(to_q.in_features, out_features, bias=use_bias, device=device, dtype=dtype)
|
||
self._splits = (to_q.out_features, to_k.out_features, to_v.out_features)
|
||
with torch.no_grad():
|
||
self.linear.weight.copy_(torch.cat([to_q.weight, to_k.weight, to_v.weight], dim=0))
|
||
if self.linear.bias is not None:
|
||
bias_parts = []
|
||
for src in (to_q, to_k, to_v):
|
||
if src.bias is None:
|
||
bias_parts.append(torch.zeros(src.out_features, device=device, dtype=dtype))
|
||
else:
|
||
bias_parts.append(src.bias)
|
||
self.linear.bias.copy_(torch.cat(bias_parts, dim=0))
|
||
|
||
def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||
return self.linear(hidden_states).split(self._splits, dim=-1)
|
||
|
||
|
||
def _fuse_refiner_self_qkv(transformer: nn.Module) -> int:
|
||
converted = 0
|
||
for block in getattr(transformer, "transformer_blocks", ()):
|
||
attn = getattr(block, "attn1", None)
|
||
if attn is None or getattr(attn, "_sana_fused_qkv", None) is not None:
|
||
continue
|
||
to_q = getattr(attn, "to_q", None)
|
||
to_k = getattr(attn, "to_k", None)
|
||
to_v = getattr(attn, "to_v", None)
|
||
if not all(isinstance(module, nn.Linear) for module in (to_q, to_k, to_v)):
|
||
continue
|
||
fused = _FusedQKVLinear(to_q, to_k, to_v)
|
||
fused.train(bool(to_q.training or to_k.training or to_v.training))
|
||
attn._sana_fused_qkv = fused
|
||
attn.to_q = nn.Identity()
|
||
attn.to_k = nn.Identity()
|
||
attn.to_v = nn.Identity()
|
||
converted += 1
|
||
return converted
|
||
|
||
|
||
def _replace_linear_with_te_nvfp4(
|
||
module: nn.Module,
|
||
*,
|
||
recipe,
|
||
params_dtype: torch.dtype,
|
||
skip_patterns: tuple[str, ...],
|
||
include_patterns: tuple[str, ...] | None = None,
|
||
prefix: str = "",
|
||
) -> tuple[int, int]:
|
||
import transformer_engine.pytorch as te
|
||
|
||
converted = 0
|
||
skipped = 0
|
||
for name, child in list(module.named_children()):
|
||
child_prefix = f"{prefix}.{name}" if prefix else name
|
||
if _te_name_matches(skip_patterns, child_prefix):
|
||
skipped += 1
|
||
continue
|
||
if isinstance(child, nn.Linear):
|
||
if include_patterns is not None and not _te_name_matches(include_patterns, child_prefix):
|
||
skipped += 1
|
||
continue
|
||
if child.in_features % 16 != 0 or child.out_features % 16 != 0:
|
||
skipped += 1
|
||
continue
|
||
use_cpu_staging = _env_flag("SANA_WM_TE_NVFP4_CPU_STAGING")
|
||
child_training = child.training
|
||
has_bias = child.bias is not None
|
||
params_dtype_for_replacement = (
|
||
child.weight.dtype
|
||
if child.weight.dtype in {torch.float16, torch.bfloat16, torch.float32}
|
||
else params_dtype
|
||
)
|
||
if use_cpu_staging:
|
||
old_weight = child.weight.detach().to("cpu", copy=True)
|
||
old_bias = child.bias.detach().to("cpu", copy=True) if child.bias is not None else None
|
||
setattr(module, name, nn.Identity())
|
||
del child
|
||
gc.collect()
|
||
_empty_cuda_cache()
|
||
else:
|
||
old_weight = child.weight.detach()
|
||
old_bias = child.bias.detach() if child.bias is not None else None
|
||
try:
|
||
ctx = te.fp8_model_init(
|
||
enabled=True,
|
||
recipe=recipe,
|
||
preserve_high_precision_init_val=False,
|
||
)
|
||
except TypeError:
|
||
ctx = te.fp8_model_init(enabled=True, recipe=recipe)
|
||
with ctx:
|
||
replacement = te.Linear(
|
||
old_weight.shape[1],
|
||
old_weight.shape[0],
|
||
bias=has_bias,
|
||
params_dtype=params_dtype_for_replacement,
|
||
device=str(torch.device("cuda", torch.cuda.current_device())),
|
||
)
|
||
replacement.train(child_training)
|
||
with torch.no_grad():
|
||
replacement.weight.copy_(old_weight.to(device=replacement.weight.device))
|
||
if old_bias is not None:
|
||
replacement.bias.copy_(old_bias.to(device=replacement.bias.device))
|
||
if use_cpu_staging:
|
||
del old_weight, old_bias
|
||
_empty_cuda_cache()
|
||
setattr(module, name, replacement)
|
||
converted += 1
|
||
continue
|
||
child_converted, child_skipped = _replace_linear_with_te_nvfp4(
|
||
child,
|
||
recipe=recipe,
|
||
params_dtype=params_dtype,
|
||
skip_patterns=skip_patterns,
|
||
include_patterns=include_patterns,
|
||
prefix=child_prefix,
|
||
)
|
||
converted += child_converted
|
||
skipped += child_skipped
|
||
return converted, skipped
|
||
|
||
|
||
def _offload_video_unused_audio_modules(transformer: nn.Module, device: torch.device | str) -> None:
|
||
for name in (
|
||
"audio_proj_in",
|
||
"audio_caption_projection",
|
||
"audio_time_embed",
|
||
"av_cross_attn_video_scale_shift",
|
||
"av_cross_attn_audio_scale_shift",
|
||
"av_cross_attn_video_a2v_gate",
|
||
"av_cross_attn_audio_v2a_gate",
|
||
"audio_rope",
|
||
"cross_attn_rope",
|
||
"cross_attn_audio_rope",
|
||
"audio_norm_out",
|
||
"audio_proj_out",
|
||
):
|
||
child = getattr(transformer, name, None)
|
||
if isinstance(child, nn.Module):
|
||
child.to(device)
|
||
for block in getattr(transformer, "transformer_blocks", ()):
|
||
for name in (
|
||
"audio_norm1",
|
||
"audio_attn1",
|
||
"audio_norm2",
|
||
"audio_attn2",
|
||
"audio_to_video_norm",
|
||
"audio_to_video_attn",
|
||
"video_to_audio_norm",
|
||
"video_to_audio_attn",
|
||
"audio_norm3",
|
||
"audio_ff",
|
||
):
|
||
child = getattr(block, name, None)
|
||
if isinstance(child, nn.Module):
|
||
child.to(device)
|
||
|
||
|
||
def _move_ltx2_video_modules_to(transformer: nn.Module, device: torch.device | str) -> None:
|
||
for name in ("proj_in", "caption_projection", "time_embed", "rope", "norm_out", "proj_out"):
|
||
child = getattr(transformer, name, None)
|
||
if isinstance(child, nn.Module):
|
||
child.to(device)
|
||
_move_tensor_attr(transformer, "scale_shift_table", device)
|
||
for block in getattr(transformer, "transformer_blocks", ()):
|
||
_move_tensor_attr(block, "scale_shift_table", device)
|
||
for name in ("norm1", "attn1", "norm2", "attn2", "norm3", "ff"):
|
||
child = getattr(block, name, None)
|
||
if isinstance(child, nn.Module):
|
||
child.to(device)
|
||
|
||
|
||
def _move_tensor_attr(module: nn.Module, name: str, device: torch.device | str) -> None:
|
||
value = getattr(module, name, None)
|
||
if isinstance(value, nn.Parameter):
|
||
if value.device != torch.device(device):
|
||
setattr(module, name, nn.Parameter(value.to(device), requires_grad=value.requires_grad))
|
||
elif isinstance(value, torch.Tensor) and value.device != torch.device(device):
|
||
setattr(module, name, value.to(device))
|
||
|
||
|
||
def _empty_cuda_cache() -> None:
|
||
if torch.cuda.is_available():
|
||
torch.cuda.empty_cache()
|
||
gc.collect()
|