Files
vllm-omni/vllm_omni/diffusion/distributed/autoencoders/wan_decoder_utils.py
T

115 lines
5.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Utility functions for the streaming Wan decoder fast path."""
from __future__ import annotations
import torch
from torch import nn
from vllm_omni.diffusion.distributed.autoencoders.wan_decoder_kernels import (
can_use_nearest_upsample_nhwc,
canonical_nhwc,
nearest_upsample_nhwc,
)
_CACHE_T = 2 # diffusers.models.autoencoders.autoencoder_kl_wan.CACHE_T
def _upsample_forward(self: nn.Upsample, x: torch.Tensor) -> torch.Tensor:
# ``WanUpsample.forward`` is ``super().forward(x.float()).type_as(x)``; a nearest gather never changes a
# value, so running it on ``x`` itself is the same tensor without the two casts.
return nn.Upsample.forward(self, x)
def _is_nearest_upsample(module: nn.Module) -> bool:
return (
module.__class__.__name__ == "WanUpsample"
and isinstance(module, nn.Upsample)
and str(getattr(module, "mode", "")) in ("nearest", "nearest-exact")
)
def _upsample_forward_channels_last(self: nn.Upsample, x: torch.Tensor) -> torch.Tensor:
if x.dim() == 4 and self.size is None and can_use_nearest_upsample_nhwc(x, self.scale_factor, self.mode):
return nearest_upsample_nhwc(x, self.scale_factor)
return nn.Upsample.forward(self, x)
def _dense_channels_last_3d(x: torch.Tensor) -> bool:
if x.dim() != 5 or x.shape[1] <= 1:
return False
b, c, t, h, w = x.shape
return x.stride() == (t * h * w * c, 1, h * w * c, w * c, c)
def _interleave_time_pairs(x: torch.Tensor, b: int, c: int, t: int, h: int, w: int) -> torch.Tensor:
"""``[B, 2C, T, H, W] -> [B, C, 2T, H, W]``: the time-conv's two channel halves interleaved along time.
Same values as the eager ``reshape / stack / reshape``; on a dense channels_last_3d input the result is
written channels_last_3d with one copy instead of materialising it channels-first.
"""
if _dense_channels_last_3d(x):
out = torch.empty((b, c, t * 2, h, w), device=x.device, dtype=x.dtype, memory_format=torch.channels_last_3d)
out.view(b, c, t, 2, h, w).copy_(x.view(b, 2, c, t, h, w).permute(0, 2, 3, 1, 4, 5))
return out
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0], x[:, 1]), 3)
return x.reshape(b, c, t * 2, h, w)
def _frames_nhwc(x: torch.Tensor) -> torch.Tensor:
"""``[B, C, T, H, W] -> [B*T, C, H, W]`` for the 2D resample, with canonical NHWC strides when the data is
laid out that way (a size-1 batch leaves a degenerate stride that aten's layout test rejects)."""
b, c, t, h, w = x.shape
frames = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
if frames.is_contiguous(memory_format=torch.channels_last) and not canonical_nhwc(frames) and frames.stride(1) == 1:
frames = frames.as_strided((b * t, c, h, w), (h * w * c, 1, w * c, c))
return frames
def _resample_forward_channels_last(self: nn.Module, x: torch.Tensor, feat_cache=None, feat_idx=[0]) -> torch.Tensor: # noqa: B006
"""``WanResample.forward`` for the upsample modes with the layout-preserving interleave and frame view."""
b, c, t, h, w = x.size()
if self.mode == "upsample3d" and feat_cache is not None:
idx = feat_idx[0]
if feat_cache[idx] is None:
feat_cache[idx] = "Rep"
feat_idx[0] += 1
else:
cache_x = x[:, :, -_CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx] != "Rep":
cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx] == "Rep":
cache_x = torch.cat([torch.zeros_like(cache_x).to(cache_x.device), cache_x], dim=2)
if feat_cache[idx] == "Rep":
x = self.time_conv(x)
else:
x = self.time_conv(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
x = _interleave_time_pairs(x, b, c, t, h, w)
t = x.shape[2]
frames = self.resample(_frames_nhwc(x))
return frames.view(b, t, frames.size(1), frames.size(2), frames.size(3)).permute(0, 2, 1, 3, 4)
def _persistent_input_buffer(module: nn.Module, shape: tuple[int, ...], reference: torch.Tensor) -> torch.Tensor:
"""One zero-initialised input buffer per conv, reallocated only when the shape, dtype or device changes.
The callers write only the activation interior and the halo slots, so the padding rows and columns stay
zero for the buffer's lifetime and are never filled again.
"""
memory_format = getattr(module, "input_memory_format", torch.contiguous_format)
buf = module._input_buf
if (
buf is None
or buf.shape != shape
or buf.dtype != reference.dtype
or buf.device != reference.device
or not buf.is_contiguous(memory_format=memory_format)
):
buf = torch.empty(shape, dtype=reference.dtype, device=reference.device, memory_format=memory_format).zero_()
module._input_buf = buf
return buf