multimodalart's picture
multimodalart HF Staff
Initial Uranus demo
f110cb1 verified
Raw History Blame Contribute Delete
4.53 kB
"""
Functional API for VAE decoding of video latents to pixel frames.
"""
import inspect
import torch
from einops import rearrange
from uranus.modules.vae import VideoVAE38_, WanVideoVAE, count_conv3d, unpatchify
def decode_latents(
vae: WanVideoVAE,
latents: torch.Tensor, # [B, N_CAM, z_dim, F_lat, H_lat, W_lat]
device: torch.device,
tiled: bool = True,
tile_size: tuple[int, int] = (34, 34),
tile_stride: tuple[int, int] = (18, 16),
) -> torch.Tensor:
"""
VAE-decode latent frames to video frames.
VAE temporal expansion:
- 1st latent frame → 1 video frame
- subsequent latent frames → 4 video frames each
Args:
vae: WanVideoVAE instance
latents: Video latents [B, N_CAM, z_dim, F_lat, H_lat, W_lat]
device: Target device
tiled: Whether to use tiled VAE decoding
tile_size: Tile size in latent space
tile_stride: Tile stride in latent space
Returns:
Video frames [B, N_CAM, 3, F_video, H_pix, W_pix] in [-1, 1]
"""
batch, n_cam, _, _, _, _ = latents.shape
latents_flat = rearrange(latents, "B N_CAM C F H W -> (B N_CAM) C F H W")
videos = vae.decode(
latents_flat,
device=device,
tiled=tiled,
tile_size=tile_size,
tile_stride=tile_stride,
)
videos = rearrange(
videos, "(B N_CAM) C F H W -> B N_CAM C F H W", B=batch, N_CAM=n_cam
)
return videos.clamp_(-1, 1)
def _apply_vae_scale(z: torch.Tensor, vae: WanVideoVAE) -> torch.Tensor:
scale = vae.scale
if isinstance(scale[0], torch.Tensor):
scale = [s.to(dtype=z.dtype, device=z.device) for s in scale]
return z / scale[1].view(1, vae.z_dim, 1, 1, 1) + scale[0].view(
1, vae.z_dim, 1, 1, 1
)
scale = scale.to(dtype=z.dtype, device=z.device)
return z / scale[1] + scale[0]
def _decode_single_chunk(
vae: WanVideoVAE,
latents: torch.Tensor,
dec_feat_map,
decoded_latent_frames: int,
) -> tuple[torch.Tensor, list, list]:
model = vae.model
z = _apply_vae_scale(latents, vae)
x = model.conv2(z)
# Flash-VAED decoder exposes its own cache-slot count (its causal convs
# are depthwise-separable); fall back to the legacy count otherwise.
count_fn = getattr(model.decoder, "count_conv3d", None) or count_conv3d
if dec_feat_map is None:
dec_feat_map = [None] * count_fn(model.decoder)
decoder_signature = inspect.signature(model.decoder.forward)
supports_first_chunk = "first_chunk" in decoder_signature.parameters
outputs = []
for frame_idx in range(x.shape[2]):
feat_idx = [0]
decoder_kwargs = {
"feat_cache": dec_feat_map,
"feat_idx": feat_idx,
}
if supports_first_chunk:
decoder_kwargs["first_chunk"] = (
decoded_latent_frames == 0 and frame_idx == 0
)
out, dec_feat_map, feat_idx = model.decoder(
x[:, :, frame_idx : frame_idx + 1], **decoder_kwargs
)
outputs.append(out)
video = torch.cat(outputs, dim=2) if len(outputs) > 1 else outputs[0]
if isinstance(model, VideoVAE38_):
video = unpatchify(video, patch_size=2)
return video, dec_feat_map, feat_idx
def decode_latents_chunk(
vae: WanVideoVAE,
latents: torch.Tensor,
device: torch.device,
dec_feat_map=None,
dec_feat_idx: list | None = None,
decoded_latent_frames: int = 0,
tiled: bool = False,
tile_size: tuple[int, int] = (34, 34),
tile_stride: tuple[int, int] = (18, 16),
) -> tuple[torch.Tensor, list, list, int]:
"""
Incrementally decode a latent chunk while reusing decoder feature cache.
This mirrors `VideoVAE_.decode()` / `VideoVAE38_.decode()` but externalizes
decoder cache so streaming callers can carry it in their own state.
"""
del tiled, tile_size, tile_stride
batch, n_cam, _, f_lat, _, _ = latents.shape
latents_flat = rearrange(latents, "B N_CAM C F H W -> (B N_CAM) C F H W").to(
device=device
)
video, dec_feat_map, dec_feat_idx = _decode_single_chunk(
vae=vae,
latents=latents_flat,
dec_feat_map=dec_feat_map,
decoded_latent_frames=decoded_latent_frames,
)
videos = rearrange(
video, "(B N_CAM) C F H W -> B N_CAM C F H W", B=batch, N_CAM=n_cam
)
return (
videos.clamp_(-1, 1),
dec_feat_map,
dec_feat_idx,
decoded_latent_frames + f_lat,
)