项目文件夹

文件
2026-07-13 13:09:03 +08:00

2631 行
111 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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()