Signed-off-by: tzhouam <tzhouam@connect.ust.hk> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
193 lines
10 KiB
Python
193 lines
10 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""Exact fast path for the Wan VAE decoder on a streaming (frame-at-a-time) decode.
|
|
|
|
A kernel ledger of the LingBot-World served path (4xH200, 480x832, width-sharded bf16 decode) put the
|
|
decoder at 91 ms of GPU time per chunk per rank, half of it in ~2,300 launches of glue around the
|
|
convolutions: autocast re-casting every conv weight and bias on every call (the autocast weight cache only
|
|
serves leaves that require grad, so under ``torch.no_grad`` it never hits), the nearest upsample's fp32
|
|
round trip, and a freshly allocated, zero-filled, concatenated input tensor per conv call. None of that
|
|
touches a value that reaches the convolution, so it can go without changing a single output bit:
|
|
|
|
- ``conv_dtype``: the decoder's convolution parameters are cast once to the dtype the decode already runs
|
|
under (``decode_autocast_dtype``), the same cast autocast applied per call. Norm parameters stay in their
|
|
own dtype, so the normalisation arithmetic and its fp32 promotion are untouched.
|
|
- ``WanUpsample`` runs its nearest-exact gather on the activation dtype directly instead of via
|
|
``x.float()`` and ``type_as``: a gather moves values, so the result is identical.
|
|
- The spatially sharded conv wrappers keep one input buffer per conv across calls
|
|
(``reuse_input_buffer``), writing only the activation interior and the halo slots into it.
|
|
|
|
Numerics: bit-identical to the plain path (``tests/diffusion/distributed/test_wan_decoder_fast_path.py``).
|
|
Memory: one persistent input buffer per sharded conv (about the size of that conv's input).
|
|
|
|
Level ``"fused"`` adds the rest of what the ledger showed, at the cost of exactness (it is gated with the bf16
|
|
decode option, whose quality gate it shares): the decoder runs channels_last_3d end to end so cuDNN uses its
|
|
NHWC kernels without the NCHW<->NHWC transposes it otherwise inserts around every convolution, every
|
|
``WanRMS_norm -> SiLU`` chain becomes one Triton kernel that reads and writes the activation dtype (the eager
|
|
chain promotes to fp32 at ``* gamma`` and leaves the cast to the next convolution), the nearest upsample is a
|
|
channels_last gather, and the ``upsample3d`` time-pair interleave writes channels_last_3d directly. Values
|
|
differ from eager only through the norm's fp32 reduction order and the single rounding at the SiLU output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import MethodType
|
|
from typing import Any
|
|
|
|
import torch
|
|
from torch import nn
|
|
from vllm.logger import init_logger
|
|
|
|
from vllm_omni.diffusion.distributed.autoencoders.wan_decoder_kernels import (
|
|
can_use_wan_rmsnorm_silu,
|
|
wan_rmsnorm_silu,
|
|
wan_rmsnorm_silu_reference,
|
|
)
|
|
from vllm_omni.diffusion.distributed.autoencoders.wan_decoder_utils import (
|
|
_dense_channels_last_3d,
|
|
_is_nearest_upsample,
|
|
_resample_forward_channels_last,
|
|
_upsample_forward,
|
|
_upsample_forward_channels_last,
|
|
)
|
|
from vllm_omni.diffusion.distributed.autoencoders.wan_spatial_shard import WanDistCausalConv3d, WanDistConv2d
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
_INSTALLED_ATTR = "_vllm_omni_wan_decoder_fast_path"
|
|
LEVELS = ("exact", "fused")
|
|
# diffusers' WanRMS_norm, and vLLM-Omni's RMSNormVAE that patch_wan_rms_norm swaps in for every Wan-family VAE.
|
|
_NORM_CLASSES = ("WanRMS_norm", "RMSNormVAE")
|
|
|
|
|
|
class FusedWanRMSNormSiLU(nn.Module):
|
|
"""``WanRMS_norm`` followed by SiLU as one kernel on channels_last_3d CUDA tensors.
|
|
|
|
Keeps the norm's parameters registered under their own names (``...norm1.gamma``), so state dicts and
|
|
weight loading are unchanged. Off the kernel's domain (CPU, other layouts, grad) it runs the eager op
|
|
chain, and a channels-first CUDA input is converted to channels_last_3d once (the attention block's
|
|
gathered frame is the only such producer).
|
|
"""
|
|
|
|
def __init__(self, norm: nn.Module) -> None:
|
|
super().__init__()
|
|
self.gamma = norm.gamma
|
|
bias = norm.bias
|
|
if isinstance(bias, torch.Tensor):
|
|
self.bias = bias
|
|
self._bias_value = 0.0
|
|
else:
|
|
self.bias = None
|
|
self._bias_value = float(bias) if bias is not None else 0.0
|
|
self.scale = float(norm.scale)
|
|
# diffusers' WanRMS_norm normalises x.float() with F.normalize's default eps; vLLM-Omni's RMSNormVAE
|
|
# (patched in for every Wan-family VAE it loads) normalises x itself with its own epsilon.
|
|
self.upcast = norm.__class__.__name__ == "WanRMS_norm"
|
|
self.eps = 1e-12 if self.upcast else float(getattr(norm, "epsilon", 1e-6))
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
if x.dim() == 5 and x.is_cuda and not (torch.is_grad_enabled() and x.requires_grad):
|
|
if not _dense_channels_last_3d(x):
|
|
x = x.contiguous(memory_format=torch.channels_last_3d)
|
|
if can_use_wan_rmsnorm_silu(x, self.gamma, self.bias):
|
|
return wan_rmsnorm_silu(x, self.gamma, self.bias, self.scale, self.eps, self.upcast)
|
|
bias = self.bias if self.bias is not None else self._bias_value
|
|
return wan_rmsnorm_silu_reference(x, self.gamma, bias, self.scale, self.eps, self.upcast)
|
|
|
|
|
|
def _install_fused(decoder: nn.Module, post_quant_conv: nn.Module | None, counts: dict[str, int]) -> None:
|
|
"""Channels_last weights and buffers, fused norm+SiLU, layout-preserving resample. Fails closed."""
|
|
blocks = [m for m in decoder.modules() if m.__class__.__name__ == "WanResidualBlock"]
|
|
for m in blocks:
|
|
for name in ("norm1", "norm2"):
|
|
norm = getattr(m, name, None)
|
|
if norm.__class__.__name__ not in _NORM_CLASSES or not getattr(norm, "channel_first", False):
|
|
raise ValueError(
|
|
f"fused fast path needs a channel-first Wan RMSNorm at {name}; got {type(norm).__name__}"
|
|
)
|
|
if not isinstance(m.nonlinearity, nn.SiLU) or m.nonlinearity.inplace:
|
|
raise ValueError("fused fast path needs a plain SiLU residual block nonlinearity")
|
|
head_norm = getattr(decoder, "norm_out", None)
|
|
if head_norm.__class__.__name__ not in _NORM_CLASSES or not isinstance(
|
|
getattr(decoder, "nonlinearity", None), nn.SiLU
|
|
):
|
|
raise ValueError("fused fast path needs the standard WanRMS_norm + SiLU decoder head")
|
|
for m in blocks:
|
|
m.norm1 = FusedWanRMSNormSiLU(m.norm1)
|
|
m.norm2 = FusedWanRMSNormSiLU(m.norm2)
|
|
m.nonlinearity = nn.Identity()
|
|
counts["fused_norm_silu"] += 2
|
|
decoder.norm_out = FusedWanRMSNormSiLU(decoder.norm_out)
|
|
decoder.nonlinearity = nn.Identity()
|
|
counts["fused_norm_silu"] += 1
|
|
roots = [decoder] + ([post_quant_conv] if post_quant_conv is not None else [])
|
|
for root in roots:
|
|
for module in root.modules():
|
|
if isinstance(module, nn.Conv3d):
|
|
module.weight.data = module.weight.data.contiguous(memory_format=torch.channels_last_3d)
|
|
counts["channels_last_convs"] += 1
|
|
elif isinstance(module, nn.Conv2d):
|
|
module.weight.data = module.weight.data.contiguous(memory_format=torch.channels_last)
|
|
counts["channels_last_convs"] += 1
|
|
if isinstance(module, WanDistCausalConv3d):
|
|
module.input_memory_format = torch.channels_last_3d
|
|
elif isinstance(module, WanDistConv2d):
|
|
module.input_memory_format = torch.channels_last
|
|
if _is_nearest_upsample(module):
|
|
module.forward = MethodType(_upsample_forward_channels_last, module)
|
|
if module.__class__.__name__ == "WanResample" and str(getattr(module, "mode", "")).startswith("upsample"):
|
|
module.forward = MethodType(_resample_forward_channels_last, module)
|
|
counts["resample_forwards"] += 1
|
|
|
|
|
|
def install_wan_decoder_fast_path(vae: Any, *, conv_dtype: torch.dtype | None, level: str = "exact") -> dict[str, int]:
|
|
"""Install the fast path on ``vae``'s decoder (and ``post_quant_conv``); idempotent.
|
|
|
|
``conv_dtype`` must be the dtype the decode runs under (``decode_autocast_dtype``): with ``None`` the
|
|
parameters are left alone, because casting them without autocast would change the convolution's
|
|
arithmetic rather than remove a cast. ``level`` is ``"exact"`` or ``"fused"`` (see the module docstring).
|
|
"""
|
|
if level not in LEVELS:
|
|
raise ValueError(f"level must be one of {LEVELS}; got {level!r}")
|
|
installed = getattr(vae, _INSTALLED_ATTR, None)
|
|
if installed is not None:
|
|
if installed.get("level") != level:
|
|
raise ValueError(f"Wan decoder fast path already installed at level {installed.get('level')!r}")
|
|
return installed
|
|
decoder = getattr(vae, "decoder", None)
|
|
if decoder is None:
|
|
raise ValueError("Wan decoder fast path requires a decoder module.")
|
|
if conv_dtype is not None and conv_dtype not in (torch.float16, torch.bfloat16):
|
|
raise ValueError(f"conv_dtype must be float16, bfloat16 or None; got {conv_dtype!r}")
|
|
roots = [decoder]
|
|
post_quant_conv = getattr(vae, "post_quant_conv", None)
|
|
if isinstance(post_quant_conv, nn.Module):
|
|
roots.append(post_quant_conv)
|
|
counts: dict[str, Any] = {
|
|
"level": level,
|
|
"conv_params_cast": 0,
|
|
"upsamples": 0,
|
|
"persistent_input_buffers": 0,
|
|
"fused_norm_silu": 0,
|
|
"channels_last_convs": 0,
|
|
"resample_forwards": 0,
|
|
}
|
|
for root in roots:
|
|
for module in root.modules():
|
|
if conv_dtype is not None and isinstance(module, (nn.Conv2d, nn.Conv3d)):
|
|
if any(p.dtype != conv_dtype for p in module.parameters(recurse=False)):
|
|
module.to(dtype=conv_dtype)
|
|
counts["conv_params_cast"] += 1
|
|
if _is_nearest_upsample(module) and not getattr(module, "_vllm_omni_fast_path_upsample", False):
|
|
module.forward = MethodType(_upsample_forward, module)
|
|
module._vllm_omni_fast_path_upsample = True
|
|
counts["upsamples"] += 1
|
|
if isinstance(module, (WanDistCausalConv3d, WanDistConv2d)):
|
|
module.reuse_input_buffer = True
|
|
counts["persistent_input_buffers"] += 1
|
|
if level == "fused":
|
|
_install_fused(decoder, post_quant_conv if isinstance(post_quant_conv, nn.Module) else None, counts)
|
|
setattr(vae, _INSTALLED_ATTR, counts)
|
|
logger.info("Installed the %s Wan decoder fast path (conv dtype %s): %s", level, conv_dtype, counts)
|
|
return counts
|